feed one WordPress site with post from another WP using xmlrpc protocol

To begin…
This post is published to relate recent experiments with xmlrpc protocol now fully available in WordPress (version >3.1) source code (server and client like desktop clients listed in codex).
Researchs on other tests on web were not fully successful because some posts are incomplete or too old according recent adds in WP code and specially about processes to sent media and attach it to remote post.

The context

One master (local) WP site and a target (remote) WP site where to send a post (or a part of this) when published locally.

Technical context

  • WordPress 3.1.3 on both sites and error.log available on both servers…
  • One website named local containing a plugin working as a xmlrpc client : the purpose of this article.
  • Another target site named remote accepting xmlrpc input (and login/pass with editing capabilities).
  • TIPS : to test use the error_log ( ‘message’) php function (and not echo or print_r) !

The testing process

We will create a small plugin without admin UI. When a local post is published, a remote post is created through xmlrpc with his attachment copied to target site. On local post a custom field is added and contains the ID of the post on the remote site.

This process is simple and can fully improved with, by example, cron tasks with sub-selection.

By traveling inside the source code, you will discover some tips and tricks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
/*
Plugin Name: xili-xmlrpc
Plugin URI: http://dev.xiligroup.com/
Description: xili-xmlrpc is a tool to post from one site to another.
Author: dev.xiligroup.com - MS
Version: 0.5.0
Author URI: http://dev.xiligroup.com
*/
 
 
define('XILIXMLRPC_VER','0.5.0'); /* used in admin UI */
 
include_once(ABSPATH . WPINC . '/class-IXR.php'); /* not included in wp-settings */
include_once(ABSPATH . WPINC . '/class-wp-http-ixr-client.php'); /* not included in wp-settings */
 
/**
 * Class without admin UI
 *
 */
class xili_xmlrpc {
 
	var $test_url = 'http://subdomain.domain.tld/xmlrpc.php';
	var $remote_login = 'adlog';
	var $remote_passwd = 'thepasswd';
	var $remote_server = 'remoteslug'; // shortname used in custom field
 
	var $currentrequest = null;
	var $currentdate = true; // option to update target/remote date with now !
 
