【14日目】音と画像の投稿とCSS【作曲の補助ツールを作るまでの日記】

プログラミング

2023年8月6日~2023年8月9日

13日目の続きをやっていく。

色々やる前にlaravel-adminをアンインストールする。

なんか、githubを確認すると以下のような通知が来ていた。

要約すると「laravel-admin v1.8.19で脆弱性が発見されたから対策しといてや~」というgithubのDependabotからの警告だった。こんなのあるんだね、素晴らしいね。

恐らく、laravel-adminってオフラインのテスト環境でしか使わないものだから、別にこの脆弱性はあまり問題ないと思うんだけど、そもそもlaravel-admin使ってなかったし、この警告めっちゃ気になるので一回アンインストールを試みる。

あと、larave-admin自体全然更新が来ていないというのもある。
更新が来なくなったソフトは終わりが早い。

アンインストールをする

アンインストールをするといっても、アンインストール方を調べても出てこないのでとりあえずChatGPTに助けを求める。

ChatGPT曰く

らしい。

とりあえずやってみよう。

現在のcomposer.jsonを見るとlaravel-adminがあるのを確認できる。

“composer remove encore/laravel-admin”

を使い削除してみる。

おー。なくなってるねぇ。

サービスプロバイダは見た感じ無かったので一旦スルー。

  • config/admin.php
  • public/vendor/laravel-admin
  • マイグレーションファイル (database/migrationsの中でadmin_で始まるものなど)
  • routes/admin.php
  • database/seeds/AdminTablesSeeder.php

は、git rmで消す。

別にテーブルも削除する必要はないと思うけど、消しとく。

drop table テーブル名;で消せるっぽい。

おk、削除完了。

ウェブサイトも通常通り動くので問題なし。

音源を投稿できるようにしたい

音とか画像ファイルはどこに保存するの?

まず最初に、通常音とか画像ファイルはどこに保存するの? という疑問
普通はどこに保存するんだろうか。Cloudなのか、文字列に変換してDBなのか。

調べて見た感じ、普通はクラウドサービスと契約してそこに保存し、そのパスをDBに保存するっぽい。
今回はまだ勉強中だし、無制限にお金かかるのが怖いのでローカルストレージに保存する形にしようと思う。

色々調べてみる。

最初、CKEditorに音を入れるプラグインがあるって話をしたんだけど、あれCKEditor4の話だった。今CKEditorは5で、4との互換性は無いしもうサポートもしてないんだよね。

ここで私が取れる選択肢は以下の通り

  • CKEditor4を無理やり使う
  • 他のリッチテキストエディタを探す
  • 音楽のアップロードを別のところでして、そのファイルパスをCKEditor内で使用してもらう。
  • CKEditor5の音源追加プラグインを自分で作る

うーーーん。

いい機会だし、音源のアップロードを別のところでしてそのURLをCKEditor内で使用してもらう形にしようか。

実用性とか快適性で言えば絶望的だけど、勉強にはなりそう。
というか、Javascriptが使えないせいで出来ない機能が結構ある。Javascriptってフロントエンドなイメージがあるんだけど、バックエンド側の人が学習しても損しなさそう。

とにかくまず、音源をアップロードしてブラウザで使えるようにならないと意味ないね。

音源のアップロード機能を追加してみる

とりあえず、アップロードViewとコントローラ、保存場所を管理するDB、そのバリデーションが必要?

バリデーションは最後でいいか。

とりあえず、
php artisan make:model Audio -m -c -r
でAudioモデル、コントローラ、マイグレーションファイルを作成。

マイグレーションファイルの記述

マイグレーションファイルは以下の通りにした

    public function up()
    {
        Schema::create('audio', function (Blueprint $table) {
            $table->id();
            $table->foreignId("user_id")->constrained()->onDelete("cascade");
            $table->string("filename");
            $table->string("path")->unique();
            $table->timestamps();
        });
    }
    public function down()
    {
        Schema::dropIfExists('audio');
    }

他にも音源の長さ、タグ、説明文、音源の公開状況とかの要素もあるといいかなと思ったんだけど、とりあえずシンプルにいく。あとから追加でマイグレーションすればいいしね。

ルートの記述

ルートは以下の通り

Route::get('/audio/create', [AudioController::class, "create"])->name("audio.create");
Route::post('/audio/store', [AudioController::class, "store"])->name("audio.store");

音源を”作る”わけではないけど、統一する為にアップロードサイトはauido/createで、実際にpostするときはaudio/storeになっている。

コントローラ

createメソッドはただviewの遷移しか書いてない。

