Laravelの基本はこちらから学べます

Laravel でスレッド掲示板を作成(中級)。8, 9 章 URL をリンクに変換




今回は前回に続けて開発を進めていきましょう。
前章をまだご覧になっていない方は以下から開発を始めましょう。

8 章:スレッド詳細画面

それではこの章では、サクッとスレット詳細画面を作成していきましょう。

ThreadController.php

// 略
  /**
     * The ThreadRepository implementation.
     *
     * @var ThreadRepository
     */
    protected $thread_repository;

    /**
     * Create a new controller instance.
     *
     * @param  ThreadService  $thread_service
     * @return void
     */
    public function __construct(
        ThreadService $thread_service,
        ThreadRepository $thread_repository
    ) {
        $this->middleware('auth')->except('index');
        $this->thread_service = $thread_service;
        $this->thread_repository = $thread_repository;
    }

//  略
    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $thread = $this->thread_repository->findById($id);
        $thread->load('messages.user');
        return view('threads.show', compact('thread'));
    }
油そば

別の章で作成した、findById が再利用できたな。
なるほど、repository の良さが出てきたな。

それでは次に、詳細画面を作成していきましょう。

views\threads\show.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-10">
            @include('layouts.flash-message')
            <h3>{{ $thread->name }}</h3>
        </div>
        <div class="col-md-10 mb-3">
            <a href="{{ route('threads.index') }}" class="btn btn-primary">掲示板に戻る</a>
        </div>
    </div>
    <div class="row justify-content-center">
        <div class="col-md-10 mb-5">
            @foreach ($thread->messages as $message)
            <div class="card mb-2">
                <div class="card-body">
                    <p>{{ $loop->iteration }} {{ $message->user->name }} {{ $message->created_at }}</p>
                    <p class="mb-0">{{ $message->body }}</p>
                </div>
            </div>
            @endforeach
        </div>
    </div>
    <div class="row justify-content-center">
        <div class="col-md-10">
            <div class="card">
                <h5 class="card-header">レスを投稿する</h5>
                <div class="card-body">
                    <form method="POST" action="{{ route('messages.store', $thread->id) }}" class="mb-4">
                        @csrf
                        <div class="form-group">
                            <label for="thread-first-content">内容</label>
                            <textarea name="body" class="form-control" id="thread-first-content" rows="3"
                                required></textarea>
                        </div>
                        <button type="submit" class="btn btn-primary">書き込む</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection

それでは、localhost/threads/1 にアクセスして画面を確認してみましょう。

見た目はひとまずとし、詳細画面を作成することができました。
現状、コメントを投稿すると、localhost/threads にリダイレクトされてしまいます。
なので、詳細画面にリダイレクトできるようにしましょう。

やってみよう

現状、コメントを投稿すると、localhost/threads にリダイレクトされてしまいます。
なので、詳細画面にリダイレクトできるようにしましょう。

それでは解答です。

MessageController.php

// 略
    /**
     * Store a newly created resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function store(MessageRequest $request, int $id)
    {
        try {
            $data = $request->validated();
            $data['user_id'] = Auth::id();
            $this->message_service->createNewMessage($data, $id);
        } catch (Exception $error) {
            return redirect()->route('threads.show', $id)->with('error', 'メッセージの投稿ができませんでした。');
        }

        return redirect()->route('threads.show', $id)->with('success', 'メッセージを投稿しました');
    }

MessageController.php の store メソッドのリダイレクト先を変更しました。これで、メッセージを保存、失敗した際には詳細画面にリダイレクトされるように変更しました。

では次に、index.blade.php から詳細画面に遷移できるようにしていきます。

index.blade.php

// 略
                        <a href="{{ route('threads.show', $thread->id) }}">全部読む</a>
                        <a href="{{ route('threads.show', $thread->id) }}">最新50</a>
                        <a href="{{ route('threads.show', $thread->id) }}">1-100</a>
                        <a href="{{ route('threads.index') }}">リロード</a>
// 略

全部読む、最新50、1-100 とありますが全て同じ遷移先としました。
また、リロードの遷移先は、index としリロードできるようにしております。

9 章:URLを検出し、リンク化する

油そば

コメントに URL を投稿しても、クリックできないな。これは不便だ。

そうですね。せっかく URL を投稿してもらったのにも関わらずリンク化できないのは UX がよくないですね( UX と言うワード使い所間違っていたらご指摘願います )。

では、「PHP URL 正規表現 aタグ」などとググってみましょう。
沢山の記事が出てきますね。ありがたい。

今回はその中の一つ https://wemo.tech/2160 こちらの記事を参考にさせていただきます。

それでは、実装のイメージを立てましょう。

  1. MessageService に 新規メソッドを作る。
    引数に message を受け取り、リンクに変換して message を返す。
  2. index 及び show blade で上記のメソッドを使用。URL に変換できるようになる。

ではサービスにメソッドを追加しましょう。

MessageService.php

// 略

    /**
     * Convert link from message
     *
     * @param string $message
     * @return string $message
     */
    public function convertUrl(string $message)
    {
        $pattern = '/((?:https?|ftp):\/\/[-_.!~*\'()a-zA-Z0-9;\/?:@&=+$,%#]+)/';
        $replace = '<a href="$1" target="_blank">$1</a>';
        $message = preg_replace($pattern, $replace, $message);
        return $message;
    }

