OAuth認証でTwitterを利用するWebアプリケーション(PHP twitteroauthの場合)


新しい記事:OAuth認証でTwitterを利用するWebアプリケーション(PHP PECL/oauthの場合)

OAuth認証が必要なAPIでは、Twitter APIとFacebook APIが有名ですが、ちょっと仕様が違うので、使い方を簡単に紹介しようとすると、記事を分けなければなりません。さらに、JavaとPHPの両方を学べるというお得な本を書いたこともあって、その読者をサポートするために、JavaとPHPの両方で記事を書かなければなりません。都合、以下の4パターンが必要です。

今回は、「PHPでTwitter API」です。「JavaでFacebook API」はまた別の機会に。

アプリの登録

http://dev.twitter.com/appsでアプリケーションを登録し、Concumer keyとConsumer secretを取得してください。その際、クライアントアプリケーションではなくブラウザウェブアプリケーションとして登録するように注意してください。

OAuthのためのライブラリ

PHPには、標準の「PECL/oauth」や準標準と言える「Pear HTTP_OAuth」、Twitterに特化した「Pear Services_Twitter」がありますが、例によって、PECLはWindowsで使いにくいし、HTTP_OAuthはベータ版、Services_Twitterはアルファ版なので、さっさと見限って「twitteroauth」を使いましょう。

OAuth.phpとtwitteroauth.phpをダウンロードして、ウェブサーバで配信できる場所に置きます。

cURL

XAMPP

PHPでcURLを使うために、php.iniに以下のように記述して、Apacheを再起動します。

extension=php_curl.dll

Ubuntu

次のコマンドを実行してcURLモジュールをインストールしてからApacheを再起動します。

sudo apt-get install php5-curl

お約束ですが、このあたりでもうついて行けないという場合は、拙著『Webアプリケーション構築入門』などを参照してください。以下のPHPプログラミングで必要な知識も本書にまとめてあります。

OAuth認証

以下の3画面で認証することにします(oauth-start.phpとoauth-end.phpは一つのファイルにまとめることもできますが、ここではわかりやすいように二つに分けています)。

  1. OAuth認証のためのURIを生成し、それにアクセスするためのリンクを表示するoauth-start.php
  2. アプリケーションを許可するTwitterページ
  3. Twitterからのコールバックを受信し、access tokenとtoken secretを取得するoauth-end.php

実装

oauth-start.php

OAuth認証の入り口となるoauth-start.jspは以下のようになります。「Consumer key」や「Consumer secret」、コールバックURIの部分は、状況に合わせて書き換えてください。

<?php
require_once('twitteroauth.php');

session_start();

$_SESSION['consumer_key'] = Consumer key;
$_SESSION['consumer_secret'] = Consumer secret;
$callbackUri = 'http://localhost/twitter/oauth-end.php';

$connection = new TwitterOAuth($_SESSION['consumer_key'], $_SESSION['consumer_secret']);
$request_token = $connection->getRequestToken($callbackUri);
$_SESSION['oauth_token'] = $token = $request_token['oauth_token'];
$_SESSION['oauth_token_secret'] = $request_token['oauth_token_secret'];

$authUrl = $connection->getAuthorizeURL($token);
?>
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>OAuth Start</title>
  </head>
  <body>
    <p><a href="<?php echo $authUrl; ?>">Twitter OAuth認証開始</a></p>
    <p><a href="logout.php">logout</a></p>
  </body>
</html>

oauth-end.php

コールバック後の処理を実装するoauth-endは以下のようになります。

<?php
require_once('twitteroauth.php');

session_start();

$connection = new TwitterOAuth($_SESSION['consumer_key'], $_SESSION['consumer_secret'],
                $_SESSION['oauth_token'], $_SESSION['oauth_token_secret']);
$access_token = $connection->getAccessToken($_GET['oauth_verifier']);
unset($_SESSION['oauth_token']);
unset($_SESSION['oauth_token_secret']);

$_SESSION['access_token'] = $access_token;
?>
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Twitter OAuth認証完了</title>
  </head>
  <body>
    <pre><?php print_r($access_token); ?></pre>
    <p>access_tokenをセッションに保存した</p>
    <p><a href="post.php">送信テスト</a></p>
    <p><a href="logout.php">logout</a></p>
  </body>
