2008年11月29日 土曜日

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

Filed under: WordPress, プラグイン, ハック — 投稿者:よしとも
時間:3時45分|パーマリンク
AddClips 経由でソーシャルブックマークに登録:
人気度: 1%
by Popularity Contest 日本語版

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

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

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

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

PHP:
  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();

PHP:
  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_type = 'post'
  9.                                       GROUP BY DATE_FORMAT(post_date, '%m')
  10.                                       ORDER BY post_date");

 今回問題になっているのは月の情報なので、2箇所目を詳しく見ていきます。PHP のコードと SQL が混在してわかりにくいので、最終的な 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_type` = 'post'
  9. GROUP BY DATE_FORMAT(`post_date`, '%m')
  10. ORDER BY `post_date`;

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

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

PHP:
  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月は表示されなくなっています。

コメント (0)

2008年3月14日 金曜日

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

Filed under: WordPress, ハック — 投稿者:よしとも
時間:1時10分|パーマリンク
AddClips 経由でソーシャルブックマークに登録:
人気度: 15%
by Popularity Contest 日本語版

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

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

解析編

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

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/ にありました。

PHP:
  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() と言うことになります。定義は次のようになっています。

PHP:
  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 節で次のように処理をしているだけです。

PHP:
  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 があります。

CODE:
  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;}}}

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

コメント (0)

2007年12月1日 土曜日

akismet-multibyteview のリンクの修正

Filed under: WordPress, プラグイン, ハック — 投稿者:よしとも
時間:9時28分|パーマリンク
AddClips 経由でソーシャルブックマークに登録:
人気度: 25%
by Popularity Contest 日本語版

 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行目です。

PHP:
  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 で次のように定義されていました。

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行目で次のように定義されています。

PHP:
  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 のコードでは次のように修正されています。

PHP:
  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行目を次のように変更しました。ついでに & を文字参照にしてあります。やらなくても大丈夫ですが、気分的に。

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

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

コメント (1)

2007年6月23日 土曜日

WordPress と Ajax ライブラリ

Filed under: 雑多, プラグイン, ハック — 投稿者:よしとも
時間:14時41分|パーマリンク
AddClips 経由でソーシャルブックマークに登録:
人気度: 38%
by Popularity Contest 日本語版

 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 が大丈夫そうなので、これを使って開発に挑んでみようと思います。

コメント (0)

sibling has no properties

Filed under: 雑多, ハック — 投稿者:よしとも
時間:12時21分|パーマリンク
AddClips 経由でソーシャルブックマークに登録:
人気度: 43%
by Popularity Contest 日本語版

 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。
 よくわからないけど直ったようです。原因不明のままですが、まあよかった。

コメント (0)
Page 1 of 3123»