`permitted-attachment-types` によるエディタ境界での添付ファイル制御

basecamp/lexxy

<lexxy-editor>data-permitted-attachment-types 属性を宣言するだけで、ファイルドロップ・HTMLインポート・プロンプト起動・プログラム挿入の全経路にわたって添付ファイルのコンテンツタイプを一括制限できるようになりました。ホストアプリがアタッチメントの種別を自前でフィルタリングする必要はなくなります。

背景

Lexxyを利用するホストアプリには「メンションとOGエンベッドは許可しつつ、ファイルアップロードは禁止したい」というフォーム単位の要件が長年存在していました。しかし既存の attachments=false は添付ファイル全体のオン/オフしか制御できず、種別ごとの許可リストはホスト側が独自に実装するほかありませんでした。保存済みのメンションやOGエンベッドを再編集する際も、インポートパスでのフィルタリングが欠落していたため、ラウンドトリップが壊れるバグが発生していました。

このPRはその課題を解消するため、コンテンツタイプの許可リストをエディタ要素自身が保持し、すべての添付エントリポイントでその判定を一元的に参照する設計を採用しています。

技術的な変更

permittedAttachmentTypes / permitsAttachmentContentType の追加

LexicalEditorElementsrc/elements/editor.js)に2つの公開APIが追加され、許可リストのパースと判定が一箇所に集約されました。

get permittedAttachmentTypes() {
  const raw = this.config.get("permittedAttachmentTypes")
  if (raw == null) {
    return null
  } else {
    const tokens = Array.isArray(raw) ? raw : String(raw).split(/\s+/)
    return Object.freeze(tokens.filter(t => t && t !== "false"))
  }
}

permitsAttachmentContentType(contentType) {
  if (!this.supportsAttachments) {
    return false
  } else {
    const list = this.permittedAttachmentTypes
    return list === null || list.includes(contentType)
  }
}

permittedAttachmentTypes は属性が未設定の場合に null を返し、設定されている場合はスペース区切りのトークンリストを凍結配列で返します。permitsAttachmentContentTypesupportsAttachmentsattachments=false のチェック)を先に評価し、その後で許可リストを参照します。これにより attachments=false と空の許可リストが同一の振る舞いを示します。後方互換性のため、レガシーな "false" 文字列トークンをフィルタして空リスト相当に変換する処理も含まれています。

$generateFilteredNodesFromDOM ヘルパー

HTMLインポートパスに新しいフィルタリングレイヤーが追加されました。新設の src/helpers/attachment_filter_helper.js が、@lexical/html$generateNodesFromDOM をラップします。

export function $generateFilteredNodesFromDOM(editorElement, doc) {
  const nodes = $generateNodesFromDOM(editorElement.editor, doc)
  return filterDisallowedAttachmentNodes(nodes, editorElement)
}

export function filterDisallowedAttachmentNodes(nodes, editorElement) {
  return nodes
    .filter(node => !isDisallowedAttachment(node, editorElement))
    .map(node => {
      $descendantsMatching([ node ], descendant => isDisallowedAttachment(descendant, editorElement))
        .forEach(descendant => descendant.remove())
      return node
    })
}

トップレベルノードのフィルタリングに加え、$descendantsMatching で子孫ノードを再帰的に走査することで、テーブルセル内にネストされた添付ファイルも確実に除去されます。contents.jseditor.jsprompt.js の各コールサイトで $generateNodesFromDOM への直接呼び出しがこのヘルパーに置き換えられました。

ファイルドロップ・ペーストのフィルタリング

エディタ初期化時に lexxy:file-accept イベントの内部リスナーが登録されます。MIMEタイプが許可リストに含まれない場合、このリスナーが event.preventDefault() を呼び出してアップロードをキャンセルします。内部リスナーは lexxy:initialize イベントより前にインストールされるため、ホストが initialize ハンドラで独自リスナーを後から登録した場合は「最後に登録したリスナーが最終決定権を持つ」セマンティクスが維持されます。

プロンプト起動の制御

src/elements/prompt.js#addTriggerListener 冒頭に #promptContentTypePermitted ゲートが追加されました。

#addTriggerListener() {
  if (!this.#promptContentTypePermitted) return
  // ...
}

