Google Crisis Response(Google)
災害に関する情報源や、行方不明者情報の収集と検索を行う『パーソンファインダー』を初めとするツールの提供が行われています。

2012年2月4日 土曜日

開発者必見 WordPressのデバッグモード

Filed under: WordPress,ハック,プラグイン
タグ:, , ,
時間:17時47分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

WordPressにはデバッグモードがあります。このモードでは普段抑制されている警告メッセージが表示されるようになり、非推奨APIを使用した場合にメッセージが表示されます。実装されたのは2.5からのようです。

デバッグモードにするには、定数 WP_DEBUGtrue にするための記述を wp-config.php に次のように記述します。3.0からは wp-config-sample.php に初期状態で記述があり、値は false です。

  1. define('WP_DEBUG', true);

今までデバッグモードでの動作確認をしたことのないプラグイン開発者の方は、ぜひ1度行ってください。予想以上に警告が表示されると思います。可能であれば開発用のWordPress環境を用意し、常にデバッグモードにしておくことをお勧めします。

初期化されていない変数への参照

ありがちなのは、初期化されていない変数や配列要素への参照が行われたと言うもの。PHPは初期化しなくても変数と配列要素が使用できますが、これは警告メッセージが表示されます。

  1. print $data;    // 警告が出る
  2. $data = '';
  3. print $data;    // 警告は出ない
  1. $data = array();
  2. print $data[0];  // 警告が出る
  3. $data[0] = '';
  4. print $data[0];  // 警告は出ない
  1. $data = array();
  2. print $data['i1'];  // 警告が出る
  3. $data['i1'] = '';
  4. print $data['i1'];  // 警告は出ない

最後のコードでは次のような内容となります。

Notice: Undefined index: i1 in ファイルのパス on line 警告対象のある行

拙作の AmazonLink では配列要素への参照でこの警告が多く出ました。例えば次のようなコードです。

  1. if ( '' != $dataList['idx'] )
  2. {
  3.     // 処理
  4. }

この場合、初期化していない $dataList['idx'] を参照すると警告が出ます。そこで、次のように変更しました。

  1. if ( isset($dataList['idx']) and '' != $dataList['idx'] )
  2. {
  3.     // 処理
  4. }

非推奨APIの使用

WordPressはAPIの整理を以前から行っており、多くのAPIが非推奨になったり特定の使用方法が非推奨となっています。デバッグモードにするといつから非推奨になったかと代替APIの有無を表示してくれますが、該当箇所として表示されるのは実際の場所ではなく、そのメッセージを表示するための関数が記述された場所になっています。自分のプログラムではないと勘違いしないよう注意が必要です。

次の例は、 get_settings が2.1から非推奨となっているので get_option を使うようにと言うものです。

Notice: get_settings is deprecated since version 2.1! Use get_option() instead. in ファイルのパス/wp-includes/functions.php on line 3237

この例ではほぼ単純な置き換えだけですのでわかりやすいのですが、中には何が悪いのかわかりにくいものがあります。

add_option

Function Reference/add option(WordPress.ORG Codex)

オプションを追加するためのAPIです。引数が4つありますが、オプションの説明を指定する3つ目の引数は2.3から非推奨となりました。メッセージだけを見るとAPI自体が非推奨になって代替APIがないように思えてしまいますが、実際には引数の1つだけが対象です。警告が表示されないようにするには、引数を空文字('')にします。

add_options_page

Function Reference/add options page(WordPress.ORG Codex)

管理画面にオプションページへのサブメニューを追加するAPIです。ややこしいのは、WP_User::has_cap での警告となること。その処理の過程で WP_User::has_cap が呼ばれるためこのようになってしまうのです。

2.0以前はユーザーレベルとして0~10の数値だった3つ目の引数が2.0で権限を示す文字列になり、3.0からは数値での指定は非推奨となりました。

なお、数値での指定をすると同じ関数にある次のAPIによって文字列に置き換えられます。

  1. /**
  2.      * Convert numeric level to level capability name.
  3.      *
  4.      * Prepends 'level_' to level number.
  5.      *
  6.      * @since 2.0.0
  7.      * @access public
  8.      *
  9.      * @param int $level Level number, 1 to 10.
  10.      * @return string
  11.      */
  12.     function translate_level_to_cap( $level ) {
  13.         return 'level_' . $level;
  14.     }

置き換えられた結果得られる文字列は、権限グループに再度割り当てなおされると説明されています。単純にこの処理と同じことをすれば警告は出なくなりますが、将来のことを考えればきちんと権限または権限グループを示す文字列に置き換えるのが賢明でしょう。

Comments (0)

2011年9月17日 土曜日

WordPress の Transients API

Filed under: WordPress,ハック,プログラミング
タグ:, , ,
時間:18時29分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

ウェブアプリケーションでは、一定期間状態を保持したいことが多くあります。それを実現するためにさまざまな方法が提案され、利用されてきました。古くはブラウザに保持させる Cookie、サーバーに保存させるセッション。最近では HTML5 の Web Storage があります。

WordPress には、この一定期間保持するという目的にぴったりのAPI(Application Programming Interface)があります。Transients API と呼ばれるもので、指定した秒数が経過するまでデーターベース上に保持してくれるというものです。保存・取り出し・削除の3つの関数で構成されているシンプルなものです。WordPress 2.8.0 から使えるようになったもので、現在ではコアファイルのさまざまなところで使われています。

使い方については他のサイトを見ていただくとして、この記事ではhackネタを書いてみたいと思います。

