忍者ブログ
[PR]
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。



2024/05/08 00:15 |
PHP5.3:open_basedirの限界
open_basedirとは、PHPから開くことのできるディレクトリを限定する機能です。
ディレクトリトラバーサル対策には入力値から.や/を外すのが正道ですが、万一それが突破された際の防波堤として役に立ちます。
が、
ini_set('open_basedir', '/hoge/fuga/');
file_get_contents('../hoge.txt'); //エラーになる
exec('more ../hoge.txt');         //エラーにならない

見てのとおり、exec()やpassthru()関数はopen_basedirの制限を突破します。

exec('ls')ってやった場合lsは何処にあるかというと普通/bin/lsあたりで、そのためだけにわざわざini_set('open_basedir', '/bin/')なんてやったりしませんね。

ということでexec()を使う場合、open_basedirはディレクトリトラバーサル対策にはなりません。
まあexec()に入力値は使うなってこった。
PR


2011/03/08 00:36 | Comments(0) | TrackBack() | PHP
Symfony1.4 11日目その6
前回の続き

さて、ひとつの求人は30日経ったら見えなくなるという機能をだいぶ昔実装しました。
とりあえず、実際これが動作しているか確認してみましょう。

/lib/test/JobeetTestFunctional.class.php
1
2
3
4
5
6
7
8
9
10
11
<?php
  $browser->info('  3.55 - Expired Job')->
  createJob(array('position' => 'FOO5','is_public'=>true), true)
  ->getJobByPosition('FOO5')->setExpiresAt(date('Y-m-d H:i;s'))->save();
 
  $browser->get(sprintf('/job/a/a/%s/a',
    $browser->getJobByPosition('FOO5')->getId()))
  ->with('response')->begin()->
    isStatusCode(404)->
  end()
;

>php test/functional/frontend/jobActionsTest.php
>   3.55 - Expired Job
# get /job/new
# post /job
# put /job/ab6567fd4b5095bc8762729cc8244c2bfb79a0a9/publish
# get /job/a/a/3774/a
ok 1 - status code is 404

記事を投稿して公開後、公開期限を過去にセットして見ようとしたら404になりました、というテストです。
setExpiresAt('NOW')とか指定したいんだけどどうすればいいんだ?


この終わってしまった記事、もうすぐ終了する記事に対し、公開期限延長を行うボタンを追加してみましょう。
仕様としては、期限切れ5日以内、あるいは期限切れになった記事に管理画面からアクセスした際に「公開期限延長」リンクを用意し、押したら期限が30日延長されるというものになります。

まずルーティングを追加。

apps/frontend/config/routing.yml
1
2
3
4
5
6
7
job:
  class:    sfDoctrineRouteCollection
  options:
    model: JobeetJob
    column: token
    object_actions: { publish: PUT, extend: PUT }
  requirements: { token: \w+ }

object_actionsにextendを追加しました。
/job/:token/extendにPUTメソッドでアクセスするとjobモジュールのextendアクションが起動します。

apps/frontend/modules/job/actions/actions.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
    /*
     * フォーム、Extendリンクを押したら期限延長
     * */
    public function executeExtend(sfWebRequest $request){
      $request->checkCSRFProtection();
      //ルート取得
      $job = $this->getRoute()->getObject();
      //extend実行
      $this->forward404Unless($job->extend());
      //ワンタイムセッション
      $this->getUser()->setFlash('notice', sprintf(
          'Your job validity has been extended until %s.',
           date('m/d/Y', strtotime($job->getExpiresAt()))
      ));
      $this->redirect('job_show_user', $job);
    }

JobeetJob::extend()
を呼ぶようにしたので中身を実装します。

lib/model/doctrine/JobeetJob.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
    /*
     * 表示期限延長
     * */
    public function extend(){
      //期限切れor期限切れ間近でなければアウト
      if (!$this->expiresSoon()){ return false; }
      //期限延長
      $this->setExpiresAt(date('Y-m-d', time() + 
      86400 * sfConfig::get('app_active_days')));
      $this->save();
      return true;
  }

頼むから'NOW() + INTERVAL '.(int)sfConfig::get('app_active_days').' DAY 'って書かせてくれ。


以上で設定が完了したはずです。
確認のためには記事を投稿し、公開期限を色々いじくって、リンクが正しく出るか、押したらきちんと動作するかといった検証が必要となります。
面倒なのでテストコードで試してみましょう。

