カスタム投稿タイプとパーマリンクと分割ページ

WordPressで、カスタム投稿タイプを作って、テーマを作り込んでいる途中、ちょっとはまった点があり、その備忘録です。

まぁ、既存のプラグインを導入すればいいわけですが、一つのテーマ内で全てを完結させたい、という方針で作ってる訳です、ハイ。
導入も簡単ですしね。プラグインをアレとコレと入れて、設定して・・・という手間はなるべく避けたいわけです。

さて、本題。

カスタム投稿タイプを作ってパーマリンクを投稿IDでアクセスさせようとしました。要するに・・・

ttp://hoge.com/information/123

っていう風に。これ自体はFAQなので、検索すると、下記コードが載ったブログ記事がいっぱいヒットします。

/* カスタム投稿タイプ information の例 */

add_action('init', 'myposttype_rewrite');
function myposttype_rewrite() {
    global $wp_rewrite;
    $queryarg = 'post_type=information&p=';
    $wp_rewrite->add_rewrite_tag('%information_id%', '([^/]+)', $queryarg);
    $wp_rewrite->add_permastruct('information', '/information/%information_id%', false);
}

add_filter('post_type_link', 'myposttype_permalink', 1, 3);
function myposttype_permalink($post_link, $id = 0, $leavename) {
    global $wp_rewrite;
    $post = &get_post($id);
    if ( is_wp_error( $post ) )
        return $post;
    $newlink = $wp_rewrite->get_extra_permastruct('information');
    $newlink = str_replace("%information_id%", $post->ID, $newlink);
    $newlink = home_url(user_trailingslashit($newlink));
    return $newlink;
}

だけど・・・これ現在のバージョン(3.5)では致命的な欠陥があります。

カスタム投稿タイプの投稿記事内に分割ページ(<!–nextpage–>>)を入れて、2ページ目、3ページ目・・・とアクセスできません。
要するに・・・

ttp://hoge.com/information/123/2

などのようにアクセスすると、投稿がありません、とかなってしまいます。つまり、上記コードを入れることで、Rewrite処理が上手く機能しなくなっているようです。試しに上記コードをコメントアウトすると、ちゃんと分割ページが機能します。カスタム投稿タイプのパーマリンクは、%postname%になっているはずですから、以下のようになるわけです。

ttp://hoge.com/information/投稿のタイトルのスラッグ/2

おそらく、パーマリンクを投稿IDにする、上記コードには何かが足りないのだと思います。Wordpressのフォーラムにも質問を投げて見たのですが結局アドバイス頂けなかったので、いろいろ試行錯誤しながら調べた結果、以下の一行を入れることで、無事分割ページが機能するようになりました。

/* ごめんなさい、浅学なんで、これで上手くいく理屈が分かりません。 */
$post_type = 'information'; //これはサンプル
add_rewrite_rule(&quot;{$post_type}/(.+?)/([0-9]+)$&quot;,'index.php?post_type='.$post_type.'&amp;p=$matches[1]&amp;page=$matches[2]','top');

つまり、分割ページのために、リライトルールを一個追加してしまえ、ってことです。

それと、ネットで検索して出てくる上記コードで、現在のWordpressのバージョンにおいて、フィルターフック名 ‘post_type_link’ でのコールバック関数の第二引数が間違っていると思います。

//上記コードでは、
function myposttype_permalink($post_link, $id = 0, $leavename){}

//となってますが、今のバージョンでは、
function myposttype_permalink($post_link,$post,$leavename){}

//という風に、バージョン3.1から、投稿IDではなく、WP_Postオブジェクト自体が渡されるように変更になっていると思われます。
// 参考URL http://adambrown.info/p/wp_hooks/hook/post_type_link

この点も踏まえて、修正したコードが、以下のようになります。

/* カスタム投稿タイプ information の例 の分割ページ対応修正版 */
add_action('init', 'myposttype_rewrite');
function myposttype_rewrite() {
    global $wp_rewrite;
    $queryarg = 'post_type=information&amp;p=';
    $wp_rewrite-&gt;add_rewrite_tag('%information_id%', '([^/]+)', $queryarg);
    $wp_rewrite-&gt;add_permastruct('information', '/information/%information_id%', false);

    add_rewrite_rule(&quot;information/(.+?)/([0-9]+)$&quot;,'index.php?post_type=information&amp;p=$matches[1]&amp;page=$matches[2]','top');
}

add_filter('post_type_link', 'myposttype_permalink', 1, 3);
function myposttype_permalink($post_link, $post, $leavename) {
    global $wp_rewrite;

    $newlink = $wp_rewrite-&gt;get_extra_permastruct('information');
    $newlink = str_replace(&quot;%information_id%&quot;, $post-&gt;ID, $newlink);
    $newlink = home_url(user_trailingslashit($newlink));
    return $newlink;
}

