メディア掲載: レバテックフリーランス様のサイトで当ブログが紹介されました

GAS: HTML側にデータを受け渡す方法(スクリプレット)

Google Apps Scriptで、HtmlServiceを使ってWebアプリを作成する場合、GAS側で持っている値をHTML側に受け渡したいことが多々あると思います。

主な受け渡し方法としては、スクリプトレットを使う方法と、google.script.runを使う方法がありますが、この記事では、スクリプトレットを使って受け渡す方法を説明します。

強制出力スクリプトレット<?!= 変数 ?>を使う場合は、不正な値の混入に注意してください。
記事の最後に説明がありますので、気になるようであればご覧ください。

スクリプトレットでの受け渡しの基本

スクリプトレットを使って、Google Apps Script側の値をHTML側に受け渡す方法をいくつか紹介します。

方法1: グローバル変数を受け渡す

Google Apps Script側(コード.gsなど)でグローバル変数として定義した場合は、HTML側のスクリプトレットで直接参照することができます。

const message = 'Hello';  // グローバル変数

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  return template.evaluate();
}
<script>
  console.log(<?= message ?>);  // コンソールに Hello が出力される
</script>

方法2: テンプレートにプッシュして受け渡す

Google Apps Script側(コード.gsなど)でHtmlTemplateオブジェクトのプロパティとして値を代入することで、HTML側のスクリプトレットで値を参照することができます。

function doGet() {
  const message = 'Hello';
  const template = HtmlService.createTemplateFromFile('index');
  template.message = message;
  return template.evaluate();
}
<script>
  console.log(<?= message ?>);  // コンソールに Hello が出力される
</script>

方法3: テンプレートから関数を呼び出して戻り値を受け取る

Google Apps Script側(コード.gsなど)で、受け渡したい値を返す関数を定義し、HtmlTemplate側でその関数を呼び出すことで、値を受け取ることができます。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  return template.evaluate();
}

function getMessage() {
  const message = 'Hello';
  return message;
}
<script>
  console.log(<?= getMessage() ?>);  // コンソールに Hello が出力される
</script>

これ以降の解説はシンプルにしたいため、方法2: テンプレートにプッシュして受け渡すパターンを題材として扱います。

Boolean(論理値)を受け渡す

論理値を受け渡す場合のコードサンプルです。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.bool = true;
  return template.evaluate();
}
<script>
  const bool_A = JSON.parse(<?= bool ?>);  // 例A
  const bool_B = <?!= bool ?>;             // 例B
  console.log(bool_A);
  console.log(bool_B);
</script>

出力されたHTMLは下記のとおりです。

<script>
  const bool_A = JSON.parse('true');  
  const bool_B = true;             
  console.log(bool_A);
  console.log(bool_B);
</script>

コンソールへの出力は下記のとおりでした。

true
true

Number(数値)を受け渡す

数値を受け渡す場合のコードサンプルです。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.num = 123;
  return template.evaluate();
}
<script>
  const num_A = Number(<?= num ?>);      // 例A
  const num_B = JSON.parse(<?= num ?>);  // 例B
  const num_C = <?!= num ?>;             // 例C
  console.log(num_A);
  console.log(num_B);
  console.log(num_C);
</script>

出力されたHTMLは下記のとおりです。

<script>
  const num_A = Number('123');      
  const num_B = JSON.parse('123');  
  const num_C = 123;
  console.log(num_A);
  console.log(num_B);
  console.log(num_C);
</script>

コンソールへの出力は下記のとおりでした。

123
123
123

String(文字列)を受け渡す

文字列を受け渡す場合のコードサンプルです。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.str = 'abc';
  return template.evaluate();
}
<script>
  const str = <?= str ?>;
  console.log(str);
</script>

出力されたHTMLは下記のとおりです。

<script>
  const str = 'abc';
  console.log(str);
</script>

コンソールへの出力は下記のとおりでした。

abc

配列を受け渡す

配列を受け渡す場合のコードサンプルです。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.ary = [1, 'a', [2, 'b']];
  return template.evaluate();
}
<script>
  const ary_A = JSON.parse(<?= JSON.stringify(ary) ?>);     // 例A
  const ary_B = JSON.parse('<?!= JSON.stringify(ary) ?>');  // 例B
  console.log(ary_A);
  console.log(ary_B);
