はじめに
Workspaceで添付ファイル・HTMLフィールドを利用していた時に困った仕様があったので
結論的としては非推奨なのですがその回避策を考えてみました。
折角なので記事にします。
今回のテーマは次です。
HTMLフィールドに貼り付けた画像を別レコードにコピーするとどうなるのか?
PDI環境
試したPDI環境はこんな感じです。
Version: Zurich

仕様の確認
ServiceNow の HTML フィールドに画像を貼り付けた場合の内部動作を確認します。
HTML フィールドに画像を貼り付けると、想像ですが画像は以下の仕組みで保存されるようです。
- 画像ファイルは
sys_attachmentテーブルに保存される - HTMLフィールドには 添付ファイルの参照URLが保存される
つまり、HTMLフィールドには実際の画像データではなく
添付ファイルへのリンクが保存されています。
※厳密にいうとサムネイル用レコードのリンク
HTMLフィールドに保存される内容
HTMLフィールドには次のようなタグが保存されます。
<img src="sys_attachment.do?sys_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
画像保存の仕組み
HTMLフィールドと添付ファイルの関係は次のような構造になっています。
つまり
HTMLフィールド → 添付ファイルを参照
という構造になっています。
困る仕様(HTMLコピー時)
ここで問題になるのが HTMLフィールドのコピー&ペーストです。
例えば次のような操作を行った場合
- レコードAに画像を貼り付ける
- HTMLフィールドの内容をコピー
- レコードBのHTMLフィールドに貼り付ける
- この場合、HTML内の sys_id は変更されません。
コピー時の状態
【レコードA】
<img src="sys_attachment.do?sys_id="AAB9c18383eb7650678dfbb6feaad354>
【レコードB】
<img src="sys_attachment.do?sys_id=AAB9c18383eb7650678dfbb6feaad354">
つまり
両方のレコードが同じ添付ファイルを参照する状態になります。
問題点
この仕様により次の問題が発生します。
① ACLの影響
レコードA
↓
閲覧不可
の場合
レコードBの画像も表示できない
可能性があります。
② 元レコード削除
レコードA削除
↓
添付削除
↓
レコードBの画像消える
③ 添付ファイル管理が分かりづらくなる
別レコードが同じ添付ファイルを参照する状態が発生
になります。
本来期待する構造
別レコードの画像コピーペースト時も本来はこうなっていてほしいのです。
つまり
レコードごとに添付を持つ状態です。
実際の現象を確認
この記事で使っていたワークスペースを利用して検証します。
ここでadmin権限のあるアカウントでLOG0001023レコードにあるHTMLフィールドに対して画像を貼り付けます。
Saveを押して保存すると右側のサイドバーに添付ファイルとして画像が添付されている事が確認できます。
別のレコードLOG0001058に貼り付けてSaveします。

保存後、レコードLOG0001058の右サイドバーを見ても添付ファイルはありません。元レコードにあるからです。

ここでレコードLOG0001023を参照できないtest.taroさんになりすまし、レコードLOG0001058を表示してみます。

添付ファイルの元レコードLOG0001023がACLによって読み取り不可となっているので、画像アイコンが表示されるだけとなります。

