PHPでBlueskyの埋め込み用HTMLを取得

(1/1)
PHPでツイートの埋め込み用HTMLを取得
Bluesky クライアントの機能として、ホームページやブログに投稿メッセージを埋め込むためのHTML取得機能がある。ところが、Twitter(現・X) と異なり、Bluesky API にこの機能が用意されていない。
そこで今回は、PHPで メッセージ取得用の Bluesky APIを利用し、Bluesky の埋め込み用HTMLを取得するプログラムを作ってみることにする。
(2026年9月26日)取得失敗時のエラー表示,画面表示のXSS対策,TLS証明書の検証を有効化,エラー処理の見直し
(2025年11月21日)PHP8.5対応:curl_close,imagedestroyを実行しないようにした.

目次

サンプル・プログラムの実行例

PHPでBlueskyの埋め込み用HTMLを取得

サンプル・プログラム

圧縮ファイルの内容
embedBlueskyPost.phpサンプル・プログラム本体
.pahooEnvクラウドサービスを利用するためのアカウント情報などを記入する .env ファイル。
使い方は「各種クラウド連携サービス(WebAPI)の登録方法」を参照。include_path が通ったディレクトリに配置すること。
pahooInputData.phpデータ入力に関わる関数群。
使い方は「数値入力とバリデーション」「文字入力とバリデーション」などを参照。include_path が通ったディレクトリに配置すること。
pahooBlueskyAPI.phpBluesky APIに関わるクラス pahooBlueskyAPI。
使い方は「PHPでPHPでBlueskyに投稿する」などを参照。include_path が通ったディレクトリに配置すること。
pahooScraping.phpスクレイピング処理に関わるクラス pahooScraping。
スクレイピング処理に関わるクラスの使い方は「PHPでDOMDocumentを使ってスクレイピング」を参照。include_path が通ったディレクトリに配置すること。
embedBlueskyPost.php 更新履歴
バージョン 更新日 内容
1.6.0 2025/08/14 .pahooEnv導入
1.0.0 2025/01/18 初版
pahooBlueskyAPI.php 更新履歴
バージョン 更新日 内容
2.9.4 2026/09/27 uploadBlob() -- 1Mバイトを超える画像はJPEGにして縮小(limitImageSize追加),OGPサムネイルはOGP用の大きさにする
2.9.3 2026/09/26 getEmbedPosts() -- 投稿へのリンクURLが at:// URI のままになっていた bug-fix
2.9.2 2026/09/26 トークン保存ファイル名を .htpahootoken に変更(Webから読まれないように)
2.9.1 2026/09/26 strcut(), atruri2postURL(), getValidToken(), getMyProfiles() bug-fix,aspectRatioを画像ごとに付ける
2.9.0 2026/09/26 deleteSession() -- refreshJwtでセッションを終了しトークンを消去,サーバ証明書を検証,getEmbedPosts() -- XSS対策,extractMediaURL() -- file:/// 形式を受け付けない
pahooScraping.php 更新履歴
バージョン 更新日 内容
1.2.1 2024/10/31 __construct() 文字化け対策
1.2.0 2024/09/29 getValueFistrXPath() 属性値でない指定に対応
1.1.0 2023/10/15 getValueFistrXPath() 追加
1.0.1 2023/09/29 __construct() bug-fix
1.0.0 2023/09/18 初版
pahooInputData.php 更新履歴
バージョン 更新日 内容
2.0.3 2026/09/26 validEmail(), validNumber(), getValidNumber(), roundFloat(), pahooLoadEnv() bug-fix
2.0.2 2026/09/26 pahooLoadEnv() -- str_starts_with()を使わないようにした(PHP7対応)
2.0.1 2025/08/11 getParam() bug-fix
2.0.0 2025/08/11 pahooLoadEnv() 追加
1.9.0 2025/07/26 getParam() 引数に$trim追加

準備:PHP の https対応

クラウド連携や相手先サイトのデータを読み込むのに https通信を使うため、PHPに OpenSSLモジュールが組み込まれている必要がある。関数  phpinfo  を使って、下図のように表示されればOKだ。
OpenSSL - PHP
そうでない場合は、次の手順に従ってOpenSSLを有効化し、PHPを再起動させる必要がある。

