XML-RPCCLib Archive

DEC
5

XMLRPCでeval

Published:2008-12-05 21:24:24 UTC

今回もWordPressのXML-RPCについて。但し、今回はサーバーサイドのお話。XML-RPCを使うと、WordPressのエントリやコメントなどを外部のプログラムから弄ることが出来てなかなか便利ですが、アクセスできるのはあくまでもXML-RPCのメソッドとして公開されている部分だけです。自分でメソッドを追加できたらなー。寧ろ任意のPHPスクリプトを実行できたら便利じゃね?ということで、引数として渡したPHPコードをeval関数で評価した結果を返すXML-RPCサーバーをxmlrpc.phpを改変して作ってみました。思いつきで試しに書いてみただけなので、プラグインとして纏まっていないのはご愛嬌。customrpc.phpとでも名前をつけて、xmlrpc.phpと同じディレクトリに配置すると動きます。

<?php
/**
 * Custom RPC Server
 * created by shiroica
 * @license GPL v2 <./license.txt>
 * Most of this code is based on "xmlrpc.php" file of WordPress.
 */

/**
 * Whether this is a XMLRPC Request
 *
 * @var bool
 */
define('XMLRPC_REQUEST', true);

// Some browser-embedded clients send cookies. We don't want them.
$_COOKIE = array();

// A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
// but we can do it ourself.
if ( !isset( $HTTP_RAW_POST_DATA ) ) {
	$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
}

// fix for mozBlog and other cases where '<?xml' isn't on the very first line
if ( isset($HTTP_RAW_POST_DATA) )
	$HTTP_RAW_POST_DATA = trim($HTTP_RAW_POST_DATA);

/** Include the bootstrap for setting up WordPress environment */
include('./wp-load.php');

include_once(ABSPATH . 'wp-admin/includes/admin.php');
include_once(ABSPATH . WPINC . '/class-IXR.php');

// Turn off all warnings and errors.
// error_reporting(0);

/**
 * Posts submitted via the xmlrpc interface get that title
 * @name post_default_title
 * @var string
 */
$post_default_title = "";

/**
 * Whether to enable XMLRPC Logging.
 *
 * @name xmlrpc_logging
 * @var int|bool
 */
$xmlrpc_logging = 0;

/**
 * logIO() - Writes logging info to a file.
 *
 * @uses $xmlrpc_logging
 * @package WordPress
 * @subpackage Logging
 *
 * @param string $io Whether input or output
 * @param string $msg Information describing logging reason.
 * @return bool Always return true
 */
function logIO($io,$msg) {
	global $xmlrpc_logging;
	if ($xmlrpc_logging) {
		$fp = fopen("../xmlrpc.log","a+");
		$date = gmdate("Y-m-d H:i:s ");
		$iot = ($io == "I") ? " Input: " : " Output: ";
		fwrite($fp, "nn".$date.$iot.$msg);
		fclose($fp);
	}
	return true;
}

if ( isset($HTTP_RAW_POST_DATA) )
	logIO("I", $HTTP_RAW_POST_DATA);

/**
 * error2ExceptionConverter - handle an error and throw it as an exception.
 */
function error2ExceptionConverter($errno, $errstr, $errfile, $errline) {
	throw new Exception($errstr, $errno);
}



/**
 * XMLRPC server implementation that enables users to evaluate passed PHP code.
 */
class custom_rpc_server extends IXR_Server {

	/**
	 * Register all of the XMLRPC methods that XMLRPC server understands.
	 *
	 * PHP4 constructor and sets up server and method property. Passes XMLRPC
	 * methods through the 'xmlrpc_methods' filter to allow plugins to extend
	 * or replace XMLRPC methods.
	 *
	 * @since 1.5.0
	 *
	 * @return wp_xmlrpc_server
	 */
	function custom_rpc_server() {
		$this->methods = array(
			'eval' => 'this:callEval'
		);
		$this->methods = apply_filters('xmlrpc_methods', $this->methods);
		$this->IXR_Server($this->methods);
	}

