「wp_insert_postという関数でユーザーに投稿させることができるらしいですが、どうすればいいですか」という質問を受けたのですが、その関数だけでは投稿することができません。質問者さんはおそらくWordPress でゲストに記事投稿させるフォームが作れる wp_insert_post() 関数が便利すぎてぎっくり腰になる件とかを見て「へー、そんなに簡単に作れるんだ」と思ったんでしょうが、この記事の作者さんはCakePHPとかでWebサイトを作ったりしているスキルセットの持ち主なので、「便利」とか「簡単」とか思うポイントがちょっと違うと思います。「SQL書かないで関数一発で保存できるなんて超便利」というぐらいじゃないでしょうか。「何も考えずコピペしたらSNS作れた嬉しい」という意味ではありません。
ぼくが声を大にして言いたいのは、フォームも作らないとダメですよということです。データを保存する機能がわかっても、ユーザーからデータを取得するフォームがなければ投稿なんて作れないでしょう。要はTwitterのあの窓です。
今回は掲示板を例にして、このフォームの作り方から説明しつつ、チュートリアルビデオとソースコードを300円で売りつけるという社会実験へと至る華麗な流れを見せたいと思います。
サマリー
オライリー本などには「この記事の対象読者」という前段が必ずあるので、真似して書いてみます。
この記事の内容
WordPressをブログツールではなく、CMS的に使っている場合、「ユーザーが自由に投稿できたらいいな」と思うことは多々あります。基本的にはそういうことをするプラグインはすでにあります(Post from site)が、あえて自分で投稿する画面を作る方法を紹介します。
この記事の対象読者
WordPressでサイトを制作したことがある方を対象にしています。WordPressのインストール方法やテーマカスタマイズの基本については説明しません。
WordPressというのは単なるブログシステムとして使うこともできますが、Webサービスの土台として活用することもできます。この記事はそのファーストステップです。
目の前にやらなければいけないことがあって、とりあえずそれを解消したいという方にとっては遠回りな方法ですが、実現したいWebサービスがあって、それはいま自分にはできそうもないのでそのとっかかりを掴みたいという方を対象にしています。
この記事で解決できること
WordPressは多くの人に使われているツールですが、その管理画面を一目見て使いこなすことはまだ難しいです。Twitterのようにシンプルな投稿画面にすると多くのコンテンツが集まるというのはわかるでしょう。
そういえば、Greeの田中社長は楽天勤務時代、先輩に「おまえはコメントすら書けないユーザーが世の中の大多数だということをわかってない」と言われて衝撃を受けたそうです。
それでは、説明します。
ステップ1. 仕様の確認
コードを書く前にまず仕様を確認しましょう。
- 登録しているユーザーは誰でも投稿できる
- 投稿できるのはカスタム投稿タイプのthreadsである
- 投稿を行うのは固定ページの「スレッドを作成する(create-thread)」である
- 投稿する内容は投稿タイトル、投稿本文、カテゴリー、参考URLである
- 参考URLはカスタムフィールドで実装する
イメージとしては、はてな人力検索のようなコンテンツです。
ステップ2. カスタム投稿タイプの作成
まずはカスタム投稿タイプを作成しましょう。名前はthreadsです。これに関してはCustom Post UIなどのプラグインを利用してください。
ステップ3. 固定ページのテンプレートを作る
固定ページで作成するテンプレートを作ります。名前はpage-create-thread.phpなどにして、中身をこんな感じにします。
<?php /** * Template Name: スレッド投稿フォーム */ get_header(); ?> これは投稿フォームです <?php get_footer(); ?>
ステップ4. 投稿用の固定ページを作る
投稿用の固定ページを作成します。タイトルは「スレッドを作成する」で、スラッグはcreate-threadにします。本文はなにも書かなくて構いません。
ページテンプレートは先ほど作成した「スレッド投稿フォーム」にしてください。ページを表示し、「これは投稿フォームです」と表示されることを確認してください。
ステップ5. フォームを作る
先ほど作成したpage-create-thread.phpにフォームを書きます。URLはテキストフィールドで、カテゴリーはドロップダウンで表示します。
<?php /** * Template Name: スレッド投稿フォーム */ get_header(); ?> <form action="<?php the_permalink(); ?> " method="post"> <table class="form-table"> <tbody> <tr> <th><label for="title">タイトル</label></th> <td><input id="title" type="text" name="title" value=""/></td> </tr> <tr><span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span> <th><label for="content">本文</label></th> <td><textarea id="content" name="content"></textarea></td> </tr> <tr> <th><label>カテゴリー</label></th> <td><?php wp_dropdown_categories(); ?></td> </tr> <tr> <th><label for="url">参考URL</label></th> <td><input id="url" type="text" name="url" value=""/></td> </tr> </tbody> </table> <input type="submit" value="投稿する"/> </form> <?php get_footer(); ?>
このフォームは同じURLに戻るフォームです。これだけだと何も起きません。
ステップ6. フォームの投稿内容を確認する
フォームができたら、そのデータを受け取る場所を作ります。テーマフォルダにfunctionsというフォルダを作成し、その中にcreate-thread.phpというファイルを作成してください。これをfunctions.phpから読み込みます。
//投稿用ファイルを読み込む get_template_part('functions/create-thread');
読み込んだら、create-thread.phpにこんな感じでフックを書きます。
/** * テンプレートが読み込まれる直前で実行される */ function _my_create_thread(){ //POSTデータを表示して終了 var_dump($_POST); die(); } add_action('template_redirect', '_my_create_thread');
template_redirectというアクションフックは、ページが読み込まれてテンプレートを取得する直前に実行されるフックです。ここで$_POST(フォームが投げるデータ)を表示しています。
これだけだとすべてのページが表示されなくなってしまうので、if文で囲みます。
/** * テンプレートが読み込まれる直前で実行される */ function _my_create_thread(){ if(is_page('create-thread') && !empty($_POST)){ //POSTデータを表示して終了 var_dump($_POST); die(); } } add_action('template_redirect', '_my_create_thread');
どうでしょう、「スレッド投稿フォーム」でポストしたときだけvar_dumpが実行されているのがわかりましたでしょうか。
ステップ7. nonceを設定する
こうしたフォームというのはPOSTメソッドを使っているだけなので、よそのページからフォームを飛ばしても受け付けてしまいます。「スレッド投稿フォーム」以外からのPOSTを受け付けてしまうと、予期せぬエラーが発生しますし、CSRF脆弱性を生むことになります。
これを防ぐために、WordPressではnonceという仕組みを採用しています。24時間限り有効なトークンを発行して、その有効性を確かめることで特定のフォームから来たであろうことを保証します。
このための関数はwp_verify_nonceです。先ほどのフォーム内に書きましょう。
<?php /** * Template Name: スレッド投稿フォーム */ get_header(); ?> <form action="<?php the_permalink();?>" method="post"> <?php wp_nonce_field('create_thread'); ?>
これでフォームを見ると、2つのinput[type=hidden]ができていることがわかります。_wpnonceというinputタグにある値がnonceですね。16進数のわけのわからない値になっていると思います。
先ほど作ったtemplate_redirectフックにおいて、必ずこのnonceをチェックし、合っている場合だけ処理を進めます。
function _my_create_thread(){ if(is_page('create-thread') && isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'create_thread')){ //POSTデータを表示して終了 var_dump($_POST); die(); } }
動くかどうか確かめてください。OKですね。
ステップ8. ログインしているユーザーにのみフォームを表示
ログインしていないユーザーにはそもそもフォームを表示せず、「ログインしてください」というリンクをだしておく必要があります。このためフォーム全体をif文でくくります。
<?php /** * Template Name: スレッド投稿フォーム */ get_header(); ?> <?php if(is_user_logged_in()): /*ログイン確認*/ ?> <form action="<?php the_permalink();?>" method="post"> <!-- 中略 --> </form> <?php else: ?> <a href="<?php echo wp_login_url(get_permalink()); ?>">ログイン</a>してください。 <?php endif; ?> <?php get_footer(); ?>
フォームだけではなく、POSTをうけつける部分もログイン判別を条件として追加しておきます。
function _my_create_thread(){ if( is_page('create-thread') && is_user_logged_in() && isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'create_thread') ){ //POSTデータを表示して終了 var_dump($_POST); die(); } }
ステップ9. データを保存する
データの保存自体はwp_insert_postで実現できます。配列にpost_titleなどの形で渡すことで保存できます。2番目の引数にtrueを渡すと、失敗した場合にWP_Errorが返ってきますので、エラーメッセージも利用できます。注意点としては、カテゴリーは必ず配列で渡し、個々の値は整数にキャストすることです。整数以外は受け付けないというのはPHPだと珍しい仕様ですね。
//_my_create_threadのif内 $id = wp_insert_post(array( 'post_title' => (string)$_POST['title'], 'post_content' => (string)$_POST['content'], 'post_status' => 'publish', 'post_author' => get_current_user_id(), 'post_type' => 'threads', 'post_category' => array(intval($_POST['cat'])) ), true); //データの挿入に成功していたら移動 if(!is_wp_error($id)){ //カスタムフィールドurl_rel(参考URL)を追加 update_post_meta($id, 'url_ref', $_POST['url']); //ページを移動 header('Location: '.get_permalink($id)); die(); }
完成したら、動くかどうか確認してみましょう。こうした「誰でも投稿できるフォーム」でHTMLをうけつけることは少ない(Githubぐらい?)ですが、今のところはノーチェックで保存します。出力するときにhtmlspecialcharsなどでエスケープすることにしてください。
ステップ10. エラーメッセージを表示
これまでは上手くいった場合だけを想定していますが、タイトル空っぽの投稿が入ったりしたら困りますね。なので、バリデーションを入れ、ダメだった場合はメッセージを表示します。メッセージ格納用のグローバル変数を用意して、エラーが発生するたびにメッセージを追加します。
//create-thread.phpの上の方に書く global $create_thread_error; $create_thread_error = array();
それでは、実際にバリデーションを行ってみましょう。
function _my_create_thread(){ global $create_thread_error; if( is_page('create-thread') && is_user_logged_in() && isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'create_thread') ){ //ここまできたらnonceはOK if(!isset($_POST['title']) || empty($_POST['title'])){ $create_thread_error[] = 'タイトルが空白です'; } if(!isset($_POST['content']) || empty($_POST['content'])){ $create_thread_error[] = '本文が空です'; } if(!isset($_POST['url']) || empty($_POST['url']) || !preg_match('/^(https?)(:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#]+)$/', $_POST['url'])){ $create_thread_error[] = '参考URLは必ず入力してください'; } //エラーが無かったら投稿を追加 if(empty($create_thread_error)){ $id = wp_insert_post(array( 'post_title' => (string)$_POST['title'], 'post_content' => (string)$_POST['content'], 'post_status' => 'publish', 'post_author' => get_current_user_id(), 'post_type' => 'threads', 'post_category' => array(intval($_POST['cat'])) ), true); //データの挿入に成功していたら移動 if(!is_wp_error($id)){ //カスタムフィールド追加 update_post_meta($id, 'url_ref', $_POST['url']); //ページを移動 header('Location: '.get_permalink($id)); die(); }else{ $create_thread_error[] = 'エラーが発生しました'.$id->get_error_message(); } } } }
あとはエラーメッセージを表示する関数をcreate-thread.php内で定義します。
/** * スレッド作成画面でエラーがあれば表示 * @global array $create_thread_error */ function show_thread_error(){ global $create_thread_error; if(!empty($create_thread_error)){ echo ' <div id="error">'; echo implode(' ', $create_thread_error); echo '</div> '; } }
フォームの上の方に、エラーメッセージを表示しましょう。
<?php /** * Template Name: スレッド投稿フォーム */ get_header(); ?> <?php if(is_user_logged_in()): /*ログイン確認*/ ?> <?php show_thread_error(); ?> <form action="<?php the_permalink();?>" method="post">
これでエラーがある場合はリダイレクトされず、エラーメッセージを表示するようになります。おめでとうございます。
応用編はこんな風にしてはいかが?
これまでのチュートリアルで、「フォームを用意し、そこから取得したデータをWordPressに保存する」ということはできたと思うので、こんな関数を使って色々やってみてください。
- 匿名ユーザーを作成して、ゲストには匿名ユーザーとして投稿させる。recaptchaなどと組み合せれば、ボットからの襲撃をうけても安心!
- update_user_metaやdelete_user_metaなどを使って、ユーザープロフィール編集画面をカスタマイズ
他にも色々あります。
おまけ:知っておきたいセキュリティ
今回の通りに作ると<script>タグを埋め込まれてXSS脆弱性を生んでしまう可能性があるので、セキュリティに関しては色々と勉強してみてください。以下の本がよくまとまっています。
[429] [429] Client error: `POST https://webservices.amazon.co.jp/paapi5/getitems` resulted in a `429 Too Many Requests` response: {"__type":"com.amazon.paapi5#TooManyRequestsException","Errors":[{"Code":"TooManyRequests","Message":"The request was de (truncated...)
ワンモアシング
このチュートリアルで実際に稼働させながらコードを書いている様子を収めたビデオ(1時間)とソースコード一式を300円で販売中です。記事を読んでも理解できなかったという方は購入してみてください。サンプルビデオも上げておきます。