はらへり日記

腹に弾丸

身内向けにCTF開催した

CTF開いた

某コミュニティでCTF開いた

やるきっかけとしては

  • ISUCONで人権を失う
  • SECCON予選で人権を再び取り戻そうという決意を固める
  • でもCTF力全然ないし練習会しよう

という感じ。

学生の頃にちょっとだけCTFに参加したことがあったのと、ちょっと使ってみたいOSSがあったので主催した。

忙しいのに合間を縫ってyagihashooには問題を作ってもらったり、当日参加して解説してくれたりした。ありがとう。

ちなみに同コミュニティではISUCONの練習会もやってたり(こちらはhoto氏が開いてくれた\(^o^)/)。

ISUCON 練習会をするために InfluxDB + Grafana でポータルサイトを作った - ほとラボ

CTFを開いたきっかけ

一番のきっかけは私がずっと前からこれを使ってみたかったというのが実はあった。

github.com

CTFをやったことある人はわかるんだけど、Flagを送信したり得点を表示するポータルサイトなるものが必要になる。

これはFacebookが作ったポータルサイトOSSでめっちゃカッコイイ。

gyazo.com

これが実際の画面。プロビジョニングするだけですぐできるし機能も多くて、もしCTFやる人がいたらぜひオススメしたい。

AWSのEC2インスタンス立ち上げてgit cloneしてshell叩いてLet's Encryptしたら終わりです。

AWS慣れてないので金銭感覚わからないけど7, 8人参加で2000円くらいしかかからなかった。

問題構成

CTFやったことない人が大半、かつWebの人が多かったので全ジャンルの初級編 + Webの中~上級編という感じで15問出した。

とはいいつつ私も全然CTFしたことないのでいわゆる良問なるものを探したり、昔自分が解いたやつを参考にしたりして作った。

