風柳メモ

ソフトウェア・プログラミング関連の覚書が中心

Git for WindowsのGit BASHでウィンドウタイトルだけが文字化けする現象(winptyが原因)と対処方法

[2020.10.27追記] Git for Windows 2.29.1-64-bit(mintty 3.4.0 / winpty 0.4.3 の組み合わせ)では、exec winpty bash した後も Git Bash のウィンドウタイトルが文字化けしなくなった模様。以下は古いバージョンについての記述であり、新バージョンでは環境変数 PS1 を書き換えるとかえって文字化けしてしまうので、注意。



Git BASHで、たまたま日本語を含むフォルダ下で作業していたら、ウィンドウのタイトルだけが文字化けしていることに気がついた(ウィンドウ内については正常に表示されている)。
Windows 10 Pro バージョン 1909 (OS ビルド 18363.959)、Git for Windows 2.28.0-64-bit、mintty 3.2.0 (x86_64-pc-msys) [Windows 18363]

f:id:furyu-tei:20200812030339p:plain
Git BASHでウィンドウタイトルだけが文字化けしている様子

この現象の原因と、対処方法についての覚え書きを残しておく。




原因

どうやら、~/.bash_profile にて

exec winpty bash

を記述していたためらしい。

試しにこの記述を外すと、ウィンドウタイトルは化けなくなる。
ただし当然ながら、対話型コマンドがそのままではうまく動作しなくなる。

f:id:furyu-tei:20200812040929p:plain
exec winpy bash の記述を外した場合


Python なんかの Windows ネイティブな対話型コマンドを呼び出すのに、いちいち

$ winpty python

としたり、あるいは ~/.bashrc で対話型コマンド毎に

alias python="winpty python.exe"

のように alias 定義するのが面倒だったので bash 自体を winpty で呼び出していたのだけれど、これが原因でウィンドウタイトルが化けてしまうのは想定外……。

対処方法

A. 必要なコマンド毎に個別に winpty を付けて呼び出す alias を定義しておく