storeメソッドは以下の通り

    public function store(Request $request)
    {
        $path = ''; // スコープ対策
        DB::transaction(function () use ($request, &$path) {
            // アップロードとpathの取得
            $file = $request->file("audio");
            $user = \Auth::user();

            // ユーザーが指定した名前をoriginalFilenameに
            $originalFilename = $request->filename;
            // 拡張子を取得し、保存する際の名前を「指定した名前_一意ID.拡張子」にしている。
            $extension = $request->file('audio')->getClientOriginalExtension();
            $filename = $originalFilename . '_' . uniqid() . '.' . $extension;

            $path = $file->storeAs('uploads/audio', $filename, 'public');

            // データベースにパスを保存
            $audio = new Audio();
            $audio->path = $path;
            $audio->filename = $filename;
            $audio->user_id = $user->id;
            $audio -> save();
        });

        // 成功メッセージとURLをセッションに格納
        return redirect('/audio/create')
        ->with('success', '音源のアップロードに成功しました!')
        ->with('audio_url', asset('storage/' . $path));
    }

流石にコントローラに書きすぎ感はある。

DB::transactionはそのままでトランザクション処理をしている。
また、DBにはパス、ユーザーが付けたファイル名、ユーザーidを保存している。

普通に保存する方のファイル名は「ユーザーが付けたファイル名_一意のID.拡張子」という形にしている。
ファイル名を含んでいるのは何かあった時判別をつきやすくする為で、もしかしたら要らないかもしれない。

つまり、「ヨッシーの鳴き声」という名前で音を保存するとpathは「http://qtm.test/storage/uploads/audio/ヨッシーの鳴き声_64d0a234f3da4.mp3」みたいになる。

以下の部分ではセッションを使い、アップロードの成功と生成されたURLを送信している。

        return redirect('/audio/create')
        ->with('success', '音源のアップロードに成功しました!')
        ->with('audio_url', asset('storage/' . $path));

->withを付けることでリダイレクトのレスポンスにセッションを追加できる。

ここでは、
success => “音源のアップロードに成功しました!”
audio_url => asset(‘storage/’ . $path)
が送信されていて、
ビュー側で
$request->session()->get(‘success’)
みたいな感じで取得できる。

また、assetはLaravelのヘルパ関数で、条件からURLを作成してくれる……といっても「http://qtm.test/」を追加しているだけっちゃだけ。

実際に音源のアップロードをしてみる

ミクさんに、(仮)ではあるけどサイト名のQTMと言ってもらったので、それをアップロードしてみる。

アップロードを押すと。

こんな感じでURLが生成されて、そのURLのアクセスすると

こんな感じでよく見る音源の再生ページに遷移して、音を聞ける。

うん、だいぶごり押し感はある。

これを、CKEditorで<auido>タグで指定できるようにしてあげれば、記事内で音源を扱えるようになる……よね?

CKEditorで音源が使えない

音源のアップロード、再生までは出来るようになったけど、それをCKEditor内で扱えないとあまり意味はない。

HTML5で扱うには
<audio controls src=”http://qtm.test/storage/uploads/audio/初音ミク_QTM_64d0acc9b4303.wav” >QTM</audio>
みたいな感じでaudioタグで囲んであげるといける。やってみよう。

以下みたいな感じで投稿する。

投稿を見てみると

うーーん、まあそうだよね。
CKEditor側で超丁寧にサニタイズ処理されて、そのまま表示されている。

恐らく、CKEditorのサニタイズ設定を弄れば表示できるかな?
DBから取り出すときもサニタイズ処理は施しているので、セキュリティ的にも問題ないはず。

今使っているCKEditorは
<script src=”https://cdn.ckeditor.com/ckeditor5/38.1.1/classic/ckeditor.js“></script>
みたいな感じでネット経由で取得しているものなので、とりあえずローカルで入れよう。

いったんCKEditorをローカルに入れる

とりあえず、色々弄るにもローカルに入れる必要があると思うので、ローカルにCKEditorを入れる。

ローカルに入れる方法は色々あるが、今回は.zipファイルをダウンロードしてassets/vendorに入れる方法で行う。理由は、カスタマイズできるのが面白いのと、npmだとgitで共有されなくて、vimで操作することになるから。
参考サイトはCKEditorの公式ドキュメント

エディタのカスタマイズ

まず、エディタをカスタマイズする。

エディタタイプはClassicで。

入れるプラグインというか、要素は以下の通り(後にソースコード機能なども追加)

といっても、正直なところ自分でもよくわかっていない。

注目しているのがGeneral HTML Supportというもので、これを使えば比較的簡単に<audio>を埋め込めるかもしれない。