</html>

つぶやく

認証が終わったら、以下のようなコードでつぶやけます。

<?php

header('Content-type:text/plain; charset=utf-8');
require_once('twitteroauth.php');

session_start();

$access_token = $_SESSION['access_token'];
$connection = new TwitterOAuth($_SESSION['consumer_key'], $_SESSION['consumer_secret'],
                $access_token['oauth_token'], $access_token['oauth_token_secret']);

$message = "テスト at " . time();
$parameters = array('status' => $message);
$status = $connection->post('statuses/update', $parameters);

if (isset($status->errors)) { //エラーがある
  $errors = $status->errors;
  $error = $errors[0];
  echo $error->message."\n";
}

print_r($status);

ブラウザを再起動すればはじめに戻りますが、次のようなlogout.phpを作っておいてもいいでしょう。

<?php
session_start();
session_destroy();
?>
<!doctype html>
<html>
  <head>
    <title>Logout</title>
  </head>
  <body>
    <p><a href="oauth-start.php">start</a></p>
  </body>
</html>

Wolfram CDF PlayerをMathematicaとして使う方法


2016.1.27 Edgeでは動きません。

2015.3.14 Chromeでは動きません。

http://www.unfindable.net/umm3/

計算可能ドキュメント形式(Computable Document Format, CDF)を閲覧するためのソフトウェアWolfram CDF PlayerとMathematicaの関係は、Portable Document Format (PDF)を閲覧するためのソフトウェアAdobe ReaderとAcrobatの関係に似ています。Wolfram CDF Playerで閲覧可能なCDF文書を作るにはMathematicaが、Adobe Readerで閲覧可能なPDF文書を作るにはAcrobatが必要です。

しかし、CDFとPDFには大きな違いがあります。PDF文書は内容が固定された静的な文書であるのに対して、CDF文書は内容を変化させられる動的な文書です。下はCDF文書の簡単な例です。Wolfram CDF Playerがインストールされている環境なら、aの値を変えながらSin[a x]をプロットしてみることができます。CDF文書の内容は計算によって変化するのです。


Sin[a x]のaの値を変えられるなら、もっと大胆に「Sin[a x]」という式全体を変えられるのではないかと考えるのは自然でしょう。Mathematicaの式を処理できるCDF文書、それはMathematicaとして使えるCDF文書です。使い勝手は多少悪くても、「Mathematicaを使いたいけれど高すぎて買えない」という人にとっては有用でしょう。みんな大好きWolframAlphaも、Mathematicaのすべての機能を使えるわけではありませんし。

残念ながら、直接的な方法はうまくいきません。CDF文書に入力できるのは数値だけであり、「Sin[a x]」のような文字列は入力できないからです。しかし、コンピュータ上で表現されるすべてのものは、メモリ上では数で表現されています。「Sin[a x]」のような式ももちろん数で表現されています(メモリのことがよくわからない人は、ゲーデル数を思い出してもいいでしょう)。ですから、Mathematicaの式を一度数値に変換してからCDF文書に入力し、CDF文書内でそれを元に戻すというような工夫をすれば、CDF文書で式を扱えます。このアイディアを実現したのが、以前紹介したUniversal Mathematica Manipulator (UMM)です。

UMMには、Mathematicaの式を変換してできる数値が長大なため入力に手間がかかるという問題がありました。Wolfram CDF Playerには、文書上でクリップボードからの貼り付けができないという制約があるため、長大になる数値をCDF文書上で入力するための面倒な仕掛け(VBScriptやAppleScript)が必要でした。(Mathematicaに付属するCDF Playerなら貼り付けられます。このように仕様がばらばらなことが後で混乱を生まないことを祈ります。)

ここでは別の方法を紹介します。

まず、Wolfram CDF Playerをインストールします。これがなくては始まりません。

次に、PHPを使えるウェブサーバをlocalhostに用意します。WindowsならXAMPPを導入するのが簡単でしょう。

下のような内容のexpression.phpをhttp://localhost/umm3/expression.phpというURLで呼び出せるようにしておきます。ディレクトリumm3は、ウェブサーバから書き込めるようにしてください。