いろんな問題とかWriteupとか読んで、世のCTFerはすごいなぁと思った|ω・`)

やってみて

CTFをやるって決まった時は実装で死ぬかなぁと思ってたけど、どっちかっていうといい問題を作ることに頭を使ってる時間のほうが長かった。

「この問題が解けたらこんな学びがあるように」みたいなことを考えつつ、脆弱性を作り込むのは結構楽しかった。

そしてPHPとDocker超便利。本当に便利。

CentOS7でPHP5.2をビルドしようとして絶望してたけどDocker使ったら瞬殺だった。

当日まで楽しんでもらえるかめっちゃ不安だったし、学びも何もない会になったら飛び降りるしかないと思ってたけど楽しいと言ってもらえたのでよかった。

まとめ

そもそも自分も全然できないのに企画するの不安だったり、運営ぐだぐだだったりしたけど学びが多かったしCTFに興味ある人いたらぜひオススメしたい。

唯一はしゅに作ってもらった問題が解けなくてやはり人権得られなかったので修行していきたい。

頑張って来年のSECCON予選でるぞい!

Laravelでセッターインジェクションする

この記事は

Laravelアドベントカレンダー8日目の記事です。

qiita.com

前提知識

この記事ではDIパターンを実現する1つの手段であるセッターインジェクションをLaravelで実現する方法を紹介します。

なのでDIパターンやDIコンテナを知らない方は先にこれらの記事を読んでいただくと理解が進むと思います。

Inversion of Control コンテナと Dependency Injection パターン

さくっと知りたい方は私が今年発表したスライドの55枚目までを流し読みしていただければと思います。

LaravelにおけるDependency Injection

LaravelはHTTPリクエストが来たタイミングでDIコンテナが起動します。

それがアプリケーションコードのほぼ全ての依存を解決するので、開発者はインスタンスの生成方法を意識せずにコーディングすることが可能となっています。

例えば以下のようなコントローラークラスがあるとしましょう。

<?php

namespace App\Http\Controllers;

use App\Services\SampleService;

/**
 * Class SampleController
 */
class SampleController 
{
    /**
     * @param SampleService  $sampleService
     *
     * @return \Illuminate\Http\Response
     */
    public function main(SampleService $sampleService)
    {
        $data = $sampleService->getData();
        
        return view('main', compact('data'));
    }
}

なんてことはない、mainという名前のViewを必要なデータを入れてレンダリングして返すコントローラーメソッドです。

このコントローラーをもしLaravelを使わずに使おうと思うとおそらくこんな感じのコードを書かなければなりません。

(コードはイメージ)

<?php

namesapce App\Http;

use App\Http\Controllers;
use App\Services\SampleService;
use App\Repositories\SampleRepository;

/**
 * Class OriginalRoute
 */
class OriginalRoute
{
    /**
     * @return array
     */
    public function route()
    {
        return [
            '/main' => function (\Illuminate\Http\Request $request) {
                $controller = new SampleController;
                $response = $controller->main(
                    new SampleService(new SampleRepository)
                );
                return $response;
            },
        ];
    }
}

注目してほしいのは連想配列/mainに指定しているClouser部分です。

実装者はSampleControllerインスタンスを実装し、mainメソッドが依存しているインスタンスを自分の手で生成する必要があります。

小規模なアプリケーションであればこのような方法で実装するのは問題にはなりづらいかもしれません。

しかしクラス数が増えたら?インスタンスの生成方法が変わったら?実装の差し替えが起こったら?

そういったことを考えるとこのような方法では将来的につらいことになる可能性が高いです。

しかしLaravelだとこんなことはしなくても大丈夫です。

auto wiring

先ほど述べたようにLaravelではHTTPリクエストが来た際にDIコンテナが立ち上がります。

そのDIコンテナがアプリケーションコード中でDIパターンによって明示的に指定されている依存関係を全てよしなに解決します。

詳しい仕組みはコードを読むとよいと思いますが、LaravelのDIコンテナはReflection等を活用して自動で必要な依存関係を調べるauto wiringという仕組みで動いています。

詳しくは下記リンクを読むと理解が進むかと思います。

Aura.Di/auto.md at 3.x · auraphp/Aura.Di · GitHub

Auto Wiring - Container

これによって何が嬉しいかというと、開発者は例えばSampleController::main()メソッドが必要としているSampleServiceインスタンスを生成する必要が無いということです。

普段は意識することは少ないかもしれませんが、これを覚えておくと設定なしにInterfaceやスカラー型をタイプヒントしてもDIコンテナからインジェクションしてくれない理由がわかると思います。

セッターインジェクション

セッターインジェクションとはDIパターンを実現するための1つの手段です。

Laravelでよく使われる手段としてコンストラクタインジェクションがあります。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * ConstructorでタイプヒントしておくとLaravelのauto wiringにより
     * インスタンスが注入される
     *
     * @param SampleRepository  $sample
     */
    public function __construct(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

これをセッターメソッドを用意して行うのがセッターインジェクションです。

上記のコードをセッターインジェクションを用いたコードに書き直すとこんな感じ。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * Constructor
     */
    public function __construct()
    {
        //
    }

    /**
     * @param SampleRepository  $sample
     */
    public function setSampleRepository(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

こうすることで以下のような形でSampleRepositoryインスタンスをDIすることができます。

<?php

$sampleService = new \App\Services\SampleService;

// Dependency Injection!!
$sampleService->setSampleRepository(new SampleRepository);

簡単ですよね。

Laravelでのセッターインジェクションのやり方

ここからがこの記事の本編です。

Laravelではセッターインジェクションに対する定義を行うAPIが用意されていません(あったら教えてください…)。

なので方針としては

といった感じでやります。

対象のクラスは先ほど出てきたSampleServiceを利用します。

<?php

namespace App\Services;

use App\Repositories\SampleRepository;

/**
 * Class SampleService
 */
class SampleService
{
    /** @var SampleRepository */
    protected $sample;

    /**
     * Constructor
     */
    public function __construct()
    {
        //
    }

    /**
     * @param SampleRepository  $sample
     */
    public function setSampleRepository(SampleRepository $sample)
    {
        $this->sample = $sample;
    }
}

このクラスをそのままLaravelで利用したら、普通に動きます。

しかしながらセッターは実行されないのでインスタンス生成直後にセッターメソッドを実行したい。

そういった場合はIlluminate/Container/Container::extendを利用します。

任意のServiceProviderでこんな処理を書きます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class DependencyServiceProvider extends ServiceProvider
{
    public function register()
    {
        // set SampleRepository instance
        $this->app->extend(\App\Services\SampleService::class, function ($sampleService, $app) {
            $sampleService->setSampleRepository(new \App\Repositories\SampleRepository);
            return $sampleService;
        });
    }
}

また、extend等を通じて完全に依存解決された後にセッターインジェクションしたい場合にはIlluminate/Container/Container::resolvingを使用できます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class DependencyServiceProvider extends ServiceProvider
{
    public function register()
    {
        // set SampleRepository instance
        $this->app->resolving(\App\Services\SampleService::class, function ($sampleService) {
            $sampleService->setSampleRepository(new \App\Repositories\SampleRepository);
            return $sampleService;
        });
    }
}

こうすることでDIコンテナによって生成されたSampleServiceインスタンスにセッターインジェクションを行うことができました!

セッターインジェクションの利用場面

このセッターインジェクションですが、普通の実装をしているとあまり使う場面は出てきません。

というのも大抵のインスタンスはコンストラクタインジェクションで事が足りるからです。

しかし、この方法を覚えておくと設計の幅が広がります。

例えばインスタンス生成の方法が複雑でコンストラクタの拡張が困難な場合などはセッターを生やした継承クラスを作成し、bindした上でセッターインジェクションするといったことが可能です。

他にも例えばログやトランザクションといった汎用的な処理を行うインスタンスのセッターをTraitで作成し、DIコンテナでセッターインジェクションすればクラス本体を汚さずに欲しいインスタンスを注入することも可能です。

まとめ

Laravelの良さはDIコンテナの柔軟さだと思ってます。

DIコンテナをしっかり利用できれば開発も楽になりますし何より楽しくなるので、依存解決で難解な場面に出くわすことがあればぜひこのセッターインジェクションという方法を思い出してみてください。

明日の記事はIganinTeaの記事です。お楽しみに!

アイスタイルを退職します

近況報告 兼 ポエムです。悪しからず

アイスタイルを退職します

新卒として入って2年目ですが、今月一杯でアイスタイルを退職することになりました。

退職の動機として、「やめたいから転職する」というより「行きたい場所があるから転職する」というニュアンスが非常に近いです。

アイスタイルでの2年間

じつは学生の頃のアルバイト期間から合わせるとぴったし2年程、勤めた計算になります。

最初の頃は本当にポンコツで、SQLがわからないだの設計って何だのconfigって何だの、本当にひどい有様でした。

ですが技術研修や業務中の指導をしていただいたりですとか、初歩的な質問をしても快く答えてくれる同僚達のおかげでちょっとは使い物になるエンジニアになれたのかなと今は思っています。

特に@ytakeさんには感謝しきれてもし足りません。この方の指導がなければ転職どころかまともにコーディングもできないままだった気さえするくらい、いろいろご指導頂きました。

本当にありがとうございます。

新卒というレッテルを捨てること

今回の転職でいろんな変化が起こると思っているのですが、一番大きな変化は新卒のレッテルがなくなることかなと思ってたりします。

特にアイスタイルに関して言えば新卒を特別扱いする文化が良くも悪くも存在します。

新卒で入った人は部署が違っても気にかけてくださる先輩社員も多く居たり、新卒で入った同士の縦のつながりが非常に強かったりと本当に過ごしやすいのです。

一方で新卒という立場だからこそ普通の人以上に注意を受けたりもするのですが、それも悪いことではなくて、むしろ注意されなくなったら社会人として終わり感があるので今では本当にありがたかったなと感じています。

新卒バッジをつけながらアイスタイルで働くのは本当に居心地が良くてかわいがってもらえることが非常に多かったです。

今回それを捨てることになるわけですが、一生に一度の物を捨てる惜しさもありつつ僕自身はワクワクしていたりします。

仕事(というよりもエンジニアリング)に関して言えば僕はドMと言われてもしょうがない性格をしていて、例えばダメなコードはダメとはっきり言われたいしフィードバックは常に求めているし、願わくば一生誰かに「お前のコードは本当にダメだな!」と言われながら仕事をしたいと思っています。

それを考えると私にとって新卒ではなく「中途社員」という立場に武者震いをしています。

中途のいわば即戦力としてお賃金をもらうわけですから、会社としてはコストをかけて育てる対象ではないわけです。

となると私が成すべきことはもらっている対価以上の価値をすぐにでも生み出すことです。今のお休み期間に死ぬほど勉強しなきゃですね。

といっても今までも「新卒だからしょうがないよね」みたいな扱いはほぼされたことはないので実態は変わらないかもしれません。僕のメンタルの問題なんでしょうかね。

今までありがとうございました

アイスタイルではエンジニアがやめる時はプチ送別会的なものを開いて上長からありがたいお言葉を頂いたり、辞める人がちょびっと話したりする文化があるのですが、それに来ていただいた方が想像以上に多くて本当に多くの人に支えられてたんだなという気持ちです。

最終日前日にあいさつ回りする人を数えたら180人いたりして、たかだか2年と思っていたりしたんですが意外と重いんだなと思ったり。

時間が合わなかったりして挨拶できなかった方も何人かいらっしゃって、本当に申し訳なかったのですがまたどこかでお会いしたらシカトしないでいただけると嬉しいです。

今後共何卒よろしくお願いします。

次の会社のことはまた入社エントリポエムでも書くのでその時に。。。

おまけ

伝統芸能らしいので貼るだけ貼っておきます。

amzn.asia

社内で横断的に使えるPHPライブラリを書こう!

この記事は

アイスタイルアドベントカレンダー3日目の記事です。

タイトル的に技術知見っぽいんですがどちらかと言うとポエムに近いのであしからず。。。

qiita.com

社内の共有ライブラリを作る

弊社といえば化粧品クチコミサイトの@cosmeを運営していることで有名だと思うのですが、

実は@cosme以外にもかなり多くのサイトを運営しています。

それらの大半はPHPで実装されており、そのバージョンや使用しているフレームワークは多種多様です。

そうなると問題になってくるのが社内で同じような処理を実装したものが各所で発生することです。

弊社の例でいうと以下のような実装コードが数々のリポジトリで見られるようになりました。

  • タグのバリデーション実装
  • 社内APIとの通信処理
  • OAuth認証のロジック

せっかく言語が共通なのにこれらの処理とテストを新しく実装するたびに作っているのはもったいないなと感じ、

いくつか社内で使いまわせるライブラリを作りました。

この記事ではその時に得られたノウハウを共有したいと思います。

目指すこと

まず初めに、社内ライブラリを作る際に目指すことを考えます。

  • Composerによる配布を前提とする
  • 保守性の高さを意識する
  • 属人性を排除する

Composerによる配布

この時点でPHP5.3.2+が必須になってしまいますが、下位互換性を意識すると出来ることの幅が狭くなるのでここでは意識しません。

PHP5.5以下はサポート切れてますし…ね…

社内ライブラリなのでOSS化できない場合はPrivateな環境で配布する必要があります。

この際、Gitを導入していればComposer配布が可能になります。

まずは作成したリポジトリをComposerで落とせるようにするところまでやってみます。

Composerパッケージを作る

Gitリポジトリを作成したらカレントディレクトリでcomposer initを実行します。

すると対話式で作成するComposerパッケージについて聞かれるので答えていきましょう。

入力が完了するとcomposer.jsonが自動で生成されます。

これで最低限の配布準備は完了です。簡単!

Composerライブラリを落とす

Composerは通常、Packagistでパッケージを配布し、インストールします。

ですが、明示的にgitリポジトリのURLを指定することでPackagistからでなくgitサーバーからパッケージをインストールすることが可能になります。

まず、インストールしたいリポジトリcomposer.jsonrepositoriesという項目に追記します。

{
  "repositories": {
    "type": "git",
    "url": "https://github.com/private_organization/sample_repo"
  }
}

これでcompsoer require private_organization/sample_repoを叩くことでパッケージを落とすことができます。

これでPrivateなgitサーバーからComposerパッケージを配布することができます。

開発する

ここから実際に実装したいコードを開発していくことができますが、1年間やってきてこうすればよかったよというのを述べていきます。

第三者がプルリクを出せる環境を作る

社内で共有ライブラリを作り、浸透していくとバグ修正や機能追加のプルリクが飛んで来ることがあります。

その際、必ずしもレビューするのはライブラリ作成者ではないのでライブラリの品質を担保できなくなる場面が時たまあります。

例えばテスト未実行によるデグレやコーディング規約の揺れ、ドキュメント不足等がそれにあたります。

頑張ってそれらを取り締まる共有ライブラリ警察をやってもいいのですが、せっかくプログラマーをしてるので自動化できる部分は自動化してしまうとよいです。

弊社の流れで言うと

  • GitHubにプルリクエストが来る
  • Jenkinsでテストジョブを実行する
    • このジョブが通らないとマージボタンが押せないようになっている
  • PHPUnitによる単体テスト、cs-fixerによる構文チェック等が行われる

と言った形で、テストの実行漏れやコーディング規約の制約をブランチテストという形で課しています。

また、GitHubを使用している場合はCONTRIBUTING.mdを追加することでプルリクを出す際にそれを読むよう促してくれたり、.githubディレクトリにテンプレートを入れるとPull Requestのテンプレートを追加できたりします。

ドキュメントの追加等、開発者にしてほしい作業があればそのテンプレートにチェックボックス形式で追加することで作業を促しています。

バージョン管理を厳密に行う

ライブラリが社内に浸透していくと破壊的変更を行った時の影響範囲が大きくなります。

なのでバージョニングはセマンティックバージョニングに従い厳密に行います。

どんなに些細な変更でもAPI変更があればメジャーバージョンをあげることでバージョン変更によるバグが起きない用心がけましょう。

コードレビューは誰がするのか

作成するライブラリがどのプロジェクトにも該当しない場合、コードレビューを誰に頼めばいいかわかりづらい状況でした。

なので弊社ではSlackに#codereviewチャンネルを作成し、「自分の分かる言語ならレビューしていいよ」という人を募ってそこにレビュー依頼を投げるようにしました。

そうすることでレビューを投げる先が明確になり、放置されるPull Requestの数が減りました。

布教する

個人的に、これがかなり大事だと思っています

テストも完璧、ドキュメントも完璧な品質の高い便利ライブラリを作っても誰にも使われなければ時間の無駄になってしまうし保守もされなくなっていきます。

なので社内ライブラリを作ったら必ず社内に宣伝するようにしています。

社内wikiで「こんな便利ライブラリを作りました」と言って使い方を詳しく解説したり、その記事をSlackの#generalチャンネルに貼り付けたり。

また、自分がそのタイミングで入ってるPJTで採用できそうなら積極的に採用し、運用実績を作ると結構みんなノリ気になってくれることが多いように感じます。

社内ライブラリを作ってよかったこと

業務改善や再実装の無駄の排除はもちろんなのですが、それ以外にもよかったことがいくつかありました。

自分の勉強になる

ライブラリを作成するとフレームワークに頼って開発してるときと違い、全て1から自分で考えなければなりません。

どんなフォルダ構成にするのか。ライブラリのInterfaceはどうするのか。どこまで汎用化、抽象化するのか。

そういったことを考えるために普段使ってるライブラリのソースコードを読んだり、社内のできる先輩に設計を相談することでコーディングの知見を多く得られました。

社内に文化を広める

弊社は元々、OSS指向の人は多くなくGitHubによるプルリクエスト開発も導入して何年も経っていません。

なので隣のPJTのコードを読むとか、それにプルリクを出すと言ったことはほとんどありませんでした。

しかし、社内ライブラリが少しずつ増え、お互いコードレビューをしていくうちにその知見を各PJTで持ち帰ってもらったり、テストの書き方を学んでもらったりと社内の技術知見の風通しが改善されました。

普段はなかなか関わらない人と「このライブラリこう使いたいんだけど」みたいな相談をしたりとコミュニケーションも増え、100人規模になった弊社としては非常によい効果をもたらしていると感じています。

自分の名前を社内に残せる

これは完全にネタなんですが、私が作ったlaravel/socialiteを社内のOAuth向けに拡張したライブラリがあるのですが、そのGitHubリポジトリの作成を上長に依頼した時に

上長1「名前何にしましょう」

私「socialiteの拡張なのでis_auth_socialiteでお願いします」

上長2「sugi_socialite」

上長3「sugiurasocialite」

上長1「了解です」

私「ちょっと何言ってるかよくわからないです」

上長2「sugiura」

上長1「sugiulite」

上長3「sugiuliteいいですね!それでいきましょう。ぜひ」

私「マネージャー×3にいじめられてる…社会ってコワイ…」

って茶番がSlackに繰り広げられた末に弊社にはsugiuliteというライブラリが存在します。私の名前は杉浦です。

というわけでみなさんも積極的に社内ライブラリを作っていきましょう。楽しいです👏

明日は

明日はanntoque君の「changefinderの詳細について書く」だそうです。

お楽しみに!

今さら聞けないChromeエクステンションの作り方

この記事は

JavaScriptアドベントカレンダーの1日目の記事です。

qiita.com

JavaScriptというよりはChromeの話かもしれませんが、最近エクステンションを作って楽しかったので簡単に作り方を書いてみます。

Chromeエクステンションとは

github-label-creater

私は普段、GitHubで開発するときにIssueのラベルを自分好みに変えてから使うのですが、

毎回作り直すのが面倒でブックマークレットを作ろうとしました。

ただ、ブックマークレットだとソースコードの量が限られてしまうので、Chromeエクステンションにしようと思い立ち、github-label-createrなるエクステンションを作成しました。

github.com

こんな感じで使えます。

エクステンションの種類

一言でエクステンションと言っても、いくつか種類があります。

とてもざっくり言うと以下の3種類のタイプが存在します。

Browser Action

追加するとURLのバーの横にアイコンが追加され、それでいろいろ機能が使えるタイプ。

はてブのエクステンションなんかはこれですね。

Page actions

特定のページでのみ動くエクステンションです。

Override Pages

Chromeの内部ページを書き換えるタイプのエクステンションです。

作りたいもの

今回作りたいものの要件は以下の通り。

  • https://github.com/{username}/{reponame}/labelsでのみ動けばよい
  • ページ上のDOMを操作してラベルを作り替える
  • アイコンをクリックして出てくるボタンを押すとラベルが作り替わってほしい

ということで、先ほどのPage ActionsとBrowser Actionの機能を組み合わせて作っていきます。

雑な実装イメージ図

構造的にはこんな感じで作っていきます

Content Scripts

まずは特定のページ上で読み込まれるContent Scriptsを作成します。

Chromeエクステンションではエクステンションの情報や読み込むスクリプトの情報をmanifest.jsonというファイル名で作成します。

{
  "manifest_version": 2,
  "name": "GitHub label creater",
  "version": "0.0.1",

  "description": "Create labels of GitHub.",

  "author": "sota1235"
}

ここにcontent_scriptsという名前で情報を追加します。

今回はhttps://github.com/*/*/labelsに一致するページでdist/index.jsを読み込ませたいので以下のように書きます。

URL matchの記法はドキュメントで確認できます。

{
  "manifest_version": 2,
  "name": "GitHub label creater",
  "version": "0.0.1",

  "description": "Create labels of GitHub.",

  "content_scripts": [
    {
      "matches": [
        "https://github.com/*/*/labels"
      ],
      "js": ["dist/index.js"]
    }
  ],

  "author": "sota1235"
}

これでGitHubのLabelページでdist/index.jsが読み込まれるようになりました。

Browser Action

今回は「アイコンをクリックして出てくるボタンをクリックしたらWebページにアクションを起こす」といった形で実装するので、Browser Actionのスクリプトと設定を追加します。

Browser Actionではアイコンがクリックされたときに表示するHTMLを設定できます。

今回はpopup.htmlを表示するようにします。

manifest.jsonに以下の設定を追加しましょう。

{
  "browser_action": {
    "default_icon": {
    },
    "default_title": "GitHub label creater",
    "default_popup": "popup.html"
  }
}

default_iconのところにはアイコンに表示したい画像を入れることができますが、無い場合は空でも動きます(誰かアイコン作って…|ω・`))。