Windowsでは、"php.ini" の下記の行を有効化する。
extension=php_openssl.dll
Linuxでは --with-openssl=/usr オプションを付けて再ビルドする。→OpenSSLインストール手順

また、クラウド連携や相手先サイトとの https通信では、なりすましを防ぐため、相手サーバのSSL/TLS証明書を検証している。
Windows版のPHPには、証明書の検証に使うCA証明書(ルート証明書の一覧)が同梱されていないため、そのままでは https によるデータ取得に失敗する。CA証明書ファイル(cacert.pem)を入手し、"php.ini" に次の2行を追加してから、PHP(Webサーバ)を再起動してほしい。パスは cacert.pem を置いた場所に合わせること。
curl.cainfo = "C:\php\extras\ssl\cacert.pem"
openssl.cafile = "C:\php\extras\ssl\cacert.pem"
Linuxでは、通常はOSのCA証明書が使われるので、この設定は要らない。

これで準備は完了だ。

準備:pahooInputData 関数群

PHPのバージョンや入力データのバリデーションなど、汎用的に使う関数群を収めたファイル "pahooInputData.php" が同梱されているが、include_path が通ったディレクトリに配置してほしい。他のプログラムでも "pahooInputData.php" を利用するが、常に最新のファイルを1つ配置すればよい。

また、各種クラウドサービスに登録したときに取得するアカウント情報、アプリケーションパスワードなどを登録した .pahooEnv ファイルから読み込む関数 pahooLoadEnv を備えている。こちらについては、「各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
.pahooEnv には「名前=値」を1行に1つずつ書く。値は "..." や '...' で囲んでもよい(囲んだ引用符は取り除かれる)。# ではじまる行はコメントで、空行や「=」のない行は読み飛ばす。

解説:pahooBlueskyAPIクラス

Bluesky に投稿したりプログラムで操作するAPIについては、公式リファレンスが詳しい。APIを利用するには、事前に、あなたのアカウントから利用登録を行い、アプリパスワードを取得する必要がある。その手順は「Bluesky API - 各種クラウド連携サービス(WebAPI)の登録方法」をご覧いただきたい。
Bluesky APIを利用するメソッドはクラス "pahooBlueskyAPI.php" に分離している。また、このクラスからクラス "pahooScraping.php" を呼び出すので、2つのクラス・ファイルを include_path の通ったディレクトリに配置すること。

解説:セッション開始

BlueskyAPI を利用するには、まずセッションを開き、アクセストークン accessJwt を取得する。使用するエンドポイントは com.atproto.server.createSession だ。
com.atproto.server.createSession
URL
https://{PDSドメイン}/xrpc/com.atproto.server.createSession
リクエスト・データ(http) header Content-Type "application/json" post identifier ハンドル名 password アプリケーション・パスワード
レスポンス・データ(json) accessJwt アクセストークン refreshJwt リフレッシュトークン handle ハンドル名 did did
アクセストークン accessJwt は、後述するメッセージや画像の投稿で使用する。寿命は1~2時間だ。
アクセストークン refreshJwt は、アクセストークンの再発行や、セッションの終了・破棄に用いることができ、寿命は数十日と長い。リフレッシュトークン refreshJwt をストレージに保存しておき、次回はアクセストークンを再発行するというのが BlueskyAPI の望ましい運用方法と思われるが、リフレッシュトークン refreshJwt だけでアクセストークン accessJwt を再発行できてしまうので、流出するとたいへん危険である。

pahooBlueskyAPI クラスは、取得したトークンを定数 FILENAME_TOKEN で示すファイル(初期値は "./.htpahootoken")に保存し、有効期限内であれば再利用する。期限が切れたら、リフレッシュトークン refreshJwt でアクセストークンを再発行する。Apache は標準設定で、「.ht」ではじまるファイルへのWebからのアクセスを拒否するので、プログラムと同じディレクトリに置いても、トークンをWebから読まれることはない。詳しくは「解説:API認証とセッション - PHPでBlueskyに投稿する」をご覧いただきたい。