でも、この修正によって、別の不具合が出るかも・・・。まぁ、いいや。

パーマリンク関係のコードを変更したら、必ず、設定 => パーマリンクの空更新を忘れずに。

テーマやテンプレートを動的に変更

私的記録です。

WordPressで、サイトの「お知らせ」というカスタム投稿タイプを作成した時のこと。

たとえば他のサイトからインラインフレームで読み込ませるときは、特定のテンプレートに変更したい、という場合、template_includeというフィルターが使える。トリガーとして、URLパラメータになにがしらのパラメータを渡したときに切り替えるようにすれば上手くいきます。

また、ある特定のURLパラメータをつけてアクセスしたときは、別のテーマで表示したい! という、それって何の意味があるの?的なこともしたかったので、纏めてプラグインにした。

チョー簡単(^^; WordPressってホントよく出来てますね~。

&lt;?php
/*
Plugin Name: Switcher for Theme or Template
*/

/************************************************************
 動的にテーマやテンプレートを変える
 すっごい手抜きサンプル
************************************************************/
add_filter('template_include','DynaChange::Template');
add_filter('stylesheet', 'DynaChange::Theme');
add_filter('template', 'DynaChange::Theme');

class DynaChange
{
  public static function Template($template)
    {
      //URLパラメータにchtmp=プレフィックス があれば、
      //現在のテンプレートファイル名にプレフィックスを付けた
      //テンプレートファイルに切り替える。
      //適当サンプルのため、子テーマには対応していない。
      if(!empty($_GET['chtmp']))
        {
          $name = str_replace(STYLESHEETPATH . '/', '', $template);
          $template = STYLESHEETPATH . '/' . $_GET['chtmp'] .'-'. $name;
        }

      return $template;
    }

  public static function Theme($stylesheet)
    {
      //URLパラメータに chth=テーマ名 があれば、
      //そのテーマに切り替える。

      if(!empty($_GET['chth']))
        {
          $theme = wp_get_theme($_GET['chth']);
          if($theme-&gt;exists())
            $stylesheet = $theme-&gt;Template;
        }
      return $stylesheet;
    }
}
?&gt;

説明要らないっすね~。

WORDPRESSプラグインのテンプレート

私的記録。

WordPressのテーマやプラグインは便利ですよね。
既存のプラグインを組み合わせて使うと、あら不思議、それなりにWEBシステムが出来てしまうではありませんか(^_^;)

あまりにも便利なので、自分でも足りない機能を作ってしまいたい、と思うのは当然でしょう。

というわけで、プラグインをすぐ作れるようにスケルトン的なテンプレートを記録。

Add Html Code

このスケルトン・プラグインは、wp_head,wp_footerのアクションを登録するプラグインである。
要するにテーマのヘッダとフッタに好きなHTMLコードをインジェクトするプラグイン。

このスケルトンは4つのファイルで構成されています。

■プラグイン本体 (addhtml.php) まずはこれがないと。

<?php
/*
Plugin Name: Add HTML code for WordPress
Description: Add head or foot html code
Version: 1.0
Author: Kenji Nakagawa
License: none
*/

//start up!
require_once(dirname( __FILE__ ) .'/addhtml-common.php');

if(is_admin())
{
  require_once(dirname( __FILE__ ) .'/addhtml-setting.php');
  AddHtmlCodeSetting::register(plugin_basename(__FILE__));
}
else
{
  require_once(dirname( __FILE__ ) .'/addhtml-doaction.php');
  AddHtmlCode::register();
}

?>

以下、これまで散々関数名のバッティングに悩まされてきたので、片っ端からclass作って、staticメンバ関数に放り込んでます。

■共通変数と関数 (addhtml-common.php)

<?php
class AddHtmlCodeCommon
{
  protected static $options;

  protected static function unescape($str)
    {
      $str = str_replace("\\\"","\"",$str);
      $str = str_replace("\\'","'",$str);

      return $str;
    }
}

?>

■設定管理ページ (addhtml-setting.php)

<?php
class AddHtmlCodeSetting extends AddHtmlCodeCommon
{
  private static $plugin_file;

  public static function register($pfile)
    {
      self::$plugin_file = $pfile;
      // addon check
      if ( !function_exists( 'add_action' ) )
        {
          echo "I'm just a plugin, not much I can do when called directly.";
          exit;
        }

      add_action('admin_menu', __CLASS__.'::option');
      add_filter( 'plugin_action_links', __CLASS__.'::action', 10, 2 );
    }

  public static function action( $links, $file )
    {
      if($file !== self::$plugin_file)
        return $links;
      
      array_unshift( $links, '<a href="options-general.php?page=addhtmlcode">設定</a>');
      return $links;
    }
  
  public static function option()
    {
      add_option('addhtmlcode');
      add_options_page('Add Html Code設定', 'Add Html Code', 10, 'addhtmlcode', __CLASS__.'::options_page');
    }

  public static function options_page()
    {
      // フィールドと設定項目名のための変数
      $opt_name = 'addhtmlcode';
      self::$options = get_option($opt_name);

      // ユーザが何かの情報を投稿したかどうかをチェックする
      // 投稿していれば、このhiddenフィールドの値は'Y'にセットされる
      if($_POST['action'] === 'update')
        {
          // 投稿された値を読む
          self::$options = array();
         
          self::$options['header']  = $_POST['header'];
          self::$options['footer']  = $_POST['footer'];

          // データベースに値を設定する
          update_option( $opt_name, self::$options);

          // 画面に更新されたことを伝えるメッセージを表示
          echo '<div class="updated"><p><strong>設定が保存されました。</strong></p></div>';
        }

      self::$options['header'] = self::unescape(self::$options['header']);
      self::$options['footer'] = self::unescape(self::$options['footer']);

      // 設定変更画面を表示する
?>
<div class="wrap">
<div id="icon-options-general" class="icon32"><br></div>
<h2>Adding HTML Code</h2>
<p>
 ※ヘッダー、フッターに任意のHTMLコードを挿入します。<br>
 このプラグインを適用させるには、テンプレートヘッダ(header.php)・フッター(footer.php)にそれぞれ、
 wp_head(),wp_footer()を記述する必要があります。
</p>
<form name="form1" method="post" action="<?php echo str_replace( '%7E', '~', $_SERVER['REQUEST_URI']); ?>">
<?php wp_nonce_field('update-options'); ?>

<h3 class="text-box-title">ヘッダー&nbsp;<span>&lt;head&gt;タグ内に挿入されます。</span></h3>
<textarea name="header" class="text-box"><?php echo self::$options['header']; ?></textarea>

<h3 class="text-box-title">フッター&nbsp;<span>&lt;/body&gt;直前付近に挿入されます。</span></h3>
<textarea name="footer" class="text-box"><?php echo self::$options['footer']; ?></textarea>

<p class="submit">
<input type="hidden" name="action" value="update">
<input type="hidden" name="page_options" value="header,footer">
<input type="submit" name="Submit" value="設定を更新する">
</p>

</form>
</div>
<style type="text/css"><!--
form { padding: 1em; margin-top: 1em;}
.text-box { display: block;width: 80%; height: 10em; margin-bottom: 3em;padding: 0.5em;}
.text-box-title { margin-bottom: 5px;}
.text-box-title span { font-size: 80%; color: green;}
--></style>
<?php
    }
}

?>

■実際の出力 (addhtml-doaction.php)

<?php
class AddHtmlCode extends AddHtmlCodeCommon
{
  public static function register()
    {
      // addon check
      if ( !function_exists( 'add_action' ) )
        {
          echo "I'm just a plugin, not much I can do when called directly.";
          exit;
        }
      
      self::$options = get_option('addhtmlcode');
      
      add_action('wp_head', __CLASS__.'::add_header');
      add_action('wp_footer', __CLASS__.'::add_footer');
    }

  public static function add_header()
    {
      echo self::unescape(self::$options['header']),"\n";
    }

  public static function add_footer()
    {
      echo self::unescape(self::$options['footer']),"\n";
    }
}

?>

C#でWordPressのエクスポートファイルを取得する

とある業務ソフトのヘルプをWordpressのブログシステム上で作成しよう、ということになってまして、まぁ、フツーはそのWordpressをユーザー認証をかけて、そのまま公開すれば話は済むんですが・・・普通はね・・・。でもクローズドな環境にも対応しなければいけません! とかいう、メンドクサイ仕様になってまして・・・。
それなら、Wordpressなんか使う必要あらへん、ホームページビルダなり、なんなり、使いやすいソフトにすればいいじゃねーの? とフツーはなるんですが・・・。
まぁ、ヘルプ作成について色んな方法を僕がいくら提案しても受け入れてくれないのは分かっているので、グダグダ文句を言っても始まりません。 結局は、ヘルプファイル(HTML)はローカルに置けるように、Wordpressの記事をすべてHTMLに落とすという全くもって本末転倒&意味不明のプログラムを組むことで落ち着きました。

はじめは、そんなの誰かがもうやってるだろう・・・とググったけど、探し方がまずいのか、ヒットしません。ま、そりゃ、そーだよな、わざわざWordpressの記事を全部HTMLに落とすなんてこと・・・する必要もねーし、そんなヘルプの作成の仕方しているの素人集団のウチだけだよな。

ま、グチを言ってても始まりません(笑)

WordPressはXML-RPCをサポートしてますので、XML-RPCで・・・と思ったんですが、Wordpressが吐き出すWXR形式のエクスポートファイル(XML)をダウンロードしてパースした方が早いんじゃねーの? という根拠のない脳内ベンチを信じて、C#で組み始めることに。

エクスポートファイルをパースしてHTMLファイルを一気に吐き出すコードは簡単にできたんですが、エクスポートファイルをWordpressから排出させるところで躓いた。

サーバー上のWordpressへのアクセスに System.Net.WebClient クラスを使ったんですが・・・ログイン処理、具体的にはクッキー処理がうまく行かず、ちょっと時間がかかってしまいました。ネットで検索して出てくる、WebClientを継承して、GetRequestメソッドをオーバーライドしてCookieを保存できるようにカスタマイズしたWebClientを試してみたんですが、どういうわけか、Wordpressへのログインは成功するんだけど、クッキーが保存されないのでログイン画面にリダイレクトされてしまう。
結局、HttpWebRequestの自動リダイレクトを無効/AllowAutoRedirectプロパティをfalseにすることで、解決しました。

/*
  エラー処理・タイムアウト処理はしていない。
*/
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;
using System.Net;
using System.Threading;

///<summary>
/// 自動リダイレクトを無効にするため、WebClientを継承します。
///</summary>
class XmlWebClient : WebClient
{
  protected override WebRequest GetWebRequest(Uri address)
    {
      WebRequest request = base.GetWebRequest(address);
      
      if (request is HttpWebRequest)
        ((HttpWebRequest)request).AllowAutoRedirect = false;
      
      return request;
    }
  
  ///<summary>
  ///エクスポートファイルを新規スレッドでダウンロードする。
  ///非同期APIを使えばいいんですが、ややこしいので、スレッドを一本起こしてます。
  ///</summary>
  public static Thread GetWXRFileAsync(string url,string user,string phrase,string ofilepath,Action cb)
    {
      try
        {
          var thread = new Thread(o =>
                                  {
                                    GetWXRFile(url, user, phrase, ofilepath);
                                    cb();
                                  });
          
          thread.Start();
          return thread;
        }
      catch(Exception exception)
        {
          throw exception;
        }
    }
  
  ///<summary>
  ///エクスポートファイルをダウンロードする。
  ///</summary>
  public static void GetWXRFile(string url,string user,string phrase,string ofilepath)
    {
      var xwc = new XmlWebClient { Encoding = Encoding.UTF8 };
      
      xwc.Headers[HttpRequestHeader.UserAgent] = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0)";
      xwc.Headers[HttpRequestHeader.Referer] = url + "wp-login.php";
      
      var param = new NameValueCollection();
      param.Add("log", user);
      param.Add("pwd", phrase);
      param.Add("rememberme","forever");
      param.Add("wp-submit","login");
      param.Add("redirect_to",url + "wp-admin/");
      param.Add("testcookie","1");
      
      xwc.UploadValues(url + "wp-login.php",param);
      
      string setCookie = xwc.ResponseHeaders[HttpResponseHeader.SetCookie];
      string[] cookies = Regex.Split(setCookie, "(?<!expires=.{3}),")
        .Select(s => s.Split(';').First().Split('='))
          .Select(xs => new { Name = xs.First(), Value = string.Join("=", xs.Skip(1).ToArray()) })
            .Select(a => a.Name + "=" + a.Value)
              .ToArray();
      
      xwc.Headers[HttpRequestHeader.Cookie] = string.Join(";", cookies);
      xwc.DownloadFile(url + "wp-admin/export.php?content=all&download=true", ofilepath);
    }
}

クッキー処理のところは、たまたま検索したページ(サイト失念(m_m))のコピペ。SetCookieヘッダーの処理って面倒なんですよね・・・助かる(^^)
あとは、下記のように WordPressのURLとログイン名・パスワード・ダウンロードするエクスポートファイルを保存するファイルパスを指定してコールするだけ。

//ダウンロード
XmlWebClient.GetWXRFile("ワードプレスのURL","ユーザー名","パスワード","ファイルパス");

//おまけで、コールバック関数を指定して別スレッドで動かす「なんちゃって非同期バージョン」
//Windowsフォームを使うアプリだと、非同期は必須っすね。
XmlWebClient.GetWXRFileAsync("ワードプレスのURL",
                             "ユーザー名",
                             "パスワード",
                             "ファイルパス",
                             () => {Console.WriteLine("エクスポートファイルの取得が終了しました。");});