popup.html上ではボタンがクリックしたときの処理をしたいので<script>タグを使ってdist/popup.jsを読み込んでいます。

Browser ActionとContent Scriptsで通信する

さて、ここから少しChromeエクステンションならではの処理が出てきます。

仕様上、Browser Actionからページ上のDOMに直接アクセスすることはできません。

なのでBrowser Actionからイベントを発火し、Content Script側でそれを監視する形で実装を行います。

Browser Actionでのイベント発火

API等のドキュメントはこちら

import domready from 'domready';
import $ from 'jquery';

domready(async () => {
  // 作成するラベルデータ一覧
  const labels = await get('labels');

  $('button.create-labels').on('click', () => {
    
    // 
    chrome.tabs.query({active: true, currentWindow: true}, tabs => {
      chrome.tabs.sendMessage(tabs[0].id, { type: 'CLICK_POPUP', labels });
    });
  });
});

ただ、あらかじめmanifest.jsonchrome.tabsAPIの使用を許可する必要があります。

以下のように追記しましょう。

{
  "permissions": [
    "tabs", "https://github.com/*/*/labels"
  ]
}

これで現在表示されてるタブのContent ScriptにイベントをPublishできます。

Content Scriptでのイベント監視

Content Script側でのイベント監視はこんな感じ。