bash そのものを winpty 経由で呼び出しているのが原因なのでこれを止め、個別のコマンド毎に winpty 付き呼び出しを行うようにすれば問題はなくなる。
その場合、標準では /etc/profile.d/aliases.sh にて

    for name in node ipython php php5 psql python2.7
    do
        case "$(type -p "$name".exe 2>/dev/null)" in
        ''|/usr/bin/*) continue;;
        esac
        alias $name="winpty $name.exe"
    done

のように、いくつかのコマンドについて対応されているのを参考にして ~/.bashrc に必要なコマンドについて同様の記述を追記するというのが一つの方法。

B. 環境変数 PS1 を書き換える

A. のようにすると、対話型コマンドの種類が増えるごとにメンテナンスしなければならないため、面倒なのが難点。
そこで、bash を winpty で呼び出したときにウィンドウタイトルのみが化けている点に着目し、プロンプト定義用の環境変数(PS1)を書き換えてやれば良いのではないか? と考えた。
状況的に、bash を winpty 経由にした際には、mintty はウィンドウタイトル文字列としては Shift-JIS(CP932) で渡されることを期待しているのではないかと推測、$PS1 中に含まれる $PWD の文字コードを変換するように定義してみると、

export PS1=`echo "$PS1" | sed "s~\\$PWD~\\\`echo \\$PWD | iconv -f utf-8 -t cp932 2>/dev/null\\\`~"`

これでうまくいった。
ウィンドウタイトルに関してだけはなんとかなったということで、winpty bash したときに出る潜在的な問題は他にもあるかも知れない。

f:id:furyu-tei:20200812042525p:plain
ウィンドウタイトルも文字化けせず、対話型コマンドも正常に起動できている

覚え書きを兼ねて、現状の ~/.bash_profile および ~/.bashrc を残しておく。

~/.bash_profile

#【覚書】
# ※参考:[bashの.profileや.bashrc等を実行する動作仕様 - sgryjp.log](https://blog.sgry.jp/entry/2019/11/09/232927)
#===============================================================================
# ・ ~/.bash_profile はログインシェルで一度だけ実行される
#   このため、主に
#
#   - ログイン時に一度だけ実行したい処理
#   - 対話操作では必要のない(対話操作以外のコマンド実行に必要な)設定(環境変数 LANG 定義等)
#   - ログインシェル/サブシェルに関わらず基本変更不要な設定(環境変数 PATH 定義等)
#
#   を記述するのに向く
#
# ・ ~/.bashrc はサブシェルでは自動的に呼ばれる(ログインシェルでは自動では呼ばれない)
#   このため、~/.bash_profile からも呼び出されるよう設定の上で (^1)
#
#   - alias や関数等、暗黙的に引き継がれない設定
#   - 対話操作のみで必要となる設定(環境変数 PS1、EDITOR 等)
#
#   を記述するのに向く
#
#   (^1) ~/.bashrc があるのに ~/.bash_profile, ~/.bash_login or ~/.profile が無い環境は推奨されず、
#        例えば Git for Windows においては、その場合、/etc/profile.d/bash_profile.sh により
#        ~/.bashrc を呼び出す記述が入った ~/.bash_profile が生成される
#===============================================================================

# ■ ~/.bashrc 実行
#   ※ログインシェルでは ~/.bashrc は自動的には実行されないため、この設定が必要
#test -f ~/.profile && . ~/.profile
test -f ~/.bashrc && . ~/.bashrc

# ■ 各種定義
export PYTHONUTF8=1
export PYTHONSTARTUP=~/.pythonstartup

# ■ bash を winpty 経由で起動
#   ※ Windows ネイティブな対話型コマンド(Python 等の、interactive mode が mintty に対応していないもの)に対応
exec winpty bash
#   ※ exec によりシェルプロセス自体を置換しているため、これ以降に書いた内容は反映されないことに注意

~/.bashrc

if [[ $TERM_PROGRAM = "mintty" ]]; then
    # ■ mintty 専用設定
    case `tty` in
        /dev/cons*) # winpty
            # winpty 経由だと $PWD が日本語を含む場合ウィンドウタイトルが文字化けする(mintty 3.2.0)問題に対処
            # → mintty 3.4.0 / winpty 0.4.3 の組み合わせでは発生しなくなったため、必要がなくなった
            # export PS1=`echo "$PS1" | sed "s~\\$PWD~\\\`echo \\$PWD | iconv -f utf-8 -t cp932 2>/dev/null\\\`~"`
            ;;
        /dev/pty*) # (default)
            ;;
    esac
fi


# ■ 暗黙的にサブシェルに引き継がれない設定(alias・関数定義)
# ※サブシェルでは、
#   - profile系スクリプト(/etc/profile、/etc/profile.d/*、~/.bash_profile等)は実行されない
#   - ログインシェルで定義された alias や関数は引き継がれない
#   ことに注意
shopt -q login_shell || {
    # サブシェル専用設定
    # ※必要に応じて /etc/profile や /etc/profile.d/* (aliases.sh等)で定義されている alias 等をコピーしておく
    alias ls='ls -F --color=auto --show-control-chars'
    alias ll='ls -l'
}

# ~/.bash_profile 経由/サブシェル共用設定
alias ls='ls -aF --color=auto --show-control-chars'
alias tree='tree -N'
alias python2='py -2'
alias python3='py -3'
alias python='python3'
alias pip2='python2 -m pip'
alias pip3='python3 -m pip'
alias pip='pip3'


# ■ 非対話ログインで不要かつ対話ログインで使いたい設定(環境変数定義)
export EDITOR=vim

その他の覚え書き等

ユーザー独自のファイル(bashスクリプト)をログインシェル/サブシェルどちらでも呼び出す(Git BASH 独自仕様?)

/etc/profile.d/git-prompt.sh
の最後の方に、以下のような記述があることに気づいた。

# Evaluate all user-specific Bash completion scripts (if any)
if test -z "$WINELOADERNOEXEC"
then
        for c in "$HOME"/bash_completion.d/*.bash
        do
                # Handle absence of any scripts (or the folder) gracefully
                test ! -f "$c" ||
                . "$c"
        done
fi
  • /etc/profile.d 下のファイルは通常はログインシェルからしか呼ばれないものだが、git-prompt.sh に限り、サブシェルからも呼び出される(/etc/bash.bashrc 経由)
  • ~/bash_completion.d/ というディレクトリを作成し、この下に拡張子 .bash の付いた任意の bash スクリプトを置いておくと、/etc/profile.d/git-prompt.sh 経由で呼び出される(結果的にはログインシェル/サブシェルの区別なく呼ばれることになる)

参考

winpty・mintty 関連

mseeeen.msen.jp

bash 関連ファイル(~/.bash_profile・~/.bashrc 等)の動作仕様について

blog.sgry.jp
なんとなくで設定しがちだった bash の ~/.bash_profile や ~/.bashrc といったファイルの動作仕様が分かりやすくまとまっていて非常に参考になった。

Google ドライブの共用 PDF を画像データとしてダウンロードするユーザースクリプトを試作

Google ドライブで共用されている PDF ファイルを、画像データにして ZIP 化し、ダウンロードできるユーザースクリプトを作ってみました。
通常は PDF をそのままダウンロードすればよいのですが、まれにダウンロード権限が「閲覧者はダウンロードできません」となっている場合があるため、主にその対策用です。
画像として取得しても、PDF 化したり OCR にかけたりと結局は別途手間はかかってしまいますが……まぁ、手動で1ページずつ落としたりするよりは幾分マシかなと。

インストール・使い方など

github.com
をご参照下さい。

ユーザースクリプトのソースコード


はてなブログでは Gist 貼り付けしかできないみたいなので、gist-it.appspot.com - Embed files from a github repository like a gist を使って GitHub 上のソースコードを貼り付けてみました

chrome-extension-kickstart-typescript が Node.js v12 だとうまく動かない件とその対策

■ 追記
2020年02月11日にid:SWIMATH2さんが対応して下さりgenerator-chrome-extension-kickstart-typescript v4.4.1 以降では、Node.js 12 でも元の手順のままで正常動作するようになった模様です。
以下は古い情報となります。


勉強を兼ねてブラウザ拡張機能をTypeScriptで書いてみようと、swimath2.hatenablog.comgenerator-chrome-extension-kickstart-typescript を試してみたところ、Node.js v12 ではそのままでは動作しませんでした。
試行錯誤の末にとりあえず動くようになったので、覚え書きとして手順等を書いておきます。




対策(お急ぎの方はこちらだけお読みください)

試行環境
# Node.js v12.14.1 LTS および Yarn 1.21.1 は、それぞれインストーラをダウンロードしてインストール済
$ node -v && npm -v
v12.14.1
6.13.4

$ yarn -v
1.21.1

# yo および generator-chrome-extension-kickstart-typescript を npm もしくは yarn にて global インストール
#$ npm install -g yo generator-chrome-extension-kickstart-typescript
$ yarn global add yo generator-chrome-extension-kickstart-typescript

$ yo --version
3.1.1
手順

1. 拡張機能用のプロジェクト作成
拡張機能用のフォルダを作り(例では sample)chrome-extension-kickstart-typescript を --skip-install 付きで実行

$ mkdir -p sample
$ cd sample
# --skip-install を付けて、npm による自動インストールを抑制
$ yo chrome-extension-kickstart-typescript --skip-install

2. package.json 編集

$ vi ./package.json

※差分例

--- ./package.json.orig 2020-01-11 14:33:14.473509100 +0900
+++ ./package.json      2020-01-11 14:44:36.316563000 +0900
@@ -32,7 +32,7 @@
     "chromereload": "0.x.x",
     "debounce": "1.x.x",
     "del": "3.x.x",
-    "gulp": "3.x.x",
+    "gulp": "3.9.0",
     "gulp-bump": "2.x.x",
     "gulp-cache": "0.x.x",
     "gulp-clean-css": "^3.x.x",
@@ -57,6 +57,10 @@
     "vinyl-named": "1.x.x",
     "webpack": "3.x.x",
     "webpack-stream": "3.x.x",
-    "yargs": "^8.x.x"
+    "yargs": "^8.x.x",
+    "typescript": "^3.x.x"
+  },
+  "resolutions": {
+    "gulp/**/graceful-fs": "^4.x.x"
   }
 }