/test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php
  $browser->info('  3.6 - A job validity 
    cannot be extended before the job expires soon')->
    createJob(array('position' => 'FOO4'), true)->
   call(sprintf('/job/%s/extend', 
     $browser->getJobByPosition('FOO4')->getToken())
    , 'put', array('_with_csrf' => true))->
  with('response')->begin()->
    isStatusCode(404)->
  end()
;
 
$browser->info('  3.7 - A job validity 
  can be extended when the job expires soon')->
  createJob(array('position' => 'FOO5'), true)
;
 
$job = $browser->getJobByPosition('FOO5');
$job->setExpiresAt(date('Y-m-d'));
$job->save();
 
$browser->
  call(sprintf('/job/%s/extend', $job->getToken()), 
  'put', array('_with_csrf' => true))->
  with('response')->isRedirected()
;
 
$job->refresh();
$browser->test()->is(
  $job->getDateTimeObject('expires_at')->format('y/m/d'),
  date('y/m/d', time() + 86400 * sfConfig::get('app_active_days')));

そして実行

>php test/functional/frontend/jobActionsTest.php

>   3.6 - A job validity cannot be extended before the job expires soon
# get /job/new
# post /job
# put /job/cb4f932d526ca6a672713df4b7bcdcffaa415eed/publish
# put /job/cb4f932d526ca6a672713df4b7bcdcffaa415eed/extend
ok 1 - status code is 404
>   3.7 - A job validity can be extended when the job expires soon
# get /job/new
# post /job
# put /job/7fe018cc4866ac53cd6272a4ab6e9acd94e4a5e2/publish
# put /job/7fe018cc4866ac53cd6272a4ab6e9acd94e4a5e2/extend
ok 2 - page redirected to
http://localhost/index.php/job/sensio-labs/atlanta-usa
/4086/foo5
ok 3
1..3
# Looks like everything went fine.

めでたし。
まあどうせ結局最終的にはデザイン確認用にブラウザから見ないといけないんですがね。


2011/02/28 22:22 | Comments(0) | TrackBack() | PHP
ZF1.11 Zend_Cloud
ググっても日本語情報が
http://www.google.co.jp/search?hl=ja&lr=lang_ja&q=%22Zend_Cloud%22
http://phpspot.org/blog/archives/2010/01/phpsimple_cloud.html
のコピペしか見あたらないZend_Cloudを使ってみます。
つうかtwitterを検索結果に出すのやめてくれんかね。邪魔すぎる。

Zend_Cloudは元々SimpleCloud APIという名前だったのですが、ZF1.11においてZendFrameworkに取り込まれました。

Zend_CloudはDocumentServiceQueueServiceStorageServiceの三つに分かれていますが、どれがなんだかよくわかりません。
ざっと中を見てみたところ基本的にどのアダプタもAmazon EC2かWindows Azureに接続して使うようです。
使ってみると言ったはいいもののそこらへんのアカウントを持っていないので、唯一無料で使えそうなStorageServiceのFileSystemを試してみます。
Zend_Cloud_StorageService_Adapter_FileSystemはローカルファイルをクラウド的に使用するというなんだかいまいちよくわからないクラスです。
<?php
	//require
		require_once('Zend/Cloud/StorageService/Adapter/FileSystem.php');
	
	//設定
		$config = array('local_directory' => 'C:\tmp');
	
	//Zend_Cloud_StorageService_Adapter
		$zfCloud = new Zend_Cloud_StorageService_Adapter_FileSystem($config);
	
	//保存
		$zfCloud->storeItem('test.txt', serialize($zfCloud));
	
	//取得
		$data = unserialize($zfCloud->fetchItem('test.txt'));
	
	//ファイルリスト
		$fileList = ($zfCloud->listItems('/'));

非常に簡単ですね。
他にもファイルを移動するZend_Cloud_StorageService_Adapter::moveItem、削除するZend_Cloud_StorageService_Adapter::deleteItemなんかがあるようです。

ちなみにZend_Cloud_StorageService_Adapter_FileSystem::storeItem()の中身はfile_put_contentsなので、文字列以外を渡すとさくさく消えてしまいます。
@paramはmixedってなってるのになあ。

ローカルファイルであればわざわざZend_Cloudを使う意義は薄いので、こちらで練習してWindowsAzureなりNirvanixなりに移行するという流れになるのでしょう。
外部ストレージを使用する際も、コンストラクタの引数$configに認証情報等を渡してしまえば、後はローカルファイルと同じように扱えるみたいです。
なかなか便利そうです。


2011/02/14 22:56 | Comments(0) | TrackBack() | PHP
ZF1.11 Zend_Config

設定ファイルでdefine()を並べる、ってのはよく見かける風景ですが、あれは少しでもPHPの知識がないとよくわかりません。
ということで簡単にiniやXML形式で設定を書けるようにできるのがZend_Configです。

> 現時点で Zend_Config が提供している設定データアダプタは Zend_Config_Ini と Zend_Config_Xml の二種類
http://framework.zend.com/manual/ja/zend.config.introduction.html
とか書かれていますが、実はしれっとYAML、JSON、そしてPHPの配列に対応しておりヘルプも書かれています。
http://framework.zend.com/manual/ja/zend.config.html
サンプルは何故かそれぞれのサンプルがバラバラな形になっているのですが、とりあえずZend_Config_Iniと形式を揃えて使ってみます。

config.ini

[production]
webhost                  = www.example.com
database.adapter         = pdo_mysql
database.params.host     = db.example.com
database.params.username = dbuser
database.params.password = secret
database.params.dbname   = dbname
[staging : production]
database.params.host     = dev.example.com
database.params.username = devuser
database.params.password = devsecret

config.json
{
  "production":{
    "webhost": "www.example.com",
    "database":{
      "adapter": "pdo_mysql",
      "params":{
        "host": "db.example.com",
        "username": "dbuser",
        "password": "secret",
        "dbname": "dbname"
      }
    }
  },
  "staging":{
    "_extends": "production",
    "database":{
      "params":{
        "host": "dev.example.com",
        "username": "devuser",
        "password": "devsecret"
      }
    }
  }
}

config.xml
<?xml version="1.0"?>
<configdata>
  <production>
    <webhost value="www.example.com" />
    <database>
      <adapter value="pdo_mysql" />
      <params>
        <host value="db.example.com" />
        <username value="dbuser" />
        <password value="secret" />
        <dbname value="dbname" />
      </params>
    </database>
  </production>
  <staging extends="production">
    <database>
      <params>
        <host value="dev.example.com" />
        <username value="devuser" />
        <password value="devsecret" />
      </params>
    </database>
  </staging>
</configdata>

config.yaml
production:
  webhost: www.example.com
    database:
      adapter: pdo_mysql
      params:
        host: db.example.com
        username: dbuser
        password: secret
        dbname: dbname
staging:
  _extends: production
    database:
      params:
        host: dev.example.com
        username: devuser
        password: devsecret

config.php
<?php
  $arr_production = array(
    'webhost'  => 'www.example.com',
    'database' => array(
      'adapter' => 'pdo_mysql',
      'params'  => array(
          'host'     => 'db.example.com',
          'username' => 'dbuser',
          'password' => 'secret',
          'dbname'   => 'mydatabase'
      )
    )
  );
  $arr_staging = array(
    'webhost'  => 'www.example.com',
    'database' => array(
      'adapter' => 'pdo_mysql',
      'params'  => array(
          'host'     => 'dev.example.com',
          'username' => 'devuser',
          'password' => 'devsecret',
          'dbname'   => 'mydatabase'
      )
    )
  );
  return _IS_PRODUCTION ? $arr_production : $arr_staging;


index.php
<?php

	//Zend_Config
		require_once('Zend/Config.php');
		require_once('Zend/Config/Ini.php');
		require_once('Zend/Config/Json.php');
		require_once('Zend/Config/Xml.php');
		require_once('Zend/Config/Yaml.php');
	
	//Zend_Config_Ini
		$config_ini = new Zend_Config_Ini('config.ini', 'staging');
	
	//Zend_Config_Json
		$config_json = new Zend_Config_Json('config.json', 'staging');
	
	//Zend_Config_Xml
		$config_xml = new Zend_Config_Xml('config.xml', 'staging');
	
	//Zend_Config_Yaml
		$config_yaml = new Zend_Config_Yaml('config.yaml', 'staging');
	
	//Zend_Config
		define('_IS_PRODUCTION', true);
		$config_php = new Zend_Config(require_once('config.php'));

簡易さではiniとYAMLが飛び抜けてますね。

実は$config = new Zend_Config('config.ini', 'staging', Zend_Config::INI)とかできません。
Zend/Config.phpではなく、Zend/Config/Ini.phpなど使用形式に対応したアダプタを直接インクルードしないといけません。
これ、ヘルプの何処にも書かれてないんですが。

上記で取得したZend_Configインスタンスはどれも、
Zend_Db::factory($config->database);
というふうに同じ使い方が可能です。
またCountableおよびIteratorインターフェイスを実装しているのでcount()やforeachも使えます。
好きな形式でコンフィグを書くとよいでしょう。

PHPだけ設定の継承をしておらず、プロダクションかステージングかを引数に渡すこともできず残念なことになっています。
PHPの配列による設定はあくまでおまけのようなものです。
つうかPHPで書くんだったらZend_Config使う意味ねえよ。
 

 



2011/02/07 23:33 | Comments(0) | TrackBack() | PHP
ZF1.11 Zend_Tag_Cloud
前回作成したZend_TagをZend_Tag_Cloudに突っ込むとそのまんまタグクラウドになるよ、というわかりやすいクラスです。
ただオプションがわかりにくいです。
とりあえず試してみましょう。
<?php
	//require
		require_once('Zend/Tag/Cloud.php');
		require_once('Zend/Tag/ItemList.php');
	
	//アイテムリスト
		$list = new Zend_Tag_ItemList();
	
	//アイテムをアイテムリストに投入
		$list[] = new Zend_Tag_Item(array('title' => '紅莉栖', 'weight' => 45, 'params' => array('url' => '/kurisu')));
		$list[] = new Zend_Tag_Item(array('title' => 'まゆり', 'weight' => 45, 'params' => array('url' => '/mayuri')));
		$list[] = new Zend_Tag_Item(array('title' => 'るか', 'weight' => 44, 'params' => array('url' => '/rukako')));
		$list[] = new Zend_Tag_Item(array('title' => '鈴羽', 'weight' => 51, 'params' => array('url' => '/suzuha')));
		$list[] = new Zend_Tag_Item(array('title' => 'フェイリス', 'weight' => 43, 'params' => array('url' => '/faris')));
		$list[] = new Zend_Tag_Item(array('title' => '萌郁', 'weight' => 54, 'params' => array('url' => '/moeka')));
		$list[] = new Zend_Tag_Item(array('title' => '綯', 'weight' => 31, 'params' => array('url' => '/nae')));
		$list[] = new Zend_Tag_Item(array('title' => '倫太郎', 'weight' => 59, 'params' => array('url' => '/kyouma')));
		$list[] = new Zend_Tag_Item(array('title' => '至', 'weight' => 98, 'params' => array('url' => '/daru')));
 	
	//タグクラウド
		$cloud = new Zend_Tag_Cloud();
		$cloud->setItemList($list);
		//クラウドデコーダ、タグデコーダの選択
		$cloud->setCloudDecorator('HtmlCloud');
		$cloud->setTagDecorator('HtmlTag');
		//タグを変更
		$cloud->getCloudDecorator()->setHtmlTags(array('div'));
		$cloud->getTagDecorator()->setHtmlTags(array('span'=>array('style'=>'border:1px solid green;')));
		//文字サイズを指定
		$cloud->getTagDecorator()->setMinFontSize(100);
		$cloud->getTagDecorator()->setMaxFontSize(400);
		$cloud->getTagDecorator()->setFontSizeUnit('%');
	
	//表示
		print($cloud);

出力は以下のようになります。

まずZend_Tag_Cloud::setItemList()Zend_Tag_ItemListをそのまま突っ込みますが、それだけで実質完成です。

中身は二段階になっており、CloudDecoratorで全体の表示を、TagDecoratorが個別のアイテム表示を司っています。
Zend_Tag_Cloud::getCloudDecoratorおよびZend_Tag_Cloud::getTagDecoratorで各レンダラを取得でき、それぞれのオプションを変更することが可能です。

Zend_Tag_Cloud::setCloudDecoratorおよびZend_Tag_Cloud::setTagDecoratorでレンダラ自体を変更可能です。
が、デフォルトだとCloudDecoratorにはHtmlCloud、TagDecoratorにはHtmlTagしか用意されていないので、自力でレンダラを作成しないかぎり指定する意味はありません。

Zend_Tag_Cloud::getCloudDecorator()->setHtmlTagsでタグクラウド全体を包むタグ、Zend_Tag_Cloud::getTagDecorator()->setHtmlTagsで各アイテムを包むタグを指定できます。
デフォルトだと<ul><li>になってしまうので、使いやすい<div><span>に変えておくといいかもしれません。
値を配列として突っ込むと、各タグの属性として設定されます。
例では手っ取り早くstyleとかやってますが、普通はclassを指定します。

Zend_Tag_Cloud::getTagDecorator()->setMinFontSizesetMaxFontSizesetFontSizeUnitでタグクラウドの文字サイズを変更できます。
例では最低値が100%、最大値が400%となっています。
setFontSizeUnitには%以外にpx、em、inなんかを設定することができます。

さて、上記例ではZend_Tag_ItemList::spreadWeightValuesが無くなっているわけですが、どうやらこの指定を行ってもZend_Tag_Cloudには反映されないようです。
前回のように級数的なフォントサイズ設定を行おうとしても無視されてしまい、単純な比例になってしまいます。
ちょっとこれはバグ(機能未実装)な気がしますね。
spreadWeightValuesを反映させたい場合、いったんZend_Tag_Item->getParam('weightValue')spreadWeightValuesを反映した値を取得し、再度weightに代入し直すという手順が必要なようです。

ちなみにZend_Tag_Item作成時にparam→urlにURLを突っ込んでいますが、これがあると<a href>の値に反映されます。
無いと<a href="">になってしまってタグクラウドの意味が無くなるので指定しておきましょう。
/から始まると絶対パスで、無しだと現在のURLからの相対パスになります。


以上でとりあえずタグクラウドはできましたが、見た目的には単に<div><span>が並んでいるだけです。
平面的にプロットしたりうにうに動いたりするような時々見かける本格的なタグクラウドはZend_Tag_Cloudだけでは作れません。
正直そっちのほうがライブラリで処理してほしい機能のような気がしますが、そっちはJavaScriptにでも任せるってことで。


2011/02/04 23:54 | Comments(0) | TrackBack() | PHP
ZF1.11 Zend_Tag
http://framework.zend.com/manual/ja/zend.tag.html

日本語が難解すぎていくら読んでもまったく理解できないZend_Tagですが、てっきりニコニコ動画のタグみたいなフォークソノミーかと思っていたら全然違う機能でした。

とりあえず使ってみます。
<?php
//require
	require_once('Zend/Tag/Item.php');
	require_once('Zend/Tag/ItemList.php');

//アイテムリスト
	$list = new Zend_Tag_ItemList();

//アイテムをアイテムリストに投入
	$list[] = new Zend_Tag_Item(array('title' => '紅莉栖', 'weight' => 45));
	$list[] = new Zend_Tag_Item(array('title' => 'まゆり', 'weight' => 45));
	$list[] = new Zend_Tag_Item(array('title' => '鈴羽', 'weight' => 51));
	$list[] = new Zend_Tag_Item(array('title' => 'フェイリス', 'weight' => 43));
	$list[] = new Zend_Tag_Item(array('title' => 'るか', 'weight' => 44));
	$list[] = new Zend_Tag_Item(array('title' => '萌郁', 'weight' => 54));
	$list[] = new Zend_Tag_Item(array('title' => '倫太郎', 'weight' => 59));
	$list[] = new Zend_Tag_Item(array('title' => '至', 'weight' => 98));
	$list[] = new Zend_Tag_Item(array('title' => '綯', 'weight' => 31));
 	
//重みを付ける
	$list->spreadWeightValues(array(1, 2, 4, 8, 16, 32, 64, 128));

//確認
	foreach ($list as $item) {
	    printf("%s: %d\n", $item->getTitle(), $item->getParam('weightValue'));
	}
weightの意味を明らかに取り違えていますが気にしない。

結果は
 紅莉栖: 4 まゆり: 4 鈴羽: 8 フェイリス: 4 るか: 4 萌郁: 8 至: 128 倫太郎: 8 綯: 1
となります。

どういう意味?

動作を確認したところ、どうやら次のような意味のようです。

・weightの最小値をspreadWeightValues()の左端、weightの最大値をspreadWeightValues()の右端にセット
・それぞれのweightを最小値から最大値までの割合でspreadWeightValues()の何れかの値にセット

綯を左端の1、至を右端の128とすると、紅莉栖は左端から3番目の4の位置あたりになり、鈴羽は4番目の8の位置あたりになるということです。

つまり、どうやらタグクラウドのタグという意味だったようです。
時々ある検索件数や重要度が高いものを大きな文字で表示したりするあれです。
使い方としては、単語と検索件数をZend_Tag_Itemに突っ込み、spreadWeightValues()でfont-sizeを段階的に指定するなどといった使い方になるでしょう。


つうか綯と10キロちょいしか違わないとかこいつらおかしい。


2011/01/31 22:50 | Comments(0) | TrackBack() | PHP
PECL::OAuthでtwitterログイン
Zend_Oauthでリダイレクトを行いつつのTwitterログインtwitterauthでリダイレクト無しでのTwitterログインに成功しました。

何が障害になっていたかというとアクセストークンです。
TiwitterAPIを利用するアプリにはパスワードを知らせず、アクセストークンだけを教えることでパスワード漏洩を防ぐのがoAuthの目的です。
逆にパスワードを登録してもいいから直接投稿したいんだという場合でも直接の投稿はできず、一旦アクセストークンを入手し、それを利用して投稿する必要があります。
そのアクセストークンを入手するためにブラウザを経由してパスワードを入力したのが前々回で、直接アクセストークンを突っ込んだのが前回となります。

では、PHPだけでパスワードから直接アクセストークンを拾ってこれないのかというと、Zend_OauthやZend_Service_Twitterには何故かそういう機能が見あたらないんですよね。

実はPecl::OAuthという拡張モジュールがしれっと対応していたりします。
標準では入っていないのでLinuxであれば"pecl install oauth-beta"、XAMPPであれば"extension=php_oauth.dll"を有効にします。
<?php
	//xAuthオプション
		//アプリ認証
		$oauth_consumer_key		= 'hogehoge';
		$oauth_consumer_secret	= 'fugafuga';
		//ユーザ認証
		$x_auth_username		= '********';
		$x_auth_password		= '********';
		//固定
		$oauth_signature_method	= 'HMAC-SHA1';
		$oauth_version			= '1.0';
		$x_auth_mode			= 'client_auth';
		
		//URL
			$xAuthAccessTokenUrl = 'https://api.twitter.com/oauth/access_token';
			$oAuthStatusesUpdateUrl = 'http://twitter.com/statuses/update.xml';
	
	//xAuth認証
		//変数纏め
			$x_auth_params = array('x_auth_mode'=>$x_auth_mode, 'x_auth_username'=>$x_auth_username, 'x_auth_password'=>$x_auth_password);
		
		//PECL::oAuthを利用してxAuthもできる
			$oauth = new OAuth($oauth_consumer_key, $oauth_consumer_secret, OAUTH_SIG_METHOD_HMACSHA1, OAUTH_AUTH_TYPE_URI);
			$oauth->disableSSLChecks();
			
			$oauth->fetch($xAuthAccessTokenUrl, $x_auth_params, OAUTH_HTTP_METHOD_POST);
			$response = $oauth->getLastResponse();
		
	//oAuthトークンを取得
		//URLそのままになってるのでパースが必要
			parse_str($response, $accessTokenInfo);
			$oauthToken       = $accessTokenInfo['oauth_token'];
			$oauthTokenSecret = $accessTokenInfo['oauth_token_secret'];
	
	//投稿
		$status_string = '投稿てすと';
		
		//変数纏め
			$parameters = array('status'=>$status_string);
		
		//認証トークンセット
			$oauth->setToken($oauthToken, $oauthTokenSecret);
		
		//送信実行
			$oauth->fetch($oAuthStatusesUpdateUrl, $parameters, OAUTH_HTTP_METHOD_POST);
			$response = $oauth->getLastResponse();


完成しました。
さっそく実行

Fatal error: Uncaught exception 'OAuthException' with message 'Invalid auth/bad request (got a 401, expected HTTP/1.1 20X or a redirect)'

おうふ!

実はこのプログラムはOAuthではなく、xAuthという認証機構を使用しています。
内容は似たようなものですが、OAuthがアプリ側にパスワードを一切知らせることがないのに対し、xAuthはアプリにパスワードを知らせるという大きな違いがあります。
で、このxAuthはパスワードを扱える性質上、OAuthに比べて使用の敷居が高くなっています。

具体的には、Twitterにメールで許可を取らないとxAuth認証できません。
http://written.4403.biz/archives/2010/03/twitter-xauth.html

めんどくせー。


許可を取りさえすれば、$oauthToken$oauthTokenSecretを突っ込むだけで簡単に投稿ができるようになります。
めでたし。


2011/01/24 21:45 | Comments(0) | TrackBack() | PHP
twitterauth.phpでログイン
前回までZend_Oauthでtwitter認証を行いましたが、この仕組みはブラウザ経由でログインさせて云々しないといけません。

何故このような作りになっているかというとそれはOauthの目的そのものなのですが、前回のプログラムをよく見ると、プログラム自体には投稿を行うユーザのID、パスワードの情報が一切渡されていません。
ID、パスワードは直接twitterのサイトに入力します。
するとIDに紐付くAccessTokenおよびAccessTokenSecretというものが発行され、パスワード代わりにこのAccessTokenAccessTokenSecretを使用することで投稿が可能になります。
プログラムはパスワードを一切知らないのに、認証できたという結果だけを利用できるのです。

これはあくまで不特定のユーザにアプリを公開して使ってもらう際に、私はIDやパスワードを盗んだりできませんよという証明に使うものです。
本来AccessTokenAccessTokenSecretには有効期限があるため、それを過ぎると再度パスワードを入力したりする必要があります。
ただ現在のtwitterは何故か無制限になってるみたいですが、これだと結局ID/パスワードを渡したのと同レベルに弄ばれてしまう気がするんだがどうなんだろう。
一応ユーザで取り消すことはできますが、毎回削除とか面倒だし普通しないでしょうし。

もっと手っ取り早く、ブラウザを経由しないで自分専用にローカルに保存したID、パスワードで直接投稿とかはできないでしょうか。
自分専用に期間無制限のAccessTokenAccessTokenSecretをtwitterのサイトから発行できるので、これを予め保存しておくことで、前回まで行っていたOauth認証をパスしていきなり投稿を行うことができます。

http://dev.twitter.com/apps/からアプリケーションを選択し、メニューの「My Access Token」を選びます。
AccessTokenAccessTokenSecretが表示されますが、これがIDやパスワードのかわりになります。

では投稿してみましょう。
今回はtwitterauthというものを使ってみます。
http://apiwiki.twitter.com/OAuth-Examples
こちらの「Code(GitHub)」リンクからサンプルプログラムが一式ダウンロード出来ます。
認証には、twitteroauthディレクトリ下にある
・twitteroauth.php
・OAuth.php
を使います。
<?php
	//require
	require_once('twitterauth.php');
	
	//定義
	$consumer_key = '*****';
	$consumer_secret = '*****';
	$access_token = '*****';
	$access_token_secret = '*****';
	
	//投稿
	$TwitterAuth = new TwitterOAuth($consumer_key,$consumer_secret,$access_token,$access_token_secret);
	$req = $TwitterAuth->OAuthRequest("https://twitter.com/statuses/update.xml","POST",array("status"=>"投稿テスト"));

実に簡単ですね。


元々Zend_Service_Twitterで呟こうとしたらZend_OAuthのリダイレクトにぶち当たり、PHPPRO掲示板で聞いてtwitterauthを紹介され、どうにかここまで解消したのですが、今さらになってまったくそのままの回答が書いてあるのを発見してしまった。

なんてこったい。


2011/01/21 21:51 | Comments(0) | TrackBack() | PHP
Symfony1.4 11日目その5
前回の続き。

求人を一旦公開してしまったら、その後の編集は行うことができません。
直接該当のtokenを指定してURLを辿っても、現れるリンクは削除用だけになります。
http://symfony.localhost/frontend_dev.php/job/token

が、実はエディット画面のURLを直接指定すれば平然と編集ができてしまいます。
http://symfony.localhost/frontend_dev.php/job/token/edit

公開後に編集とかされるとブラック的に色々とアレなので防ぎましょう。

まず自動投稿を行うJobeetTestFunctional::createJob()に公開機能を付けてみます。

/lib/test/JobeetTestFunctional.class.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
  public function createJob($values = array(), $publish = false){
    $this->
      get('/job/new')->
      click('Preview your job', array('job' => array_merge(array(
        'company'      => 'Sensio Labs',
        'url'          => 'http://www.sensio.com/',
        'position'     => 'Developer',
        'location'     => 'Atlanta, USA',
        'description'  => 'You will work with symfony',
        'how_to_apply' => 'Send me an email',
        'email'        => 'for.a.job@example.com',
        'is_public'    => false,
        'type'         => 'freelance',
      ), $values)))->
      followRedirect()
    ;
    if ($publish){
      $this->
        click('Publish', array(),
           array('method' => 'put', '_with_csrf' => true))->
        followRedirect()
      ;
    }
    return $this;
  }

第二引数$publishにtrueを指定すると、新たな投稿を行ったあとでPublishリンクを踏んで公開状態にまでしてくれます。

次にテストを書くわけですが、公開完了ページには編集画面へのリンクがありません。
投稿内容からtokenを拾ってきてURLに突っ込む仕組みを作りましょう。

/test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
8
9
<?php
$browser->info('3.5 - When a job is published, it cannot be edited anymore')->
  createJob(array('position' => 'FOO3'), true)->
  get(sprintf('/job/%s/edit',
     $browser->getJobByPosition('FOO3')->getToken()))->
  with('response')->begin()->
    isStatusCode(404)->
  end()
;

求人を作成、投稿した後、エディット画面を拾ってきて、そのステータスコードが404であることを確認するという流れになります。
エディット画面はリンクがないので、/job/:token/editってURLを直接作って取得してきます。
tokenを取得するために、JobeetJobオブジェクトを取得するgetJobByPosition()メソッドを新設します。

/lib/test/JobeetTestFunctional.class.php
1
2
3
4
5
6
7
<?php
  public function getJobByPosition($position){
    $q = Doctrine_Query::create()
      ->from('JobeetJob j')
      ->where('j.position = ?', $position);
    return $q->fetchOne();
  }

さっくりできました。
さて実行。

>php test/functional/frontend/jobActionsTest.php
>   3.5 - When a job is published, it cannot be edited anymore
# get /job/new
# post /job
# put /job/03586d0f02dd19df6158eee0ccd0369529fb48e2/publish
# get /job/03586d0f02dd19df6158eee0ccd0369529fb48e2/editnot ok 1 - status code is 404
#     Failed test (symfony/test/sfTesterResponse.class.php at line 412)
#            got: 200
#       expected: 404

404が返ってくるはずなのに200が返ってきました。
現状ではエディット画面に行けてしまいます。
公開後の求人は編集できないようにしましょう。

エディット画面のURLは/job/:token/editとなり、ルーティングのjob:sfDoctrineRouteCollectionにひっかかってjobモジュールのeditアクションが実行されます。

/apps/frontend/modules/job/actions/actions.class.php
1
2
3
4
5
6
7
8
9
10
11
12
<?php
    /*
     * フォームのエディット
     */
    public function executeEdit(sfWebRequest $request){
        //オブジェクト取得
        $job = $this->getRoute()->getObject();
        //is_activatedがtrueであれば404
        $this->forward404If($job->getIsActivated());
         //フォームに投入
        $this->form = new JobeetJobForm($job);
    }

$job->getIsActivated()がtrueであれば404という処理を追加しました。

>php test/functional/frontend/jobActionsTest.php
>   3.5 - When a job is published, it cannot be edited anymore
# get /job/new
# post /job
# put /job/38bd3acdf34b045c27f69b53d02e48fe10efdf42/publish
# get /job/38bd3acdf34b045c27f69b53d02e48fe10efdf42/edit
ok 1 - status code is 404

きちんと404がかえってきました。
ブラウザで見ても一旦公開した求人はエディットできません。


2011/01/17 21:54 | Comments(0) | TrackBack() | PHP
Symfony1.4 11日目その4
前回の続き

さて、ブラウザから投稿してみると、投稿後は編集画面に移動します。
この時点ではまだjobeet_job.is_activated=0なので、フロントには表示されていません。
Publishリンクを踏んで公開するようにしてみましょう。

/test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
8
9
10
11
<?php
$browser->info('  3.3 - On the preview page, you can publish the job')->
  createJob(array('position' => 'FOO1'))->
  click('Publish', array(), array('method' => 'put', '_with_csrf' => true))->
  with('doctrine')->begin()->
    check('JobeetJob', array(
      'position'     => 'FOO1',
      'is_activated' => true,
    ))->
  end()
;

>php test/functional/frontend/jobActionsTest.php
>   3.3 - On the preview page, you can publish the job
# get /job/new
# post /job
# put /job/e83769f22dcaf47b37b3b1c22b8e2d4586f6e6d8/publish
ok 1 - JobeetJob objects that matches the criteria have been found

click()の第三引数にはクリックする際の付加情報を記述します。
この場合はメソッドをPUTに、CSRFプロテクションを有効にしています。
ということらしいのですが意味がわかりません。
該当のリンクをブラウザで見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
<a onclick="var f = document.createElement('form'); 
  f.style.display = 'none'; this.parentNode.appendChild(f); 
  f.method = 'post';
  f.action = this.href;
  var m = document.createElement('input');
  m.setAttribute('type', 'hidden');
  m.setAttribute('name', 'sf_method');
  m.setAttribute('value', 'put');
  f.appendChild(m);
  f.submit();
  return false;"
  href="/frontend_dev.php/job/
  151bef1f9cf92b7b561752e4ad35cfe932a92f4b/publish"&gt;Publish&lt;/a>

なんだこれ。

実はルーティングによって投稿記事の編集はPUTメソッドを使えということになっています。
勿論対応ブラウザやサーバなんて無いのでSymfonyではPOSTで擬似的に実現しています。
その疑似PUTを指定するのが'method' => 'put'です。
これを外してしまうとデフォルトのメソッド(GETかPOSTか不明)で送信されてしまい、エラーになります。

>php test/functional/frontend/jobActionsTest.php
# get /job/new
# post /job
# get /job/700cf9ac61d7abdd35e88720cf13be6b9d3584d2/publish
not ok 1 - JobeetJob objects that matches the criteria have been found
#     Failed test (symfony/plugins/sfDoctrinePlugin/lib/test/sfTesterDoctrine.class.php at line 90)

次の_with_csrfについては全く意味がわかりません。
CSRFがどうこうと言ってますが別に外しても普通に動くし一体何の意味があるんだ。
リクエストのクエリを見るメソッドとか無いのかね?

PublishのかわりにDeleteリンクを押せば削除できますが、その流れもPublishとほぼ一緒です。
DeleteリンクはDELETEメソッドを使用することになっていますが、こちらも無論擬似的実現です。

/test/functional/frontend/jobActionsTest.php
1
2
3
4
5
6
7
8
9
10
<?php
$browser->info('  3.3 - On the preview page, you can publish the job')->
  createJob(array('position' => 'FOO2'))->
  click('Delete', array(), array('method' => 'delete', '_with_csrf' => true))->
  with('doctrine')->begin()->
    check('JobeetJob', array(
      'position'     => 'FOO2',
    ), false)->
  end()
;

Deleteリンクを踏むと投稿が削除され、position='F002'の投稿が見つからないよ、という状態になることを確認できます。

>php test/functional/frontend/jobActionsTest.php
>   3.3 - On the preview page, you can publish the job
# get /job/new
# post /job
# delete /job/34b28d5f5d5f3a7bc32c8b91ff61ea3e6ee8123f
ok 1 - no JobeetJob object that matches the criteria has been found


2011/01/10 21:15 | Comments(0) | TrackBack() | PHP

<<前のページ | HOME | 次のページ>>
忍者ブログ[PR]