pahooBlueskyAPI.php

  15: // スクレイピング処理に関わるクラス:include_pathが通ったディレクトリに配置
  16: require_once('pahooScraping.php');
  17: 
  18: // Bluesky API クラス =======================================================
  19: class pahooBlueskyAPI {
  20:     public $pds;            // PDSドメイン
  21:     public $webapi  ;       // 直前に呼び出したWebAPI URL
  22:     public $errmsg;         // エラーメッセージ
  23:     public $accessJwt;      // accessJwt
  24:     public $refreshJwt;     // refreshJwt
  25: 
  26:     const INTERNAL_ENCODING = 'UTF-8';  // 内部エンコーディング
  27:     const MAX_MESSAGE_LEN = 300;        // 投稿可能なメッセージ文字数
  28:     const URL_LEN = 23;                 // メッセージ中のURL文字数(相当)
  29:     const MAX_IMAGE_WIDTH  = 1700;      // 投稿可能な最大画像幅(ピクセル)
  30:     const MAX_IMAGE_HEIGHT = 1700;      // 投稿可能な最大画像高(ピクセル)
  31:     const MAX_IMAGE_NUMBER = 4;         // 投稿可能な最大画像数
  32:     const MAX_IMAGE_WIDTH_OGP  = 1200;  // OGPの最大画像幅(ピクセル)
  33:     const MAX_IMAGE_HEIGHT_OGP = 630;   // OGPの最大画像高(ピクセル)
  34:     const MAX_BLOB_SIZE = 1000000;      // 投稿に添付できる画像の最大サイズ(バイト)
  35:     const ASPECT_WIDE   = 1.91;         // 画像アスペクト比(横長)
  36:     const ASPECT_SQUARE = 1.0;          // 画像アスペクト比(正方形)
  37:                                         // これより大きいときは自動縮小する
  38:     // トークンを保存するファイル名
  39:     // 秘匿性を保つことができ、かつ、PHPプログラムから読み書き可能であること
  40:     // Apacheは標準設定で「.ht」ではじまるファイルへのアクセスを拒否するため,この名前にしている
  41:     const FILENAME_TOKEN = './.htpahootoken';
  42: 
  43:     // -- 以下のデータは .env ファイルに記述可能
  44:     // Bluesky API アプリパスワード
  45:     // https://bsky.app/
  46:     public $BLUESKY_HANDLE   = '';      // ハンドル名
  47:     public $BLUESKY_PASSWORD = '';      // アプリケーション・パスワード

BlueskyAPI を利用するユーザー定義クラス pahooBlueskyAPIをつくる。
上述の手順で取得したアプリケーション・パスワードをプロパティ変数 $BLUESKY_PASSWORD に、あなたのハンドル名を $BLUESKY_HANDLE に代入する。
投稿可能な最大文字数は定数 MAX_MESSAGE_LEN として用意した。現在の仕様では300文字だ。

pahooBlueskyAPI.php

  49: /**
  50:  * コンストラクタ
  51:  * もしAPIエラーが出る場合には,新規セッションにしてみる.
  52:  * @param   string $pds PDSドメイン
  53:  * @param   bool $newSession 新規セッションにするかどうか(TRUE:新規,デフォルトはFALSE)
  54:  * @return  なし
  55: */
  56: function __construct($pds, $newSession=FALSE) {
  57:     if (isset($_ENV['PAHOO_BLUESKY_HANDLE'])) {
  58:         $this->BLUESKY_HANDLE   = $_ENV['PAHOO_BLUESKY_HANDLE'];
  59:     }
  60:     if (isset($_ENV['PAHOO_BLUESKY_PASSWORD'])) {
  61:         $this->BLUESKY_PASSWORD = $_ENV['PAHOO_BLUESKY_PASSWORD'];
  62:     }
  63: 
  64:     // プロパティを初期化する.
  65:     $this->pds        = $pds;
  66:     $this->webapi     = '';
  67:     $this->errmsg     = '';
  68:     $this->accessJwt  = '';
  69:     $this->refreshJwt = '';
  70: 
  71:     // 新規セッションを開始する.
  72:     if ($newSession) {
  73:         $this->createSession();
  74:     }
  75: }

コンストラクタの引数は PDSドメインで、変数 $pds に保管し、API呼び出し時に参照できるようにした。

pahooBlueskyAPI.php

 384: /**
 385:  * 新規セッションを開始する.
 386:  * @param   なし
 387:  * @return  bool TRUE:成功/FALSE:失敗
 388: */
 389: function createSession() {
 390:     // エラーメッセージ・クリア
 391:     $this->clearerror();
 392: 
 393:     // リクエストURL
 394:     $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.createSession';
 395:     $this->webapi = $requestURL;
 396:     $ch = curl_init($requestURL);
 397:     // cURLを使ったリクエスト
 398:     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
 399:     curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
 400:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
 401:     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);  // サーバ証明書を検証する
 402:     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);     //  〃
 403:     curl_setopt($ch, CURLOPT_POST, TRUE);
 404:     curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
 405:         'identifier' => $this->BLUESKY_HANDLE,
 406:         'password'   => $this->BLUESKY_PASSWORD,
 407:     ]));
 408: 
 409:     // レスポンス処理
 410:     $response = curl_exec($ch);
 411:     $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
 412:     if ($httpStatusCode != 200) {
 413:         $this->seterror('セッションを開始できません');
 414:         return FALSE;
 415:     }
 416:     if (PHP_VERSION_ID < 80500) {
 417:         curl_close($ch);
 418:     }
 419:     $items = json_decode($response, TRUE);
 420: 
 421:     // エラーチェックとリターン
 422:     if (isset($items['accessJwt']) && isset($items['refreshJwt'])) {
 423:         $this->accessJwt = (string)$items['accessJwt'];
 424:         $this->refreshJwt = (string)$items['refreshJwt'];
 425:         // トークンをファイルに保存する
 426:         $contents = $this->accessJwt . "\n" . $this->refreshJwt;
 427:         file_put_contents(self::FILENAME_TOKEN, $contents);
 428:         return TRUE;
 429:     } else if (isset($items['error'])) {
 430:         $this->seterror($items['message']);
 431:         return FALSE;
 432:     } else {
 433:         $this->seterror('セッションを開始できません');
 434:         return FALSE;
 435:     }
 436: }

