こんにちわ、サーバーサイドエンジニアのいいだです。
前回はそうめんのおかずについて悩みながらねこを育てました。今回はハッシュドポテトはそもそもおかずになるか否かについて悩みながら自分専用のブラウザ拡張を作ります。
特定のサービスの特定のボタンを押して入力を書き換えてもらいたい気持ち
弊社では、勤怠管理としてOZOというシステムを活用しています。詳細は割愛しますが、今回はこのシステムの中にどうしても押したいボタンが一つあり、ついでに入力してほしい入力欄が一つあり、これを入力してもらおうという魂胆でいます。それが以下の画面です。

これは工数管理の実績情報を入力するためのテーブルです。いろいろな機能があり様々な活用が可能な項目ではありますが、種々の歴史的イベントや紆余曲折を経て利用面ではかなり簡便化が進み、現在に至って、少なくとも私はこの画面に入り、コピーと書かれたボタンを押し、出現した行の作業時間を当日の総労働と等しい値で上書きし、登録ボタンを押す、というとてもシンプルな作業をしています。
この作業をするにあたっては、特に私の判断が必要な箇所がありません。つまり、私以外の誰かがこの作業を行なっても結果は同じであり、自動化が可能ということになります。ありがたいですね。
特定のサービスの特定のボタンを押して入力を書き換えるブラウザ拡張
ブラウザ拡張を作ろうという記事は既にほとんど無限にありますので、今回はいかに楽をして、自分専用の、特に人々の役には立たないけど、とりあえず自分が楽をできるようになるものを作るかに焦点を当てていきたいと思います。
ファイル構成
ozo_autofill/ ┣ manifest.json ┣ content.js ┗ moo.js
どのブラウザのものを作るのも似たようなものですが、私はChromeを使っていますので、これを例に進めます。まずはmanifest.jsonを書きましょう。
manifest.json
{
"manifest_version": 3,
"name": "ozoの勤怠表埋めるやつ",
"description": "ozoの勤怠表をいい感じにします。",
"version": "1.0.0",
"host_permissions": ["https://スクリプトを働かせたいURL"],
"content_scripts": [
{
"matches": ["https://スクリプトを働かせたいURL"],
"js": ["moo.js", "content.js"]
}
]
}
今回のようなちょっと楽をするだけの拡張であれば、ほとんどcontent_scriptsで十分です。content_scriptsは任意のページの文脈の上で動くスクリプトです。つまり、ページの要素にアクセスできますし、書き換えられます。ありがたいですね。
content.js
function tryObserve() {
if (!document.URL.match(/knt_kinmuinput/)) {
return;
}
const taskCopyButton = document.querySelector(
'[onclick="javascript:onClick_CopyYesterday();"]'
);
if (!taskCopyButton) {
setTimeout(tryObserve, 200);
return;
}
taskCopyButton.click();
const row = taskCopyButton.closest("table").querySelector("#tr_1");
const moo = new Moo(row);
moo.register(fillTaskTime);
moo.observe();
}
function fillTaskTime(row, _, observer) {
const workTimeText = document.querySelector("#disp_sou_roudou").innerText;
const workTimeInput = row.querySelector("#db_WORK_TIME");
if (!workTimeInput) {
return;
}
observer.disconnect();
workTimeInput.focus();
workTimeInput.value = workTimeText;
workTimeInput.blur();
}
tryObserve();
moo.js
class Moo {
element = undefined;
callbacks = [];
mo = undefined;
constructor(element) {
this.element = element;
}
register(callback) {
this.callbacks.push(callback);
}
observe() {
this.mo = new MutationObserver((mutations, observer) => {
this.callbacks.forEach((callback) => {
callback(this.element, mutations, observer);
});
});
this.mo.observe(this.element, {
childList: true,
subtree: true,
});
}
}
スクリプト本体については、たいへんシンプルです。ページが読み込まれたらボタンを押して、ボタンの機能がよしなに働いて、ほしい値が狙った場所に埋まったら入力欄を書き換えます。
const moo = new Moo(row); moo.register(fillTaskTime); moo.observe();
中身を知らない非同期の機能の完了を待つ時は、MutationObserverが便利です。内部でどのような処理が行われていようと、さしあたって画面が書きかわったことは見ればわかりますので、そこを監視対象に選びます。お手軽です。分離して牛さんにしておけばいろんな拡張で使えます。
const taskCopyButton = document.querySelector(
'[onclick="javascript:onClick_CopyYesterday();"]'
);
こうして人の作ったDOMに乗っかってものぐさをする時、意外と苦労するのが適切な要素を特定するためのセレクタを見つけることだったりします。今回は時間周りについてはわかりやすいidを得られましたが、コピーボタンは使えそうなものがなかったのでonclick属性を指定しました。なかなか見る機会のないすごい字面です。ここで一度拝んでおきます。
これを世に出したら怒られそうなものですが、使うのは自分だけであり、動くのは自分のパソコンの上なので、自分に怒られなければセーフです。どうか自分を許してあげてください。

