--> -->

skimemo


Laravel のバックアップ(No.10)


_ 今日のLaravel

これは、2018アドベントカレンダー記事で、Laravel(5.4)を使った開発の中で気づいた事を毎日一つメモしていくものです。


_ 2018/12/5 resourceのroute名の変更方法

開発を進めていくと、routeの記述がどうしても長大になりがちな中で、resource定義は一行で複数の挙動が定義できて、しかもroute名も自動で定義してくれてとても助かります。
しかし、例えば以下のように別々のディレクトリ内で同じ定義名を使いたい場合があります。

  1
  2
Route::resource('user/regist', 'UserHogeController');
 Route::resource('product/regist', 'ProductHogeController'); 

この場合、route名はどちらも「regist.index」等となってしまい、別々に識別することができなくなってしまいます。

この問題の解決方法として、マニュアルでは以下のように再定義できると書かれています。

  1
  2
  3
Route::resource('photo', 'PhotoController', ['names' => [
    'create' => 'photo.build'
]]); 

しかし、アクションがindex,create,store,show,edit,update,destroyと7つもあるのに、いちいち書いていたらroute定義が無駄に長くなってしまいます。 そこで、resource()のソースを確認すると、Illuminate\Routing\ResourceRegistrar::getResourceRouteName()で以下のように作成していました。

  1
  2
  3
  4
  5
  6
  7
  8
if (isset($options['names'])) {
    if (is_string($options['names'])) {
        $name = $options['names'];
    } elseif (isset($options['names'][$method])) {
        return $options['names'][$method];
    }
}
(後略) 

stringも許可しています。なので、以下の通り指定すると7つのアクション全てを書き換えてくれます。

  1
  2
Route::resource('user/regist', 'UserHogeController',['names'=>'user-regist']);
 Route::resource('product/regist', 'ProductHogeController',['names'=>'product-regist']); 

便利(^^)


_ 2018/12/6 apiでセッションを使おうとするとformと両立できない

LaravelではAPIアクセスはapi.phpで定義できますが、ステートレスなアクセスを前提としているため、セッションを使うことができません。*1
参考ページの通りに対応すればセッションが有効になりますが、以下の手順で操作した場合にCSRFエラーになります。

  1. フォームの中でボタンを押すとajaxを使用してAPIアクセス
  2. フォームからpostでsubmit
csrf.png


想像ですが、APIアクセス時にセッション保持しているCSRFトークンが書き換わってしまい、フォームに埋め込まれたトークンと一致しなくなってしまうと考えられます。

私の場合はやむなく無認証にしましたが、どうしても認証したい場合は、アクセストークン方式にする必要があるのではないかと思います。


_ 2018/12/7 php5とphp7での配列を使ったメソッド呼び出しの挙動の違い

以下のコードにおいて、php5とphp7で挙動の違いがありました。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
<?php
 
class test {
 
    public function __construct() {
        echo "test!\n";
        $method[0] = "test2";
        $this->$method[0]();
    }
 
    private function test2() {
        echo "test2\n";
    }
 
}
 
new test();
  • php5(5.6.30)の場合
    >php test.php
    test!
    test2
  • php7(7.1.24)の場合
    >php test.php
    test!
    PHP Notice:  Array to string conversion in F:\test.php on line 8
    PHP Stack trace:
    PHP   1. {main}() F:\test.php:0
    PHP   2. test->__construct() F:\test.php:17
            :
    PHP Fatal error:  Uncaught Error: Function name must be a string in F:\test.php:8
    どうやら変数が配列の場合、php7だと「$this->$method」で一旦区切って解釈するようです。

    解決方法は以下の通り。
    -	$this->$method[0]();
    +	$this->{$method[0]}();
    これでphp5でも7でも動作するコードになりました。

_ 2018/12/8 WYSWYGエディタには要注意

WYSWYGはWhat You See What You Getの略で、直訳すると見たままの物が得られる、という意味です。
そんなの当たり前じゃん、と言っているそこの若者! これが誕生した頃は画期的だったのですよ(Appleが広めた)。

まあそんなことはいいとして、フリーで使えるWYSWYGエディタは各種あります。*2
私が試したのはTrumbowyg。簡単にこんなTEXTAREAが得られます。

trumbowyg1.png


使い方はこんな感じ。

  1
  2
  3
  4
  5
  6
  7
  8
  9
{{Form::textarea('description')}}
 {{Html::style(asset('trumbowyg/dist/ui/trumbowyg.min.css'))}}
 {{Html::script(asset('trumbowyg/dist/trumbowyg.min.js'))}}
 {{Html::script(asset('trumbowyg/dist/langs/ja.min.js'))}}
 <script language="JavaScript">
    $('textarea').trumbowyg({
        lang: 'ja',
    });
 </script> 