	/**
	 * Evaluate passed PHP code by "eval()" function.
	 * @param array $args Method Parameters.
	 * @return depends "eval()" function returned.
	 */
	function callEval($args) {

		$user_login = $this->escape($args[0]);
		$user_pass  = $this->escape($args[1]);
		$code_str   = $args[2];

		set_current_user( 0, $user_login );
		if( !current_user_can( 'edit_posts' ) )
			return new IXR_Error( 401, __( 'Sorry, you do not have access to user data on this blog.' ) );

		if (!$this->login_pass_ok($user_login, $user_pass)) {
			return $this->error;
		}
		set_error_handler('error2ExceptionConverter');
		try{
			@$result = eval($code_str);
		}
		catch(Exception $exception){
			restore_error_handler();
			return new IXR_Error( 500, __( 'Runtime error was occurred while executing your code.' ) );					
		}
		restore_error_handler();
		if($result === false){
			return new IXR_Error( 500, __( 'Parse error was occurred while executing your code.' ) );					
		}
		return $result;
	}

	/**
	 * Check user's credentials.
	 *
	 * @since 1.5.0
	 *
	 * @param string $user_login User's username.
	 * @param string $user_pass User's password.
	 * @return bool Whether authentication passed.
	 */
	function login_pass_ok($user_login, $user_pass) {
		if ( !get_option( 'enable_xmlrpc' ) ) {
			$this->error = new IXR_Error( 405, sprintf( __( 'XML-RPC services are disabled on this blog.  An admin user can enable them at %s'),  admin_url('options-writing.php') ) );
			return false;
		}

		if (!user_pass_ok($user_login, $user_pass)) {
			$this->error = new IXR_Error(403, __('Bad login/pass combination.'));
			return false;
		}
		return true;
	}

	/**
	 * Sanitize string or array of strings for database.
	 *
	 * @since 1.5.2
	 *
	 * @param string|array $array Sanitize single string or array of strings.
	 * @return string|array Type matches $array and sanitized for the database.
	 */
	function escape(&$array) {
		global $wpdb;

		if(!is_array($array)) {
			return($wpdb->escape($array));
		}
		else {
			foreach ( (array) $array as $k => $v ) {
				if (is_array($v)) {
					$this->escape($array[$k]);
				} else if (is_object($v)) {
					//skip
				} else {
					$array[$k] = $wpdb->escape($v);
				}
			}
		}
	}
}

$custom_rpc_server = new custom_rpc_server();

?>

ユーザー名、パスワード、評価させたいPHPコードを引数として渡すと、評価された結果が帰ってきます。戻り値はThe Incutio XML-RPC Library for PHPでパースできるデータ型で構成されていないとまずいかも。自分でも全然使っていないからよく分からない(笑)。あと、eval関数の都合上、戻り値がfalseだと実行時エラーと区別がつかないので、戻り値はbool型はやめましょう。

OCT
26

image

WordPressの開発版、2.7-almost-beta-9300をXAMPP上に入れてみました。メニューの配置が変わったりと、UIに結構手が入れられたのが2.7一番のポイントですが、それで物凄く使いやすくなったかというと、別にそうでもないというのが自分の受けた印象。まぁ、長く使っていくうちにじわじわと道具としての良さが分かってくるのかもしれないですが。

さて、WP2.7の重要な新機能の一つである、Comment APIをちょっと調べてみました。
2.7からはコメントをXMLRPCを通じて編集出来るようになりました。この機能をWPではComment APIとして呼ぶのですが、具体的には以下に引用した通りのメソッドが実装されています。

http://trac.wordpress.org/ticket/7446

Latest patch:

http://trac.wordpress.org/attachment/ticket/7446/7446.9.diff

The following methods are implemented:

wp.getComment(blog_id, username, password, comment_id)