</script>

出力されたHTMLは下記のとおりです。
例Aの方法では、ダブルクオートがエスケープされて\x22になっています。

<script>
  const ary_A = JSON.parse('[1,\x22a\x22,[2,\x22b\x22]]');
  const ary_B = JSON.parse('[1,"a",[2,"b"]]');
  console.log(ary_A);
  console.log(ary_B);
</script>

コンソールへの出力は下記のとおりでした。例Aの方も問題なくparseできているようです。

0: 1
1: "a"
2: Array(2)
  0: 2
  1: "b"

0: 1
1: "a"
2: Array(2)
  0: 2
  1: "b"

オブジェクトを受け渡す

オブジェクトを受け渡す場合のコードサンプルです。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.obj = {
    id: 1,
    name: 'taro',
    props: {
      age: 33,
      hobby: 'guitar'
    }
  };
  return template.evaluate();
}
<script>
  const obj_A = JSON.parse(<?= JSON.stringify(obj) ?>);     // 例A
  const obj_B = JSON.parse('<?!= JSON.stringify(obj) ?>');  // 例B
  console.log(obj_A);
  console.log(obj_B);
</script>

出力されたHTMLは下記のとおりです。
例Aの方法では、ダブルクオートがエスケープされて\x22になっています。

<script>
  const obj_A = JSON.parse('{\x22id\x22:1,\x22name\x22:\x22taro\x22,\x22props\x22:{\x22age\x22:33,\x22hobby\x22:\x22guitar\x22}}');     
  const obj_B = JSON.parse('{"id":1,"name":"taro","props":{"age":33,"hobby":"guitar"}}');
  console.log(obj_A);
  console.log(obj_B);
</script>

コンソールへの出力は下記のとおりでした。例Aの方も問題なくparseできているようです。

{
  "id": 1,
  "name": "taro",
  "props": {
    "age": 33,
    "hobby": "guitar"
  }
}

{
  "id": 1,
  "name": "taro",
  "props": {
    "age": 33,
    "hobby": "guitar"
  }
}

Dateオブジェクト(日付)を受け渡す

Dateオブジェクトを受け渡す場合のコードサンプルです。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.date = new Date();
  return template.evaluate();
}
<script>
  const date_A = new Date(JSON.parse(<?= JSON.stringify(date) ?>));  // 例A
  const date_B = new Date(Number(<?= (date.getTime()) ?>));          // 例B
  const date_C = new Date(<?!= date.getTime() ?>);                   // 例C
  console.log(date_A);
  console.log(date_A.getTime());
  console.log(date_B);
  console.log(date_B.getTime());
  console.log(date_C);
  console.log(date_C.getTime());
</script>

出力されたHTMLは下記のとおりです。
2つ目の方法では、ダブルクオートがエスケープされて\x22になっています。

<script>
  const date_A = new Date(JSON.parse('\x222023-09-26T15:02:26.198Z\x22'));
  const date_B = new Date(Number('1695740546198'));
  const date_C = new Date(1695740546198);
  console.log(date_A);
  console.log(date_A.getTime());
  console.log(date_B);
  console.log(date_B.getTime());
  console.log(date_C);
  console.log(date_C.getTime());
</script>

コンソールへの出力は下記のとおりでした。いずれも同じ結果となります。

Wed Sep 27 2023 00:02:26 GMT+0900 (日本標準時)
1695740546198

Wed Sep 27 2023 00:02:26 GMT+0900 (日本標準時)
1695740546198

Wed Sep 27 2023 00:02:26 GMT+0900 (日本標準時)
1695740546198

クロスサイトスクリプティング(XSS)

上述の例では、出力スクリプトレット<?= 変数 ?>と、強制出力スクリプトレット<?!= 変数 ?>両方の例が紹介されていますが、Google公式の解説にもあるとおり、強制出力スクリプトレットは信用できない値(≒ユーザーが入力した値)を扱う際には使わない方が安全です。