(スクショはプラグインを幾つか入れてます)

このエディタ、画面上に得るべき物を表示しているということは、HTMLタグをレンダリングしているということになります。このTrumbowygは生HTMLも書けるので、試しにJavascriptタグを埋めてみます。

trumbowyg2.png


そして生HTMLモードを解除してみると・・・

trumbowyg3.png


うーん、ダメだこりゃ。これではユーザーが自由にこのサーバー上でJavascriptを動かせちゃうということです。
生HTMLは書けないモードにして使う必要がありそうです。
Trumbowygはボタンのカスタマイズができますので、例えばこんな感じです。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
<script language="JavaScript">
    $('textarea').trumbowyg({
        lang: 'ja',
        btns: [
            ['undo', 'redo'], // Only supported in Blink browsers
            ['formatting'],
            ['strong', 'em', 'del'],
            ['superscript', 'subscript'],
            ['link'],
            ['insertImage'],
            ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull'],
            ['unorderedList', 'orderedList'],
            ['horizontalRule'],
            ['removeformat'],
            ['fullscreen'],
        ]
    });
</script> 


ただし、これでもブラウザのJavascriptを無効にして、<script>タグ書いて、再度有効にすればやはり実行できます。うーん悩ましい・・・。

追記: ぬれぎぬでした・・・→#q4ca3ee1


_ 2018/12/9 Laravelのファイルストレージ(S3)を使う

一番ひっかかっのはS3の設定です(^^;)。当初S3の設定が良く分からなかったのですが、こちら*3に非常に詳しく書かれています。感謝!
S3のバケットを作るだけではダメで、S3とは独立した設定でユーザーを作るのが必要でした。AWSの基本?(^^;)
但し、上記の方法はユーザーの認証情報とバケットを結びつけていないため、このユーザーのS3には全てアクセスできてしまうと思われます。実際には当該バケットに限定した権限設定が必要でしょう。

まずはコードの確認でlocalに保存してみます。

  • View
      1
      2
      3
      4
    
    {{Form::open(['files' => true])}}
        {{Form::file('file')}}
        {{Form::submit()}}
    {{Form::close()}} 
    いや、まあこれだけじゃないでしょうけど(笑)、Form::open()に'files' => trueを付けるのを忘れずに。
  • Controller
      1
      2
      3
    
    $file = $request->file('file');
    $filename = $file->getClientOriginalName();
    $path= Storage::disk('local')->putFile('hoge', $file); 
    hogeはサブディレクトリ名です。特に必要がなければ'/'。

すると、storage/app/hogeの下に、ユニークなファイル名でファイルが保存されます。(Oc4GsSGFIe3NkjnUUttt4mnaRtpKklQhv5ZArdMq.txt みたいな感じ)
従って、元のファイル名はgetClientOriginalName()を使って別途保存しておく必要があります。

次に、S3に保存してみます。
これは簡単で、コードを以下のように変えるだけです。(もちろんS3の設定、.envの設定、league/flysystem-aws-s3-v3の導入は済んでいる前提です)

  1
$path= Storage::disk('s3')->putFile('hoge', $file); 


しかし、ここで問題が起きました(ネタ、来た(笑))
以下のようなエラーが発生しました。

curlsslerr.png


要約すると、ローカルの証明書が無いからSSLアクセスができない、ということです。調べるとWindowsのCURLはデフォルトで証明書が無いためSSLアクセスがエラーになるということが分かりました(S3関係ないやん・・・(笑))。
検索するとCURLにオプションを付ける対策などが出てくるのですが、LaravelFWをいじることになるので却下。
こちら*4のページにある方法で解決しました。

  1. http://curl.haxx.se/ca/cacert.pemから証明書をダウンロード。
  2. 適当なディレクトリに保存。
  3. php.iniでcurl.cainfo = "C:\php5.6.37\cacert.pem"のように指定
  4. Apache再起動

実行後、S3のコンソールでファイルが確認できれば成功です。
ちなみに、localからS3に切り替える場合、いちいちコードを修正しなければいけないのでしょうか? もちろんそんなことはありません。Storage::disk()のソースを見ると、

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
public function disk($name = null)
{
   $name = $name ?: $this->getDefaultDriver();
   return $this->disks[$name] = $this->get($name);
}
      :
public function getDefaultDriver()
{
   return $this->app['config']['filesystems.default'];
} 

のようになっています。つまりStorage::disk()と引数無しにしておけば、.envを変更するだけで切り替えられるようになります。(もちろん切り替え時のデータ移行は必要ですけど)


_ 2018/12/10 phpの5と7.1と7.2、MySQLの5.5と5.6