get #promptContentTypePermitted() {
  const el = this.#editorElement
  if (!el.supportsAttachments) {
    return false
  } else {
    const templates = Array.from(this.querySelectorAll("template[type='editor']"))
    const types = templates.length
      ? templates.map(t => t.getAttribute("content-type") || this.#defaultPromptContentType)
      : [ this.#defaultPromptContentType ]
    return types.some(t => el.permitsAttachmentContentType(t))
  }
}

プロンプトが持つすべての <template type="editor">content-type を評価し、いずれか1つでも許可されていればプロンプトが起動します。許可されない場合はトリガーリスナー自体が登録されないため、ポップオーバーがDOMに現れることはありません。

プログラム挿入のフォールバック

lexxy:insert-link イベントの replaceLinkWith / insertBelowLink コールバックは、#createCustomAttachmentNodeWithHtml を経由して添付ノードを生成していました。このメソッドが permitsAttachmentContentType を参照するように変更され、コンテンツタイプが不許可の場合は #createHtmlNodeWith へフォールバックします。フォールバック先も $generateFilteredNodesFromDOM を経由するため、フォールバックHTMLの中に不許可の添付タグが含まれていても除去されます。

Rubyサイドの content-type 伝播

lib/lexxy/rich_text_area_tag.rb に1行の修正が加わり、<action-text-attachment> ノードに content-type 属性が付与されていない場合でも、Attachmentオブジェクトから content_type を補完するようになりました。

node["content-type"] ||= attachment.content_type

これにより tag_helper_test.rb のアサーションが content-type 属性の存在を検証するよう更新されています。

設計判断

単一の述語メソッドを全エントリポイントが参照する設計が採用されました。フィルタリングロジックが permitsAttachmentContentType に一元化されているため、将来エントリポイントが増えた場合も判定ロジックの重複が生じません。

属性のパースは connectedCallback 時に一度だけ行い結果をキャッシュするというLexxy既存の初期化規約に揃えた点も注目に値します。これは「設定変更が必要な場合は要素の再接続で対応する」という設計方針の一貫性を保つためです。動的な許可リスト変更(attributeChangedCallback による再パース)を採用しなかったことで、実行時の状態管理が単純になっています。

attachments=false と空の許可リストが同一の振る舞いをとる設計も意図的です。これにより既存の attachments=false コードパスはそのまま維持しつつ、permitsAttachmentContentType という単一のゲートで両方のケースを統一的に扱えます。

まとめ

本PRは、エディタ要素自身が添付ファイルの許可リストを保持し、インポート・ドロップ・プロンプト・プログラム挿入の4経路すべてで同一の述語を参照することで、ホスト側の実装コストを排除した変更です。permitted-attachment-types を宣言するだけでラウンドトリップを含む全経路が一貫して制御され、既存の attachments=false との後方互換性も維持されています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
bf983a15

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)の3部構成が明確に適用されており、ガイドラインを完全に満たしています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(例: ```javascript:src/elements/editor.js)およびPR番号のリンク記法(例: [PR #1005](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門用語が適切に使用され、過度な説明がなく、対象読者である専門知識を持つエンジニアに適した技術レベルで書かれています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が遵守されています。非常に読みやすい構成です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内で引用されているコードブロックは、提供されたDiff情報と完全に一致しており、ファイルパスも正確です。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「述語メソッド」「ラウンドトリップ」「MIMEタイプ」などの技術用語が、PRの文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

添付ファイルフィルタリングの4つの経路(インポート、ドロップ、プロンプト、プログラム挿入)に関する説明は、PR DescriptionやDiffのコード変更と整合しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(例:「ラウンドトリップが壊れるバグが発生していた」)は、PRのDescriptionやDiffの内容によって裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#1005)や、コード内の関数名・属性名などの固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトル「`permitted-attachment-types` によるエディタ境界での添付ファイル制御」は、PRの主題「Honor permitted-attachment-types across the editor」を的確に反映しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に記載のないバージョンサポート状況やリリース日程などの外部知識は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「長年存在していました」といった時間表現は、PRの「long-standing」という表現と一致しており、時間的な歪曲はありません。