3. パッケージのインストール
yarn install で必要な全パッケージをインストール

$ yarn install

4. 動作確認
動作するかどうかを yarn run で確認

$ yarn run dev:chrome

※ 正常動作したときの例

yarn run v1.21.1
$ gulp --watch --vendor=chrome
[14:56:00] Requiring external module babel-core/register
[14:56:07] Using gulpfile D:\webext\sample\gulpfile.babel.js
[14:56:07] Starting 'build'...
[14:56:07] Starting 'clean'...
[14:56:07] Finished 'clean' after 3.37 ms
[14:56:07] Starting 'manifest'...
[14:56:07] Starting 'scripts'...
[14:56:07] Starting 'styles:css'...
[14:56:07] Starting 'styles:less'...
[14:56:07] Starting 'styles:sass'...
[14:56:07] Starting 'pages'...
[14:56:07] Starting 'locales'...
[14:56:07] Starting 'images'...
[14:56:07] Starting 'fonts'...
[14:56:07] Starting 'chromereload'...
[14:56:07] Starting 'livereload-server'
[14:56:07] Finished 'styles:css' after 32 ms
[14:56:07] Finished 'styles:less' after 26 ms
[14:56:07] Finished 'styles:sass' after 26 ms
[14:56:07] Starting 'styles'...
[14:56:07] Finished 'styles' after 20 μs
[14:56:07] Finished 'pages' after 27 ms
[14:56:07] Finished 'fonts' after 26 ms
[14:56:07] Finished 'manifest' after 264 ms
[14:56:07] Finished 'locales' after 237 ms
[14:56:07] Finished 'images' after 237 ms
ts-loader: Using typescript@3.7.4 and D:\webext\sample\tsconfig.json
[14:56:10] Finished 'scripts' Hash: 941f7e8c10f53a4b8956
Version: webpack 3.12.0
Time: 2076ms
        Asset     Size  Chunks             Chunk Names