<?php

$file = 'expression.txt';

if (isset($_GET['body'])) {
  file_put_contents($file, $_GET['body']);
  echo $_GET['callback'].'()';
} else {
  if (is_file($file)) echo file_get_contents($file);
  echo '(* OK *)';
}

PHPのmagic_quotes_gpcがOffであることを確認します。XAMPPの場合はデフォルトでOffになっています。Macの場合はphp.iniを編集してApacheを再起動する必要があるかもしれません。

http://www.unfindable.net/umm3/にアクセスします。

お約束ですが、上の手順がよくわからない人は、拙著『Webアプリケーション構築入門』などを参照してください。

以上の準備ができたらhttp://www.unfindable.net/umm3/にアクセスします。ピタゴラス3体問題のような比較的大きなプログラムも実行できることを、Wolfram CDF Player 8.0.4(Win, Mac)で確認しています。

Twitter時代の文字の数え方


正確には、「Unicode 3.1時代の文字の数え方」なのでしょうが、Unicodeの最新バージョンが6.0の今、それではぱっとしないので。

はじめに

一言で言えば、「𠮷野家」は3文字なのか4文字なのか、という話です。これはもちろん3文字で、Twitterの1回のつぶやきに46個含められます。しかし、4文字だとみなされるケースがたくさんあるのです。

30過ぎて数も数えられない、なんてことになるとは思っていなかったのですが、文字を数えるのはけっこう難しいです。とはいえ、Twitterがこれだけ普及しているのに、「難しい」と言って済ますわけにもいかないので、ちょっとまとめておきましょう(拙著『Webアプリケーション構築入門』で使ったもの+αで)。

注意:ちゃんとやるためには、Unicode正規化の話とか、もしかしたらIVSの話とか、いろいろやらなければいけないのかもしれませんが、ここでは話を少し単純にしています。

まとめのまとめ

Unicodeの登録文字数が少ない時代に作られたシステムが、登録文字数の多い時代に対応できていないという問題があります。例えば「𠮷」(U+20BB7)はもちろん1文字ですが、UTF-8・UTF-16ともに、想定を超える4バイトで表されるため、2文字とみなされることがあります(UTF-8のナイーブな想定は3バイト、UTF-16のそれは2バイト)。

𠮷
コードポイント U+20BB7 U+91CE U+5BB6
UTF-8 F0 A0 AE B7 E9 87 8E E5 AE B6
4バイト 3バイト 3バイト
UTF-16 D842 DFB7 91CE 5BB6
4バイト 2バイト 2バイト

対策方法は以下の通りです。

  • HTML5のフォーム検証:あきらめる
  • JavaScript:注意が必要(ふつうの方法ではダメ)
  • PHP:問題なし
  • Java:注意が必要(ふつうの方法ではダメ)
  • MySQL:5.1以前ではあきらめる。5.5以降ではutf8mb4を使う
  • Perl 5.?:問題なし(コメントでの指摘)
  • C#:注意が必要(ふつうの方法ではダメ。正規表現も要注意)
  • Visual Basic:注意が必要(ふつうの方法ではダメ。正規表現も要注意)
  • ASP.NET:注意が必要(ふつうの方法ではダメ。正規表現も要注意)

PerlやPHPは問題ないとは言っても、MySQLを使うようなアプリケーションでは問題が起こることに注意してください。

この結論に至る理由を、少し詳しく説明します。


クライアント側

HTML5

HTML5のフォームでは入力されたデータの検証ができるようになっているのですが、この機能を使って文字数をチェックするのは、やめたほうがよさそうです。

maxlength属性

HTML5のフォームでは、input要素やtextarea要素において、入力できる文字数をmaxlength属性で指定できるようになっています。たとえば、次のようなinput要素では、3文字までしか入力できません。

<input type="text" maxlength="3" />

ところが、ブラウザによっては、ここに「𠮷野家」という3文字を入力できません。そもそもこの3文字を正しく表示できないブラウザもあるようなので、手元のWindows上のブラウザで調べてみました(環境に依ります。たとえば、MacのSafariでは表示が○になります)。