	/**
	 * hook when post is published - minimum for test
	 *
	 */
	function xili_xmlrpc () {
		add_action( 'publish_post', array( &$this,'send_post_to') );
	}

The start of the source code is easy readable.
The first function is the hook when a post is published and now the function fired :

40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
	/**
	 * function fired when a post is published
	 *
	 * @param post ID
	 *
	 */
	function send_post_to ( $post_id ) {
 
		$to_post_id = get_post_meta ( $post_id , $this->remote_server, true );
 
		$this -> create_remote_categories_if ( $post_id );
 
		if ( $to_post_id > 0 ) { // mise à jour
			$remote_id = $this->edit_post_to ( $post_id ); /* not used now */
 
		} else {
			$guids = $this->send_attachments ( $post_id );
			$remote_id = $this->new_post_to ( $post_id ) ;
			if ( $remote_id ) {
			 		update_post_meta ( $post_id , $this->remote_server, $this->currentrequest ) ;
			}
			// update content after changing attachment url
			if ( $guids ) {	
				//error_log ( '- array -');
				$remote_id = $this->edit_post_to ( $post_id, $guids ); // create attachments link to parent in remote
			}
			// get attached medias
			$medias = $this->get_attached_medias ( $post_id , 'image') ;
			$remote_images = array();
			if ( $medias ) { //print_r( $medias );
				// create remote links of medias images
				foreach ( $medias as $oneimage ) {
					$remote_images[] = array ( 
					'link' => $oneimage['link'], 
					'file' => $oneimage['metadata']['file'], 
					'thumbnail' => array ( 'link' => $oneimage['thumbnail'], 
											'file' => $oneimage['metadata']['sizes']['thumbnail']['file'] ), 
					'medium' => array ('file' => $oneimage['metadata']['sizes']['medium']['file'] ), 
					'post-thumbnail' => array('file' => $oneimage['metadata']['sizes']['post-thumbnail']['file'] )   
					);
				}
				// synchronize 
				$search = array(); //local remaining link
				$replace = array();
				foreach ( $guids as $one_id => $one_attachment ) {
 
					foreach ( $remote_images as $oneimage ) {
						if ( $one_attachment['remote'] == $oneimage['link'] ) { //error_log ( '----> '.$oneimage['link'] );
							$remote_folder = str_replace ($oneimage['thumbnail']['file'], "", $oneimage['thumbnail']['link'] );
 
							$replace[] = $remote_folder.$oneimage['medium']['file']; //error_log ( '---med replace-> '.$remote_folder.$oneimage['medium']['file'] );
							$src_m = wp_get_attachment_image_src($one_id, 'medium');
							$search[] = $src_m[0]; // error_log ( '---med search-> '.$src_m[0] );
							$replace[] = $oneimage['thumbnail']['link'];
							$src = wp_get_attachment_image_src($one_id, 'thumbnail');  
							$search[] = $src[0];
						}
					}	
				}
				if ( $replace ) {	
				    // error_log ( '- array replace -');
					$remote_id = $this->edit_post_to ( $post_id, $guids, array ('search' => $search, 'replace' => $replace) );
				}
			}
 
		}
		// send the attachments
 
	}

In this main function, the step by step tips when creating a new remote one is :
Send the attachment BEFORE the new post containing link in content to attachment. (if the content don’t contain media links, attachments (child to parent) are impossible.
Create the new post,
Save the remote ID inside custom field of the local post,
Attach medias links in the content of the remote post.
After doing a fitting table of links inside contents (local and remote): updating remote content of post.

Other functions used by the main presented above :

A new post is created on remote with datas from local one (the links (src or…) inside content are not yet updated and link to local wp site:

140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
	/**
	 * Create a new remote post from a local one
	 *
	 * @param local post ID
	 *
	 */
	function new_post_to ( $post_id ) {
 
		$rpc = new WP_HTTP_IXR_Client( $this->test_url );
		$original_post = & get_post ($id = $post_id);
 
		$post = array(
		    'title' => $original_post->post_title,
		    'categories' => wp_get_object_terms($post_id, 'category', array('fields'=>'names') ),
		    'mt_keywords' => wp_get_object_terms($post_id, 'post_tag', array('fields'=>'names') ),
		    'description' => $original_post->post_content,
		    'wp_slug' => $original_post->post_name
		);
 
		$params = array(
		    0,
		    $this->remote_login,
		    $this->remote_passwd,
		    $post,
		    'publish'
		);
 
		$status = $rpc->query(
			'metaWeblog.newPost',
			$params	
		);
 
		$this->currentrequest = $rpc->getResponse(); 
		return $status; // ID or false
	}

Edit and update the remote post according optional 2nd and 3rd parameters:

176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
/**
	 * edit the local post clone content before to edit the remote one
	 *
	 * @param local ID
	 * @param guids of local images or attachments
	 * @param array of local - remote src or url inside content to proceed search and replace
	 */
	function edit_post_to ( $post_id , $guids = array(), $search_replace = array() ) {
 
		$rpc = new WP_HTTP_IXR_Client( $this->test_url );
		$original_post = & get_post ($id = $post_id);
 
		if ( $guids ) {
			$post_content = $original_post->post_content ;
			foreach ( $guids as $one_id => $one ) {
				$post_content = str_replace ( $one['local'], $one['remote'], $post_content );
			}
			if ( isset( $search_replace ['search']) ) 
				$post_content = str_replace ($search_replace ['search'], $search_replace ['replace'], $post_content);
 
		} else {
			$post_content = $original_post->post_content ;
		}
 
		$post = array(
		    'title' => $original_post->post_title,
		    'categories' => wp_get_object_terms($post_id, 'category', array('fields'=>'names') ),
		    'mt_keywords' => wp_get_object_terms($post_id, 'post_tag', array('fields'=>'names') ),
		    'description' => $post_content,
		    'wp_slug' => $original_post->post_name
		);
 
		if ( $this->currentdate ) {
			$newDate = new IXR_Date(strtotime('now'));
			$post['dateCreated'] =  $newDate;
		}
 
		$to_post_id = get_post_meta ( $post_id , $this->remote_server, true );
 
		if ( $to_post_id > 0 ) {
			$params = array(
		    	$to_post_id,
		    	$this->remote_login,
		    	$this->remote_passwd,
		    	$post,
		    	'publish'
			);
 
			$status = $rpc->query(
				'metaWeblog.editPost',
				$params	
			);
 
		/*
		if(!$status) {
		    echo 'Error [' . $rpc->getErrorCode() . ']: ' . $rpc->getErrorMessage();
		    exit();
		}
		*/
			$request = $rpc->getResponse(); 
 
			return $status; // ID or false
 
		} else {
 
			return false ;
		}
	}

This function must be launched BEFORE a new post containing attachment is created.
Why ?
Because the xmlrpc server process analyze the content of the post (links inside) and yet don’t use the parent/child way.

245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/** 
	 * create a remote attachment
	 * use metaWeblog.newMediaObject
	 */
	function send_attachments ( $post_id ) {
 
		// get_attachments
		$attachments = & get_children( array(
			'post_parent' => $post_id,
    		'post_type'   => 'attachment',
			) 
		);
		if ( $attachments != array() ) {
 
			$rpc = new WP_HTTP_IXR_Client( $this->test_url );
			$to_post_id = get_post_meta ( $post_id , $this->remote_server, true );
			// 
			$guids = array();
			foreach ( $attachments as $attachment_id => $attachment ) {
 
 
 
				$params = array(
						0,
				    	$this->remote_login,
				    	$this->remote_passwd,
				    	array( 
				    		'name' => basename( get_attached_file( $attachment_id ) ), //$attachment->post_title,
							'type' => $attachment->post_mime_type,
							'bits' => new IXR_Base64 ( file_get_contents ( get_attached_file( $attachment_id ) ) )  // get_attached_file( $attachment_id)
						)
					);
 
				//error_log ('***'.$params[3]['bits']->getXml() );
				$status = $rpc->query(
						'metaWeblog.newMediaObject',
						$params	
					);
				if(!$status) {
		    			error_log ( 'Error [' . $rpc->getErrorCode() . ']: ' . $rpc->getErrorMessage() );
					}	
				if ( $status ) {
 
					$request = $rpc->getResponse(); //error_log( print_r( $request ) );
					$guids[$attachment_id] = array( 'local' => $attachment->guid, 'remote' => $request['url'] );
						error_log ( $request['url'] );
 
 
					/*
 
					*/
				}
 
			}
 
			return $guids;
		}	
 
		return array();	
	}

Used to collect remote infos about attachments sent to target WP site.

	/**
	 * get attached medias
	 *
	 *
	 *
	 */
	function get_attached_medias ( $post_id = 0, $mime_type = '' ) {
 
		$rpc = new WP_HTTP_IXR_Client( $this->test_url );
		$remote_post_id = get_post_meta ( $post_id , $this->remote_server, true );
 
		// get recent
		$params = array(
			0,
	    	$this->remote_login,
	    	$this->remote_passwd,
	    	array(
	    	'mime_type' => $mime_type,
	    	'parent_id' => $remote_post_id
		)
		);
 
		$status = $rpc->query(
			'wp.getMediaLibrary',
			$params	
		);
		if ( $status ) {
			$requestattach = $rpc->getResponse(); //print_r($requestattach);
 
			return $requestattach ;
			//$guids[$attachment_id] = array( 'local' => $attachment->guid, 'remote' => $requestattach[0]['link'] );
			error_log ( $requestattach[0]['link'] );
		}
		if(!$status) {
   			error_log ( 'Error [' . $rpc->getErrorCode() . ']: ' . $rpc->getErrorMessage() );
		}
		return array();
	}

Before ending the class of the plugin, a function that create categories on remote site if they not exists. Here in the example, we copy without choice the categories. but on a final release or for the special way of publishing, it is forecast to sub-select or to use a translation table.

345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
	/** 
	 * If remote category don't exist, create it
	 *
	 */
	function create_remote_categories_if ( $post_id ) {
 
		$thecats = wp_get_object_terms($post_id, 'category', array('fields'=>'names') ) ;
 
		$rpc = new WP_HTTP_IXR_Client( $this->test_url );
 
		$params = array(
				0,
		    	$this->remote_login,
		    	$this->remote_passwd
			);
 
		$status = $rpc->query(
				'metaWeblog.getCategories',
				$params	
			);
		if ( $status ) {
 
			$request = $rpc->getResponse();
 
			//print_r( $request );
 
			$remotecatsnames = array();
			foreach ( $request as $remotecategory ) {
 
				$remotecatsnames[] =  $remotecategory ['categoryName'] ;
			}
 
			foreach ( $thecats as $onecatname ) { // error_log ( $onecatname ) ;
 
				if ( !in_array ( $onecatname,  $remotecatsnames ) ) { //error_log ( $onecatname ) ;
					$params = array(
						0,
		    			$this->remote_login,
		    			$this->remote_passwd,
		    			array("name" => $onecatname)
					);
 
					$status = $rpc->query(
						'wp.newCategory',
						$params	
					);
					if(!$status) {
		    			error_log ( 'Error [' . $rpc->getErrorCode() . ']: ' . $rpc->getErrorMessage() );
					}
					if ( $status )  error_log ( $onecatname ) ;
 
				}
			}
		}
	}
 
} // end class
 
/**
 * plugin class instantiation
 *
 */
$xili_xmlrpc = new xili_xmlrpc ();

When the plugin is activated, and when a post is published in the local WP containing the plugin with right parameters, a post is created on the remote WP.

And for annexe, a function to get a remote post not yet used because, in the test, we only create a new post but don’t modify it after the first creating process including attachements (if exists).

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
	/**
	 * get_post from remote with his remote ID
	 *
	 */
	function get_post_from ( $post_id ) { // 
 
		$rpc = new WP_HTTP_IXR_Client( $this->test_url );
 
		$params = array(
		    $post_id,
		    $this->remote_login,
		    $this->remote_passwd,
		);
 
		$status = $rpc->query(
			'metaWeblog.getPost',
			$params		
		);
 
		if(!$status) {
		    //echo 'Error [' . $rpc->getErrorCode() . ']: ' . $rpc->getErrorMessage();
		    //exit();
		    return false;
		} else {
			$request = $rpc->getResponse();
			return $request ;
		}
		//print_r($request);
	}

Infos for readers :
Around one day to go deep in xmlrpc processes and functions in WP Source Code.
Around two hours to write this post.

Enjoy it !

Michel of dev.xiligroup team

This entry was posted in Experts corner, News, Studies and tagged , , . Bookmark the permalink.

7 Responses to feed one WordPress site with post from another WP using xmlrpc protocol

  1. Laura Garcia says:

    This plugin doesn’t seem to work! I edited the login info but i am unclear as to what the remote slug’s purpose is and how to use it could this be the problem why i am not seeing post on my remote blog?

  2. remoteslug value is used as key to save in local post’s custom post field the value of ID of the remote post… see in functions where attachments are transfered…

    This example is a small example. To test on local and remote server use tracing by inserting error_log line.

  3. BOB says:

    Salut, Ton article est parfait tu est le seul qui montre une solution pour poster de wordpress à wordpress. Par contre il y a deux trucs que je comprends pas : var $remote_server = ‘remoteslug’; // shortname used in custom field Sa correspond à quoi?

    Et surtout comment tu récupère l’id du post qui se trouve sur le blog ou tu as posté l’article afin de l’éditer Tu utilise get_post_meta ( 0, $remote_server, true ); Mais du coup comme je comprend pas le $remote_server je ne peux pas l’utiliser!

  4. Cet article est un exemple simplifié. Le $remote_server est un slug qui identifie le serveur cible. Cela permet, utilisé en tant que clé post_meta, sur le post local de remplir/conserver un champ personnalisé avec l’ID du post sur le WP distant. Ainsi, c’est le cas dans une utilisation pro et non publiée émettant plusieurs flux, il est possible de conserver pour un post local les numéros des posts dans plusieurs wp distants chacun étant identité par ce slug. Bon travail.

  5. BOB says:

    C’est exactement ce dont j’ai besoin. Mais ce que je comprends pas c’est ou tu le définis? Par ce que dans ma fonction qui post sur le blog je fais : $blog_server = ‘blogslug’; $to_post_id = get_post_meta ( $post_ID, $blog_server, true ); echo (‘id du post envoyer : ‘.$to_post_id); Et sa m’affiche rien. Donc je sais pas ou tu définis le nom de tom slug

  6. Ce plugin est l’instantiation d’une classe. Donc ici la valeur remote_server est défini à sa déclaration (ligne 26) . Dans la classe on y accède via $this->remote_server (ex ligne 48). En dehors $xili_xmlrpc->remote_server… Comme il s’agit d’un exemple, vous avez libre choix pour les adaptations.

  7. BOB says:

    Ok c’est bon en faite j’ai trouver. Merci beaucoup.

Poster un commentaire