ほたてメモ

日々学んだことをメモメモ

PHP8.2 で DOM操作をする際のエンティティの変換について

PHP7.4 で HTML を DOMDocument を使って変換する処理をしていたが PHP8.2 に上げたところ、 mb_convert_encoding の Deprecated エラーが出るようになった。

PHP Deprecated:  mb_convert_encoding(): Handling HTML entities via mbstring is deprecated; use htmlspecialchars, htmlentities, or mb_encode_numericentity/mb_decode_numericentity 

エラーを解消するのに手間取ったので記録として残しておく。

従来の方法

これまでは以下のような変換をしていた。

<?php

// 1. DOMDocument を使いたいが、単独の & を渡すと warning が出るので事前に &amp; に変換する
$escaped = str_replace('&', '&amp;', $html);

// 2. DOMDocument に utf-8 の文字列を渡すとそのままでは文字化けするので、mb_convert_encoding でエンティティに変換する
$encoded = mb_convert_encoding($escaped, 'HTML-ENTITIES', 'utf-8');

// 3. DOM操作をして変換後の HTML を取得する
$doc = new DOMDocument();
$doc->loadHTML($encoded);
$savedHtml = $doc->saveHTML();

// 4. 変換後のHTMLはエンティティのままなので、再度 mb_convert_encoding で元に戻す
$result = mb_convert_encoding($savedHtml, 'utf-8', 'HTML-ENTITIES');

新しい方法

最終的に以下の形にした。

<?php

// 1.事前に & → &amp; に変換する処理は同じ
$escaped = str_replace('&', '&amp;', $html);

// 2. mb_convert_encoding の代わりに mb_encode_numericentity を使う
$map = [0x80, 0x10ffff, 0, 0x1fffff];  // ascii を除くユニコード文字の範囲
$encoded = mb_encode_numericentity($escaped, $map, 'utf-8');

// 3. DOM操作をして変換後の HTML を取得する
$doc = new DOMDocument();
$doc->loadHTML($encoded);
$savedHtml = $doc->saveHTML();

// 4. html_entity_decode でエンティティを元の文字に戻す
$result = html_entity_decode($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'utf-8');

変換処理について

mb_convert_encoding の代わりに、 mb_encode_numericentityhtml_entity_decode を使用したが、それぞれどのように変換されるか実験した。

例として、元の HTML が以下の形だったとする。

<p>&</p>

<p> タグの中は以下のように変化した。

初期状態 あ & ♥ あ & ♥
1. & に変換 あ &amp; ♥ あ &amp; ♥
2. エンティティに変換 &#12354; &amp; &hearts; &#12354; &amp; &#9829;
3. DOM操作 &#12354; &amp; &hearts; &#12354; &amp; &hearts;
4. デコード あ & ♥ あ & ♥
  1. &&amp; の置き換えは DOMDocument に渡すために必要なので、どちらの方式も同じ
  2. mb_convert_encoding では「♥」が名前付きエンティティ (&hearts;) に変換されていたが、 mb_encode_numericentityでは数値エンティティに置き換わっている点が異なる
  3. DOMDocument にエンティティに変換した文字列を渡すと、名前付きエンティティを持っている数値エンティティは名前付きに変換されるため、新方式ではこのタイミングで &hearts; に置き換わっている
  4. 旧方式 (mb_convert_encoding) では名前付き・数値エンティティのどちらも変換されるので、元の文字列に戻る。
    新方式は 2. で使った mb_encode_numericentity の対になる関数は mb_decode_numericentity だが、数値エンティティのみデコードするため、&amp;&hearts; は残ってしまう。 html_entity_decode であればどちらも変換するので、今回はデコードに html_entity_decode を使う形にした。

Devcontainerで立てたAPIサーバに接続できなくなった問題の調査

VSCode で Dev Container を使用して API サーバを立てていたのだが、とあるタイミングでフロントエンドの開発環境から接続できなくなっていた。

構成は以下の通り。

  • バックエンド (Dev Container) : PHP + Nginx
  • フロントエンド : React + Next.js

ローカルからはポート 8000 番でコンテナ側の Nginx の 80 番に接続できるようにしていた。

つまりローカルからは

http://localhost:8000/

で接続できていた。

現象としては、React から fetch() で接続できなくなっていた。

試したところ、コマンドラインから curl で接続しても取得できないが、なぜか wget だと取得できる。

