#Laravelのファイルアップロード
Laravelのファイルアップロードでは、CSRF保護のためにCSRF「トークン」をフォームに含めて送信を行う必要があります。CSS,JSフレームワークのUIkitにはアップロードコンポーネントが用意されていますが、そのまま使うとCSRF「トークン」が用意されていないため419エラーが出てしまします。
#環境
- Laravel 6
- UIkit 3
#bladeテンプレート
今回はDrop areaを使います。
<div class="js-upload uk-placeholder uk-text-center">
<span uk-icon="icon: cloud-upload"></span>
<span class="uk-text-middle">Attach binaries by dropping them here or</span>
<div uk-form-custom>
@csrf //CSRFトークン生成のBladeディレクティブを追加
<input type="file" multiple>
<span class="uk-link">selecting one</span>
</div>
</div>
<progress id="js-progressbar" class="uk-progress" value="0" max="100" hidden></progress>
</div>
#Script
JavaScriptはblade内に<script></script>
でおいても別ファイルにして読み込んでもいいです。解説はソースの後!
var bar = document.getElementById('js-progressbar');
UIkit.upload('.js-upload', {
csrf_token: $('meta[name="csrf-token"]').attr('content'), //csrfトークンを取得
name: 'thumbnail', //name指定はここでやります。HTMLのinputには書きません。
url: '', //送信先のurl、Controllerにファイルを渡します。
multiple: true,
beforeSend: function () {
console.log('beforeSend', arguments);
},
beforeAll: function () {
console.log('beforeAll', arguments);
},
load: function () {
console.log('load', arguments);
},
error: function () {
console.log('error', arguments);
},
complete: function () {
console.log('complete', arguments);
},
loadStart: function (e) {
console.log('loadStart', arguments);
bar.removeAttribute('hidden');
bar.max = e.total;
bar.value = e.loaded;
},
progress: function (e) {
console.log('progress', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
loadEnd: function (e) {
console.log('loadEnd', arguments);
bar.max = e.total;
bar.value = e.loaded;
},
completeAll: function () {
console.log('completeAll', arguments);
setTimeout(function () {
bar.setAttribute('hidden', 'hidden');
}, 1000);
alert('アップロードが完了しました');
}
});
UIkitでは各コンポーネントをカスタムできるようにJavaScript compornentが用意されています。
UIkit.upload(element, options);
Uploadはこれの機能がやたらと豊富なんですね。オプションで値を送ってやることで色々とカスタマイズできます。ただ、csrfトークンは送れないんですね...
だから渋々ですがcsrfトークンを送って、受け取れるようにuikit.js
を書き換えちゃいます。とりあえず、options
としてscrf_token
を勝手に作ってcsrfトークンを取得してセットしましょう。
#uikit.js
やたら長いですが、書き足すところはちょっとなのでご容赦を...
//11998行目くらいです。
var upload = {
props: {
allow: String,
clsDragover: String,
concurrent: Number,
maxSize: Number,
method: String,
mime: String,
msgInvalidMime: String,
msgInvalidName: String,
msgInvalidSize: String,
multiple: Boolean,
name: String,
params: Object,
type: String,
url: String
},
data: {
allow: false,
clsDragover: 'uk-dragover',
concurrent: 1,
maxSize: 0,
method: 'POST',
mime: false,
msgInvalidMime: 'Invalid File Type: %s',
msgInvalidName: 'Invalid File Name: %s',
msgInvalidSize: 'Invalid File Size: %s Kilobytes Max',
multiple: false,
name: 'files[]',
params: {},
type: '',
url: '',
abort: noop,
beforeAll: noop,
beforeSend: noop,
complete: noop,
completeAll: noop,
error: noop,
fail: noop,
load: noop,
loadEnd: noop,
loadStart: noop,
progress: noop,
csrf_token: ''//送ったcsrf_tokenを受け取れるようにする。
},
events: {
change: function(e) {
if (!matches(e.target, 'input[type="file"]')) {
return;
}
e.preventDefault();
if (e.target.files) {
this.upload(e.target.files);
}
e.target.value = '';
},
drop: function(e) {
stop(e);
var transfer = e.dataTransfer;
if (!transfer || !transfer.files) {
return;
}
removeClass(this.$el, this.clsDragover);
this.upload(transfer.files);
},
dragenter: function(e) {
stop(e);
},
dragover: function(e) {
stop(e);
addClass(this.$el, this.clsDragover);
},
dragleave: function(e) {
stop(e);
removeClass(this.$el, this.clsDragover);
}
},
methods: {
upload: function(files) {
var this$1 = this;
if (!files.length) {
return;
}
trigger(this.$el, 'upload', [files]);
for (var i = 0; i < files.length; i++) {
if (this.maxSize && this.maxSize * 1000 < files[i].size) {
this.fail(this.msgInvalidSize.replace('%s', this.maxSize));
return;
}
if (this.allow && !match$1(this.allow, files[i].name)) {
this.fail(this.msgInvalidName.replace('%s', this.allow));
return;
}
if (this.mime && !match$1(this.mime, files[i].type)) {
this.fail(this.msgInvalidMime.replace('%s', this.mime));
return;
}
}
if (!this.multiple) {
files = [files[0]];
}
this.beforeAll(this, files);
var chunks = chunk(files, this.concurrent);
var upload = function (files) {
var data = new FormData();
files.forEach(function (file) { return data.append(this$1.name, file); });
for (var key in this$1.params) {
data.append(key, this$1.params[key]);
}
ajax(this$1.url, {
//ここから
headers: {
'X-CSRF-TOKEN': this$1.csrf_token,
}, //ここまで追加
data: data,
method: this$1.method,
responseType: this$1.type,
beforeSend: function (env) {
var xhr = env.xhr;
xhr.upload && on(xhr.upload, 'progress', this$1.progress);
['loadStart', 'load', 'loadEnd', 'abort'].forEach(function (type) { return on(xhr, type.toLowerCase(), this$1[type]); }
);
this$1.beforeSend(env);
}
}).then(
function (xhr) {
this$1.complete(xhr);
if (chunks.length) {
upload(chunks.shift());
} else {
this$1.completeAll(xhr);
}
},
function (e) { return this$1.error(e); }
);
};
upload(chunks.shift());
}
}
};
##ポイント1
まずは、optionsとして送ったcsrf_tokenの受け皿を作るところ。
data: {
allow: false,
// ~~
progress: noop,
csrf_token: ''//送ったcsrf_tokenを受け取れるようにする。
},
##ポイント2
ajaxを使っているので、ajaxに対応したcsrf保護を行います。headersを追加して、X-CSRF-TOKEN
で値を渡しましょう。公式ドキュメント
ajax(this$1.url, {
//ここから
headers: {
'X-CSRF-TOKEN': this$1.csrf_token,
}, //ここまで追加
data: data,
method: this$1.method,
responseType: this$1.type,
beforeSend: function (env) {
// ~~
}
}).then(
// ~~
);
これで、Controllerに値を渡せるようになります。
#おしまい
Laravelを使って趣味でWebアプリを作って1ヶ月半が経ちました。CSSとJSは自分であまり書きたくないのでフレームワーク何使おうかと色々考えて、なんとなくBootstrapはやだなあと思っていたところにUIkitというフレームワークが刺さりました。シンプルですがめちゃくちゃパワフルで楽しく使っています。ただ情報が少ないんですね... 日本語でのUIkit紹介記事はそこそこあるんですが、実際使って困ったことを共有している人はほとんどいない。そもそもユーザーが少ない可能性は大いにありますが、とりあえず需要あるか実験的に記事投稿していこうかと思います。