まずは、ちょっと長いですが関数の定義部分から。引用元は WordPress 3.2.1 の wp-includes/function.php です。

  1. /**
  2.  * Delete a transient
  3.  *
  4.  * @since 2.8.0
  5.  * @package WordPress
  6.  * @subpackage Transient
  7.  *
  8.  * @uses do_action() Calls 'delete_transient_$transient' hook before transient is deleted.
  9.  * @uses do_action() Calls 'deleted_transient' hook on success.
  10.  *
  11.  * @param string $transient Transient name. Expected to not be SQL-escaped.
  12.  * @return bool true if successful, false otherwise
  13.  */
  14. function delete_transient( $transient ) {
  15.     global $_wp_using_ext_object_cache;
  16.  
  17.     do_action( 'delete_transient_' . $transient, $transient );
  18.  
  19.     if ( $_wp_using_ext_object_cache ) {
  20.         $result = wp_cache_delete( $transient, 'transient' );
  21.     } else {
  22.         $option_timeout = '_transient_timeout_' . $transient;
  23.         $option = '_transient_' . $transient;
  24.         $result = delete_option( $option );
  25.         if ( $result )
  26.             delete_option( $option_timeout );
  27.     }
  28.  
  29.     if ( $result )
  30.         do_action( 'deleted_transient', $transient );
  31.     return $result;
  32. }
  1. /**
  2.  * Get the value of a transient
  3.  *
  4.  * If the transient does not exist or does not have a value, then the return value
  5.  * will be false.
  6.  *
  7.  * @uses apply_filters() Calls 'pre_transient_$transient' hook before checking the transient.
  8.  *  Any value other than false will "short-circuit" the retrieval of the transient
  9.  *  and return the returned value.
  10.  * @uses apply_filters() Calls 'transient_$option' hook, after checking the transient, with
  11.  *  the transient value.
  12.  *
  13.  * @since 2.8.0
  14.  * @package WordPress
  15.  * @subpackage Transient
  16.  *
  17.  * @param string $transient Transient name. Expected to not be SQL-escaped
  18.  * @return mixed Value of transient
  19.  */
  20. function get_transient( $transient ) {
  21.     global $_wp_using_ext_object_cache;
  22.  
  23.     $pre = apply_filters( 'pre_transient_' . $transient, false );
  24.     if ( false !== $pre )
  25.         return $pre;
  26.  
  27.     if ( $_wp_using_ext_object_cache ) {
  28.         $value = wp_cache_get( $transient, 'transient' );
  29.     } else {
  30.         $transient_option = '_transient_' . $transient;
  31.         if ( ! defined( 'WP_INSTALLING' ) ) {
  32.             // If option is not in alloptions, it is not autoloaded and thus has a timeout
  33.             $alloptions = wp_load_alloptions();
  34.             if ( !isset( $alloptions[$transient_option] ) ) {
  35.                 $transient_timeout = '_transient_timeout_' . $transient;
  36.                 if ( get_option( $transient_timeout ) < time() ) {
  37.                     delete_option( $transient_option  );
  38.                     delete_option( $transient_timeout );
  39.                     return false;
  40.                 }
  41.             }
  42.         }
  43.  
  44.         $value = get_option( $transient_option );
  45.     }
  46.  
  47.     return apply_filters( 'transient_' . $transient, $value );
  48. }
  1. /**
  2.  * Set/update the value of a transient
  3.  *
  4.  * You do not need to serialize values. If the value needs to be serialized, then
  5.  * it will be serialized before it is set.
  6.  *
  7.  * @since 2.8.0
  8.  * @package WordPress
  9.  * @subpackage Transient
  10.  *
  11.  * @uses apply_filters() Calls 'pre_set_transient_$transient' hook to allow overwriting the
  12.  *  transient value to be stored.
  13.  * @uses do_action() Calls 'set_transient_$transient' and 'setted_transient' hooks on success.
  14.  *
  15.  * @param string $transient Transient name. Expected to not be SQL-escaped.
  16.  * @param mixed $value Transient value. Expected to not be SQL-escaped.
  17.  * @param int $expiration Time until expiration in seconds, default 0
  18.  * @return bool False if value was not set and true if value was set.
  19.  */
  20. function set_transient( $transient, $value, $expiration = 0 ) {
  21.     global $_wp_using_ext_object_cache;
  22.  
  23.     $value = apply_filters( 'pre_set_transient_' . $transient, $value );
  24.  
  25.     if ( $_wp_using_ext_object_cache ) {
  26.         $result = wp_cache_set( $transient, $value, 'transient', $expiration );
  27.     } else {
  28.         $transient_timeout = '_transient_timeout_' . $transient;
  29.         $transient = '_transient_' . $transient;
  30.         if ( false === get_option( $transient ) ) {
  31.             $autoload = 'yes';
  32.             if ( $expiration ) {
  33.                 $autoload = 'no';
  34.                 add_option( $transient_timeout, time() + $expiration, '', 'no' );
  35.             }
  36.             $result = add_option( $transient, $value, '', $autoload );
  37.         } else {
  38.             if ( $expiration )
  39.                 update_option( $transient_timeout, time() + $expiration );
  40.             $result = update_option( $transient, $value );
  41.         }
  42.     }
  43.     if ( $result ) {
  44.         do_action( 'set_transient_' . $transient );
  45.         do_action( 'setted_transient', $transient );
  46.     }
  47.     return $result;
  48. }

基本的には、2つの項目をセットにしてオプションとして保存しています。項目はデータ自体となる _transient_識別子文字列 と、保持期限となる _transient_timeout_識別子文字列

識別子文字列と値に対してのサニタイズやシリアライズ処理は関数が面倒を見てくれますが、識別子文字列が長い場合は注意が必要です。テーブルのカラムから来る制限で45文字を超えることが出来ないのですが、サニタイズによってこの長さを超えてしまう可能性があります。半角英数を使うのが無難でしょう。

取り出すときには有効期限ないかどうかを確認し(756行目)、期限を過ぎている場合には削除もしてくれます(757・758行目)。Codex を見る限りでは何もしなくても期限が過ぎれば削除してくれるようですが、相当する処理は見つけられませんでした。

オブジェクトキャッシュが有効になっている場合には、データーベースの変わりにそちらを使うので更なる高速化が期待できます。

拙作プラグインの AmazonLink でも試験的に導入してみました。自前で行っていた処理がほとんどいらなくなったおかげで、プログラムがかなり簡略化できています。今のところ問題も起きていないので、このまま次のリリースに入れる予定です。2.8.0以上という条件はありますが、これはお勧めです。

Comments (2)

2009年12月13日 日曜日

WordPressのアップグレードとユーザー権限

Filed under: WordPress,ハック
タグ:, , ,
時間:17時43分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