さらに調べたところ、IPv6 (http://[::1]:8000/) だと接続できるが、IPv4だと接続できないという状態になっていた。

そこで LISTEN しているポートを調べたところ以下のようになっていた。

$ lsof -P -i TCP -s TCP:LISTEN
COMMAND     PID           USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Code\x20H  1461 xxxxxxxxxxxxxx   34u  IPv4 0x31fa87a050a0f887      0t0  TCP localhost:8000 (LISTEN)
com.docke 16144 xxxxxxxxxxxxxx  181u  IPv6 0x31fa87a50aea79df      0t0  TCP *:8000 (LISTEN)

2行目は docker なので想定通りのもの。TYPE が IPv6 になっているが Docker はIPv4 でも接続できるみたい。

参考)

1行目が怪しいのでプロセスの詳細を確認。

$ ps -p 1461 -ww
  PID TTY           TIME CMD
 1461 ??         0:12.68 /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper --type=utility --utility-sub-type=node.mojom.NodeService --lang=ja --service-sandbox-type=none --user-data-dir=/Users/xxxxxxxxxxxxxx/Library/Application Support/Code --standard-schemes=vscode-webview,vscode-file --secure-schemes=vscode-webview,vscode-file --bypasscsp-schemes --cors-schemes=vscode-webview,vscode-file --fetch-schemes=vscode-webview,vscode-file --service-worker-schemes=vscode-webview --streaming-schemes --shared-files --field-trial-handle=1718379636,r,6655308202130409097,12078965550584404602,131072 --disable-features=CalculateNativeWinOcclusion,SpareRendererForSitePerProcess

VSCode の Code Helper が 8000 番を握っているように見える。

実際に、Devcontainer を使用しないで、docker-compose を直接実行したら 8000 番で正常に接続できた。

試しに上記のプロセスを kill してみたところ、接続できるようにはなったが、VSCode が不安定になった。 何度か立ち上げ直しているとまたプロセスが復活したので根本解決にはならない。

リモート設定をいじっても変化がなかったが、最終的に以下の設定で解決した。

ターミナルを表示すると、タブに「ポート」がある。 そこに docker ではない 8000 番の設定があるので右クリック→「ポートの転送を停止する」を選択。

これでプロセスも消えて、API が接続できるようになった。

この設定を自分でした記憶がないのだが、したのかなぁ。。

Amazon EventBridge Scheduler ユニバーサルターゲットの設定

TerraformでEventBridge Schedulerを利用してECSを操作しようと思ったときに困ったのでメモ。

targetのarnについて

ユニバーサルターゲットの場合、targetのarnはService ARNにする必要がある。

ドキュメントにも書いてあったが、最初ECS Serviceのarnを設定していて間違えた。

registry.terraform.io

Service ARNについて

Service ARNの一覧は下記に記載がある。

docs.aws.amazon.com

ECSの場合は

arn:aws:scheduler:::aws-sdk:ecs:[apiAction]

となっている。

apiActionのフォーマットが不明だったが、例えば UpdateService の場合は、updateService と最初を小文字にする必要があった。

今回は

arn:aws:scheduler:::aws-sdk:ecs:updateService

で良かった。

inputに渡すjson

jsonは、最初 aws ecs update-service --generate-cli-skeleton で生成したjsonを加工して設定してみたが、この場合キーの先頭は小文字になる。

{
  "cluster" : xxxx,
  "service" : xxxx,
  "desiredCount" : 0
}

これではダメで、実際には下記のように先頭を大文字にする必要があった。

{
  "Cluster" : xxxx,
  "Service" : xxxx,
  "DesiredCount" : 0
}

on_content_end のエラーについて

Chromeのconsoleに以下のログが出ていた。

on_content_end ==>response to loadPagePattern, href=xxxx, pattern.id=undefined

調べても全く情報が出てこなくて困っていたのだが、ウイルスバスターを入れたときに追加された拡張機能が出していると分かった。 拡張機能をOFFにしたら表示されなくなった。

入っていた拡張機能はこちら。

Macでタイムスタンプと日付を相互変換する

いつも忘れるのでメモ

タイムスタンプ→日付

$ date -r 1666230165
2022年 10月20日 木曜日 10時42分45秒 JST

現在時刻のタイムスタンプ

$ date +%s
1666230333

指定した日時のタイムスタンプ