私は複数箇所に開発環境があるのですが、片方は本番と同じphp5.6+MySQL5.7です。もう一方はなるべく新しい環境でも動作することを担保するため、php7+MySQL8にしてみました。そこで分かったことを2つ(3つ?)。

  1. php7の環境でcomposer updateしてはいけない
    updateするとLaravelのライブラリが色々上がります。その環境ではもちろん動くのですが、php5の環境で動かそうとした時に問題が発生します。php7で新たに追加された機能(戻り値の型の宣言など*5)でエラーになるのです。
    従って、composer updateは必ずphp5.6の環境で行う必要があります。
    もし誤ってphp7で上げてしまってもphp5.6環境でupdateすれば戻ります。

  2. count(null)がエラーになる
    php7.1では問題ありませんが、php7.2ではcount(null)がNOTICEを吐くようになりました*6。LaravelではExceptionが発生します。
    厳密に型を考えてコードを書いている人(なんてphpで居るの?)は問題無いと思いますが、私はそこかしこでやってしまっているので、php7.2は断念しました。

  3. MySQL8ではstrictのモードが違う MySQLはバージョンが変わるとデフォルトの設定があれこれ変わることで有名(?)です。
    MySQL8とLaravelの環境では、こんなエラーが出ました。
    strict.png
    簡単に回避するには、database.phpの設定でstrictをfalseにしてやると出なくなります。*7
    でも本当はMySQLの環境を合わせた方が良いでしょうね。*8

_ 2018/12/11 WYSWYGエディタの使い方

12/8にWYSWYGエディタに生HTMLが書けてしまう危険性について書きました。
その後色々調べてみると、どうやら使い方に問題があることが分かりました。エディタのせいにしてしまってごめんなさいm(_ _)m

前回は{{Form::textarea()}}を使っていたのですが、これだとJavascriptを無効にしてもテキスト入力ができてしまい、脆弱性が生まれます。
Javascriptが無効の時は入力も無効になるよう、以下のようにすればOKです。

  1
  2
  3
  4
  5
  6
  7
<div id="description">{!!$contents['description']!!}</div>
                    :
<script language="JavaScript">
    $('#description').trumbowyg({
        lang: 'ja',
    });
</script> 

サンプルもこのようになっているんですが、一部のドキュメント*9はtextareaを使っちゃってるんですよね。これ危険では・・・(^^;)

ちなみにsubmitの時は、divでは値が飛ばないので一工夫必要です。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
<script language="JavaScript">
    function doSubmit(){
        let desc = $('#description').html();
        // 要素追加
        let elm = document.createElement('input');
        // 属性を設定
        elm.setAttribute('type', 'hidden');
        elm.setAttribute('name', 'description');
        elm.setAttribute('value', desc);
        // 要素を追加
        let frm = document.getElementById('formEdit');
        frm.appendChild(elm);
        frm.submit();
    }
</script>
         :
{{Form::submit('更新',['onclick'=>'doSubmit();'])}} 



_ 2018/12/12 WindowsのMultiByteファイル名とエンコード

今日はディレクトリ内にあるファイルを拾ってSQL文にかけるというコマンドライン処理を書いていました。
サンプル的に書くと以下のような感じです。

  1
  2
  3
  4
  5
  6
  7
public function handle() {
    $list = scandir('F:\\');
    foreach($list as $filename) {
        echo $filename."\n";
        DB::table('migration')->where('filename',$filename)->count();
    }
} 

2カ所ある環境のうち、1カ所(環境A)では問題無く動いたのですが、もう一方(環境B)では何故かエラーになりました。どちらもWindows10Proです。

SQLSTATE[HY000]: General error: 1267 Illegal mix of collations (utf8_general_ci,IMPLICIT) and (utf8mb4_unicode_ci,COERCIBLE) for operation '=' (SQL: select count(*) as aggregate from `migration` where `name` = 20181202_instにstatus追加.txt)

DBのcollationはutf8_general_ciです。何故かWindowsのファイル名はutf8mb4_unicode_ciとなっているようです。ちなみにソースはUTF8で書かれています。

環境Aでこのまま動いたのが謎ではあるのですが、普通に考えるとWindowsはSJIS環境ですので、以下の変換を入れてみます。

$filename = mb_convert_encoding($filename,mb_internal_encoding(),(PHP_OS=='WINNT'?'SJIS':'UTF-8'));

すると、エラー無く動作するようになりました*10。(DBへの照会も正しくされていました)

何故同じWindows10でこのような差が出るのかは謎です。思い当たる違いと言えば、環境Aは今年のWindows10クリーンインストールで、環境BはMSDNのWindows10 1151からのアップデートということぐらいですが・・・。なぜ?(笑)

  • 2018/12/14追記
    改修コードは環境Aではやはり文字化けしてしまったのですが、そういえば環境AはPHP7、環境BはPHP5という違いもありました。そもそもphp.iniのencodeの設定も比較してみる必要があります。結論はもう少し先送りに。。