こちらは先ほどの記事 https://wemo.tech/2160 を参考にさせていただきました。
preg_replace を用いて検索及び置換を行っています。

preg_replace のドキュメントは以下。
https://www.php.net/manual/ja/function.preg-replace.php

それでは、次にこのメソッドをまずは show.blade.php に組み込みましょう。

油そば

blade ファイルで service クラスのメソッドを呼ぶことはできるのか?
今までだと、controller から 変数として渡していたよな。

Bladeテンプレート 「サービス注入 service injection 」
https://readouble.com/laravel/6.x/ja/blade.html

こちらを用いると、サービスを blade に inject して使うことができるようになります。
では実際にサービス注入(日本語だと変名前)をしていきます。

show.blade.php

@inject('message_service', 'App\Services\MessageService')

// 略
<div class="card-body">
                    <p>{{ $loop->iteration }} {{ $message->user->name }} {{ $message->created_at }}</p>
                    <p class="mb-0">{{ $message_service->convertUrl($message->body) }}</p>
                </div>
// 略

実装ができたので、実際に URL がリンクに変換されるか確認してみましょう。

実際に URL を投稿
油そば

お、。。。できては、ないな。

リンクには変換できているので、サービス注入は成功していますね。
では、なぜこのようにリンクが文字列として表示されてしまっているのでしょうか。

Tip!! Bladeの{{ }}記法はXSS攻撃を防ぐため、自動的にPHPのhtmlspecialchars関数を通されます。

https://readouble.com/laravel/6.x/ja/blade.html
油そば

htmlspecialchars 関数か。
https://www.php.net/manual/ja/function.htmlspecialchars.php
これかな。XSS を防ぐために自動的にエスケープしてくれているのか

そうですね。XSS 皆さんご存知でしょうか。WEB サービスを作成する時には注意しなければならない脆弱性です。
実際に htmlspecialchars がないとどうなってしまうのか試してみましょう。

デフォルトでブレードの{{ }}文はXSS攻撃を防ぐために、PHPのhtmlspecialchars関数を自動的に通されます。しかしデータをエスケープしたくない場合は、以下の構文を使ってください。

https://readouble.com/laravel/6.x/ja/blade.html
油そば

{!! !!} を用いることで、htmlspecialchars関数を自動的に通さずにできそうだな。
これでリンクが表示されるんじゃないか!

では変更していきましょう。

show.blade.php

// 略
<p class="mb-0">{!! $message_service->convertUrl($message->body) !!}</p>
// 略

変更が完了しましたら、確認してみましょう。

クリックできるようになりましたね。
では、XSS の実践をしていきましょう。

レスを投稿で、下記の投稿を書き込んでみましょう。
実際にウイルスを入れているわけではないのでご安心ください。(ハッカーほどの技術はない T です。ご了承ください。)

<script>alert('ウイルスに感染させたよ🥺');</script>
投稿するとこんな画面に….
油そば

おお、大変だ。{!! !!} これを使用すると、<script></script>を埋め込まれると作動してしまうのか。

と言うことで、危険な掲示板ができましたね。

では、どのように実装すれば ①リンクを表示させつつ ② スクリプトはエスケープできるでしょうか。

MessageService.php

// 略

    /**
     * Convert link from message
     *
     * @param string $message
     * @return string $message
     */
    public function convertUrl(string $message)
    {
        $message = e($message);
        $pattern = '/((?:https?|ftp):\/\/[-_.!~*\'()a-zA-Z0-9;\/?:@&=+$,%#]+)/';
        $replace = '<a href="$1" target="_blank">$1</a>';
        $message = preg_replace($pattern, $replace, $message);
        return $message;
    }

Laravel が用意している「ヘルパ」関数 e() を利用しました。

ヘルパ 「 e( ) 」
https://readouble.com/laravel/6.x/ja/helpers.html

このヘルパ関数は、htmlspecialchars 関数を実行しエスケープをしてくれます。

つまり、上記の convertUrl メソットでは

  1. ユーザーから受け取ったメッセージをまずはエスケープ処理(安全に)
  2. エスケープ後の中から URL を検索しリンクに変更

この流れで実装しているため、安全にリンクを表示することが可能になりましたので、実際に確認してみましょう。

script をエスケープできた!!
やってみよう

では、index.blade.php にも サービスインジェクトを行い、URL がリンクで表示できるようにしておきましょう。

では次の章では画像投稿をできるようにしていきましょう。

8, 9 章での変更点は以下からご確認いただけます。
https://github.com/t-aburasoba/thread-board/pull/7/files

もしこの記事をいいねと思ったたり、ご参考になりましたら下記ボタンからサポートしていただけますと、とても励みになります!




コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です