$ date -j -f '%Y-%m-%d %H:%M:%S' '2022-10-01 12:34:56' +%s
1664595296

AWS Client VPNを使って固定IPでインターネットに接続する

試しで、AWS Client VPNを利用してVPN環境を構築した際の記録。

構成

VPN構成

  • 固定IPでインターネットに接続するために、プライベートサブネットからパブリックサブネットにあるNAT Gateway経由でアクセスできる経路を作成する。

必要なリソース

  • VPC
  • サブネット (Public / Private)
  • Internet Gateway
  • NAT Gateway
  • ルートテーブル
  • セキュリティグループ (クライアント VPN エンドポイント用)
  • ACM
  • クライアント VPN エンドポイント

VPCの作成

  • VPN用にVPCを作成する。
  • 今回は、IPv4 CIDR を 172.19.0.0/16 とした。

サブネット

  • Public / Privateそれぞれのサブネットを作成する
  • 各設定は下記のようにした
    • Public
      • CIDR : 172.19.0.0/21
      • パブリック IPv4 アドレスを自動割り当ては「はい」
      • ネットワークACLはデフォルト(全てのトラフィックを通す)
    • Private
      • CIDR : 172.19.16.0/21
      • パブリック IPv4 アドレスを自動割り当ては「いいえ」
      • ネットワークACLはデフォルト(全てのトラフィックを通す)

Internet Gateway

  • インターネットに接続するために Internet Gateway を作成する
  • 上記で作成したVPC上に作成する。

Nat Gateway

  • Privateサブネットからインターネットに接続するために NAT Gatewayを作成する。
  • 各設定
    • サブネット : Publicサブネット
    • 接続タイプ : Public

ルートテーブル

  • こちらも、Public / Private用にそれぞれ作成する。
  • 作成後、サブネットの関連付けで、各サブネットと紐付ける
  • また、ルートを下記のように設定する
Public

Publicのルートテーブルには、全てのトラフィックのターゲットをInternet Gawateyにする。

Private

Privateは全てのトラフィックのターゲットをNAT Gatewayにする。


セキュリティグループ

  • クライアント VPN エンドポイント用のセキュリティグループを作成する。
  • インバウンドルールはなし(空)
  • アウトバウンドルールは全てのトラフィックを許可する。

ACM

  • 今回は相互認証を利用する。
  • クライアント VPN エンドポイントを作成する際に、サーバー証明書が必要なので、先に作成してACMに登録しておく。
証明書の作成

証明書の作成は、公式記載の方法に従った。

docs.aws.amazon.com

  • 作成した証明書をACMのインポートから登録を行う。

  • 上から順に対応するファイルは下記の通り。

    • 証明書本文: server.crt
    • 証明書のプライベートキー: server.key
    • 証明書チェーン; ca.crt
  • 証明書やプライベートキーは BEGINEND の内容を貼り付ける。

クライアント VPN エンドポイント

  • これまで作成したリソースを元にクライアント VPN エンドポイントを作成する。
  • 設定
  • 作成後、「ターゲットネットワークの関連付け」タブからPrivateサブネットを紐付ける
  • また、「セキュリティグループ」で事前に作成しておいたセキュリティグループを設定する
  • さらに、「承認ルール」のタブで、全ての送信先(0.0.0.0/0)を追加する

接続

ここまでの設定で、一度VPNに接続する。

クライアントソフトのダウンロード

  • 以下から、AWS Client VPN for Desktop (AWS VPN Client)をダウンロード&インストールする。

aws.amazon.com

  • また、AWSコンソールのクライアント VPN エンドポイントの画面から、「クライアント設定をダウンロード」のボタンでovpnファイルをダウンロードする。

プロファイルの設定

  • VPN Clientを起動し、プロファイルの追加でダウンロードしたovpnファイルを指定する。
  • ただ、そのままだと下記のエラーが表示される。

  • エラーを解消するためには、ovpnファイルにクライアント証明書と鍵を追加する

  • 変更前

...
-----END CERTIFICATE-----

</ca>


reneg-sec 0
  • 変更後
...
-----END CERTIFICATE-----

</ca>

<cert>
-----BEGIN CERTIFICATE-----
xxxx
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN PRIVATE KEY-----
xxxx
-----END PRIVATE KEY-----
</key>

reneg-sec 0