_ 2018/12/13 ValidatorのURLは何を見ているのか?

バリデーションにurlを使っている処理をテストしていて妙な事に気づきました。
https://localhostは通るのに、https://localhost2はバリデーションエラーになるのです。
まさか実在するサーバーをチェックしているのか? と思ってhttps://test.example.jpとか入れてもエラーにはなりません。
では一体何をチェックしているのか?

ソースを見ます。
URLチェックはIlluminate\Validation\Concerns\ValidatesAttributes->validateUrl()で行われています。
この中には以下のような長大な正規表現が書かれており、これでチェックしている事が分かります。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
$pattern = '~^
            ((aaa|aaas|about|acap|acct|acr|adiumxtra|afp|afs|aim|apt|attachment|aw|barion|beshare|bitcoin|blob|bolo|callto|cap|chrome|chrome-extension|cid|coap|coaps|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-playcontainer|dlna-playsingle|dns|dntp|dtn|dvb|ed2k|example|facetime|fax|feed|feedready|file|filesystem|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|ham|hcp|http|https|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris.beep|iris.lwz|iris.xpc|iris.xpcs|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|ms-help|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|msnim|msrp|msrps|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|oid|opaquelocktoken|pack|palm|paparazzi|pkcs11|platform|pop|pres|prospero|proxy|psyc|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|secondlife|service|session|sftp|sgn|shttp|sieve|sip|sips|skype|smb|sms|smtp|snews|snmp|soap.beep|soap.beeps|soldat|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|things|thismessage|tip|tn3270|turn|turns|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|videotex|view-source|wais|webcal|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\.beep|xmlrpc.beeps|xmpp|xri|ymsgr|z39\.50|z39\.50r|z39\.50s))://                                 # protocol
            (([\pL\pN-]+:)?([\pL\pN-]+)@)?          # basic auth
            (
                ([\pL\pN\pS-\.])+(\.?([\pL]|xn\-\-[\pL\pN-]+)+\.?) # a domain name
                    |                                              # or
                \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}                 # an IP address
                    |                                              # or
                \[
                    (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
                \]  # an IPv6 address
            )
            (:[0-9]+)?                              # a port (optional)
            (/?|/\S+|\?\S*|\#\S*)                   # a /, nothing, a / with something, a query or a fragment
        $~ixu';
 
        return preg_match($pattern, $value) > 0; 

2行目はプロトコル(こんなにあるの!?(笑))、3行目はURLの前に付けるBASIC認証、ドメイン部分は5行目です。
完全には読み解けていませんが、簡単に言うと数字で終わるものはNG。但し2センテンス以降に「xn--」の後に続く文字列が数字で終わるのはOKです。*11

○localhost、×localhost2、○local.xn--abc2、×xn--abc2

ちなみにこの「xn--」は国際化ドメイン名を7bit ASCIIで表すときの開始文字列です。

正規のリモートURLで数字で終わるものは確かに無いでしょうが、ホスト名だけ指定する場合はあり得ますので、パターンとしては通してくれても良かったんじゃないかなと思います。
まあ、実用上問題は無いでしょうけど。(イントラで運用するような特殊な環境では問題が出る場合無いとは言えない(笑))


 


LaravelでセッションIDで認証状態をチェックするステートフルなAPIを使う
https://qiita.com/pinekta/items/d10c8374b1a3003cd952


商用でも利用できる、イケてるWYSIWYGエディタ7選 2017年版
https://engineer.blog.lancers.jp/2017/12/wysiwyg_editor_best_7/


超簡単!LaravelでS3を利用する手順
https://qiita.com/tiwu_official/items/ecb115a92ebfebf6a92f


cURL error 60: SSL certificate in Laravel 5.4
https://stackoverflow.com/questions/42094842/curl-error-60-ssl-certificate-in-laravel-5-4


PHP 5.6.x から PHP 7.0.x への移行
http://php.net/manual/ja/migration70.new-features.php#migration70.new-features.return-type-declarations


PHP7.2のcountにハマった話
https://qiita.com/masaki-ogawa/items/1671d110b2286ececd09


Laravel5 MySQL8.0 NO_AUTO_CREATE_USERのSQL_MODEエラー対策
https://qiita.com/ucan-lab/items/2a482a9537dcc5daeb97


Default laravel's strict mode isn't compatible with MySQL 8.0 (NO_AUTO_CREATE_USERS)
https://github.com/laravel/framework/issues/23970


https://alex-d.github.io/Trumbowyg/documentation/#prefix


この変換処理が環境AやLinux環境で正常に動作するかは未確認です


http://php.net/manual/ja/regexp.reference.unicode.php