ツールバーは以下の通り

ぱんっぱんだね!

言語は勿論Japaneseで

そんで、このダウンロードしたzipファイルを入れればええんか?

入れてみる

public\vendorに解凍したckeditorのファイルを入れて。これでいいはず。

そんで、CKEditorのscriptを以下のようにして

<script src=”/assets/vendor/ckeditor5-39.0.0-mrtoyv547is/build/ckeditor.js”></script>

おk!

入れられた!

けどなんか日本語じゃなくない?

今見直したら、これjavaneseだ。なんだjavaneseって。
……どうやらジャワ語らしい。めっちゃ似てるなぁjapaneseとjavanese。

configファイルの言語設定を弄ってみたけど治らなかったので、再ダウンロードしてくる。

いいね、日本語だね。

一応これで投稿できるか、色んな機能が使えるか試してみる。

うーん。コードブロック・フォントサイズ・取り消し線が使えてないのと、audioタグもやっぱこのままだとダメだね。
DTM系サイトでコードブロックが本当に必要かという議論はある。

DBを確認すると、しっかりタグは残っているのでサニタイズされているわけではない。

恐らく、Bootstrapが余計なお世話をしてくれているので、CSSを上書きしないと上手く表示されなさそう。

音を挿入できるようにしたい

ってことで、CKEditorはとりあえずローカルで入れた。

ここで注目したいのがソースコード機能(といっても後から気づいて追加した)。

こんな感じのテキストを

ソースボタンを押すと

こんな感じでソースコードを見れるようになる。

というかあんたh2なんかい。

ここに
<audio controls src=”http://qtm.test/storage/uploads/audio/初音ミク_QTM_64d0acc9b4303.wav” >QTM</audio>
を入れて、本文に戻してみると

という感じで<audio>がサニタイズされているのがわかる。

つまり、auidoのサニタイズさえ取ってしまえば、かなりごり押しだけどオーディオファイルを挿入できるようになるということ。

General HTML Support

更にここで注目したいのが
General HTML Support
というもの。

恐らくこれで<audio>を許可すれば<audio>タグが使えるはず!

ということで、CKEditorをViewで呼び出すときの設定をいじる。

以下のようになった。

    <script>
        ClassicEditor
            .create( document.querySelector( '#editor' ),{
                htmlSupport: {
                    allow: [
                        {
                            name:/^audio$/,
                            attributes: true,
                            classes: true,
                            styles: true
                        }
                    ]
                    // disallow: [ /* HTML features to disallow */ ]
                }
            })
            .catch( error => {
                console.error( error );
            } );
    </script>

これは何をしているかというと、/^audio$/が正規表現でがっちり>>audio<<って一字一句合っているタグだけを許可している。

attributesは属性を許可するかというもので、今回はsrc=でURLを指定するのに属性を使うので許可。

classesはclass=でCSSを適用できるか。
stylesはタグの中で<p style=”color: blue;”>みたいな感じでCSSを適用できるか。

どちらも別に問題ないと思うので許可。

これで、編集してみると…

いけたか!?

ハイダメー

これは、DBからHTMLを取り出すときのHTMLPurifier for Laravelのサニタイズが恐らく弾いちゃってる。

HTMLPurifier for Laravelのサニタイズを調整

詳しくはHTMLPurifier for Laravelの公式リポジトリを見てもらって。

とりあえず、

php artisan vendor:publish --provider="Mews\Purifier\PurifierServiceProvider"


で設定ファイルをコピーする。

コピーするとconfig/purifier.phpが出来ているはずなので、ここをいじる。

その中でも今回弄るのは

    'settings'      => [
        'default' => [
            'HTML.Doctype'             => 'HTML 4.01 Transitional',
            'HTML.Allowed'             => 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src]',
            'CSS.AllowedProperties'    => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
            'AutoFormat.AutoParagraph' => true,
            'AutoFormat.RemoveEmpty'   => true,
        ],

の部分で、HTML.Allowedにaudioを加えればいいのかな?

ここに
audio[src|controls]
を加えることで、audioタグのsrc属性とcontrols属性だけを許可できるみたいだね。

よし、これでいけるか?

だめだねぇ。

これ、たぶんコードに’HTML 4.01 Transitional’って書いてあるからHTML5のタグに対応してないのか。