import domready from 'domready';
import { createNewLabels, deleteLabels } from './label-creater';

domready(() => {
  // popup.jsからのイベント監視
  chrome.runtime.onMessage.addListener(message => {
    // コールバックでpopup.jsからの値を受け取れる
    if (message.type !== 'CLICK_POPUP') {
      return;
    }

    // ここでページ上のDOMを書き換える
    if (window.confirm('All labels will be overwritten. Are you OK?')) {
      deleteLabels();
      createNewLabels(message.labels);
    }
  });
});

これでアイコン上のボタンを押すとContent Scriptが実行される流れが作れました!

History APIの罠

ただ、このままだと特定の条件で動作することができません。

Content Scriptはそのページに対してHTTPリクエストが飛んだ際に読み込まれますが、History APIによるURL書き換えの際は読み込まれません。

GitHubではIssueページからLabelページに行くタイミング等ではHistory APIによる遷移を行っているため、Content Scriptが読み込まれません。

そこでBackground Scriptを使って少しハックします。

Chromeエクステンションではchrome.webNavigationというAPIが用意されており、これに生えてるonHistoryStateUpdated.addListenerでURL履歴が変更された際のイベントをcatchできます。

なのでこれを利用し、History update時にContent Scriptを読み込むBackground Scriptを作成します。