WordPress の自動アップグレードをしようとすると「このブログのプラグインを更新するための十分な権限がありません。」となってしまうので調査をしていたのですが、原因が判明しましたので記事にします。

まず、調査対象のバージョンは2.8.4です。2.6系から直接アップグレードしたもので、一番初めは2.0ME系でした。接頭辞は wp です。

権限がないといわれてしまう原因ですが、そのまま権限が与えられていないためです。管理者で作業しているので普通はそんなことはありえないのですが、どうやらアップグレード時に行われるデータベースの更新に問題があったようです。

権限情報の記録場所

権限の情報はオプションとしてデータベース上に記録されていて、次のSQLで見つけることができます。

  1. SELECT `option_name`,`option_value` FROM `wp_options` WHERE `option_name` = 'wp_user_roles';

このオプションの値はPHPでシリアライズしたもので、管理者や編集者といったロール(役割)ごとに権限を配列の形で格納しています。wp-includes/capabilities.php の冒頭には、次のように書かれています。

  1. * The role option is simple, the structure is organized by role name that store
  2.  * the name in value of the 'name' key. The capabilities are stored as an array
  3.  * in the value of the 'capability' key.
  4.  *
  5.  * <code>
  6.  * array (
  7.  *      'rolename' => array (
  8.  *          'name' => 'rolename',
  9.  *          'capabilities' => array()
  10.  *      )
  11.  * )
  12.  * </code>

権限の記録は20行目の配列で、権限名がキーでその権限があるかどうかを示す真偽値が値となります。

現状の確認

さて、ここまでを踏まえて実際にどのように権限情報が記録されているかを確認します。PHPで表示させるプログラムを書いてみてもいいのですが、面倒なのでもっと手っ取り早い方法を使いました。

前述したSQLでオプション値を取得し、それをテキストエディタ(私は秀丸エディタを使いました)に貼り付けます。このままではすべてが1行に繋がってわかりにくいので、{ と } と ; の後ろにテキスト置換で改行を入れました。
こうすると、権限名と真偽値が交互になってわかりやすくなります。

準備ができたら権限名を探します。自動アップグレードのアドレスから、きっかけとなるコードは wp-admin/update-core.php であることがわかりますので確認します。

冒頭の12行目に次のようにあります。

  1. if ( ! current_user_can('update_plugins') )
  2.     wp_die(__('You do not have sufficient permissions to update plugins for this blog.'));

update_plugins という名前の権限を確認して、権限がなければメッセージを表示して終了するという処理ですね。早速この権限名を先ほど準備したテキストデータで探しますと、見当たりません。この名前をキーとする配列要素がないということになりますので、PHPでは真偽値は false となります。

念のため新規に同じバージョンのインストールをしてみた場合で確認したところ、しっかりとこの権限については true となっていました。

なぜこうなったのか

こうなると原因が気になります。アップグレードスクリプトの権限処理をするところを探してみました。アップグレードインストールをするときのファイル名を基点にしてたどってみたところ、wp-admin/includes/upgrade.php に定義されている upgrade_all() という関数の中で、WordPressデータベースのバージョンによってアップグレード用の関数を呼び出していることがわかりました。

  1. /**
  2.  * Functions to be called in install and upgrade scripts.
  3.  *
  4.  * {@internal Missing Long Description}}
  5.  *
  6.  * @since unknown
  7.  */
  8. function upgrade_all() {
  9.     global $wp_current_db_version, $wp_db_version, $wp_rewrite;
  10.     $wp_current_db_version = __get_option('db_version');
  11.  
  12.     // We are up-to-date.  Nothing to do.
  13.     if ( $wp_db_version == $wp_current_db_version )
  14.         return;
  15.  
  16.     // If the version is not set in the DB, try to guess the version.
  17.     if ( empty($wp_current_db_version) ) {
  18.         $wp_current_db_version = 0;
  19.  
  20.         // If the template option exists, we have 1.5.
  21.         $template = __get_option('template');
  22.         if ( !empty($template) )
  23.             $wp_current_db_version = 2541;
  24.     }
  25.  
  26.     if ( $wp_current_db_version < 6039 )
  27.         upgrade_230_options_table();
  28.  
  29.     populate_options();
  30.  
  31.     if ( $wp_current_db_version < 2541 ) {
  32.         upgrade_100();
  33.         upgrade_101();
  34.         upgrade_110();
  35.         upgrade_130();
  36.     }
  37.  
  38.     if ( $wp_current_db_version < 3308 )
  39.         upgrade_160();
  40.  
  41.     if ( $wp_current_db_version < 4772 )
  42.         upgrade_210();
  43.  
  44.     if ( $wp_current_db_version < 4351 )
  45.         upgrade_old_slugs();
  46.  
  47.     if ( $wp_current_db_version < 5539 )
  48.         upgrade_230();
  49.  
  50.     if ( $wp_current_db_version < 6124 )
  51.         upgrade_230_old_tables();
  52.  
  53.     if ( $wp_current_db_version < 7499 )
  54.         upgrade_250();
  55.  
  56.     if ( $wp_current_db_version < 7796 )
  57.         upgrade_251();
  58.  
  59.     if ( $wp_current_db_version < 7935 )
  60.         upgrade_252();
  61.  
  62.     if ( $wp_current_db_version < 8201 )
  63.         upgrade_260();
  64.  
  65.     if ( $wp_current_db_version < 8989 )
  66.         upgrade_270();
  67.  
  68.     if ( $wp_current_db_version < 10360 )
  69.         upgrade_280();
  70.  
  71.     maybe_disable_automattic_widgets();
  72.  
  73.     update_option( 'db_version', $wp_db_version );
  74.     update_option( 'db_upgraded', true );
  75. }

呼び出されている upgrade_***() という関数の中では、さらに必要に応じて populate_roles_***() という関数を呼び出しています。

この関数の定義は wp-admin/includes/schema.php にあり、そこではまさにロールごとに権限の追加を行っていました。

次のSQLでデータベースバージョンを確認して呼び出されるはずの関数の処理を確認してみました。

  1. SELECT `option_name`,`option_value` FROM `wp_options` WHERE `option_name` = 'db_version';