セッション開始メソッド createSession は、引数はなく、セッション開始に成功したかどうかを戻り値にする。

メソッドの中身は、上述のAPI仕様の通りに作った。
POSTプロトコルとして、これまでのクラウドサービス利用でも使ってきた cURL関数を利用する。
なりすましたサーバにアプリパスワードやトークンを送らないよう、cURLではサーバ証明書を検証するよう設定している(CURLOPT_SSL_VERIFYPEER、CURLOPT_SSL_VERIFYHOST)。証明書の準備については「準備:PHP の https対応」をご覧いただきたい。

解説:セッション終了

アクセストークン accessJwt のセッションを終了するには、エンドポイント com.atproto.server.deleteSession を呼び出す。
com.atproto.server.deleteSession
URL
https://{PDSドメイン}/xrpc/com.atproto.server.deleteSession
リクエスト・データ(json) header Authorization "Bearer {リフレッシュトークン}"
セッションを終了するときは、アクセストークンではなく、リフレッシュトークン refreshJwt を Authorization ヘッダに付けて呼び出す。以後、このセッションのアクセストークン accessJwt とリフレッシュトークン refreshJwt は使用できなくなる。
トークンの盗用を避ける意味で、トークンを保存して再利用する必要がないときは、処理が終わったらセッション終了するようにしよう。

APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。