まずmanifest.jsonに以下を書き足します。

{
  "background": {
    "scripts": ["background.js"]
  }
}

webNavigatoinAPIの使用を許可するため、permissionsに値を足しましょう。

{
  "permissions": [
    "https://github.com/*/*/labels", "tabs", "webNavigation"
  ]
}

そして以下のようなシンプルなBackground Scriptを作成します。

/** @var {boolean} Is the content script always executed. */
let isExecuted = false;

chrome.webNavigation.onHistoryStateUpdated.addListener(() => {
  if (!isExecuted) {
    chrome.tabs.executeScript(null, { file: "dist/index.js"} );
  }

  isExecuted = true;
});

ページ遷移するたびにContent Scriptを実行するとイベントが多重に登録されてしまうので、変数でイベント登録済みかどうか判定しています。

これでHTTPリクエストを伴わないページ遷移時にもContent Scriptが呼ばれるようになりました!めでたし!

開発の時に便利だったもの

Content Scriptはページで読み込まれるのでconsole.logデバッグできますが、Browser Actionでは同じようにデバッグができません。

そこで、chromeにエクステンションを読み込ませた際に発行されるextension IDを利用して直接、popup.htmlにアクセスすることができます。

下記画像のID部分を

chrome-extension://{エクステンションID}/popup.htmlといった形式でアクセスします。

これでデバッグ作業ができます!

まとめ

独自APIとか仕様に振り回されそうな印象が強く、今まで避けてたエクステンション開発でしたが思ったより簡単だなというのが僕の感想です。

古い情報とかも多いので困ったら公式ドキュメントを読めばだいたい解決するのでみなさんもぜひ何か作ってみてください。

ブックマークレットと違ってnpmライブラリを使えたりするので幅は広いんじゃないかなと思います。

参考サイト

Google Chrome Extensionを作ってみた-その2(デバッグ)- | Developers.IO

Chromeのオリジナル拡張機能を開発しよう(ソースコードあり) | 株式会社LIG

javascript - Chrome extension Content Script not loaded until page is refreshed - Stack Overflow