2.6の新規インストール環境では8201となっていましたので、upgrade_all() の342~346行目が該当します。呼び出される関数の定義は次のとおり。

  1. /**
  2.  * Execute changes made in WordPress 2.7.
  3.  *
  4.  * @since 2.7.0
  5.  */
  6. function upgrade_270() {
  7.     global $wpdb, $wp_current_db_version;
  8.  
  9.     if ( $wp_current_db_version < 8980 )
  10.         populate_roles_270();
  11.  
  12.     // Update post_date for unpublished posts with empty timestamp
  13.     if ( $wp_current_db_version < 8921 )
  14.         $wpdb->query( "UPDATE $wpdb->posts SET post_date = post_modified WHERE post_date = '0000-00-00 00:00:00'" );
  15. }
  16.  
  17. /**
  18.  * Execute changes made in WordPress 2.8.
  19.  *
  20.  * @since 2.8.0
  21.  */
  22. function upgrade_280() {
  23.     global $wp_current_db_version;
  24.  
  25.     if ( $wp_current_db_version < 10360 )
  26.         populate_roles_280();
  27. }
  1. /**
  2.  * Create and modify WordPress roles for WordPress 2.7.
  3.  *
  4.  * @since 2.7.0
  5.  */
  6. function populate_roles_270() {
  7.     $role =& get_role( 'administrator' );
  8.  
  9.     if ( !empty( $role ) ) {
  10.         $role->add_cap( 'install_plugins' );
  11.         $role->add_cap( 'update_themes' );
  12.     }
  13. }
  14.  
  15. /**
  16.  * Create and modify WordPress roles for WordPress 2.8.
  17.  *
  18.  * @since 2.8.0
  19.  */
  20. function populate_roles_280() {
  21.     $role =& get_role( 'administrator' );
  22.  
  23.     if ( !empty( $role ) ) {
  24.         $role->add_cap( 'install_themes' );
  25.     }
  26. }

populate_roles_***() で追加している権限を確認したところ、3つとも登録されていました。今度はさかのぼって確認してみると、populate_roles_260() で追加される2つの権限のみがありませんでした。しかもその1つは今回問題となっている update_plugins です。

どうやらここで失敗していたようです。

  1. /**
  2.  * Create and modify WordPress roles for WordPress 2.6.
  3.  *
  4.  * @since 2.6.0
  5.  */
  6. function populate_roles_260() {
  7.     $role =& get_role( 'administrator' );
  8.  
  9.     if ( !empty( $role ) ) {
  10.         $role->add_cap( 'update_plugins' );
  11.         $role->add_cap( 'delete_plugins' );
  12.     }
  13. }

修復方法

原因まで判明したので、権限情報の修復を行います。直接書き換えてもいいのですが、間違えると面倒なので簡単なプログラムを作りました。

データを抽出して権限を追加し、再度シリアライズして表示するだけ。表示されたテキストデータをデータベース用の管理画面(ここのサーバーは phpMyAdmin が使えるのでそれを使いました)から直接反映させました。

このプログラムは自由に使っていただいてかまいませんが、何かあったときの補償はできませんので自己責任でお願いします。

  1. <?php
  2. header('Content-type: text/plain;charset=UTF-8');
  3. $link = mysql_connect('データベースサーバー', 'ユーザー', 'パスワード');
  4.  
  5. if ( $link )
  6. {
  7.     mysql_set_charset('utf8');
  8.     if ( mysql_select_db('データベース') )
  9.     {
  10.         $sql = "SELECT `option_name`,`option_value` FROM `wp_options` WHERE `option_name` = 'wp_user_roles'";
  11.         if ( $res = mysql_query($sql) )
  12.         {
  13.             if ( $rows = mysql_fetch_assoc($res) )
  14.             {
  15.                 $rol_data = unserialize($rows['option_value']);
  16.                 $rol_data['administrator']['capabilities']['update_plugins'] = true;
  17.                 $rol_data['administrator']['capabilities']['delete_plugins'] = true;
  18.                 print serialize($rol_data);
  19.             }
  20.         }
  21.         else
  22.         {
  23.             print mysql_error();
  24.         }
  25.     }
  26.     else
  27.     {
  28.         print mysql_error();
  29.     }
  30. }
  31. else
  32. {
  33.     print mysql_error();
  34. }
  35. ?>

というわけで、無事自動アップグレードの画面を拝むことができました。

Comments (1)

2008年11月29日 土曜日

WordPress 用プラグイン monthchunks のバグを修正

