0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターの拡張HTMLの仕組みを詳しくみてみる

0
Posted at

はじめに

プリザンターにはHTMLを任意の位置に挿入できる仕組みが複数あります。その中でも「拡張HTML」と総称される機能は、グローバルに効かせるものからサイト単位・項目単位のものまで、実はいくつかの異なるレイヤーで成り立っています。

この記事では、拡張HTMLが内部でどのように処理されているかをソースコードベースで解説します。それぞれの仕組みがHTMLのどこに挿入されるか、どう設定するか、そして運用上の注意点まで詳しくみてみます。

バージョン 1.5.2.0 を対象にしています

拡張HTMLの3つのレイヤー

プリザンターの「拡張HTML」は、以下の3つの異なるレイヤーで構成されています。

レイヤー 設定方法 適用範囲
グローバル拡張HTML ファイル配置またはExtensionsテーブル システム全体(条件指定可)
サイト設定HTML 管理画面(スクリプト設定の隣にある「HTML」タブ) サイト単位
項目拡張HTML 管理画面(エディタ設定 → 各項目の詳細設定) 編集画面の各項目周辺

それぞれの仕組みと挿入位置を順番にみてみます。


グローバル拡張HTML

概要

グローバル拡張HTMLは、ファイルまたはDBのExtensionsテーブルを使って設定するシステム全体向けのHTMLです。挿入先はページのHTML構造における固定のプレースホルダー(HtmlHeaderTop・HtmlHeaderBottom・HtmlBodyTop・HtmlBodyBottom)によって決まります。

設定方法

ファイルを配置する方法

App_Data/Parameters/ExtendedHtmls/ ディレクトリに .html ファイルを配置します。ファイル名(拡張子なし)が挿入先のプレースホルダーIDになります。

App_Data/
└── Parameters/
    └── ExtendedHtmls/
        ├── HtmlHeaderTop.html       ← <head> の最上部に挿入
        ├── HtmlHeaderBottom.html    ← <head> の最下部に挿入
        ├── HtmlBodyTop.html         ← <body> の最上部に挿入
        └── HtmlBodyBottom.html      ← <body> の最下部に挿入

多言語対応が必要な場合はファイル名に言語コードを _ でつなげます。

ExtendedHtmls/
├── HtmlBodyTop_ja.html    ← 日本語のときに使われる
└── HtmlBodyTop_en.html    ← 英語のときに使われる

ファイルの読み込みはアプリ起動時のみ行われます。ファイルを追加・変更した場合は、プリザンターのプロセスを再起動する必要があります。

Extensionsテーブルを使う方法

DBのExtensionsテーブルでは、ExtensionType に "Html" を指定し、ExtensionName にプレースホルダーID(例: "HtmlBodyTop")、Body にHTML本文を記述します。

ExtensionSettings の Language フィールドで言語を指定することもできます。

ExtensionSettings の例
{
  "Language": "ja",
  "SiteIdList": [123]
}

読み込みの仕組み

アプリ起動時に Initializer が ExtendedHtmls/ ディレクトリを走査し、全 .html ファイルをメモリに読み込みます。

Implem.DefinitionAccessor/Initializer.cs(抜粋)
foreach (var file in new DirectoryInfo(path).GetFiles("*.html"))
{
    var extendedHtml = Files.Read(file.FullName);
    if (!extendedHtml.IsNullOrEmpty())
    {
        var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file.Name);
        var displayElement = new DisplayElement
        {
            // アンダースコア区切りの最後の部分が言語コード
            Language = fileNameWithoutExtension?.Split('_').Skip(1).LastOrDefault(),
            Body = extendedHtml
        };
        // アンダースコアより前の部分がプレースホルダーID
        var name = displayElement.Language.IsNullOrEmpty()
            ? fileNameWithoutExtension
            : fileNameWithoutExtension?.Substring(
                0,
                fileNameWithoutExtension.Length
                    - displayElement.Language.Length - 1);
        // name(プレースホルダーID)をキーにしてDictionaryに格納
        listDisplay.AddIfNotContainsKey(key: name, ...).Get(name).Add(displayElement);
        list.Add(new ExtendedHtml() { Html = listDisplay });
    }
}