回避策
今回の問題はHTML内の画像がコピー元レコードの添付ファイルを参照している事が原因でした。
そのため回避策として次の処理を行います。
HTML保存時に
HTML内sys_id検出
↓
添付コピー
↓
HTML書き換え
を行います。
When:after
Advanced:True
Insert:True
Update:True
ビジネスルールはこんな感じにします。
※スクリプトインクルード化した方がいいと思いますが検証なのでビジネスルールのみで完結しています。
/**
* HTMLフィールド(html, translated_html, wiki_text)取得
*/
function getHtmlFields(tableName) {
let fields = [];
const types = ["html", "translated_html", "wiki_text"];
const dict = new GlideRecord("sys_dictionary");
dict.addQuery("name", tableName);
dict.addQuery("internal_type", "IN", types.join(","));
dict.addQuery("active", true);
dict.query();
while (dict.next()) {
if (dict.element) fields.push(dict.element.toString());
}
return fields;
}
/**
* 添付ファイル複製関数
*/
function copyAttachment(attachmentId, grObj, glideSysAttachmentObj) {
const srcAtt = new GlideRecord("sys_attachment");
// 添付ファイルIDが存在しない場合は何もしない
if (!srcAtt.get(attachmentId)) {
gs.warn("Attachment not found: " + attachmentId);
return null;
}
// 自分自身に紐づく添付ファイルは何もしない
if (srcAtt.table_sys_id.toString() == grObj.sys_id.toString() &&
srcAtt.table_name == grObj.getTableName())
return null;
const newId = glideSysAttachmentObj.writeContentStream(
grObj,
srcAtt.file_name,
srcAtt.content_type,
glideSysAttachmentObj.getContentStream(attachmentId)
);
return newId;
};
/**
* HTML内の添付参照を修正
*/
function fixAttachmentReference(htmlValue, grObj, glideSysAttachmentObj) {
// HTML探索用の正規表現
const regex = /(?:sys_attachment\.do\?sys_id(?:=|=)|api\/now\/attachment\/)([a-f0-9]{32})/g;
regex.lastIndex = 0;
let updated = false;
let match;
let resultHtml = htmlValue.toString();
// sys_idに該当する箇所を全て抜き出し
const matchAllList = [...htmlValue.matchAll(regex)];
// matchした回数分ループして作り直した添付ファイルIDで置換を実行
matchAllList.forEach((match) => {
const attachmentId = match[1];
// 添付ファイルをコピー
const newId = copyAttachment(attachmentId, grObj, glideSysAttachmentObj);
if (!newId) return; // 次のループへ
// ★1箇所だけ置換
resultHtml = resultHtml.replace(attachmentId, newId);
updated = true;
});
return {
html: resultHtml,
updated: updated
};
}
(function executeRule(current, previous) {
let session = gs.getSession();
try {
gs.info("HTML attachment fix BR executed: " + current.sys_id);
if (session.getClientData("html_attachment_fix_running") == "true")
return;
session.putClientData("html_attachment_fix_running", "true");
var htmlFields = getHtmlFields(current.getTableName());
// HTMLフィールドが取得できない場合は何もしない
if (!htmlFields.length) return;
let updated = false;
const gsa = new GlideSysAttachment();
htmlFields.forEach(function(fieldName) {
const html = current.getValue(fieldName);
if (!html)
return;
gs.debug("Processing HTML field: " + fieldName);
// gs.info(`originalHTML:${html}`);
const result = fixAttachmentReference(html, current, gsa);
// gs.info(`fixedHTML:${JSON.stringify(result)}`);
// gs.info(`current:${JSON.stringify(current)}`);
// HTMLが更新されていたらcurrentオブジェクトの値を修正する
if (result.updated) {
current.setValue(fieldName, result.html);
updated = true;
}
});
if (updated) {
gs.info("Record Update:Image Attachments in HTML Fields");
// gs.info("Record updated: " + current.sys_id);
// gs.info(`updated current:${JSON.stringify(current)}`);
current.setWorkflow(false);
current.update();
}
} catch (e) {
gs.error(e);
} finally {
session.putClientData("html_attachment_fix_running", "false");
}
})(current, previous);
注意点
この方法は
- current.update() を使用
- HTMLをregexで書き換え
しているため少し強引な実装です。
ServiceNow公式でも
Business Ruleでcurrent.updateは非推奨
とされています。
問題を解消できたか確認してみる
一応これで問題が解決できたか確認しておきます。
まず、admin件アカウントでPriority:1-Criticalのレコードを作成し、HTMLフィールドに画像を貼り付けて保存します。

次にレコードを新規作成し、先ほどのレコードにあるHTMLフィールド内の画像を貼り付けて保存します。
ビジネスルール作成前はこの画像についてtaro.testさんはアクセスが出来なかったのでそれを確認すれば良さそうです。

taro.testさんでレコードを覗いてみます。
画像を閲覧できることが確認できます。

確認する限りでは問題なさそうではありますが、パフォーマンス検証もしていないですし、サイドバーの添付ファイルを削除した場合にHTMLフィールドの画像が見えなくなるタイミングが若干標準と違うのも気になります。
(ブラウザページを更新しないと見えなくならない)
終わりに
最後に忘れず、このビジネスルールを非アクティブ化しておきます。

なんとなく出来そうな感じはありましたが、ビジネスルールでのcurrent.updateを利用したり、正規表現で無理やりHTMLの参照を変えたりしているので今回のやり方だと非推奨かなぁと思いました。
大人しく公式にコピペ時の仕様を変えてもらうようにお願いした方がいいのかもしれません。
少し凝った事しようとすると非推奨・カスタマイズになってしまうので塩梅が難しいです。
※今回の記事は一部chatgptの力を借りております。記載内容に誤りがある可能性もあるのでご了承ください。
また、検証も極少数のテストケースで試したのみである為、このコードを利用して何らかの問題が生じたとしても責任を負うことはできません。このコードを利用する際は自己責任でお願いします。
参考URL
以下のURLを参考にしました。有益なサイト達に感謝します。
添付ファイルを「sys_attachment」からスコープ付きアプリケーションテーブルにコピーします。
ビジネスルールではcurrent.updateを使用しないでください。
アフタービジネスルールでcurrent.update()を使用する