Filed under: WordPress,ハック,プラグイン
タグ:, , ,
時間:3時45分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

 このブログでは、月ごとアーカイブをコンパクトに表示してくれるプラグイン monthchunks を使用していますが、記事がないのに表示されている月があることが判明しました。

 問題の月は、2006年の9月。Google ウェブマスターツールのリンクエラー報告によると、76箇所で表示されているとのこと。さすがに数が多いので、本腰を入れて調べてみることにしました。久しぶりのプラグインハックです。

 何はともあれ、まずはプラグインのコードを読みます。使用しているバージョンは、最新の2.1。関数が1つ定義してあるだけでコメントを含めても154行と非常に短いので、初めてのハックにはお勧めかもしれません。

 大まかな流れは、SQL 文を生成して実行するだけ。オプションによって多少分岐がありますが、上から追いかけていけば大したことはありません。101行目からと114行目(110行目から始まる foreach の中)からの2箇所でデータベースにアクセスしています。1箇所目は記事のある年度の取得、2箇所目は各年度別に記事のある月の取得をしています。

  1. // get an array of the years in which there are posts
  2.     $wpdb->query("SELECT DATE_FORMAT(post_date, '%Y') as post_year
  3.                  FROM $wpdb->posts
  4.                  WHERE post_status = 'publish'
  5.                  GROUP BY post_year
  6.                  HAVING post_year <> '0000'
  7.                  ORDER BY post_year $year_order");
  8.     $years = $wpdb->get_col();
  1. // get an array of months for the current year without leading zero
  2.         // sort by month with leading zero
  3.         $months = $wpdb->get_results("SELECT DATE_FORMAT(post_date, '%c') as post_month,
  4.                                      $month_format AS display_month,
  5.                                      DATE_FORMAT(post_date, '%M') as post_month_name
  6.                                      FROM $wpdb->posts
  7.                                      WHERE DATE_FORMAT(post_date, '%Y') = $year
  8.                                      AND post_status = 'publish'
  9.                                      GROUP BY DATE_FORMAT(post_date, '%m')
  10.                                      ORDER BY post_date");

 今回問題になっているのは月の情報なので、2箇所目を詳しく見ていきます。PHP のコードと SQL が混在してわかりにくいので、最終的な SQL 文を記述してみます。オプションを特に指定しない場合は、次のようなものが出来上がります。(改行やインデントなどを加えてあります)

  1. SELECT
  2.     DATE_FORMAT(`post_date`, '%c') AS `post_month`,
  3.     DATE_FORMAT(`post_date`, '%c') AS `display_month`,
  4.     DATE_FORMAT(`post_date`, '%M') AS `post_month_name`
  5. FROM `wp_posts`
  6. WHERE
  7.     DATE_FORMAT(`post_date`, '%Y') = 2006
  8.     AND post_status = 'publish'
  9. GROUP BY DATE_FORMAT(`post_date`, '%m')
  10. ORDER BY `post_date`;

 実際にこの SQL 文を手がかりに検索してみると、記事ではなく固定ページが見つかりました。記事と固定ページを区別していないため、固定ページしかない月も表示してしまっていたのです。

 ここまでわかればあとは簡単。検索条件に記事であることを加えればいいのです。記事の場合は post_type というフィールドの値が post となるので(WordPress 2.1以降)、post_type = 'post' を加えます。変更後は次のようになります。私は120行目に加えました。

  1. // get an array of months for the current year without leading zero
  2.         // sort by month with leading zero
  3.         $months = $wpdb->get_results("SELECT DATE_FORMAT(post_date, '%c') as post_month,
  4.                                      $month_format AS display_month,
  5.                                      DATE_FORMAT(post_date, '%M') as post_month_name
  6.                                      FROM $wpdb->posts
  7.                                      WHERE DATE_FORMAT(post_date, '%Y') = $year
  8.                                      AND post_status = 'publish'
  9.                                      AND post_type = 'post'
  10.                                      GROUP BY DATE_FORMAT(post_date, '%m')
  11.                                      ORDER BY post_date");

 これにより、その年の公開されている記事という条件で絞り込むことができるようになりました。現在2006年の9月は表示されなくなっています。

2010年6月21日追記

prioさんからコメントをいただきましたので、年度の表示についても追記します。確認はしていませんので、参考程度にどうぞ。

100行目からの年度のコードを見ると、月の取得同様に固定ページも含むようになっています。SQLにしてみるとこんな感じ。

  1. SELECT
  2.     DATE_FORMAT(post_date, '%Y') as post_year
  3. FROM `wp_posts`
  4. WHERE
  5.     post_status = 'publish'
  6. GROUP BY post_year
  7. HAVING post_year <> '0000'
  8. ORDER BY post_year DESC

記事の投稿に限定する条件を加えると次のようになります。

  1. // get an array of the years in which there are posts
  2.     $wpdb->query("SELECT DATE_FORMAT(post_date, '%Y') as post_year
  3.                  FROM $wpdb->posts
  4.                  WHERE post_status = 'publish'
  5.                  AND post_type = 'post'
  6.                  GROUP BY post_year
  7.                  HAVING post_year <> '0000'
  8.                  ORDER BY post_year $year_order");
  9.     $years = $wpdb->get_col();

104行目が追加した行です。意味については、月の場合と同じです。

Comments (2)

2008年3月14日 金曜日

管理者なのに新規にページが公開できない原因

Filed under: WordPress,ハック
時間:1時10分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

 先週の記事でも書きましたように、Google AdSense の規約変更のためにプライバシーポリシーの掲載が必要となりました。いろいろと考えて自力で作ったのですが、なぜか肝心のページの公開ができません。

 公開するには、ステータスを「公開」にすればいいはずですが、私の場合はなぜかその項目が表示されていません。その部分のコードを探して確認してみることに。久しぶりのハックです。バージョンは ME2.1.3。ME2.0 からアップグレードしてあります。

解析編

 ページ編集画面のアドレスからファイルを推測したどっていくと、wp-admin/edit-page-form.php に該当箇所が見つかりました。

  1. <fieldset class="dbx-box">
  2. <h3 class="dbx-handle"><?php _e('Page Status') ?></h3>
  3. <div class="dbx-content"><?php if ( current_user_can('publish_pages') ) : ?>
  4. <label for="post_status_publish" class="selectit"><input id="post_status_publish" name="post_status" type="radio" value="publish" <?php checked($post->post_status, 'publish'); checked($post->post_status, 'future'); ?> /> <?php _e('Published') ?></label>
  5. <?php endif; ?>
  6.       <label for="post_status_draft" class="selectit"><input id="post_status_draft" name="post_status" type="radio" value="draft" <?php checked($post->post_status, 'draft'); ?> /> <?php _e('Draft') ?></label>
  7.       <label for="post_status_private" class="selectit"><input id="post_status_private" name="post_status" type="radio" value="private" <?php checked($post->post_status, 'private'); ?> /> <?php _e('Private') ?></label></div>
  8. </fieldset>

 63行目が公開とする項目で、62行目のところで権限の有無によって処理を分岐しているようです。管理者アカウントで作業しているので権限がないとは思えないのですが。

 今度は条件式に使われている関数 current_user_can() を調べます。まずは定義場所を見つける必要があります。ファイル群から検索という方法でもいいのですが、試しに Google で検索してみることに。キーワードは「WordPress current_user_can」。
 capabilities.php というファイルにあるという情報が見つかりました。コアファイルの置かれているところを探してみると、wp-includes/ にありました。

  1. // Capability checking wrapper around the global $current_user object.
  2. function current_user_can($capability) {
  3.     $current_user = wp_get_current_user();
  4.  
  5.     $args = array_slice(func_get_args(), 1);
  6.     $args = array_merge(array($capability), $args);
  7.  
  8.     if ( empty($current_user) )
  9.         return false;
  10.  
  11.     return call_user_func_array(array(&$current_user, 'has_cap'), $args);
  12. }

 446行目で wp_get_current_user() が返す値を変数に入れています。変数の値を調べてみると、クラス WP_User のインスタンスが入っていました。このクラスも、capabilities.php に定義があります。長いのでコードは省略します。

 448行目の処理は、この関数の2番目以降の引数を要素として持つ配列を取得しています。今回引数は1つだけなので、当然この行の直後では $args は要素を持たない配列です。449行では配列の結合をしていて、変数 $args には array('publish_pages') に相当する値が入ります。

 454行目では、コールバック処理の結果を返しています。変数 $current_user にはクラス WP_User のインスタンスが入っていますので、コールバック関数は WP_User のメンバ関数 has_cap() と言うことになります。定義は次のようになっています。

  1. //has_cap(capability_or_role_name) or
  2.     //has_cap('edit_post', post_id)
  3.     function has_cap($cap) {
  4.         if ( is_numeric($cap) )
  5.             $cap = $this->translate_level_to_cap($cap);
  6.  
  7.         $args = array_slice(func_get_args(), 1);
  8.         $args = array_merge(array($cap, $this->ID), $args);
  9.         $caps = call_user_func_array('map_meta_cap', $args);
  10.         // Must have ALL requested caps
  11.         $capabilities = apply_filters('user_has_cap', $this->allcaps, $caps, $args);
  12.         foreach ($caps as $cap) {
  13.             //echo "Checking cap $cap<br/>";
  14.             if(empty($capabilities[$cap]) || !$capabilities[$cap])
  15.                 return false;
  16.         }
  17.  
  18.         return true;
  19.     }

 引数 $cap には、コールバックで渡された文字列 publish_pages が入ります。数値ではないので、262行目の処理は行われません。264行目と265行目は、current_user_can() の場合と同じですね。$this->ID は、ログイン中のユーザーのID(ログインIDではなく、データベーステーブルの主キー)が入っています。

 266行目で再びコールバックです。この段階 $args には array(‘publish_pages’, ユーザーID) が入っているので、map_meta_cap('publish_pages', ユーザーID) ということをしていることになります。

 関数の定義は、285行目から442行目にかけて記述されています。これもかなり長いですが、やっていることは第一引数の値で switch 分岐処理をしているだけです。publish_pages のところはないので、default 節で次のように処理をしているだけです。

  1. default:
  2.         // If no meta caps match, return the original cap.
  3.         $caps[] = $cap;

 そして、その戻り値は予想通り array('publish_pages') でした。

 268行目。プラグイン作成ではおなじみの、apply_filters が使われています。以前にもこの関数のハックに挑んだことがありますが、ややこしくて苦手です。面倒なので $capabilities の中身を見てみるだけに。連想配列になっていて、ユーザステータスを記録しているようです。$capabilities['publish_pages'] はありませんでした。

 269~273行目では、値がないか Not true のものが見つかったら false でリターンするという処理です。値がないので false になってしまいます。

解決編

 すっかり弱ってしまいました。散々調べ続けて、わかったのは間違いなくページを公開する権限がないことだけ。(仕組みがわかって勉強になったというのもありますけど・・・)

 方向を変えて、publish_pages について調べてみることに。「WordPress publish_pages」と検索。WordPress Japan のフォーラムに、よく似た不具合のスレッドを発見。

 そこからリンクされているページを見ると、データベースに記録されているシステム設定に問題がある模様。どれどれと確認してみると、$capabilities の内容によく似たデータが。そして、そこには publish_pages はありませんでした。2.0 からアップグレードするときに、スクリプトが修正してくれなかっただけなのではないでしょうか。実際、プラグインチェック用に新規インストールした 2.1 のほうでは次のような内容で、そこにはちゃんと publish_pages があります。

  1. a:5:{s:13:"administrator";a:2:{s:4:"name";s:27:"管理人 - (Administrator)";s:12:"capabilities";a:47:{s:13:"switch_themes";b:1;s:11:"edit_themes";b:1;s:16:"activate_plugins";b:1;s:12:"edit_plugins";b:1;s:10:"edit_users";b:1;s:10:"edit_files";b:1;s:14:"manage_options";b:1;s:17:"moderate_comments";b:1;s:17:"manage_categories";b:1;s:12:"manage_links";b:1;s:12:"upload_files";b:1;s:6:"import";b:1;s:15:"unfiltered_html";b:1;s:10:"edit_posts";b:1;s:17:"edit_others_posts";b:1;s:20:"edit_published_posts";b:1;s:13:"publish_posts";b:1;s:10:"edit_pages";b:1;s:4:"read";b:1;s:8:"level_10";b:1;s:7:"level_9";b:1;s:7:"level_8";b:1;s:7:"level_7";b:1;s:7:"level_6";b:1;s:7:"level_5";b:1;s:7:"level_4";b:1;s:7:"level_3";b:1;s:7:"level_2";b:1;s:7:"level_1";b:1;s:7:"level_0";b:1;s:17:"edit_others_pages";b:1;s:20:"edit_published_pages";b:1;s:13:"publish_pages";b:1;s:12:"delete_pages";b:1;s:19:"delete_others_pages";b:1;s:22:"delete_published_pages";b:1;s:12:"delete_posts";b:1;s:19:"delete_others_posts";b:1;s:22:"delete_published_posts";b:1;s:20:"delete_private_posts";b:1;s:18:"edit_private_posts";b:1;s:18:"read_private_posts";b:1;s:20:"delete_private_pages";b:1;s:18:"edit_private_pages";b:1;s:18:"read_private_pages";b:1;s:12:"delete_users";b:1;s:12:"create_users";b:1;}}s:6:"editor";a:2:{s:4:"name";s:20:"編集者 - (Editor)";s:12:"capabilities";a:34:{s:17:"moderate_comments";b:1;s:17:"manage_categories";b:1;s:12:"manage_links";b:1;s:12:"upload_files";b:1;s:15:"unfiltered_html";b:1;s:10:"edit_posts";b:1;s:17:"edit_others_posts";b:1;s:20:"edit_published_posts";b:1;s:13:"publish_posts";b:1;s:10:"edit_pages";b:1;s:4:"read";b:1;s:7:"level_7";b:1;s:7:"level_6";b:1;s:7:"level_5";b:1;s:7:"level_4";b:1;s:7:"level_3";b:1;s:7:"level_2";b:1;s:7:"level_1";b:1;s:7:"level_0";b:1;s:17:"edit_others_pages";b:1;s:20:"edit_published_pages";b:1;s:13:"publish_pages";b:1;s:12:"delete_pages";b:1;s:19:"delete_others_pages";b:1;s:22:"delete_published_pages";b:1;s:12:"delete_posts";b:1;s:19:"delete_others_posts";b:1;s:22:"delete_published_posts";b:1;s:20:"delete_private_posts";b:1;s:18:"edit_private_posts";b:1;s:18:"read_private_posts";b:1;s:20:"delete_private_pages";b:1;s:18:"edit_private_pages";b:1;s:18:"read_private_pages";b:1;}}s:6:"author";a:2:{s:4:"name";s:20:"投稿者 - (Author)";s:12:"capabilities";a:10:{s:12:"upload_files";b:1;s:10:"edit_posts";b:1;s:20:"edit_published_posts";b:1;s:13:"publish_posts";b:1;s:4:"read";b:1;s:7:"level_2";b:1;s:7:"level_1";b:1;s:7:"level_0";b:1;s:12:"delete_posts";b:1;s:22:"delete_published_posts";b:1;}}s:11:"contributor";a:2:{s:4:"name";s:25:"寄稿者 - (Contributor)";s:12:"capabilities";a:5:{s:10:"edit_posts";b:1;s:4:"read";b:1;s:7:"level_1";b:1;s:7:"level_0";b:1;s:12:"delete_posts";b:1;}}s:10:"subscriber";a:2:{s:4:"name";s:24:"購読者 - (Subscriber)";s:12:"capabilities";a:2:{s:4:"read";b:1;s:7:"level_0";b:1;}}}

 思い切ってデータを差し替えてみると、見事公開の選択肢が表示されるように。これで、やっとプライバシーポリシーの公開ができます。

Comments (0)

2007年12月1日 土曜日

akismet-multibyteview のリンクの修正

Filed under: WordPress,ハック,プラグイン
時間:9時28分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

 WordPress のアップグレードと一緒に、そのままになっていた akismet-multibyteview のアップグレードもしました。1.20 からはダッシュボードのボックス枠(名前はあるんでしょうか。hook が activity_box_end なので、アクティビティーボックス?)にマルチバイトスパムの数が表示されるようになってさらに便利に。

 サイト内トラックバックが捕獲されてしまったので早速リンクから一覧に飛ぼうとしたのですが、File Not Found になってしまいました。ブラウザのアドレス覧を見ると http://edit-comments.php?page=akismet-admin&multibyte=true となっています。ソースコードでもやはりおかしなことに。

 こういうのを見るとついつい調べてみたくなります。早速ハックです。

ソースを見る

 何はともあれ、ソースコードを見ます。該当の処理は akismet-multibyteview.php の関数 akismet_stats_mbinfo が行っています。1.21 では139行目から始まっていて、目的の場所は156行目です。

  1. if ( $mbs_count ) {
  2.             echo
  3.                 '<li><a href="' . clean_url("{$this->akismet_admin_uri}&multibyte=true") .
  4.                 '"><strong>要チェック</strong></a>: スパムじゃないかもしれないコメントを ' .
  5.                 $mbs_count.' 件捕獲中です。</li>';
  6.         } else {
  7.             echo '<li>この中に日本語を含むものはありません。</li>';
  8.         }

 clean_url() という関数が使われています。URL として正しくなるように修正をするもので、WordPress ME2.1.3では wp-includes/formatting.php で次のように定義されていました。

  1. function clean_url( $url, $protocols = null ) {
  2.     if ('' == $url) return $url;
  3.     $url = preg_replace('|[^a-z0-9-~+_.?#=!&;,/:%]|i', '', $url);
  4.     $strip = array('%0d', '%0a');
  5.     $url = str_replace($strip, '', $url);
  6.     $url = str_replace(';//', '://', $url);
  7.     // Append http unless a relative link starting with / or a php file.
  8.     if ( strpos($url, '://') === false &&
  9.         substr( $url, 0, 1 ) != '/' && !preg_match('/^[a-z0-9]+?\.php/i', $url) )
  10.         $url = 'http://' . $url;
  11.    
  12.     $url = preg_replace('/&([^#])(?![a-z]{2,8};)/', '&#038;$1', $url);
  13.     if ( !is_array($protocols) )
  14.         $protocols = array('http', 'https', 'ftp', 'ftps', 'mailto', 'news', 'irc', 'gopher', 'nntp', 'feed', 'telnet');
  15.     if ( wp_kses_bad_protocol( $url, $protocols ) != $url )
  16.         return '';
  17.     return $url;
  18. }

 1078行目で、:// を含まなく / で始まっていなくて半角英数字+拡張子 php になっていない場合に http:// をつけるという処理をしています。

 そして、引数の文字列に含まれる変数 $this->akismet_admin_uri は、同じファイルの62行目で次のように定義されています。

  1. $this->akismet_admin_uri = 'edit-comments.php?page=akismet-admin';

 さて、ここで関数に渡される引数を確認してみます。変数が展開されると、引数は edit-comments.php?page=akismet-admin&multibyte=true となります。拡張子は php ですが、半角英数のファイル名ではありません。ハイフン(-)が入ってしまっています。そのため、clean_url() は http:// をつけてしまっていました。

 初めは akismet-multibyteview のバグだと思い解析をしていましたが、実は WordPress 側のバグでした。2.2.1 のコードでは次のように修正されています。

  1. if ( strpos($url, '://') === false &&
  2.         substr( $url, 0, 1 ) != '/' && !preg_match('/^[a-z0-9-]+?\.php/i', $url) )
  3.         $url = 'http://' . $url;

 作者のひろまささんはすでに2.3系になっていますので気が付かなかったのかもしれません。

修正

 コアファイルのほうを修正してもいいのですが、影響範囲が少ないプラグインのほうを直してしまいます。固定アドレスなので、関数の処理は必要ないと判断。156行目を次のように変更しました。ついでに & を文字参照にしてあります。やらなくても大丈夫ですが、気分的に。

  1. '<li><a href="' . "./{$this->akismet_admin_uri}&amp;multibyte=true" .

 これによってリンク先が正しくなり、マルチバイトスパムの一覧に飛ぶことができるようになりました。

Comments (1)

2007年6月23日 土曜日

WordPress と Ajax ライブラリ

Filed under: ハック,プラグイン,雑多
時間:14時41分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

 WordPress ME(以下、ME と表記)はバージョンが上がるごとに Ajax の導入が進んでいるようです。当然のようにいくつか Ajax ライブラリが標準で付属していて、その数も導入具合同様増えている模様。
 標準で付いているというのは一見便利なようですが、プラグインを開発する側から見るとなかなか悩ましいのです。自分が使いたいものが付属していればいいのですが、そうでない場合は相性の問題が出てきます。

 現在このブログで使用している WP のバージョンは 2.0.10 です。このバージョンには Simple AJAX Code-Kit(SACK) のバージョン不明のものが付属しています。ME 2.2 からは jQuery が中心になっていくようなので、Ajax を導入したプラグインの開発に jQuery を使用しようとしたのですが、どうやら SACK と相性が悪いようでエラーが出てしまいました。
 また、ME 2.1.3 と 2.2.1 に付属の SACK のバージョンは 1.6.1 となっており、2.0.10 付属のものとは使い方に違いがあります。SACK を開発に使用するとしても、2つのバージョンに対応させる必要があります。ME 本体で使っているのですから、自前で持っているものを使うというわけにも行きません。正直、うまく動くとは思えません。

 とりあえず、ME 2.0.10 には付属していなくて ME 2.1.3 と ME 2.2.1 に 1.5.0 が付属している Prototype.js が大丈夫そうなので、これを使って開発に挑んでみようと思います。

Comments (0)

sibling has no properties

Filed under: ハック,雑多
時間:12時21分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

 WordPress の投稿画面にあるトラックバックとかカスタムフィールドといった、ドラッグして順番を入れ替えられる部分(ドッキングボックスというらしい)を追加できる dbx_post_advanced というフックを見つけていろいろやっていたら、いつの間にか順番の入れ替えができなくなってしまいました。

 Firefox2 だとドラッグのたびに sibling has no properties という JavaScript エラーが出てます。IE の場合は位置がおかしくなり、そのままフリーズしてしまいました。

 Firefox での該当箇所は見つけたけどよくわからない。仕方がないのでプラグインを無効にしたりファイルをオリジナルのものからアップロードし直したりしてみましたが変化なし。

 別に順番を変えられなくてもそんなに困ることじゃないのですが、プラグインの開発中に変なエラーが出ても嫌なので直したいところです。同じバージョンを別のところにインストールですかねぇ。原因を切り分けないと・・・。

2007年6月23日 午後12時28分 追記

 デバッグのため該当ファイルの wp-includes/js/dbx.js に改行を入れた(ファイルサイズを抑えるために全部1行になってます)ものをアップロードしたら直ってました。試しに me209-to-2010.zip からアップロードしてみるとこれもOK。
 よくわからないけど直ったようです。原因不明のままですが、まあよかった。

Comments (0)

2007年3月24日 土曜日

スパムコメントのアクセス元を抽出してブロック

Filed under: WordPress,ハック
時間:18時44分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

 スパムコメントは99%以上が Akismet によってブロックされているのですが、鬱陶しいので Apache の機能で特定 IP アドレスからのアクセスをブロックしています。
 最近またスパムコメントが増えてきましたので改めて設定。どうやっているかをついでにメモ。

 まずはスパム業者の IP アドレスの抽出です。このブログを設置しているサーバではデータベースに phpMyAdmin でアクセスできるので、次の SQL を直接発行しました。

  1. SELECT `comment_author_IP`, count(`comment_author_IP`) AS cnt
  2. FROM `wp_comments`
  3. WHERE `comment_approved` = 'spam'
  4. AND TO_DAYS( NOW() ) - TO_DAYS(`comment_date`) < = 30
  5. GROUP BY `comment_author_IP`
  6. ORDER BY cnt DESC;

 これで、最近の30日で回数の多い順に IP アドレスが抽出されます。この IP アドレスを Limit ディレクティブでアクセス禁止にします。私は上位10個を指定しました。

  1. <Limit GET POST>
  2. order allow,deny
  3. deny from 012.345.678.9
  4. allow from all
  5. </Limit>

 やっていることは、WorstOffenders(旧名称 Akismet htaccess extension) というプラグインと同じだと思います。面倒なら、このプラグインを入れてしまうほうがいいかもしれません。

Comments (0)

2007年2月15日 木曜日

WordPress プラグインのトラブル解決

Filed under: EntryKeywords,WordPress,ハック,プラグイン
時間:4時14分
投稿者:よしとも
AddClips 経由でソーシャルブックマークに登録

 フック admin_headdo_action を実行している直前で $wp_filter の中身をすべて表示させてみたところ、あっさり解決しました。
 関数名を検索してみるとしっかり登録されていたので登録内容を確認。フックのところを見たとき愕然としました。

 フック名は、「admin_head 」。後ろに余計なスペースが・・・。

 はい。単なる入力ミスです。こんなのに3日も悩んでたんですか。
 スペルを間違えないようにコピーしてきたときにスペースが入ってしまったのではないかと。本末転倒もいいところです(^^;

 フックは開発者が自由に決められる仕組みなので、スペルミスには注意が必要ですね。構文エラーなら PHP が面倒を見てくれますが、今回のような論理エラーは思い込みなどで気がつくのに時間がかかります。皆さんもご注意くださいませ。

Comments (0)
Page 1 of 212

HTML convert time: 1.160 sec. Powered by

Images is enhanced with WordPress Lightbox JS by Zeo