拡張 HTML の仕組み
プリザンターで「拡張 HTML」と総称される機能は、システム全体に効く グローバル拡張 HTML、サイト単位の サイト設定の HTML、編集画面の項目周辺に入る 項目拡張 HTML の 3 つのレイヤーでできています。このページでは、それぞれの設定方法と HTML のどこに挿入されるかを、ソースコードをもとに整理します。
3 つのレイヤー
| レイヤー | 設定方法 | 適用範囲 |
|---|---|---|
| グローバル拡張 HTML | ファイル配置、または Extensions テーブル | システム全体(条件指定可) |
| サイト設定の 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 ← 英語のときに使われるファイル名とプレースホルダー ID の対応では大文字・小文字が区別されます。HtmlBodyTop と htmlbodytop は別物として扱われ、有効なプレースホルダーに対応するのは前者だけです。
WARNING
ファイルはアプリ起動時に読み込まれ、メモリに保持されます。ファイルを追加・変更したら、プリザンターを再起動するか、特権ユーザでパラメータリロード(/admins/reloadparameters)を実行してください。確認したソースでは、パラメータリロードでも ExtendedHtmls フォルダを読み直します(Initializer.cs、L122)。ExtendedHtmls 配下のサブフォルダにある .html も読み込まれます(L689-L692)。
Extensions テーブルで設定する
Extensions テーブルでは、ExtensionType に "Html"、ExtensionName にプレースホルダー ID(例: "HtmlBodyTop")、Body に HTML 本文を指定します。ExtensionSettings の Language で言語を指定することもできます。
{
"Language": "ja",
"SiteIdList": [123]
}部門・グループ・ユーザ・サイト ID・コントローラ・アクションによる絞り込みができるのは、DB で設定した場合だけです。
読み込みの仕組み
アプリ起動時に Initializer が ExtendedHtmls/ を走査し、すべての .html ファイルをメモリに読み込みます。ファイル名の _ 以降を言語コード、それより前をプレースホルダー ID として扱います。
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 });
}
}挿入位置
ページのレンダリング時に、4 か所のプレースホルダーに展開されます。
// <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.PositionTypes 列挙型で定義された 4 つの位置から選びます。
| 位置タイプ | 値 | 挿入先 |
|---|---|---|
HeadTop | 1000 | <head> の上部(LinkedStyles・ExtendedStyles より前) |
HeadBottom | 1010 | <head> の下部(ExtendedStyles・Title の後) |
BodyScriptTop | 9000 | <body> 内、スクリプト群の前 |
BodyScriptBottom | 9010 | <body> 内、スクリプト群の後(グローバル拡張 HTML の HtmlBodyBottom より前) |
グローバル拡張 HTML との挿入順
グローバル拡張 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>適用画面の絞り込み
スクリプト・スタイルと同じように、適用する画面の種類を選べます。
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 などが渡され、設定したチェックボックスに応じて適用が決まります。
確認したソースでは、条件にさらに DraftOutputEnabled が加わっています(SiteSettings.cs)。HTML の DraftOutputMode が 1 のときは URL のクエリ Draft が DraftKey(未設定なら 1)と一致したときだけ、2 のときは一致しないときだけ出力されます(L5746-L5760)。
サイトの管理画面を開いている間は適用されない
管理画面でサイトを編集している間(サイトエディタを開いている状態)は、サイト設定の HTML は適用されません(IsSiteEditor が true のとき null を返します)。管理画面の操作性を守るための意図的な仕様です。「設定は正しいのに反映されない」ときは、管理画面の編集状態になっていないかを確認してください。
項目拡張 HTML
編集画面の各項目(フィールド)の周辺に挿入する HTML です。管理画面の「エディタ」で各項目の詳細設定を開き、「拡張 HTML」として設定します。挿入位置は 5 か所あります。
| 設定名 | プロパティ名 | 挿入位置 |
|---|---|---|
| フィールドの前 | ExtendedHtmlBeforeField | フィールドのコンテナ全体の前 |
| ラベルの前 | ExtendedHtmlBeforeLabel | フィールド内のラベル要素の前 |
| ラベルとコントロールの間 | ExtendedHtmlBetweenLabelAndControl | ラベルと入力コントロールの間 |
| コントロールの後 | ExtendedHtmlAfterControl | 入力コントロールの後 |
| フィールドの後 | ExtendedHtmlAfterField | フィールドのコンテナ全体の後 |
これとは別に、グローバル拡張 HTML と同じ Parameters.ExtendedHtmls の仕組みで、columnName を指定して特定の項目の上下(ColumnTop・ColumnBottom)にも挿入できます。項目の絞り込みは ColumnList で行うため、実際に使えるのは Extensions テーブルで ExtensionName を ColumnTop などにし、ExtensionSettings に ColumnList を書いた場合です。ファイル(ColumnTop.html)で置くと、条件が無いためすべての項目に挿入されます(HtmlHtmls.cs)。
確認したソースでは、項目だけを描画し直すレスポンス(isResponse が true のとき)では「フィールドの前」「フィールドの後」は出力されません(HtmlFields.cs、L202-L204)。
挿入位置
編集画面のフィールドの 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="フィールド名"] -->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を追加
if (model.ClassA === "重要") {
columns.NumA.ExtendedHtmlBeforeLabel =
'<span style="color:red;font-weight:bold;">[重要] </span>';
}逆に言えば、サーバースクリプト側の設定が管理画面の設定より優先されます。意図しない上書きが起きていないか、サーバースクリプトの設定も合わせて確認してください。
運用上の注意点
Ajax での描画と head 側の HTML
確認したソースでは、<head> 側(HtmlHeaderTop・HtmlHeaderBottom・HeadTop・HeadBottom)を出力するのは Ajax ではないリクエスト(ページ全体の読み込み)のときだけです。Ajax のリクエストでは <html>・<head> を組み立てず、<body> の中身だけを描画します(HtmlTemplates.cs)。
<body> 側(HtmlBodyTop・BodyScriptTop・BodyScriptBottom・HtmlBodyBottom)は、Ajax のリクエストでもページのテンプレートが描画されるときには出力されます(L43-L102)。そのとき埋め込んだ <script> が再実行されるかどうかはクライアント側での差し込み方によるため、ソースだけでは一律に決められません。
WARNING
HtmlBodyTop・HtmlBodyBottom などに <script> タグを埋め込むと、画面の描画のされ方によって実行される回数やタイミングが変わります。スクリプトの処理は拡張スクリプトや管理画面のスクリプトを使うほうが適切です。
script タグと CSP
グローバル拡張 HTML の本文に直接 <script> タグを書くと、CSP(コンテンツセキュリティポリシー)の設定によってはブロックされることがあります。プリザンターが付与する nonce 属性は拡張 HTML には引き継がれないためです。JavaScript を実行したいなら、拡張スクリプトを使うほうが CSP と整合します。
XSS に注意する
拡張 HTML の本文は、そのまま(RAW で)HTML に出力されます。ユーザの入力などの動的な値を埋め込む場合は、適切なエスケープが必要です。管理者しか設定できない場面でも、インジェクションのリスクは意識してください。
まとめ
| レイヤー | 設定場所 | 挿入先 | 補足 |
|---|---|---|---|
| グローバル拡張 HTML | App_Data/Parameters/ExtendedHtmls/*.html または Extensions テーブル | <head> 最上部・最下部、<body> 最上部・最下部の 4 か所(ファイル名 = プレースホルダー ID) | 条件による絞り込みは DB 設定のみ。ファイルは再起動またはパラメータリロードで反映 |
| サイト設定の HTML | 管理画面の「HTML」 | 位置タイプ 4 種(HeadTop / HeadBottom / BodyScriptTop / BodyScriptBottom) | 新規・編集・一覧などの画面を選べる。サイトエディタ表示中は無効 |
| 項目拡張 HTML | 管理画面 → エディタ → 各項目の詳細設定 | フィールドの前後・ラベルの前・ラベルとコントロールの間・コントロールの後の 5 か所 | サーバースクリプトで上書き可能(サーバースクリプトが優先) |