ブラウザ 表示 入力
Chrome 12.0.742.100
Safari 5.0.5 ×
Firefox 21.0 ×
IE 8 ×
IE 9 ×
IE 10
Opera 12.15 × ×

入力「×」のブラウザでは、「𠮷」が2文字とみなされるため、2文字目まで、つまり「𠮷野」までしか入力できません。

Mozillaの文書には、Unicode code pointsで数えると書いてあるので、そのうち改善されるのかもしれませんが、現時点ではTwitterのために「maxlength="140"」を使うことはできません。

pattern属性

Firefox 21とChrome 27、IE 10、Opera 12.15は、「pattern=".{0,3}"」(任意の文字からなる0から3文字)のような正規表現を使った検証にも対応していますが、やはり「𠮷野家」は4文字とみなされてしまいます。


JavaScript

追記:javascript – でBMP以外のUnicode文字をきちんと扱う(404 Blog Not Found)

JavaScriptでは、文字列strの長さをstr.lengthで取得できることになっていますが、残念ながらこの方法では、上記のすべてのブラウザで「𠮷野家」は4文字とみなされてしまいます。

かつて、Twitterのウェブサイトではこの方法で文字数を数えていたと思われます。APIでならつぶやける文字列が、ウェブサイトからでは文字数超過でつぶやけなかったのです。Twitterに報告して直してもらいましたが(参考リンク切れ)、「𠮷野家」を4文字とみなすクライアントはまだたくさんあるでしょう(手元のHootSuiteとついっぷるはそうでした)。

ちなみに、「.{0,3}」という正規表現で検証することもできません。「𠮷野家」はやはり4文字だとみなされます。

次のような関数を使えば、JavaScriptでも文字列の長さを正しく測ることができます(Stringオブジェクトをそのまま拡張する方法が、「サロゲート・ペアに対応した文字列操作関数を書いてみた」で紹介されています)。

function strlen(str) {
  var i = 0, len = str.length, result = 0;
  while (i < len) {
    result++;
    var x = str.charCodeAt(i++);
    if (0xD800 <= x && x < 0xDC00) i++;
  }
  return result;
}

この方法で数えた文字数が140以下なら、Twitterでつぶやくことができるでしょう(サーバ側も自分で作るときは、サーバ側でのチェックも必要です)。


サーバ側

PHP

mb_strlen()

関数mb_strlen()で正しく長さを測れます。ただし、クライアントから送信されたデータを処理する際には、関数mb_check_encoding()を使って、文字以外の不正なものがないことをまず確認するといいでしょう(拙著『Webアプリケーション構築入門』でも触れました)。

正規表現

長さがある範囲にあることをチェックするだけなら、徳丸浩『体系的に学ぶ 安全なWebアプリケーションの作り方』(ソフトバンククリエイティブ, 2011)で紹介されている方法が洒落ています。