HTMLへの挿入位置

グローバル拡張HTMLは、ページレンダリング時に4か所のプレースホルダーに展開されます。

Implem.Pleasanter/Libraries/HtmlParts/HtmlTemplates.cs(抜粋)
// <head> の構造
hb.Head(action: () => hb
    .Raw(HtmlHtmls.ExtendedHtmls(context, id: "HtmlHeaderTop"))   // ← head最上部
    .Htmls(...)  // サイト設定のHeadTop
    .Meta(...)
    .LinkedStyles(...)
    .ExtendedStyles(...)
    .Title(...)
    .Htmls(...)  // サイト設定のHeadBottom
    .Raw(HtmlHtmls.ExtendedHtmls(context, id: "HtmlHeaderBottom")) // ← head最下部
);

// <body> の構造
hb.Body(action: () =>
    hb.Raw(HtmlHtmls.ExtendedHtmls(context, id: "HtmlBodyTop"))   // ← body最上部
      .MainContainer(...)  // メインコンテンツ
      .Styles(...)         // 管理画面のスタイル
      .Htmls(...)          // サイト設定のBodyScriptTop
      .Scripts(...)        // スクリプト類
      .Htmls(...)          // サイト設定のBodyScriptBottom
      .Raw(HtmlHtmls.ExtendedHtmls(context, id: "HtmlBodyBottom")) // ← body最下部
);

各プレースホルダーの位置をまとめると次のとおりです。

プレースホルダーID 挿入先 詳細な位置
HtmlHeaderTop <head> 最上部(Meta タグなどより前)
HtmlHeaderBottom <head> 最下部(</head> 直前)
HtmlBodyTop <body> 最上部(メインコンテンツより前)
HtmlBodyBottom <body> 最下部(</body> 直前、スクリプトより後)

サイト設定HTML(管理画面のHTML)

概要

サイト設定HTMLは、管理画面の「スクリプト」「スタイル」と同じ操作感で設定できるサイト単位のHTMLです。適用する画面(新規・編集・一覧など)と挿入位置を選択できます。

設定方法

管理画面 → 管理 → スクリプト設定(またはスタイル設定)と並んで「HTML設定」から設定します。各エントリに「位置タイプ」を指定できます。

挿入位置(位置タイプ)

Html.PositionTypes 列挙型で定義された4つの位置から選択します。

位置タイプ 値 挿入先
HeadTop 1000 <head> の上部(LinkedStyles・ExtendedStylesより前)
HeadBottom 1010 <head> の下部(ExtendedStyles・Titleの後)
BodyScriptTop 9000 <body> 内、スクリプト群の前
BodyScriptBottom 9010 <body> 内、スクリプト群の後(グローバル拡張HTMLの HtmlBodyBottom より前)

グローバル拡張HTMLとサイト設定HTMLの挿入順序を整理すると次のとおりです。

<head>
  [HtmlHeaderTop]          ← グローバル拡張HTML
  [HeadTop]                ← サイト設定HTML
  <meta ...>
  <link rel="stylesheet" ...>  ← 標準CSS, 拡張スタイル
  <title>
  [HeadBottom]             ← サイト設定HTML
  [HtmlHeaderBottom]       ← グローバル拡張HTML
</head>
<body>
  [HtmlBodyTop]            ← グローバル拡張HTML
  <!-- メインコンテンツ -->
  <style>...</style>       ← 管理画面のスタイル
  [BodyScriptTop]          ← サイト設定HTML
  <script src="...">       ← jQuery等プラグイン
  <script src="/resources/scripts?...">  ← 拡張スクリプト
  <script>...</script>     ← 管理画面のスクリプト
  [BodyScriptBottom]       ← サイト設定HTML
  [HtmlBodyBottom]         ← グローバル拡張HTML
</body>

フィルタリング(適用画面の選択)

サイト設定HTMLでは、スクリプト・スタイルと同様に適用する画面の種類を選択できます。

SiteSettings.cs(抜粋)
public string GetHtmlBody(
    Context context,
    Func<Html, bool> peredicate,
    Html.PositionTypes positionType)
{
    return !IsSiteEditor(context: context)
        ? Htmls
            ?.Where(html => html.Disabled != true
                && html.PositionType == positionType
                && peredicate(html))
            .Select(o => o.Body).Join("\n")
        : null;
}