証明書と鍵を公式の手順( クライアント認証 - AWS クライアント VPN )で作成していた場合は、<cert />client1.domain.tld.crt<key />client1.domain.tld.keyを設定する形になる。

  • ovpn更新後再度プロファイル設定を行うと、VPNに接続できる。

FirebaseUIを日本語化する

Nuxt.jsで使っているfirebaseを8→9に上げる際に、合わせてfirebaseuiのバージョンも最新にした。 これまでFIrebaseUIの日本語化に firebaseui-ja - npm を使用していたが、3年以上メンテナンスされていなかったので、オフィシャルな方式に変更した。

開発環境

開発環境はM1 Macでdocker化した環境

対応方法

公式を見ると、自前でビルドする方法が載っているのでこちらを参考にした。 https://www.npmjs.com/package/firebaseui-ja

github.com

ざっくり手順を書くと

  1. git clone https://github.com/firebase/firebaseui-web.git
  2. npm install
  3. npm run build build-npm-ja
  4. import firebaseui from 'firebaseui/dist/npm__ja' で利用する

失敗パターン

最初にうまくいかなかった方法のまとめ。

今回ビルドが必要なので、dockerのマルチステージビルドでファイルを生成してから本体にコピーする形にした。

ベースイメージは、node:16.14-alpine

FROM node:16.14-alpine AS builder

WORKDIR /var/builder
RUN apk add \
  git \
  bash \
  openjdk11-jre-headless
RUN git clone https://github.com/firebase/firebaseui-web.git
WORKDIR /var/builder/firebaseui-web
RUN git checkout refs/tags/v6.0.1
RUN npm install -g npm@8.5.5
RUN npm install
RUN npm run build build-npm-ja

FROM node:16.14-alpine
# ...

COPY --from=builder /var/builder/firebaseui-web/dist/npm__ja.js $BASE_PATH/node_modules/firebaseui/dist/npm__ja.js

# ...

試した結果

まず、alpineでgitが入っていないので追加。

apk add git

特定バージョンをインストールするためにcheckout後にタグを指定する。

git checkout refs/tags/v6.0.1

npm install をすると、package.jsonに書かれている

"prepublish": "npm run test && cp -r dist demo/public"

も動くため、npm run testが実行される。

ちなみに、npm run testの定義は

"test": "npm run build && npm run generate-test-files && ./buildtools/run_tests.sh"

このまま、npm run testが実行されるとエラーになる。エラーになってもnpm run buildだけなら動くので最終的に生成したいファイルは作られるが、自動化する場合は途中でエラーになると困る。

エラーの解消を試みる

[Closure Templates Error] Java (JRE) is needed!

Javaが必要なので入れる。

apk add openjdk11-jre-headless

Javaを入れると先に進むが、Sassのdeprecateエラーがたくさん出る。エラーではないのでいったんスルー。 今度は

sh: ./buildtools/generate_test_files.sh: not found

というエラーが出る。

ファイルはあるので、最初分からなかったが、generate_test_files.shの中身が、#!/bin/bashなので、bashも追加が必要。

apk add bash

再度、npm run testを実行。

python: command not found
google-chrome: command not found

pythongoogle-chromeも必要。。 pythonは入れられるが、google-choromeはarm64のパッケージがないので入れられない。

これ以上がんばってビルドを通してもビルド時間が長くなりすぎそうだったので方向転換。

成功パターン

最終的に実行したいのは

npm run build build-npm-{LANGUAGE_CODE}

なので、

npm install後の、prepublishをスキップすることにした。

一時的なスキップは下記でできる。

npm set-script prepublish ""

最終的な Dockerfile はこちら

FROM node:16.14-alpine AS builder

WORKDIR /var/builder
RUN apk add \
  git \
  openjdk11-jre-headless
RUN git clone https://github.com/firebase/firebaseui-web.git
WORKDIR /var/builder/firebaseui-web
RUN git checkout refs/tags/v6.0.1
RUN npm install -g npm@8.5.5
RUN npm set-script prepublish ""
RUN npm install
RUN npm run build build-npm-ja


FROM node:16.14-alpine
# ...

COPY --from=builder /var/builder/firebaseui-web/dist/npm__ja.js $BASE_PATH/node_modules/firebaseui/dist/npm__ja.js

# ...

テストを通さないはどうかというのはあるが、firebaseui自体では通っているものなので今回はこの形とした。