そして自分を許し終えたら、おもむろにこの拡張をブラウザに読み込んでもらいます。満を持して、勤怠入力画面を開きましょう。

めでたいことに動きましたね。画面を開いてすぐに工数情報がコピーされて時間が埋められますので大変に地味ですが、とにかく動いています。あとは登録ボタンを押せばその日の作業は終わります。ありがたいですね。
これで記事を終えたいところですが、念の為開発者ツールを開いてエラーを確認してみます。

すると悲しいことに、強めのエラーが出ています。このスクリプトを許可した覚えはないぞと、そのようなことを言われているところです。結構な剣幕であり、大変怖いエラーです。
ブラウザ拡張はページが存在を許しているスクリプトではないので、ページの中身を触ろうとすると怒られることがあります。今回怒られたのはContent Security Policyによるもので、サーバーないしブラウザによって指定されたポリシーに従って読み込まれたリソースを検査し、必要であればブロックすることで攻撃を防ぐというセキュリティ機能です。
ただのコピーボタンを押すだけの自分で作ったスクリプトを自分で実行しただけなのでちょっと不服ではありますが、これが画面にかわいいねこを表示する大人気拡張に埋め込まれた自動アカウント削除ボタン連打機能だったりなんかしますと大変なことですので、正しい反応と言うべきでしょう。ありがたいことですね。
もちろん、ここで悪い開発者の気持ちになってとにかくスクリプトにボタンを押してもらえるような実装を行うこともできますが、ここは悪いことをしたと反省したのち、ちょっと善良な気持ちを取り戻して、ボタンはスクリプトではなく自分に押してもらうことにしましょう。content.jsからボタンを押している部分を削除するだけです。
function tryObserve() {
if (!document.URL.match(/knt_kinmuinput/)) {
return;
}
const taskCopyButton = document.querySelector(
'[onclick="javascript:onClick_CopyYesterday();"]'
);
if (!taskCopyButton) {
setTimeout(tryObserve, 200);
return;
}
const row = taskCopyButton.closest("table").querySelector("#tr_1");
const moo = new Moo(row);
moo.register(fillTaskTime);
moo.observe();
}
function fillTaskTime(row, _, observer) {
const workTimeText = document.querySelector("#disp_sou_roudou").innerText;
const workTimeInput = row.querySelector("#db_WORK_TIME");
if (!workTimeInput) {
return;
}
observer.disconnect();
workTimeInput.focus();
workTimeInput.value = workTimeText;
workTimeInput.blur();
}
tryObserve();
この変更を行ったのち、拡張機能を再読み込みしてみると、エラーが消えると共に正しい開発をしている気持ちが得られます。自分でボタンを押して前回の項目をコピーすると最初の行の時間を自動で変更してくれるというだけの無害な拡張ですが、キーボードに手を伸ばさなくてよい分、毎日面倒な作業をしているという気持ちはかなり減ったと思います。ありがたいですね。
ユニファでは自分や世界中の人々の毎日をちょっとずつ楽にしたい善良なエンジニアを募集しています。便利なサービスを正しく開発しましょう。