background.js  6.83 kB       0  [emitted]  background
   [0] multi ./app/scripts/background.ts 28 bytes {0} [built]
   [1] ./app/scripts/background.ts 254 bytes {0} [built]
[14:56:10] webpack is watching for changes

経緯

発生した不具合

最初に、qiita.com を参考にして、

$ mkdir -p sample
$ cd sample
$ yo chrome-extension-kickstart-typescript
# 試行のためとりあえず問にはすべて[Enter]
$ npm install --save-dev typescript
$ npm install --save-dev gulp@3.9.0
$ npm run dev:chrome

のようにしたところ、

$ npm run dev:chrome

> sample@0.0.0 dev:chrome D:\webext\sample
> gulp --watch --vendor=chrome

[08:33:12] Requiring external module babel-core/register
fs.js:27
const { Math, Object } = primordials;
                         ^

ReferenceError: primordials is not defined
    at fs.js:27:26
    at req_ (D:\webext\sample\node_modules\natives\index.js:143:24)
    at Object.req [as require] (D:\webext\sample\node_modules\natives\index.js:55:10)
    at Object.<anonymous> (D:\webext\sample\node_modules\vinyl-fs\node_modules\graceful-fs\fs.js:1:37)
    at Module._compile (internal/modules/cjs/loader.js:955:30)
    at Module._extensions..js (internal/modules/cjs/loader.js:991:10)
    at Object.require.extensions.<computed> [as .js] (D:\webext\sample\node_modules\babel-register\lib\node.js:152:7)
    at Module.load (internal/modules/cjs/loader.js:811:32)
    at Function.Module._load (internal/modules/cjs/loader.js:723:14)
    at Module.require (internal/modules/cjs/loader.js:848:19)
npm ERR! code ELIFECYCLE
npm ERR! errno 1
npm ERR! sample@0.0.0 dev:chrome: `gulp --watch --vendor=chrome`
npm ERR! Exit status 1
npm ERR!
npm ERR! Failed at the sample@0.0.0 dev:chrome script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\testuser\AppData\Roaming\npm-cache\_logs\2020-01-11T06_33_12_337Z-debug.log
調査

Node.js v12 と gulp.js 3 は相性が悪いらしい?!
とりあえず「ReferenceError: primordials is not defined」で検索してみると
stackoverflow.com
に行き当たる。
こちらの回答によると、

  1. gulp.js を version 4 にする
  2. Node.js のバージョンを v12 未満へ下げる

のいずれかで対応できるらしい。

1. の gulp.js のバージョンを変える方は、3→4 の変更に対応してソースコードも変更が必要になると思われるので手間がかかりそうだったため、2. の方法を試してみる。
nvm-windows で Node.js のバージョンを 10.18.1 LTS にしてみると、確かに動作するようになった。