pahooBlueskyAPI.php

 491: /**
 492:  * セッション終了する.
 493:  * com.atproto.server.deleteSession は refreshJwt で認証する(accessJwtでは終了できない).
 494:  * 終了後はトークン保存ファイルの中身を空にする.
 495:  * @param   なし
 496:  * @return  bool TRUE:成功/FALSE:失敗
 497: */
 498: function deleteSession() {
 499:     $res = TRUE;
 500: 
 501:     // セッションが残っていれば終了する
 502:     if ($this->refreshJwt != '') {
 503:         // リクエストURL
 504:         $requestURL = 'https://' . $this->pds . '/xrpc/com.atproto.server.deleteSession';
 505:         $this->webapi = $requestURL;
 506:         $ch = curl_init($requestURL);
 507:         // cURLを使ったリクエスト
 508:         curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
 509:         curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $this->refreshJwt]);
 510:         curl_setopt($ch, CURLOPT_POST, TRUE);
 511:         curl_setopt($ch, CURLOPT_POSTFIELDS, '');
 512:         curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
 513:         curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);  // サーバ証明書を検証する
 514:         curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);     //  〃
 515: 
 516:         // レスポンス処理
 517:         $response = curl_exec($ch);
 518:         $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
 519:         if (curl_errno($ch)) {
 520:             $this->seterror('セッション終了できません' . curl_error($ch));
 521:             $res = FALSE;
 522:         } else if ($httpStatusCode != 200) {
 523:             $this->seterror('セッション終了できません(http code:' . $httpStatusCode . ')');
 524:             $res = FALSE;
 525:         }
 526:         if (PHP_VERSION_ID < 80500) {
 527:             curl_close($ch);
 528:         }
 529:     }
 530: 
 531:     // トークンを消去する(保存ファイルは中身を空にする)
 532:     $this->accessJwt = '';
 533:     $this->refreshJwt = '';
 534:     if (is_file(self::FILENAME_TOKEN)) {
 535:         @file_put_contents(self::FILENAME_TOKEN, '');
 536:     }
 537: 
 538:     return $res;
 539: }

セッション終了メソッド deleteSession は、引数はなく、セッション終了に成功したかどうかを戻り値にする。
APIを呼び出したあと、プロパティのトークンを消し、トークンを保存したファイルの中身も空にする。

解説:メッセージ情報を取得

メッセージ情報を取得するエンドポイントは app.bsky.feed.getPosts だ。
app.bsky.feed.getPosts
URL
https://{PDSドメイン}/xrpc/app.bsky.feed.getPosts?uris={atURI(複数)}
取得したい複数のメッセージの atURI をGETで渡すことで、一度の呼び出しで複数のメッセージ情報を取得することができる。

APIの戻り値は、httpステータスが200であれば成功、それ以外であればエラー情報が戻る。
レスポンス・データ(json) posts 0 uri メッセージURI(atURI):1つ目 cid メッセージCID author did DID handle ユーザー名 displayName 表示名 avatar アバター画像URL createdAt 登録日時(UTC) record $type "app.bsky.feed.post"(OGP情報) text メッセージ(UTF-8) createdAt 投稿日時(ローカル時刻;ISO 8601形式) embed $type "app.bsky.embed.external" external uri コンテンツURL thumb メディアをアップしたPDSのUR title コンテンツのタイトル description コンテンツの概要 facets 0 index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL 1 index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL replyCount リプライ数 repostCount リポスト数 likeCount いいね数 quoteCount 引用数 1 uri メッセージURI(atURI):2つ目 cid メッセージCID author did DID handle ユーザー名 displayName 表示名 avatar アバター画像URL createdAt 登録日時(UTC) record $type "app.bsky.feed.post"(OGP情報) text メッセージ(UTF-8) createdAt 投稿日時(ローカル時刻;ISO 8601形式) embed $type "app.bsky.embed.external" external uri コンテンツURL thumb メディアをアップしたPDSのUR title コンテンツのタイトル description コンテンツの概要 facets 0 index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL 1 index byteStart リンクの開始位置 byteEnd リンクの終了位置 features $type "app.bsky.richtext.facet#link" uri URL replyCount リプライ数 repostCount リポスト数 likeCount いいね数 quoteCount 引用数

pahooBlueskyAPI.php