peredicate には o => o.All == true・o => o.New == true・o => o.Edit == true などが渡され、設定したチェックボックスに応じて適用が制御されます。

サイトを管理画面で編集中(サイトエディタを開いている状態)は、サイト設定HTMLが適用されません(IsSiteEditor が true のとき null を返す)。管理画面の操作性を守るための制御です。


項目拡張HTML

概要

項目拡張HTMLは、編集画面の各項目(フィールド)の周辺に挿入できるHTMLです。管理画面のエディタ設定から各項目ごとに設定でき、5か所の挿入位置が用意されています。

設定方法

管理画面 → 管理 → エディタ → 各項目の詳細設定から「拡張HTML」として設定します。

設定名(日本語) プロパティ名 説明
フィールドの前 ExtendedHtmlBeforeField フィールドコンテナ全体の前
ラベルの前 ExtendedHtmlBeforeLabel フィールド内のラベル要素の前
ラベルとコントロールの間 ExtendedHtmlBetweenLabelAndControl ラベルと入力コントロールの間
コントロールの後 ExtendedHtmlAfterControl 入力コントロールの後
フィールドの後 ExtendedHtmlAfterField フィールドコンテナ全体の後

さらに、グローバル拡張HTMLと同じ Parameters.ExtendedHtmls の仕組みを使って、columnName フィルターで特定の項目の上下にも挿入できます(ColumnTop・ColumnBottom ID)。

HTMLへの挿入位置

編集画面のフィールドは次のような HTML 構造になっています。

<!-- [ExtendedHtmls id="ColumnTop" columnName="フィールド名"] -->
<!-- [BeforeField] -->
<div id="Results_NumAField" class="field-normal">
  <!-- [BeforeLabel] -->
  <p class="field-label">
    <label for="Results_NumA">数値A</label>
  </p>
  <!-- [BetweenLabelAndControl] -->
  <div class="field-control">
    <input id="Results_NumA" class="control-text" type="text" value="...">
    <!-- [AfterControl] -->
  </div>
</div>
<!-- [AfterField] -->
<!-- [ExtendedHtmls id="ColumnBottom" columnName="フィールド名"] -->

ソースコードでの実装は次のとおりです。

Implem.Pleasanter/Libraries/HtmlParts/HtmlFields.cs(抜粋)
return hb
    // フィールドの最上部(グローバル拡張HTMLのcolumnNameフィルタ)
    .Raw(HtmlHtmls.ExtendedHtmls(context, id: "ColumnTop",
                                 columnName: column.ColumnName))
    // フィールドコンテナの前
    .Raw(Strings.CoalesceEmpty(
        serverScriptModelColumn?.ExtendedHtmlBeforeField,
        column.ExtendedHtmlBeforeField))
    // フィールドコンテナ(内部でラベル・コントロールを描画)
    .SwitchField(
        extendedHtmlBeforeLabel: Strings.CoalesceEmpty(
            serverScriptModelColumn?.ExtendedHtmlBeforeLabel,
            column.ExtendedHtmlBeforeLabel),
        extendedHtmlBetweenLabelAndControl: Strings.CoalesceEmpty(
            serverScriptModelColumn?.ExtendedHtmlBetweenLabelAndControl,
            column.ExtendedHtmlBetweenLabelAndControl),
        extendedHtmlAfterControl: Strings.CoalesceEmpty(
            serverScriptModelColumn?.ExtendedHtmlAfterControl,
            column.ExtendedHtmlAfterControl))
    // フィールドコンテナの後
    .Raw(Strings.CoalesceEmpty(
        serverScriptModelColumn?.ExtendedHtmlAfterField,
        column.ExtendedHtmlAfterField))
    // フィールドの最下部(グローバル拡張HTMLのcolumnNameフィルタ)
    .Raw(HtmlHtmls.ExtendedHtmls(context, id: "ColumnBottom",
                                 columnName: column.ColumnName));

Strings.CoalesceEmpty はサーバースクリプトによる設定を優先し、設定がなければ管理画面の設定を使う「優先順位制御」です。つまり、サーバースクリプトで動的にHTMLを上書きすることも可能です。