Node.js v12 & gulp.js 3 のまま使いたいな……
できれば Node.js や gulp.js のバージョンを変えずに使う方法はないか、と調べていると、
https://timonweb.com/posts/how-to-fix-referenceerror-primordials-is-not-defined-error/timonweb.com
に行き当たる。

要は、gulp.js が利用している graceful-fs のバージョンが古いのが悪いらしい。

$ npm list graceful-fs

で調べてみると、他はほとんどが "graceful-fs@4.2.3" を使っているのに対し、gulp(及び
gulp が依存しているパッケージ)については

+-- gulp@3.9.0
| `-- vinyl-fs@0.3.14
|   +-- glob-watcher@0.0.6
|   | `-- gaze@0.5.2
|   |   `-- globule@0.1.0
|   |     `-- glob@3.1.21
|   |       `-- graceful-fs@1.2.3
|   `-- graceful-fs@3.0.12

のように、確かに古いバージョン("graceful-fs@1.2.3"、"graceful-fs@3.0.12")を使っている。

そういえば、yo chrome-extension-kickstart-typescript 実行時にも

npm WARN deprecated graceful-fs@1.2.3: please upgrade to graceful-fs 4 for compatibility with current and future versions of Node.js

のような警告が出ていた。これによれば、graceful-fs 4 以降を使うようにすれば良さげ。

上記の記事を参考に、npm-shrinkwrap.json を

{
  "dependencies": {
    "graceful-fs": {
        "version": "^4.x.x"
     }
  }
}

の内容で作成した後、改めて npm install 後に npm run dev:chrome をすると、

# package-lock.json は npm-shrinkwrap.json と排他なため、消しておく
$ rm package-lock.json
# パッケージのインストール
$ npm install
# ※このとき、npm-shrinkwrap.json も更新されることに注意

# 開発環境を起動
$ npm run dev:chrome

すると、今度はエラーになると起動することができた。

ただし、この方法だと、

  • npm install --save-dev でパッケージを入れたりすると、最初に指定した "dependencies" が維持されないため、再度 npm-shrinkwrap.json を上記内容に書き換えた上で npm install を実行しなおす必要がある

ということで、やや保守性に欠ける。

特定のパッケージが依存関係にあるパッケージだけバージョンを指定したい
要望としては

  • gulp が依存している graceful-fs のみ、バージョンを指定したい
  • メンテナンスは最小限(意識するのは初回のみ)としたい(パッケージの追加時等に影響無いようにしたい)

となる。

調べてみると npm の代わりに Yarn を使用し、『選択的な依存関係の解決(Selective dependency resolutions)』機能を使えばできそう(package.json に "resolutions" フィールドを追加)。

  "resolutions": {
    "gulp/**/graceful-fs": "^4.x.x"
  }

この方針で調整したのが、こちらの手順になる

参考:npm で Yarn の "resolutions" っぽいことはできないか?
一応、npm 用にそういうパッケージも存在する模様。
github.com

ただ、試してみた限りでは

  • 階層指定("gulp/**/graceful-fs" 等といった書き方)はできない
  • "scripts"セクションに"preinstall"を追加するのは、一度 npm install を実行後にする必要あり
    いったん npm install してから(package-lock.json が作成されてから)でないと、npm-force-resolutions がエラーになる
  • パッケージの追加時等でpackage-lock.jsonが更新された際には、npm install をやり直す必要あり

のような感じで、今ひとつ使い勝手が良くないように感じた。
ざっくりとか見ていないため、誤解しているかもしれない

その他

generator-chrome-extension-kickstart 及び主な fork の状況

【覚書】他社で取得した独自ドメインのメールアカウントをさくらのレンタルサーバーで使う場合のDNS設定方法

さくらのレンタルサーバーのメールサーバー機能を利用して、(さくら外で取得した)独自ドメインのアカウントでメールを送信したら、Gmail で「未認証メール」扱いとなった(「?」マークがついた)。
調べてみると、独自ドメインの DNS 設定(SPFレコード)が誤っていた。
再発防止のため、メモ書きとして恥をさらしておく。





メール用の DNS 設定例