これも一応調べて見ると対応策はあって、
‘custom_definition’ => [
のとこに

                ['audio', 'Block', 'Flow', 'Common', [
                    'src' => 'URI',
                    'controls' => 'Bool',
                ]],

を入れると、認識してくれる。
ちょっとなんでかは聞かないで。

いけるかな?

いけた…疲れた。。。

よし。一段落。

音源関連のページを作る

とりあえず、音源はかなり無理やりだけど使えるようになったので、音源関連のインフラを整えていく。

作りたいのは

  • 音源の詳細ページと削除
  • 音源の一覧ページ
  • それぞれの認可処理とバリデーション

くらいかな?

音源一覧ページを作る

テーブル形式で、音源・名前・投稿者・URLがあればいいかな?

とりあえず、作った。

自分の投稿した音源だけ削除ボタンが出て、他は削除ボタンは出ないようにした。

ビューとかルータで新しいことはしてないけど、コントローラは少し新しいことしたので、コントローラだけ説明。

    public function delete($id)
    {
        DB::transaction(function () use ($id) {
            $audio = Audio::find($id);

            // ファイルを削除
            Storage::delete('public/' . $audio->path);
            // データベースのレコードを削除
            $audio->delete();
        });

        return redirect("/audios");
    }

Storageのデリートメソッドで指定したファイルを削除している。

これらと、音源投稿ページとかにバリデーションと認可処理を施していく……といっても音源投稿ページにバリデーションと認可処理やってあげれば解決するのでは?

とりあえず、

  • store
  • create
  • delete

メソッドに認可処理を適用。

次は音源投稿ページのバリデーションで

  • mp3.wavのみ
  • 10MBの制限

を適用したい。

やってきた。

midiを入れようとしてみる

おk。とりあえずいけてるっぽい。

最後に、それぞれ記事一覧から音源一覧に行けたりのリンクを整える。

あと、php.iniの設定も変える

ここのupload_max_filesizeとpost_max_sizeを変えておかないと、大きい容量をアップロードできない。
こちらはVM側でvim使って編集。

最後に見た目を整える

恐らく普通はバックエンドとフロントエンドで役割が分かれてて、見た目系はフロントエンドの方がやってくれることになるんだけど、今回は一人なので自分でCSSを弄っていく。

CSSのいじり方は、私の場合恐らく以下の4種類がある

  1. 普通にHTMLの中にCSSを書く
  2. app.cssにCSSを書く
  3. TailwindCSSを使って書く
  4. Bootstrapを使って書く

恐らく、どれを使っても問題ないので、今回はapp.cssとTailwindCSSを使っていく。

CSSは特に解説することはないけど、結構Tailwindを使った。
便利ねこれ。

いいねボタンをせっかく独立させていたので、多様してみたり、h2とかのCSSを変えて見たり、タグの下に座布団しいたり色々やってみた。

正直、これで完成でもいいんだけど、ちょっと朝になるまでに時間があるので画像投稿機能を追加する。

CKEditorで画像投稿

CKEditorには画像をアップロードする機能が標準で搭載されている(是非audioをアップロードする機能も…)。

しかし、当然だがこちらも同じように画像投稿のシステムを組み込んであげないといけない。

そもそも、CKEditor上で何かファイルをアップロードしたいという時は、アップロードアダプターというものを使う必要があるみたい。詳しい解説は公式ドキュメント


このアダプターはクライアント側とサーバー側の橋渡し的な意味合いがあり、今回はSimpleUploadAdapterというものを利用する。

SimpleUploadAdapterの具体的な処理は以下の通り

  1. ユーザーが画像を投稿しようとする
  2. SimpleUploadAdapter(以下、アダプターとする)が画像を取得し、予め設定されたエンドポイント(今回はコントローラのアップロードメソッド)にPOSTリクエストを送る
  3. Laravelがコントローラ内で画像の保存、画像場所のURL生成を行う
  4. Laravelが画像のURLをJSON形式でアダプタに返却
  5. アダプタはそのURLを使用し画像タグを生成し、表示

ここで注意しなきゃいけないのが、POSTリクエストを送信するというところ。このPOSTリクエストにはCSRFトークンが標準で付かないので、付けてやる必要がある。

実際に画像投稿機能を作ってみる

今回はルートもコントローラもCKEditor専用の物を作る。

とりあえず、ルート作成

Route::post('/upload-image', [ImageUploadController::class,"upload"])->name('image.upload');

次にコントローラを作成。

php artisan make:controller ImageUploadController

で作成し、記述。

    public function upload(Request $request)
    {
        $messages = [
            'upload.required' => 'アップロードする画像を指定してください。',
            'upload.image'    => '指定されたファイルが画像ではありません。',
            'upload.mimes'    => '許可されている画像形式はjpeg, png, jpg, gifのみです。',
            'upload.max'      => '画像サイズは4MB以下でなければなりません。',
        ];
        $this->validate($request, [
            'upload' => 'required|image|mimes:jpeg,png,jpg,gif|max:4096',
        ]);

        $file = $request->file('upload');
        $imageName = uniqid();

        $path = $file->storeAs('uploads/ckeditor/images', $imageName, 'public');

        $url = asset('storage/'.$path);

        return response()->json(['url' => $url]);
    }

一応バリデーションとそのメッセージを書いたけど、無くても問題ないかもしれない。

ここまでやったらCKEditorの設定を変える。

    <script>
        ClassicEditor
            .create( document.querySelector( '#editor' ),{
                htmlSupport: {
                    allow: [
                        {
                            name:/^audio$/,
                            attributes: true,
                            classes: true,
                            styles: true
                        }
                    ]
                    // disallow: [ /* HTML features to disallow */ ]
                },
                // 画像投稿の為の設定
                simpleUpload: {
                    uploadUrl: '/upload-image',
                    headers: {
                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
                    }
                }
            })
            .catch( error => {
                console.error( error );
            } );
    </script>

htmlSupportはaudioタグを有効化するための物なので今回は関係なし。


simpleUploadが大事で、uploadUrl: ‘/upload-image’でAPIエンドポイントを指定している。
つまり、/upload-imageというURLにPOSTするとRoute::post(‘/upload-image’, [ImageUploadController::class,”upload”])するようにさっきルート設定した処理が実行される。

‘X-CSRF-TOKEN’: document.querySelector(‘meta[name=”csrf-token”]’).getAttribute(‘content’)
はCSRFトークンをヘッダ―に付ける為のもの。

更に、そのWebページの<head>に

        <meta name=”csrf-token” content=”{{ csrf_token() }}”>

という記述も必要。

画像投稿してみる

よし、投稿できた。

実際に使うとするとこんな感じ

形になった

完成……とは言えないんだけど、一応形にはなったので一旦ここで区切る。

実際に概要は以下の通り

記事一覧と検索機能

記事一覧ページでは、記事の一覧の閲覧・検索・いいねができる。

以下は「DTM」と調べたときの表示。

記事閲覧ページ

記事閲覧ページでは、記事の閲覧ができる。
記事作成者のみ、「編集」ボタンが出現し、編集画面に遷移できる。

勿論、ボタンだけじゃなく、その遷移先にも認可処理が施されているので、本人以外は編集できない……よね?

編集画面

編集画面では記事の編集・削除ができる。
勿論本人しか弄れないし、更新するときのpostも削除のdeleteも認可処理をしている。

また、画像はD&DかCKEditorのメニューから張り付けられて、音源はソースコードに直接入れる必要がある。

記事投稿画面

編集画面とそんな変わらない。新しい記事を投稿できる画面。

この画面はログインしていれば、遷移できるし、postもログインしていればできる。

音源投稿画面

音源は個別で投稿して、そのURLをソースコードに張り付けるというごり押し仕様な為、作成。

こちらも、ログインしていないと遷移・投稿共にできないし、ボタンも現れない。

音源一覧画面

投稿した音源の一覧が見られる画面。

自分の投稿した音源は削除ボタンが出る。ここは誰でも閲覧可能。
一度投稿した音源はここからURLを取得し、使いまわせる。

備考・その他

一応、考えられるところは認可処理をしているつもりで、バリデーションも考えられるところはやっている。
HTMLもDBに入れるとき、出すときでサニタイズをしている。
けど、何か穴はあるかもしれないし、恐らくあるので、セキュリティ関連も精進していきたい。

それと、DTMに関する記事を投稿出来るサイトを目指しているはずなのに、音源の貼り付けがかなり面倒な仕様なので、そこを改善したい。
恐らく改善方法は、CKEditorのプラグインを自作することになるのかなと思う。

終わりに

まだ至らない点はかなりあると思うんだけど、一応動くものは出来た。

コードが汚かったりするところはあると思うし、コントローラがまあまあ肥大化しているので、次は一旦リファクタリングに入るのかなと思う。

それと、改めて指定された本「PHPフレームワークLaravel Webアプリケーション開発 バージョン8.x対応」を読んでみると、少し理解できるようになっていたので、ここら辺も読みつつ進めていきたい。

一応、自分の勉強記録アプリでLaravelとかPHPとかLinuxの環境構築とかを弄ってるときの時間を記録しているんだけど、丁度1ヵ月で142時間だった。
勿論時間が全てではないけど、夏休みに入ったのでこれからは1日+1時間くらいのペースで進めていきたい。

とりあえず、おやすみなさい。