1392: /**
1393:  * メッセージURLからメッセージ情報を取得する
1394:  * @param   array $urls メッセージURL(複数)
1395:  * @return  array メッセージ情報 / FALSE:取得失敗
1396: */
1397: function getPosts($urls) {
1398:     $atURIs = [];
1399:     foreach ($urls as $url) {
1400:         // ユーザー名、投稿IDを取得する
1401:         if (preg_match('/\/profile\/([^\/]+)\/post\/([0-9a-zA-Z]+)/ui', $url, $arr) == 0) {
1402:             $this->seterror($url . 'は投稿URLではありません');
1403:             return FALSE;
1404:         }
1405:         if (count($arr) < 3) {
1406:             $this->seterror($url . '投稿URLではありません');
1407:             return FALSE;
1408:         }
1409:         $userName = $arr[1];
1410:         $postID   = $arr[2];
1411: 
1412:         // ユーザーDIDを取得する
1413:         $userDID = $this->getDID($userName);
1414:         if ($userDID == FALSE) {
1415:             $this->seterror($url . 'はユーザーDIDを取得できません');
1416:             return FALSE;
1417:         }
1418: 
1419:         // AT-URIを生成する
1420:         $atURIs[] = 'at://' . $userDID . '/app.bsky.feed.post/' . $postID;
1421:     }
1422: 
1423:     // トークンを取得する.
1424:     $this->getValidToken();
1425: 
1426:     // リクエストURL (認証必要)
1427:     $requestURL = 'https://' . $this->pds . '/xrpc/app.bsky.feed.getPosts';
1428:     $ch = curl_init($requestURL . '?' . http_build_query(['uris' => $atURIs]));
1429:     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
1430:     curl_setopt($ch, CURLOPT_HTTPHEADER, [
1431:         'Content-Type: application/json',
1432:         'Authorization: Bearer ' . $this->accessJwt,
1433:     ]);
1434:     curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
1435:     curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, TRUE);  // サーバ証明書を検証する
1436:     curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);     //  〃
1437: 
1438:     // レスポンス処理
1439:     $response = curl_exec($ch);
1440:     $httpStatusCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
1441:     if (PHP_VERSION_ID < 80500) {
1442:         curl_close($ch);
1443:     }
1444:     $items = json_decode($response, TRUE);
1445:     if ($httpStatusCode != 200) {
1446:         $errmsg = 'メッセージ情報を取得できません(http code:' . $httpStatusCode . ')';
1447:         if (isset($items['message'])) {
1448:             $errmsg .= ';' . $items['message'];
1449:         }
1450:         $this->seterror($errmsg);
1451:         return FALSE;
1452:     }
1453: 
1454:     return $items;
1455: }

メッセージ情報を取得メソッド getPosts は、引数に atURI ではなく BlueskyメッセージのURLを配列で渡す。内部でURLを atURI に変換してから BlueskyAPI に渡すようにしている。

解説:埋め込み用HTMLを取得

pahooBlueskyAPI.php