wp.getComments(blog_id, username, password, {status, post_id, number, offset}

wp.deleteComment(blog_id, username, password, comment_id)

wp.editComment(blog_id, username, password, comment_id, {status,
date_created_gmt, content, author, author_url, author_email, })

wp.newComment(blog_id, username, password, post, {content, author,
author_email, author_url})
// author info is optional if authorization is successful.
Unregistered commenting is allowed if a plugin sets the
xmlrpc_allow_anonymous_comments filter to true. Default is to not
allow unregistered comments. User must auth.

wp.getCommentStatusList(blog_id, username, password)

[wp-xmlrpc] Comments API

で、これらの中で注意が必要なのが、wp.newCommentメソッド。引用文中に

author info is optional if authorization is successful.

というコメントが入っていますが、認証に通っている場合、実はauthorやauthor_emailに指定した値は無視され、使用されません。WPのユーザーアカウント情報の対応する値が使用されることに注意しましょう。以上のことは、xmlrpc.phpの1275行目で確認できます。

		if ( $logged_in ) {
			$user = wp_get_current_user();
			$comment['comment_author'] = $wpdb->escape( $user->display_name );
			$comment['comment_author_email'] = $wpdb->escape( $user->user_email );
			$comment['comment_author_url'] = $wpdb->escape( $user->user_url );
			$comment['user_ID'] = $user->ID;
		} else {

では認証を通さなければ、一般ビジターとして好きなメルアドやURLを指定してコメントをつけられるかというと、そうでもなくて、今度は

Unregistered commenting is allowed if a plugin sets the xmlrpc_allow_anonymous_comments filter to true.

という問題に引っかかります。ままならないものですね…。

あと気になった点といえば、同一内容のコメントを連投しようとした場合、Response Code 500で撥ねられるという点。XMLRPC経由なのだから、XMLRPC Faultでエラーを通知するべきだと思うのですが…。

image 折角なので昨日のWordPressにXML-RPC経由でサイズの大きなファイルを送信する際にパースエラーが発生した場合の対処法 – SharpLab.というエントリの現象を確認するためのデモンストレーション用コードを公開しておきます。

WordPressXmlrpcErrorDemonstration.zip

実行に先立っては、WordPressのログイン情報等を適宜書き加える必要があります。

namespace WordPressXmlrpcErrorDemonstration {
	public partial class Form1 : Form {

		private const string _hostName = "";//テスト用WordPressのXMLRPCエンドポイントURLを指定のこと
		private const string _userAgent = "WordPressXmlrpcErrorDemonstration";
		private const string USERNAME = "";//テスト用WordPressのユーザー名を指定のこと
		private const string PASSWORD = "";//テスト用WordPressのパスワードを指定のこと

また、XMLRPC応答は35行目のresponseという変数に収められます。

XmlRpcMethodResponse response = client.CallMethod("wp.uploadFile", addXMLDeclaration,
    new XmlRpcInt(0),
    new XmlRpcString(USERNAME),
    new XmlRpcString(PASSWORD),
    new XmlRpcStruct(new XmlRpcStructMember[]{
        new XmlRpcStructMember("name",new XmlRpcString(file.Name)),
        new XmlRpcStructMember("type",new XmlRpcString("")),
        new XmlRpcStructMember("bits",new XmlRpcBase64(bytes)),
        new XmlRpcStructMember("overwrite",new XmlRpcBoolean(false))
    }));

ところでこのプロジェクトではCompact Framework向けにビルドしたライブラリを特に弄ることなく参照しているのですが、問題ないのですかね?とりあえず普通に動いているようなのですが…。.Net Compact Frameworkと.Net Frameworkの関係がいまいち分からない。

最近Compact Framework用XML-RPCクライアントライブラリを書いているのですが、WordPress XML-RPC APIのwp.uploadFileメソッドを使ってファイルを送信しようとした場合、パースエラーが発生する現象に悩まされました。ファイルが小さい場合は問題がないのですが、ファイルのサイズが大きくなるとパースエラーを返すという現象です。

調べてみたところ、WordPressのXML-RPC関係の処理を担っているIncutio XML-RPC Library for PHPがどうやら問題の原因のようで、class-IXR.php(wp-includes以下にあります)で定義されているIXR_Messageクラスのparseメソッドのところでエラーが返されているようです。

function parse() {
    // first remove the XML declaration
    $this->message = preg_replace('/<?xml(.*)??'.'>/', '', $this->message);
    if (trim($this->message) == '') {
        return false;
    }
//以下省略

どうもエラーが起きる場合は157行目を境に$this->messageの内容が空になっているようで、それが原因で159行目でreturn falseされてしまっているようです。なぜファイルが大きい場合だけそのようなことになるのかはちょっとよく分からないのですが(不適切な正規表現なのでしょうか?)、とりあえずPOSTするXMLからXML宣言を削除し、methodCall要素以下だけのXMLフラグメントとして送信するようにしたところ上手くいくようになりました。参考まで。

追記:

WP-XMLRPCのパースエラーのデモンストレーション用コード – SharpLab.を書きました。