yuw27b’s blog

技術メモと雑記

videoタグのダウンロードオプション

HTMLのvideoタグで動画を表示すると、Chromeで右下のメニューをクリックすると「ダウンロード」という項目があります。(他のブラウザでは確認できませんでした。)
これは、controlslist属性に"nodownload"というパラメータを指定すると非表示になります。

controlslist属性には他にも様々な値が指定できますが、HTML標準にはなってないようです。
<video>: 動画埋め込み要素 - HTML: HyperText Markup Language | MDN
[HTMLMediaElement] Add a controlsList/controlslist attribute. by avayvod · Pull Request #2426 · whatwg/html · GitHub
(MDNのページには実験段階を示すアイコンがついているし、WHATWGの提案はマージされていない様子。)

<video
  class="mediaWrapper_video"
  src="video.mp4"
  type="video/mp4"
  controls
  controlslist="nodownload"
>
</video>

これでChromeでも「ダウンロード」というメニューは表示されなくなりますが、動画の上で右クリックするとコンテキストメニューに「名前をつけて動画を保存」という項目は表示されたままです。できれば保存されたくない、という要望があった場合、やはり右クリックも無効化するしかなさそうです。

document.querySelector('.mediaWrapper').addEventListener('contextmenu', function(e) {
  e.preventDefault();
  return false;
});

ユーザビリティの面からは、コンテキストメニューの非表示はあまりやりたくないので、せめて動画の部分(ここではmediaWrapperクラス)だけを無効化。)


もちろんソースコード上では動画のURLが見えていてダウンロードできてしまうので、厳密にやるのであればストリーミング配信するしかないですが、「なるべくなら保存されたくない」というような場合は、現状はこれがベターでしょうか。

GETで見られたくないものを送信するときはヘッダーに入れる

HTTPSで通信していても、GETのクエリパラメータは暗号化されないので、見られたくないものを送信するときは、POSTで送るか、HTTPのリクエストヘッダーに入れるようにしています。

クエリパラメータはURLの一部なので、https://example.com?page=1みたいな平文が公衆Wi-Fiの通信に乗ったり、中間サーバのログに残ったりする可能性があります。ページ番号であればそれでも構いませんが、例えばログインリクエストの場合にはhttps://example.com?user=xxxxx&password=yyyyyのような情報を送ることになるので、これは見られては困ります。

単純にPOSTリクエストのリクエストボディに入れても良いのですが、リクエスト先がRESTfulなAPIで、セマンティクス的にGETがふさわしい場合(サーバのリソースを書き換えないリクエスト)には、HTTPのカスタムヘッダーを使うようにしています。

例:
https://example.com/api/login のようなURLのエンドポイントにGETリクエストを送るときに、
MyApp-User-Authorization:{ユーザー認証のための文字列}
のようなカスタムヘッダーを追加します。

「ユーザー認証のための文字列」には、「ログイン名:パスワード」をbase64エンコードしたものを入れて、サーバ側でデコード・パースして認証を行います。

このやり方は、cybozuさんのAPIを勝手に参考にさせていただいたものです:
kintone REST APIの共通仕様 – cybozu developer network

上記のAPI仕様書によると、cybozuさんのカスタムヘッダーは「X-Cybozu-Authorization」という名前ですが、カスタムなヘッダーの場合に接頭辞を「X-」とする、という習慣は、現在は非推奨とのことなので、シンプルにプロダクトのサービス名から始めるようにしています。

「X-」接頭辞についての参考資料:
HTTP ヘッダー - HTTP | MDN
RFC 6648 - Deprecating the "X-" Prefix and Similar Constructs in Application Protocols

PHPがセッションIDを発行しなくなった

突然PHPsession_id()が空白を返すようになってしまった

もちろん本当に「突然」なのではなく、何かいじってしまったからですが。

PHPのセッション情報は、デフォルトではファイルに保存される

ので、そのファイルの読み書きができなければセッションIDも発行されません。

phpinfo();

でセッションの項目を確認。
session.save_handlerfilesの場合、
session.save_pathに設定されているディレクトリに読み書きの権限があるかを確認。



なぜそんなことになったのかの推測(自分メモ)

対象ディレクトリのユーザーがroot、グループがapacheになっていた。
当該サーバではApacheではなくNginxを使っているので、グループがapacheなのはおかしいようにも思える。
おかしくなる直前に、PHPの追加モジュールを入れたりしていたので、そこでパーミッションをいじってしまったか、グループがapacheに変わってしまったのかもしれない。

letter-spacingとtext-align: center(とcalc())

letter-spacingとテキストの中央揃えを併用すると、左右の空白がずれて見えるのでpaddingで調整することがよくある。

例えばこんな見た目のボタンを作るのに、
f:id:yuw27b:20201105213339p:plain

padding: 5px;
font-size: 16px;
letter-spacing: 1em;
text-align: center;
/*〜略〜*/

とする。
(全角スペースで表現されたデザイン画を受け取ることもあるけど、HTMLに「検 索」と入れるのはなしですよね。ぱっと思いつくところだとアクセシビリティの問題とか。)

そうすると、letter-spacingは各文字の後ろに入るので、右側の余白が大きく見えてしまう。
f:id:yuw27b:20201105213414p:plain

これをpaddingで調整するのに、

padding: 5px 5px 5px 21px; /* 5 + 16 = 21 */

でも良いのだが、ここ数年はモダンブラウザであれば「calc()」が問題なく使えるので*1

padding: 5px 5px 5px calc(5px + 1em);

としておくと、コードの意図がより分かりやすくなって余白の調整もやりやすい。
f:id:yuw27b:20201105213433p:plain

GCP Cloud FunctionsでPuppeteerを使う

1日1回スクレイピングしてきてSlackに流してほしい、みたいな簡単なやつなので、Firebaseまではいらない。

Cloud FunctionでランタイムにNode.jsを選択→package.jsonとindex.jsを登録しておき、Cloud Scheduler->Pub/Sub経由で呼び出せばOK。

プチハマりどころ
  • Puppeteerのバージョンが新しすぎると動かない→2020年1月現在ではv2.0.0なら動く。Node.jsのバージョンが10系なので仕方ないのかも。
  • Puppeteer起動時のオプションで、--single-processとか--no-sandboxとかを有効にしておかないと動かない。

最終的にこうなった:

  const browser = await puppeteer.launch({
    headless: true,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '-–disable-dev-shm-usage',
      '--disable-gpu',
      '--no-first-run',
      '--no-zygote',
      '--single-process',
    ],
  });
  • メモリ1GB割り当てておかないと足りなかった。(処理内容によっては512MBでもなんとかなるかもしれない。または、たまになら失敗しても構わないケースとか。)

console.log()でGCPのログに吐き出せるし、1日1回くらいの頻度であれば無料枠におさまってくれた。
データの永続化も、小さいデータならFirestoreに読み書きで十分という印象。
同じプロジェクト内ならデフォルトでアクセス権もあるので、こんな感じ:

  const firestore = new Firestore({
    projectId: '(PROJECT_ID)'
  });
  const document = firestore.doc('path/to/doc');
  const currentDoc = await document.get(doc => {
    return doc;
  });

/////(なんか処理とか)

  await document.update({
    'xxx': 'yyy'
  });