1466: /**
1467:  * メッセージURLから埋め込みHTMLを取得する
1468:  * @param   array $url メッセージURL
1469:  * @return  string 埋め込みHTML / FALSE:取得失敗
1470: */
1471: function getEmbedPosts($url) {
1472:     $items = $this->getPosts([$url]);
1473:     if ($items == FALSE) {
1474:         return FALSE;
1475:     }
1476:     if (! isset($items['posts'][0])) {
1477:         $this->seterror($url . ' の投稿が見つかりません');
1478:         return FALSE;
1479:     }
1480:     $post = $items['posts'][0];
1481: 
1482:     // 投稿日時を日本語に変換する
1483:     $createdDateTime = isset($post['record']['createdAt']) ? (string)$post['record']['createdAt'] : '';
1484:     if (preg_match('/([0-9]{4})\-([0-9]{2})\-([0-9]{2})T([0-9]{2})\:([0-9]{2})/', $createdDateTime, $arr) > 0) {
1485:         $dt = sprintf('%04d年%d月%d日 %02d:%02d', (int)$arr[1], (int)$arr[2], (int)$arr[3], (int)$arr[4], (int)$arr[5]);
1486:     } else {
1487:         $dt = '';
1488:     }
1489: 
1490:     // 投稿ページのURL:https://bsky.app/profile/{DID}/post/{投稿ID}
1491:     // AT URI(at://{DID}/app.bsky.feed.post/{投稿ID})から組み立てる
1492:     $rawURI = (string)($post['uri'] ?? '');
1493:     if (preg_match('/^at:\/\/(did:[a-z0-9]+:[A-Za-z0-9\.\_\:\%\-]+)\/app\.bsky\.feed\.post\/([A-Za-z0-9\.\_\:\~\-]+)$/', $rawURI, $m) > 0) {
1494:         $postURL = 'https://bsky.app/profile/' . $m[1] . '/post/' . $m[2] . '?ref_src=embed';
1495:     // 組み立てられないときは,引数の投稿URLを使う
1496:     } else if (preg_match('/^https?:\/\//i', (string)$url) > 0) {
1497:         $postURL = (string)$url;
1498:     } else {
1499:         $postURL = '#';
1500:     }
1501: 
1502:     // XSS対策:投稿データをエスケープしてからHTMLに埋め込む
1503:     $postURL  = $this->h($postURL);
1504:     $uri      = $this->h($post['uri'] ?? '');
1505:     $cid      = $this->h($post['cid'] ?? '');
1506:     $text     = $this->h($post['record']['text'] ?? '');
1507:     $dispName = $this->h($post['author']['displayName'] ?? '');
1508:     $did      = $this->h($post['author']['did'] ?? '');
1509:     $handle   = $this->h($post['author']['handle'] ?? '');
1510: 
1511:     // 埋め込みHTML生成(公式の出力に合わせる)
1512:     return <<< EOT
1513: <blockquote class="bluesky-embed" data-bluesky-uri="{$uri}" data-bluesky-cid="{$cid}"><p lang="">{$text}<br><br><a href="{$postURL}">[image or embed]</a></p>&mdash; {$dispName} (<a href="https://bsky.app/profile/{$did}?ref_src=embed">@{$handle}</a>) <a href="{$postURL}">{$dt}</a></blockquote><script async src="https://embed.bsky.app/static/embed.js" charset="utf-8"></script>
1514: 
1515: EOT;
1516: }

メソッド getEmbedPosts は、メッセージ情報URLを1つ渡すと、前述のメソッド getPosts を利用して、戻ってきたメッセージ情報をHTML形式に成形する。整形テンプレートは、Bluesky 公式が出力するものに合わせた。
投稿文、表示名、ハンドル名などは、HTMLに埋め込む前にエスケープする(XSS対策)。投稿へのリンクは、atURI から "https://bsky.app/profile/{ユーザーDID}/post/{ポストID}" の形式に組み立てる。

解説:メイン・プログラム

embedBlueskyPost.php

 242: // メイン・プログラム =======================================================
 243: // オブジェクトを生成する.
 244: $pbs = new pahooBlueskyAPI('bsky.social');
 245: 
 246: // パラメータを取得する.
 247: $errmsg = $html = '';
 248: $url = trim((string)getParam('url', FALSE, DEF_URL));
 249: if ($url != '') {
 250:     validURL($url, $errmsg);
 251: }
 252: 
 253: // 埋め込み用HTMLを取得
 254: if (isButton('exec') && ($url != '') && ($errmsg == '')) {
 255:     $html = $pbs->getEmbedPosts($url);
 256:     // 取得に失敗したらエラー・メッセージを表示する
 257:     if ($html === FALSE) {
 258:         $html = '';
 259:         $errmsg = $pbs->geterror();
 260:     }
 261: }
 262: 
 263: // 表示HTMLを作成する.

メイン・プログラムでは、入力されたURLをユーザー関数 validURL で検査し、[取出し]ボタンが押されたら、前述のメソッド getEmbedPosts で埋め込み用HTMLを取得する。取得に失敗したときは、メソッド geterror で得たエラー・メッセージを赤字で表示する。
画面に表示するURLやエラー・メッセージは、 htmlspecialchars  関数でエスケープしてから出力する(XSS対策)。
なお、公開時(定数 FLAG_RELEASE が TRUE)は、エラー処理ハンドラ myErrorHandler が、警告を含むエラーで処理を止める。ただし、@演算子 で抑止した警告や、 error_reporting  で除外したエラーは無視する。また、公開時はエラーの内容(ファイルのパスなど)を画面に出さないよう、display_errors を無効にしている。

参考サイト

(この項おわり)
header