サーバースクリプトによる動的制御

サーバースクリプトから項目拡張HTMLを動的に設定することができます。

サーバースクリプトの例
// 条件に応じて特定の項目にHTMLを追加
if (model.ClassA === "重要") {
  columns.NumA.ExtendedHtmlBeforeLabel =
    '<span style="color:red;font-weight:bold;">[重要] </span>';
}

AJAXリクエスト時の挙動

グローバル拡張HTMLおよびサイト設定HTMLの挿入は、非AJAXリクエスト(フルページ読み込み)時のみ行われます。プリザンターの通常操作(一覧→詳細への遷移など)はAJAXで行われるため、これらのHTMLは最初のページ読み込み時に一度だけ挿入されます。

HtmlBodyTop・HtmlBodyBottom などのプレースホルダーに <script> タグを埋め込んでも、AJAXでのページ遷移後には再実行されません。スクリプト系の処理は拡張スクリプトや管理画面のスクリプトを使うほうが適切です。


運用上の注意点

グローバル拡張HTML(ファイルベース)はプロセス再起動が必要

ファイルベースのグローバル拡張HTMLはアプリ起動時にメモリに読み込まれます。ファイルを追加・変更した場合はプリザンターのプロセスを再起動する必要があります。

<script> タグを挿入する際のCSP対応

グローバル拡張HTMLの Body に直接 <script> タグを書く場合、CSP(コンテンツセキュリティポリシー)の設定によってはスクリプトがブロックされることがあります。プリザンターが付与するNonce属性は拡張HTMLには引き継がれないため、CSPを設定している環境では注意が必要です。

JavaScriptの実行が目的であれば、拡張スクリプトを使う方がCSPとの整合性が高くなります。

XSS(クロスサイトスクリプティング)への注意

拡張HTMLの Body はRAWとしてHTMLにそのまま出力されます。ユーザー入力などの動的な値を埋め込む場合は、適切なエスケープ処理が必要です。管理者のみが設定できる場面であってもインジェクションのリスクは意識してください。

サイト設定HTMLはサイトエディタ表示時は無効

サイト設定HTMLは管理画面でサイトを編集中には適用されません。これは意図的な仕様です。開発中に「設定は正しいのに反映されない」と感じた場合は、管理画面の編集状態になっていないか確認してください。

項目拡張HTMLはサーバースクリプトで上書き可能

項目拡張HTMLはサーバースクリプトによる動的な設定が管理画面の設定より優先されます(Strings.CoalesceEmpty による優先順位制御)。意図しない上書きが起きていないか、サーバースクリプトの設定も合わせて確認してください。

ファイル名とプレースホルダーIDの対応

グローバル拡張HTML(ファイルベース)では、ファイル名の拡張子を除いた部分がそのままプレースホルダーIDになります。大文字・小文字も区別されます。HtmlBodyTop と htmlbodytop は別物として扱われ、前者のみ有効なプレースホルダーに対応しています。

まとめ

プリザンターの拡張HTML機能を3つのレイヤーで整理しました。

グローバル拡張HTML

項目 内容
ファイル配置場所 App_Data/Parameters/ExtendedHtmls/*.html
ファイル名の役割 プレースホルダーID(HtmlHeaderTop など)に対応
挿入先 <head> 最上部/最下部、<body> 最上部/最下部の4か所
フィルタリング DBベースのみ(部門・グループ・ユーザー・サイトID・コントローラー・アクション)

サイト設定HTML

項目 内容
設定場所 管理画面 → HTML設定
挿入先 4つの位置タイプ(HeadTop / HeadBottom / BodyScriptTop / BodyScriptBottom)
適用画面の絞り込み 新規・編集・一覧・カレンダーなどを選択可

項目拡張HTML

項目 内容
設定場所 管理画面 → エディタ → 各項目の詳細設定
挿入先 フィールドの前後・ラベルの前・ラベルとコントロールの間・コントロールの後の5か所
サーバースクリプト連携 動的な上書き可能(サーバースクリプトが優先される)

それぞれの適用範囲と挿入位置を理解して使い分けることで、HTMLレベルのカスタマイズを安全かつ効果的に行うことができます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?