さくらのレンタルサーバーの設定が以下の様になっている場合、

項目 値(例) 備考
初期ドメイン myaccount.sakura.ne.jp さくらのアカウント名が頭に付くドメイン名
ホスト名 wwwNNN.sakura.ne.jp さくらのサーバーのホスト名(IPv4アドレス=YYY.YYY.YYY.YYY)
※サーバ情報の表示>サーバに関する情報の「ホスト名」
SPFチェック対象となる(メールヘッダのReceived: from ~に記載される)のはこちら
IPアドレス XXX.XXX.XXX.XXX 初期ドメインに紐づいたIPv4アドレス
※サーバ情報の表示>サーバに関する情報の「IPアドレス」
※共有アドレスか専用アドレスかはプランによって異なる
サーバーのホスト名に紐づいたもの(YYY.YYY.YYY.YYY)とは異なる場合があることに注意

以下の様に独自ドメインを取得し、

項目 値(例) 備考
独自ドメイン mydomain.com (さくら外で取得した)独自ドメイン名

"user@mydomain.com" のようなメールアドレス(アカウント)を、さくらのレンタルサーバーで使用することを想定する。
さくらのコントロールパネルから、ドメイン/SSL設定>[新しいドメインの追加] にて、「5. 他社で取得したドメインを移管せずに使う」により、mydomain.com を追加してあるものとする。

このとき、メール用として、(他社の)DNSサーバーには以下の様に MX レコードと TXT レコード(SPFレコード)を登録する。

mydomain.com.          3600    IN      MX      10 myaccount.sakura.ne.jp.
mydomain.com.          3600    IN      TXT     "v=spf1 a:wwwNNN.sakura.ne.jp mx ~all"

SPFレコードの記述は、さくらのドメイン詳細設定を使用した際に「□SPFレコードを利用する」にチェックを入れた場合に設定されるものに準拠している(SPFディレクトリ中、'mx' は無くても良い。また、'a:wwwNNN.sakura.ne.jp' の代わりに 'ip4:YYY.YYY.YYY.YYY' と直接 IP アドレスを記載しても動作はするが、IPv6アドレスが使用されることもあるため、ドメイン名にしておいた方が無難)。

失敗例

SPF レコードを

mydomain.com.          3600    IN      TXT     "v=spf1 ip4:XXX.XXX.XXX.XXX ~all"
mydomain.com.          3600    IN      TXT     "v=spf1 a:myaccount.sakura.ne.jp ~all"
mydomain.com.          3600    IN      TXT     "v=spf1 mx ~all"

といった様に、許容するIPアドレス/ドメインとして初期ドメインを設定してしまうと、認証に失敗してしまう(場合がある)。
ビジネスプラン等では、初期ドメイン(myaccount.sakura.ne.jp)とサーバーのホスト(wwwNNN.sakura.ne.jp)が示すIPアドレスが異なっているため、失敗(softfail)する。スタンダードプラン等では同じIPアドレスを示すので、結果的に通る(pass)場合もある。

例えば Gmail アドレス宛にメールを送信したとき、認証に失敗していると、メールヘッダで

ARC-Authentication-Results: i=1; mx.google.com;
       spf=softfail (google.com: domain of transitioning user@mydomain.com does not designate YYY.YYY.YYY.YYY as permitted sender) smtp.mailfrom=user@mydomain.com

のように SPF チェックの結果が "softfail" であったことが記載され、Gmail で当該メールを見たときに「未認証メール」として、「?」マークが付く。
メールを開き、「?」マークにマウスオーバ(スマートフォン等の場合にはタップ)すると、「Gmail では、このメールが(スパム発信者からではなく)本当に mydomain.com から送信されたメールであることを確認できませんでした。」といった文言が表示される。

SPF レコードの妥当性チェック方法

設定した SPF レコードが有効かどうかは、port25が提供しているサービスを使うのが手っ取り早い。

check-auth@verifier.port25.com

宛にメールを送信すると、「Authentication Report」という Subject がついて、

==========================================================
Summary of Results
==========================================================
SPF check:          pass
"iprev" check:      pass
DKIM check:         none
SpamAssassin check: ham

のように SPF チェックの結果などを含むメールが返信されてくる。

Firefox のアドオン(content_scripts)でXMLHttpRequestやfetchを使う場合の注意

