Quantcast
Channel: rfcタグが付けられた新着記事 - Qiita
Viewing all 199 articles
Browse latest View live

【PHP8】short_open_tagにさよなら、しないかも?

$
0
0

Deprecate PHP Short open tagsというRFCが投票フェーズに入りました。
投票期間は2019/04/10から2019/04/24、採択には投票数の2/3+1の賛成が必要です。

Deprecate PHP Short open tags

Short open tagsとは

PHPの開始を示すタグは<?php、もしくは<?=です。
後者は<?php echoとほぼ同じであり、今回のRFCでは一切影響を受けません。

php.iniの設定でshort_open_tagを有効にすると、PHPの開始タグを<?と省略して書けるようになります。

// 通常
<?php
    echo 'hoge';

// short_open_tag=1ならこう書ける
<?
    echo 'hoge';

Short open tagsのデフォルト値

PHP7.3時点では1、つまり初期設定で有効になっている。
ただし、多くの実装系(XAMPPとか)では0と無効にされているようだ。

Short open tagsの問題点

うっかりXMLをコピペすると死ぬ。

<?xml version="1.0"?>
<?php
    echo 'XMLの中身';

このコードは、short_open_tagが有効な場合syntax errorのE_PARSEになります。
short_open_tagが無効であれば普通にXMLが出力されます。

このように、設定によって動作が根本的に変わってしまうので、あまりよろしくありません。

Remove alternative PHP tags

PHP5時代は、これ以外にも<%<script language="php">といった記述でPHPに入ることが可能でした。
それらはPHP7移行の際に、Remove alternative PHP tagsのRFCで削除されました。

が、なぜか<?だけは残ったままでした。

RFC

<?をPHP7.4でDeprecatedにし、PHP8で削除しよう、という提案です。
これによってPHPの開始タグは<?php<?=の2種類だけになり、とてもすっきりします。

プルリク

PHP7.4ではshort_open_tagのデフォルト値を1から0に変更します。
また有効にするとDirective 'short_open_tag' is deprecatedのE_DEPRECATEDが発生するようになります。

PHP8では設定項目そのものが削除され、有効にすることができなくなります。

internal

「え?short_open_tag削除して常に有効になるんじゃないの?」
「わざわざ削除するメリットが見当たらない」
「今PHP7で書いてる人はほぼ影響受けないだろうけど、未だにPHP5の人はアプデの障壁がさらに高くなるよ。」
「Facebookでアンケ取ってみたら96%が削除賛成だったよ」
<?=が削除されないんだったらかまわない」

投票

2019/04/12時点では、PHP7.4でのDeprecateは賛成17反対9で、このままだと却下されます。
PHP8でのRemoveは賛成19反対9で、このままだと受理されます。

感想

PHP7.4でのDeprecateが却下され、かつPHP8でのRemoveが受理されると、それまで使えてたのにPHP8でいきなり削除ってなるんだけどどうなんだろう。
その場合はPHP8でDeprecateになるのかな?

個人的には<?は全く使ってないので消してもらって全くかまわないんだけど、その結果vendor配下からDeprecatedが大量発生ってなったりすると困りますね。
かつてPEARでもAssigning the return value of new by reference is deprecated湧き潰しできず死ぬという事件がありましたが、それの再来にならないことを祈りましょう。


【PHP7.4】PHPでアロー関数を使えるようになる

$
0
0

かつて、提出されたもののいつのまにか取り下げられていたArrow FunctionsというRFCがありました。

    // $x*2を返す関数
    $mul2 = fn($x) => $x * 2;

    // 使い方
    $mul2(3); // 6

が、なんかV2として復活してました
しかも今回は提案者として重鎮Nikitaが参戦しています。
NikitaはPHPのコア開発者のひとりで、記憶に新しいところではプロパティ型指定を作った人です。

既にコードもできていてプルリクが出されています。

しかしRFCの提出が2019/03/12で、投票は2019/04/17開始・2019/05/01終了です。
なんでそんなスケジュールきつきつなのだ。

RFC Arrow Functions 2.0

Introduction

PHPの無名関数は、単純なことしか行わない場合でもやたら冗長になる場合があります。
使用する変数を毎回手動でインポートしなければならないなど、構文に定型句が多いためです。
このせいで、簡単なクロージャですら読みづらいものとなってしまいます。
このRFCでは、上記の問題に対してより簡潔な構文を提案します。

オーバーヘッドの例として、オンラインで見つけた関数を考えてください。

    function array_values_from_keys($arr, $keys) {
        return array_map(function ($x) use ($arr) { return $arr[$x]; }, $keys);
    }

クロージャが行っている$arr[$x]はたいして難しい内容ではありませんが、定型句のせいで少々わかりづらくなっています。
アロー関数を使うと以下のように書けるようになります。

function array_values_from_keys($arr, $keys) {
    return array_map(fn($x) => $arr[$x], $keys);
}

かつて提出された~>を使うRFCは投票の結果却下されました。
このRFCは、却下されたRFCにおいて持ち上がったいくつかの懸念を解消しています。

このRFCではさらに、様々な代替構文についての詳細な説明も含まれています。
残念ながら、クロージャの短縮構文については、構文や実装に対する制約上の問題で、完璧な解決策を見つけることはできそうにありません。
このRFCでは、その中でも一番ましだと思われる選択をしています。
クロージャ短縮構文の議論は完全に停滞しているため、今後何年も塩漬けしておくよりも、ここで妥協した方がよいでしょう。

Proposal

アロー構文の基本形は以下のようになります。

    fn(parameter_list) => expr

式中で使用されている変数が親スコープ内で定義されている場合、暗黙的に値渡しされます。
次の例では、$f1$f2は同じ動作になります。

$y = 1;

$fn1 = fn($x) => $x + $y;

$fn2 = function ($x) use ($y) {
    return $x + $y;
};

ネストもできます。

$z = 1;
$fn = fn($x) => fn($y) => $x * $y + $z;

外側の関数fn($x)は暗黙的に変数$zを取り込みます。
内側の関数fn($y)も暗黙的に変数$zを取り込みます。
全体として、$zは内側の関数からでも利用可能です。

Function signatures

アロー構文では、引数と戻り値の型、デフォルト値、可変長引数、リファレンスなど、既存の関数と同じ機能を使用できます。
以下は全て有効なアロー関数の構文です。

    fn(array $x) => $x;
    fn(): int => $x;
    fn($x = 42) => $x;
    fn(&$x) => $x;
    fn&($x) => $x;
    fn($x, ...$rest) => $rest;

$this binding and static arrow functions

通常のクロージャ同様に、クラスメソッド中では$this、遅延静的束縛が自動的にバインドされます。
通常のクロージャではstaticキーワードでこれを打ち消すことができるため、アロー関数でも同じ機能をサポートしています。

class Test {
    public function method() {
        $fn = fn() => var_dump($this);
        $fn(); // object(Test)#1 { ... }

        $fn = static fn() => var_dump($this);
        $fn(); // Error: Using $this when not in object context
    }
}

staticクロージャは滅多に使用されません。
これは主に、GCの発生を予測しづらくする$thisのバインドを防ぐために使用されます。
しかし、ほとんどのコードはこのようなことを意識する必要はありません。

この動作について、実際にクロージャ内部で$thisが使用されている場合のみ$thisをバインドするように実装することは可能です。
GCを置いておいても、この変更によって動作が変わることはありません。

残念ながら、PHPは暗黙のうちに$thisを使用することがあります。
たとえば、Fooと互換性のあるスコープからFoo::bar()を呼び出した場合、$thisが継承されます。
我々は内部仕様を知っているため$thisがどうなるか分析できますが、ユーザにとっては$thisがどうなるかは予測不可能です。
そのため、常に$thisを使用する通常のクロージャを使うことを勧めます。

By-value variable binding

既に述べましたが、アロー関数は変数束縛を値渡しで行います。
これは、関数内で使用している全ての変数にuseをすることとほぼ同じです。
従って、値渡しバインディングの値をスコープ内から変更することはできないということになります。

    $x = 1;
    $fn = fn() => $x++; // スコープ外には影響しない
    $fn();
    var_dump($x); // 1

バインディング方法の検討については、discussionセクションを参照してください。

変数の暗黙的な使用と明示的な使用には僅かな違いがあります。
暗黙的な使用では、変数未定義のE_NOTICEは、変数バインド時には発生しません。
すなわち、以下のコードで発生するエラーは、$undefをバインドするときと使用するときの2カ所ではなく、使用するときの一カ所だけです。

    $fn = fn() => $undef;
    $fn();

その理由としては、変数が読み取りのために使用されているのか、参照返り値のために使用されているのかを常に判断することができないからです。
次にやや人為的な例を示します。

$fn = fn($str) => preg_match($regex, $str, $matches) && ($matches[1] % 7 == 0)

この$matchesはpreg_matchによって代入されるので、アロー関数より前に$matchesが存在している必要はありません。
この場合はエラーを出力すべきではありません。

最後に、この自動バインドは文字列リテラルで渡された変数にのみ行われます。
すなわち、以下のコードでは関数内で$xというリテラルが使用されていないため、$xが未定義であるというE_NOTICEが発生します。

    $x = 42;
    $y = 'x';
    $fn = fn() => $$y;

この問題は、使われている変数だけをバインドするのではなく、全ての変数を束縛することによって解消することができます。
不要だと思われるためここでは対応していませんが、それが必要だという声が大きければ変更されるかもしれません。

Precedence

=>の優先順位は最も低くなります。
すなわち、=>の右側は全て=>の中になります。

    fn($x) => $x + $y;

    // ↑と同じ
    fn($x) => ($x + $y);

    // こうではない
    (fn($x) => $x) + $y;

Backward Incompatible Changes

残念ながら、fnはトップレベルのキーワードでなければならず、そしてfnは予約済ではありません。

Ilija ToviloがGitHubのトップ1000リポジトリを調査してfnの使用を調査しました。
概要として、fnの使用は全てテスト内であるか、名前空間内での使用でした。

Examples

以下の例は旧RFCからのコピペです。

silexphp/Pimple

// 現在
$extended = function ($c) use ($callable, $factory) {
    return $callable($factory($c), $c);
};

// アロー関数
$extended = fn($c) => $callable($factory($c), $c);

定型文が44文字から8文字に減りました。

Doctrine/DBAL

// 現在
$this->existingSchemaPaths = array_filter($paths, function ($v) use ($names) {
    return in_array($v, $names);
});

// アロー関数
$this->existingSchemaPaths = array_filter($paths, fn($v) => in_array($v, $names));

31文字から8文字に減りました。

多くのライブラリで見られる関数も、以下のように書けるようになります。

// 現在
function complement(callable $f) {
    return function (...$args) use ($f) {
        return !$f(...$args);
    };
}

// アロー関数
function complement(callable $f) {
    return fn(...$args) => !$f(...$args);
}

// 現在
$result = Collection::from([1, 2])
    ->map(function ($v) {
        return $v * 2;
    })
    ->reduce(function ($tmp, $v) {
        return $tmp + $v;
    }, 0);

echo $result; // 6

// アロー関数
$result = Collection::from([1, 2])
    ->map(fn($v) => $v * 2)
    ->reduce(fn($tmp, $v) => $tmp + $v, 0);

echo $result; // 6

Discussion

Syntax

おそらく最も望ましいアロー関数の構文は($x) => $x * $y、もしくは$x => $x * $yです。
非常に簡潔で、JavaScriptなどの多くの言語で使用されています。
しかしPHPにおいてこの構文は問題が多く、使用することは困難です。
このセクションではアロー関数のために考慮されたいくつかの構文について、利点と欠点を解説します。

($x) => $x * $y

最も一般的であり、そしてPHPでは不可能な構文です。
この構文最大の問題が、配列やyieldなどにおいてキーと値のペアを指定する目的で=>が既に使用されていることです。
従って、以下のコードの=>は配列宣言なのか、アロー関数なのかを特定することができません。

$array = [
    $a => $a + $b,
    $x => $x * $y,
];

しかし、この曖昧さ自体は問題ではありません。
式の構文は既に曖昧さに満ちていますが、優先順位、結合度、あるいはその他の規則によって一意に解決できます。
今回の場合は、後方互換性を保つため、上記の構文は配列宣言であり、アロー関数は以下のようにする、と定義すれば一意にすることはできます。

$array = [
    ($a => $a + $b),
    ($x => $x * $y),
];

同じ問題がyieldにも存在します。

yield $foo => $bar; // 配列
yield ($foo => $bar); // アロー関数

実はアロー関数がなかったとしても、既に解釈が曖昧になる構文は存在します。

$array = [yield $k => $v];
// こうなる
$array = [(yield $k => $v)];
// こう解釈することもできる
$array = [(yield $k) => $v];

$x => $yには、さらに次のセクションで解説する問題もあり、そして実はそちらが致命傷です。

($x) ==> $x * $y

Hackが使用している($x) ==> $x * $y、以前のRFCで提出された($x) ~> $x * $y、あるいは類似の構文についてです。
($x, $y) ==> $x + $yのような単純な構文であればサポートも可能ですが、==>の左側に任意の構造を記述できるようにすると、パーサの実装が非常に困難になります。

($x = [42] + ["foobar"]) ==> $x; // 代入式
(Type &$x) ==> $x;               // 定数とビット積

$a ? ($b): Type ==> $c : $d;

($a = ($a = ($a = ($a = ($a = 42) ))))
($a = ($a = ($a = ($a = ($a = 42) ==> $a))))

以下構文のパース処理がとってもたいへんで、解決するにはパーサを入れ替えるレベルの改修が必要だ的な記述が延々書かれているのですが、細かすぎてよくわからないので省略。

fn($x) => $x * $y