なぜ使わない方が良いかを説明するために、強制出力スクリプトレットを悪用したクロスサイトスクリプティング(XSS)攻撃をされるケースを考えてみました。
すみませんが、私の浅い知識では不自然な例しか思い浮かびませんでした・・・。不備やお気づきの点などありましたら、コメントなどで是非ご指摘ください。

サンプルとして、賛同者を募るサイトを作ってみました。

サンプルサイトの画面はこんな感じ

サイトを開くと、今までに賛同した人の一覧が表示されます。
名前年齢を入力して登録ボタンを押せば、賛同者としてDB(スプレッドシート)に登録されます。

DBはスプレッドシート。氏名と年齢が記録される。

さて、早速、悪意のある値を入力して登録してみます。

名前欄に悪意のある値を入力し、登録した。
"]]'); location.href='https://web-breeze.net'; //

悪意のある値がスプレッドシートに登録されてしまいました。

見るからにまずい

さて、この状態でサイトに再度アクセスすると・・・

別のサイトに飛ばされてしまいました

スクリプトが意図しない形に改変されてしまい、別のサイトに飛ばされてしまいました。
この例では私のブログに飛んでいますが、フィッシングサイトなどに飛ばされると、よろしくないですね。

なお、不正な値を埋め込まれてしまったサイトのソースは、下記のような感じです。

JSON.parse()がうまいこと途中で終わらせられて、その後にlocation.href=…が埋め込まれました。

// 元のコード
const supporters = JSON.parse('<?!= JSON.stringify(supporters) ?>');

// 出力されたコード
const supporters = JSON.parse('[["山田 太郎",33],["田中 花子",28],["佐藤 健太",49],["\"]]'); location.href='https://web-breeze.net';

・・・ということで、強制出力スクリプトレットは危険な場合があることがわかりました。
本来は、データを登録するタイミングでエスケープや不正チェックをするはずなので、こんなザルな事にはならないと思います。
・・・が、特段理由がなければ、出力スクリプトレットの方を使うのが良いかと思います。

最後に、今回の脆弱なサンプルサイトのコードを掲載いたします。
ツッコミどころが多い気もしますので、あくまで参考程度ということで・・・。

function doGet() {
  const template = HtmlService.createTemplateFromFile('index');
  template.supporters = getSupporters();
  return template.evaluate();
}

/**
 * 賛同者をスプレッドシートに追加する
 */
function registration(name, age) {
  SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName('Supporters')
    .appendRow([name, age]);
}

/**
 * 賛同者一覧を二次元配列で取得する
 */
function getSupporters() {
  return SpreadsheetApp
    .getActiveSpreadsheet()
    .getSheetByName('Supporters')
    .getDataRange()
    .getValues().slice(1);
}
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
  </head>
  <body>
    <h2>賛同者一覧</h2>
    <ul id="list"></ul>

    <h2>賛同する</h2>
    <label for="name">名前:</label>
    <input id="name" type="text">
    <label for="age">年齢:</label>
    <input id="age" type="number">
    <button onclick="registration()">登録</button>
      
    <script>
      /**
       * スプレッドシートに賛同者(氏名, 年齢)を追加する
       */
      function registration() {
        const name = document.getElementById('name').value;
        const age = document.getElementById('age').value;
        google.script.run.withSuccessHandler(() => {
          alert('登録しました');
        }).registration(name, age);
      }

      /**
       * 賛同者一覧を表示する
       */
      function createList() {
        // GAS側で作成した配列を受け取る
        const supporters = JSON.parse('<?!= JSON.stringify(supporters) ?>');
        
        // 賛同者一覧を作成
        const ul = document.getElementById('list');
        supporters.forEach((supporter) => {
          const li = document.createElement('li');
          li.innerHTML = `${supporter[0]}(${supporter[1]})`;
          ul.appendChild(li);
        })
      }

      /**
       * 初期処理
       */
      window.addEventListener('DOMContentLoaded', () => {
        createList();
      });
    </script>
  </body>
</html>

---
最後までお読みいただきありがとうございました。*ᴗ ᴗ)⁾⁾
この記事がお役に立ちましたら、筆者への応援をいただけますと大変励みになります。

Google
\    SHARE    /
ichi3270をフォローする
微風 on the web…

コメント

タイトルとURLをコピーしました