先日、Firefox で Twitter メディアダウンローダが正常に動作しないという報告を受け(その後、近傍ツイート検索も異常な動作をすることが判明)、原因を突き止めるのに時間がかかったので、覚え書き。

結論としては、

ということ。





現象

Firefox 上で、Twitter 用のアドオン(Twitter メディアダウンローダ(0.1.1.2801以前)/近傍ツイート検索(0.2.6.1200以前))を動かすと、jQuery.getJSON() や jQuery.ajax() で、本来 JSON が返されるべきところが HTML が返ってきてしまい、正常に動作しない。
基本的には同一のスクリプトであるにも関わらず、Google Chrome 拡張機能版や、Firefox であっても Tampermonkey 上のユーザースクリプトとしてであれば問題なく動作する。

原因

当該関数による GET リクエストの HTTP Request Header に

  • Origin: null がセットされる
  • Referer はセットされない

という動作になっていた。

なお、この現象が発生した際には、当該通信は開発者ツールのネットワークモニタ上に表示されない、という困った不都合もある(こちらは、未だに表示させる方法が不明のまま)。

Twitter 側で(リクエストの正当性検証のため?おそらく 最近になって)Referer等をチェックするようになったことが原因で、正常な応答が返されなくなっていたものと思われる。

追記(2018/07/19)

SWSが本脅威を対策するためには、Refererなどを確認してリクエストの正当性を検証したり、CookieにSameSite属性を付与する手法などが考えられます。

ソーシャルウェブサービスにおける新たなプライバシー脅威「Silhouette」を発見:NTT持株会社ニュースリリース:NTT HOME

実は、このプライバシー脅威(Silhouette)への対策の影響だった、とか?

対策

This is accomplished by exposing more privileged XHR and fetch instances in the content script, which has the side-effect of not setting the Origin and Referer headers like a request from the page itself would, this is often preferable to prevent the request from revealing its cross-orign nature. From version 58 onwards extensions that need to perform requests that behave as if they were sent by the content itself can use content.XMLHttpRequest and content.fetch() instead. For cross-browser extensions their presence must be feature-detected.

Content scripts - Mozilla | MDN

バージョン58以上の Firefox においては、アドオンで content_scripts 内で XMLHttpRequest や fetch (およびそれらを使用している jQuery 等のライブラリ)を使用するときには、window.XMLHttpRequest や window.fetch の代わりに、content.XMLHttpRequest や content.fetch を用いればよいらしい。
これにより、普通にページ(コンテンツ)上から発行されたのと同様に、Origin や Referer がセットされるようになる。

注意事項

content_scripts 内では、window.XMLHttpRequest や window.fetch は read-only になっているため、content.* で上書きしようとしてもできない。

自作スクリプト内ならば、使用するスコープの最初で、

const XMLHttpRequest = ( typeof content != 'undefined' && typeof content.XMLHttpRequest == 'function' ) ? content.XMLHttpRequest  : window.XMLHttpRequest;
const fetch = ( typeof content != 'undefined' && typeof content.fetch == 'function' ) ? content.fetch  : window.fetch;

のようにしておけばよいかも。

問題なのは、jQuery のようなライブラリを使用する場合で、作りによってはソースコードを直接書き換えるしか無いかもしれない。
なお、Firefox のアドオンを AMO にアップする際には、既知のライブラリを使う場合、改変されたものは同梱出来ない、という制約がある。レビューでチェックされ、改変が見つかった場合は当該バージョンが無効化されてしまう(既に経験済み(汗))。

幸い、jQuery の場合には、読み込んだ後にjQuery.ajaxSettings.xhrを次のような感じにオーバーライドしてやればよさげ。
追記と訂正:直接書き換えるよりも、jQuery.ajaxSetup() を介するべきだった。

if ( ( typeof content != 'undefined' ) && ( typeof content.XMLHttpRequest == 'function' ) ) {
    /*
    //jQuery.ajaxSettings.xhr = function () {
    //    try {
    //        return new content.XMLHttpRequest();
    //    } catch ( e ) {}
    //};
    */
    jQuery.ajaxSetup( {
        xhr : function () {
            try {
                return new content.XMLHttpRequest();
            } catch ( e ) {}
        }
    } );
}