kintoneで外部APIからPDF帳票をダウンロードする
前提
- 徳丸浩さんの体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践を読んでいる、もしくは同程度の知識があること
- 特に、JavaScriptによるクロスドメイン通信の方法とセキュリティ的な危険性(XSSなど)を理解していること
- Kintone APIのドキュメントをなんとなく見た
やりたいこと
kintone内にあるデータを外部API(※PDF帳票作成用API)に投げて、PDFファイルをダウンロードしたい
想定するユースケース
- 既存システムからkintoneにデータを移行したが、PDF帳票機能(API)が既存システム側にある
- なんらかの事情でkintoneプラグインのプリントクリエイターを使えないため、新規に外部APIを作成する
構成
構成は以下のような感じです。
外部API側
上記の構成の通り、JavaScriptでクロスドメイン通信することになるので、外部API側でクロスドメイン通信を許可してあげる必要があります。
例えば外部API側でapacheを使用している場合には、以下のようにAccess-Control-Allow-OriginでkintoneのURLを指定して許可します。
<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "[kintoneのURL (例 https://hoge123.cybozu.com)]"
Header add Access-Control-Allow-Headers "content-type"
Header add Access-Control-Expose-Headers "Content-Disposition"
Header add Access-Control-Allow-Methods "POST"
</IfModule>
クロスドメイン通信について、詳しくは以下のドキュメントを読んでください。
参照:
HTTP アクセス制御 (CORS)
Ajaxブラウザセキュリティ - 主戦場をDOMに移したXSS
完成コード(kintone側)
以下2つのJavaScriptファイルをkintone上にアップロードしたら完了です。
kintone上に「PDF出力」というボタンが表示されているはずです。
- export_pdf.js : PDFファイルを出力する共通関数
- sample_form_download.js : 実際にkintone上にPDF出力ボタンを設置してPDFファイルダウンロードするプログラム
/*
PDFファイルを出力する関数
(jQueryだとバイナリファイルのダウンロードが上手くいかないので、仕方なくXMLHttpRequestを使用している)
*/
function export_pdf(url, post_data) {
// aタグを作成するのがコツみたい。後のコードでここで作成したaタグをクリックする。
// 参照: http://stackoverflow.com/questions/19327749/javascript-blob-filename-without-link
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
// バイナリファイルをJavaScriptでダウンロードする方法
// 参照: http://www.html5rocks.com/en/tutorials/file/xhr2/
window.URL = window.URL || window.webkitURL;
var xhr = new XMLHttpRequest();
xhr.open('POST', url, true);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.responseType = 'arraybuffer'; // バイナリで読み込むようにレスポンスタイプを指定(重要!)
xhr.onload = function(e) {
if (this.status == 200) { // レスポンスステータスが200(成功)の時だけ実行する
// ファイル名をレスポンスヘッダのContent-Dispositionから取得しているだけ。
// Content-Disposition: attachment; filename=[ファイル名]
// arraybufferで指定しているためか、以下のように頑張って取得するしかないみたい。。。
var filename = "default.pdf"; // 念のためデフォルトのファイル名を設定(※すぐ下で上書きされる)
var disposition = this.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
filename = matches[1].replace(/['"]/g, ''); // ファイル名があったら上書きする
}
}
// PDFファイルをダウンロードする
var response_data = this.response;
var file = new Blob([response_data], {type: 'application/pdf'});
var url = URL.createObjectURL(file);
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url); // 別タブで開いてPDFファイルを表示する
}
};
xhr.send(JSON.stringify(post_data)); // POSTする前に、投げるデータをstringifyして文字列として渡すこと
}
/*
kintone内のデータを外部APIに投げてPDFファイルをダウンロードするプログラム
*/
(function () {
"use strict";
// 詳細画面を表示した際にイベント発火する
// ※別に一覧画面にイベントを設置してもOK。
kintone.events.on('app.record.detail.show', function(event) {
// PDF出力用のボタンを作成
var button = document.createElement('button');
button.id = 'pdf_button'; // IDは何でもいいです
button.innerHTML = 'PDF出力'; // お好きなボタンの表示名
button.onclick = function() { // PDF出力ボタンをクリックするとPDFファイルをダウンロード
// 詳細画面で表示しているレコード情報を取得
// ※ここでデータをごにょごにょして、外部APIに投げる用のデータを作成する感じです
var data = kintone.app.record.get().record;
export_pdf('https://[外部APIのURLもしくはIPアドレス]', data); // PDFファイル出力(※export_pdf.jsの関数を呼ぶ)
};
// PDF出力用ボタンをkintone上のヘッダの箇所に表示する
// ※基本的にヘッダ部分がスペースとして空いているため。
kintone.app.record.getHeaderMenuSpaceElement().appendChild(button);
});
})();
Q&A
Q1. なぜjQueryのajaxじゃなくてXMLHttpRequest使ってるの?
A1. PDFファイルをバイナリとしてダウンロードする場合に、jQueryだとうまくいかないので。。。汗
参照: バイナリファイルをAjaxで取得する際に注意する点
Q2. 外部API側でPDFファイルを作成して、そのファイルパスを渡してダウンロードした方が楽じゃない?
**A2. そっちの方が楽ですけど、外部API側でファイルを作りたくないです。**理由として、
①セキュリティ的にファイルのリンクを公開したくない
②外部API側の容量を圧迫する可能性があるので作成したファイルを消す必要がある(面倒くさい)
③古いファイルのリンクが残っているとkintone側のデータと作成済みのファイルの内容がズレる可能性がある(なんか色々面倒くさい)
ためです。