上記の構文候補に関する最大の問題は、(の対となる==>が見つかるまではアロー関数であるかそうでないかを調べ続けなければならない、ということでした。
明確な解決策は、特有の先行記号を持つように構文を修正することです。
このRFCでは、短くて読みやすい候補としてfnを提案しています。
問題点は、fnが予約語ではないことです。

先行記号のシンボルについては、もちろん他の候補もあります。
特に未使用の単項演算子というパンドラの箱を開けたら。

function($x) => $x * $y
fn($x) => $x * $y
\($x) => $x * $y
^($x) => $x * $y

*($x) => $x * $y
$($x) => $x * $y
%($x) => $x * $y
&($x) => $x * $y
=($x)=> $x * $y

// NG PHPとして正しい構文である
!($x) => $x * $y
+($x) => $x * $y
-($x) => $x * $y
~($x) => $x * $y
@($x) => $x * $y

// NG _は正しい関数名である
_($x) => $x * $y

実際に実現可能であろう文法は最初の4例でしょう。

fnは今回提案されているものです。

fucntionは既にキーワードとして使用されているので安全です。
不利な点はキーワードが長いということで、そしてアロー関数のセールスポイントのひとつが短いということです。

\($x) => $x * $yはHaskellのラムダ構文と類似の例で\はプアマンズλです。

^はC言語でサポートされています。

先頭に記号が付いた構文を使うとなれば、=>も邪魔なので消したくなります。
fn($x) ⇒ $x * $yのかわりにfn($x) $x * $yとは書けないでしょうか?
残念ながら、返り値が曖昧になるためこれは不可能です。

    fn($x): T \T \T
    // こう?
    fn($x): T\T (\T)
    // それともこう?
    fn($x): T (\T\T)

名前空間の空白サポートをやめ、名前空間は単一トークンと字句解析すれば、この曖昧さは解決可能です。
しかし、それは破壊的変更になってしまいます。

Using -> and --> as arrows

=>のかわりに->-->を使おうという提案がありました。
しかしこれらは別の既存構文と衝突します。
->はプロパティアクセスで既に使用されています。

($x) -> $x
// 正しい構文。↓と同じ
$x->{$x}

-->-->の組み合わせです。

$x --> $x
// 正しい構文。↓と同じ
$x-- > $x

必ず括弧を使うと定義した場合、-->は使用可能です。
($x)--は現在のところ正しい構文ではないからです。

いずれの記号も先行記号と組み合わせれば使用可能ですが、そうするのであれば=>でも同じことです。

Different parameter list separators

Rustなどいくつかの言語は、クロージャのパラメータに異なる種類のセパレータを使います。

|$x| => $x * $y

|は有効な単項演算子ではないため、先行記号と同じ区別に使用できます。
しかし残念ながら、|はUnion typesやビット演算子、デフォルト値などで使用されています。

|T1|T2 $x = A|B| => $x

構文上の曖昧さはないと思われますが、意味を読み取るのは困難です。
また|$x|のような構文はPHPとしては異質でしょう。

Block-based syntax

これまでに解説したものとは全く異なるアプローチとして、RubyやSwiftで使用されているブロックベース構文があります。
考えられる構文は以下のようなものです。

{ ($x) => $x + $y }

先頭に{がありますが、PHPは独立ブロックとしての{の使用をサポートしているので、先行記号としては使えません。
以下は正当なPHPコードです。

{ ($x) + $y };

すなわち、構文解析の発散問題がやはり発生することを意味します。
ただし、この場合は簡単な回避策があります。
すなわち、式文としてのクロージャ構文を禁止することです。
単独のクロージャは許可されず、何らかの式の一部である必要があるということにします。

{ ($x) => $x + $y }; // NG
$fn = { ($x) => $x + $y }; // OK

これでブロックベース構文は汎用的に実行可能になります。
個人的には、これがfn()より優れているとは思えず、特にアロー関数をネストする場合はさらにややこしくなります。

fn($x) => fn($y) => $x * $y
{ ($x) => { ($y) => $x * $y } }

C++ syntax

C++11では、以下の構文でラムダを使用可能です。

cpp
[captures](params){body}

PHPの場合は[$x]($y)が既に有効な構文であるため、この構文は使用できません。

Miscellaneous

Haskellの構文に近い、パラメータを括弧で囲まない\param_list => expr構文も考えられました。
が、PHPではこの構文は曖昧です。

[\T &$x => $y]
// こう?
[\(T &$x) => $y)]
// それともこう?
[(\T & $x) => $y]

Binding behavior

アロー関数に関するもうひとつの議論点はバインディングです。

アロー関数は親スコープに存在する変数を自動的にバインドします。
問題はそのバインドをどのように動作させるべきかです。
基本的に3つの選択肢があり、それぞれを値・参照・変数によるバインディングとします。

$x = 1;
$fn = fn() => $x++;
$fn();
var_dump($x); // 値渡しなら1
              // 参照渡しなら2

値によるバインドはuse ($x)に、参照によるバインドはuse (&$x)に相当します。
参照バインディングの利点はアロー関数内で変数を変更できることです。

残念ながら、2つの大きな問題のため、参照バインディングが値バインディングより優れているとは言えません。
ひとつめは、参照バインディングのためには参照ラッパーの作成と参照が必要となるため、パフォーマンスが低下するということです。
アロー関数と通常クロージャを選ぶときの基準が性能差であるとなれば、それはとても残念なことです。

もうひとつ重要な問題が、クロージャの内側から外側の変数を変更することができるのと同時に、クロージャの内側の変数を外側から変更することもできるということです

次の例では、なぜこれが問題になるのか、暗黙の参照バインディングがいかに直感的でない結果になるのかを説明します。

$range = range(1, 5);
$fns = [];
foreach ($range as $i) {
    $fns[] = fn() => $i;
}
foreach ($fns as $fn) {
    echo $fn();
}
// 値:   1 2 3 4 5
// 参照: 5 5 5 5 5
// 変数: 5 5 5 5 5

アロー関数が値バインドである場合、全て正常に動作します。

参照バインドの場合、以下のように動作します。
最初のループでクロージャ内の$iがforeach内の$iへの参照にバインドされます。
2回目のループで、この参照先の値が上書きされ、さらに新しいクロージャで$iにバインドされます。
ループが終了した後、全てのクロージャはひとつの参照を共有します。
そして値は最後に割り当てられた値です。

今回は議論されておらず、PHPでは使用されていない3番目のバインドが、変数バインドです。
これこそが本当のスコープバインディングで、クロージャ外の変数とクロージャ内の変数は共有されています。
これは参照バインドと同じように見えますが、次の例が示すように全く同じではありません。

$range = range(1, 5);
$fns = [];
foreach ($range as &$i) { // &を追加
    $fns[] = fn() => $i;
}
foreach ($fns as $fn) {
    echo $fn();
}
// 値:   1 2 3 4 5
// 参照: 1 2 3 4 5
// 変数: 5 5 5 5 5

参照バインドをリファレンス付きでforeachすると動作が変わります。
参照によるforeachは、値の代入ではなく、参照の代入を実行します。
これにより、前ループの参照関係は解除されます。
すなわち、各クロージャは対応する配列要素を参照する個々の参照を取得することになります。

変数バインドを使用した場合、foreachの呼び出し方は問題になりません。
外側の$iとクロージャ内の$iは文字どおり完全に同じなので、クロージャが呼び出された時点の最終的な$iの値だけが意味を持ちます。

変数バインドはPHPでは実装が難しく、値バインドと同程度の性能を出すことは不可能かもしれません。

このような問題があるため、PHPで実装可能な唯一のバインディング形式は値バインドであると私は考えています。
ただし、特に下記のブロックベース構文がサポートされたような場合は、以下のように明示的に参照バインドを許可すると有用かもしれません。

$fn = fn() use(&) {
    // ...
};

Future Scope

将来は以下のように拡張される可能性がありますが、必ずしも推奨されるわけではありません。

Multi-statement bodies

このRFCでは、アロー関数は暗黙的に返される式をひとつだけしか持つことができません。
しかし、他の言語ではブロック形式で複数の式を受け入れるのが一般的です。

fn(params) {
    stmt1;
    stmt2;
    return expr;
}

この構文は優先価値が低いため、このRFCでは省略されています。
この構文をサポートする利点は、単一の式を使用するか複数のステートメントを使用するかによって2つの異なる構文を使い分けるのではなく、常に単一のクロージャ構文を使用できるようになることです。

Switching the binding mode

デフォルトでは値バインドを使用しますが、参照バインドもできるように構文を拡張することもできます。
複数ステートメントを使用する際の本文は、外側の変数を変更することに興味がある可能性が高いので、Multi-statement bodiesと組み合わせると特に役立つでしょう。
構文は以下のようになると思われます。

$a = 1;
$fn = fn() use(&) {
    $a++;
};
$fn();
var_dump($a); // int(2)

あるいは、基本的に値バインドを維持し、参照バインドしたい変数は明示することです。

$a = 1;
$b = 2;
$fn = fn() use(&$a) {
    $a += $b;
};
$fn();
var_dump($a); // int(3)

この例では$bは値バインドであり、$aは参照バインドとなります。
ただし、この構文は既存のクロージャ構文に近いため、混乱を招く可能性があります。
通常クロージャ構文では$bはバインドされません。

Allow arrow notation for real functions

通常の関数やメソッドにもアロー関数を使用できるようにするのもよいかもしれません。
getterのような単一定型文を減らすことができるでしょう。

class Test {
    private $foo;
    private $bar;

    fn getFoo() => $this->foo;
    fn getBar() => $this->bar;
}

対象バージョン

PHP7.4です。
7.4の新機能多すぎるんだけど大丈夫なのこれ。

投票

2019/04/17に投票開始、2019/05/01に投票終了。
有権者の2/3+1の賛成で受理されます。

2019/04/22時点では賛成36、反対7で、よほどの問題でも発生しないかぎりPHP7.4で導入されます。

感想

JavaScriptのアロー関数って個人的に好きじゃないんですよね。

JavaScript
const hoge = () => a++;

let a = 1;
hoge();
a; // 2 ←

普段スコープがー副作用がーとか言ってるわりに、どうしてこれが平気なのか理解に苦しむ。

PHPのアロー関数は上記のとおり値バインドであり、このような問題は発生しません。

PHP
$hoge = fn() => $a++;

$a = 1;
$hoge();
echo $a; // 1

しかし、そういう意思を持った上で値バインドを選んだ、というわけではなく単に実装上の理由というのは少々もにょる。

また、これまでのPHPの文法とはかけ離れた異質な構文なので、慣れないと扱いづらそうです。
特にPHPの場合、=>は既に別の構文で使用されています。
パーサによるパースに対しては=>を使っていても誤解が生まれないようになっているのですが、しかし人間によるパースが誤解しやすいことは間違いありません。
どうせ既存構文とは全く互換性がないのだから、人力パースにも優しい新たな演算子を設けてほしかったところですね。

あと全く関係ないのだけど、名前空間にスペース空けられるってのをこのRFCで初めて知ったよ。

namespace A                 \B;
    echo __NAMESPACE__; // A\B

【PHP8】PHPの三項演算子が他言語の実装に一歩近付く

$
0
0

Deprecate left-associative ternary operatorというRFCが投票に入っています。

提案者のNikitaは、最近アロー関数やらAlways generate fatal error for incompatible method signaturesやらConsistent type errors for internal functionsやら立て続けに凄い勢いで活躍しててすごい草生えてる
過去一年のcontributions2000超えって何なの…?

Deprecate left-associative ternary operator

Introduction

ほとんど(全て?)の他言語と異なり、PHPの三項演算子は右結合ではなく左結合です。
左結合の振る舞いは一般的に有用ではなく、複数言語を使い分けるプログラマにとって混乱の元になっています。
このRFCでは三項演算子の左結合性を廃止・削除し、かわりに括弧の明示的な使用を強制します。

例として以下のコードを上げます。

return $a == 1 ? 'one'
     : $a == 2 ? 'two'
     : $a == 3 ? 'three'
     : $a == 4 ? 'four'
               : 'other';

大抵の言語ではこのように解釈されます。

return $a == 1 ? 'one'
     : ($a == 2 ? 'two'
     : ($a == 3 ? 'three'
     : ($a == 4 ? 'four'
               : 'other')));

これは直感的であり便利な解釈です。
PHPではそうではなく次のようになります。

return ((($a == 1 ? 'one'
     : $a == 2) ? 'two'
     : $a == 3) ? 'three'
     : $a == 4) ? 'four'
               : 'other';

これは一般的に考えられるような動作ではありません。

Proposal

PHP7.4では、括弧を使わずに三項演算子のネストを使用すると非推奨の警告を表示します。
PHP8では、コンパイルエラーになります。

1 ? 2 : 3 ? 4 : 5;   // deprecated
(1 ? 2 : 3) ? 4 : 5; // ok
1 ? 2 : (3 ? 4 : 5); // ok

これはエルビス演算子においても同様です。

1 ?: 2 ? 3 : 4;   // deprecated
(1 ?: 2) ? 3 : 4; // ok
1 ?: (2 ? 3 : 4); // ok

1 ? 2 : 3 ?: 4;   // deprecated
(1 ? 2 : 3) ?: 4; // ok
1 ? 2 : (3 ?: 4); // ok

ただし例外として、エルビス演算子を2回重ねる場合は括弧が必要ありません。

1 ?: 2 ?: 3;   // ok
(1 ?: 2) ?: 3; // ok
1 ?: (2 ?: 3); // ok

なぜかというと、($a ?: $b) ?: $c$a ?: ($b ?: $c)は左結合であろうと右結合であろうと常に同じ結果になるからです。

三項演算子の中央を三項演算子にする場合も括弧は必要ありません。
これは解釈を間違えることがなく、結合性の影響を受けないからです。

1 ? 2 ? 3 : 4 : 5 // ok
1 ? 2 ?: 3 : 4    // ok

Null合体演算子??は既に右結合なので、このRFCによる影響はありません。

Backward Incompatible Changes

三項演算子の左結合性を悪用するコードは、PHP8ではエラーになります。
左結合の三項演算子を使っているようなコードはほぼ確実にバグなので、このRFCによる影響は最小限に抑えられます。

Future Scope

しばらくエラーにしておけば、いずれ正しい三項演算子にすることができます。

投票

2019/04/23に投票開始、2019/05/07に投票終了。
有権者の2/3+1の賛成で受理されます。

2019/04/25時点では賛成22、反対8で、賛成が優勢ですが、まだ簡単にひっくり返る程度の差です。
果たしてどうなることでしょうか。

感想

一気に他言語と同じ動作にしてしまうと、同じ書き方でもPHPのバージョンによって動作が完全に異なってしまい、間違いなく大混乱になるでしょう。
そのためいったん動かないようにして、順次移行するという形にしたようです。

三項演算子について調べたとき、PHPと同じ解釈をする言語がひとつだけあったと思うんだけど何だったっけ。
その言語にとっては遂に仲間が居なくなってしまいますね。

まあそもそも根本的に、三項演算子をネストするなって話ですが。
遊ぶと楽しいのは確かですが、会社でこんなコードを書いてきたらぶん殴られて当然です。

メールアドレスの長さ

$
0
0

DBのカラム定義をするときに、メールアドレスを入れるカラムの長さに迷ったので調べた。
varchar(255)で良さそう。

基本

メールアドレスの長さについては、RFC5321 4.5.3で規定されている。

https://tools.ietf.org/html/rfc5321#section-4.5.3

  • ローカル部の長さの最大値は64文字
  • ドメインの長さの最大値は253文字
  • メールアドレス全体の長さの最大値は254文字

他にもメールアドレスに関する規約とかがあればまた追記していきます。

NTP network time protocol

$
0
0

RFC5905 Network Time Protocol Version 4: Protocol and Algorithms Specification
https://tools.ietf.org/html/rfc5905

RFC7822 Network Time Protocol Version 4 (NTPv4) Extension Fields
https://tools.ietf.org/html/rfc7822

NTP(network time protocol)の記事に、いいね 0がちらほら。
当たり前だからという場合と、GUIで設定できるからという場合と、知らなかったという場合とがあるかも。
古い記事で200以上のいいね。その後変わっていない場合はいいねがつかないのかも。最新ソフトの紹介は400以上のいいね。

時刻に厳密な仕事をしているのでなければ、公開NTPサーバと同期していれば、困ることはない。

特定のソフトウェアのライセンスサーバは、利用者の時刻がずれていないか確認し、一定以上ずれている利用者には警告を発するか、起動しないものがある。また、将来の日付のファイルがあるシステムでは動作させないようにしているソフトウェアもある。これらは、公開NTPサーバと同期していれば問題ない。

科学技術の観測結果、遅延誤差の測定など、往復の遅延誤差が許容できない測定・実験では、複数経路の差、GPSなどの衛星からの時刻など、往路と復路の遅延誤差なども考慮することがある。

確認・設定(verification, setting)

Raspberry PiでNTPサーバーを立てる
https://qiita.com/msrks/items/8a816b32e513c43d248b

NTPメモ
https://qiita.com/sesame/items/f35c9e3eae9f9158ef04

centos7で 時刻がずれていた時の対処法
https://qiita.com/cheshi/items/6650adb024bea8b7daae

NTP設定メモ
https://qiita.com/nikofu501/items/1ef06ef7eb0758db65d4

CentOS 7 NTPの変更
https://qiita.com/knutpb1205/items/f5f73b7d34c5a05c47a7

CentOS7のNTP設定
https://qiita.com/yuki476/items/4b08e79022605198d9b9

Linuxサーバー時刻を(現在時刻と)合わせる
https://qiita.com/ntkgcj/items/0ef1c02665b3d9a3971d

【Debian 8 Jessie】時刻を同期する
https://qiita.com/osktak/items/d775555df0a88d23f021

FreeNAS tips - NTP
https://qiita.com/katz_engineer/items/c50abcdd44460e9502d3

Pythonで時刻同期(Windows)する
https://qiita.com/kon1127/items/dd6869ff092696702772

Python で ntp server から時刻を取得する (Python 3 版)
https://qiita.com/c-yan/items/90209045909e1f1c2ffb

Windowsでコマンドラインから時刻同期する
https://qiita.com/mo12ino/items/838b9b5e2149c4d61ec9

VMwareでの時刻同期方法
https://qiita.com/kite_999/items/1143e575de7e89a87ba1

Docker コンテナの時刻同期(時刻データ)について
https://qiita.com/hirotaka-tajiri/items/f5900b236a005d7ffe58

dockerコンテナ上の時刻同期について(NTP不使用)
https://qiita.com/Gin/items/9f22ce8639aaa9f656dc

閉じたネットワークでNTPで時刻同期する
https://qiita.com/rouge_pawn/items/e176243618ed1f9989de

片手落ちNTP設定
https://qiita.com/okiami123/items/99943699ece240b92c22

debianでNTPサーバ・クライアント
https://qiita.com/ryosec0311/items/d9f42f3fb4fe9909bae7

CentOSにntpサーバを入れて、日本標準時刻に自動的に合わせるためのメモ
https://qiita.com/tsu-nera/items/9be676b04b190e45b281

NTPの同期ズレを直す
https://qiita.com/ATS534/items/e9838b717fbefe5bfb6a

時刻同期 centos
https://qiita.com/tukiyo3/items/a8a1bd12b6159577320b

Chrony

raspbianでchrony(時刻同期編
https://qiita.com/ngkazu/items/916f476985fa3e3f2951

NTP クライアント Chrony
https://qiita.com/bezeklik/items/702cdcbeaa7ee473f546

新しいNTPクライアント&サーバ、chrony
https://qiita.com/yunano/items/7883cf295f91f4ef716b

時刻同期chronyの導入
https://qiita.com/mattsun/items/f2dc3519e1a628f81923

CentOS7の時刻(同期)設定 chrony
https://qiita.com/Pirlo/items/c4c23cc1ba2b1d3c0673

NTP server

NTPサーバ起動コマンド
https://qiita.com/taro0219/items/28cc709232bc7ef11e29

馬鹿の一つ覚え: UbuntuでNTPサーバ設定
https://qiita.com/glires/items/e942c71d3725440d48ef

CentOS7.3にNTPサーバを立てる
https://qiita.com/elu_jaune/items/32aba0acf89a143331c6

内部向けのntpdサーバの設定
https://qiita.com/hishi1008/items/8f0ac4647b55cbcb89bd

公開 NTP サーバー
https://qiita.com/bezeklik/items/6c2ebbc8da774a053704

【負荷OK】Public NTPサーバの一覧
https://qiita.com/halpas/items/5a451ead0b13f6792595

説明(explanation)

NTPざっくりまとめ
https://qiita.com/mogulla3/items/7559a7c0bdfa72515bdf

NTPのpeerの使い方
https://qiita.com/legitwhiz/items/80b9313d9e8024ee65c3

うるう秒

メモ: うるう秒の時刻同期メッセージ
https://qiita.com/tukiyo3/items/de7e9094f8a3ee314c9f

課題(issue)

ntpの2036年
https://qiita.com/yamori813/items/ca9169dd9c365e2fc5e0

未確認

Powershell で時刻同期無効
https://qiita.com/a-hiroyuki/items/384e2ccef349d01b748c

なんのために時刻同期を無効にするのかの記載がない。GUIでもできるし、、、。

LinuxPTP(Software Timestamping)で時刻同期
https://qiita.com/rp_pen/items/9e8658ae70ab09b7249a

文書履歴(document history)

ver. 0.01 初稿 20190504 午後
ver. 0.02 往復遅延追記 20190504 夕
ver. 0.03 追記 20190519

【PHP8】演算子.と+の優先順位が変わる

$
0
0

PHP7.3現在、演算子+-.の優先順位は同じです。
01.png

すなわち左から右に評価されます。

    echo 1 . 2 + 3 . 4;

    echo ((( 1 . 2 ) + 3 ) . 4 ) ; // これと同じ

マニュアルでもわざわざ例を挙げて解説しています。
02.png

さて2019年3月にChange the precedence of the concatenation operatorというRFCが提出されました
2019/05/07現在は投票中のステータスですが賛成多数で、PHP8で上記の動作は変わることになりそうです。
演算子の追加削除はよくあることですが、優先順位の変更というのは他言語含めてもなかなか見ることのないレアなイベントではないでしょうか。

Change the precedence of the concatenation operator

Introduction

+-、そして.は長年にわたる問題です。
それは左から右に解釈されます。

echo "sum: " . $a + $b;

// こう解釈される
echo ("sum: " . $a) + $b;

// こうではない
echo "sum :" . ($a + $b);

このRFCでは、この動作をより直感的に、問題が出にくくなるようにすることを目的としています。

Proposal

現在、+-.の各演算子は優先順位が同じです。
これらは、単純に左から右に評価されます。

これは直感に反します。
一般に、数字ではない文字列を足したり引いたりすることはほとんどありません。
PHPが整数を文字列にシームレスに変換できることを考えると、文字列連結が先に来ることが望ましいでしょう。

従って、このRFCでは.に、+-より低い優先順位を与えることを提案します。
具体的には、新しい優先順位は<<>>のすぐ下になります。

Backward Incompatible Changes

.の後に、括弧を使わず+-を使用している式全てが影響を受けます。
例として、"3" . "5" + 742ではなく"312"になります。

これは、予告や警告なしに出力が変更されるという点で微妙な動作変更ですが、コードを静的解析して、この問題が発生する箇所を全て見つけることは簡単にできます。
私の知るかぎり、この問題が発生することは稀であり、そして大抵は最初から間違っています。

NikitaがMLで言及したように、既存のOSSへの影響は事実上ありません。
見つかったものは全て単なるバグです。
つまり、全体的に影響は非常に小さいということになります。

Proposed PHP Version

PHP7.4でE_DEPRECATEDを発生させ、PHP8で動作を変更します。

投票

投票開始は2019/04/30、投票終了は2019/05/14です。
2019/05/07現在、PHP8で動作変更する提案は賛成23反対3で、ほぼ確実に受理されます。
PHP7.4でDeprecatedにする提案は賛成23反対4で、こちらもおそらく受理されます。

NikitaのML

またNikitaか。

Composerパッケージの上位2000件を調査したところ、影響を受けるコードは僅か5件しかありませんでした
しかも5件とも修正後の優先順位を想定したコードで、つまり現状ではバグっています。

$this->errors->add( 'unknown_upgrade_error_' . $errors_count + 1, $string );

例を一つあげると、上記コードはadd('unknown_upgrade_error_5', $string)のような文字列を与えたいのだと思われますが、実際はadd(1, $string)になります。

今回のRFCが通ると、想定していたであろう動作にエンバグすることになります。

感想

考えてみたら、むしろどうして今まで同じだったんだ、って感じですね。
優先順位が同じであるという仕様と、文字列と数値を自由に行き来できるという仕様が相まって、PHPで.+を同時に使った演算は、ぱっと見から予想できない結果になることがありました。
今回のRFCが通ると、そのあたりの動作がすっきりすることになります。

もっとも、そういったややこしい演算には普通は括弧を使っているから、実害は全くないはずですけどね。

ところでDeprecateにする提案に"The second (secondary) voting requires a 50%+1 majority."って書いてあるんだけど、50%の投票は廃棄されたんじゃなかったのか?

Litghttpd で localhost では繋がるのにホスト名を指定すると"400 Bad Request"

$
0
0

Litghttpd で 400 Bad Request エラー

docker-compose 内に立ち上げた Lighttpd の Web サーバーに、他のコンテナから http://sample_1/ とホスト名で curl すると 400 Bad Request エラーが返ってくる。

IP アドレス(http://172.23.0.4/)でも cURL できるし、ホスト名の sample_1 には PING は通る。

エラーの現象
$ # Localhost で取得 → OK
$ curl http://localhost/
<p>Hello World!</p>

$ # IPアドレス で取得 → OK
$ curl http://172.23.0.4/
<p>Hello World!</p>

$ # ホスト名で取得 → NG
$ curl http://sample_1/
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
 <head>
  <title>400 Bad Request</title>
 </head>
 <body>
  <h1>400 Bad Request</h1>
 </body>
</html>

$ # ホスト名宛に PING → OK
$ ping -c 3 sample_1
PING app2-php (172.23.0.4): 56 data bytes
64 bytes from 172.23.0.4: seq=0 ttl=64 time=4.026 ms
64 bytes from 172.23.0.4: seq=1 ttl=64 time=0.206 ms
64 bytes from 172.23.0.4: seq=2 ttl=64 time=0.203 ms

--- sample_1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.203/1.478/4.026 ms

TL;DR

ホスト名として利用できない文字が使われています。RFC2616 および RFC2396 で定義されている文字である必要があります。

ドットのないホスト名(トップレベル)は [a-zA-Z0-9-] のみであるため、この場合 _(アンダースコア/アンダーバー)が使われていることが原因です。

RFC2396
toplabel = alpha | alpha *( alphanum | "-" ) alphanum

TS;DR

docker-compose で、Hello World タグを返すだけのコンテナ名に sample_1 と付けて、他のコンテナから cURL したところ取得できなくなりました。

docker-compose は、コンテナ名(container_name ディレクティブ)を指定すると、それをホスト名としても割り当ててくれます。

lighttpd は起動時にホスト名のチェックを行うらしく、RFC に準拠していないホスト名によるアクセスは CGI に渡さないようです。

コンテナ名(ホスト名)のアンダーバーをハイフンに変え sample-1 に変更すると、他のコンテナからも HTTP リクエストのレスポンスをするようになりました。

こんなことあるのか。。。

参考文献

RFC2616

RFC2616より抜粋
14.23 Host
   Host = "Host" ":" host [ ":" port ] ;

3.2.1 General Syntax
   RFC 2396 [42] (which replaces RFCs
   1738 [4] and RFC 1808 [11]). This specification adopts the
   definitions of "URI-reference", "absoluteURI", "relativeURI", "port",
   "host","abs_path", "rel_path", and "authority" from that
   specification.

RFC2396

RFC2396より抜粋
3.2.2. Server-based Naming Authority

      host          = hostname | IPv4address
      hostname      = *( domainlabel "." ) toplabel [ "." ]
      domainlabel   = alphanum | alphanum *( alphanum | "-" ) alphanum
      toplabel      = alpha | alpha *( alphanum | "-" ) alphanum

論文でRFC参照

$
0
0

インタネットの経路制御(routing)研究時、RFCを引用していた。

博論(端末間経路選択片方向遅延差測定方式)

参考文献RFC更新一覧
https://researchmap.jp/jox0eiafk-1826017/

渡辺尚、飯田登、澤井新

との共同研究が多数ある。

寺岡文男

:Mobilityまわりの通信規約の先駆者。研究上の目標。
 検討されていない部分を主題にした。

塚本昌彦

:RFCのSIPとの比較をするように研究会で助言。研究室でRFCを引用しても論文にならないのではという意見に対する反論。
 データリンク層での移動と、セッション層での移動とを比較することにより、ネットワーク層での移動の価値を明確化できた。

小林 武史

: BSD系を研究室に構築してもらった。カーネルソースのコンパイルを自分でできるようになった。
 Linux通信対抗試験としてBSDとの通信結果で確認できた。

斉藤直希

:Debianを研修室に構築してもらった。Linuxカーネルソースのコンパイルを自分でできるようになった。
 Mobile IPのDebianソースを、カーネルの版をパッチ当てで戻して、コンパイルして実験できた。

間瀬健一

:電子情報通信学会誌に投稿する際の、対抗論文として参照させていただいた。

萬代雅希

:とあるところに投稿してコメントがついたものの表題変更の助言をいただいた。

水野忠則

:博論の副査。博士号をいただいた時に記念写真を撮っていただいた。


【PHP7.4】__toString()が例外を吐けるようになる

$
0
0

プロパティの設定を必須にしたかったとしましょう。

class HOGE{
    public $var;
    public function __toString(){
        if(!$this->var){
            throw new \Exception('$var must set.');
        }
        return sprintf('$var is %1$s', $this->var);
    }
}

try{
    echo new HOGE();
}catch(\Exception $e){
    var_dump($e);
}

何の変哲も無いように見えるコードですが、これ動きません。
Fatal error: Method HOGE::__toString() must not throw an exception, caught ExceptionというFatalエラーを吐いて死にます。

実は__toString()メソッド内では例外を出すことができないのです。
他のあらゆるマジックメソッドにはこんな制限はないのに、__toString()だけそういうことになっています。
よくわかりませんね。

しかしまあ、そんなよくわからない制限なら要らないよね、ということでAllow throwing exceptions from __toStringのRFCが提出されました。

PHP RFC: Allow throwing exceptions from __toString()

Introduction

__toString()からの例外スローは禁止されており、致命的エラーになります。
これは__toString()から任意のコードを呼び出すことを困難にし、一般的なAPIとして使用することが難しくなります。
このRFCは、この制限を取り除くことを目的としています。

現在のような動作になっている理由は、PHPエンジンと標準ライブラリのあらゆるところで文字列変換が行われており、そして全ての箇所が例外を正しく処理できるようになっているわけではないからです。

技術的観点からすると、この制限は無駄でしかありません。
なぜならば、文字列変換中の例外は、回復可能なエラーを例外に変換するエラーハンドラによって引き起こされる可能性があるからです。

set_error_handler(function() {
    throw new Exception();
});

try {
    (string) new stdClass;
} catch (Exception $e) {
    echo "(string) threw an exception...\n";
}

実際、Symfonyは現在の制限を回避するためにこの抜け穴を使用しています。
残念ながら、これはPHP8で削除予定の$errcontextパラメータに依存しています。

Proposal

__toString()からの例外スローを許可します。
もう致命的なエラーは発生しません。

さらにPHP7のエラーポリシーに基づいて、could not be converted to stringおよび__toString() must return a string valueというrecoverable fatal errorを、適切なError exceptionに変更します。

Extension Guidelines

エクステンション開発者は、文字列変換からの例外を適切に処理するため、以下のガイドラインを考慮に入れてください。

zval_get_string()convert_to_string()および類似の関数は、例外を生成した場合でも文字列を生成します。この文字列を含めてください。必ずしも従う必要はありませんが、そうすることができます。
・文字列変換がエラーハンドラによって例外になった場合の結果は、オブジェクトから文字列への変換の場合は空文字列、配列の場合は"Array"という文字列になります。これは以前と同じ動作です。
・通常は(EG(exception))で例外がスローされたかどうかを確認すれば十分です。

zend_string *str = zval_get_string(val);
if (EG(exception)) {
    // リソース解放など
    return;
}

・失敗する可能性のある操作を行うヘルパーAPIが多数追加されます。

// zval_get_string()に似ているが、変換に失敗したらNULLを返す
zend_string *str = zval_try_get_string(val);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_string_release(str);

// zval_get_tmp_string()に似ているが、変換に失敗したらNULLを返す
zend_string *tmp, *str = zval_try_get_tmp_string(val, &tmp);
if (!str) {
    // リソース解放とか
    return;
}
// Main code.
zend_tmp_string_release(tmp);

// convert_to_string()に似ているが、変換に成功したか否かのbooleanを返す
if (!try_convert_to_string(val)) {
    // リソース解放とか
    return;
}
// Main code.

try_convert_to_string()は、変換失敗時には元の値を返しません。そのため、これを使った方が、convert_to_string()と例外チェックを使うより安全です。
・あらゆる文字列変換操作にチェックを行うことができますが、チェックを省略しても通常は余剰な警告が発生するだけです。気をつけて実装しなければならないのは、主にデータベースのような、永続的な構造を変更する操作です。

Backward Incompatible Changes

recoverable fatal errorからError exceptionへの変更は、破壊的変更です。

Vote

投票開始は2019/05/22、終了が2019/06/05、投票数の2/3+1の賛成票で受理されます。

2019/05/27現在、賛成32反対0で、ほぼ確実に導入決定です。

感想

これによって、あらゆるメソッドから例外を出すことができるようになりました。

さて、なんでこんな簡単そうな変更が今まで残っていたの?
かというと、変更が簡単ではないからです。

最初に報告があったのはなんと2011年1月であり、2012年にはあのNikitaが実装が大変なんじゃよーと言っています。
しかし結局全てのcommitがNikita自身の手によって行われました。
最終的な修正は111ファイル1000行以上に及びます。
どんだけ働いてんだこの人

PHPフォーラムは基本的に保守的な人が多く、破壊的な変更はあまり受理されない傾向にあるのですが、Nikitaについては既に、Nikitaが言うなら仕方ないというレベルに達しているようです。
三項演算子のDeprecateとか、他の人が提案してたら絶対通らなかったよね。

しかしこれ、てっきりuninitialized__toString()したときのために作ったものなのかと思ったのですが、特に関係なかったみたいですね。

そして修正規模のわりに、あまり重要な使いどころが思いつかないというか、そもそも個人的には__toString()自体まず使うことがないですね。
デバッグにはvar_dump()という超絶便利関数がありますし、文字列化したいなら適当にメソッド生やします。

【PHP7.4】数値セパレータが書けるようになる

$
0
0
echo 2305843008139952128 === 230584308139952128; // true?false?
echo 10000000000000000; // 0が何個?

わかりにくいですね。

ということでどうにかしようというRFCが提出されました
以下はNumeric Literal Separatorの日本語訳です。

Numeric Literal Separator

Introduction

人間の目は、一続きの長い数値を素早く解析するために最適化されてはいません。
従って、視覚的な区切り文字がなければ、コードの読み取りとデバッグに時間がかかり、読み取りミスを招く可能性があります。

1000000000;   // 1億?10億?100億?
107925284.88; // オーダーはいくつ?

区切り文字のない数値リテラルはさらに、財務情報がセントなのかドルなのかなど、追加情報を伝えることができません。

$discount = 13500; // 13500ドル?13500セント?

.

Proposal

数値リテラルでアンダースコアをサポートし、数値を視覚的にグループ化することによって、コードの読みやすさを向上させます。

$threshold = 1_000_000_000;  // 10億!
$testValue = 107_925_284.88; // 1億オーダー
$discount = 135_00;          // $135

アンダースコア区切り文字は、PHPがサポートしているあらゆる数値リテラルで使用可能です。

6.674_083e-11; // 指数表記
299_792_458;   // 整数
0xCAFE_F00D;   // 16進数
0b0101_1111;   // 2進数
0137_041;      // 8進数

Restrictions

唯一の制限は、アンダースコアは数値に挟まれていなければならないということです。
つまり、以下のような書式は有効ではありません。

_100; // 定数として有効な書式

100_;       // 末尾は×
1__1;       // 連続は×
1_.0; 1._0; // 小数点前後は×
0x_123;     // xの次なので×
0b_101;     // bの次なので×
1_e2; 1e_2; // eの前後なので×

Unaffected PHP Functionality

数値リテラルの間にアンダースコアを追加しても、その値は変わりません。
字句解析の時点でアンダースコアは取り除かれるため、ランタイムには影響しません。

var_dump(1_000_000); // int(1000000)

Backward Incompatible Changes

後方互換性を壊す変更はありません。

Discussion

Use cases

使用を推奨する例

数値区切り文字は、数値の視覚的認知を可能にします。
すなわち、数値が現れたときに桁数を数える必要もなく、正確に間違いなく桁数が一目でわかるようになります。
これによって、4桁を超える数字を正しく読み取るためにかかる時間が大幅に短縮されます。

大きな数値リテラルは、ビジネスロジック定数、単体テスト値、データの変換などによく使用されます。

たとえばComposerがファイルを削除する際の再実行遅延は以下です。

usleep(350000); // without separator
usleep(350_000); // with separator

Active DirectoryのタイムスタンプからUNIXタイムスタンプへの変換。

$time = (int) ($adTime / 10000000 - 11644473600); // without separator
$time = (int) ($adTime / 10_000_000 - 11_644_473_600); // with separator

物理定数。

const ASTRONOMICAL_UNIT = 149597870700; // without separator
const ASTRONOMICAL_UNIT = 149_597_870_700; // with separator

2進数リテラル、16進数リテラルのバイト区切り。

0b01010100011010000110010101101111; // without separator
0b01010100_01101000_01100101_01101111; // with separator

0x42726F776E; // without separator
0x42_72_6F_77_6E; // with separator

Use cases to avoid

使用を避けるべき例

電話番号、クレジットカード番号、社会保障番号などのデータを格納するためにセパレータの使用が魅力的に映るかもしれません。
これらの値は数値のようも見えるからです。
しかし、それはほぼ常に悪い考え方です。

経験則として、これらの値に数値演算をする意味はありません。
これらの値を格納するための最良の方法は、整数ではありません。

// 使ってはいけない
$phoneNumber = 345_6789;
$creditCard = 231_6547_9081_2543;
$socialSecurity = 111_11_1111;

Will it be harder to search for numbers?

数値の検索が難しくなるのでは?

懸念となるのは、同じ値を複数の方法で記述することができるため、数値の検索が困難になることです。

しかし、これは既に現在起こっていることです。
同じ値を2進数、8進数、10進数、16進数、指数表記などで書くことが可能です。
実際には、フォーマットが一貫しているかぎり問題は起こりません。

逆に数値を見つけやすくなることもあります。
上で出ていた例を挙げると、検索時に13_500135_00を区別することが可能になります。
また16進数リテラルをバイト区切りにすることで、_6F_のように特定のバイトを含む数値だけを見つけることが可能になります。

Should it be the role of an IDE to group digits?

数値のグループ化はIDEの役目?

IDEは大きな数値を自動的に3桁区切りにフォーマットすることは可能ですが、区切りは必ずしも必要というわけではありません。

数値を同じ方法でグループ化することが常に望ましいわけではありません。
たとえば、財務数量がセント単位であるかドル単位であるかによって、異なる方法で表すことができます。

$total = 100_500_00; // $100,500.00
$total = 10_050_000; // $10,050,000

2進数リテラル・16進数リテラルは、nibbles/bytes/wordsなどの使用用途によって、様々な桁数でグループ化することができます。

IDEは、プログラマによるこれらの意図を読み取って自動整形することはできないでしょう。

Why resurrect this proposal?

何故このRFCを復活させた?

かつてのRFCは3年以上前(2016年1月)に投票があり、過半数の賛成があったものの2/3には足りず採用されませんでした。

当時の議論を見返したところ、このRFCは良いユースケースが提示されておらず、また投票期間も一週間と短かったため、多くの賛同は得られませんでした。

そのとき以来、数値リテラルのアンダースコア区切りは、多くの言語(Python、JavaScript、TypeScript等)で実装されており、以前より有用なユースケースをこの機能に対して適用できます。

Should I vote for this feature?

この機能に投票する必要はありますか?

Andrea Fauldsが要点をまとめました

この機能はいくつかの利点があります。
それほど複雑な機能ではありません。
新しい構文やトークンはありません。既存の数値トークンの形式を変更するだけです。
既存の全ての数値リテラルに適用され、よく適合します。
他言語で確立された慣習に従います。
見た目は明らかに定数や識別子ではなく数値であることが一目でわかり、混乱の可能性は高くありません。
濫用には制限があります(先頭、末尾、連続はできません)
使用法は直感的です。

Comparison to other languages

他言語での実装例。

Ada 単独、数値間
C# 連続、数値間
C++ 単独、数値間、シングルクオート
Java 連続可、数値間
JavaScript 単独、数値間
Julia 単独、数値間
Kotlin 連続可、数値間
Perl 単独、数値間
Python 単独、数値間
Ruby 単独、数値間
Rust 連続可、どこでも可
Swift 連続可、数値間

Vote

2019/05/30投票開始、2019/06/13投票終了、可決には2/3+1の賛成が必要です。
2019/06/10現在は賛成30反対11で、おそらく受理されます。

感想

本文にもあるように、かつて却下されたRFCのリバイバルです。

当時は保守的傾向が強かったため却下されましたが、今回はユースケースをしっかり記載してきたこと、最近は(主にNikitaの働きにより)革新要素も取り込みやすくなりつつあること、そのNikitaが賛成していること、そしてJavaScriptなどでも導入されそうなことなどにより、PHPでも受け入れられそうです。

日本語だと数値は4桁区切りなので、3桁区切りの数値が入ってきても、結局桁数把握に時間がかかってしまいますね。
思考回路が英語の人であればシームレスに把握できるのでしょうが、私は完全に日本語脳なので、そこまで多大な恩恵を得ることはできないと思われます。
もっとも、一切区切りがない状態に比べたらはるかに良いでしょうけど。

Robots.txtの標準化仕様が(今さら)提出される

$
0
0

みんな知ってるし使ってるRobots.txtですが、実は今まで標準仕様というものが存在せず、みんななんとなくで運用されてきました。
しかしこれではさすがに困るよってことで、初出から25年経った今になってついにというかようやくというか、RFCが提出されました。
ちなみにRFCの最初に書かれてるMartijn Kosterは、Robots.txtを最初に考えた人です。

ということで以下は対象のRFC、Robots Exclusion Protocolの日本語訳です。

Robots Exclusion Protocol

Abstract

この文書は、Martijn Kosterが1996年に定義したRobots Exclusion Protocolを標準化し、拡張したものです。
サービス所有者が自分のコンテンツに対し、クローラと呼ばれる自動アクセスクライアントからのアクセスを制御する方法を提供します。

1. Introduction

この文書は、RFC3986で定義されているURIによってアクセスできるリソースを提供するサービスにアクセスするクライアントに対して適用されます。
たとえばHTTPは、ブラウザがWebページのコンテンツ表示するクライアントです。

クローラは自動化されたクライアントです。
たとえば検索エンジンは、RFC8288で定義されているように、索引付けのため再帰的にリンクを辿るクローラを所有しています。

クローラがURI空間にアクセスすると、サービス所有者にとっては不便になる可能性があります。
このドキュメントは、クローラがURIにアクセスするときに従わなければならない(MUST)規則を定義します。

この規則は、認可形式ではありません。

1.1. Terminology

キーワード"MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"NOT RECOMMENDED"、"MAY"、"OPTIONAL"はBCP 14RFC2119RFC8174としての意味で解釈されます。

2. Specification

2.1. Protocol definition

このプロトコル言語は、ルールとグループで構成されます。

Rule:クローラがURIにアクセスする方法を定義する、キーと値のペアで構成される行。The Allow and Disallow linesを参照のこと。

Group:1つ以上のRule行が後に続く、ユーザエージェントを記載した行。グループは、ユーザエージェント行もしくはファイル終端で区切られます。"User-agent line"を参照のこと。最後のグループはルールを含まなくてもかまわず、その場合は暗黙的に全てを許可します。

2.2. Formal syntax

以下の書式はRFC5234で定義される拡張バッカス・ナウア記法(ABNF)で表記されます。

robotstxt = *(group / emptyline)
group = startgroupline                ; グループをユーザエージェント行で開始
       *(startgroupline / emptyline) ; ユーザエージェント行を複数行配置可能
                                     ; 
       *(rule / emptyline)           ; そのUAに対するルール
                                     ; 

startgroupline = *WS "user-agent" *WS ":" *WS product-token EOL

rule = *WS ("allow" / "disallow") *WS ":"
      *WS (path-pattern / empty-pattern) EOL

; 任意の追加行を登録可能。
; パーサは理解できないところは寛容に読み飛ばすこと。

product-token = identifier / "*"
path-pattern = "/" *(UTF8-char-noctl) ; 有効なURIパターン
empty-pattern = *WS

identifier = 1*(%x2d / %x41-5a / %x5f / %x61-7a)
comment = "#"*(UTF8-char-noctl / WS / "#")
emptyline = EOL EOL = *WS [comment] NL ; 末尾コメントをつけてもいい(MAY)
NL = %x0D / %x0A / %x0D.0A
WS = %x20 / %x09

; RFC3629で定義されたUTF8のうち、制御文字以外を使用可能

UTF8-char-noctl = UTF8-1-noctl / UTF8-2 / UTF8-3 / UTF8-4
UTF8-1-noctl = %x21 / %x22 / %x24-7F ; Ctrl、スペース、#
UTF8-2 = %xC2-DF UTF8-tail
UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
        %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
        %xF4 %x80-8F 2( UTF8-tail )

UTF8-tail = %x80-BF

2.2.1. The user-agent line

クローラは自分の所属するグループを表すために製品トークンを設定します。
製品トークンに使用できる文字は"a-zA-Z_-"だけ(MUST)です。
クローラは、サービスを使用する際に識別用に製品トークンを送信しなければなりません(SHOULD)。
たとえばHTTPの場合、製品トークンはuser-agentヘッダに含める必要があります(SHOULD)。

HTTP header robots.txtのuser-agent line
user-agent: Mozilla/5.0 (compatible;ExampleBot/0.1;https://www.example.com/bot.html) user-agent:ExampleBot

クローラは製品トークンが一致するグループを見つけ、そしてそのグループのルールに従わなければなりません(MUST)。
ユーザエージェントに合致するグループが複数ある場合、ルールをひとつにまとめなければなりません(MUST)。
このマッチングにおいて大文字小文字は区別されません(MUST)。
一致するグループが存在しない場合、クローラは"*"のユーザエージェントで指定された最初のグループに従わなければなりません(MUST)。
いずれの条件にも当てはまらなかった場合、あるいはグループが全く存在しなかった場合、ルールは何もありません。

2.2.2. The Allow and Disallow lines

ルールの行は、対応するパスと一致するURIへのアクセスが許可されているか否かを示します。
URIへのアクセスが許可されていることを確認するために、ロボットはURIが許可リストもしくは拒否リストに入っていないかチェックします(MUST)。
このマッチングにおいて大文字小文字は区別されます(SHOULD)。
見つかったうちで最も正確に一致した対象を使わなければなりません(MUST)。

同じ詳細度で許可と不許可が両方ある場合、許可されます(SHOULD)。
グループ内に一致するルールが見つからない、あるいはグループ内にルールがない場合、URIは許可されます。
/robots.txtは暗黙的に許可されます。

robots.txtのパスは、US-ASCIIコードの範囲外の文字列はRFC3986で定義されているパーセントエンコードでなければなりません(MUST)。

US-ASCIIコードの文字列が見つかった場合、それがRFC3986で予約されていないかぎり、比較する前にエンコードしてはいけません(MUST)。
差異が見つかる前に行の終端に達した場合にのみ、その行に一致したと見做されます。

以下は例です。

Path エンコード表記 マッチング文字列
/foo/bar?baz=quz /foo/bar?baz=quz /foo/bar?baz=quz
/foo/bar?baz=http://foo.bar /foo/bar?baz=http%3A%2F%2Ffoo.bar /foo/bar?baz=http%3A%2F%2Ffoo.bar
/foo/bar/U+E38384 /foo/bar/%E3%83%84 /foo/bar/%E3%83%84
/foo/bar/%E3%83%84 /foo/bar/%E3%83%84 /foo/bar/%E3%83%84
/foo/bar/%62%61%7A /foo/bar/%62%61%7A /foo/bar/baz

最初のユーザエージェント行の前にあるルールなど、どのグループにも属していない規則については、クローラは無視します(SHOULD)。

2.2.3. Special characters

クローラは、以下の特殊文字を解釈する必要があります(SHOULD)。

Character 解説
# コメント allow: / # ここはコメント
#この行は全てコメント
$ 行末を表す allow:/this/path/exactly$
* 0文字以上の文字列 allow:/this/*/exactly

URIが特殊文字と一致する場合、クローラはパーセントエンコードを使用すべきです(SHOULD)。

パターン URI
/path/file-with-a-%2A.html https://www.example.com/path/file-with-a-*.html
/path/foo-%24 https://www.example.com/path/foo-$

2.2.4. Other records

クライアントは、robots.txtプロトコルに含まれないレコードを解釈してもかまいません(MAY)。
たとえばsitemapなどがあります。

2.3. Access method

robots.txtはサービスの最上位パスに配置し、全て小文字の"/robots.txt"でアクセス可能でなければなりません(MUST)。
文字コードはRFC3629で定義されているUTF-8でなければなりません(MUST)。
メディアタイプはRFC2046で定義されている"text/plain"でなければなりません(MUST)。

RFC3986に従うと、robots.txtのURIは以下のようになります。

"scheme:[//authority]/robots.txt"

たとえば、HTTP、FTPでのURIは以下のとおりです。

http://www.example.com/robots.txt
https://www.example.com/robots.txt
ftp://ftp.example.com/robots.txt

2.3.1. Access results

クローラがrobots.txtを正常にダウンロードできた場合、クローラは規則に従わなければなりません(MUST)。

robots.txtへのリクエストに対し、サーバはHTTP 301 / 302リダイレクトを返すことがあります。
クローラは、RFC1945で定義されているとおり、最低5回はリダイレクト要求に従う必要があります(SHOULD)。

5回以内にrobots.txtに到達できた場合は、そのファイルを解析し、その規則に従わなければなりません(MUST)。
5回で到達できなかった場合、クローラはrobots.txtを使用できないと解釈してもかまいません(MAY)。

robots.txtが利用不能である場合もあります。
たとえばHTTPでは、400から499のステータスコードは利用不能を表します。
robots.txtに対して利用不能であるステータスコードが帰ってきた場合、クローラはあらゆるファイルにアクセス可能(MAY)です。
または24時間以内のキャッシュがあればそれを使用することもできます(MAY)。

サーバエラーやネットワークエラーによりrobots.txtにアクセスできない場合、クローラは完全に不許可であると想定しなければなりません(MUST)。
たとえばHTTPでは、500から599のステータスコードは到達不能を表します。
それ以外の未定義のステータスコードである場合も、クローラはrobots.txtが到達不能であると想定しなければなりません(MUST)。
かなりの長期間(たとえば30日)robots.txtに到達不能である場合、クライアントはrobots.txtが到達不能であると扱ってもよい(MAY)し、キャッシュを使い続けることも可能です(MAY)。

パースエラーがある場合でも、クローラはrobots.txtを行ごとに解析しなければなりません(SHOULD)。
クローラは解析できた規則は使用しなければなりません(MUST)。

2.4. Caching

クローラは、取得したrobots.txtをキャッシュすることができます(MAY)。
RFC2616で定義されている標準のキャッシュ制御を使用できます(MAY)。
robots.txtが到達不能である場合を除き、24時間を超えてキャッシュを使用することはできません(SHOULD NOT)。

2.5. Limits

クローラは、少なくとも500KiBのrobots.txtに対応しなければなりません(MUST)。

2.6. Security Considerations

Robots Exclusion Protocolを、セキュリティ対策として使用してはいけません(MUST NOT)。
robots.txtにURIを載せると、そのURIは公開されるため、検出が可能になります。

2.7. IANA Considerations

IANAのアクションは特にありません。

3. Examples

3.1. Simple example

以下に簡潔な例を示します。

・foobot:一般的なケースです。単独のユーザエージェントトークンと、それに続くルール。
・barbot/bazbot:ひとつ以上のユーザエージェントをグループ化可能。
・quxbot:ファイルの終わりには空の規則を指定可能。

User-Agent : foobot
Disallow : /example/page.html
Disallow : /example/disallowed.gif

User-Agent : barbot
User-Agent : bazbot
Allow : /example/page.html
Disallow : /example/disallowed.gif

User-Agent: quxbot

3.2. Longest Match

2つの規則にひっかかる場合、最も長くマッチするものを使用しなければなりません(MUST)。
以下の例では、example.com/example/page/disallow.gifにアクセスしてきた場合は、/example/page/disallowed.gifの規則が適用されます(MUST)。

User-Agent : foobot
Allow : /example/page/
Disallow : /example/page/disallowed.gif

Author's Address

Martijn Koster
Stalworthy Manor Farm
Suton Lane, NR18 9JG
Wymondham, Norfolk
United Kingdom
Email: m.koster@greenhills.co.uk

Gary Illyes
Brandschenkestrasse 110
8002, Zurich
Switzerland
Email: garyillyes@google.com

Henner Zeller
1600 Amphitheatre Pkwy
Mountain View, CA 94043
USA
Email: henner@google.com

Lizzi Harvey
1600 Amphitheatre Pkwy
Mountain View, CA 94043
USA
Email: lizzi@google.com

感想

使う側としては現状の書き方は変わらないので、特に意識して何かを変更したりする必要は特にありません。

この仕様が必要になるのは、パーサやクローラを書いている側です。
もっとも、この手のパーサを一から書くのも大変です。
Googleがパーサを公開しているので、必要があればこちらを使用しましょう。

日本語記事

Google、REP(ロボット排除プロトコル)のWeb標準化に乗り出す / ITMedia
Googleがウェブサイト管理に欠かせない「robots.txt」のインターネット標準化を推進 / Gigazine

【PHP7.4】波括弧による文字列|配列アクセスが削除される

$
0
0

大改修が入ることはないでしょうと言ったな、あれは嘘だ。
2019/07/22の仕様凍結を目前に、いくつかのRFCが駆け足で投票に入っています。
そのうちのひとつで、あらゆるPHPプログラムに影響する可能性のある変更が入りました。

Deprecate curly brace syntax for accessing array elements and string offsetsというRFCが投票中です。
2019/07/03投票開始、2019/07/17投票終了、受理には2/3+1の賛成が必要です。
2019/07/08時点では賛成23反対6で、おそらく受理されます。

Deprecate curly brace syntax for accessing array elements and string offsets

Introduction

PHPは、配列要素と文字列オフセットへのアクセスに、角括弧と波括弧の両方を使用することができます。

    $array = [1, 2];
    echo $array[1]; // 2
    echo $array{1}; // こっちも2

    $string = "foo";
    echo $string[0]; // "f"
    echo $string{0}; // こっちも"f"

しかし、これら両方の構文のサポートは、混乱をもたらす可能性があります。
波括弧構文は角括弧構文と同じ動作ですか?
パフォーマンスに違いはありますか?
波括弧はスコープを分ける標準的な方法ですが、波括弧構文にもスコープを切る機能はありますか?
そもそも波括弧構文はどうして存在しているのですか?

マニュアルにほんの少しある注釈を除いて、波括弧構文はほぼ文書化されていません。
さらに、通常の角括弧構文より機能は少なくなっています。
たとえば配列への要素の追加に波括弧を使うことはできません。

$array[] = 3;
echo $array[2]; // 3

$array{} = 3; // Parse error: syntax error, unexpected '}'

配列の作成もできません。

$array = [1, 2]; // OK

$array = {1, 2}; // Parse error: syntax error, unexpected '{'

list短縮構文にも使えません。

[$one, $two] = $array; // OK

{$one, $two} = $array; // Parse error: syntax error, unexpected ','

Proposal

波括弧構文による配列と文字列オフセットへのアクセスを非推奨にします。

PHP7.4
$arr = [1, 2, 3];
var_dump($arr{1}); // Warning: Array and string offset access syntax with curly braces is deprecated

Discussion

Wasn't the curly brace syntax deprecated once before?

波括弧構文、前いちどDeprecatedにならなかったっけ?

2008年6月の議論によると、波括弧構文はPHP5.1のRC5時点では非推奨でしたが、最終リリース前に非推奨警告は削除されました。
2006年8月のPHPマニュアルには「PHP6以降廃止予定」と書かれていましたが、PHP6はなくなったのでリリースされることはありませんでした。

Is the curly brace syntax valuable for differentiating string and array offset access?

波括弧構文は、文字列アクセスと配列オフセットアクセスを区別するのに役立たないかな?

重複した構文があれば、文字列アクセスと配列オフセットアクセスを区別することができます。
問題は言語によって使い分けが強制されず、どちらの構文も文字列と配列の両方に使用できることです。
その結果、あるコードベースでは文字列アクセスに常に$str[0]、配列アクセスに常に$arr{0}を使うようにすることはできますが、他のコードベースでは逆の規則になっているかもしれません。

How frequently is the curly brace syntax used?

波括弧構文はどれだけ使われている?

Composerパッケージのトップ2000ライブラリについてNikitaが調べたところ、2.2k箇所で使用され、これは全ての配列アクセス888.3kのうち0.25%でした。
しかし、これは調査したパッケージが重複しているため、実体より多少多く出ています。
たとえばWordPressコアリポジトリのコピーが二つあり、それぞれが182箇所で使用しています。
使用された波括弧の92%は、トップ2000のうちわずか25リポジトリに集中していました。

Will it be too much work for people to migrate code away from the curly brace syntax?

移行は大変?

パッチと一緒に移行スクリプトが公開されています。

Backward Incompatible Changes

文字列・配列に波括弧構文でアクセスすると非推奨の警告が発生するようになります。

Future Scope

PHP8もしくはその後のどこかで、波括弧構文を完全に削除します。
それ以降、波括弧構文はコンパイルエラーになります。

References

・現行スレッド:https://externals.io/message/104744
・2008年6月の議論スレッド:https://externals.io/message/38153
・2005年11月の議論スレッド:https://externals.io/message/20143

感想

消したいという話は14年も前から出ていたようですが、最近のPHP構文刷新の流れに乗ってついに可決されます。
色々なライブラリを見てきましたがこの書き方を見たことないですし、そもそもこの構文を知らない人も多いかもしれません。
なくなっても問題ないですね。

むしろ何故存在していたのかがわからない。

このRFCでは今後の予定は決まっておらず、後の人に任された状態になっています。
素直にPHP8以降で削除されればいいのですが、このまま宙ぶらりんになると困っちゃいますね。

【PHP7.4】レガシーな仕様はどんどんしまっちゃおうねぇ

$
0
0

恒例の仕様凍結直前駆け込みRFC第二弾。
色々な古い書き方について、PHP7.4でE_DEPRECATEにし、PHP8で削除を目指すRFCが投票中です。

以下はDeprecations for PHP 7.4の日本語訳です。

Deprecations for PHP 7.4

Introduction

このRFCでは、以下に列挙されている機能についてPHP7.4で非推奨とし、PHP8で削除することを提案します。

Proposal

各提案は個別に投票を行い、投票数の2/3+1の賛成で受理されます。
投票開始は2019/07/08、投票終了は2019/07/22です。

The 'real' type

現在のPHPでは、float型にはdoubleとrealという2種類のエイリアスが存在します。
後者は滅多に使用されず、廃止されるべきです。
これは(real)キャストと、is_real関数の両方が含まれます。
settype関数はrealをサポートしていないため影響はありません。

プログラム側の対応は簡単で、全ての(real)キャストを(float)に、is_real関数をis_floatに置き換えるだけです。

Proposal:(real)キャストおよびis_real関数をE_DEPRECATEDにします

2019/07/11時点では賛成24反対6で、おそらく受理されます。

Magic quotes legacy

悪名高いmagic_quotesはPHP5.4で削除され、magic_quotesの値を調べる関数はそれ以来ずっとfalseを返しています。
PHP7にはmagic_quotesに関する機能は全く存在しないため、これらの関数はもはや存在する意義がありません。

Proposal:get_magic_quotes_gpc関数およびget_magic_quotes_runtime関数をE_DEPRECATEDにします。
この変更が影響するのは、もはやサポートされていないPHP5.4以前のレガシーなコードだけです。

2019/07/11時点では賛成34反対0で、おそらく受理されます。

array_key_exists() with objects

array_key_existsは互換性のためにオブジェクトにも対応しています。

01.png

オブジェクトに対するarray_key_existsは問題があります。
プロパティの可視性を無視して直接中身を覗きます。
また、配列のキーとオブジェクトのキーの取り扱いの違いが考慮されていないため、数値キーのプロパティに対して正しくない結果を返す可能性があります。

さらにarray_key_existsがオブジェクトを受け入れるという事実は、ArrayAccessオブジェクトをarray_key_existsで適切に操作できるのだとユーザに誤解させかねません。
これは誤りで、array_key_existsは実際にはArrayAccessをサポートしていません。

Proposal:array_key_existsにオブジェクト渡すとE_DEPRECATEDにします。

2019/07/11時点では賛成32反対0で、おそらく受理されます。

FILTER_SANITIZE_MAGIC_QUOTES

magic_quotesはPHP5.3で廃止され、PHP5.4で削除されました。

Filter関数は、magic_quotesと同じ動作をaddslashesを呼び出すことで再現するサニタイズフィルタを提供しています。
PHP7.3では、magic_quotesという単語から抜け出すため、FILTER_SANITIZE_MAGIC_QUOTESのエイリアスとしてFILTER_SANITIZE_ADD_SLASHESを追加しました。

Proposal:FILTER_SANITIZE_MAGIC_QUOTESをE_DEPRECATEDにします。かわりにFILTER_SANITIZE_ADD_SLASHESを使ってください。

2019/07/11時点では賛成33反対0で、おそらく受理されます。

Reflection export() methods

全てのリフレクションクラスはReflectorインターフェイスを実装しており、__toString()export()という2つのメソッドが存在します。
後者は静的メソッドであり、一見して引数を受け付けません。

しかし実際は、各サブクラスにおいて引数を受け入れるようにオーバーライドされています。
これは本来は互換性のない引数のエラーが発生するはずですが、内部的にエラーを抑制して実装されています。

exportメソッドは、本質的にコンストラクタ+__toStringと同じです。

    // 同じ
    ReflectionFunction::export('foo');
    echo new ReflectionFunction('foo'), "\n";

    // 同じ
    $str = ReflectionFunction::export('foo', true);
    $str = (string) new ReflectionFunction('foo');

exportメソッドはPHPの継承の規則に反していて混乱を招くため、全く不要です。

Proposal:Reflectorインターフェイスと全ての実装クラスからexportsメソッドをE_DEPRECATEDにします。PHP8で実装を削除します。

2019/07/11時点では賛成25反対3で、おそらく受理されます。

mb_strrpos() with encoding as 3rd argument

mb_strrposのドキュメントにはこのようなことが書かれています。

02.png

非推奨と書かれてはいますが、現状特に警告が出たりはしません。
両方の引数を受け入れるため、このパラメータは他のパラメータとは異なる動作をします。
たとえばstrict typesにしても型エラーが出ません。
PHP5.1とPHP7.4の両方をサポートするアプリはほぼ存在しないと思われるので、ここの動作を変更しても、後方互換性に関する重大な問題は発生しません。

Proposal:mb_strrpos関数の第三引数に文字列を渡すとE_DEPRECATEDにします。PHP8で実装を削除します。

2019/07/11時点では賛成32反対0で、おそらく受理されます。

implode() parameter order mix

歴史的な経緯により、implode関数は引数$glue$piecesを、どちらの順番でも受け入れることができます。
join関数も同じです。

Proposal:implode(array, string)の順番での引数をE_DEPRECATEDにします。implode(array)は引き続き許可されます。

2019/07/11時点では賛成27反対5で、おそらく受理されます。

Unbinding $this from non-static closures

現在、$closure->bindTo(null)を用いてクロージャから$thisを削除することができます。
PHP8では非静的メソッドの静的呼び出しが削除されたため、非静的メソッドには常に$thisが存在することが保証されました。
非静的メソッド内で宣言された非静的クロージャに対して、$thisが常に存在するという同様の保証を得たいと思います。
さもなくば、通常のアクセスもしくはクロージャからのアクセスの何れかに対して、不合理が発生するでしょう。

Proposal:クロージャから$thisの削除をE_DEPRECATEDにします。特に非静的メソッド内で宣言された非静的クロージャに対して適用されます。$thisが不要な場合は静的クロージャを使うことで余計なバインドを避けられます。

2019/07/11時点では賛成31反対0で、おそらく受理されます。

hebrevc() function

hebrevc関数は、hebrev関数の結果をnl2brするのと同じです。
これはヘブライ語のテキストを論理順から視覚順に変換する関数です。

視覚的順序付けの使用は、Unicode bidiに対応していない端末など、ごく一部のコンテキストにのみ使用するべきです。
W3Cが表明しているように、視覚的順序付けはHTMLには使用しないでください。
hebrevc関数はこの原則に明らかに反しています。

Proposal:hebrevc関数をE_DEPRECATEDにします。

2019/07/11時点では賛成20反対8で、受理されるか微妙なところです。

convert_cyr_string()

convert_cyr_string関数は、キリル文字のテキストを他のキリル文字セットに変換します。
変換する文字コードは、convert_cyr_string($str, "k", "i")のように理解しがたい1文字で指定しなければなりません。
この関数は、PHPに文字コード変換のための一般的手法が存在していなかった頃の古い関数です。
現在はmb_convert_encodingiconv、あるいはUConverterなどが、この目的のために使用されます。

Proposal:convert_cyr_string関数をE_DEPRECATEDにします。

2019/07/11時点では賛成17反対7で、受理されるか微妙なところです。

money_format()

money_format関数は、ロケールを見て金額をフォーマットします。
これはC言語のstrfmonを使用していますが、これは全てのプラットフォームがサポートしているわけではなく、特にWindowsでは利用できません。
今日ではintlが提供するNumberFormatter::formatCurrencyを使用すべきです。
これはプラットフォームに依存せず、ロケールにも依存しません。
intlではさらに金額フォーマットをパースするNumberFormatter::parseCurrencyも提供しています。

あと、MacOSでのstrfmonの実装を見ていると、どうもこの関数はバッファオーバーランしているように見受けられます。
これはすなわち、この関数が十分なテストを受けていないことを示しています。

Proposal:money_format関数をE_DEPRECATEDにします。

2019/07/11時点では賛成22反対8で、おそらく受理されます。

ezmlm_hash()

ezmlm_hash関数は、EZMLM/QMailメーリングリストが理解できるメールアドレスのハッシュを作成します。
EZMLM/QMailは最終リリースが2007年であり、全くメンテナンスされていないため、この関数を必要とするPHP開発者は限りなく少数です。
この関数は、元々php.netのメーリングリストで使用するために実装されたものです。
必要であればユーザランドで簡単に実装が可能です。

Proposal:ezmlm_hash関数をE_DEPRECATEDにします。

2019/07/11時点では賛成25反対4で、おそらく受理されます。

restore_include_path() function

restore_include_path関数は、本質的にini_restore('include_path')のエイリアスです。
restore_error_handlerrestore_exception_handlerと異なり、この関数はスタック上で動作せず、必ず初期値にリセットされます。

set_error_handler()とrestore_error_handler()をペアで使用して一時的に動作を変更する操作は有用ですが、set_include_path()とrestore_include_path()を同様にペアで使用することは安全ではありません。
従って、この関数はini_restore('include_path')と別に存在する利点はなく、誤った使い方を助長するだけです。

Proposal:restore_include_path関数をE_DEPRECATEDにします。

2019/07/11時点では賛成19反対9で、受理されるか微妙なところです。

allow_url_include

allow_url_includeディレクティブは、requirerequire_onceincludeinclude_onceの各言語構造にURLストリームラッパーの使用を許可します。
このディレクティブはデフォルト無効で、使用するにはallow_url_fopenも有効にする必要があります。

includeするパスに外部入力を使用している場合、このオプションを有効にするとセキュリティ上の問題が発生します。
外部ドメインからPHPファイルをインクルードする仕様そのものがそもそも極めて問題であり、大きなセキュリティリスクが存在するため、廃止されるべきです。

Proposal:allow_url_includeディレクティブが有効である場合E_DEPRECATEDが発生します。

2019/07/11時点では賛成30反対0で、おそらく受理されます。

Backward Incompatible Changes

PHP7.4では、非推奨の警告が表示されます。
PHP8では、動作しなくなります。

Changelog

元々は以下の項目も廃止予定に含まれていましたが、最終的に対象外になりました。

get_called_class。あまり考えられていなかった。
enable_dl。当初の提案が間違っていた。
・INPUT_SESSIONとINPUT_REQUEST。そもそも実装されてなかったので直接削除した
is_writeable。ただの誤字だけど検索しやすいから好ましいとか主張した人がいたので取り下げられた。
apache_request_headers。大きな問題の中のひとつなので、この関数だけ消して終わりではなく大規模な考察が必要。
hebrev。一部の環境ではまだ有用なため。ただしhebrevcは削除する。
enable_argc_argv、と書いてあるんだけど全く出てこないんだけど存在するのこれ?

感想

ほとんどは削除されて当然というか、そもそも全く知らない関数だった。
hebrevcとかconvert_cyr_stringとか聞いたこともないよ。
使ってる人いるんですかね?
あとmagic_quotesは、むしろどうしてPHP7.0で削除されなかったんだと聞きたいくらいですね。
概ね順当に削除されそうでなによりです。

でもReflection::export()は個人的にデバッグとかで時々使っているからちょっと困るかな。
__toString()はなんというか、これを使うのだ、と明示できないからちょっと気持ち悪いというかなんというか。

clear text(cleartext) と plain text(plaintext)

$
0
0

https://tools.ietf.org/html/rfc4949

  • clear text
    $ clear text
      1. (I) /noun/ Data in which the semantic information content
      (i.e., the meaning) is intelligible or is directly available,
      i.e., not encrypted. (See: cleartext, in the clear. Compare:
      cipher text, plain text.)

      2. (O) /noun/ "Intelligible data, the semantic content of which is
      available." [I7498-2]

      3. (D) /noun/ Synonym for "plain text".

      Deprecated Definition: IDOCs SHOULD NOT use this term as a synonym
      for "plain text", because the plain text that is input to an
      encryption operation may itself be cipher text that was output
      from a previous encryption operation. (See: superencryption.)

   $ cleartext
      1. (O) /noun/ Synonym for "clear text" [I7498-2].

      2. (I) /adjective/ Referring to clear text. Usage: Commonly used
      instead of "clear-text". (Compare: ciphertext, plaintext.)
      3. (D) /adjective/ Synonym for "plaintext".

      Deprecated Definition: IDOCs SHOULD NOT use this term as a synonym
      for "plaintext", because the plaintext data that is input to an
      encryption operation may itself be ciphertext data that was output
      from a previous encryption operation. (See: superencryption.)
  • plain text
    $ plain text
      1. (I) /noun/ Data that is input to an encryption process. (See:
      plaintext. Compare: cipher text, clear text.)

      2. (D) /noun/ Synonym for "clear text".

      Deprecated Definition: IDOCs SHOULD NOT use this term as a synonym
      for "clear text". Sometimes plain text that is input to an
      encryption operation is clear text, but other times plain text is
      cipher text that was output from a previous encryption operation.
      (See: superencryption.)

   $ plaintext
      (I) Data that is input to and transformed by an encryption
      process, or that is output by a decryption process.

      (C) Usually, the plaintext input to an encryption operation is
      cleartext. But in some cases, the input is ciphertext that was
      output from another encryption operation. (See: superencryption.)
2.4. Definition Type and Context

   Each entry is preceded by a character -- I, N, O, or D -- enclosed in
   parentheses, to indicate the type of definition (as is explained
   further in Section 3):
   -  "I" for a RECOMMENDED term or definition of Internet origin.
   -  "N" if RECOMMENDED but not of Internet origin.
   -  "O" for a term or definition that is NOT recommended for use in
      IDOCs but is something that authors of Internet documents should
      know about.
   -  "D" for a term or definition that is deprecated and SHOULD NOT be
      used in Internet documents.

参考

【PHP8.0】未定義変数へのアクセスが例外にな・・・らない

$
0
0

ついにこの日が来てしまったようです。

PHPのユルさの象徴のひとつとして『未定義変数に普通にアクセスできる』というものがあります。

echo $a; // Notice: Undefined variable: a

大抵の言語ではエラーや例外で落ちますが、PHPでは処理が中断することはありません。
警告は出ますが、最もエラーレベルの低いE_NOTICEです。

が、PHP8.0でこの挙動が変わることになりそうです。

Reclassifying engine warningsというRFCが提出され(提出者はいつものNikita)、2019/09/26まで投票中です。
これは様々な警告のエラーレベルを変更しようというRFCなのですが、ここではそのうちのUndefined variable、Undefined array indexに絞って紹介してみます。
RFCでもこれらの項目は特別に分けられていますしね。

Reclassifying engine warnings

変更には投票の2/3の賛成が必要です。

Proposal

警告や例外には主にE_NOTICE、E_WARNING、Error例外の3段階がありますが、一部の警告についてエラーレベルを変更します。
ただし上げる場合でも1段階しか上げません。
現在E_NOTICEの警告のエラーレベルを上げる場合はE_WARNINGに上げるだけで、いきなり例外まで上げるとはない、ということです。

ただしUndefined variableのみ例外。

Undefined variable

大抵の場合、未定義変数へのアクセスは重大なバグです。
現在のE_NOTICEというとても軽い警告は、register_globalsのような外から変数を定義してしまうことができていたPHP暗黒時代の遺物です。
理想的には、未定義変数へのアクセスはコンパイルエラーになるべきですが、PHPの性質上コンパイル時に全ての分析を行うことはできないため、このRFCでは代わりにError例外にすることを提案します。

しかしながら、例外にするとこれまでのように警告を無視することができなくなるため、大量の警告を抑制して運用しているようなレガシーコードは多大な改修が必要となります。
また一部には、未定義変数の使用は正当なコーディングスタイルであるとさえ考えている過激派も存在します。

これらの理由のため、Error例外にするか、E_WARNINGにするか、E_NOTICEのままにするかの個別投票を行います。

2019/09/14現在、Error例外が22票、E_WARNINGが8票、E_NOTICEのままが4票で、おそらくError例外になります。

Undefined array index

未定義変数や未定義プロパティと同様、未定義の配列キーへのアクセスは、モダンPHPではプログラミングのエラーであるはずです。
しかし、変数やプロパティは主に静的に定義されるのに対し、配列のキーは動的に生成されることが多いため、全く同じ扱いをすることは難しいかもしれません。

JavaScriptのような一部の言語は、未定義の配列キーへのアクセスをエラーにしませんし、そのような操作をサイレントに実行します。
PHPでも主流ではないものの、同じようなコーディングスタイルを取る者たちがいて、彼らは未定義の配列キーへのアクセスが抑制可能な警告であることを願っています。

従って個別投票により、E_WARNINGに上げるか、E_NOTICEのままかを決定します。

2019/09/14現在、E_WARNINGが23票、E_NOTICEのままが13票で拮抗しています。

感想

私としては、仕事とかで書くちゃんとしたコードは常にerror_reporting(-1)と書いているので、業務上は影響ありません。
ただ使い捨てで適当に書き散らすコードなんかは警告が出てても一回動けばいいやとかやったりしているのですが、今後はそのような手抜きが許されなくなります。
書いたらなんかとりあえず動く、という他言語に対するPHPの大きな利点(にして同時に欠点)を投げ捨ててまで得られるものがあるのかというと、個人的には少々疑問に感じます。

巷に溢れるテキトーなコードも大半が即死し、PHPを始める敷居が大きく上がってしまうことは間違いありません。
テキトーなコードなんて死滅してもいい?
むしろ絶滅させるべき?
みんな正しいコードを書くべき?
正しいコードしか許されないべき?

未定義変数アクセスの例外化は、プログラミング言語として正しい方向性でしょう。
しかし世の中、正しければ勝てるというわけでもありません
使用前の変数宣言なんて難しいコーディング、誰も彼もができるわけではないのです。
この正しい変更は、入口が狭まるというデメリットを上回るメリットを果たして得られるのでしょうか。

あと最大の問題点として、が不可能になります。
こりゃ致命的だ。

回避策はあるか?

レガシーコードではerror_reporting(E_ALL~E_NOTICE)なんてことをやって警告の通知を無視していたわけですが、例外になるとこれが無視できなくなります。
プログラムを変更せずに回避する方法はないでしょうか?

PHPには例外の挙動を変更するset_exception_handlerという関数があります。
しかし、これは実のところ単にプログラム全体を囲むtry/catchなので、発生自体を無視したり、発生箇所に戻って先に進むといったことはできません。

ということでちょっと思いつかないですね。
誰かがきっと考えてプルリクしてくれるはず。

うん、まあ、そもそも例外が出ないように修正しろよって話ですが。


HTTPのリクエストヘッダーでタイムアウトを操作できない

$
0
0

まとめ

  • リクエストのタイムアウト制御はリクエストを送るサーバー、メソッドの責任?
  • TimeOutというヘッダーがあるが、それを遵守する責務はない
  • そもそもTimeOutヘッダーはHTTPメソッド1のLOCKリクエストに使用するものであり、通常は使用しない

疑問

HTTPでGETやPOSTするときに、
リクエスト先に到達できなかったり処理に時間がかかる場合に、リクエストにタイムアウトを設定したい。
リクエストに対する設定なので、HTTPのリクエストヘッダーで設定できないか?

(ヘッダーは自由に記述できるが送信機能に手を加えられない場合がある)

観測として、一部のネット上の記事でTimeOutヘッダーを付与しているものを確認しているので、これを使えないか?

HTTPヘッダーの確認

HTTP ヘッダー - HTTP | MDN

'X-' 接頭辞を使用して独自のヘッダーを追加できますが、この慣習は 2012 年 6 月に非推奨になりました。これは、 RFC 6648 で非標準のフィールドが標準になったときに発生した不便さのためです。それ以外のヘッダーは IANA レジストリ に収録されており、その基になったものは RFC 4229 です。また IANA は 新たに提案された HTTP ヘッダーのレジストリ も管理しています。

とあるのでIANA レジストリ を参照する

Message Headers

Header Field Name Template Protocol Status Reference
Timeout http standard [RFC4918]

それらしいTimeOutヘッダーを確認。

RFC4918を参照

RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)#section-10.7

10.7. Timeout Request Header

  TimeOut = "Timeout" ":" 1#TimeType
  TimeType = ("Second-" DAVTimeOutVal | "Infinite")
             ; No LWS allowed within TimeType
  DAVTimeOutVal = 1*DIGIT

Clients MAY include Timeout request headers in their LOCK requests.
However, the server is not required to honor or even consider these
requests. Clients MUST NOT submit a Timeout request header with any
method other than a LOCK method.

The "Second" TimeType specifies the number of seconds that will
elapse between granting of the lock at the server, and the automatic
removal of the lock. The timeout value for TimeType "Second" MUST
NOT be greater than 2^32-1.

See Section 6.6 for a description of lock timeout behavior.

機械翻訳と別途部分機械翻訳

RFC4918 日本語訳 - [RFC/技術資料] ぺんたん info

クライアントは、LOCK要求にタイムアウト要求ヘッダーを含めることができます。
ただし、サーバーはこれらを尊重することも考慮する必要もありません。
リクエスト。 クライアントは、タイムアウト要求ヘッダーを送信しないでください。
LOCKメソッド以外のメソッド。

ちょっと後半の翻訳がイマイチですが、つまり

LOCK要求(LOCK requests.)に使ってください。
LOCKメソッド以外に使わないでください(Clients MUST NOT submit a Timeout request header with any
method other than a LOCK method)

そして
ただし、サーバーはこれらを尊重することも考慮する必要もありません。

送ったとしてそれが守られるとは限らないと読みました。

LOCK methodとはなんぞや

GETにPOSTにCRUDプラスあれやこれやはわかりますが、LOCKは知らなかったので調査。

"svn lock"で"400 Bad Request"が返ってくる - kakakakakku blog

access.log
127.0.0.1 - kakku22 [24/Jan/2013:20:41:25 +0900] "LOCK /svn/trunk/Hello.txt HTTP/1.1" 400 226

WebDAV時代のセキュリティ対策[前編](1/4)

LOCK コレクションを含むリソースのロック

というわけでリソースに対する処理であり、通常のGETやPOSTとは違うということはわかりますね。

GETのように扱えないし、そもそも守ってもらえないわけです。

というわけで

HTTPヘッダーだけでは制御できず、どうしても
XMLHttpRequest.timeout - Web API | MDN
のようにリクエスト側の機能として実装されているものを使う必要があるように見受けられました。

レスポンスのデータの寿命やキャッシュにはかなりヘッダーが絡んでくるので、リクエストの制御も出来るかなと思ったのですが、
ここだけ扱えなくてとても困っております。


  1. GETとかPOST 

【PHP8.0】オブジェクト初期化子のRFCが却下されそう

$
0
0

いつのまにやらObject InitializerというRFCが投票に入っていました。
ちょっとだけ面白そうと思ったのですが、ただ、ほぼ確実に却下されるので詳しく見てもしょうがないのでざっくり紹介してみます。

Object Initializer

文法

class Customer{
  public $id;
  public $name;
  private DateTimeImmutable $createdAt;
}

$customer = new Customer{
  id = 123,
  name = "John Doe",
};

newするときに中括弧で引数を渡すと、自動的にプロパティにセットされます。

キーが文字列ではないところが、PHPとして物凄い違和感がありますね。

上の例は、下のようなよくある文と同等です。

$customer = new Customer();
$customer->id = 123;
$customer->name = "John Doe";

従って、privateである$createdAtに値を突っ込むことはできません。

制約

オブジェクト初期化子を使う場合、全てのpublicプロパティを指定しなければなりません。

$customer = new Customer{
  id = 123, // RuntimeException class object failed due to missing required properties
};

オブジェクト初期化子自体を使わない場合は、普通にインスタンス化できます。

$customer = new Customer();

未定義プロパティ

未定義のプロパティに値を突っ込めます。

$baz = 'baz';

$obj = new stdClass {
  foo = "bar",
  $baz = true,
};

ええー、と思いますが、そもそもこれPHPの仕様だったわ。

コンストラクタ

オブジェクト初期化子を使う場合、コンストラクタに引数は渡せません。
即ち、以下のような書き方は文法エラーになります。

$customer = new Customer($dateTime){
  id = 123,
  name = "John Doe",
};

マジックメソッド

可視プロパティがなかった場合、普通にマジックメソッド__setが呼ばれます。
RFCに書かれている以下の例では、$nameはpublicなので直接値が入り、protectedである$emailはマジックメソッド__setが呼ばれることになるようです。

class EmailAddress
{
  protected string $email;
  public ?string $name = null;

  public function __set(string $name, $value): void
  {
    if ($name !== "email") {
      return;
    }
    if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
      throw new InvalidArgumentException("Invalid email address");
    }
    $this->email = $value;
  }
}

$email = new EmailAddress {
  email = "john.doe@example.org",
  name = "John Doe",
};

将来の予定

無名クラスstdClassを使うときは、もうクラス名すらも書かなくてよくない?

$obj = new {
  foo = "bar",
  $baz = true,
};

配列展開してpublicプロパティ突っ込めたらよくない?

$array = [
  'a' => 1,
  'b' => 2,
];

$obj = new { ...$array };

投票

2019/10/07投票開始、2019/10/21投票終了、可決には2/3+1の賛成が必要です。
2019/10/09現在は賛成3反対18で、ほぼ確実に却下されます。

この投票期間、RFCには何故か書かれていません。

感想

うん、まあ却下だよね。

特にpublicプロパティは全て指定しなければならないという厳格さと、未定義プロパティを指定できるという緩さが同居してるのは気持ち悪いというかなんというか。
これならまだAutomatic Property Initializationのほうがいいと思います。
Code free constructorよりはいいと思いますが。

ただ無名クラスはすごい便利そうなので、これは欲しいところですね。

$obj = new{
  'id' = 123,
  'name' = 'John Due',
};

まあ、今でもほぼ同じように書けたりはするんですけどね。

$obj = (object)[
  'id' => 123,
  'name' => 'John Due',
];

メールフォーマットに関する仕様まとめ

$
0
0

メールフォーマットに関する仕様(テキストメールとMIMEによる拡張)を簡単にまとめた。

  • RFC 5322(旧RFC 2822、RFC 822)に定義されるテキストメールには以下の問題があった。
    • ヘッダ・本文にASCII(US-ASCII)しか利用できない。
      • ASCII以外の文字を利用する地域では、本文に対する文字コードを独自に決めた(日本ではISO-2022-JP)ため、地域間の互換性がなくなった。
    • 本文はテキスト形式のみであり、1つのメールで1つの本文しか扱うことができない。
      • バイナリファイルを格納するために、uuencode形式のエンコードが用いられたが、これは一部のシステムで誤動作を起こす可能性があった。
  • これらの問題を解決するためMultipurpose Internet Mail Extension (MIME) が定義された。
  • MIMEで定義される仕様のポイントは以下の通り。
    • ヘッダで非US-ASCII文字を利用できる。
      • 書式:=?charset?encoding?encoded-text?=
      • メールの件名に日本語を使うことができるようになった。
    • Content-Typeヘッダでデータ形式を指定する。
      • 書式:Content-Type: type/subtype; parameter
      • type(大分類)とsubtype(小分類)を分けて記述できるため、受信者は非対応のsubtypeに対しても最低限の取り扱いが可能となる。
      • テキスト形式の場合にはparameterで文字コードを指定できる。
    • マルチパート(Content-Type: multipart/...)により本文を複数に分割できる。
      • 本文と添付ファイルを1つのメールで送信する、本文にHTML形式と代替用のテキスト形式の両方を含める、といったことが可能となる。
    • Content-Transfer-Encodingヘッダでデータのエンコード形式を指定する。
      • 書式:Content-Transfer-Encoding: mechanism
      • uuencodeはサポート対象外となっている。

参考

【PHP8.0】PHP8で警告のエラーレベルが軒並み厳しくなる

$
0
0

多くの警告について、PHP8.0でエラーレベルが変更されます。

これはReclassifying engine warningsというRFCで受理されたものです。
提案者はいつものNikita。
影響の大きい未定義変数アクセスについては個別に紹介しましたが、ここではそこで紹介しなかった細かい警告について見ていきます。

これまでE_NOTICEだった警告の一部がE_WARNINGに、これまでE_WARNINGだった警告の一部が例外になります。
E_WARNINGを抑制するような書き方をしている場合、PHP8では動かなくなる可能性が高いので気をつけましょう。
現在E_NOTICE以下であればいきなり動かなくなることはありませんが、そもそも抑制する書き方がよくないので、なるべく修正した方がよいでしょう。

エラーレベルの変更がない警告も並んでいるので、もしかしたら全警告が列挙されてるのか?と思ったのですが、expected to be a %s, %s givenとか色々無いものもあるので、全てを出しているわけではないようです。
どういう基準なんだろうか?

Reclassifying engine warnings

Attempt to increment/decrement property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数のプロパティをインクリメント/デクリメントすると発生する。

    $a = 1;
    $a->b++;

Attempt to modify property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数のプロパティを変更すると発生する。

    $a = 1;
    $a->b['c'] = 1;

Attempt to assign property '%s' of non-object

E_WARNING → Error exception

オブジェクトではない変数にプロパティを追加すると発生する。

    $a = 1;
    $a->b = 1;

このへん全部同じでいいんじゃないか?

Creating default object from empty value

E_WARNING → Error exception

未定義の変数にプロパティを追加すると発生する。

    $a->b = 1;

PHP7.3では$aが定義されるのだが、PHP8では何も定義されなくなると思われる。

ところで未定義の変数のプロパティを変更しようとすると何の警告もなくオブジェクトが生成されるのだが、こっちは今後もいいのだろうか。

    $a->b['c'] = 1; // エラー出ない
    var_dump($a); // object(stdClass)#1 (1) { ["b"]=> array(1) { ["c"]=> int(1) } }

Trying to get property '%s' of non-object

E_NOTICE → E_WARNING

オブジェクトではない変数のプロパティを参照すると発生する。

    $a = 1;
    $a->b;

Undefined property: %s::$%s

E_NOTICE → E_WARNING

オブジェクトの未定義プロパティを参照すると発生する。

$a = new stdClass();
$a->b;

PHPの場合、入力として外部引数やらAPIやらを使うことが多いため、読み取りの失敗については書き込みより寛容気味。

Cannot add element to the array as the next element is already occupied

E_WARNING → Error exception

配列の自動挿入による整数キーがPHP_INT_MAXを超えたときに発生する。

    $a = [
        PHP_INT_MAX => 1,
    ];
    $a[] = 2;

ちなみに計算値で指定すれば、PHP_INT_MAXを超えていてもいける。

    $a = [
        PHP_INT_MAX => 1,
    ];
    $a[PHP_INT_MAX+1] = 2; // -2147483648とかになる

Cannot unset offset in a non-array variable

E_WARNING → Error exception

エラーの出し方がわからない

Cannot use a scalar value as an array

E_WARNING → Error exception

文字列型ではないスカラー型の変数に配列値を追加すると発生する。

    $a = true;
    $a[] = 1;

文字列型の場合は文字単位アクセスという正しい文法。

ちなみにnullで初期化した場合は問題なく動く。

    $a = null;
    $a[] = 1; // [ 0 => 1]

なぜかfalseでも動く。

    $a = false;
    $a[] = 1; // [ 0 => 1]

Trying to access array offset on value of type %s

E_NOTICE → E_WARNING

文字列型ではないスカラー型の変数を配列形式で読み込もうとすると発生する。

    $a = true;
    $a[1];

このE_NOTICE自体PHP7.4で追加されたもので、それ以前は何も出さずにnullを返していた。

Only arrays and Traversables can be unpacked

E_WARNING → TypeError exception

関数呼び出し時の引数展開にiterableでない値を渡すと発生する。

    var_dump(...1);

unpackとは特に関係ない。

Invalid argument supplied for foreach()

E_WARNING → TypeError exception

iterableでない値をforeachすると発生する。

    $a = 1;
    foreach($a as $loop){}

Illegal offset type

E_WARNING → TypeError exception

配列のキーに配列やオブジェクトを指定すると発生する。

$a = [
    new stdClass() => 1,
    [] => 2,
];

Illegal offset type in isset or empty

E_WARNING → TypeError exception

issetおよびemptyでチェックする配列のキーに配列やオブジェクトを指定すると発生する。

    $a = [];
    isset($a[new stdClass()]);

ちなみに$aが未定義やスカラー型の場合は何のエラーも起こらない。

    isset($a[new stdClass()]); // エラー出ない
    $a = 1;
    isset($a[new stdClass()]); // エラー出ない

未定義やint型等であれば配列形式アクセスした時点でfalseだから中身を見る必要もないというのはわかるが、文字列型でもエラーが出ない理由はよくわからない。

    $a = 'a';
    isset($a[1]); // true
    isset($a[new stdClass()]); // false エラー出ない

Illegal offset type in unset

E_WARNING → TypeError exception

unsetする配列のキーに配列やオブジェクトを指定すると発生する。

    $a = [];
    unset($a[new stdClass()]);

$aが未定義の場合Illegal offset typeは発生しないが、かわりにUndefined variableのE_NOTICEが出る。
文字列以外のスカラー型には何のエラーも出さず、文字列型やオブジェクトにはFatal errorが発生する。

    unset($a[new stdClass()]); // E_NOTICE: Undefined variable
    $a = 1;
    unset($a[new stdClass()]); // エラー出ない
    $a = 'a';
    unset($a[new stdClass()]); // Fatal error: Cannot unset string offsets
    $a = new stdClass();
    unset($a[new stdClass()]); // Fatal error: Cannot use object of type stdClass as array

このあたりの法則はさっぱりわからない。

Indirect modification of overloaded element of %s has no effect

E_NOTICEのまま

SplFixedArrayに突っ込んだ配列の値を直接変更すると発生する。

    $a = new SplFixedArray(1);
    $a[0] = [1];
    $a[0][0] = 2;

値を変更しているつもりだが、実際には変更されていないという注意。

SplFixedArrayに限らず、ArrayAccessをimplementsしたクラスに一般的に発生する症状のようだ。

Indirect modification of overloaded property %s::$%s has no effect

E_NOTICEのまま

マジックメソッド__getが配列を返す場合、その返り値を直接変更すると発生する。

    class A{
        private $value = ['a' => 1, 'b' => 2];
        public function __get($k){
            return $this->value;
        }
    }

    $a = new A;
    $a->value['a'] = 3;

こちらも値を変更したつもりだが、実際には変更されていない。

なお配列ではなくオブジェクトであれば、エラーも出ないし値を直接変更できてしまう。

    class A{
        private $obj;
        public function __construct(){
            $this->obj = new stdClass();
        }
        public function __get($k){
            return $this->obj;
        }
    }

    $a = new A;
    $a->obj->b = 1;

    var_dump($a); // { 'obj' => stdClass{ 'b'=>1 } }

Object of class %s could not be converted to int/float/number

E_NOTICEのまま

オブジェクトをスカラー型にキャストすると発生する。

    (int)new stdClass();

緩い比較が内部的にこのキャストを使用しているため、オブジェクトとスカラー型を緩く比較するとE_NOTICEが発生する。

    $a = new stdClass();
    var_dump($a == 1); // E_NOTICE
    var_dump($a === 1); // エラー出ない

比較ではエラーが出るべきではないので、こちらの問題がどうにかなるまでエラーレベルを変更しない。

A non-numeric value encountered

E_WARNINGのまま

次項で一緒に解説する。

A non well formed numeric value encountered

E_NOTICEのまま

非数値文字列を数値演算すると発生する。

    1 + '1';  // エラー出ない
    1 + '1a'; // E_NOTICE: A non well formed numeric value encountered
    1 + 'a';  // E_WARNING: A non-numeric value encountered

完全に数値形式の文字列ではエラーは出ず、一部だけ数値として評価できるときはnon well formed numeric value、完全に数値でない場合はnon-numeric valueになる。
今回はエラーレベルが変わらないが、数値形式文字列の計算は安全のためキャストしておいた方がよいだろう。

    1 + (int)'a'; // エラー出ない

Accessing static property %s::$%s as non static

E_NOTICEのまま

staticプロパティにインスタンスからアクセスすると発生する。

    class A{
        public static $property = 1;
    }

    $a = new A();
    $a->property;

正しくは$a::$property、もしくはA::$property
インスタンス内部からであればself::$propertyもいける。

Array to string conversion

E_NOTICE → E_WARNING

配列を文字列型にキャストすると発生する。

    (string)[];

変換前の配列の中身がどうなっていたとしても変換後の文字列はArrayになるので、実質的に機能していない状態なのでExceptionでもいい気がする。

Resource ID#%d used as offset, casting to integer (%d)

E_NOTICE → E_WARNING

リソースIDを配列のキーとして使用すると発生する。

    $fp = fopen('hoge', 'w+');
    $array = [$fp => $fp];
    var_dump($array); // []

リソースIDは整数っぽい値であり、かつプログラム中ではユニークなので、このような使い方ができそうではあるが実際は動いていない。
明示的にキャストするとint型になるため警告は発生せず、正しく動作する。

    $fp = fopen('./hoge', 'w+');
    $array = [(int) $fp => $fp];
    var_dump($array); // [1=>resource]

そもそも動いてないので、これもいきなりExceptionでいい気がしないでもない。

String offset cast occurred

E_NOTICE → E_WARNING

文字列への角括弧オフセットアクセスのキーに整数ではない数値を使ったときに発生する。

    'string'[1.5];
    'string'[true];

下の項目と同じような内容なのでエラーレベルを揃えたという話のようだ。

Illegal string offset '%s'

E_WARNINGのまま

文字列への角括弧オフセットアクセスのキーに数値ではない値を使ったときに発生する。

    'string'['a'];

Uninitialized string offset: %d

E_NOTICE → E_WARNING

文字列への角括弧オフセットアクセスで範囲外の値を読み込もうとしたときに発生する。

    'string'[10];

Illegal string offset: %d

E_WARNINGのまま

文字列への角括弧オフセットアクセスでマイナスの範囲外の値を変更したときに発生する。

    $str = 'string';
    $str[-10] = 'a';

正の範囲外を変更したときは単に文字列が伸びるだけでエラーは発生しない。

    $str = 'string';
    $str[10] = 'a'; // 'string    a'

Cannot assign an empty string to a string offset

E_WARNING → Error exception

文字列への角括弧オフセットアクセスで値を空文字に変更しようとしたときに発生する。

    $str = 'string';
    $str[1] = '';

2文字以上与えた場合は2文字目以降が無視されるだけでエラーは発生しない。

    $str = 'string';
    $str[1] = 'abcde'; // 'saring'

Only variables should be passed by reference

E_NOTICEのまま

リファレンス関数に値を直接渡すと発生する。

    sort([2, 1]);

Only variable references should be returned by reference

E_NOTICEのまま

リファレンス返しで値を直接返すと発生する。

    function &ref(){
        return 1;
    }
    ref();

Only variable references should be yielded by reference

E_NOTICEのまま

リファレンス返しで値を直接yieldすると発生する。

    function &ref(){
        yield 1;
    }
    foreach(ref() as $v);

リファレンス返しは百害しかないので使用してはならない。

Only variables should be assigned by reference

E_NOTICEのまま

リファレンスではない関数をリファレンスで受け取ろうとすると発生する。

    function ref(){
        return 1;
    }
    $x = &ref();

Attempting to set reference to non referenceable value

E_NOTICEのまま

出し方がわからないどころか、事例すら一切出てこない謎の警告。

Cannot pass by-reference argument %d of %s%s%s() by unpacking a Traversable, passing by-value instead

E_WARNINGのまま

参照渡し関数にTraversableな値を引数アンパックして渡すと発生する。

    function ref(&$var){}
    ref(...new ArrayIterator([1]));

いみがわからない。

Division by zero

E_WARNING → DivisionByZeroError exception

数値を0で割ると発生する。

    1 / 0;

PHP7.4までは計算結果がfloat(INF)になる。

Undefined variable

E_NOTICE → E_WARNING

未定義変数を参照すると発生する。

    echo $a;

詳細は個別記事を参照のこと。

Undefined array index

E_NOTICEのまま

配列の未定義キーを参照すると発生する。

    $a = [];
    echo $a[1];

詳細は個別記事を参照のこと。

感想

そもそもどうすれば出せるのかすらわからないエラーがあった。

警告に寛容なプログラミングをしている場合、Invalid argument supplied for foreachCreating default object from empty valueあたりはよく見かけるのではないかと思います。
これらはPHP8では例外になって完全に動かなくなるので注意しましょう。

それ以外でも、ゆるふわぺちぱーに対する締め付けは年々厳しくなる一方で、彼らの肩身はどんどん狭まりつつあります。
かつてはPHP以上にアバウトで破壊と慈悲の混沌だったJavaScript界も、最近は型に嵌まっていないゆるふわJavaScripterを完全排除する流れができあがっています。
やがて彼らの居場所が完全に失われてしまったとき、難民たちはいったいどこに行くのでしょうね。

IETF OAuth WGの仕様全部見る - 2019/10

$
0
0

目的

OAuth 2.0の仕様について

  • "RFC6749"
  • "RFC6750"

の2つです。という紹介がされることがよくあります。

しかし、このRFCが策定された IETF OAuth WG には他にもたくさんの策定済み、策定中の仕様が存在します。

実際のサービスにOAuth 2.0を導入する際、扱うデータやアプリケーションの動作環境などにより、別のRFCの仕様を参照、導入する機会も発生するでしょう。例えば最近聞くことの多くなった PKCE と呼ばれる拡張仕様も RFC7636 です。

様々な仕様を「完全に理解した -> 全然わからん」の繰り返しで調べていっても途中で幽体離脱、もしくはいわゆる "RFC疲れ" という重篤な症状に陥る可能性もありますし、それを乗り越えられたとしてもプロフェッショナル達の「RFC番号が飛び交う会話」についていくためにはさらなる鍛錬を必要とするでしょう。

このような現状を少しでも改善するため、2019年10月23日現在のIETF OAuth WGにて策定済み、策定中の仕様をほぼ全部ざっくりと解説していきます。
自分の他の投稿やブログ、他の方の投稿で触れられた仕様もありますが、細かく誘導はせずに一旦こんな感じの仕様だよというところを残しておきます。

仕様 is どこ?

Oauth Status Pages

こんな風になってます。

スクリーンショット 2019-10-19 4.25.58.png

色々ありますが、雑に整理すると、PublishedってのがいわゆるRFCになったやつです。
IESGなんちゃらとRFC-Editor's なんちゃらってのはWGでの議論が進んでRFC化待ったなし、他のは熱い議論が進められてドラフトといった感じです。

他に draft-ietf-xxx じゃなく draft-(名前)-xxx みたいな仕様もあり、世の中にはこのレベルから拾っていく強者もいます。しかし、手練れ以外は簡単に手を出すなってじっちゃんが言ってた。ので今回は扱いません

ということで、現状でRFC化されたやつ、されてないやつで区切って見ていって、次回以降(があるなら)その差分を見ていくことにします。

ちなみに eyJから始まる〜!!! でおなじみ JSON Web なんとかは別のWGだったりします

Published

まずはRFCになっちゃってる奴から。

RFC6749 The OAuth 2.0 Authorization Framework 日本語訳

OAuth 2.0 のコアな仕様です。

「OAuth とはなんぞや」というところから、「Access Token の渡し方」「Access Token を用いたリソースアクセス」のところまで書かれています。

  • 概要、登場人物、用語
  • Client について、種類や識別子、認証方式
  • プロトコルに登場するエンドポイント
  • アクセス許可を与えるためのフロー
  • Access Token の発行、更新、リソースアクセスの部分
  • 様々な拡張について

この仕様についてはたくさんの解説記事や書籍もありますが、この仕様だけ見たら迷わずに AuthZ Server / Client 実装ができるかというと、そうでもないでしょう。

「Implicit Grant と Resource Onwer Password Credential Grantには気をつけろ。」

RFC6750 The OAuth 2.0 Authorization Framework: Bearer Token Usage

RFC6749では様々な種類の Access Token をサポートできるようになっており、シンプルな Bearer Token(ベアラートークン) という種類のトークンの扱い方がこの RFC6750 にて定義されています。

「電車の切符」などに例えられ「持っていることでアクセス許可されたものとみなせる」このトークンの存在もOAuth 2.0の主な特徴として挙げられます。

  • Bearer Token を用いたリクエスト方法(ヘッダ, フォームデータ, クエリ)
  • "WWW-Authenticate" ヘッダに含まれるレスポンスの形式、エラーコード
  • RFC6749 のフローでも定義されている Authorization Server から Bearer Token 形式の Access Token を受け取る方法

一般開発者向けにAPIを提供しているサービスについて、昔は独自に X-なんたら ヘッダみたいなのを使っていたところが多かった気がしますが、最近はこの Bearer Token を利用しているところも増えた印象です。

「「AUthorization: Beader xxx」という HTTP Header の指定方法は基本なので覚えましょう」

RFC6819 OAuth 2.0 Threat Model and Security Considerations

RFC6749 / RFC6750 にも Security Consideration にも大事なことが書かれていますが、これはより具体的な脅威と対策をまとめたドキュメントです。

  • 前提としている環境、攻撃者、アプリケーション、Client
  • 既にRFC6749 / 6750に組み込まれているセキュリティ機能
  • 脅威モデル
    • Client向け
    • AuthZ Endpoint
    • Token Endpoint
    • 各フロー
    • Access Token 更新
    • リソースアクセス
  • 上記脅威への対策
    • 全体
    • AuthZ Server
    • Client Application
    • Resource Server
    • その他

最初の方は用語の整理のような感じもあるので、これで比較的安全な設計/実装が可能になるのではないかと(当時は)思っていました。
最近は想定されるアプリケーションの変化に伴い個別にNative App/Browser Based App向けにベストプラクティス的な仕様が策定されています。

「これクッソ長いぞ。ランチ後に読んだら意識持ってかれるやつだ。覚悟しろよ。」

RFC8414 OAuth 2.0 Authorization Server Metadata

本投稿で紹介しているように、OAuth 2.0 にはコアとなる仕様と拡張仕様群があります。
Client を実装しようと思った時に、開発者は AuthZ Server の仕様が書かれたドキュメントを読んで各種エンドポイントなどの情報を取得する必要があります。

この仕様では AuthZ Server が自らの識別子、提供している各種エンドポイント、サポートしている Client 認証方式やパラメータと言ったメタデータを JSON 形式で提供する仕組みが定義されています。

# リクエスト
GET /.well-known/oauth-authorization-server HTTP/1.1
Host: example.com
# レスポンス
HTTP/1.1 200 OK
Content-Type: application/json

{
 "issuer":
  "https://server.example.com",
 "authorization_endpoint":
  "https://server.example.com/authorize",
 "token_endpoint":
  "https://server.example.com/token",
 "token_endpoint_auth_methods_supported":
  ["client_secret_basic", "private_key_jwt"],
 "token_endpoint_auth_signing_alg_values_supported":
  ["RS256", "ES256"],
 "userinfo_endpoint":
  "https://server.example.com/userinfo",
 "jwks_uri":
  "https://server.example.com/jwks.json",
 "registration_endpoint":
  "https://server.example.com/register",
 "scopes_supported":
  ["openid", "profile", "email", "address",
   "phone", "offline_access"],
 "response_types_supported":
  ["code", "code token"],
   "service_documentation":
 "http://server.example.com/service_documentation.html",
  "ui_locales_supported":
   ["en-US", "en-GB", "en-CA", "fr-FR", "fr-CA"]
}

この後に紹介していく拡張機能の仕様では、この "AuthZ Server Metadata" で表現されている機能を拡張するものもあります。
その場合、このメタデータにどのような値を追加するか、というところもそれぞれの仕様に含まれます。

ある時点での AuthZ Server の仕様にしたがって Client 実装を行うというユースケースではこの情報をあまり利用する機会はないかもしれませんが、後述する "Dynamic Registration" を用いた Client 設定の自動化などが行われるようになると結構重要になる仕組みでしょう。

「OpenID Connect にもメタデータが取れる仕組みがあるよ」

RFC6755 An IETF URN Sub-Namespace for OAuth

この仕様では RFC6749 のパラメータを拡張したい時などに使われるSub-Namespace(urn:ietf:params:oauth)が定義されています。

「拡張仕様で Token Endpoint に送る grant_type の指定に使われます。」

RFC7636 Proof Key for Code Exchange by OAuth Public Clients

Public Client から AuthZ Code Grant を利用する際、Client Secret を安全に保持できないために(カスタムURIスキームの重複の扱いなどによって) AuthZ Code を取得されることで Access Token まで取得されてしまうリスクがあります。

この仕様では AuthZ Code Grant の Access Token 取得までの一連の流れに AuthZ Code 交換用のキーである "Code Verifier" を利用することで攻撃を緩和させるための方法が定義されています。

  • プロトコル概要
    • Clien tは Code Verifier, Code Challenge を作成
    • Client は Code Challenge を認可リクエストに含んで送信
    • AuthZ Serverは Code Challenge と紐付けた AuthZ Code を返却
    • Client は AuthZ Code と Code Verifier を AuthZ Server の Token Endpoint に送信
    • AuthZ Server は Access Token 返却前に Code Verifier を検証
  • Security Consideration

Client は AuthZ Request を送る前に Code Verifier(Code Challenge) と自身のセッションを紐づけます。
AuthZ Server は AuthZ Request に含まれた Code Challenge と AuthZ Code を紐づけます。よって、セッションと AuthZ Code が紐づけられ、それを認可サーバーが検証します。
AuthZ Code が第3者の手に渡ったとしても、Code Verifier を知らなければ Access Token などの取得はできません。

鍵を一つだけセッションに保持するだけでセッションと AuthZ Code の紐づけが可能となるため、Client Secretを安全に保持できる Confidential Client でも導入するメリットはあります。
OAuth 2.0のコア仕様と合わせてもはや標準装備ぐらいの感覚で覚えておくのが良いでしょう。

「Client が毎回同じ OAuth Verifier を送って来たら意味ないのでそこだけちゃんとやれ。」

RFC7009 OAuth 2.0 Token Revocation

Client が Access Token を取得後、どのように扱うかについては様々です。

  • 永続化されたストレージに保存して非同期でAPIアクセスを行うようなユースケースの場合、Client は Access Token が有効な限り利用し続ける
  • Access Token をログインセッションに紐づけ、ログアウト時にセッションとの紐づけを破棄する

後者の場合、Client から AuthZ Server に対して Access Token / Refresh Token を無効化するリクエストを送り、AuthZ Server 側でも無効化することができればより安全性が高まります。

この仕様では、特定トークンの無効化を要求するためのエンドポイントの追加、リクエスト/レスポンスの形式が定義されています。

  • トークン無効化
    • Client が AuthZ Server に送るリクエスト
    • AuthZ Server からのレスポンス
    • CORSサポートについて

「AuthZ Serverが気合い入れて実装してもClientがちゃんと使わないと無意味。

RFC7662 OAuth 2.0 Token Introspection

OAuth 2.0 の登場人物である AuthZ Server, Resource Server ですが、両者は必ずしも同一のシステムで動作しているとは限りません。
ある程度規模が大きなサービスになると、機能単位でサーバー、ドメインを分けるという設計は珍しいものでもないでしょう。
リソースアクセス時に Resource Server は Client から Access Token を受け取り、Client が自身が提供するリソースへのアクセスを許可されていることを確認しますが、どのように実現するかについては定義されていません。

この仕様では Resource Server が AuthZ Server に Access Token についての問い合わせを行い、AuthZ Server がメタデータを返す方法を定義しています。

  • エンドポイント
    • リクエスト
    • レスポンス

Client から受け取った Access Token を Resource Server が AuthZ Server に問い合わせるための仕様 であり、Client 向けに提供されるエンドポイントおよび機能ではないことに注意が必要です。

似たような用途の仕様として、後述する "JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens" ではJWTを利用して構造化された Access Token を利用する方法が定義されており、仕様策定が進んでいます。
こちらは Resource Server が受け取った JWT 形式の Access Token を検証し、AuthZ Server への問い合わせを行わずにリソースアクセスを提供することも可能ですが、Access Token が無効化されることを考慮すると有効期限を短くするなど、細かい検討が必要です。

この辺りはAPIで提供される機能やシステム全体の負荷なども考慮し、設計していく必要があるでしょう。

「後でこのレスポンスをJWTにする仕様も出て来るぞ」

RFC8628 OAuth 2.0 Device Authorization Grant

OAuth 2.0 の AuthZ Code Grant や Implicit Grant なんてのは、PC/スマートフォンといったユーザーの手元にある端末のブラウザ上で Client から AuthZ Server の AuthZ Endpoint にリダイレクトして...という処理が行われます(いわゆる OAuth Dance)。
しかし、ブラウザを持たないデバイス、テレビなどのように認証処理が困難な場合、RFC6749で定義されているやり方では OAuth Dance を実現できません。

そこで、この仕様ではHTTPSのリクエストが送れ、URLの案内などの対話が可能なデバイスにおいて、手元の端末を用いて OAuth 2.0 のアクセス許可を可能とし、Access Token 等を取得する方法が定義されています。

  • プロトコル
    • Device AuthZ 用リクエスト : RFC6749 の AuthZ Request 相当の情報を送信
    • Device AuthZ 用レスポンス : ユーザーと対話するためのURLなど
    • ユーザーインタラクション : QRコードやURL表示などでユーザーを誘導し、認証やアクセス許可を行う
    • Device Access Token のリクエスト : Token Endpoint にポーリング
    • Device Access Token のレスポンス : ユーザーインタラクションが終わっていたらRFC6749相当のレスポンス

手元の端末でアクセス許可できるのは可能性を感じますね。
比較的新しいRFCなので今後これを用いた製品なども出てきそうです。

「QRコードはURL手打ちみたいなところはUXきついと言われるので導入時によく考慮すべし」

RFC7591 OAuth 2.0 Dynamic Client Registration Protocol

OAuth 2.0 の Client 登録といえば開発者が AuthZ Server を提供するサービス場で静的に登録するイメージが強いでしょう。
この仕様では Dynamic Client Registration つまり、動的に Client を登録する方法を定義しています。

  • Client のメタデータ
  • Client 登録用エンドポイント
    • リクエスト
    • レスポンス
# リクエスト
POST /register HTTP/1.1
Content-Type: application/json
Accept: application/json
Host: server.example.com

{
 "redirect_uris": [
   "https://client.example.org/callback",
   "https://client.example.org/callback2"],
 "client_name": "My Example Client",
 "client_name#ja-Jpan-JP":"\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u540D",
 "token_endpoint_auth_method": "client_secret_basic",
 "logo_uri": "https://client.example.org/logo.png",
 "jwks_uri": "https://client.example.org/my_public_keys.jwks",
 "example_extension_parameter": "example_value"
}
# レスポンス
HTTP/1.1 201 Created
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "client_id": "s6BhdRkqt3",
  "client_secret": "cf136dc3c1fc93f31185e5885805d",
  "client_id_issued_at": 2893256800,
  "client_secret_expires_at": 2893276800,
  "redirect_uris": [
    "https://client.example.org/callback",
    "https://client.example.org/callback2"],
  "grant_types": ["authorization_code", "refresh_token"],
  "client_name": "My Example Client",
  "client_name#ja-Jpan-JP":
     "\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u540D",
  "token_endpoint_auth_method": "client_secret_basic",
  "logo_uri": "https://client.example.org/logo.png",
  "jwks_uri": "https://client.example.org/my_public_keys.jwks",
  "example_extension_parameter": "example_value"
}

開発者のアクションを伴わない動的な登録というとどうしても「緩い」 Client が作成されるようなイメージもあります。
仕様ではあるソフトウェアが "software statement" を含むことで端末ごとやインスタンス単位で Client を登録するようなユースケースも想定されています。

RFC7592 OAuth 2.0 Dynamic Client Registration Management Protocol

RFC7591 で定義されている Client 登録に加え、登録されている情報の参照、更新、削除機能を定義します。

  • エンドポイント
    • 参照リクエスト : GET
    • 更新リクエスト : PUT
    • 削除リクエスト : DELETE

RFC8176 Authentication Method Reference Values

ここでは純粋な OAuth 2.0 の話ではなく OpenID Connect を用いたID連携の話をしましょう。
OAuth 2.0 で言う所の AuthZ Server / Resource Server である Identity Provider からエンドユーザーの情報を受け取るサービスがあり、このサービスが自前でパスワード認証、さらに TOTP や FIDO U2F などの多要素認証を導入しているような場合を考えます。

ID連携における認証強度を足し算で考えてみると

  • Identity Provider上でパスワード認証のみを行なったエンドユーザーに対しては追加で認証したい
  • Identity Provider上でパスワード以外の単一の認証(SMSのみとか)を行なったエンドユーザーに対しては別の認証方式で追加認証を求めたい
  • Identity Provider上でパスワード認証 + α の MFA を行なったエンドユーザーに対してはそのまま通したい

といった判定をしたいと思うこともあるでしょう。
そのためには、ID連携時に 「エンドユーザーがどの認証を済ませてきたのか」 をやりとりする必要があります。
OpenID Connect では、エンドユーザーの認証イベント情報をやりとりするための ID Token の中に "amr(Authentication Methods References)" というクレームを含むことが定義されています。

この仕様では、その "amr" クレームとしてやりとりされる値が定義されています。

  • "face", "fpt", "geo", "hwk", "iris", "kba", "mca", "mfa", "otp", "pin", "pwd", "rba", "retina", "sc", "sms", "swk", "tel", "user", "vbm", "wia"
  • "acr(Authentication Context Class Reference)" クレームとの関係

ここまで紹介しておいてなんですが、IdPとしては具体的な認証方式についてはなるべく渡したくないという気持ちもあるかもしれません。
ID連携しているサービス自体、もしくはこの結果を知る存在が悪意を持ち、 "pwd(=パスワード認証)" で認証済みのユーザーに対してパスワードリスト攻撃を行うこともできます。

認証方式にフォーカスした "amr" に対し、"acr(Authentication Context Class Reference)" は別途定義されたレベルを満たすことを要求したり、満たしていることを応答したりというやりとりに利用できるクレームです。

一般的なC向けのサービスではここまで踏み込むことはほぼないかもしれませんが、今後お金やセンシティブな情報をやりとりする仕組みにID連携が利用されるようになった時、この辺りのクレームの必要となるでしょうし、IdPとしても具体的な認証方式よりもサポートされる可能性が高いかもしれません。

「欲しいけど あげたくはない "amr"」

RFC7521 Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants

OAuth 2.0 をハードに使いこなしていくと、異なるIdentity System(例えば SAML など)との連携をしたくなるかもしれません。

この仕様では Token Endpoint に Access Token を要求する際に下記の部分で別システムのアサーションを利用するための方法が定義されています。

  • Authorization Grant : 別システムのアサーションから Access Token を発行する。RFC6749 でも "Extension Grants" として拡張できることが示されている
  • Client 認証 : 別システムのアサーションを用いて Client の認証を行う

この仕様で定義されているのは

  • アサーションの指定方法 : パラメータなど
    • Authorization Grant : "grant_type" の拡張、"assertion"
    • Client認証 : "client_assertion_type", "client_assertion"
  • アサーションの内容、処理方法 : アサーションにはどのような値が含まれていて、どう検証されるか
  • アサーション適用のシナリオ

といったあたりです。

ただし、この仕様で定義されているものはあくまでもフレームワークであり、この後に続く2つの仕様で具体的な利用方法を定義しています。

  • SAML Bearer Assertion : RFC7522
  • JSON Web Token : RFC7523

個人的に、以下のような仕組みを設計/実装する際にこの仕様を参考にしました。

  • Backend Server が存在する Native App が OAuth 2.0 の Server と Client の立ち位置で実装されてた
  • パスワード認証に ROPC(Resource Owner Password Credentials)が利用されてた (※このあたりの経験から一般論としてROPCをDisり始める事になる)
  • 外部ID連携(ソーシャルログイン)を実装するところでこれが使えそう

Googleアカウントでログインの例でいうと、これは OAuth 2.0(対Backend Server) × OpenID Connect(対Google) となります。
この仕様でいうと、Googleから受け取った ID Token や Authorization Code の値をアサーションとして利用することでソーシャルログインを実装しました。

ということで、割と応用の効くフレームワークだと思います。
サービスが複数のIDシステムの連携などを扱う場合、何か参考になることがあると思います。

RFC7522 Security Assertion Markup Language (SAML) 2.0 Profile for OAuth 2.0 Client Authentication and Authorization Grants

上記アサーションフレームワークのSAML用プロファイルです。

  • "grant_type" : "urn:ietf:params:oauth:grant-type:saml2-bearer"
  • "client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
# Authorization Grant
POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Asaml2-bearer&
assertion=PHNhbWxwOl...[omitted for brevity]...ZT4
# Client Authentication
POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=n0esc3NRze7LTCu7iYzS6a5acc3f0ogp4&
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Asaml2-bearer&
client_assertion=PHNhbW...[omitted for brevity]...ZT

SAMLのアサーションのどの値を用いて検証するかなども書かれています。

C向けというよりはB向けでSAML使われてるところとOAuth 2.0の組み合わせを考える人とかは、読んでみると良いかもしれません。

RFC7523 JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants

上記アサーションフレームワークのJWT用プロファイルです。

  • "grant_type" : "urn:ietf:params:oauth:grant-type:jwt-bearer"
  • "client_assertion_type" : "urn:ietf:params:oauth:client-assertion-type:saml2-bearer"
# Authorization Grant
POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer
&assertion=eyJhbGciOiJFUzI1NiIsImtpZCI6IjE2In0.
eyJpc3Mi[...omitted for brevity...].
J9l-ZhwP[...omitted for brevity...]
# Client Authentication
POST /token.oauth2 HTTP/1.1
Host: as.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=n0esc3NRze7LTCu7iYzS6a5acc3f0ogp4&
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3A
client-assertion-type%3Ajwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsImtpZCI6IjIyIn0.
eyJpc3Mi[...omitted for brevity...].
cC4hiUPo[...omitted for brevity...]

RFC7522と同様に、JWTのどの値を検証に用いるかなどが書いてあります。
アサーションの話はここまでにしましょう。

RFC8252 OAuth 2.0 for Native Apps

RFC6749 / 6750 が策定された2012年、世の中では「爆速」「スマホファースト」なるワードが飛び交っていました。
WebApp 中心だった OAuth 1.0 時代とは異なり、OAuth 2.0では船出から「モバイルアプリでOAuthどう実装するの問題」が発生し、そこから数年はカオスな状態が続きました。いや今も続いているのかもしれない。

  • RFC6749 でいうと Implicit Grant なのか
  • Implicit Grant 危ないと誰かが
  • Native App から AuthZ Code Grant 使いたいが Client Secret どうしよう
  • Public Client 向けに Client Secret なしの Client が作れるようになった
  • PKCE登場!これで勝てる?

このRFCは Native App から OAuth 2.0 を利用するにあたってのベストプラクティスがまとめられています。
RFC6749/6819 の想定になかった部分、サポートしきれていない部分も Security Considerations で整理されています。

もちろん Client 側からすると Server がサポートしている機能の範囲内で安全に実装するための方法を探る必要はありますが、これを読んで関連する仕様をみていくという進め方もあるかもしれません。

RFC7519 JSON Web Token (JWT)

いわゆるJWTのHeader/Payloadに含まれるclaim定義です。
これだけは JOSE WG ではなく OAuth WG だったことに最近気づきました。

JSON Web Token...というかよく使われているのは JSON Web Signature かと思いますが、基本的にルールに基づいた署名つきBase64URLエンコードと言えるため、その Payload には自由度があります。
しかし、2つのパーティー間で構造化されたデータをやりとりし、受け取った側が送信元などを検証するためにはこの仕様で定義されている claim の値が必要となります。

OpenID Connect の ID Token やこの投稿で紹介している JWT プロファイルのように、この仕様により定義されているクレームについてはそれを利用、それ以外のものに独自のクレーム名を付与して利用するべきです。

RFC7800 Proof-of-Possession Key Semantics for JSON Web Tokens (JWTs)

RFC7519 で定義されている claim に加えて、この仕様では

  • JWTの送信者が特定の Proof-of-Possession(PoP) Key を所有していること
  • JWTの受信者がそれを暗号技術を用いて確認できること

を実現するために必要な "cnf" claim の値と

  • 非対称な PoP Key
  • 暗号化された対称な PoP Key
  • PoP Key の識別子(kid)
  • PoP Key のURL

をどのように表現するかが定義されています。

    # 非対称な PoP Key
     {
      "iss": "https://server.example.com",
      "aud": "https://client.example.org",
      "exp": 1361398824,
      "cnf":{
        "jwk":{
          "kty": "EC",
          "use": "sig",
          "crv": "P-256",
          "x": "18wHLeIgW9wVN6VD1Txgpqy2LszYkMf6J8njVAibvhM",
          "y": "-V4dS4UaLMgP_4fY4j8ir7cl1TXlFdAgcx55o7TkcSA"
         }
       }
     }

    # 暗号化された対称な PoP Key
    {
      "iss": "https://server.example.com",
      "sub": "24400320",
      "aud": "s6BhdRkqt3",
      "nonce": "n-0S6_WzA2Mj",
      "exp": 1311281970,
      "iat": 1311280970,
      "cnf":{
        "jwe":
          "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhDQkMtSFMyNTYifQ.
          (remainder of JWE omitted for brevity)"
        }
    }

    # PoP Key の識別子(kid)
    {
      "iss": "https://server.example.com",
      "aud": "https://client.example.org",
      "exp": 1361398824,
      "cnf":{
        "kid": "dfd1aa97-6d8d-4575-a0fe-34b96de2bfad"
       }
    }

    # PoP Key URL 
    {
      "iss": "https://server.example.com",
      "sub": "17760704",
      "aud": "https://client.example.org",
      "exp": 1440804813,
      "cnf":{
        "jku": "https://keys.example.net/pop-keys.json",
        "kid": "2015-08-28"
       }
    }

そもそも、JSON Web Signature(RFC7515) では JWT の検証のために JWT Header に "jwk", "kid", "jku" などを使うことが定義されています。
この仕様では鍵を所有していることを表すために Payload に "cnf" の値として鍵の情報を含みます。
よって、JWTの検証に用いる鍵ではなく、例えば JWT を含むリクエスト自体で暗号を用いた検証をする時にも利用できます。

RFC-Editor's Queue

ここからは RFC 待った無し状態なので、もう見ておいて良いでしょう。

OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens

FAPIのようによりセキュアな要件が求められるユースケースにおいては、トークンを所持しているだけでリソースアクセスを行使しようとしている者がトークンの発行対象者であるとみなす Bearer Token ではなく、トークンの発行対象者が行使していることを検証可能な Sender-constrained Token と呼ばれるトークンが利用される場合があります。

RFC6749 の AuthZ Code Grant でやりとりされる AuthZ Code を用いて Access/Refresh Token を要求する時、Client は Client 認証を求められて AuthZ Server は AuthZ Code と Client の紐付けを検証します。
つまり AuthZ Code は Sender-constrained Token と言えます。

この仕様では、X.509証明書を用いた相互TLS認証をClient認証と Access/Refresh Token に紐付ける方法が定義されています。

  • クライアント証明書について、PKIを用いるものと自己署名ものを両方サポート
  • クライアント証明書を用いた Token Endpoint へのリクエスト時に AuthZ Server が Access/Refresh Token への紐付けを行う
  • Access Token を用いたリソースアクセス時にもクライアント証明書を利用し、Resource Server が検証する
  • JWT形式の Access Token や RFC7662 で定義されている Token Introspection のレスポンスで紐付けられたクライアント証明書を表現するために、RFC7800 の "cnf" claim を利用する
  # クライアント証明書のハッシュ値を含む Payload
  {
    "iss": "https://server.example.com",
    "sub": "ty.webb@example.com",
    "exp": 1493726400,
    "nbf": 1493722800,
    "cnf":{
      "x5t#S256": "bwcK0esc3ACC3DB2Y5_lESsXE8o9ltc05O89jdN-dg2"
    }
  }

TLS を利用した Sender-constrained Token の実現方法として RFC8471~8473 として策定された Token Binding と比較されることがありますが、Token Binding がHTTP/TLS自体の仕様に手を入れる必要があることから、Client/Serverが相互に仕様に準拠することで実装可能な本仕様の方が導入しやすいと言えます。

既にサポートされているライブラリも存在するので、興味のある方は探してみてください。

Resource Indicators for OAuth 2.0

OAuth 2.0 で Client がアクセスしたいリソースの範囲は scope パラメータとして要求されます。
OpenID Connect では openid という scope を利用していますし、Google などは scope をURL形式になっています。
この scope はリソースアクセスの種類を定義したものであり、AuthZ Server もしくは Resource Server が scope に対応する Resource Server 自体やエンドポイントの組み合わせを持ってアクセスコントロールを実現しているでしょう。

この仕様では、Client が AuthZ Request や Access Token Request に resource パラメータを指定することで直接利用したいリソースを指定する方法が定義されています。

 # AuthZ Request for AuthZ Code Grant
 GET /as/authorization.oauth2?response_type=code
     &client_id=s6BhdRkqt3
     &state=tNwzQ87pC6llebpmac_IDeeq-mCR2wLDYljHUZUAWuI
     &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
     &scope=calendar%20contacts
     &resource=https%3A%2F%2Fcal.example.com%2F
     &resource=https%3A%2F%2Fcontacts.example.com%2F HTTP/1.1
  Host: authorization-server.example.com

# Access Token Request for AuthZ Code Grant
POST /as/token.oauth2 HTTP/1.1
Host: authorization-server.example.com
Authorization: Basic czZCaGRSa3F0Mzpoc3FFelFsVW9IQUU5cHg0RlNyNHlJ
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
&code=10esc29BWC2qZB0acc9v8zAv9ltc2pko105tQauZ
&resource=https%3A%2F%2Fcal.example.com%2F 

個人的には scope との違いをもう少し明確に定義する必要がありそうですが、例えば scope と client_id との組み合わせで実際にアクセス可能なリソースが決まる場合などもありそうなので、この方式の方が実装しやすいケースもあるんだろうなと思っています。

OAuth 2.0 Token Exchange

RFC7521~7523 では、Client が JWT や SAML 2.0 のアサーションといったセキュリティトークンを用いた Access Token 要求方法が定義されています。

例えば AuthZ Server / Resource Server がマイクロサービス的な構成になっている場合、Resource Server は自ら受け取った Access Token から別のセキュリティトークンを要求、外部サービスに利用するといったユースケースはありそうです。

この仕様では、"Delegation", "Impersonation" という概念を意識しつつ AuthZ Server にセキュリティトークンを要求、取得する方法、つまりHTTPおよびJSONベースのSecurity Token Service(STS)実装のためのプロトコルが定義されています。

セキュリティトークン発行処理は Token Endpoint で行われます。

# Token Exchange Request
POST /as/token.oauth2 HTTP/1.1
Host: as.example.com
Authorization: Basic cnMwODpsb25nLXNlY3VyZS1yYW5kb20tc2VjcmV0
Content-Type: application/x-www-form-urlencoded

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange
&resource=https%3A%2F%2Fbackend.example.com%2Fapi
&subject_token=accVkjcJyb4BWCxGsndESCJQbdFMogUC5PbRDqceLTC
&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token
# Token Exchange Response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-cache, no-store

{
 "access_token":"eyJhbGciOiJFUzI1NiIsImtpZCI6IjllciJ9.eyJhdWQiOiJo...y5-kdXjwhw",
 "issued_token_type":"urn:ietf:params:oauth:token-type:access_token",
 "token_type":"Bearer",
 "expires_in":60
} 

特定の用途のための Access Token を要求するということで、先ほど説明した resource パラメータも任意ですが登場します。
セキュリティトークンの発行を柔軟に使うための仕様のため、パラメータの数もそれなりにあります。
比較的規模の大きなマイクロサービスの認証認可基盤みたいなのを設計/実装する際には目を通しておくと参考になるでしょう。

IESG Processing:

これも結構煮詰まってる感じの仕様です。

JWT Response for OAuth Token Introspection

RFC7662 で定義されている Token Introspection API のレスポンスはプレーンなJSONオブジェクトです。
この仕様では Token Instropection API のレスポンスに JWT を利用する方法を定義しています。

AuthZ Server が署名を作成した JWT 形式のレスポンスを返すことで、例えば受け取った Resource Server が Payload に含まれる値を改ざんすることなく別のサービスに送るような場合に受け取った側が検証可能になります。

# Token Introspection Request
POST /introspect HTTP/1.1
Host: server.example.com
Accept: application/jwt
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

token=2YotnFZFEjr1zCsicMWpAA
# Token Introspection Response
HTTP/1.1 200 OK
Content-Type: application/jwt

eyJ0eXAiOiJ0b2tlbi1pbnRyb3NwZWN0aW9uK2p3dCIsImFsZyI6IlJTMjU2In0.eyJ
...
JOd0ziYBpAbEpYGE4p3wog4

## JWT Header
{
 "typ": "token-introspection+jwt",
 "alg": "RS256"
}

JWT Header の typ claim なども独自のものとなるので、Introspection Endpointで作られたものであることを検証できます。
Token Introspection を利用しつつさらに...というユースケースはあまり想像できていないですが、こういうのもあるんだと覚えておくと良いでしょう。

JSON Web Token Best Current Practices

最近は様々なところでJSON Web Token(JWT)が使われるようになりました。
これは JWT を安全に実装して展開していくための実用的なガイダンスを提供することを目的としたベストプラクティスです。

内容は脅威と脆弱性に対するベストプラクティスの紹介という形です。
例えば、昔からある話で JWS の Header を書き換えて alg=none にしたり RS256 を HS256 に変更することでライブラリがその値で検証してしまったりする問題も書いてあります。

他にも色々あるので JWT についての仕様と合わせて読んでみるのも良いでしょう。

The OAuth 2.0 Authorization Framework: JWT Secured Authorization Request(JAR)

RFC6749 では AuthZ Request のパラメータはURLクエリパラメータとして指定され、ブラウザのリダイレクトにより AuthZ Endpoint に送られますが、パラメータの改ざんや置き換え、通信元が認証されていないといった課題があり、それを狙った攻撃も想定されています。

OpenID Connect で採用されている機能ですが、この仕様では JWT(JWS/JWE) 形式の値もしくはそのリファレンスの値を用いてパラメータの整合性、通信元の認証、AuthZ Requet の機密性を保証する方法が定義されています。

  • AuthZ Request
    • Request Object の指定
    • Request Object URI の指定、AuthZ Server のフェッチの方法
  • 検証方法
  • AuthZ Resonse (Error)
# JWTの値を指定
https://server.example.com/authorize?request=eyJ...

# JWTへのリファレンスを指定
https://server.example.com/authorize?
response_type=code%20id_token
&client_id=s6BhdRkqt3
&request_uri=https%3A%2F%2Ftfp.example.org%2Frequest.jwt
%23GkurKxf5T0Y-mnPFCHqWOMiZi4VS138cQO_V7PZHAdM
&state=af0ifjsldkj

FAPIのように、OAuth で扱う情報がセンシティブになると必須となる拡張仕様です。
ちなみにこの値を Server 側で保持するというやり方も別の仕様で提案されています。

OAuth 2.0 Pushed Authorization Requests

Active

最後は仕様策定真っ只中のドラフトの皆さんです。
詳細はまだこれから更新されていくものだとは思いますが、概要はそんなに変わらないと思うので紹介します。

RFCになってから/なりそうな段階で読むのと、MLやIETFの集まりに参加したりしながら関わっていくのではまた理解の度合いが変わると思います。
最終的に「仕様を書く」レベルまでを見据えるのであれば、その前の段階として「他の人が仕様書いてやっていくのを見る」のも大事かもしれませんね。

「IETFの流れとかは詳しい人紹介するからそっちに聞いてくれ」

JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens

OAuth 2.0 の Access Token について、RFC6749 の仕様では具体的なフォーマットなどが定義されていません。

   Access tokens are credentials used to access protected resources.  An
   access token is a string representing an authorization issued to the
   client.  The string is usually opaque to the client.  Tokens
   represent specific scopes and durations of access, granted by the
   resource owner, and enforced by the resource server and authorization
   server.

   The token may denote an identifier used to retrieve the authorization
   information or may self-contain the authorization information in a
   verifiable manner (i.e., a token string consisting of some data and a
   signature).  Additional authentication credentials, which are beyond
   the scope of this specification, may be required in order for the
   client to use a token.

この仕様では Access Token に JWT を利用するためのプロファイルが書かれています。
Server の Access Token 設計として、JWT を Access Token に利用しているところもありますが、それぞれが独自に検討したものであり細かい差異が存在します。
この仕様により標準化し、それに沿ったライブラリ、製品が出てくることで相互運用性を高められることを目的としています。

これまで標準化された他のRFCや OpenID Connect の ID Token などのように、rfc7519 で定義されているクレームをベースにして必要だと思われるクレームを追加し、Access Tokenの発行/検証処理が定義されています。
今までなんとなく Access Token に JWT を利用しようかとは思っていた開発者が、より安心して実装できる状態になることに期待ですね。

OAuth 2.0 Incremental Authorization

一昔前の「グロースハック」なテクニックの一つとして、ユーザーから新規登録時に取得する属性情報は最小限とし、必要になったら追加で取得していくべきだ。みたいな考えが広まり、Native Application の権限要求でも同様の考えがあると思います。

OAuth 2.0 を認証に利用しようと思う場合は別として、Client の開発者からすると実装コストという面ではアクセス許可を得るための OAuth Dance の回数は少ないに越したことはありません。
しかし、今後利用する可能性のある AuthZ Request にたくさんの scope を指定して Resource Owner がアクセス許可を要求する画面をみたとき、どう思うでしょうか?
ほとんどの Resource Owner は同意画面を見てないとは思いますが、中には細かく内容を見て「このscopeどこで使うんだよ」と思う人もいます。
かと言って、各処理で必要とする単一のscopeに対して個別に OAuth Dance を行い、各種 Token を管理するというのも複雑になります。

この仕様では、Server が Client - Resource Owner に対して以前要求された scope を保持しつつ新たに要求された scope に対してのアクセス許可を行う方法が定義されています。

と言っても内容を見ると、簡単なパラメータ追加があるのみで後は Server 側で頑張れと言ったところです。
Server の AccessToken 設計によっては既に導入されているところもあるかもしれませんが、標準化されることでより多くの Server でアクセス許可のUXが改善されることを期待したいところです。

Reciprocal OAuth

OAuth 2.0 の Resource Owner, Client, Server(AuthZ/Resource) の3者が登場する方式を 3-legged OAuth などと呼びますが、基本的には Client が Server のリソースにアクセスするための仕組みです。

しかし、実際のサービスでは

  • Client もリソースを持っており、Serverになり得る
  • Server も他のサービスの Client になり得る

ということで、これが重なると

  • ある Resource Owner に対して2つのサービス(サービスA/B)が Client/Server の両方になる

というケースもあるでしょう。このようなケースで仕様に忠実に従うと

  • サービスA -> サービスB に送られてサービスAからのリソースアクセスを許可する
  • サービスB -> サービスA に送られてサービスBからのリソースアクセスを許可する

というように2回 OAuth Dance を踊る必要があります。

この仕様では、Resource Owner がよりシームレスにアクセス許可をできるような拡張仕様が定義されています。

  • Client は通常の OAuth 2.0 のフローを始める
  • Server は Access Token Response(AuthZ Code) or AuthZ Response(Implicit)に "reciprocal" パラメータで必要なリソースを指定する(Reciprocal Scope Request)
  • Client は Resource Owner の同意を得たあと、Server に Reciprocal Authorization Code を送る
  • Server は取得したコードから Client に向けてAccess Token Request を送る
   # Access Token Response(AuthZ Code)
   HTTP/1.1 200 OK
   Content-Type: application/json;charset=UTF-8
   Cache-Control: no-store
   Pragma: no-cache

   {
     "access_token":"2YotnFZFEjr1zCsicMWpAA",
     "token_type":"example",
     "expires_in":3600,
     "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
     "reciprocal":"example_scope",
     "example_parameter":"example_value"
   }
   # AuthZ Response(Implicit)
   HTTP/1.1 302 Found
   Location: http://example.com/cb#
       access_token=2YotnFZFEjr1zCsicMWpAA&
       state=xyz&
       token_type=example&
       expires_in=3600&
       reciprocal="example_scope"   

図にするとこんな感じです。

Reciprocal OAuth.png

最後の Party B が Party A から Access Token を貰うところの認証のあたりとか、まだちょっと怪しいところがあるので今後整理されると思います。

OAuth 2.0 Security Best Current Practice

これはRFC6749/6750 に対して RFC6819 で脅威と対策が整理された後の様々な変化に対して更新された OAuth 2.0 のセキュリティベストプラクティスです。

  • RFC6819 に書かれてる内容についても何を推奨するかを整理
  • ブラウザのフラグメント処理とか、変わってるものに追従
  • Open Banking, eHealth, eGovernment ... より安全性が求められる環境も考慮
  • Dynamic Registration のユースケースも考慮

といった観点から、攻撃のモデルと対策として推奨される実装が整理されています。

「Implicit Grant と Resource Onwer Password Credential Grantには気をつけてください。」

この中で、Implicit Grant と Resource Onwer Password Credential Grant にも言及されています。

   In order to avoid these issues, clients SHOULD NOT use the implicit
   grant (response type "token") or any other response type issuing
   access tokens in the authorization response, such as "token id_token"
   and "code token id_token", unless the issued access tokens are
   sender-constrained and access token injection in the authorization
   response is prevented.

"Bearer Token" に対する "Sender-Constrained Token" がここでも出てきています。

ROPCについては "MUST NOT be used" です。

   The resource owner password credentials grant MUST NOT be used.

...

   Furthermore, adapting the resource owner password credentials grant
   to two-factor authentication, authentication with cryptographic
   credentials, and authentication processes that require multiple steps
   can be hard or impossible (WebCrypto, WebAuthn).

他にも色々あるので、読むのは大変ですが実践的な内容になっていると思います。

「整理の仕方がうまいのか、6819よりは辛くないかも?」

OAuth 2.0 for Browser-Based Apps

この仕様は Browser-Based Apps の OAuth 2.0 実装についてのベストプラクティスが整理されています。

OAuth 2.0 において、当初から Browser-Based Apps はいわゆる Public Client として Native Apps と同じような扱いになっています。
Native Apps については RFC8252 としてベストプラクティスが用意され、それをサポートしたいわゆる AppAuth と呼ばれるライブラリ群も用意されています。

Native Apps と Browser-based Apps の実装の類似点に注目し、ブラウザ上で実行されることで追加で考慮しなければならない点を含みます。

  • 基本は AuthZ Code Grant + PKCE 利用
  • バックエンドサーバーの有無によって推奨される実装を整理
  • AuthZ Code Grant への追加要件
  • Refresh Token の有効期限について

SPAだと JWT や localStrage がどうこうで度々燃えそうになりますが、結局はできることとできないことを整理してリスク需要していくという落とし所を探すしかありません。
上記 Security Best Current Practice への参照も結構あるようなので、合わせて読むのが良いでしょう。

最後に

日頃、OAuthやOpenID Connect(OIDC)あたりでQiitaの記事を検索して大きな誤りがないかを確認していますが、RFC6749/6750 + 7636ぐらいから先に踏み込まれた記事はあまり多くありません。
また、一部の詳しすぎる人が初学者向けの投稿に関連するRFC一覧などを載せたところで、一体どれだけの読者がそこから先に進んでいるのでしょうか?と思うところもあります。

これだけ読んでももちろん全ての仕様を完全に理解することは困難です。
しかし、既存のユースケースや設計/実装の課題に対し、どのようなアプローチで解決するための仕様があるかを覚えておけば、自分の設計や実装の場で壁にぶち当たった時にちょっとだけ役立つかもしれません。

ではまた。

(追記:これ書いてる時の様子)

Viewing all 199 articles
Browse latest View live