if (preg_match('/\A\P{Cc}{1, 140}\z/u', $str) == 1) {
  // OK

Java

codePointCount()

Javaでは、文字列strの長さをstr.length()で取得できることになっていますが、残念ながらこの方法では「𠮷野家」は4文字とみなされてしまいます(JavaScriptの場合と同じです)。

拙著でも紹介したように、長さを正しく測りたい場合は、「str.codePointCount(0, str.length())」としなければなりません。ただし、クライアントから送信されたデータを処理する際には、条件「str.indexOf(0xFFFD) < 0」をチェックして、文字以外の不正なものがないことをまず確認するといいでしょう。

正規表現

長さがある範囲にあることをチェックするだけなら、徳丸浩『体系的に学ぶ 安全なWebアプリケーションの作り方』(ソフトバンククリエイティブ, 2011)で紹介されている方法が洒落ています(PHPの場合と同じです)。

if (str.matches("\\P{Cc}{1,140}")) {
  // OK

MySQL

MySQL 5.1以前

UTF-8で4バイトになるような文字を文字として扱うようにはなっていないので、文字列はbinaryとして保管し、文字列の長さを関数char_length()で測ったりするのはあきらめましょう(PHPやJavaなど、MySQLの外で測るのが簡単)。n文字まで格納したいなら、VARCHAR(4n)にしなければなりませんが、使う文字によってはn文字より多く格納できてしまうのが困りものです(全部ASCIIなら4n文字)。

MySQL 5.5以降

UTF-8で4バイトになるような文字を文字として扱うためのutf8mb4が導入されたので、これを使うのが簡単です。文字列の長さは関数char_length()で正しく測れます。n文字まで格納したいときも、VARCHAR(n)で大丈夫です。


おわりに

「𠮷野家」は単なる例です。「𠮷野家」でも「吉野家」を出してくるGoogleは偉いとは思いますが、拙著でも述べたように、それでも「吉野家」と書くべきだと私は思います。しかし、ここで述べたような話は、絵文字が大量に導入されたUnicode 6.0の普及が進めば、深刻な問題になるかもしれません。Javaが虐げられる理由が増えなければいいのですが。

想定外(あるいは無視された想定)の津波が原子力発電所を襲うほどの深刻度ではありませんが、ITの世界では、過去に2000年問題がありましたし、現在もこういう問題が発生していますし、将来もおそらく似た問題は起こるのでしょう(例えば2038年問題とか)。

先祖を笑えるようにはならず、先祖からは笑われ続けるのでしょうか。

文字を数えるのはけっこう難しいです。


追記

Perl

Perlは問題ないということをコメントで教えてもらいました。しかし、まさにここで紹介した問題のせいでコメントを壊してしまったので、本文にコードを追記します。

$ perl -v
This is perl 5, version 14, subversion 0 (v5.14.0) built for darwin-2level

$ perl -Mutf8 -E 'say length(𠮷野家)'
3

.NET Framework 4.0

追記

.NET Framework 4.0で文字列の長さを知りたい時は、StringInfo.LengthInTextElementsを使います。String.Lengthではダメです。PHPやJavaでは有効だった正規表現による長さの検査が使えないことにも注意してください。参考:田丸健三郎『UnicodeによるJIS X0213実装入門』(日経BPソフトプレス, 2008)

C#

String str = "𠮷野家";
System.Console.WriteLine(str.Length);
//4

StringInfo si = new StringInfo("𠮷野家");
System.Console.WriteLine(si.LengthInTextElements);
//3

Regex pattern = new Regex("^\\P{Cc}{4}$");
System.Console.WriteLine(pattern.IsMatch("𠮷野家"));
//True(「4文字」にマッチ)

Visual Basic

Dim str As String = "𠮷野家"
Console.WriteLine(str.Length)
'4

Dim si As New StringInfo("𠮷野家")
Console.WriteLine(si.LengthInTextElements)
'3

Dim pattern As New Regex("^\P{Cc}{4}$")
Console.WriteLine(pattern.IsMatch("𠮷野家"))
'True(「4文字」にマッチ)

ASP.NET

拙著『Microsoft Visual Web Developer 2008 Express Edition入門』で紹介した、正規表現による検証コントロールであるRegularExpressionで「.{0,3}」のような正規表現を設定しても、文字列の長さを正しく検証することはできません。「𠮷野家」は4文字とみなされてしまうからです。

PHPやJavaで有効だった「\P{Cc}{0,3}」という正規表現は機能しないようです(機能したとしても、上述のC#やVisual Basicの結果を見ると役には立たなそうですが)。

そこで、CustomValidatorを使うことになります。しかし、MSDNで公開されている「方法 : ASP.NET サーバー コントロールをカスタム関数で検証する」はダメです。文字列の長さを正しく測れません。正しく検証するためには、次のようなコードを使うといいでしょう。


StringInfo si = new StringInfo(args.Value);
args.IsValid = si.LengthInTextElements <= 3; [/csharp] クライアント側で処理したいときも、MSDNの方法はダメで、次のようなJavaScriptが必要になるでしょう(関数strlen()の定義は上述)。

function validateLength(oSrc, args) {
    args.IsValid = (strlen(args.Value) <= 3);
}

JSONに関しては、例えば、PHPを避ける


ここで紹介している問題は、PHP 5.4で解決されました。

JavaScriptのための記法であるJSONですが、最近はさまざまなプログラミング言語でサポートされています。たとえば、PHP 5には、JSON文字列をオブジェクトに変換するための関数、json_decode()が用意されています。

<?php
var_dump(json_decode('{"id":12345}'));
&#91;/php&#93;

<p>この実装はなかなか親切で、<a href="http://codepad.org/XT2fs3S5">実行結果</a>を見るとわかるように、データが数値なら、数値として取り出してくれます。</p>

<p>しかし、最近ではTwitterのつぶやきのIDがintの範囲を超えたりしているので、ちょっと不安です。下の例では、$idaと$idbの値は異なることが期待されますが、<a href="http://codepad.org/BoGUJERQ">実行結果</a>を見るとわかるように、intの範囲を超えた場合は自動的にfloatになるので、期待は裏切られます。</p>

[php]
<?php
$a = json_decode('{"id":10000000000000000}');
$b = json_decode('{"id":10000000000000001}');

$ida = $a->id;
$idb = $b->id;

var_dump($a);
var_dump($b);

if ($ida==$idb) {
  echo 'a==b';
}

ですから、JSONを書くときには、数値であっても文字列として(引用符で囲って)書くようにするといいでしょう()。つまり、データを数値として扱うことの責任は、アプリケーション側に持ってもらうしかないでしょう。

PHPの他にも、Perlやawk、JavaScriptなどは、整数がintの範囲を超えたときは、自動的にfloatになる仕様なので、気をつけないと悲惨なことになるでしょう。この機能、フェルマーの最終定理の「反例」などと冗談を言う以外に使い道ってあるんですかね。

PHPのマニュアルによれば、次のように書くことで、データを数値ではなく文字列として取り出せるようになるらしいのですが、この仕様は正式版にはまだ取り入れられてはいません。

<?php
var_dump(json_decode('{"id":12345}, false, 512, JSON_BIGINT_AS_STRING));
&#91;/php&#93;

<p>JSONに関しては、例えば、PHPを避ける</p>

<p>と言いたいところですが、JavaScriptのほうがもっと深刻ですね。jQuery.getJSON()もだめですね。</p>

<p>こういうだめな言語を使わざるを得ない開発者をサポートするために、TwitterはJSON文字列にid_strという文字列属性を含めるようになったので、Twitterの場合はこれでなんとかなります<del datetime="2013-03-05T08:15:19+00:00">(それでもPHPならXMLのほうがいいかもしれません)</del>。</p>

<p>どうしてもPHPでJSONを使いたいときは、次のようにデコードの前にJSON文字列を修正するのが簡単でしょうか(「id」で終わるものすべてを文字列にする例です)。</p>

[php]
$json = preg_replace( '/id":(\d+)/', 'id":"\1"', $json);

どうしてもPHPでJSONを使いたいときは、標準のjson_decode()ではなく、PEARのServices_JSON(すでに開発は終了している)のコードを修正するのが簡単でしょうか。

511c511
<                         : (float)$str;
---
>                         : $str;

JavaScriptではどうするのが正解なのでしょう。

OAuth認証でFacebookを利用するWebアプリケーション(PHPの場合)


OAuth認証でTwitterを利用するWebアプリケーションの作り方を以前紹介しました。OAuth認証はさまざまなサイトで使われていますが、使い方がちょっとずつ違っていたりして、プログラミングが苦手な人には敷居が高い気がします。せっかくおもしろいことを思いついても、最初の段階で躓いて挫折するのはがっかりなので、簡単なサンプルがたくさんあるといいと思います。

というわけで、PHPでFacebookのOAuth認証を利用する例を紹介しましょう。

Facebookの開発者サイトでアプリケーションを登録し、アプリケーションIDシークレットキーを取得します。

アプリケーション登録

2つのファイルでOAuth(説明用)

OAuth認証の開始ページを/facebook/oauth-start.php、終了ページを/facebook/oauth-end.phpとします。

oauth-start.phpは次のようになります。おおざっぱなTwitterと比べて、Facebookでは、OAuth認証時に認可する機能をかなり細かく設定できます。ここでは、掲示板への登録を許可するように、「scope=publish_stream」をつけておきます。他の権限については、Extended Permissionsを参照してください。

<?php
session_start();
$_SESSION['application_id'] = アプリケーションID;
$_SESSION['application_secret'] = シークレットキー;
$_SESSION['redirect_uri'] = 'http://localhost/facebook/oauth-end.php';
$_SESSION['state'] = md5(uniqid(rand(), TRUE)); //CSRF対策

$url = 'https://www.facebook.com/dialog/oauth'
        . '?client_id=' . $_SESSION['application_id']
        . '&redirect_uri=' . urlencode($_SESSION['redirect_uri'])
        . '&state=' . $_SESSION['state']
        . '&scope=publish_stream'; //投稿する場合
?>
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>OAuth Start</title>
  </head>
  <body>
    <p><a href="<?php echo $url; ?>">OAuth</a></p>
  </body>
</html>

oauth-start.phpが生成するURLにアクセスすると、認証画面になります。

認証画面

「許可する」をクリックすると、oautu-start.phpで指定したリダイレクトURL(ここではhttp://localhost/facebook/oauth-end.php)にリダイレクトされます。oauth-end.phpでは、リダイレクトが伴うcodeというパラメータを利用して、認証を完了させます。(ちなみに、http://www.facebook.com/settings/?tab=applicationsで許可を取り消せます。)

<?php
session_start();
if (!isset($_SESSION['state'], $_GET['state']) || ($_SESSION['state'] !== $_GET['state'])) {
  session_destroy();
  die();
}
        
$cxContext = NULL;
//$cxContext = stream_context_create(array(//プロキシサーバを利用する場合
//    'http' => array('proxy' => 'tcp://proxy.example.net',
//        'request_fulluri' => True)));
 
$url = 'https://graph.facebook.com/oauth/access_token'
        . '?client_id=' . $_SESSION['application_id']
        . '&client_secret=' . $_SESSION['application_secret']
        . '&redirect_uri=' . $_SESSION['redirect_uri']
        . '&code=' . $_GET['code'];
$result = file_get_contents($url, False, $cxContext);
$output = null;
parse_str($result, $output);
if (!isset($output['access_token'])) {
  session_destroy();
  die();
}
$_SESSION['access_token'] = $output['access_token'];
?>
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>OAuth End</title>
  </head>
  <body>
    <p>access_tokenをセッションに保存した。</p>
    <p><a href="post.php">送信テスト</a></p>
  </body>
</html>

oauth-end.phpで認証が完了したら、次のようなpost.phpで、メッセージを送信してみましょう。

<?php
session_start();
if (!isset($_SESSION['access_token'])) {
  session_destroy();
  header('Location: http://localhost/facebook/oauth-start.php');
  die();
}

//投稿
require_once 'HTTP/Request2.php';
$url = "https://graph.facebook.com/me/feed"
        . '?access_token=' . $_SESSION['access_token']
        . '&message=' . urlencode("テスト at " . time());
$request = new HTTP_Request2($url, HTTP_Request2::METHOD_POST);

if (substr(PHP_OS, 0, 3) == 'WIN') {//XAMPP(いいかげん)
  $sslCafile = 'C:/xampp/perl/vendor/lib/Mozilla/CA/cacert.pem';
} else {
  $sslCafile = '/etc/ssl/certs/ca-certificates.crt';
}
$request->setConfig(array(
    //'proxy_host' => 'proxy.example.net',//プロキシサーバを利用する場合
    //'proxy_port' => 3128,
    'ssl_cafile' => $sslCafile
));

$response = $request->send();
if ($response->getStatus() / 100 != 2) {
  echo $response->getReasonPhrase();
  die();
}
session_destroy();
?>
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Message Post</title>
  </head>
  <body>
    <p>投稿成功。</p>
    <pre><?php echo $response->getBody(); ?></pre>
    <p><a href="oauth-start.php">start</a></p>
  </body>
</html>

例によって、PHPにはいろんなHTTPクライアントがあって、あまり整理されていないのですが、ここではHTTP_Request2を使っています(HTTP_Requestはプロキシ経由のSSL通信に対応していないため、ここでは使えません)。次のようなコマンドで、HTTP_Request2をインストールしておく必要があります。XAMPP for Windowsの場合、スタートメニューのコマンドプロンプトを右クリックして管理者として実行してください。プロキシが必要な環境では、事前に「set http_proxy=http://proxy.example.net:3128/」(Windows)や「export http_proxy=http://proxy.example.net:3128/」(Windows以外)が必要です。

cd \xampp\php
pear install http_request2

SSL通信のためには、PHPでOpen SSLが有効になっていなければなりません。Open SSLが有効かどうかはphpinfo()で確認できます。「OpenSSL suppor」がenabledになっていれば大丈夫です。有効になっていない場合は、php.iniに「extension=php_openssl.dll」という行を追加してください(XAMPPの場合はc:\xampp\php\php.ini)。「extension=」という記述がたくさんある場所に書いておけばいいでしょう。php.iniを変更したら、Apacheを再起動してください。

実験時には、「'ssl_cafile' => $sslCafile」の代わりに「'ssl_verify_peer' => false」としていてもいいでしょう。

1つのファイルでOAuth+投稿

oauth-start.phpとoauth-end.php、post.phpを1つのファイル1click-post.phpにまとめると次のようになります。コールバック処理の最後で一度転送しているのは、ブラウザのアドレス欄にトークンを表示させないためです(セキュリティのためではありません)。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Message Post</title>
  </head>
  <body>
    <?php
    session_start();

    $application_id = アプリケーションID;
    $application_secret = シークレットキー;
    $redirect_uri = 'http://localhost/facebook/1click-post.php';

    if (!isset($_SESSION['access_token'])) {
      if (!isset($_GET['code'])) {//OAuth
        $_SESSION['state'] = md5(uniqid(rand(), TRUE)); //CSRF対策
        $url = 'https://www.facebook.com/dialog/oauth'
                . "?client_id=$application_id"
                . '&redirect_uri=' . urlencode($redirect_uri)
                . '&state=' . $_SESSION['state']
                . '&scope=publish_stream'; //投稿する場合
        echo("<script>top.location.href='$url';</script>");
      } else if (isset($_SESSION['state'], $_GET['state']) && ($_SESSION['state'] === $_GET['state'])) {//コールバック
        $cxContext = NULL;
        //$cxContext = stream_context_create(array(//プロキシサーバを利用する場合
        //    'http' => array('proxy' => 'tcp://proxy.example.net:3128',
        //        'request_fulluri' => True,)));
        $url = 'https://graph.facebook.com/oauth/access_token'
                . "?client_id=$application_id"
                . "&client_secret=$application_secret"
                . "&redirect_uri=$redirect_uri"
                . '&code=' . $_GET['code'];
        $result = file_get_contents($url, False, $cxContext);
        $output = null;
        parse_str($result, $output);
        if (!isset($output['access_token'])) {
          session_destroy();
        } else {
          $_SESSION['access_token'] = $output['access_token'];
          header("Location: $redirect_uri"); //トークン隠すための転送(オプショナル)
        }
      }
    } else {//投稿
      require_once 'HTTP/Request2.php';
      $url = "https://graph.facebook.com/me/feed"
              . '?access_token=' . $_SESSION['access_token']
              . '&message=' . urlencode('テスト at ' . time());
      $request = new HTTP_Request2($url, HTTP_Request2::METHOD_POST);
      
      if (substr(PHP_OS, 0, 3) == 'WIN') {//XAMPP(いいかげん)
        $sslCafile = 'C:/xampp/perl/vendor/lib/Mozilla/CA/cacert.pem';
      } else {
        $sslCafile = '/etc/ssl/certs/ca-certificates.crt';
      }
      $request->setConfig(array(
          //'proxy_host' => 'proxy.example.net',//プロキシサーバを利用する場合
          //'proxy_port' => 3128,
          'ssl_cafile' => $sslCafile
      ));
      
      $response = $request->send();
      if ($response->getStatus() / 100 != 2) {
        echo $response->getReasonPhrase();
      } else {
        echo '<p>投稿成功。</p>';
        printf('<pre>%s</pre>', $response->getBody());
      }
      session_destroy();
    }
    ?>
  </body>
</html>

お約束ですが、こういう話を基本から学びたいという方には、拙著『Webアプリケーション構築入門 実践!Webページ制作からマッシュアップまで 』(森北出版, 2011)がおすすめです。