1<?php
2/**
3 * XML-RPC protocol support for WordPress.
4 *
5 * @package WordPress
6 * @subpackage Publishing
7 */
8
9/**
10 * WordPress XMLRPC server implementation.
11 *
12 * Implements compatibility for Blogger API, MetaWeblog API, MovableType, and
13 * pingback. Additional WordPress API for managing comments, pages, posts,
14 * options, etc.
15 *
16 * As of WordPress 3.5.0, XML-RPC is enabled by default. It can be disabled
17 * via the {@see 'xmlrpc_enabled'} filter found in wp_xmlrpc_server::set_is_enabled().
18 *
19 * @since 1.5.0
20 *
21 * @see IXR_Server
22 */
23#[AllowDynamicProperties]
24class wp_xmlrpc_server extends IXR_Server {
25 /**
26 * Methods.
27 *
28 * @var array
29 */
30 public $methods;
31
32 /**
33 * Blog options.
34 *
35 * @var array
36 */
37 public $blog_options;
38
39 /**
40 * IXR_Error instance.
41 *
42 * @var IXR_Error
43 */
44 public $error;
45
46 /**
47 * Flags that the user authentication has failed in this instance of wp_xmlrpc_server.
48 *
49 * @var bool
50 */
51 protected $auth_failed = false;
52
53 /**
54 * Flags that XML-RPC is enabled
55 *
56 * @var bool
57 */
58 private $is_enabled;
59
60 /**
61 * Registers all of the XMLRPC methods that XMLRPC server understands.
62 *
63 * Sets up server and method property. Passes XMLRPC methods through the
64 * {@see 'xmlrpc_methods'} filter to allow plugins to extend or replace
65 * XML-RPC methods.
66 *
67 * @since 1.5.0
68 */
69 public function __construct() {
70 $this->methods = array(
71 // WordPress API.
72 'wp.getUsersBlogs' => 'this:wp_getUsersBlogs',
73 'wp.newPost' => 'this:wp_newPost',
74 'wp.editPost' => 'this:wp_editPost',
75 'wp.deletePost' => 'this:wp_deletePost',
76 'wp.getPost' => 'this:wp_getPost',
77 'wp.getPosts' => 'this:wp_getPosts',
78 'wp.newTerm' => 'this:wp_newTerm',
79 'wp.editTerm' => 'this:wp_editTerm',
80 'wp.deleteTerm' => 'this:wp_deleteTerm',
81 'wp.getTerm' => 'this:wp_getTerm',
82 'wp.getTerms' => 'this:wp_getTerms',
83 'wp.getTaxonomy' => 'this:wp_getTaxonomy',
84 'wp.getTaxonomies' => 'this:wp_getTaxonomies',
85 'wp.getUser' => 'this:wp_getUser',
86 'wp.getUsers' => 'this:wp_getUsers',
87 'wp.getProfile' => 'this:wp_getProfile',
88 'wp.editProfile' => 'this:wp_editProfile',
89 'wp.getPage' => 'this:wp_getPage',
90 'wp.getPages' => 'this:wp_getPages',
91 'wp.newPage' => 'this:wp_newPage',
92 'wp.deletePage' => 'this:wp_deletePage',
93 'wp.editPage' => 'this:wp_editPage',
94 'wp.getPageList' => 'this:wp_getPageList',
95 'wp.getAuthors' => 'this:wp_getAuthors',
96 'wp.getCategories' => 'this:mw_getCategories', // Alias.
97 'wp.getTags' => 'this:wp_getTags',
98 'wp.newCategory' => 'this:wp_newCategory',
99 'wp.deleteCategory' => 'this:wp_deleteCategory',
100 'wp.suggestCategories' => 'this:wp_suggestCategories',
101 'wp.uploadFile' => 'this:mw_newMediaObject', // Alias.
102 'wp.deleteFile' => 'this:wp_deletePost', // Alias.
103 'wp.getCommentCount' => 'this:wp_getCommentCount',
104 'wp.getPostStatusList' => 'this:wp_getPostStatusList',
105 'wp.getPageStatusList' => 'this:wp_getPageStatusList',
106 'wp.getPageTemplates' => 'this:wp_getPageTemplates',
107 'wp.getOptions' => 'this:wp_getOptions',
108 'wp.setOptions' => 'this:wp_setOptions',
109 'wp.getComment' => 'this:wp_getComment',
110 'wp.getComments' => 'this:wp_getComments',
111 'wp.deleteComment' => 'this:wp_deleteComment',
112 'wp.editComment' => 'this:wp_editComment',
113 'wp.newComment' => 'this:wp_newComment',
114 'wp.getCommentStatusList' => 'this:wp_getCommentStatusList',
115 'wp.getMediaItem' => 'this:wp_getMediaItem',
116 'wp.getMediaLibrary' => 'this:wp_getMediaLibrary',
117 'wp.getPostFormats' => 'this:wp_getPostFormats',
118 'wp.getPostType' => 'this:wp_getPostType',
119 'wp.getPostTypes' => 'this:wp_getPostTypes',
120 'wp.getRevisions' => 'this:wp_getRevisions',
121 'wp.restoreRevision' => 'this:wp_restoreRevision',
122
123 // Blogger API.
124 'blogger.getUsersBlogs' => 'this:blogger_getUsersBlogs',
125 'blogger.getUserInfo' => 'this:blogger_getUserInfo',
126 'blogger.getPost' => 'this:blogger_getPost',
127 'blogger.getRecentPosts' => 'this:blogger_getRecentPosts',
128 'blogger.newPost' => 'this:blogger_newPost',
129 'blogger.editPost' => 'this:blogger_editPost',
130 'blogger.deletePost' => 'this:blogger_deletePost',
131
132 // MetaWeblog API (with MT extensions to structs).
133 'metaWeblog.newPost' => 'this:mw_newPost',
134 'metaWeblog.editPost' => 'this:mw_editPost',
135 'metaWeblog.getPost' => 'this:mw_getPost',
136 'metaWeblog.getRecentPosts' => 'this:mw_getRecentPosts',
137 'metaWeblog.getCategories' => 'this:mw_getCategories',
138 'metaWeblog.newMediaObject' => 'this:mw_newMediaObject',
139
140 /*
141 * MetaWeblog API aliases for Blogger API.
142 * See http://www.xmlrpc.com/stories/storyReader$2460
143 */
144 'metaWeblog.deletePost' => 'this:blogger_deletePost',
145 'metaWeblog.getUsersBlogs' => 'this:blogger_getUsersBlogs',
146
147 // MovableType API.
148 'mt.getCategoryList' => 'this:mt_getCategoryList',
149 'mt.getRecentPostTitles' => 'this:mt_getRecentPostTitles',
150 'mt.getPostCategories' => 'this:mt_getPostCategories',
151 'mt.setPostCategories' => 'this:mt_setPostCategories',
152 'mt.supportedMethods' => 'this:mt_supportedMethods',
153 'mt.supportedTextFilters' => 'this:mt_supportedTextFilters',
154 'mt.getTrackbackPings' => 'this:mt_getTrackbackPings',
155 'mt.publishPost' => 'this:mt_publishPost',
156
157 // Pingback.
158 'pingback.ping' => 'this:pingback_ping',
159 'pingback.extensions.getPingbacks' => 'this:pingback_extensions_getPingbacks',
160
161 'demo.sayHello' => 'this:sayHello',
162 'demo.addTwoNumbers' => 'this:addTwoNumbers',
163 );
164
165 $this->initialise_blog_option_info();
166
167 /**
168 * Filters the methods exposed by the XML-RPC server.
169 *
170 * This filter can be used to add new methods, and remove built-in methods.
171 *
172 * @since 1.5.0
173 *
174 * @param string[] $methods An array of XML-RPC methods, keyed by their methodName.
175 */
176 $this->methods = apply_filters( 'xmlrpc_methods', $this->methods );
177
178 $this->set_is_enabled();
179 }
180
181 /**
182 * Sets wp_xmlrpc_server::$is_enabled property.
183 *
184 * Determines whether the xmlrpc server is enabled on this WordPress install
185 * and set the is_enabled property accordingly.
186 *
187 * @since 5.7.3
188 */
189 private function set_is_enabled() {
190 /*
191 * Respect old get_option() filters left for back-compat when the 'enable_xmlrpc'
192 * option was deprecated in 3.5.0. Use the {@see 'xmlrpc_enabled'} hook instead.
193 */
194 $is_enabled = apply_filters( 'pre_option_enable_xmlrpc', false );
195 if ( false === $is_enabled ) {
196 $is_enabled = apply_filters( 'option_enable_xmlrpc', true );
197 }
198
199 /**
200 * Filters whether XML-RPC methods requiring authentication are enabled.
201 *
202 * Contrary to the way it's named, this filter does not control whether XML-RPC is *fully*
203 * enabled, rather, it only controls whether XML-RPC methods requiring authentication -
204 * such as for publishing purposes - are enabled.
205 *
206 * Further, the filter does not control whether pingbacks or other custom endpoints that don't
207 * require authentication are enabled. This behavior is expected, and due to how parity was matched
208 * with the `enable_xmlrpc` UI option the filter replaced when it was introduced in 3.5.
209 *
210 * To disable XML-RPC methods that require authentication, use:
211 *
212 * add_filter( 'xmlrpc_enabled', '__return_false' );
213 *
214 * For more granular control over all XML-RPC methods and requests, see the {@see 'xmlrpc_methods'}
215 * and {@see 'xmlrpc_element_limit'} hooks.
216 *
217 * @since 3.5.0
218 *
219 * @param bool $is_enabled Whether XML-RPC is enabled. Default true.
220 */
221 $this->is_enabled = apply_filters( 'xmlrpc_enabled', $is_enabled );
222 }
223
224 /**
225 * Makes private/protected methods readable for backward compatibility.
226 *
227 * @since 4.0.0
228 *
229 * @param string $name Method to call.
230 * @param array $arguments Arguments to pass when calling.
231 * @return array|IXR_Error|false Return value of the callback, false otherwise.
232 */
233 public function __call( $name, $arguments ) {
234 if ( '_multisite_getUsersBlogs' === $name ) {
235 return $this->_multisite_getUsersBlogs( ...$arguments );
236 }
237 return false;
238 }
239
240 /**
241 * Serves the XML-RPC request.
242 *
243 * @since 2.9.0
244 */
245 public function serve_request() {
246 $this->IXR_Server( $this->methods );
247 }
248
249 /**
250 * Tests XMLRPC API by saying, "Hello!" to client.
251 *
252 * @since 1.5.0
253 *
254 * @return string Hello string response.
255 */
256 public function sayHello() {
257 return 'Hello!';
258 }
259
260 /**
261 * Tests XMLRPC API by adding two numbers for client.
262 *
263 * @since 1.5.0
264 *
265 * @param array $args {
266 * Method arguments. Note: arguments must be ordered as documented.
267 *
268 * @type int $0 A number to add.
269 * @type int $1 A second number to add.
270 * }
271 * @return int Sum of the two given numbers.
272 */
273 public function addTwoNumbers( $args ) {
274 $number1 = $args[0];
275 $number2 = $args[1];
276 return $number1 + $number2;
277 }
278
279 /**
280 * Logs user in.
281 *
282 * @since 2.8.0
283 *
284 * @param string $username User's username.
285 * @param string $password User's password.
286 * @return WP_User|false WP_User object if authentication passed, false otherwise.
287 */
288 public function login(
289 $username,
290 #[\SensitiveParameter]
291 $password
292 ) {
293 if ( ! $this->is_enabled ) {
294 $this->error = new IXR_Error( 405, sprintf( __( 'XML-RPC services are disabled on this site.' ) ) );
295 return false;
296 }
297
298 if ( $this->auth_failed ) {
299 $user = new WP_Error( 'login_prevented' );
300 } else {
301 $user = wp_authenticate( $username, $password );
302 }
303
304 if ( is_wp_error( $user ) ) {
305 $this->error = new IXR_Error( 403, __( 'Incorrect username or password.' ) );
306
307 // Flag that authentication has failed once on this wp_xmlrpc_server instance.
308 $this->auth_failed = true;
309
310 /**
311 * Filters the XML-RPC user login error message.
312 *
313 * @since 3.5.0
314 *
315 * @param IXR_Error $error The XML-RPC error message.
316 * @param WP_Error $user WP_Error object.
317 */
318 $this->error = apply_filters( 'xmlrpc_login_error', $this->error, $user );
319 return false;
320 }
321
322 wp_set_current_user( $user->ID );
323 return $user;
324 }
325
326 /**
327 * Checks user's credentials. Deprecated.
328 *
329 * @since 1.5.0
330 * @deprecated 2.8.0 Use wp_xmlrpc_server::login()
331 * @see wp_xmlrpc_server::login()
332 *
333 * @param string $username User's username.
334 * @param string $password User's password.
335 * @return bool Whether authentication passed.
336 */
337 public function login_pass_ok(
338 $username,
339 #[\SensitiveParameter]
340 $password
341 ) {
342 return (bool) $this->login( $username, $password );
343 }
344
345 /**
346 * Escapes string or array of strings for database.
347 *
348 * @since 1.5.2
349 *
350 * @param string|array $data Escape single string or array of strings.
351 * @return string|void Returns with string is passed, alters by-reference
352 * when array is passed.
353 */
354 public function escape( &$data ) {
355 if ( ! is_array( $data ) ) {
356 return wp_slash( $data );
357 }
358
359 foreach ( $data as &$v ) {
360 if ( is_array( $v ) ) {
361 $this->escape( $v );
362 } elseif ( ! is_object( $v ) ) {
363 $v = wp_slash( $v );
364 }
365 }
366 }
367
368 /**
369 * Sends error response to client.
370 *
371 * Sends an XML error response to the client. If the endpoint is enabled
372 * an HTTP 200 response is always sent per the XML-RPC specification.
373 *
374 * @since 5.7.3
375 *
376 * @param IXR_Error|string $error Error code or an error object.
377 * @param false $message Error message. Optional.
378 */
379 public function error( $error, $message = false ) {
380 // Accepts either an error object or an error code and message
381 if ( $message && ! is_object( $error ) ) {
382 $error = new IXR_Error( $error, $message );
383 }
384
385 if ( ! $this->is_enabled ) {
386 status_header( $error->code );
387 }
388
389 $this->output( $error->getXml() );
390 }
391
392 /**
393 * Retrieves custom fields for post.
394 *
395 * @since 2.5.0
396 *
397 * @param int $post_id Post ID.
398 * @return array Custom fields, if exist.
399 */
400 public function get_custom_fields( $post_id ) {
401 $post_id = (int) $post_id;
402
403 $custom_fields = array();
404
405 foreach ( (array) has_meta( $post_id ) as $meta ) {
406 // Don't expose protected fields.
407 if ( ! current_user_can( 'edit_post_meta', $post_id, $meta['meta_key'] ) ) {
408 continue;
409 }
410
411 $custom_fields[] = array(
412 'id' => $meta['meta_id'],
413 'key' => $meta['meta_key'],
414 'value' => $meta['meta_value'],
415 );
416 }
417
418 return $custom_fields;
419 }
420
421 /**
422 * Sets custom fields for post.
423 *
424 * @since 2.5.0
425 *
426 * @param int $post_id Post ID.
427 * @param array $fields Custom fields.
428 */
429 public function set_custom_fields( $post_id, $fields ) {
430 $post_id = (int) $post_id;
431
432 foreach ( (array) $fields as $meta ) {
433 if ( isset( $meta['id'] ) ) {
434 $meta['id'] = (int) $meta['id'];
435 $pmeta = get_metadata_by_mid( 'post', $meta['id'] );
436
437 if ( ! $pmeta || (int) $pmeta->post_id !== $post_id ) {
438 continue;
439 }
440
441 if ( isset( $meta['key'] ) ) {
442 $meta['key'] = wp_unslash( $meta['key'] );
443 if ( $meta['key'] !== $pmeta->meta_key ) {
444 continue;
445 }
446 $meta['value'] = wp_unslash( $meta['value'] );
447 if ( current_user_can( 'edit_post_meta', $post_id, $meta['key'] ) ) {
448 update_metadata_by_mid( 'post', $meta['id'], $meta['value'] );
449 }
450 } elseif ( current_user_can( 'delete_post_meta', $post_id, $pmeta->meta_key ) ) {
451 delete_metadata_by_mid( 'post', $meta['id'] );
452 }
453 } elseif ( current_user_can( 'add_post_meta', $post_id, wp_unslash( $meta['key'] ) ) ) {
454 add_post_meta( $post_id, $meta['key'], $meta['value'] );
455 }
456 }
457 }
458
459 /**
460 * Retrieves custom fields for a term.
461 *
462 * @since 4.9.0
463 *
464 * @param int $term_id Term ID.
465 * @return array Array of custom fields, if they exist.
466 */
467 public function get_term_custom_fields( $term_id ) {
468 $term_id = (int) $term_id;
469
470 $custom_fields = array();
471
472 foreach ( (array) has_term_meta( $term_id ) as $meta ) {
473
474 if ( ! current_user_can( 'edit_term_meta', $term_id ) ) {
475 continue;
476 }
477
478 $custom_fields[] = array(
479 'id' => $meta['meta_id'],
480 'key' => $meta['meta_key'],
481 'value' => $meta['meta_value'],
482 );
483 }
484
485 return $custom_fields;
486 }
487
488 /**
489 * Sets custom fields for a term.
490 *
491 * @since 4.9.0
492 *
493 * @param int $term_id Term ID.
494 * @param array $fields Custom fields.
495 */
496 public function set_term_custom_fields( $term_id, $fields ) {
497 $term_id = (int) $term_id;
498
499 foreach ( (array) $fields as $meta ) {
500 if ( isset( $meta['id'] ) ) {
501 $meta['id'] = (int) $meta['id'];
502 $pmeta = get_metadata_by_mid( 'term', $meta['id'] );
503 if ( isset( $meta['key'] ) ) {
504 $meta['key'] = wp_unslash( $meta['key'] );
505 if ( $meta['key'] !== $pmeta->meta_key ) {
506 continue;
507 }
508 $meta['value'] = wp_unslash( $meta['value'] );
509 if ( current_user_can( 'edit_term_meta', $term_id ) ) {
510 update_metadata_by_mid( 'term', $meta['id'], $meta['value'] );
511 }
512 } elseif ( current_user_can( 'delete_term_meta', $term_id ) ) {
513 delete_metadata_by_mid( 'term', $meta['id'] );
514 }
515 } elseif ( current_user_can( 'add_term_meta', $term_id ) ) {
516 add_term_meta( $term_id, $meta['key'], $meta['value'] );
517 }
518 }
519 }
520
521 /**
522 * Sets up blog options property.
523 *
524 * Passes property through {@see 'xmlrpc_blog_options'} filter.
525 *
526 * @since 2.6.0
527 */
528 public function initialise_blog_option_info() {
529 $this->blog_options = array(
530 // Read-only options.
531 'software_name' => array(
532 'desc' => __( 'Software Name' ),
533 'readonly' => true,
534 'value' => 'WordPress',
535 ),
536 'software_version' => array(
537 'desc' => __( 'Software Version' ),
538 'readonly' => true,
539 'value' => get_bloginfo( 'version' ),
540 ),
541 'blog_url' => array(
542 'desc' => __( 'WordPress Address (URL)' ),
543 'readonly' => true,
544 'option' => 'siteurl',
545 ),
546 'home_url' => array(
547 'desc' => __( 'Site Address (URL)' ),
548 'readonly' => true,
549 'option' => 'home',
550 ),
551 'login_url' => array(
552 'desc' => __( 'Login Address (URL)' ),
553 'readonly' => true,
554 'value' => wp_login_url(),
555 ),
556 'admin_url' => array(
557 'desc' => __( 'The URL to the admin area' ),
558 'readonly' => true,
559 'value' => get_admin_url(),
560 ),
561 'image_default_link_type' => array(
562 'desc' => __( 'Image default link type' ),
563 'readonly' => true,
564 'option' => 'image_default_link_type',
565 ),
566 'image_default_size' => array(
567 'desc' => __( 'Image default size' ),
568 'readonly' => true,
569 'option' => 'image_default_size',
570 ),
571 'image_default_align' => array(
572 'desc' => __( 'Image default align' ),
573 'readonly' => true,
574 'option' => 'image_default_align',
575 ),
576 'template' => array(
577 'desc' => __( 'Template' ),
578 'readonly' => true,
579 'option' => 'template',
580 ),
581 'stylesheet' => array(
582 'desc' => __( 'Stylesheet' ),
583 'readonly' => true,
584 'option' => 'stylesheet',
585 ),
586 'post_thumbnail' => array(
587 'desc' => __( 'Post Thumbnail' ),
588 'readonly' => true,
589 'value' => current_theme_supports( 'post-thumbnails' ),
590 ),
591
592 // Updatable options.
593 'time_zone' => array(
594 'desc' => __( 'Time Zone' ),
595 'readonly' => false,
596 'option' => 'gmt_offset',
597 ),
598 'blog_title' => array(
599 'desc' => __( 'Site Title' ),
600 'readonly' => false,
601 'option' => 'blogname',
602 ),
603 'blog_tagline' => array(
604 'desc' => __( 'Site Tagline' ),
605 'readonly' => false,
606 'option' => 'blogdescription',
607 ),
608 'date_format' => array(
609 'desc' => __( 'Date Format' ),
610 'readonly' => false,
611 'option' => 'date_format',
612 ),
613 'time_format' => array(
614 'desc' => __( 'Time Format' ),
615 'readonly' => false,
616 'option' => 'time_format',
617 ),
618 'users_can_register' => array(
619 'desc' => __( 'Allow new users to sign up' ),
620 'readonly' => false,
621 'option' => 'users_can_register',
622 ),
623 'thumbnail_size_w' => array(
624 'desc' => __( 'Thumbnail Width' ),
625 'readonly' => false,
626 'option' => 'thumbnail_size_w',
627 ),
628 'thumbnail_size_h' => array(
629 'desc' => __( 'Thumbnail Height' ),
630 'readonly' => false,
631 'option' => 'thumbnail_size_h',
632 ),
633 'thumbnail_crop' => array(
634 'desc' => __( 'Crop thumbnail to exact dimensions' ),
635 'readonly' => false,
636 'option' => 'thumbnail_crop',
637 ),
638 'medium_size_w' => array(
639 'desc' => __( 'Medium size image width' ),
640 'readonly' => false,
641 'option' => 'medium_size_w',
642 ),
643 'medium_size_h' => array(
644 'desc' => __( 'Medium size image height' ),
645 'readonly' => false,
646 'option' => 'medium_size_h',
647 ),
648 'medium_large_size_w' => array(
649 'desc' => __( 'Medium-Large size image width' ),
650 'readonly' => false,
651 'option' => 'medium_large_size_w',
652 ),
653 'medium_large_size_h' => array(
654 'desc' => __( 'Medium-Large size image height' ),
655 'readonly' => false,
656 'option' => 'medium_large_size_h',
657 ),
658 'large_size_w' => array(
659 'desc' => __( 'Large size image width' ),
660 'readonly' => false,
661 'option' => 'large_size_w',
662 ),
663 'large_size_h' => array(
664 'desc' => __( 'Large size image height' ),
665 'readonly' => false,
666 'option' => 'large_size_h',
667 ),
668 'default_comment_status' => array(
669 'desc' => __( 'Allow people to submit comments on new posts.' ),
670 'readonly' => false,
671 'option' => 'default_comment_status',
672 ),
673 'default_ping_status' => array(
674 'desc' => __( 'Allow link notifications from other blogs (pingbacks and trackbacks) on new posts.' ),
675 'readonly' => false,
676 'option' => 'default_ping_status',
677 ),
678 );
679
680 /**
681 * Filters the XML-RPC blog options property.
682 *
683 * @since 2.6.0
684 *
685 * @param array $blog_options An array of XML-RPC blog options.
686 */
687 $this->blog_options = apply_filters( 'xmlrpc_blog_options', $this->blog_options );
688 }
689
690 /**
691 * Retrieves the blogs of the user.
692 *
693 * @since 2.6.0
694 *
695 * @param array $args {
696 * Method arguments. Note: arguments must be ordered as documented.
697 *
698 * @type string $0 Username.
699 * @type string $1 Password.
700 * }
701 * @return array|IXR_Error Array contains:
702 * - 'isAdmin'
703 * - 'isPrimary' - whether the blog is the user's primary blog
704 * - 'url'
705 * - 'blogid'
706 * - 'blogName'
707 * - 'xmlrpc' - url of xmlrpc endpoint
708 */
709 public function wp_getUsersBlogs( $args ) {
710 if ( ! $this->minimum_args( $args, 2 ) ) {
711 return $this->error;
712 }
713
714 // If this isn't on WPMU then just use blogger_getUsersBlogs().
715 if ( ! is_multisite() ) {
716 array_unshift( $args, 1 );
717 return $this->blogger_getUsersBlogs( $args );
718 }
719
720 $this->escape( $args );
721
722 $username = $args[0];
723 $password = $args[1];
724
725 $user = $this->login( $username, $password );
726 if ( ! $user ) {
727 return $this->error;
728 }
729
730 /**
731 * Fires after the XML-RPC user has been authenticated but before the rest of
732 * the method logic begins.
733 *
734 * All built-in XML-RPC methods use the action xmlrpc_call, with a parameter
735 * equal to the method's name, e.g., wp.getUsersBlogs, wp.newPost, etc.
736 *
737 * @since 2.5.0
738 * @since 5.7.0 Added the `$args` and `$server` parameters.
739 *
740 * @param string $name The method name.
741 * @param array|string $args The escaped arguments passed to the method.
742 * @param wp_xmlrpc_server $server The XML-RPC server instance.
743 */
744 do_action( 'xmlrpc_call', 'wp.getUsersBlogs', $args, $this );
745
746 $blogs = (array) get_blogs_of_user( $user->ID );
747 $struct = array();
748
749 $primary_blog_id = 0;
750 $active_blog = get_active_blog_for_user( $user->ID );
751 if ( $active_blog ) {
752 $primary_blog_id = (int) $active_blog->blog_id;
753 }
754
755 $current_network_id = get_current_network_id();
756
757 foreach ( $blogs as $blog ) {
758 // Don't include blogs that aren't hosted at this site.
759 if ( $blog->site_id !== $current_network_id ) {
760 continue;
761 }
762
763 $blog_id = $blog->userblog_id;
764
765 switch_to_blog( $blog_id );
766
767 $is_admin = current_user_can( 'manage_options' );
768 $is_primary = ( (int) $blog_id === $primary_blog_id );
769
770 $struct[] = array(
771 'isAdmin' => $is_admin,
772 'isPrimary' => $is_primary,
773 'url' => home_url( '/' ),
774 'blogid' => (string) $blog_id,
775 'blogName' => get_option( 'blogname' ),
776 'xmlrpc' => site_url( 'xmlrpc.php', 'rpc' ),
777 );
778
779 restore_current_blog();
780 }
781
782 return $struct;
783 }
784
785 /**
786 * Checks if the method received at least the minimum number of arguments.
787 *
788 * @since 3.4.0
789 *
790 * @param array $args An array of arguments to check.
791 * @param int $count Minimum number of arguments.
792 * @return bool True if `$args` contains at least `$count` arguments, false otherwise.
793 */
794 protected function minimum_args( $args, $count ) {
795 if ( ! is_array( $args ) || count( $args ) < $count ) {
796 $this->error = new IXR_Error( 400, __( 'Insufficient arguments passed to this XML-RPC method.' ) );
797 return false;
798 }
799
800 return true;
801 }
802
803 /**
804 * Prepares taxonomy data for return in an XML-RPC object.
805 *
806 * @param WP_Taxonomy $taxonomy The unprepared taxonomy data.
807 * @param array $fields The subset of taxonomy fields to return.
808 * @return array The prepared taxonomy data.
809 */
810 protected function _prepare_taxonomy( $taxonomy, $fields ) {
811 $_taxonomy = array(
812 'name' => $taxonomy->name,
813 'label' => $taxonomy->label,
814 'hierarchical' => (bool) $taxonomy->hierarchical,
815 'public' => (bool) $taxonomy->public,
816 'show_ui' => (bool) $taxonomy->show_ui,
817 '_builtin' => (bool) $taxonomy->_builtin,
818 );
819
820 if ( in_array( 'labels', $fields, true ) ) {
821 $_taxonomy['labels'] = (array) $taxonomy->labels;
822 }
823
824 if ( in_array( 'cap', $fields, true ) ) {
825 $_taxonomy['cap'] = (array) $taxonomy->cap;
826 }
827
828 if ( in_array( 'menu', $fields, true ) ) {
829 $_taxonomy['show_in_menu'] = (bool) $taxonomy->show_in_menu;
830 }
831
832 if ( in_array( 'object_type', $fields, true ) ) {
833 $_taxonomy['object_type'] = array_unique( (array) $taxonomy->object_type );
834 }
835
836 /**
837 * Filters XML-RPC-prepared data for the given taxonomy.
838 *
839 * @since 3.4.0
840 *
841 * @param array $_taxonomy An array of taxonomy data.
842 * @param WP_Taxonomy $taxonomy Taxonomy object.
843 * @param array $fields The subset of taxonomy fields to return.
844 */
845 return apply_filters( 'xmlrpc_prepare_taxonomy', $_taxonomy, $taxonomy, $fields );
846 }
847
848 /**
849 * Prepares term data for return in an XML-RPC object.
850 *
851 * @param array|object $term The unprepared term data.
852 * @return array The prepared term data.
853 */
854 protected function _prepare_term( $term ) {
855 $_term = $term;
856 if ( ! is_array( $_term ) ) {
857 $_term = get_object_vars( $_term );
858 }
859
860 // For integers which may be larger than XML-RPC supports ensure we return strings.
861 $_term['term_id'] = (string) $_term['term_id'];
862 $_term['term_group'] = (string) $_term['term_group'];
863 $_term['term_taxonomy_id'] = (string) $_term['term_taxonomy_id'];
864 $_term['parent'] = (string) $_term['parent'];
865
866 // Count we are happy to return as an integer because people really shouldn't use terms that much.
867 $_term['count'] = (int) $_term['count'];
868
869 // Get term meta.
870 $_term['custom_fields'] = $this->get_term_custom_fields( $_term['term_id'] );
871
872 /**
873 * Filters XML-RPC-prepared data for the given term.
874 *
875 * @since 3.4.0
876 *
877 * @param array $_term An array of term data.
878 * @param array|object $term Term object or array.
879 */
880 return apply_filters( 'xmlrpc_prepare_term', $_term, $term );
881 }
882
883 /**
884 * Converts a WordPress date string to an IXR_Date object.
885 *
886 * @param string $date Date string to convert.
887 * @return IXR_Date IXR_Date object.
888 */
889 protected function _convert_date( $date ) {
890 if ( '0000-00-00 00:00:00' === $date ) {
891 return new IXR_Date( '00000000T00:00:00Z' );
892 }
893 return new IXR_Date( mysql2date( 'Ymd\TH:i:s', $date, false ) );
894 }
895
896 /**
897 * Converts a WordPress GMT date string to an IXR_Date object.
898 *
899 * @param string $date_gmt WordPress GMT date string.
900 * @param string $date Date string.
901 * @return IXR_Date IXR_Date object.
902 */
903 protected function _convert_date_gmt( $date_gmt, $date ) {
904 if ( '0000-00-00 00:00:00' !== $date && '0000-00-00 00:00:00' === $date_gmt ) {
905 return new IXR_Date( get_gmt_from_date( mysql2date( 'Y-m-d H:i:s', $date, false ), 'Ymd\TH:i:s' ) );
906 }
907 return $this->_convert_date( $date_gmt );
908 }
909
910 /**
911 * Prepares post data for return in an XML-RPC object.
912 *
913 * @param array $post The unprepared post data.
914 * @param array $fields The subset of post type fields to return.
915 * @return array The prepared post data.
916 */
917 protected function _prepare_post( $post, $fields ) {
918 // Holds the data for this post. built up based on $fields.
919 $_post = array( 'post_id' => (string) $post['ID'] );
920
921 // Prepare common post fields.
922 $post_fields = array(
923 'post_title' => $post['post_title'],
924 'post_date' => $this->_convert_date( $post['post_date'] ),
925 'post_date_gmt' => $this->_convert_date_gmt( $post['post_date_gmt'], $post['post_date'] ),
926 'post_modified' => $this->_convert_date( $post['post_modified'] ),
927 'post_modified_gmt' => $this->_convert_date_gmt( $post['post_modified_gmt'], $post['post_modified'] ),
928 'post_status' => $post['post_status'],
929 'post_type' => $post['post_type'],
930 'post_name' => $post['post_name'],
931 'post_author' => $post['post_author'],
932 'post_password' => $post['post_password'],
933 'post_excerpt' => $post['post_excerpt'],
934 'post_content' => $post['post_content'],
935 'post_parent' => (string) $post['post_parent'],
936 'post_mime_type' => $post['post_mime_type'],
937 'link' => get_permalink( $post['ID'] ),
938 'guid' => $post['guid'],
939 'menu_order' => (int) $post['menu_order'],
940 'comment_status' => $post['comment_status'],
941 'ping_status' => $post['ping_status'],
942 'sticky' => ( 'post' === $post['post_type'] && is_sticky( $post['ID'] ) ),
943 );
944
945 // Thumbnail.
946 $post_fields['post_thumbnail'] = array();
947 $thumbnail_id = get_post_thumbnail_id( $post['ID'] );
948 if ( $thumbnail_id ) {
949 $thumbnail_size = current_theme_supports( 'post-thumbnail' ) ? 'post-thumbnail' : 'thumbnail';
950 $post_fields['post_thumbnail'] = $this->_prepare_media_item( get_post( $thumbnail_id ), $thumbnail_size );
951 }
952
953 // Consider future posts as published.
954 if ( 'future' === $post_fields['post_status'] ) {
955 $post_fields['post_status'] = 'publish';
956 }
957
958 // Fill in blank post format.
959 $post_fields['post_format'] = get_post_format( $post['ID'] );
960 if ( empty( $post_fields['post_format'] ) ) {
961 $post_fields['post_format'] = 'standard';
962 }
963
964 // Merge requested $post_fields fields into $_post.
965 if ( in_array( 'post', $fields, true ) ) {
966 $_post = array_merge( $_post, $post_fields );
967 } else {
968 $requested_fields = array_intersect_key( $post_fields, array_flip( $fields ) );
969 $_post = array_merge( $_post, $requested_fields );
970 }
971
972 $all_taxonomy_fields = in_array( 'taxonomies', $fields, true );
973
974 if ( $all_taxonomy_fields || in_array( 'terms', $fields, true ) ) {
975 $post_type_taxonomies = get_object_taxonomies( $post['post_type'], 'names' );
976 $terms = wp_get_object_terms( $post['ID'], $post_type_taxonomies );
977 $_post['terms'] = array();
978 foreach ( $terms as $term ) {
979 $_post['terms'][] = $this->_prepare_term( $term );
980 }
981 }
982
983 if ( in_array( 'custom_fields', $fields, true ) ) {
984 $_post['custom_fields'] = $this->get_custom_fields( $post['ID'] );
985 }
986
987 if ( in_array( 'enclosure', $fields, true ) ) {
988 $_post['enclosure'] = array();
989 $enclosures = (array) get_post_meta( $post['ID'], 'enclosure' );
990 if ( ! empty( $enclosures ) ) {
991 $encdata = explode( "\n", $enclosures[0] );
992 $_post['enclosure']['url'] = trim( htmlspecialchars( $encdata[0] ) );
993 $_post['enclosure']['length'] = (int) trim( $encdata[1] );
994 $_post['enclosure']['type'] = trim( $encdata[2] );
995 }
996 }
997
998 /**
999 * Filters XML-RPC-prepared date for the given post.
1000 *
1001 * @since 3.4.0
1002 *
1003 * @param array $_post An array of modified post data.
1004 * @param array $post An array of post data.
1005 * @param array $fields An array of post fields.
1006 */
1007 return apply_filters( 'xmlrpc_prepare_post', $_post, $post, $fields );
1008 }
1009
1010 /**
1011 * Prepares post data for return in an XML-RPC object.
1012 *
1013 * @since 3.4.0
1014 * @since 4.6.0 Converted the `$post_type` parameter to accept a WP_Post_Type object.
1015 *
1016 * @param WP_Post_Type $post_type Post type object.
1017 * @param array $fields The subset of post fields to return.
1018 * @return array The prepared post type data.
1019 */
1020 protected function _prepare_post_type( $post_type, $fields ) {
1021 $_post_type = array(
1022 'name' => $post_type->name,
1023 'label' => $post_type->label,
1024 'hierarchical' => (bool) $post_type->hierarchical,
1025 'public' => (bool) $post_type->public,
1026 'show_ui' => (bool) $post_type->show_ui,
1027 '_builtin' => (bool) $post_type->_builtin,
1028 'has_archive' => (bool) $post_type->has_archive,
1029 'supports' => get_all_post_type_supports( $post_type->name ),
1030 );
1031
1032 if ( in_array( 'labels', $fields, true ) ) {
1033 $_post_type['labels'] = (array) $post_type->labels;
1034 }
1035
1036 if ( in_array( 'cap', $fields, true ) ) {
1037 $_post_type['cap'] = (array) $post_type->cap;
1038 $_post_type['map_meta_cap'] = (bool) $post_type->map_meta_cap;
1039 }
1040
1041 if ( in_array( 'menu', $fields, true ) ) {
1042 $_post_type['menu_position'] = (int) $post_type->menu_position;
1043 $_post_type['menu_icon'] = $post_type->menu_icon;
1044 $_post_type['show_in_menu'] = (bool) $post_type->show_in_menu;
1045 }
1046
1047 if ( in_array( 'taxonomies', $fields, true ) ) {
1048 $_post_type['taxonomies'] = get_object_taxonomies( $post_type->name, 'names' );
1049 }
1050
1051 /**
1052 * Filters XML-RPC-prepared date for the given post type.
1053 *
1054 * @since 3.4.0
1055 * @since 4.6.0 Converted the `$post_type` parameter to accept a WP_Post_Type object.
1056 *
1057 * @param array $_post_type An array of post type data.
1058 * @param WP_Post_Type $post_type Post type object.
1059 */
1060 return apply_filters( 'xmlrpc_prepare_post_type', $_post_type, $post_type );
1061 }
1062
1063 /**
1064 * Prepares media item data for return in an XML-RPC object.
1065 *
1066 * @param WP_Post $media_item The unprepared media item data.
1067 * @param string $thumbnail_size The image size to use for the thumbnail URL.
1068 * @return array The prepared media item data.
1069 */
1070 protected function _prepare_media_item( $media_item, $thumbnail_size = 'thumbnail' ) {
1071 $_media_item = array(
1072 'attachment_id' => (string) $media_item->ID,
1073 'date_created_gmt' => $this->_convert_date_gmt( $media_item->post_date_gmt, $media_item->post_date ),
1074 'parent' => $media_item->post_parent,
1075 'link' => wp_get_attachment_url( $media_item->ID ),
1076 'title' => $media_item->post_title,
1077 'caption' => $media_item->post_excerpt,
1078 'description' => $media_item->post_content,
1079 'metadata' => wp_get_attachment_metadata( $media_item->ID ),
1080 'type' => $media_item->post_mime_type,
1081 'alt' => get_post_meta( $media_item->ID, '_wp_attachment_image_alt', true ),
1082 );
1083
1084 $thumbnail_src = image_downsize( $media_item->ID, $thumbnail_size );
1085 if ( $thumbnail_src ) {
1086 $_media_item['thumbnail'] = $thumbnail_src[0];
1087 } else {
1088 $_media_item['thumbnail'] = $_media_item['link'];
1089 }
1090
1091 /**
1092 * Filters XML-RPC-prepared data for the given media item.
1093 *
1094 * @since 3.4.0
1095 *
1096 * @param array $_media_item An array of media item data.
1097 * @param WP_Post $media_item Media item object.
1098 * @param string $thumbnail_size Image size.
1099 */
1100 return apply_filters( 'xmlrpc_prepare_media_item', $_media_item, $media_item, $thumbnail_size );
1101 }
1102
1103 /**
1104 * Prepares page data for return in an XML-RPC object.
1105 *
1106 * @param WP_Post $page The unprepared page data.
1107 * @return array The prepared page data.
1108 */
1109 protected function _prepare_page( $page ) {
1110 // Get all of the page content and link.
1111 $full_page = get_extended( $page->post_content );
1112 $link = get_permalink( $page->ID );
1113
1114 // Get info the page parent if there is one.
1115 $parent_title = '';
1116 if ( ! empty( $page->post_parent ) ) {
1117 $parent = get_post( $page->post_parent );
1118 $parent_title = $parent->post_title;
1119 }
1120
1121 // Determine comment and ping settings.
1122 $allow_comments = comments_open( $page->ID ) ? 1 : 0;
1123 $allow_pings = pings_open( $page->ID ) ? 1 : 0;
1124
1125 // Format page date.
1126 $page_date = $this->_convert_date( $page->post_date );
1127 $page_date_gmt = $this->_convert_date_gmt( $page->post_date_gmt, $page->post_date );
1128
1129 // Pull the categories info together.
1130 $categories = array();
1131 if ( is_object_in_taxonomy( 'page', 'category' ) ) {
1132 foreach ( wp_get_post_categories( $page->ID ) as $cat_id ) {
1133 $categories[] = get_cat_name( $cat_id );
1134 }
1135 }
1136
1137 // Get the author info.
1138 $author = get_userdata( $page->post_author );
1139
1140 $page_template = get_page_template_slug( $page->ID );
1141 if ( empty( $page_template ) ) {
1142 $page_template = 'default';
1143 }
1144
1145 $_page = array(
1146 'dateCreated' => $page_date,
1147 'userid' => $page->post_author,
1148 'page_id' => $page->ID,
1149 'page_status' => $page->post_status,
1150 'description' => $full_page['main'],
1151 'title' => $page->post_title,
1152 'link' => $link,
1153 'permaLink' => $link,
1154 'categories' => $categories,
1155 'excerpt' => $page->post_excerpt,
1156 'text_more' => $full_page['extended'],
1157 'mt_allow_comments' => $allow_comments,
1158 'mt_allow_pings' => $allow_pings,
1159 'wp_slug' => $page->post_name,
1160 'wp_password' => $page->post_password,
1161 'wp_author' => $author->display_name,
1162 'wp_page_parent_id' => $page->post_parent,
1163 'wp_page_parent_title' => $parent_title,
1164 'wp_page_order' => $page->menu_order,
1165 'wp_author_id' => (string) $author->ID,
1166 'wp_author_display_name' => $author->display_name,
1167 'date_created_gmt' => $page_date_gmt,
1168 'custom_fields' => $this->get_custom_fields( $page->ID ),
1169 'wp_page_template' => $page_template,
1170 );
1171
1172 /**
1173 * Filters XML-RPC-prepared data for the given page.
1174 *
1175 * @since 3.4.0
1176 *
1177 * @param array $_page An array of page data.
1178 * @param WP_Post $page Page object.
1179 */
1180 return apply_filters( 'xmlrpc_prepare_page', $_page, $page );
1181 }
1182
1183 /**
1184 * Prepares comment data for return in an XML-RPC object.
1185 *
1186 * @param WP_Comment $comment The unprepared comment data.
1187 * @return array The prepared comment data.
1188 */
1189 protected function _prepare_comment( $comment ) {
1190 // Format page date.
1191 $comment_date_gmt = $this->_convert_date_gmt( $comment->comment_date_gmt, $comment->comment_date );
1192
1193 if ( '0' === $comment->comment_approved ) {
1194 $comment_status = 'hold';
1195 } elseif ( 'spam' === $comment->comment_approved ) {
1196 $comment_status = 'spam';
1197 } elseif ( '1' === $comment->comment_approved ) {
1198 $comment_status = 'approve';
1199 } else {
1200 $comment_status = $comment->comment_approved;
1201 }
1202 $_comment = array(
1203 'date_created_gmt' => $comment_date_gmt,
1204 'user_id' => $comment->user_id,
1205 'comment_id' => $comment->comment_ID,
1206 'parent' => $comment->comment_parent,
1207 'status' => $comment_status,
1208 'content' => $comment->comment_content,
1209 'link' => get_comment_link( $comment ),
1210 'post_id' => $comment->comment_post_ID,
1211 'post_title' => get_the_title( $comment->comment_post_ID ),
1212 'author' => $comment->comment_author,
1213 'author_url' => $comment->comment_author_url,
1214 'author_email' => $comment->comment_author_email,
1215 'author_ip' => $comment->comment_author_IP,
1216 'type' => $comment->comment_type,
1217 );
1218
1219 /**
1220 * Filters XML-RPC-prepared data for the given comment.
1221 *
1222 * @since 3.4.0
1223 *
1224 * @param array $_comment An array of prepared comment data.
1225 * @param WP_Comment $comment Comment object.
1226 */
1227 return apply_filters( 'xmlrpc_prepare_comment', $_comment, $comment );
1228 }
1229
1230 /**
1231 * Prepares user data for return in an XML-RPC object.
1232 *
1233 * @param WP_User $user The unprepared user object.
1234 * @param array $fields The subset of user fields to return.
1235 * @return array The prepared user data.
1236 */
1237 protected function _prepare_user( $user, $fields ) {
1238 $_user = array( 'user_id' => (string) $user->ID );
1239
1240 $user_fields = array(
1241 'username' => $user->user_login,
1242 'first_name' => $user->user_firstname,
1243 'last_name' => $user->user_lastname,
1244 'registered' => $this->_convert_date( $user->user_registered ),
1245 'bio' => $user->user_description,
1246 'email' => $user->user_email,
1247 'nickname' => $user->nickname,
1248 'nicename' => $user->user_nicename,
1249 'url' => $user->user_url,
1250 'display_name' => $user->display_name,
1251 'roles' => $user->roles,
1252 );
1253
1254 if ( in_array( 'all', $fields, true ) ) {
1255 $_user = array_merge( $_user, $user_fields );
1256 } else {
1257 if ( in_array( 'basic', $fields, true ) ) {
1258 $basic_fields = array( 'username', 'email', 'registered', 'display_name', 'nicename' );
1259 $fields = array_merge( $fields, $basic_fields );
1260 }
1261 $requested_fields = array_intersect_key( $user_fields, array_flip( $fields ) );
1262 $_user = array_merge( $_user, $requested_fields );
1263 }
1264
1265 /**
1266 * Filters XML-RPC-prepared data for the given user.
1267 *
1268 * @since 3.5.0
1269 *
1270 * @param array $_user An array of user data.
1271 * @param WP_User $user User object.
1272 * @param array $fields An array of user fields.
1273 */
1274 return apply_filters( 'xmlrpc_prepare_user', $_user, $user, $fields );
1275 }
1276
1277 /**
1278 * Creates a new post for any registered post type.
1279 *
1280 * @since 3.4.0
1281 *
1282 * @link https://en.wikipedia.org/wiki/RSS_enclosure for information on RSS enclosures.
1283 *
1284 * @param array $args {
1285 * Method arguments. Note: top-level arguments must be ordered as documented.
1286 *
1287 * @type int $0 Blog ID (unused).
1288 * @type string $1 Username.
1289 * @type string $2 Password.
1290 * @type array $3 {
1291 * Content struct for adding a new post. See wp_insert_post() for information on
1292 * additional post fields
1293 *
1294 * @type string $post_type Post type. Default 'post'.
1295 * @type string $post_status Post status. Default 'draft'
1296 * @type string $post_title Post title.
1297 * @type int $post_author Post author ID.
1298 * @type string $post_excerpt Post excerpt.
1299 * @type string $post_content Post content.
1300 * @type string $post_date_gmt Post date in GMT.
1301 * @type string $post_date Post date.
1302 * @type string $post_password Post password (20-character limit).
1303 * @type string $comment_status Post comment enabled status. Accepts 'open' or 'closed'.
1304 * @type string $ping_status Post ping status. Accepts 'open' or 'closed'.
1305 * @type bool $sticky Whether the post should be sticky. Automatically false if
1306 * `$post_status` is 'private'.
1307 * @type int $post_thumbnail ID of an image to use as the post thumbnail/featured image.
1308 * @type array $custom_fields Array of meta key/value pairs to add to the post.
1309 * @type array $terms Associative array with taxonomy names as keys and arrays
1310 * of term IDs as values.
1311 * @type array $terms_names Associative array with taxonomy names as keys and arrays
1312 * of term names as values.
1313 * @type array $enclosure {
1314 * Array of feed enclosure data to add to post meta.
1315 *
1316 * @type string $url URL for the feed enclosure.
1317 * @type int $length Size in bytes of the enclosure.
1318 * @type string $type Mime-type for the enclosure.
1319 * }
1320 * }
1321 * }
1322 * @return int|IXR_Error Post ID on success, IXR_Error instance otherwise.
1323 */
1324 public function wp_newPost( $args ) {
1325 if ( ! $this->minimum_args( $args, 4 ) ) {
1326 return $this->error;
1327 }
1328
1329 $this->escape( $args );
1330
1331 $username = $args[1];
1332 $password = $args[2];
1333 $content_struct = $args[3];
1334
1335 $user = $this->login( $username, $password );
1336 if ( ! $user ) {
1337 return $this->error;
1338 }
1339
1340 // Convert the date field back to IXR form.
1341 if ( isset( $content_struct['post_date'] ) && ! ( $content_struct['post_date'] instanceof IXR_Date ) ) {
1342 $content_struct['post_date'] = $this->_convert_date( $content_struct['post_date'] );
1343 }
1344
1345 /*
1346 * Ignore the existing GMT date if it is empty or a non-GMT date was supplied in $content_struct,
1347 * since _insert_post() will ignore the non-GMT date if the GMT date is set.
1348 */
1349 if ( isset( $content_struct['post_date_gmt'] ) && ! ( $content_struct['post_date_gmt'] instanceof IXR_Date ) ) {
1350 if ( '0000-00-00 00:00:00' === $content_struct['post_date_gmt'] || isset( $content_struct['post_date'] ) ) {
1351 unset( $content_struct['post_date_gmt'] );
1352 } else {
1353 $content_struct['post_date_gmt'] = $this->_convert_date( $content_struct['post_date_gmt'] );
1354 }
1355 }
1356
1357 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
1358 do_action( 'xmlrpc_call', 'wp.newPost', $args, $this );
1359
1360 unset( $content_struct['ID'] );
1361
1362 return $this->_insert_post( $user, $content_struct );
1363 }
1364
1365 /**
1366 * Helper method for filtering out elements from an array.
1367 *
1368 * @since 3.4.0
1369 *
1370 * @param int $count Number to compare to one.
1371 * @return bool True if the number is greater than one, false otherwise.
1372 */
1373 private function _is_greater_than_one( $count ) {
1374 return $count > 1;
1375 }
1376
1377 /**
1378 * Encapsulates the logic for sticking a post and determining if
1379 * the user has permission to do so.
1380 *
1381 * @since 4.3.0
1382 *
1383 * @param array $post_data
1384 * @param bool $update
1385 * @return void|IXR_Error
1386 */
1387 private function _toggle_sticky( $post_data, $update = false ) {
1388 $post_type = get_post_type_object( $post_data['post_type'] );
1389
1390 // Private and password-protected posts cannot be stickied.
1391 if ( 'private' === $post_data['post_status'] || ! empty( $post_data['post_password'] ) ) {
1392 // Error if the client tried to stick the post, otherwise, silently unstick.
1393 if ( ! empty( $post_data['sticky'] ) ) {
1394 return new IXR_Error( 401, __( 'Sorry, you cannot stick a private post.' ) );
1395 }
1396
1397 if ( $update ) {
1398 unstick_post( $post_data['ID'] );
1399 }
1400 } elseif ( isset( $post_data['sticky'] ) ) {
1401 if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) {
1402 return new IXR_Error( 401, __( 'Sorry, you are not allowed to make posts sticky.' ) );
1403 }
1404
1405 $sticky = wp_validate_boolean( $post_data['sticky'] );
1406 if ( $sticky ) {
1407 stick_post( $post_data['ID'] );
1408 } else {
1409 unstick_post( $post_data['ID'] );
1410 }
1411 }
1412 }
1413
1414 /**
1415 * Helper method for wp_newPost() and wp_editPost(), containing shared logic.
1416 *
1417 * @since 3.4.0
1418 *
1419 * @see wp_insert_post()
1420 *
1421 * @param WP_User $user The post author if post_author isn't set in $content_struct.
1422 * @param array|IXR_Error $content_struct Post data to insert.
1423 * @return IXR_Error|string
1424 */
1425 protected function _insert_post( $user, $content_struct ) {
1426 $defaults = array(
1427 'post_status' => 'draft',
1428 'post_type' => 'post',
1429 'post_author' => 0,
1430 'post_password' => '',
1431 'post_excerpt' => '',
1432 'post_content' => '',
1433 'post_title' => '',
1434 'post_date' => '',
1435 'post_date_gmt' => '',
1436 'post_format' => null,
1437 'post_name' => null,
1438 'post_thumbnail' => null,
1439 'post_parent' => 0,
1440 'ping_status' => '',
1441 'comment_status' => '',
1442 'custom_fields' => null,
1443 'terms_names' => null,
1444 'terms' => null,
1445 'sticky' => null,
1446 'enclosure' => null,
1447 'ID' => null,
1448 );
1449
1450 $post_data = wp_parse_args( array_intersect_key( $content_struct, $defaults ), $defaults );
1451
1452 $post_type = get_post_type_object( $post_data['post_type'] );
1453 if ( ! $post_type ) {
1454 return new IXR_Error( 403, __( 'Invalid post type.' ) );
1455 }
1456
1457 $update = ! empty( $post_data['ID'] );
1458
1459 if ( $update ) {
1460 if ( ! get_post( $post_data['ID'] ) ) {
1461 return new IXR_Error( 401, __( 'Invalid post ID.' ) );
1462 }
1463 if ( ! current_user_can( 'edit_post', $post_data['ID'] ) ) {
1464 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
1465 }
1466 if ( get_post_type( $post_data['ID'] ) !== $post_data['post_type'] ) {
1467 return new IXR_Error( 401, __( 'The post type may not be changed.' ) );
1468 }
1469 } else {
1470 if ( ! current_user_can( $post_type->cap->create_posts ) || ! current_user_can( $post_type->cap->edit_posts ) ) {
1471 return new IXR_Error( 401, __( 'Sorry, you are not allowed to post on this site.' ) );
1472 }
1473 }
1474
1475 switch ( $post_data['post_status'] ) {
1476 case 'draft':
1477 case 'pending':
1478 break;
1479 case 'private':
1480 if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
1481 return new IXR_Error( 401, __( 'Sorry, you are not allowed to create private posts in this post type.' ) );
1482 }
1483 break;
1484 case 'publish':
1485 case 'future':
1486 if ( ! current_user_can( $post_type->cap->publish_posts ) ) {
1487 return new IXR_Error( 401, __( 'Sorry, you are not allowed to publish posts in this post type.' ) );
1488 }
1489 break;
1490 default:
1491 if ( ! get_post_status_object( $post_data['post_status'] ) ) {
1492 $post_data['post_status'] = 'draft';
1493 }
1494 break;
1495 }
1496
1497 if ( ! empty( $post_data['post_password'] ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
1498 return new IXR_Error( 401, __( 'Sorry, you are not allowed to create password protected posts in this post type.' ) );
1499 }
1500
1501 $post_data['post_author'] = absint( $post_data['post_author'] );
1502 if ( ! empty( $post_data['post_author'] ) && $post_data['post_author'] !== $user->ID ) {
1503 if ( ! current_user_can( $post_type->cap->edit_others_posts ) ) {
1504 return new IXR_Error( 401, __( 'Sorry, you are not allowed to create posts as this user.' ) );
1505 }
1506
1507 $author = get_userdata( $post_data['post_author'] );
1508
1509 if ( ! $author ) {
1510 return new IXR_Error( 404, __( 'Invalid author ID.' ) );
1511 }
1512 } else {
1513 $post_data['post_author'] = $user->ID;
1514 }
1515
1516 if ( 'open' !== $post_data['comment_status'] && 'closed' !== $post_data['comment_status'] ) {
1517 unset( $post_data['comment_status'] );
1518 }
1519
1520 if ( 'open' !== $post_data['ping_status'] && 'closed' !== $post_data['ping_status'] ) {
1521 unset( $post_data['ping_status'] );
1522 }
1523
1524 // Do some timestamp voodoo.
1525 if ( ! empty( $post_data['post_date_gmt'] ) ) {
1526 // We know this is supposed to be GMT, so we're going to slap that Z on there by force.
1527 $date_created = rtrim( $post_data['post_date_gmt']->getIso(), 'Z' ) . 'Z';
1528 } elseif ( ! empty( $post_data['post_date'] ) ) {
1529 $date_created = $post_data['post_date']->getIso();
1530 }
1531
1532 // Default to not flagging the post date to be edited unless it's intentional.
1533 $post_data['edit_date'] = false;
1534
1535 if ( ! empty( $date_created ) ) {
1536 $post_data['post_date'] = iso8601_to_datetime( $date_created );
1537 $post_data['post_date_gmt'] = iso8601_to_datetime( $date_created, 'gmt' );
1538
1539 // Flag the post date to be edited.
1540 $post_data['edit_date'] = true;
1541 }
1542
1543 if ( ! isset( $post_data['ID'] ) ) {
1544 $post_data['ID'] = get_default_post_to_edit( $post_data['post_type'], true )->ID;
1545 }
1546 $post_id = $post_data['ID'];
1547
1548 if ( 'post' === $post_data['post_type'] ) {
1549 $error = $this->_toggle_sticky( $post_data, $update );
1550 if ( $error ) {
1551 return $error;
1552 }
1553 }
1554
1555 if ( isset( $post_data['post_thumbnail'] ) ) {
1556 // Empty value deletes, non-empty value adds/updates.
1557 if ( ! $post_data['post_thumbnail'] ) {
1558 delete_post_thumbnail( $post_id );
1559 } elseif ( ! get_post( absint( $post_data['post_thumbnail'] ) ) ) {
1560 return new IXR_Error( 404, __( 'Invalid attachment ID.' ) );
1561 }
1562 set_post_thumbnail( $post_id, $post_data['post_thumbnail'] );
1563 unset( $content_struct['post_thumbnail'] );
1564 }
1565
1566 if ( isset( $post_data['custom_fields'] ) ) {
1567 $this->set_custom_fields( $post_id, $post_data['custom_fields'] );
1568 }
1569
1570 if ( isset( $post_data['terms'] ) || isset( $post_data['terms_names'] ) ) {
1571 $post_type_taxonomies = get_object_taxonomies( $post_data['post_type'], 'objects' );
1572
1573 // Accumulate term IDs from terms and terms_names.
1574 $terms = array();
1575
1576 // First validate the terms specified by ID.
1577 if ( isset( $post_data['terms'] ) && is_array( $post_data['terms'] ) ) {
1578 $taxonomies = array_keys( $post_data['terms'] );
1579
1580 // Validating term IDs.
1581 foreach ( $taxonomies as $taxonomy ) {
1582 if ( ! array_key_exists( $taxonomy, $post_type_taxonomies ) ) {
1583 return new IXR_Error( 401, __( 'Sorry, one of the given taxonomies is not supported by the post type.' ) );
1584 }
1585
1586 if ( ! current_user_can( $post_type_taxonomies[ $taxonomy ]->cap->assign_terms ) ) {
1587 return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign a term to one of the given taxonomies.' ) );
1588 }
1589
1590 $term_ids = $post_data['terms'][ $taxonomy ];
1591 $terms[ $taxonomy ] = array();
1592 foreach ( $term_ids as $term_id ) {
1593 $term = get_term_by( 'id', $term_id, $taxonomy );
1594
1595 if ( ! $term ) {
1596 return new IXR_Error( 403, __( 'Invalid term ID.' ) );
1597 }
1598
1599 $terms[ $taxonomy ][] = (int) $term_id;
1600 }
1601 }
1602 }
1603
1604 // Now validate terms specified by name.
1605 if ( isset( $post_data['terms_names'] ) && is_array( $post_data['terms_names'] ) ) {
1606 $taxonomies = array_keys( $post_data['terms_names'] );
1607
1608 foreach ( $taxonomies as $taxonomy ) {
1609 if ( ! array_key_exists( $taxonomy, $post_type_taxonomies ) ) {
1610 return new IXR_Error( 401, __( 'Sorry, one of the given taxonomies is not supported by the post type.' ) );
1611 }
1612
1613 if ( ! current_user_can( $post_type_taxonomies[ $taxonomy ]->cap->assign_terms ) ) {
1614 return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign a term to one of the given taxonomies.' ) );
1615 }
1616
1617 /*
1618 * For hierarchical taxonomies, we can't assign a term when multiple terms
1619 * in the hierarchy share the same name.
1620 */
1621 $ambiguous_terms = array();
1622 if ( is_taxonomy_hierarchical( $taxonomy ) ) {
1623 $tax_term_names = get_terms(
1624 array(
1625 'taxonomy' => $taxonomy,
1626 'fields' => 'names',
1627 'hide_empty' => false,
1628 )
1629 );
1630
1631 // Count the number of terms with the same name.
1632 $tax_term_names_count = array_count_values( $tax_term_names );
1633
1634 // Filter out non-ambiguous term names.
1635 $ambiguous_tax_term_counts = array_filter( $tax_term_names_count, array( $this, '_is_greater_than_one' ) );
1636
1637 $ambiguous_terms = array_keys( $ambiguous_tax_term_counts );
1638 }
1639
1640 $term_names = $post_data['terms_names'][ $taxonomy ];
1641 foreach ( $term_names as $term_name ) {
1642 if ( in_array( $term_name, $ambiguous_terms, true ) ) {
1643 return new IXR_Error( 401, __( 'Ambiguous term name used in a hierarchical taxonomy. Please use term ID instead.' ) );
1644 }
1645
1646 $term = get_term_by( 'name', $term_name, $taxonomy );
1647
1648 if ( ! $term ) {
1649 // Term doesn't exist, so check that the user is allowed to create new terms.
1650 if ( ! current_user_can( $post_type_taxonomies[ $taxonomy ]->cap->edit_terms ) ) {
1651 return new IXR_Error( 401, __( 'Sorry, you are not allowed to add a term to one of the given taxonomies.' ) );
1652 }
1653
1654 // Create the new term.
1655 $term_info = wp_insert_term( $term_name, $taxonomy );
1656 if ( is_wp_error( $term_info ) ) {
1657 return new IXR_Error( 500, $term_info->get_error_message() );
1658 }
1659
1660 $terms[ $taxonomy ][] = (int) $term_info['term_id'];
1661 } else {
1662 $terms[ $taxonomy ][] = (int) $term->term_id;
1663 }
1664 }
1665 }
1666 }
1667
1668 $post_data['tax_input'] = $terms;
1669 unset( $post_data['terms'], $post_data['terms_names'] );
1670 }
1671
1672 if ( isset( $post_data['post_format'] ) ) {
1673 $format = set_post_format( $post_id, $post_data['post_format'] );
1674
1675 if ( is_wp_error( $format ) ) {
1676 return new IXR_Error( 500, $format->get_error_message() );
1677 }
1678
1679 unset( $post_data['post_format'] );
1680 }
1681
1682 // Handle enclosures.
1683 $enclosure = isset( $post_data['enclosure'] ) ? $post_data['enclosure'] : null;
1684 $this->add_enclosure_if_new( $post_id, $enclosure );
1685
1686 $this->attach_uploads( $post_id, $post_data['post_content'] );
1687
1688 /**
1689 * Filters post data array to be inserted via XML-RPC.
1690 *
1691 * @since 3.4.0
1692 *
1693 * @param array $post_data Parsed array of post data.
1694 * @param array $content_struct Post data array.
1695 */
1696 $post_data = apply_filters( 'xmlrpc_wp_insert_post_data', $post_data, $content_struct );
1697
1698 // Remove all null values to allow for using the insert/update post default values for those keys instead.
1699 $post_data = array_filter(
1700 $post_data,
1701 static function ( $value ) {
1702 return null !== $value;
1703 }
1704 );
1705
1706 $post_id = $update ? wp_update_post( $post_data, true ) : wp_insert_post( $post_data, true );
1707 if ( is_wp_error( $post_id ) ) {
1708 return new IXR_Error( 500, $post_id->get_error_message() );
1709 }
1710
1711 if ( ! $post_id ) {
1712 if ( $update ) {
1713 return new IXR_Error( 401, __( 'Sorry, the post could not be updated.' ) );
1714 } else {
1715 return new IXR_Error( 401, __( 'Sorry, the post could not be created.' ) );
1716 }
1717 }
1718
1719 return (string) $post_id;
1720 }
1721
1722 /**
1723 * Edits a post for any registered post type.
1724 *
1725 * The $content_struct parameter only needs to contain fields that
1726 * should be changed. All other fields will retain their existing values.
1727 *
1728 * @since 3.4.0
1729 *
1730 * @param array $args {
1731 * Method arguments. Note: arguments must be ordered as documented.
1732 *
1733 * @type int $0 Blog ID (unused).
1734 * @type string $1 Username.
1735 * @type string $2 Password.
1736 * @type int $3 Post ID.
1737 * @type array $4 Extra content arguments.
1738 * }
1739 * @return true|IXR_Error True on success, IXR_Error on failure.
1740 */
1741 public function wp_editPost( $args ) {
1742 if ( ! $this->minimum_args( $args, 5 ) ) {
1743 return $this->error;
1744 }
1745
1746 $this->escape( $args );
1747
1748 $username = $args[1];
1749 $password = $args[2];
1750 $post_id = (int) $args[3];
1751 $content_struct = $args[4];
1752
1753 $user = $this->login( $username, $password );
1754 if ( ! $user ) {
1755 return $this->error;
1756 }
1757
1758 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
1759 do_action( 'xmlrpc_call', 'wp.editPost', $args, $this );
1760
1761 $post = get_post( $post_id, ARRAY_A );
1762
1763 if ( empty( $post['ID'] ) ) {
1764 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
1765 }
1766
1767 if ( isset( $content_struct['if_not_modified_since'] ) ) {
1768 // If the post has been modified since the date provided, return an error.
1769 if ( mysql2date( 'U', $post['post_modified_gmt'] ) > $content_struct['if_not_modified_since']->getTimestamp() ) {
1770 return new IXR_Error( 409, __( 'There is a revision of this post that is more recent.' ) );
1771 }
1772 }
1773
1774 // Convert the date field back to IXR form.
1775 $post['post_date'] = $this->_convert_date( $post['post_date'] );
1776
1777 /*
1778 * Ignore the existing GMT date if it is empty or a non-GMT date was supplied in $content_struct,
1779 * since _insert_post() will ignore the non-GMT date if the GMT date is set.
1780 */
1781 if ( '0000-00-00 00:00:00' === $post['post_date_gmt'] || isset( $content_struct['post_date'] ) ) {
1782 unset( $post['post_date_gmt'] );
1783 } else {
1784 $post['post_date_gmt'] = $this->_convert_date( $post['post_date_gmt'] );
1785 }
1786
1787 /*
1788 * If the API client did not provide 'post_date', then we must not perpetuate the value that
1789 * was stored in the database, or it will appear to be an intentional edit. Conveying it here
1790 * as if it was coming from the API client will cause an otherwise zeroed out 'post_date_gmt'
1791 * to get set with the value that was originally stored in the database when the draft was created.
1792 */
1793 if ( ! isset( $content_struct['post_date'] ) ) {
1794 unset( $post['post_date'] );
1795 }
1796
1797 $this->escape( $post );
1798 $merged_content_struct = array_merge( $post, $content_struct );
1799
1800 $retval = $this->_insert_post( $user, $merged_content_struct );
1801 if ( $retval instanceof IXR_Error ) {
1802 return $retval;
1803 }
1804
1805 return true;
1806 }
1807
1808 /**
1809 * Deletes a post for any registered post type.
1810 *
1811 * @since 3.4.0
1812 *
1813 * @see wp_delete_post()
1814 *
1815 * @param array $args {
1816 * Method arguments. Note: arguments must be ordered as documented.
1817 *
1818 * @type int $0 Blog ID (unused).
1819 * @type string $1 Username.
1820 * @type string $2 Password.
1821 * @type int $3 Post ID.
1822 * }
1823 * @return true|IXR_Error True on success, IXR_Error instance on failure.
1824 */
1825 public function wp_deletePost( $args ) {
1826 if ( ! $this->minimum_args( $args, 4 ) ) {
1827 return $this->error;
1828 }
1829
1830 $this->escape( $args );
1831
1832 $username = $args[1];
1833 $password = $args[2];
1834 $post_id = (int) $args[3];
1835
1836 $user = $this->login( $username, $password );
1837 if ( ! $user ) {
1838 return $this->error;
1839 }
1840
1841 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
1842 do_action( 'xmlrpc_call', 'wp.deletePost', $args, $this );
1843
1844 $post = get_post( $post_id, ARRAY_A );
1845 if ( empty( $post['ID'] ) ) {
1846 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
1847 }
1848
1849 if ( ! current_user_can( 'delete_post', $post_id ) ) {
1850 return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this post.' ) );
1851 }
1852
1853 $result = wp_delete_post( $post_id );
1854
1855 if ( ! $result ) {
1856 return new IXR_Error( 500, __( 'Sorry, the post could not be deleted.' ) );
1857 }
1858
1859 return true;
1860 }
1861
1862 /**
1863 * Retrieves a post.
1864 *
1865 * @since 3.4.0
1866 *
1867 * The optional $fields parameter specifies what fields will be included
1868 * in the response array. This should be a list of field names. 'post_id' will
1869 * always be included in the response regardless of the value of $fields.
1870 *
1871 * Instead of, or in addition to, individual field names, conceptual group
1872 * names can be used to specify multiple fields. The available conceptual
1873 * groups are 'post' (all basic fields), 'taxonomies', 'custom_fields',
1874 * and 'enclosure'.
1875 *
1876 * @see get_post()
1877 *
1878 * @param array $args {
1879 * Method arguments. Note: arguments must be ordered as documented.
1880 *
1881 * @type int $0 Blog ID (unused).
1882 * @type string $1 Username.
1883 * @type string $2 Password.
1884 * @type int $3 Post ID.
1885 * @type array $4 Optional. The subset of post type fields to return.
1886 * }
1887 * @return array|IXR_Error Array contains (based on $fields parameter):
1888 * - 'post_id'
1889 * - 'post_title'
1890 * - 'post_date'
1891 * - 'post_date_gmt'
1892 * - 'post_modified'
1893 * - 'post_modified_gmt'
1894 * - 'post_status'
1895 * - 'post_type'
1896 * - 'post_name'
1897 * - 'post_author'
1898 * - 'post_password'
1899 * - 'post_excerpt'
1900 * - 'post_content'
1901 * - 'link'
1902 * - 'comment_status'
1903 * - 'ping_status'
1904 * - 'sticky'
1905 * - 'custom_fields'
1906 * - 'terms'
1907 * - 'categories'
1908 * - 'tags'
1909 * - 'enclosure'
1910 */
1911 public function wp_getPost( $args ) {
1912 if ( ! $this->minimum_args( $args, 4 ) ) {
1913 return $this->error;
1914 }
1915
1916 $this->escape( $args );
1917
1918 $username = $args[1];
1919 $password = $args[2];
1920 $post_id = (int) $args[3];
1921
1922 if ( isset( $args[4] ) ) {
1923 $fields = $args[4];
1924 } else {
1925 /**
1926 * Filters the default post query fields used by the given XML-RPC method.
1927 *
1928 * @since 3.4.0
1929 *
1930 * @param array $fields An array of post fields to retrieve. By default,
1931 * contains 'post', 'terms', and 'custom_fields'.
1932 * @param string $method Method name.
1933 */
1934 $fields = apply_filters( 'xmlrpc_default_post_fields', array( 'post', 'terms', 'custom_fields' ), 'wp.getPost' );
1935 }
1936
1937 $user = $this->login( $username, $password );
1938 if ( ! $user ) {
1939 return $this->error;
1940 }
1941
1942 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
1943 do_action( 'xmlrpc_call', 'wp.getPost', $args, $this );
1944
1945 $post = get_post( $post_id, ARRAY_A );
1946
1947 if ( empty( $post['ID'] ) ) {
1948 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
1949 }
1950
1951 if ( ! current_user_can( 'edit_post', $post_id ) ) {
1952 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
1953 }
1954
1955 return $this->_prepare_post( $post, $fields );
1956 }
1957
1958 /**
1959 * Retrieves posts.
1960 *
1961 * @since 3.4.0
1962 *
1963 * @see wp_get_recent_posts()
1964 * @see wp_getPost() for more on `$fields`
1965 * @see get_posts() for more on `$filter` values
1966 *
1967 * @param array $args {
1968 * Method arguments. Note: arguments must be ordered as documented.
1969 *
1970 * @type int $0 Blog ID (unused).
1971 * @type string $1 Username.
1972 * @type string $2 Password.
1973 * @type array $3 Optional. Modifies the query used to retrieve posts. Accepts 'post_type',
1974 * 'post_status', 'number', 'offset', 'orderby', 's', and 'order'.
1975 * Default empty array.
1976 * @type array $4 Optional. The subset of post type fields to return in the response array.
1977 * }
1978 * @return array|IXR_Error Array containing a collection of posts.
1979 */
1980 public function wp_getPosts( $args ) {
1981 if ( ! $this->minimum_args( $args, 3 ) ) {
1982 return $this->error;
1983 }
1984
1985 $this->escape( $args );
1986
1987 $username = $args[1];
1988 $password = $args[2];
1989 $filter = isset( $args[3] ) ? $args[3] : array();
1990
1991 if ( isset( $args[4] ) ) {
1992 $fields = $args[4];
1993 } else {
1994 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
1995 $fields = apply_filters( 'xmlrpc_default_post_fields', array( 'post', 'terms', 'custom_fields' ), 'wp.getPosts' );
1996 }
1997
1998 $user = $this->login( $username, $password );
1999 if ( ! $user ) {
2000 return $this->error;
2001 }
2002
2003 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2004 do_action( 'xmlrpc_call', 'wp.getPosts', $args, $this );
2005
2006 $query = array();
2007
2008 if ( isset( $filter['post_type'] ) ) {
2009 $post_type = get_post_type_object( $filter['post_type'] );
2010 if ( ! ( (bool) $post_type ) ) {
2011 return new IXR_Error( 403, __( 'Invalid post type.' ) );
2012 }
2013 } else {
2014 $post_type = get_post_type_object( 'post' );
2015 }
2016
2017 if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
2018 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts in this post type.' ) );
2019 }
2020
2021 $query['post_type'] = $post_type->name;
2022
2023 if ( isset( $filter['post_status'] ) ) {
2024 $query['post_status'] = $filter['post_status'];
2025 }
2026
2027 if ( isset( $filter['number'] ) ) {
2028 $query['numberposts'] = absint( $filter['number'] );
2029 }
2030
2031 if ( isset( $filter['offset'] ) ) {
2032 $query['offset'] = absint( $filter['offset'] );
2033 }
2034
2035 if ( isset( $filter['orderby'] ) ) {
2036 $query['orderby'] = $filter['orderby'];
2037
2038 if ( isset( $filter['order'] ) ) {
2039 $query['order'] = $filter['order'];
2040 }
2041 }
2042
2043 if ( isset( $filter['s'] ) ) {
2044 $query['s'] = $filter['s'];
2045 }
2046
2047 $posts_list = wp_get_recent_posts( $query );
2048
2049 if ( ! $posts_list ) {
2050 return array();
2051 }
2052
2053 // Holds all the posts data.
2054 $struct = array();
2055
2056 foreach ( $posts_list as $post ) {
2057 if ( ! current_user_can( 'edit_post', $post['ID'] ) ) {
2058 continue;
2059 }
2060
2061 $struct[] = $this->_prepare_post( $post, $fields );
2062 }
2063
2064 return $struct;
2065 }
2066
2067 /**
2068 * Creates a new term.
2069 *
2070 * @since 3.4.0
2071 *
2072 * @see wp_insert_term()
2073 *
2074 * @param array $args {
2075 * Method arguments. Note: arguments must be ordered as documented.
2076 *
2077 * @type int $0 Blog ID (unused).
2078 * @type string $1 Username.
2079 * @type string $2 Password.
2080 * @type array $3 Content struct for adding a new term. The struct must contain
2081 * the term 'name' and 'taxonomy'. Optional accepted values include
2082 * 'parent', 'description', and 'slug'.
2083 * }
2084 * @return int|IXR_Error The term ID on success, or an IXR_Error object on failure.
2085 */
2086 public function wp_newTerm( $args ) {
2087 if ( ! $this->minimum_args( $args, 4 ) ) {
2088 return $this->error;
2089 }
2090
2091 $this->escape( $args );
2092
2093 $username = $args[1];
2094 $password = $args[2];
2095 $content_struct = $args[3];
2096
2097 $user = $this->login( $username, $password );
2098 if ( ! $user ) {
2099 return $this->error;
2100 }
2101
2102 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2103 do_action( 'xmlrpc_call', 'wp.newTerm', $args, $this );
2104
2105 if ( ! taxonomy_exists( $content_struct['taxonomy'] ) ) {
2106 return new IXR_Error( 403, __( 'Invalid taxonomy.' ) );
2107 }
2108
2109 $taxonomy = get_taxonomy( $content_struct['taxonomy'] );
2110
2111 if ( ! current_user_can( $taxonomy->cap->edit_terms ) ) {
2112 return new IXR_Error( 401, __( 'Sorry, you are not allowed to create terms in this taxonomy.' ) );
2113 }
2114
2115 $taxonomy = (array) $taxonomy;
2116
2117 // Hold the data of the term.
2118 $term_data = array();
2119
2120 $term_data['name'] = trim( $content_struct['name'] );
2121 if ( empty( $term_data['name'] ) ) {
2122 return new IXR_Error( 403, __( 'The term name cannot be empty.' ) );
2123 }
2124
2125 if ( isset( $content_struct['parent'] ) ) {
2126 if ( ! $taxonomy['hierarchical'] ) {
2127 return new IXR_Error( 403, __( 'This taxonomy is not hierarchical.' ) );
2128 }
2129
2130 $parent_term_id = (int) $content_struct['parent'];
2131 $parent_term = get_term( $parent_term_id, $taxonomy['name'] );
2132
2133 if ( is_wp_error( $parent_term ) ) {
2134 return new IXR_Error( 500, $parent_term->get_error_message() );
2135 }
2136
2137 if ( ! $parent_term ) {
2138 return new IXR_Error( 403, __( 'Parent term does not exist.' ) );
2139 }
2140
2141 $term_data['parent'] = $content_struct['parent'];
2142 }
2143
2144 if ( isset( $content_struct['description'] ) ) {
2145 $term_data['description'] = $content_struct['description'];
2146 }
2147
2148 if ( isset( $content_struct['slug'] ) ) {
2149 $term_data['slug'] = $content_struct['slug'];
2150 }
2151
2152 $term = wp_insert_term( $term_data['name'], $taxonomy['name'], $term_data );
2153
2154 if ( is_wp_error( $term ) ) {
2155 return new IXR_Error( 500, $term->get_error_message() );
2156 }
2157
2158 if ( ! $term ) {
2159 return new IXR_Error( 500, __( 'Sorry, the term could not be created.' ) );
2160 }
2161
2162 // Add term meta.
2163 if ( isset( $content_struct['custom_fields'] ) ) {
2164 $this->set_term_custom_fields( $term['term_id'], $content_struct['custom_fields'] );
2165 }
2166
2167 return (string) $term['term_id'];
2168 }
2169
2170 /**
2171 * Edits a term.
2172 *
2173 * @since 3.4.0
2174 *
2175 * @see wp_update_term()
2176 *
2177 * @param array $args {
2178 * Method arguments. Note: arguments must be ordered as documented.
2179 *
2180 * @type int $0 Blog ID (unused).
2181 * @type string $1 Username.
2182 * @type string $2 Password.
2183 * @type int $3 Term ID.
2184 * @type array $4 Content struct for editing a term. The struct must contain the
2185 * term 'taxonomy'. Optional accepted values include 'name', 'parent',
2186 * 'description', and 'slug'.
2187 * }
2188 * @return true|IXR_Error True on success, IXR_Error instance on failure.
2189 */
2190 public function wp_editTerm( $args ) {
2191 if ( ! $this->minimum_args( $args, 5 ) ) {
2192 return $this->error;
2193 }
2194
2195 $this->escape( $args );
2196
2197 $username = $args[1];
2198 $password = $args[2];
2199 $term_id = (int) $args[3];
2200 $content_struct = $args[4];
2201
2202 $user = $this->login( $username, $password );
2203 if ( ! $user ) {
2204 return $this->error;
2205 }
2206
2207 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2208 do_action( 'xmlrpc_call', 'wp.editTerm', $args, $this );
2209
2210 if ( ! taxonomy_exists( $content_struct['taxonomy'] ) ) {
2211 return new IXR_Error( 403, __( 'Invalid taxonomy.' ) );
2212 }
2213
2214 $taxonomy = get_taxonomy( $content_struct['taxonomy'] );
2215
2216 $taxonomy = (array) $taxonomy;
2217
2218 // Hold the data of the term.
2219 $term_data = array();
2220
2221 $term = get_term( $term_id, $content_struct['taxonomy'] );
2222
2223 if ( is_wp_error( $term ) ) {
2224 return new IXR_Error( 500, $term->get_error_message() );
2225 }
2226
2227 if ( ! $term ) {
2228 return new IXR_Error( 404, __( 'Invalid term ID.' ) );
2229 }
2230
2231 if ( ! current_user_can( 'edit_term', $term_id ) ) {
2232 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this term.' ) );
2233 }
2234
2235 if ( isset( $content_struct['name'] ) ) {
2236 $term_data['name'] = trim( $content_struct['name'] );
2237
2238 if ( empty( $term_data['name'] ) ) {
2239 return new IXR_Error( 403, __( 'The term name cannot be empty.' ) );
2240 }
2241 }
2242
2243 if ( ! empty( $content_struct['parent'] ) ) {
2244 if ( ! $taxonomy['hierarchical'] ) {
2245 return new IXR_Error( 403, __( 'Cannot set parent term, taxonomy is not hierarchical.' ) );
2246 }
2247
2248 $parent_term_id = (int) $content_struct['parent'];
2249 $parent_term = get_term( $parent_term_id, $taxonomy['name'] );
2250
2251 if ( is_wp_error( $parent_term ) ) {
2252 return new IXR_Error( 500, $parent_term->get_error_message() );
2253 }
2254
2255 if ( ! $parent_term ) {
2256 return new IXR_Error( 403, __( 'Parent term does not exist.' ) );
2257 }
2258
2259 $term_data['parent'] = $content_struct['parent'];
2260 }
2261
2262 if ( isset( $content_struct['description'] ) ) {
2263 $term_data['description'] = $content_struct['description'];
2264 }
2265
2266 if ( isset( $content_struct['slug'] ) ) {
2267 $term_data['slug'] = $content_struct['slug'];
2268 }
2269
2270 $term = wp_update_term( $term_id, $taxonomy['name'], $term_data );
2271
2272 if ( is_wp_error( $term ) ) {
2273 return new IXR_Error( 500, $term->get_error_message() );
2274 }
2275
2276 if ( ! $term ) {
2277 return new IXR_Error( 500, __( 'Sorry, editing the term failed.' ) );
2278 }
2279
2280 // Update term meta.
2281 if ( isset( $content_struct['custom_fields'] ) ) {
2282 $this->set_term_custom_fields( $term_id, $content_struct['custom_fields'] );
2283 }
2284
2285 return true;
2286 }
2287
2288 /**
2289 * Deletes a term.
2290 *
2291 * @since 3.4.0
2292 *
2293 * @see wp_delete_term()
2294 *
2295 * @param array $args {
2296 * Method arguments. Note: arguments must be ordered as documented.
2297 *
2298 * @type int $0 Blog ID (unused).
2299 * @type string $1 Username.
2300 * @type string $2 Password.
2301 * @type string $3 Taxonomy name.
2302 * @type int $4 Term ID.
2303 * }
2304 * @return true|IXR_Error True on success, IXR_Error instance on failure.
2305 */
2306 public function wp_deleteTerm( $args ) {
2307 if ( ! $this->minimum_args( $args, 5 ) ) {
2308 return $this->error;
2309 }
2310
2311 $this->escape( $args );
2312
2313 $username = $args[1];
2314 $password = $args[2];
2315 $taxonomy = $args[3];
2316 $term_id = (int) $args[4];
2317
2318 $user = $this->login( $username, $password );
2319 if ( ! $user ) {
2320 return $this->error;
2321 }
2322
2323 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2324 do_action( 'xmlrpc_call', 'wp.deleteTerm', $args, $this );
2325
2326 if ( ! taxonomy_exists( $taxonomy ) ) {
2327 return new IXR_Error( 403, __( 'Invalid taxonomy.' ) );
2328 }
2329
2330 $taxonomy = get_taxonomy( $taxonomy );
2331 $term = get_term( $term_id, $taxonomy->name );
2332
2333 if ( is_wp_error( $term ) ) {
2334 return new IXR_Error( 500, $term->get_error_message() );
2335 }
2336
2337 if ( ! $term ) {
2338 return new IXR_Error( 404, __( 'Invalid term ID.' ) );
2339 }
2340
2341 if ( ! current_user_can( 'delete_term', $term_id ) ) {
2342 return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this term.' ) );
2343 }
2344
2345 $result = wp_delete_term( $term_id, $taxonomy->name );
2346
2347 if ( is_wp_error( $result ) ) {
2348 return new IXR_Error( 500, $result->get_error_message() );
2349 }
2350
2351 if ( ! $result ) {
2352 return new IXR_Error( 500, __( 'Sorry, deleting the term failed.' ) );
2353 }
2354
2355 return $result;
2356 }
2357
2358 /**
2359 * Retrieves a term.
2360 *
2361 * @since 3.4.0
2362 *
2363 * @see get_term()
2364 *
2365 * @param array $args {
2366 * Method arguments. Note: arguments must be ordered as documented.
2367 *
2368 * @type int $0 Blog ID (unused).
2369 * @type string $1 Username.
2370 * @type string $2 Password.
2371 * @type string $3 Taxonomy name.
2372 * @type int $4 Term ID.
2373 * }
2374 * @return array|IXR_Error IXR_Error on failure, array on success, containing:
2375 * - 'term_id'
2376 * - 'name'
2377 * - 'slug'
2378 * - 'term_group'
2379 * - 'term_taxonomy_id'
2380 * - 'taxonomy'
2381 * - 'description'
2382 * - 'parent'
2383 * - 'count'
2384 */
2385 public function wp_getTerm( $args ) {
2386 if ( ! $this->minimum_args( $args, 5 ) ) {
2387 return $this->error;
2388 }
2389
2390 $this->escape( $args );
2391
2392 $username = $args[1];
2393 $password = $args[2];
2394 $taxonomy = $args[3];
2395 $term_id = (int) $args[4];
2396
2397 $user = $this->login( $username, $password );
2398 if ( ! $user ) {
2399 return $this->error;
2400 }
2401
2402 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2403 do_action( 'xmlrpc_call', 'wp.getTerm', $args, $this );
2404
2405 if ( ! taxonomy_exists( $taxonomy ) ) {
2406 return new IXR_Error( 403, __( 'Invalid taxonomy.' ) );
2407 }
2408
2409 $taxonomy = get_taxonomy( $taxonomy );
2410
2411 $term = get_term( $term_id, $taxonomy->name, ARRAY_A );
2412
2413 if ( is_wp_error( $term ) ) {
2414 return new IXR_Error( 500, $term->get_error_message() );
2415 }
2416
2417 if ( ! $term ) {
2418 return new IXR_Error( 404, __( 'Invalid term ID.' ) );
2419 }
2420
2421 if ( ! current_user_can( 'assign_term', $term_id ) ) {
2422 return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign this term.' ) );
2423 }
2424
2425 return $this->_prepare_term( $term );
2426 }
2427
2428 /**
2429 * Retrieves all terms for a taxonomy.
2430 *
2431 * @since 3.4.0
2432 *
2433 * The optional $filter parameter modifies the query used to retrieve terms.
2434 * Accepted keys are 'number', 'offset', 'orderby', 'order', 'hide_empty', and 'search'.
2435 *
2436 * @see get_terms()
2437 *
2438 * @param array $args {
2439 * Method arguments. Note: arguments must be ordered as documented.
2440 *
2441 * @type int $0 Blog ID (unused).
2442 * @type string $1 Username.
2443 * @type string $2 Password.
2444 * @type string $3 Taxonomy name.
2445 * @type array $4 Optional. Modifies the query used to retrieve posts. Accepts 'number',
2446 * 'offset', 'orderby', 'order', 'hide_empty', and 'search'. Default empty array.
2447 * }
2448 * @return array|IXR_Error An associative array of terms data on success, IXR_Error instance otherwise.
2449 */
2450 public function wp_getTerms( $args ) {
2451 if ( ! $this->minimum_args( $args, 4 ) ) {
2452 return $this->error;
2453 }
2454
2455 $this->escape( $args );
2456
2457 $username = $args[1];
2458 $password = $args[2];
2459 $taxonomy = $args[3];
2460 $filter = isset( $args[4] ) ? $args[4] : array();
2461
2462 $user = $this->login( $username, $password );
2463 if ( ! $user ) {
2464 return $this->error;
2465 }
2466
2467 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2468 do_action( 'xmlrpc_call', 'wp.getTerms', $args, $this );
2469
2470 if ( ! taxonomy_exists( $taxonomy ) ) {
2471 return new IXR_Error( 403, __( 'Invalid taxonomy.' ) );
2472 }
2473
2474 $taxonomy = get_taxonomy( $taxonomy );
2475
2476 if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) {
2477 return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign terms in this taxonomy.' ) );
2478 }
2479
2480 $query = array( 'taxonomy' => $taxonomy->name );
2481
2482 if ( isset( $filter['number'] ) ) {
2483 $query['number'] = absint( $filter['number'] );
2484 }
2485
2486 if ( isset( $filter['offset'] ) ) {
2487 $query['offset'] = absint( $filter['offset'] );
2488 }
2489
2490 if ( isset( $filter['orderby'] ) ) {
2491 $query['orderby'] = $filter['orderby'];
2492
2493 if ( isset( $filter['order'] ) ) {
2494 $query['order'] = $filter['order'];
2495 }
2496 }
2497
2498 if ( isset( $filter['hide_empty'] ) ) {
2499 $query['hide_empty'] = $filter['hide_empty'];
2500 } else {
2501 $query['get'] = 'all';
2502 }
2503
2504 if ( isset( $filter['search'] ) ) {
2505 $query['search'] = $filter['search'];
2506 }
2507
2508 $terms = get_terms( $query );
2509
2510 if ( is_wp_error( $terms ) ) {
2511 return new IXR_Error( 500, $terms->get_error_message() );
2512 }
2513
2514 $struct = array();
2515
2516 foreach ( $terms as $term ) {
2517 $struct[] = $this->_prepare_term( $term );
2518 }
2519
2520 return $struct;
2521 }
2522
2523 /**
2524 * Retrieves a taxonomy.
2525 *
2526 * @since 3.4.0
2527 *
2528 * @see get_taxonomy()
2529 *
2530 * @param array $args {
2531 * Method arguments. Note: arguments must be ordered as documented.
2532 *
2533 * @type int $0 Blog ID (unused).
2534 * @type string $1 Username.
2535 * @type string $2 Password.
2536 * @type string $3 Taxonomy name.
2537 * @type array $4 Optional. Array of taxonomy fields to limit to in the return.
2538 * Accepts 'labels', 'cap', 'menu', and 'object_type'.
2539 * Default empty array.
2540 * }
2541 * @return array|IXR_Error An array of taxonomy data on success, IXR_Error instance otherwise.
2542 */
2543 public function wp_getTaxonomy( $args ) {
2544 if ( ! $this->minimum_args( $args, 4 ) ) {
2545 return $this->error;
2546 }
2547
2548 $this->escape( $args );
2549
2550 $username = $args[1];
2551 $password = $args[2];
2552 $taxonomy = $args[3];
2553
2554 if ( isset( $args[4] ) ) {
2555 $fields = $args[4];
2556 } else {
2557 /**
2558 * Filters the default taxonomy query fields used by the given XML-RPC method.
2559 *
2560 * @since 3.4.0
2561 *
2562 * @param array $fields An array of taxonomy fields to retrieve. By default,
2563 * contains 'labels', 'cap', and 'object_type'.
2564 * @param string $method The method name.
2565 */
2566 $fields = apply_filters( 'xmlrpc_default_taxonomy_fields', array( 'labels', 'cap', 'object_type' ), 'wp.getTaxonomy' );
2567 }
2568
2569 $user = $this->login( $username, $password );
2570 if ( ! $user ) {
2571 return $this->error;
2572 }
2573
2574 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2575 do_action( 'xmlrpc_call', 'wp.getTaxonomy', $args, $this );
2576
2577 if ( ! taxonomy_exists( $taxonomy ) ) {
2578 return new IXR_Error( 403, __( 'Invalid taxonomy.' ) );
2579 }
2580
2581 $taxonomy = get_taxonomy( $taxonomy );
2582
2583 if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) {
2584 return new IXR_Error( 401, __( 'Sorry, you are not allowed to assign terms in this taxonomy.' ) );
2585 }
2586
2587 return $this->_prepare_taxonomy( $taxonomy, $fields );
2588 }
2589
2590 /**
2591 * Retrieves all taxonomies.
2592 *
2593 * @since 3.4.0
2594 *
2595 * @see get_taxonomies()
2596 *
2597 * @param array $args {
2598 * Method arguments. Note: arguments must be ordered as documented.
2599 *
2600 * @type int $0 Blog ID (unused).
2601 * @type string $1 Username.
2602 * @type string $2 Password.
2603 * @type array $3 Optional. An array of arguments for retrieving taxonomies.
2604 * @type array $4 Optional. The subset of taxonomy fields to return.
2605 * }
2606 * @return array|IXR_Error An associative array of taxonomy data with returned fields determined
2607 * by `$fields`, or an IXR_Error instance on failure.
2608 */
2609 public function wp_getTaxonomies( $args ) {
2610 if ( ! $this->minimum_args( $args, 3 ) ) {
2611 return $this->error;
2612 }
2613
2614 $this->escape( $args );
2615
2616 $username = $args[1];
2617 $password = $args[2];
2618 $filter = isset( $args[3] ) ? $args[3] : array( 'public' => true );
2619
2620 if ( isset( $args[4] ) ) {
2621 $fields = $args[4];
2622 } else {
2623 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2624 $fields = apply_filters( 'xmlrpc_default_taxonomy_fields', array( 'labels', 'cap', 'object_type' ), 'wp.getTaxonomies' );
2625 }
2626
2627 $user = $this->login( $username, $password );
2628 if ( ! $user ) {
2629 return $this->error;
2630 }
2631
2632 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2633 do_action( 'xmlrpc_call', 'wp.getTaxonomies', $args, $this );
2634
2635 $taxonomies = get_taxonomies( $filter, 'objects' );
2636
2637 // Holds all the taxonomy data.
2638 $struct = array();
2639
2640 foreach ( $taxonomies as $taxonomy ) {
2641 // Capability check for post types.
2642 if ( ! current_user_can( $taxonomy->cap->assign_terms ) ) {
2643 continue;
2644 }
2645
2646 $struct[] = $this->_prepare_taxonomy( $taxonomy, $fields );
2647 }
2648
2649 return $struct;
2650 }
2651
2652 /**
2653 * Retrieves a user.
2654 *
2655 * The optional $fields parameter specifies what fields will be included
2656 * in the response array. This should be a list of field names. 'user_id' will
2657 * always be included in the response regardless of the value of $fields.
2658 *
2659 * Instead of, or in addition to, individual field names, conceptual group
2660 * names can be used to specify multiple fields. The available conceptual
2661 * groups are 'basic' and 'all'.
2662 *
2663 * @uses get_userdata()
2664 *
2665 * @param array $args {
2666 * Method arguments. Note: arguments must be ordered as documented.
2667 *
2668 * @type int $0 Blog ID (unused).
2669 * @type string $1 Username.
2670 * @type string $2 Password.
2671 * @type int $3 User ID.
2672 * @type array $4 Optional. Array of fields to return.
2673 * }
2674 * @return array|IXR_Error Array contains (based on $fields parameter):
2675 * - 'user_id'
2676 * - 'username'
2677 * - 'first_name'
2678 * - 'last_name'
2679 * - 'registered'
2680 * - 'bio'
2681 * - 'email'
2682 * - 'nickname'
2683 * - 'nicename'
2684 * - 'url'
2685 * - 'display_name'
2686 * - 'roles'
2687 */
2688 public function wp_getUser( $args ) {
2689 if ( ! $this->minimum_args( $args, 4 ) ) {
2690 return $this->error;
2691 }
2692
2693 $this->escape( $args );
2694
2695 $username = $args[1];
2696 $password = $args[2];
2697 $user_id = (int) $args[3];
2698
2699 if ( isset( $args[4] ) ) {
2700 $fields = $args[4];
2701 } else {
2702 /**
2703 * Filters the default user query fields used by the given XML-RPC method.
2704 *
2705 * @since 3.5.0
2706 *
2707 * @param array $fields An array of user fields to retrieve. By default, contains 'all'.
2708 * @param string $method The method name.
2709 */
2710 $fields = apply_filters( 'xmlrpc_default_user_fields', array( 'all' ), 'wp.getUser' );
2711 }
2712
2713 $user = $this->login( $username, $password );
2714 if ( ! $user ) {
2715 return $this->error;
2716 }
2717
2718 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2719 do_action( 'xmlrpc_call', 'wp.getUser', $args, $this );
2720
2721 if ( ! current_user_can( 'edit_user', $user_id ) ) {
2722 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this user.' ) );
2723 }
2724
2725 $user_data = get_userdata( $user_id );
2726
2727 if ( ! $user_data ) {
2728 return new IXR_Error( 404, __( 'Invalid user ID.' ) );
2729 }
2730
2731 return $this->_prepare_user( $user_data, $fields );
2732 }
2733
2734 /**
2735 * Retrieves users.
2736 *
2737 * The optional $filter parameter modifies the query used to retrieve users.
2738 * Accepted keys are 'number' (default: 50), 'offset' (default: 0), 'role',
2739 * 'who', 'orderby', and 'order'.
2740 *
2741 * The optional $fields parameter specifies what fields will be included
2742 * in the response array.
2743 *
2744 * @uses get_users()
2745 * @see wp_getUser() for more on $fields and return values
2746 *
2747 * @param array $args {
2748 * Method arguments. Note: arguments must be ordered as documented.
2749 *
2750 * @type int $0 Blog ID (unused).
2751 * @type string $1 Username.
2752 * @type string $2 Password.
2753 * @type array $3 Optional. Arguments for the user query.
2754 * @type array $4 Optional. Fields to return.
2755 * }
2756 * @return array|IXR_Error users data
2757 */
2758 public function wp_getUsers( $args ) {
2759 if ( ! $this->minimum_args( $args, 3 ) ) {
2760 return $this->error;
2761 }
2762
2763 $this->escape( $args );
2764
2765 $username = $args[1];
2766 $password = $args[2];
2767 $filter = isset( $args[3] ) ? $args[3] : array();
2768
2769 if ( isset( $args[4] ) ) {
2770 $fields = $args[4];
2771 } else {
2772 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2773 $fields = apply_filters( 'xmlrpc_default_user_fields', array( 'all' ), 'wp.getUsers' );
2774 }
2775
2776 $user = $this->login( $username, $password );
2777 if ( ! $user ) {
2778 return $this->error;
2779 }
2780
2781 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2782 do_action( 'xmlrpc_call', 'wp.getUsers', $args, $this );
2783
2784 if ( ! current_user_can( 'list_users' ) ) {
2785 return new IXR_Error( 401, __( 'Sorry, you are not allowed to list users.' ) );
2786 }
2787
2788 $query = array( 'fields' => 'all_with_meta' );
2789
2790 $query['number'] = ( isset( $filter['number'] ) ) ? absint( $filter['number'] ) : 50;
2791 $query['offset'] = ( isset( $filter['offset'] ) ) ? absint( $filter['offset'] ) : 0;
2792
2793 if ( isset( $filter['orderby'] ) ) {
2794 $query['orderby'] = $filter['orderby'];
2795
2796 if ( isset( $filter['order'] ) ) {
2797 $query['order'] = $filter['order'];
2798 }
2799 }
2800
2801 if ( isset( $filter['role'] ) ) {
2802 if ( get_role( $filter['role'] ) === null ) {
2803 return new IXR_Error( 403, __( 'Invalid role.' ) );
2804 }
2805
2806 $query['role'] = $filter['role'];
2807 }
2808
2809 if ( isset( $filter['who'] ) ) {
2810 $query['who'] = $filter['who'];
2811 }
2812
2813 $users = get_users( $query );
2814
2815 $_users = array();
2816 foreach ( $users as $user_data ) {
2817 if ( current_user_can( 'edit_user', $user_data->ID ) ) {
2818 $_users[] = $this->_prepare_user( $user_data, $fields );
2819 }
2820 }
2821 return $_users;
2822 }
2823
2824 /**
2825 * Retrieves information about the requesting user.
2826 *
2827 * @uses get_userdata()
2828 *
2829 * @param array $args {
2830 * Method arguments. Note: arguments must be ordered as documented.
2831 *
2832 * @type int $0 Blog ID (unused).
2833 * @type string $1 Username
2834 * @type string $2 Password
2835 * @type array $3 Optional. Fields to return.
2836 * }
2837 * @return array|IXR_Error (@see wp_getUser)
2838 */
2839 public function wp_getProfile( $args ) {
2840 if ( ! $this->minimum_args( $args, 3 ) ) {
2841 return $this->error;
2842 }
2843
2844 $this->escape( $args );
2845
2846 $username = $args[1];
2847 $password = $args[2];
2848
2849 if ( isset( $args[3] ) ) {
2850 $fields = $args[3];
2851 } else {
2852 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2853 $fields = apply_filters( 'xmlrpc_default_user_fields', array( 'all' ), 'wp.getProfile' );
2854 }
2855
2856 $user = $this->login( $username, $password );
2857 if ( ! $user ) {
2858 return $this->error;
2859 }
2860
2861 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2862 do_action( 'xmlrpc_call', 'wp.getProfile', $args, $this );
2863
2864 if ( ! current_user_can( 'edit_user', $user->ID ) ) {
2865 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit your profile.' ) );
2866 }
2867
2868 $user_data = get_userdata( $user->ID );
2869
2870 return $this->_prepare_user( $user_data, $fields );
2871 }
2872
2873 /**
2874 * Edits user's profile.
2875 *
2876 * @uses wp_update_user()
2877 *
2878 * @param array $args {
2879 * Method arguments. Note: arguments must be ordered as documented.
2880 *
2881 * @type int $0 Blog ID (unused).
2882 * @type string $1 Username.
2883 * @type string $2 Password.
2884 * @type array $3 Content struct. It can optionally contain:
2885 * - 'first_name'
2886 * - 'last_name'
2887 * - 'website'
2888 * - 'display_name'
2889 * - 'nickname'
2890 * - 'nicename'
2891 * - 'bio'
2892 * }
2893 * @return true|IXR_Error True, on success.
2894 */
2895 public function wp_editProfile( $args ) {
2896 if ( ! $this->minimum_args( $args, 4 ) ) {
2897 return $this->error;
2898 }
2899
2900 $this->escape( $args );
2901
2902 $username = $args[1];
2903 $password = $args[2];
2904 $content_struct = $args[3];
2905
2906 $user = $this->login( $username, $password );
2907 if ( ! $user ) {
2908 return $this->error;
2909 }
2910
2911 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
2912 do_action( 'xmlrpc_call', 'wp.editProfile', $args, $this );
2913
2914 if ( ! current_user_can( 'edit_user', $user->ID ) ) {
2915 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit your profile.' ) );
2916 }
2917
2918 // Holds data of the user.
2919 $user_data = array();
2920 $user_data['ID'] = $user->ID;
2921
2922 // Only set the user details if they were given.
2923 if ( isset( $content_struct['first_name'] ) ) {
2924 $user_data['first_name'] = $content_struct['first_name'];
2925 }
2926
2927 if ( isset( $content_struct['last_name'] ) ) {
2928 $user_data['last_name'] = $content_struct['last_name'];
2929 }
2930
2931 if ( isset( $content_struct['url'] ) ) {
2932 $user_data['user_url'] = $content_struct['url'];
2933 }
2934
2935 if ( isset( $content_struct['display_name'] ) ) {
2936 $user_data['display_name'] = $content_struct['display_name'];
2937 }
2938
2939 if ( isset( $content_struct['nickname'] ) ) {
2940 $user_data['nickname'] = $content_struct['nickname'];
2941 }
2942
2943 if ( isset( $content_struct['nicename'] ) ) {
2944 $user_data['user_nicename'] = $content_struct['nicename'];
2945 }
2946
2947 if ( isset( $content_struct['bio'] ) ) {
2948 $user_data['description'] = $content_struct['bio'];
2949 }
2950
2951 $result = wp_update_user( $user_data );
2952
2953 if ( is_wp_error( $result ) ) {
2954 return new IXR_Error( 500, $result->get_error_message() );
2955 }
2956
2957 if ( ! $result ) {
2958 return new IXR_Error( 500, __( 'Sorry, the user could not be updated.' ) );
2959 }
2960
2961 return true;
2962 }
2963
2964 /**
2965 * Retrieves a page.
2966 *
2967 * @since 2.2.0
2968 *
2969 * @param array $args {
2970 * Method arguments. Note: arguments must be ordered as documented.
2971 *
2972 * @type int $0 Blog ID (unused).
2973 * @type int $1 Page ID.
2974 * @type string $2 Username.
2975 * @type string $3 Password.
2976 * }
2977 * @return array|IXR_Error
2978 */
2979 public function wp_getPage( $args ) {
2980 $this->escape( $args );
2981
2982 $page_id = (int) $args[1];
2983 $username = $args[2];
2984 $password = $args[3];
2985
2986 $user = $this->login( $username, $password );
2987 if ( ! $user ) {
2988 return $this->error;
2989 }
2990
2991 $page = get_post( $page_id );
2992 if ( ! $page ) {
2993 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
2994 }
2995
2996 if ( ! current_user_can( 'edit_page', $page_id ) ) {
2997 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this page.' ) );
2998 }
2999
3000 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3001 do_action( 'xmlrpc_call', 'wp.getPage', $args, $this );
3002
3003 // If we found the page then format the data.
3004 if ( $page->ID && ( 'page' === $page->post_type ) ) {
3005 return $this->_prepare_page( $page );
3006 } else {
3007 // If the page doesn't exist, indicate that.
3008 return new IXR_Error( 404, __( 'Sorry, no such page.' ) );
3009 }
3010 }
3011
3012 /**
3013 * Retrieves Pages.
3014 *
3015 * @since 2.2.0
3016 *
3017 * @param array $args {
3018 * Method arguments. Note: arguments must be ordered as documented.
3019 *
3020 * @type int $0 Blog ID (unused).
3021 * @type string $1 Username.
3022 * @type string $2 Password.
3023 * @type int $3 Optional. Number of pages. Default 10.
3024 * }
3025 * @return array|IXR_Error
3026 */
3027 public function wp_getPages( $args ) {
3028 $this->escape( $args );
3029
3030 $username = $args[1];
3031 $password = $args[2];
3032 $num_pages = isset( $args[3] ) ? (int) $args[3] : 10;
3033
3034 $user = $this->login( $username, $password );
3035 if ( ! $user ) {
3036 return $this->error;
3037 }
3038
3039 if ( ! current_user_can( 'edit_pages' ) ) {
3040 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit pages.' ) );
3041 }
3042
3043 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3044 do_action( 'xmlrpc_call', 'wp.getPages', $args, $this );
3045
3046 $pages = get_posts(
3047 array(
3048 'post_type' => 'page',
3049 'post_status' => 'any',
3050 'numberposts' => $num_pages,
3051 )
3052 );
3053 $num_pages = count( $pages );
3054
3055 // If we have pages, put together their info.
3056 if ( $num_pages >= 1 ) {
3057 $pages_struct = array();
3058
3059 foreach ( $pages as $page ) {
3060 if ( current_user_can( 'edit_page', $page->ID ) ) {
3061 $pages_struct[] = $this->_prepare_page( $page );
3062 }
3063 }
3064
3065 return $pages_struct;
3066 }
3067
3068 return array();
3069 }
3070
3071 /**
3072 * Creates a new page.
3073 *
3074 * @since 2.2.0
3075 *
3076 * @see wp_xmlrpc_server::mw_newPost()
3077 *
3078 * @param array $args {
3079 * Method arguments. Note: arguments must be ordered as documented.
3080 *
3081 * @type int $0 Blog ID (unused).
3082 * @type string $1 Username.
3083 * @type string $2 Password.
3084 * @type array $3 Content struct.
3085 * }
3086 * @return int|IXR_Error
3087 */
3088 public function wp_newPage( $args ) {
3089 // Items not escaped here will be escaped in wp_newPost().
3090 $username = $this->escape( $args[1] );
3091 $password = $this->escape( $args[2] );
3092
3093 $user = $this->login( $username, $password );
3094 if ( ! $user ) {
3095 return $this->error;
3096 }
3097
3098 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3099 do_action( 'xmlrpc_call', 'wp.newPage', $args, $this );
3100
3101 // Mark this as content for a page.
3102 $args[3]['post_type'] = 'page';
3103
3104 // Let mw_newPost() do all of the heavy lifting.
3105 return $this->mw_newPost( $args );
3106 }
3107
3108 /**
3109 * Deletes a page.
3110 *
3111 * @since 2.2.0
3112 *
3113 * @param array $args {
3114 * Method arguments. Note: arguments must be ordered as documented.
3115 *
3116 * @type int $0 Blog ID (unused).
3117 * @type string $1 Username.
3118 * @type string $2 Password.
3119 * @type int $3 Page ID.
3120 * }
3121 * @return true|IXR_Error True, if success.
3122 */
3123 public function wp_deletePage( $args ) {
3124 $this->escape( $args );
3125
3126 $username = $args[1];
3127 $password = $args[2];
3128 $page_id = (int) $args[3];
3129
3130 $user = $this->login( $username, $password );
3131 if ( ! $user ) {
3132 return $this->error;
3133 }
3134
3135 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3136 do_action( 'xmlrpc_call', 'wp.deletePage', $args, $this );
3137
3138 /*
3139 * Get the current page based on the 'page_id' and
3140 * make sure it is a page and not a post.
3141 */
3142 $actual_page = get_post( $page_id, ARRAY_A );
3143 if ( ! $actual_page || ( 'page' !== $actual_page['post_type'] ) ) {
3144 return new IXR_Error( 404, __( 'Sorry, no such page.' ) );
3145 }
3146
3147 // Make sure the user can delete pages.
3148 if ( ! current_user_can( 'delete_page', $page_id ) ) {
3149 return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this page.' ) );
3150 }
3151
3152 // Attempt to delete the page.
3153 $result = wp_delete_post( $page_id );
3154 if ( ! $result ) {
3155 return new IXR_Error( 500, __( 'Failed to delete the page.' ) );
3156 }
3157
3158 /**
3159 * Fires after a page has been successfully deleted via XML-RPC.
3160 *
3161 * @since 3.4.0
3162 *
3163 * @param int $page_id ID of the deleted page.
3164 * @param array $args An array of arguments to delete the page.
3165 */
3166 do_action( 'xmlrpc_call_success_wp_deletePage', $page_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
3167
3168 return true;
3169 }
3170
3171 /**
3172 * Edits a page.
3173 *
3174 * @since 2.2.0
3175 *
3176 * @param array $args {
3177 * Method arguments. Note: arguments must be ordered as documented.
3178 *
3179 * @type int $0 Blog ID (unused).
3180 * @type int $1 Page ID.
3181 * @type string $2 Username.
3182 * @type string $3 Password.
3183 * @type string $4 Content.
3184 * @type int $5 Publish flag. 0 for draft, 1 for publish.
3185 * }
3186 * @return array|IXR_Error
3187 */
3188 public function wp_editPage( $args ) {
3189 // Items will be escaped in mw_editPost().
3190 $page_id = (int) $args[1];
3191 $username = $args[2];
3192 $password = $args[3];
3193 $content = $args[4];
3194 $publish = $args[5];
3195
3196 $escaped_username = $this->escape( $username );
3197 $escaped_password = $this->escape( $password );
3198
3199 $user = $this->login( $escaped_username, $escaped_password );
3200 if ( ! $user ) {
3201 return $this->error;
3202 }
3203
3204 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3205 do_action( 'xmlrpc_call', 'wp.editPage', $args, $this );
3206
3207 // Get the page data and make sure it is a page.
3208 $actual_page = get_post( $page_id, ARRAY_A );
3209 if ( ! $actual_page || ( 'page' !== $actual_page['post_type'] ) ) {
3210 return new IXR_Error( 404, __( 'Sorry, no such page.' ) );
3211 }
3212
3213 // Make sure the user is allowed to edit pages.
3214 if ( ! current_user_can( 'edit_page', $page_id ) ) {
3215 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this page.' ) );
3216 }
3217
3218 // Mark this as content for a page.
3219 $content['post_type'] = 'page';
3220
3221 // Arrange args in the way mw_editPost() understands.
3222 $args = array(
3223 $page_id,
3224 $username,
3225 $password,
3226 $content,
3227 $publish,
3228 );
3229
3230 // Let mw_editPost() do all of the heavy lifting.
3231 return $this->mw_editPost( $args );
3232 }
3233
3234 /**
3235 * Retrieves page list.
3236 *
3237 * @since 2.2.0
3238 *
3239 * @global wpdb $wpdb WordPress database abstraction object.
3240 *
3241 * @param array $args {
3242 * Method arguments. Note: arguments must be ordered as documented.
3243 *
3244 * @type int $0 Blog ID (unused).
3245 * @type string $1 Username.
3246 * @type string $2 Password.
3247 * }
3248 * @return array|IXR_Error
3249 */
3250 public function wp_getPageList( $args ) {
3251 global $wpdb;
3252
3253 $this->escape( $args );
3254
3255 $username = $args[1];
3256 $password = $args[2];
3257
3258 $user = $this->login( $username, $password );
3259 if ( ! $user ) {
3260 return $this->error;
3261 }
3262
3263 if ( ! current_user_can( 'edit_pages' ) ) {
3264 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit pages.' ) );
3265 }
3266
3267 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3268 do_action( 'xmlrpc_call', 'wp.getPageList', $args, $this );
3269
3270 // Get list of page IDs and titles.
3271 $page_list = $wpdb->get_results(
3272 "
3273 SELECT ID page_id,
3274 post_title page_title,
3275 post_parent page_parent_id,
3276 post_date_gmt,
3277 post_date,
3278 post_status
3279 FROM {$wpdb->posts}
3280 WHERE post_type = 'page'
3281 ORDER BY ID
3282 "
3283 );
3284
3285 // The date needs to be formatted properly.
3286 $num_pages = count( $page_list );
3287 for ( $i = 0; $i < $num_pages; $i++ ) {
3288 $page_list[ $i ]->dateCreated = $this->_convert_date( $page_list[ $i ]->post_date );
3289 $page_list[ $i ]->date_created_gmt = $this->_convert_date_gmt( $page_list[ $i ]->post_date_gmt, $page_list[ $i ]->post_date );
3290
3291 unset( $page_list[ $i ]->post_date_gmt );
3292 unset( $page_list[ $i ]->post_date );
3293 unset( $page_list[ $i ]->post_status );
3294 }
3295
3296 return $page_list;
3297 }
3298
3299 /**
3300 * Retrieves authors list.
3301 *
3302 * @since 2.2.0
3303 *
3304 * @param array $args {
3305 * Method arguments. Note: arguments must be ordered as documented.
3306 *
3307 * @type int $0 Blog ID (unused).
3308 * @type string $1 Username.
3309 * @type string $2 Password.
3310 * }
3311 * @return array|IXR_Error
3312 */
3313 public function wp_getAuthors( $args ) {
3314 $this->escape( $args );
3315
3316 $username = $args[1];
3317 $password = $args[2];
3318
3319 $user = $this->login( $username, $password );
3320 if ( ! $user ) {
3321 return $this->error;
3322 }
3323
3324 if ( ! current_user_can( 'edit_posts' ) ) {
3325 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts.' ) );
3326 }
3327
3328 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3329 do_action( 'xmlrpc_call', 'wp.getAuthors', $args, $this );
3330
3331 $authors = array();
3332 foreach ( get_users( array( 'fields' => array( 'ID', 'user_login', 'display_name' ) ) ) as $user ) {
3333 $authors[] = array(
3334 'user_id' => $user->ID,
3335 'user_login' => $user->user_login,
3336 'display_name' => $user->display_name,
3337 );
3338 }
3339
3340 return $authors;
3341 }
3342
3343 /**
3344 * Gets the list of all tags.
3345 *
3346 * @since 2.7.0
3347 *
3348 * @param array $args {
3349 * Method arguments. Note: arguments must be ordered as documented.
3350 *
3351 * @type int $0 Blog ID (unused).
3352 * @type string $1 Username.
3353 * @type string $2 Password.
3354 * }
3355 * @return array|IXR_Error
3356 */
3357 public function wp_getTags( $args ) {
3358 $this->escape( $args );
3359
3360 $username = $args[1];
3361 $password = $args[2];
3362
3363 $user = $this->login( $username, $password );
3364 if ( ! $user ) {
3365 return $this->error;
3366 }
3367
3368 if ( ! current_user_can( 'edit_posts' ) ) {
3369 return new IXR_Error( 401, __( 'Sorry, you must be able to edit posts on this site in order to view tags.' ) );
3370 }
3371
3372 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3373 do_action( 'xmlrpc_call', 'wp.getKeywords', $args, $this );
3374
3375 $tags = array();
3376
3377 $all_tags = get_tags();
3378 if ( $all_tags ) {
3379 foreach ( (array) $all_tags as $tag ) {
3380 $struct = array();
3381 $struct['tag_id'] = $tag->term_id;
3382 $struct['name'] = $tag->name;
3383 $struct['count'] = $tag->count;
3384 $struct['slug'] = $tag->slug;
3385 $struct['html_url'] = esc_html( get_tag_link( $tag->term_id ) );
3386 $struct['rss_url'] = esc_html( get_tag_feed_link( $tag->term_id ) );
3387
3388 $tags[] = $struct;
3389 }
3390 }
3391
3392 return $tags;
3393 }
3394
3395 /**
3396 * Creates a new category.
3397 *
3398 * @since 2.2.0
3399 *
3400 * @param array $args {
3401 * Method arguments. Note: arguments must be ordered as documented.
3402 *
3403 * @type int $0 Blog ID (unused).
3404 * @type string $1 Username.
3405 * @type string $2 Password.
3406 * @type array $3 Category.
3407 * }
3408 * @return int|IXR_Error Category ID.
3409 */
3410 public function wp_newCategory( $args ) {
3411 $this->escape( $args );
3412
3413 $username = $args[1];
3414 $password = $args[2];
3415 $category = $args[3];
3416
3417 $user = $this->login( $username, $password );
3418 if ( ! $user ) {
3419 return $this->error;
3420 }
3421
3422 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3423 do_action( 'xmlrpc_call', 'wp.newCategory', $args, $this );
3424
3425 // Make sure the user is allowed to add a category.
3426 if ( ! current_user_can( 'manage_categories' ) ) {
3427 return new IXR_Error( 401, __( 'Sorry, you are not allowed to add a category.' ) );
3428 }
3429
3430 /*
3431 * If no slug was provided, make it empty
3432 * so that WordPress will generate one.
3433 */
3434 if ( empty( $category['slug'] ) ) {
3435 $category['slug'] = '';
3436 }
3437
3438 /*
3439 * If no parent_id was provided, make it empty
3440 * so that it will be a top-level page (no parent).
3441 */
3442 if ( ! isset( $category['parent_id'] ) ) {
3443 $category['parent_id'] = '';
3444 }
3445
3446 // If no description was provided, make it empty.
3447 if ( empty( $category['description'] ) ) {
3448 $category['description'] = '';
3449 }
3450
3451 $new_category = array(
3452 'cat_name' => $category['name'],
3453 'category_nicename' => $category['slug'],
3454 'category_parent' => $category['parent_id'],
3455 'category_description' => $category['description'],
3456 );
3457
3458 $cat_id = wp_insert_category( $new_category, true );
3459 if ( is_wp_error( $cat_id ) ) {
3460 if ( 'term_exists' === $cat_id->get_error_code() ) {
3461 return (int) $cat_id->get_error_data();
3462 } else {
3463 return new IXR_Error( 500, __( 'Sorry, the category could not be created.' ) );
3464 }
3465 } elseif ( ! $cat_id ) {
3466 return new IXR_Error( 500, __( 'Sorry, the category could not be created.' ) );
3467 }
3468
3469 /**
3470 * Fires after a new category has been successfully created via XML-RPC.
3471 *
3472 * @since 3.4.0
3473 *
3474 * @param int $cat_id ID of the new category.
3475 * @param array $args An array of new category arguments.
3476 */
3477 do_action( 'xmlrpc_call_success_wp_newCategory', $cat_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
3478
3479 return $cat_id;
3480 }
3481
3482 /**
3483 * Deletes a category.
3484 *
3485 * @since 2.5.0
3486 *
3487 * @param array $args {
3488 * Method arguments. Note: arguments must be ordered as documented.
3489 *
3490 * @type int $0 Blog ID (unused).
3491 * @type string $1 Username.
3492 * @type string $2 Password.
3493 * @type int $3 Category ID.
3494 * }
3495 * @return bool|IXR_Error See wp_delete_term() for return info.
3496 */
3497 public function wp_deleteCategory( $args ) {
3498 $this->escape( $args );
3499
3500 $username = $args[1];
3501 $password = $args[2];
3502 $category_id = (int) $args[3];
3503
3504 $user = $this->login( $username, $password );
3505 if ( ! $user ) {
3506 return $this->error;
3507 }
3508
3509 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3510 do_action( 'xmlrpc_call', 'wp.deleteCategory', $args, $this );
3511
3512 if ( ! current_user_can( 'delete_term', $category_id ) ) {
3513 return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this category.' ) );
3514 }
3515
3516 $status = wp_delete_term( $category_id, 'category' );
3517
3518 if ( true === $status ) {
3519 /**
3520 * Fires after a category has been successfully deleted via XML-RPC.
3521 *
3522 * @since 3.4.0
3523 *
3524 * @param int $category_id ID of the deleted category.
3525 * @param array $args An array of arguments to delete the category.
3526 */
3527 do_action( 'xmlrpc_call_success_wp_deleteCategory', $category_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
3528 }
3529
3530 return $status;
3531 }
3532
3533 /**
3534 * Retrieves category list.
3535 *
3536 * @since 2.2.0
3537 *
3538 * @param array $args {
3539 * Method arguments. Note: arguments must be ordered as documented.
3540 *
3541 * @type int $0 Blog ID (unused).
3542 * @type string $1 Username.
3543 * @type string $2 Password.
3544 * @type array $3 Category
3545 * @type int $4 Max number of results.
3546 * }
3547 * @return array|IXR_Error
3548 */
3549 public function wp_suggestCategories( $args ) {
3550 $this->escape( $args );
3551
3552 $username = $args[1];
3553 $password = $args[2];
3554 $category = $args[3];
3555 $max_results = (int) $args[4];
3556
3557 $user = $this->login( $username, $password );
3558 if ( ! $user ) {
3559 return $this->error;
3560 }
3561
3562 if ( ! current_user_can( 'edit_posts' ) ) {
3563 return new IXR_Error( 401, __( 'Sorry, you must be able to edit posts on this site in order to view categories.' ) );
3564 }
3565
3566 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3567 do_action( 'xmlrpc_call', 'wp.suggestCategories', $args, $this );
3568
3569 $category_suggestions = array();
3570 $args = array(
3571 'get' => 'all',
3572 'number' => $max_results,
3573 'name__like' => $category,
3574 );
3575 foreach ( (array) get_categories( $args ) as $cat ) {
3576 $category_suggestions[] = array(
3577 'category_id' => $cat->term_id,
3578 'category_name' => $cat->name,
3579 );
3580 }
3581
3582 return $category_suggestions;
3583 }
3584
3585 /**
3586 * Retrieves a comment.
3587 *
3588 * @since 2.7.0
3589 *
3590 * @param array $args {
3591 * Method arguments. Note: arguments must be ordered as documented.
3592 *
3593 * @type int $0 Blog ID (unused).
3594 * @type string $1 Username.
3595 * @type string $2 Password.
3596 * @type int $3 Comment ID.
3597 * }
3598 * @return array|IXR_Error
3599 */
3600 public function wp_getComment( $args ) {
3601 $this->escape( $args );
3602
3603 $username = $args[1];
3604 $password = $args[2];
3605 $comment_id = (int) $args[3];
3606
3607 $user = $this->login( $username, $password );
3608 if ( ! $user ) {
3609 return $this->error;
3610 }
3611
3612 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3613 do_action( 'xmlrpc_call', 'wp.getComment', $args, $this );
3614
3615 $comment = get_comment( $comment_id );
3616 if ( ! $comment ) {
3617 return new IXR_Error( 404, __( 'Invalid comment ID.' ) );
3618 }
3619
3620 if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
3621 return new IXR_Error( 403, __( 'Sorry, you are not allowed to moderate or edit this comment.' ) );
3622 }
3623
3624 return $this->_prepare_comment( $comment );
3625 }
3626
3627 /**
3628 * Retrieves comments.
3629 *
3630 * Besides the common blog_id (unused), username, and password arguments,
3631 * it takes a filter array as the last argument.
3632 *
3633 * Accepted 'filter' keys are 'status', 'post_id', 'offset', and 'number'.
3634 *
3635 * The defaults are as follows:
3636 * - 'status' - Default is ''. Filter by status (e.g., 'approve', 'hold')
3637 * - 'post_id' - Default is ''. The post where the comment is posted.
3638 * Empty string shows all comments.
3639 * - 'number' - Default is 10. Total number of media items to retrieve.
3640 * - 'offset' - Default is 0. See WP_Query::query() for more.
3641 *
3642 * @since 2.7.0
3643 *
3644 * @param array $args {
3645 * Method arguments. Note: arguments must be ordered as documented.
3646 *
3647 * @type int $0 Blog ID (unused).
3648 * @type string $1 Username.
3649 * @type string $2 Password.
3650 * @type array $3 Optional. Query arguments.
3651 * }
3652 * @return array|IXR_Error Array containing a collection of comments.
3653 * See wp_xmlrpc_server::wp_getComment() for a description
3654 * of each item contents.
3655 */
3656 public function wp_getComments( $args ) {
3657 $this->escape( $args );
3658
3659 $username = $args[1];
3660 $password = $args[2];
3661 $struct = isset( $args[3] ) ? $args[3] : array();
3662
3663 $user = $this->login( $username, $password );
3664 if ( ! $user ) {
3665 return $this->error;
3666 }
3667
3668 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3669 do_action( 'xmlrpc_call', 'wp.getComments', $args, $this );
3670
3671 if ( isset( $struct['status'] ) ) {
3672 $status = $struct['status'];
3673 } else {
3674 $status = '';
3675 }
3676
3677 if ( ! current_user_can( 'moderate_comments' ) && 'approve' !== $status ) {
3678 return new IXR_Error( 401, __( 'Invalid comment status.' ) );
3679 }
3680
3681 $post_id = '';
3682 if ( isset( $struct['post_id'] ) ) {
3683 $post_id = absint( $struct['post_id'] );
3684 }
3685
3686 $post_type = '';
3687 if ( isset( $struct['post_type'] ) ) {
3688 $post_type_object = get_post_type_object( $struct['post_type'] );
3689 if ( ! $post_type_object || ! post_type_supports( $post_type_object->name, 'comments' ) ) {
3690 return new IXR_Error( 404, __( 'Invalid post type.' ) );
3691 }
3692 $post_type = $struct['post_type'];
3693 }
3694
3695 $offset = 0;
3696 if ( isset( $struct['offset'] ) ) {
3697 $offset = absint( $struct['offset'] );
3698 }
3699
3700 $number = 10;
3701 if ( isset( $struct['number'] ) ) {
3702 $number = absint( $struct['number'] );
3703 }
3704
3705 $comments = get_comments(
3706 array(
3707 'status' => $status,
3708 'post_id' => $post_id,
3709 'offset' => $offset,
3710 'number' => $number,
3711 'post_type' => $post_type,
3712 )
3713 );
3714
3715 $comments_struct = array();
3716 if ( is_array( $comments ) ) {
3717 foreach ( $comments as $comment ) {
3718 $comments_struct[] = $this->_prepare_comment( $comment );
3719 }
3720 }
3721
3722 return $comments_struct;
3723 }
3724
3725 /**
3726 * Deletes a comment.
3727 *
3728 * By default, the comment will be moved to the Trash instead of deleted.
3729 * See wp_delete_comment() for more information on this behavior.
3730 *
3731 * @since 2.7.0
3732 *
3733 * @param array $args {
3734 * Method arguments. Note: arguments must be ordered as documented.
3735 *
3736 * @type int $0 Blog ID (unused).
3737 * @type string $1 Username.
3738 * @type string $2 Password.
3739 * @type int $3 Comment ID.
3740 * }
3741 * @return bool|IXR_Error See wp_delete_comment().
3742 */
3743 public function wp_deleteComment( $args ) {
3744 $this->escape( $args );
3745
3746 $username = $args[1];
3747 $password = $args[2];
3748 $comment_id = (int) $args[3];
3749
3750 $user = $this->login( $username, $password );
3751 if ( ! $user ) {
3752 return $this->error;
3753 }
3754
3755 if ( ! get_comment( $comment_id ) ) {
3756 return new IXR_Error( 404, __( 'Invalid comment ID.' ) );
3757 }
3758
3759 if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
3760 return new IXR_Error( 403, __( 'Sorry, you are not allowed to delete this comment.' ) );
3761 }
3762
3763 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3764 do_action( 'xmlrpc_call', 'wp.deleteComment', $args, $this );
3765
3766 $status = wp_delete_comment( $comment_id );
3767
3768 if ( true === $status ) {
3769 /**
3770 * Fires after a comment has been successfully deleted via XML-RPC.
3771 *
3772 * @since 3.4.0
3773 *
3774 * @param int $comment_id ID of the deleted comment.
3775 * @param array $args An array of arguments to delete the comment.
3776 */
3777 do_action( 'xmlrpc_call_success_wp_deleteComment', $comment_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
3778 }
3779
3780 return $status;
3781 }
3782
3783 /**
3784 * Edits a comment.
3785 *
3786 * Besides the common blog_id (unused), username, and password arguments,
3787 * it takes a comment_id integer and a content_struct array as the last argument.
3788 *
3789 * The allowed keys in the content_struct array are:
3790 * - 'author'
3791 * - 'author_url'
3792 * - 'author_email'
3793 * - 'content'
3794 * - 'date_created_gmt'
3795 * - 'status'. Common statuses are 'approve', 'hold', 'spam'. See get_comment_statuses() for more details.
3796 *
3797 * @since 2.7.0
3798 *
3799 * @param array $args {
3800 * Method arguments. Note: arguments must be ordered as documented.
3801 *
3802 * @type int $0 Blog ID (unused).
3803 * @type string $1 Username.
3804 * @type string $2 Password.
3805 * @type int $3 Comment ID.
3806 * @type array $4 Content structure.
3807 * }
3808 * @return true|IXR_Error True, on success.
3809 */
3810 public function wp_editComment( $args ) {
3811 $this->escape( $args );
3812
3813 $username = $args[1];
3814 $password = $args[2];
3815 $comment_id = (int) $args[3];
3816 $content_struct = $args[4];
3817
3818 $user = $this->login( $username, $password );
3819 if ( ! $user ) {
3820 return $this->error;
3821 }
3822
3823 if ( ! get_comment( $comment_id ) ) {
3824 return new IXR_Error( 404, __( 'Invalid comment ID.' ) );
3825 }
3826
3827 if ( ! current_user_can( 'edit_comment', $comment_id ) ) {
3828 return new IXR_Error( 403, __( 'Sorry, you are not allowed to moderate or edit this comment.' ) );
3829 }
3830
3831 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
3832 do_action( 'xmlrpc_call', 'wp.editComment', $args, $this );
3833 $comment = array(
3834 'comment_ID' => $comment_id,
3835 );
3836
3837 if ( isset( $content_struct['status'] ) ) {
3838 $statuses = get_comment_statuses();
3839 $statuses = array_keys( $statuses );
3840
3841 if ( ! in_array( $content_struct['status'], $statuses, true ) ) {
3842 return new IXR_Error( 401, __( 'Invalid comment status.' ) );
3843 }
3844
3845 $comment['comment_approved'] = $content_struct['status'];
3846 }
3847
3848 // Do some timestamp voodoo.
3849 if ( ! empty( $content_struct['date_created_gmt'] ) ) {
3850 // We know this is supposed to be GMT, so we're going to slap that Z on there by force.
3851 $date_created = rtrim( $content_struct['date_created_gmt']->getIso(), 'Z' ) . 'Z';
3852
3853 $comment['comment_date'] = get_date_from_gmt( $date_created );
3854 $comment['comment_date_gmt'] = iso8601_to_datetime( $date_created, 'gmt' );
3855 }
3856
3857 if ( isset( $content_struct['content'] ) ) {
3858 $comment['comment_content'] = $content_struct['content'];
3859 }
3860
3861 if ( isset( $content_struct['author'] ) ) {
3862 $comment['comment_author'] = $content_struct['author'];
3863 }
3864
3865 if ( isset( $content_struct['author_url'] ) ) {
3866 $comment['comment_author_url'] = $content_struct['author_url'];
3867 }
3868
3869 if ( isset( $content_struct['author_email'] ) ) {
3870 $comment['comment_author_email'] = $content_struct['author_email'];
3871 }
3872
3873 $result = wp_update_comment( $comment, true );
3874 if ( is_wp_error( $result ) ) {
3875 return new IXR_Error( 500, $result->get_error_message() );
3876 }
3877
3878 if ( ! $result ) {
3879 return new IXR_Error( 500, __( 'Sorry, the comment could not be updated.' ) );
3880 }
3881
3882 /**
3883 * Fires after a comment has been successfully updated via XML-RPC.
3884 *
3885 * @since 3.4.0
3886 *
3887 * @param int $comment_id ID of the updated comment.
3888 * @param array $args An array of arguments to update the comment.
3889 */
3890 do_action( 'xmlrpc_call_success_wp_editComment', $comment_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
3891
3892 return true;
3893 }
3894
3895 /**
3896 * Creates a new comment.
3897 *
3898 * @since 2.7.0
3899 *
3900 * @param array $args {
3901 * Method arguments. Note: arguments must be ordered as documented.
3902 *
3903 * @type int $0 Blog ID (unused).
3904 * @type string $1 Username.
3905 * @type string $2 Password.
3906 * @type string|int $3 Post ID or URL.
3907 * @type array $4 Content structure.
3908 * }
3909 * @return int|IXR_Error See wp_new_comment().
3910 */
3911 public function wp_newComment( $args ) {
3912 $this->escape( $args );
3913
3914 $username = $args[1];
3915 $password = $args[2];
3916 $post = $args[3];
3917 $content_struct = $args[4];
3918
3919 /**
3920 * Filters whether to allow anonymous comments over XML-RPC.
3921 *
3922 * @since 2.7.0
3923 *
3924 * @param bool $allow Whether to allow anonymous commenting via XML-RPC.
3925 * Default false.
3926 */
3927 $allow_anon = apply_filters( 'xmlrpc_allow_anonymous_comments', false );
3928
3929 $user = $this->login( $username, $password );
3930
3931 if ( ! $user ) {
3932 $logged_in = false;
3933 if ( $allow_anon && get_option( 'comment_registration' ) ) {
3934 return new IXR_Error( 403, __( 'Sorry, you must be logged in to comment.' ) );
3935 } elseif ( ! $allow_anon ) {
3936 return $this->error;
3937 }
3938 } else {
3939 $logged_in = true;
3940 }
3941
3942 if ( is_numeric( $post ) ) {
3943 $post_id = absint( $post );
3944 } else {
3945 $post_id = url_to_postid( $post );
3946 }
3947
3948 if ( ! $post_id ) {
3949 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
3950 }
3951
3952 if ( ! get_post( $post_id ) ) {
3953 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
3954 }
3955
3956 if ( ! comments_open( $post_id ) ) {
3957 return new IXR_Error( 403, __( 'Sorry, comments are closed for this item.' ) );
3958 }
3959
3960 if (
3961 'publish' === get_post_status( $post_id ) &&
3962 ! current_user_can( 'edit_post', $post_id ) &&
3963 post_password_required( $post_id )
3964 ) {
3965 return new IXR_Error( 403, __( 'Sorry, you are not allowed to comment on this post.' ) );
3966 }
3967
3968 if (
3969 'private' === get_post_status( $post_id ) &&
3970 ! current_user_can( 'read_post', $post_id )
3971 ) {
3972 return new IXR_Error( 403, __( 'Sorry, you are not allowed to comment on this post.' ) );
3973 }
3974
3975 $comment = array(
3976 'comment_post_ID' => $post_id,
3977 'comment_content' => trim( $content_struct['content'] ),
3978 );
3979
3980 if ( $logged_in ) {
3981 $display_name = $user->display_name;
3982 $user_email = $user->user_email;
3983 $user_url = $user->user_url;
3984
3985 $comment['comment_author'] = $this->escape( $display_name );
3986 $comment['comment_author_email'] = $this->escape( $user_email );
3987 $comment['comment_author_url'] = $this->escape( $user_url );
3988 $comment['user_id'] = $user->ID;
3989 } else {
3990 $comment['comment_author'] = '';
3991 if ( isset( $content_struct['author'] ) ) {
3992 $comment['comment_author'] = $content_struct['author'];
3993 }
3994
3995 $comment['comment_author_email'] = '';
3996 if ( isset( $content_struct['author_email'] ) ) {
3997 $comment['comment_author_email'] = $content_struct['author_email'];
3998 }
3999
4000 $comment['comment_author_url'] = '';
4001 if ( isset( $content_struct['author_url'] ) ) {
4002 $comment['comment_author_url'] = $content_struct['author_url'];
4003 }
4004
4005 $comment['user_id'] = 0;
4006
4007 if ( get_option( 'require_name_email' ) ) {
4008 if ( strlen( $comment['comment_author_email'] ) < 6 || '' === $comment['comment_author'] ) {
4009 return new IXR_Error( 403, __( 'Comment author name and email are required.' ) );
4010 } elseif ( ! is_email( $comment['comment_author_email'] ) ) {
4011 return new IXR_Error( 403, __( 'A valid email address is required.' ) );
4012 }
4013 }
4014 }
4015
4016 $comment['comment_parent'] = isset( $content_struct['comment_parent'] ) ? absint( $content_struct['comment_parent'] ) : 0;
4017
4018 /** This filter is documented in wp-includes/comment.php */
4019 $allow_empty = apply_filters( 'allow_empty_comment', false, $comment );
4020
4021 if ( ! $allow_empty && '' === $comment['comment_content'] ) {
4022 return new IXR_Error( 403, __( 'Comment is required.' ) );
4023 }
4024
4025 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4026 do_action( 'xmlrpc_call', 'wp.newComment', $args, $this );
4027
4028 $comment_id = wp_new_comment( $comment, true );
4029 if ( is_wp_error( $comment_id ) ) {
4030 return new IXR_Error( 403, $comment_id->get_error_message() );
4031 }
4032
4033 if ( ! $comment_id ) {
4034 return new IXR_Error( 403, __( 'An error occurred while processing your comment. Please ensure all fields are filled correctly and try again.' ) );
4035 }
4036
4037 /**
4038 * Fires after a new comment has been successfully created via XML-RPC.
4039 *
4040 * @since 3.4.0
4041 *
4042 * @param int $comment_id ID of the new comment.
4043 * @param array $args An array of new comment arguments.
4044 */
4045 do_action( 'xmlrpc_call_success_wp_newComment', $comment_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
4046
4047 return $comment_id;
4048 }
4049
4050 /**
4051 * Retrieves all of the comment status.
4052 *
4053 * @since 2.7.0
4054 *
4055 * @param array $args {
4056 * Method arguments. Note: arguments must be ordered as documented.
4057 *
4058 * @type int $0 Blog ID (unused).
4059 * @type string $1 Username.
4060 * @type string $2 Password.
4061 * }
4062 * @return array|IXR_Error
4063 */
4064 public function wp_getCommentStatusList( $args ) {
4065 $this->escape( $args );
4066
4067 $username = $args[1];
4068 $password = $args[2];
4069
4070 $user = $this->login( $username, $password );
4071 if ( ! $user ) {
4072 return $this->error;
4073 }
4074
4075 if ( ! current_user_can( 'publish_posts' ) ) {
4076 return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) );
4077 }
4078
4079 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4080 do_action( 'xmlrpc_call', 'wp.getCommentStatusList', $args, $this );
4081
4082 return get_comment_statuses();
4083 }
4084
4085 /**
4086 * Retrieves comment counts.
4087 *
4088 * @since 2.5.0
4089 *
4090 * @param array $args {
4091 * Method arguments. Note: arguments must be ordered as documented.
4092 *
4093 * @type int $0 Blog ID (unused).
4094 * @type string $1 Username.
4095 * @type string $2 Password.
4096 * @type int $3 Post ID.
4097 * }
4098 * @return array|IXR_Error
4099 */
4100 public function wp_getCommentCount( $args ) {
4101 $this->escape( $args );
4102
4103 $username = $args[1];
4104 $password = $args[2];
4105 $post_id = (int) $args[3];
4106
4107 $user = $this->login( $username, $password );
4108 if ( ! $user ) {
4109 return $this->error;
4110 }
4111
4112 $post = get_post( $post_id, ARRAY_A );
4113 if ( empty( $post['ID'] ) ) {
4114 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
4115 }
4116
4117 if ( ! current_user_can( 'edit_post', $post_id ) ) {
4118 return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details of this post.' ) );
4119 }
4120
4121 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4122 do_action( 'xmlrpc_call', 'wp.getCommentCount', $args, $this );
4123
4124 $count = wp_count_comments( $post_id );
4125
4126 return array(
4127 'approved' => $count->approved,
4128 'awaiting_moderation' => $count->moderated,
4129 'spam' => $count->spam,
4130 'total_comments' => $count->total_comments,
4131 );
4132 }
4133
4134 /**
4135 * Retrieves post statuses.
4136 *
4137 * @since 2.5.0
4138 *
4139 * @param array $args {
4140 * Method arguments. Note: arguments must be ordered as documented.
4141 *
4142 * @type int $0 Blog ID (unused).
4143 * @type string $1 Username.
4144 * @type string $2 Password.
4145 * }
4146 * @return array|IXR_Error
4147 */
4148 public function wp_getPostStatusList( $args ) {
4149 $this->escape( $args );
4150
4151 $username = $args[1];
4152 $password = $args[2];
4153
4154 $user = $this->login( $username, $password );
4155 if ( ! $user ) {
4156 return $this->error;
4157 }
4158
4159 if ( ! current_user_can( 'edit_posts' ) ) {
4160 return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) );
4161 }
4162
4163 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4164 do_action( 'xmlrpc_call', 'wp.getPostStatusList', $args, $this );
4165
4166 return get_post_statuses();
4167 }
4168
4169 /**
4170 * Retrieves page statuses.
4171 *
4172 * @since 2.5.0
4173 *
4174 * @param array $args {
4175 * Method arguments. Note: arguments must be ordered as documented.
4176 *
4177 * @type int $0 Blog ID (unused).
4178 * @type string $1 Username.
4179 * @type string $2 Password.
4180 * }
4181 * @return array|IXR_Error
4182 */
4183 public function wp_getPageStatusList( $args ) {
4184 $this->escape( $args );
4185
4186 $username = $args[1];
4187 $password = $args[2];
4188
4189 $user = $this->login( $username, $password );
4190 if ( ! $user ) {
4191 return $this->error;
4192 }
4193
4194 if ( ! current_user_can( 'edit_pages' ) ) {
4195 return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) );
4196 }
4197
4198 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4199 do_action( 'xmlrpc_call', 'wp.getPageStatusList', $args, $this );
4200
4201 return get_page_statuses();
4202 }
4203
4204 /**
4205 * Retrieves page templates.
4206 *
4207 * @since 2.6.0
4208 *
4209 * @param array $args {
4210 * Method arguments. Note: arguments must be ordered as documented.
4211 *
4212 * @type int $0 Blog ID (unused).
4213 * @type string $1 Username.
4214 * @type string $2 Password.
4215 * }
4216 * @return array|IXR_Error
4217 */
4218 public function wp_getPageTemplates( $args ) {
4219 $this->escape( $args );
4220
4221 $username = $args[1];
4222 $password = $args[2];
4223
4224 $user = $this->login( $username, $password );
4225 if ( ! $user ) {
4226 return $this->error;
4227 }
4228
4229 if ( ! current_user_can( 'edit_pages' ) ) {
4230 return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) );
4231 }
4232
4233 $templates = get_page_templates();
4234 $templates['Default'] = 'default';
4235
4236 return $templates;
4237 }
4238
4239 /**
4240 * Retrieves blog options.
4241 *
4242 * @since 2.6.0
4243 *
4244 * @param array $args {
4245 * Method arguments. Note: arguments must be ordered as documented.
4246 *
4247 * @type int $0 Blog ID (unused).
4248 * @type string $1 Username.
4249 * @type string $2 Password.
4250 * @type array $3 Optional. Options.
4251 * }
4252 * @return array|IXR_Error
4253 */
4254 public function wp_getOptions( $args ) {
4255 $this->escape( $args );
4256
4257 $username = $args[1];
4258 $password = $args[2];
4259 $options = isset( $args[3] ) ? (array) $args[3] : array();
4260
4261 $user = $this->login( $username, $password );
4262 if ( ! $user ) {
4263 return $this->error;
4264 }
4265
4266 // If no specific options where asked for, return all of them.
4267 if ( count( $options ) === 0 ) {
4268 $options = array_keys( $this->blog_options );
4269 }
4270
4271 return $this->_getOptions( $options );
4272 }
4273
4274 /**
4275 * Retrieves blog options value from list.
4276 *
4277 * @since 2.6.0
4278 *
4279 * @param array $options Options to retrieve.
4280 * @return array
4281 */
4282 public function _getOptions( $options ) {
4283 $data = array();
4284 $can_manage = current_user_can( 'manage_options' );
4285 foreach ( $options as $option ) {
4286 if ( array_key_exists( $option, $this->blog_options ) ) {
4287 $data[ $option ] = $this->blog_options[ $option ];
4288 // Is the value static or dynamic?
4289 if ( isset( $data[ $option ]['option'] ) ) {
4290 $data[ $option ]['value'] = get_option( $data[ $option ]['option'] );
4291 unset( $data[ $option ]['option'] );
4292 }
4293
4294 if ( ! $can_manage ) {
4295 $data[ $option ]['readonly'] = true;
4296 }
4297 }
4298 }
4299
4300 return $data;
4301 }
4302
4303 /**
4304 * Updates blog options.
4305 *
4306 * @since 2.6.0
4307 *
4308 * @param array $args {
4309 * Method arguments. Note: arguments must be ordered as documented.
4310 *
4311 * @type int $0 Blog ID (unused).
4312 * @type string $1 Username.
4313 * @type string $2 Password.
4314 * @type array $3 Options.
4315 * }
4316 * @return array|IXR_Error
4317 */
4318 public function wp_setOptions( $args ) {
4319 $this->escape( $args );
4320
4321 $username = $args[1];
4322 $password = $args[2];
4323 $options = (array) $args[3];
4324
4325 $user = $this->login( $username, $password );
4326 if ( ! $user ) {
4327 return $this->error;
4328 }
4329
4330 if ( ! current_user_can( 'manage_options' ) ) {
4331 return new IXR_Error( 403, __( 'Sorry, you are not allowed to update options.' ) );
4332 }
4333
4334 $option_names = array();
4335 foreach ( $options as $o_name => $o_value ) {
4336 $option_names[] = $o_name;
4337 if ( ! array_key_exists( $o_name, $this->blog_options ) ) {
4338 continue;
4339 }
4340
4341 if ( $this->blog_options[ $o_name ]['readonly'] ) {
4342 continue;
4343 }
4344
4345 update_option( $this->blog_options[ $o_name ]['option'], wp_unslash( $o_value ) );
4346 }
4347
4348 // Now return the updated values.
4349 return $this->_getOptions( $option_names );
4350 }
4351
4352 /**
4353 * Retrieves a media item by ID.
4354 *
4355 * @since 3.1.0
4356 *
4357 * @param array $args {
4358 * Method arguments. Note: arguments must be ordered as documented.
4359 *
4360 * @type int $0 Blog ID (unused).
4361 * @type string $1 Username.
4362 * @type string $2 Password.
4363 * @type int $3 Attachment ID.
4364 * }
4365 * @return array|IXR_Error Associative array contains:
4366 * - 'date_created_gmt'
4367 * - 'parent'
4368 * - 'link'
4369 * - 'thumbnail'
4370 * - 'title'
4371 * - 'caption'
4372 * - 'description'
4373 * - 'metadata'
4374 */
4375 public function wp_getMediaItem( $args ) {
4376 $this->escape( $args );
4377
4378 $username = $args[1];
4379 $password = $args[2];
4380 $attachment_id = (int) $args[3];
4381
4382 $user = $this->login( $username, $password );
4383 if ( ! $user ) {
4384 return $this->error;
4385 }
4386
4387 if ( ! current_user_can( 'upload_files' ) ) {
4388 return new IXR_Error( 403, __( 'Sorry, you are not allowed to upload files.' ) );
4389 }
4390
4391 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4392 do_action( 'xmlrpc_call', 'wp.getMediaItem', $args, $this );
4393
4394 $attachment = get_post( $attachment_id );
4395 if ( ! $attachment || 'attachment' !== $attachment->post_type ) {
4396 return new IXR_Error( 404, __( 'Invalid attachment ID.' ) );
4397 }
4398
4399 return $this->_prepare_media_item( $attachment );
4400 }
4401
4402 /**
4403 * Retrieves a collection of media library items (or attachments).
4404 *
4405 * Besides the common blog_id (unused), username, and password arguments,
4406 * it takes a filter array as the last argument.
4407 *
4408 * Accepted 'filter' keys are 'parent_id', 'mime_type', 'offset', and 'number'.
4409 *
4410 * The defaults are as follows:
4411 * - 'number' - Default is 5. Total number of media items to retrieve.
4412 * - 'offset' - Default is 0. See WP_Query::query() for more.
4413 * - 'parent_id' - Default is ''. The post where the media item is attached.
4414 * Empty string shows all media items. 0 shows unattached media items.
4415 * - 'mime_type' - Default is ''. Filter by mime type (e.g., 'image/jpeg', 'application/pdf')
4416 *
4417 * @since 3.1.0
4418 *
4419 * @param array $args {
4420 * Method arguments. Note: arguments must be ordered as documented.
4421 *
4422 * @type int $0 Blog ID (unused).
4423 * @type string $1 Username.
4424 * @type string $2 Password.
4425 * @type array $3 Optional. Query arguments.
4426 * }
4427 * @return array|IXR_Error Array containing a collection of media items.
4428 * See wp_xmlrpc_server::wp_getMediaItem() for a description
4429 * of each item contents.
4430 */
4431 public function wp_getMediaLibrary( $args ) {
4432 $this->escape( $args );
4433
4434 $username = $args[1];
4435 $password = $args[2];
4436 $struct = isset( $args[3] ) ? $args[3] : array();
4437
4438 $user = $this->login( $username, $password );
4439 if ( ! $user ) {
4440 return $this->error;
4441 }
4442
4443 if ( ! current_user_can( 'upload_files' ) ) {
4444 return new IXR_Error( 401, __( 'Sorry, you are not allowed to upload files.' ) );
4445 }
4446
4447 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4448 do_action( 'xmlrpc_call', 'wp.getMediaLibrary', $args, $this );
4449
4450 $parent_id = ( isset( $struct['parent_id'] ) ) ? absint( $struct['parent_id'] ) : '';
4451 $mime_type = ( isset( $struct['mime_type'] ) ) ? $struct['mime_type'] : '';
4452 $offset = ( isset( $struct['offset'] ) ) ? absint( $struct['offset'] ) : 0;
4453 $number = ( isset( $struct['number'] ) ) ? absint( $struct['number'] ) : -1;
4454
4455 $attachments = get_posts(
4456 array(
4457 'post_type' => 'attachment',
4458 'post_parent' => $parent_id,
4459 'offset' => $offset,
4460 'numberposts' => $number,
4461 'post_mime_type' => $mime_type,
4462 )
4463 );
4464
4465 $attachments_struct = array();
4466
4467 foreach ( $attachments as $attachment ) {
4468 $attachments_struct[] = $this->_prepare_media_item( $attachment );
4469 }
4470
4471 return $attachments_struct;
4472 }
4473
4474 /**
4475 * Retrieves a list of post formats used by the site.
4476 *
4477 * @since 3.1.0
4478 *
4479 * @param array $args {
4480 * Method arguments. Note: arguments must be ordered as documented.
4481 *
4482 * @type int $0 Blog ID (unused).
4483 * @type string $1 Username.
4484 * @type string $2 Password.
4485 * }
4486 * @return array|IXR_Error List of post formats, otherwise IXR_Error object.
4487 */
4488 public function wp_getPostFormats( $args ) {
4489 $this->escape( $args );
4490
4491 $username = $args[1];
4492 $password = $args[2];
4493
4494 $user = $this->login( $username, $password );
4495 if ( ! $user ) {
4496 return $this->error;
4497 }
4498
4499 if ( ! current_user_can( 'edit_posts' ) ) {
4500 return new IXR_Error( 403, __( 'Sorry, you are not allowed to access details about this site.' ) );
4501 }
4502
4503 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4504 do_action( 'xmlrpc_call', 'wp.getPostFormats', $args, $this );
4505
4506 $formats = get_post_format_strings();
4507
4508 // Find out if they want a list of currently supports formats.
4509 if ( isset( $args[3] ) && is_array( $args[3] ) ) {
4510 if ( $args[3]['show-supported'] ) {
4511 if ( current_theme_supports( 'post-formats' ) ) {
4512 $supported = get_theme_support( 'post-formats' );
4513
4514 $data = array();
4515 $data['all'] = $formats;
4516 $data['supported'] = $supported[0];
4517
4518 $formats = $data;
4519 }
4520 }
4521 }
4522
4523 return $formats;
4524 }
4525
4526 /**
4527 * Retrieves a post type.
4528 *
4529 * @since 3.4.0
4530 *
4531 * @see get_post_type_object()
4532 *
4533 * @param array $args {
4534 * Method arguments. Note: arguments must be ordered as documented.
4535 *
4536 * @type int $0 Blog ID (unused).
4537 * @type string $1 Username.
4538 * @type string $2 Password.
4539 * @type string $3 Post type name.
4540 * @type array $4 Optional. Fields to fetch.
4541 * }
4542 * @return array|IXR_Error Array contains:
4543 * - 'labels'
4544 * - 'description'
4545 * - 'capability_type'
4546 * - 'cap'
4547 * - 'map_meta_cap'
4548 * - 'hierarchical'
4549 * - 'menu_position'
4550 * - 'taxonomies'
4551 * - 'supports'
4552 */
4553 public function wp_getPostType( $args ) {
4554 if ( ! $this->minimum_args( $args, 4 ) ) {
4555 return $this->error;
4556 }
4557
4558 $this->escape( $args );
4559
4560 $username = $args[1];
4561 $password = $args[2];
4562 $post_type_name = $args[3];
4563
4564 if ( isset( $args[4] ) ) {
4565 $fields = $args[4];
4566 } else {
4567 /**
4568 * Filters the default post type query fields used by the given XML-RPC method.
4569 *
4570 * @since 3.4.0
4571 *
4572 * @param array $fields An array of post type fields to retrieve. By default,
4573 * contains 'labels', 'cap', and 'taxonomies'.
4574 * @param string $method The method name.
4575 */
4576 $fields = apply_filters( 'xmlrpc_default_posttype_fields', array( 'labels', 'cap', 'taxonomies' ), 'wp.getPostType' );
4577 }
4578
4579 $user = $this->login( $username, $password );
4580 if ( ! $user ) {
4581 return $this->error;
4582 }
4583
4584 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4585 do_action( 'xmlrpc_call', 'wp.getPostType', $args, $this );
4586
4587 if ( ! post_type_exists( $post_type_name ) ) {
4588 return new IXR_Error( 403, __( 'Invalid post type.' ) );
4589 }
4590
4591 $post_type = get_post_type_object( $post_type_name );
4592
4593 if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
4594 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts in this post type.' ) );
4595 }
4596
4597 return $this->_prepare_post_type( $post_type, $fields );
4598 }
4599
4600 /**
4601 * Retrieves post types.
4602 *
4603 * @since 3.4.0
4604 *
4605 * @see get_post_types()
4606 *
4607 * @param array $args {
4608 * Method arguments. Note: arguments must be ordered as documented.
4609 *
4610 * @type int $0 Blog ID (unused).
4611 * @type string $1 Username.
4612 * @type string $2 Password.
4613 * @type array $3 Optional. Query arguments.
4614 * @type array $4 Optional. Fields to fetch.
4615 * }
4616 * @return array|IXR_Error
4617 */
4618 public function wp_getPostTypes( $args ) {
4619 if ( ! $this->minimum_args( $args, 3 ) ) {
4620 return $this->error;
4621 }
4622
4623 $this->escape( $args );
4624
4625 $username = $args[1];
4626 $password = $args[2];
4627 $filter = isset( $args[3] ) ? $args[3] : array( 'public' => true );
4628
4629 if ( isset( $args[4] ) ) {
4630 $fields = $args[4];
4631 } else {
4632 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4633 $fields = apply_filters( 'xmlrpc_default_posttype_fields', array( 'labels', 'cap', 'taxonomies' ), 'wp.getPostTypes' );
4634 }
4635
4636 $user = $this->login( $username, $password );
4637 if ( ! $user ) {
4638 return $this->error;
4639 }
4640
4641 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4642 do_action( 'xmlrpc_call', 'wp.getPostTypes', $args, $this );
4643
4644 $post_types = get_post_types( $filter, 'objects' );
4645
4646 $struct = array();
4647
4648 foreach ( $post_types as $post_type ) {
4649 if ( ! current_user_can( $post_type->cap->edit_posts ) ) {
4650 continue;
4651 }
4652
4653 $struct[ $post_type->name ] = $this->_prepare_post_type( $post_type, $fields );
4654 }
4655
4656 return $struct;
4657 }
4658
4659 /**
4660 * Retrieves revisions for a specific post.
4661 *
4662 * @since 3.5.0
4663 *
4664 * The optional $fields parameter specifies what fields will be included
4665 * in the response array.
4666 *
4667 * @uses wp_get_post_revisions()
4668 * @see wp_getPost() for more on $fields
4669 *
4670 * @param array $args {
4671 * Method arguments. Note: arguments must be ordered as documented.
4672 *
4673 * @type int $0 Blog ID (unused).
4674 * @type string $1 Username.
4675 * @type string $2 Password.
4676 * @type int $3 Post ID.
4677 * @type array $4 Optional. Fields to fetch.
4678 * }
4679 * @return array|IXR_Error Array containing a collection of posts.
4680 */
4681 public function wp_getRevisions( $args ) {
4682 if ( ! $this->minimum_args( $args, 4 ) ) {
4683 return $this->error;
4684 }
4685
4686 $this->escape( $args );
4687
4688 $username = $args[1];
4689 $password = $args[2];
4690 $post_id = (int) $args[3];
4691
4692 if ( isset( $args[4] ) ) {
4693 $fields = $args[4];
4694 } else {
4695 /**
4696 * Filters the default revision query fields used by the given XML-RPC method.
4697 *
4698 * @since 3.5.0
4699 *
4700 * @param array $field An array of revision fields to retrieve. By default,
4701 * contains 'post_date' and 'post_date_gmt'.
4702 * @param string $method The method name.
4703 */
4704 $fields = apply_filters( 'xmlrpc_default_revision_fields', array( 'post_date', 'post_date_gmt' ), 'wp.getRevisions' );
4705 }
4706
4707 $user = $this->login( $username, $password );
4708 if ( ! $user ) {
4709 return $this->error;
4710 }
4711
4712 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4713 do_action( 'xmlrpc_call', 'wp.getRevisions', $args, $this );
4714
4715 $post = get_post( $post_id );
4716 if ( ! $post ) {
4717 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
4718 }
4719
4720 if ( ! current_user_can( 'edit_post', $post_id ) ) {
4721 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts.' ) );
4722 }
4723
4724 // Check if revisions are enabled.
4725 if ( ! wp_revisions_enabled( $post ) ) {
4726 return new IXR_Error( 401, __( 'Sorry, revisions are disabled.' ) );
4727 }
4728
4729 $revisions = wp_get_post_revisions( $post_id );
4730
4731 if ( ! $revisions ) {
4732 return array();
4733 }
4734
4735 $struct = array();
4736
4737 foreach ( $revisions as $revision ) {
4738 if ( ! current_user_can( 'read_post', $revision->ID ) ) {
4739 continue;
4740 }
4741
4742 // Skip autosaves.
4743 if ( wp_is_post_autosave( $revision ) ) {
4744 continue;
4745 }
4746
4747 $struct[] = $this->_prepare_post( get_object_vars( $revision ), $fields );
4748 }
4749
4750 return $struct;
4751 }
4752
4753 /**
4754 * Restores a post revision.
4755 *
4756 * @since 3.5.0
4757 *
4758 * @uses wp_restore_post_revision()
4759 *
4760 * @param array $args {
4761 * Method arguments. Note: arguments must be ordered as documented.
4762 *
4763 * @type int $0 Blog ID (unused).
4764 * @type string $1 Username.
4765 * @type string $2 Password.
4766 * @type int $3 Revision ID.
4767 * }
4768 * @return bool|IXR_Error false if there was an error restoring, true if success.
4769 */
4770 public function wp_restoreRevision( $args ) {
4771 if ( ! $this->minimum_args( $args, 3 ) ) {
4772 return $this->error;
4773 }
4774
4775 $this->escape( $args );
4776
4777 $username = $args[1];
4778 $password = $args[2];
4779 $revision_id = (int) $args[3];
4780
4781 $user = $this->login( $username, $password );
4782 if ( ! $user ) {
4783 return $this->error;
4784 }
4785
4786 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4787 do_action( 'xmlrpc_call', 'wp.restoreRevision', $args, $this );
4788
4789 $revision = wp_get_post_revision( $revision_id );
4790 if ( ! $revision ) {
4791 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
4792 }
4793
4794 if ( wp_is_post_autosave( $revision ) ) {
4795 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
4796 }
4797
4798 $post = get_post( $revision->post_parent );
4799 if ( ! $post ) {
4800 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
4801 }
4802
4803 if ( ! current_user_can( 'edit_post', $revision->post_parent ) ) {
4804 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
4805 }
4806
4807 // Check if revisions are disabled.
4808 if ( ! wp_revisions_enabled( $post ) ) {
4809 return new IXR_Error( 401, __( 'Sorry, revisions are disabled.' ) );
4810 }
4811
4812 $post = wp_restore_post_revision( $revision_id );
4813
4814 return (bool) $post;
4815 }
4816
4817 /*
4818 * Blogger API functions.
4819 * Specs on http://plant.blogger.com/api and https://groups.yahoo.com/group/bloggerDev/
4820 */
4821
4822 /**
4823 * Retrieves blogs that user owns.
4824 *
4825 * Will make more sense once we support multiple blogs.
4826 *
4827 * @since 1.5.0
4828 *
4829 * @param array $args {
4830 * Method arguments. Note: arguments must be ordered as documented.
4831 *
4832 * @type int $0 Blog ID (unused).
4833 * @type string $1 Username.
4834 * @type string $2 Password.
4835 * }
4836 * @return array|IXR_Error
4837 */
4838 public function blogger_getUsersBlogs( $args ) {
4839 if ( ! $this->minimum_args( $args, 3 ) ) {
4840 return $this->error;
4841 }
4842
4843 if ( is_multisite() ) {
4844 return $this->_multisite_getUsersBlogs( $args );
4845 }
4846
4847 $this->escape( $args );
4848
4849 $username = $args[1];
4850 $password = $args[2];
4851
4852 $user = $this->login( $username, $password );
4853 if ( ! $user ) {
4854 return $this->error;
4855 }
4856
4857 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4858 do_action( 'xmlrpc_call', 'blogger.getUsersBlogs', $args, $this );
4859
4860 $is_admin = current_user_can( 'manage_options' );
4861
4862 $struct = array(
4863 'isAdmin' => $is_admin,
4864 'url' => get_option( 'home' ) . '/',
4865 'blogid' => '1',
4866 'blogName' => get_option( 'blogname' ),
4867 'xmlrpc' => site_url( 'xmlrpc.php', 'rpc' ),
4868 );
4869
4870 return array( $struct );
4871 }
4872
4873 /**
4874 * Private function for retrieving a users blogs for multisite setups.
4875 *
4876 * @since 3.0.0
4877 *
4878 * @param array $args {
4879 * Method arguments. Note: arguments must be ordered as documented.
4880 *
4881 * @type int $0 Blog ID (unused).
4882 * @type string $1 Username.
4883 * @type string $2 Password.
4884 * }
4885 * @return array|IXR_Error
4886 */
4887 protected function _multisite_getUsersBlogs( $args ) {
4888 $current_blog = get_site();
4889
4890 $domain = $current_blog->domain;
4891 $path = $current_blog->path . 'xmlrpc.php';
4892
4893 $blogs = $this->wp_getUsersBlogs( $args );
4894 if ( $blogs instanceof IXR_Error ) {
4895 return $blogs;
4896 }
4897
4898 if ( $_SERVER['HTTP_HOST'] === $domain && $_SERVER['REQUEST_URI'] === $path ) {
4899 return $blogs;
4900 } else {
4901 foreach ( (array) $blogs as $blog ) {
4902 if ( str_contains( $blog['url'], $_SERVER['HTTP_HOST'] ) ) {
4903 return array( $blog );
4904 }
4905 }
4906 return array();
4907 }
4908 }
4909
4910 /**
4911 * Retrieves user's data.
4912 *
4913 * Gives your client some info about you, so you don't have to.
4914 *
4915 * @since 1.5.0
4916 *
4917 * @param array $args {
4918 * Method arguments. Note: arguments must be ordered as documented.
4919 *
4920 * @type int $0 Blog ID (unused).
4921 * @type string $1 Username.
4922 * @type string $2 Password.
4923 * }
4924 * @return array|IXR_Error
4925 */
4926 public function blogger_getUserInfo( $args ) {
4927 $this->escape( $args );
4928
4929 $username = $args[1];
4930 $password = $args[2];
4931
4932 $user = $this->login( $username, $password );
4933 if ( ! $user ) {
4934 return $this->error;
4935 }
4936
4937 if ( ! current_user_can( 'edit_posts' ) ) {
4938 return new IXR_Error( 401, __( 'Sorry, you are not allowed to access user data on this site.' ) );
4939 }
4940
4941 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4942 do_action( 'xmlrpc_call', 'blogger.getUserInfo', $args, $this );
4943
4944 $struct = array(
4945 'nickname' => $user->nickname,
4946 'userid' => $user->ID,
4947 'url' => $user->user_url,
4948 'lastname' => $user->last_name,
4949 'firstname' => $user->first_name,
4950 );
4951
4952 return $struct;
4953 }
4954
4955 /**
4956 * Retrieves a post.
4957 *
4958 * @since 1.5.0
4959 *
4960 * @param array $args {
4961 * Method arguments. Note: arguments must be ordered as documented.
4962 *
4963 * @type int $0 Blog ID (unused).
4964 * @type int $1 Post ID.
4965 * @type string $2 Username.
4966 * @type string $3 Password.
4967 * }
4968 * @return array|IXR_Error
4969 */
4970 public function blogger_getPost( $args ) {
4971 $this->escape( $args );
4972
4973 $post_id = (int) $args[1];
4974 $username = $args[2];
4975 $password = $args[3];
4976
4977 $user = $this->login( $username, $password );
4978 if ( ! $user ) {
4979 return $this->error;
4980 }
4981
4982 $post_data = get_post( $post_id, ARRAY_A );
4983 if ( ! $post_data ) {
4984 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
4985 }
4986
4987 if ( ! current_user_can( 'edit_post', $post_id ) ) {
4988 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
4989 }
4990
4991 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
4992 do_action( 'xmlrpc_call', 'blogger.getPost', $args, $this );
4993
4994 $categories = implode( ',', wp_get_post_categories( $post_id ) );
4995
4996 $content = '<title>' . wp_unslash( $post_data['post_title'] ) . '</title>';
4997 $content .= '<category>' . $categories . '</category>';
4998 $content .= wp_unslash( $post_data['post_content'] );
4999
5000 $struct = array(
5001 'userid' => $post_data['post_author'],
5002 'dateCreated' => $this->_convert_date( $post_data['post_date'] ),
5003 'content' => $content,
5004 'postid' => (string) $post_data['ID'],
5005 );
5006
5007 return $struct;
5008 }
5009
5010 /**
5011 * Retrieves the list of recent posts.
5012 *
5013 * @since 1.5.0
5014 *
5015 * @param array $args {
5016 * Method arguments. Note: arguments must be ordered as documented.
5017 *
5018 * @type string $0 App key (unused).
5019 * @type int $1 Blog ID (unused).
5020 * @type string $2 Username.
5021 * @type string $3 Password.
5022 * @type int $4 Optional. Number of posts.
5023 * }
5024 * @return array|IXR_Error
5025 */
5026 public function blogger_getRecentPosts( $args ) {
5027
5028 $this->escape( $args );
5029
5030 // $args[0] = appkey - ignored.
5031 $username = $args[2];
5032 $password = $args[3];
5033 if ( isset( $args[4] ) ) {
5034 $query = array( 'numberposts' => absint( $args[4] ) );
5035 } else {
5036 $query = array();
5037 }
5038
5039 $user = $this->login( $username, $password );
5040 if ( ! $user ) {
5041 return $this->error;
5042 }
5043
5044 if ( ! current_user_can( 'edit_posts' ) ) {
5045 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts.' ) );
5046 }
5047
5048 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
5049 do_action( 'xmlrpc_call', 'blogger.getRecentPosts', $args, $this );
5050
5051 $posts_list = wp_get_recent_posts( $query );
5052
5053 if ( ! $posts_list ) {
5054 $this->error = new IXR_Error( 500, __( 'No posts found or an error occurred while retrieving posts.' ) );
5055 return $this->error;
5056 }
5057
5058 $recent_posts = array();
5059 foreach ( $posts_list as $entry ) {
5060 if ( ! current_user_can( 'edit_post', $entry['ID'] ) ) {
5061 continue;
5062 }
5063
5064 $post_date = $this->_convert_date( $entry['post_date'] );
5065 $categories = implode( ',', wp_get_post_categories( $entry['ID'] ) );
5066
5067 $content = '<title>' . wp_unslash( $entry['post_title'] ) . '</title>';
5068 $content .= '<category>' . $categories . '</category>';
5069 $content .= wp_unslash( $entry['post_content'] );
5070
5071 $recent_posts[] = array(
5072 'userid' => $entry['post_author'],
5073 'dateCreated' => $post_date,
5074 'content' => $content,
5075 'postid' => (string) $entry['ID'],
5076 );
5077 }
5078
5079 return $recent_posts;
5080 }
5081
5082 /**
5083 * Deprecated.
5084 *
5085 * @since 1.5.0
5086 * @deprecated 3.5.0
5087 *
5088 * @param array $args Unused.
5089 * @return IXR_Error Error object.
5090 */
5091 public function blogger_getTemplate( $args ) {
5092 return new IXR_Error( 403, __( 'Sorry, this method is not supported.' ) );
5093 }
5094
5095 /**
5096 * Deprecated.
5097 *
5098 * @since 1.5.0
5099 * @deprecated 3.5.0
5100 *
5101 * @param array $args Unused.
5102 * @return IXR_Error Error object.
5103 */
5104 public function blogger_setTemplate( $args ) {
5105 return new IXR_Error( 403, __( 'Sorry, this method is not supported.' ) );
5106 }
5107
5108 /**
5109 * Creates a new post.
5110 *
5111 * @since 1.5.0
5112 *
5113 * @param array $args {
5114 * Method arguments. Note: arguments must be ordered as documented.
5115 *
5116 * @type string $0 App key (unused).
5117 * @type int $1 Blog ID (unused).
5118 * @type string $2 Username.
5119 * @type string $3 Password.
5120 * @type string $4 Content.
5121 * @type int $5 Publish flag. 0 for draft, 1 for publish.
5122 * }
5123 * @return int|IXR_Error
5124 */
5125 public function blogger_newPost( $args ) {
5126 $this->escape( $args );
5127
5128 $username = $args[2];
5129 $password = $args[3];
5130 $content = $args[4];
5131 $publish = $args[5];
5132
5133 $user = $this->login( $username, $password );
5134 if ( ! $user ) {
5135 return $this->error;
5136 }
5137
5138 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
5139 do_action( 'xmlrpc_call', 'blogger.newPost', $args, $this );
5140
5141 $cap = ( $publish ) ? 'publish_posts' : 'edit_posts';
5142 if ( ! current_user_can( get_post_type_object( 'post' )->cap->create_posts ) || ! current_user_can( $cap ) ) {
5143 return new IXR_Error( 401, __( 'Sorry, you are not allowed to post on this site.' ) );
5144 }
5145
5146 $post_status = ( $publish ) ? 'publish' : 'draft';
5147
5148 $post_author = $user->ID;
5149
5150 $post_title = xmlrpc_getposttitle( $content );
5151 $post_category = xmlrpc_getpostcategory( $content );
5152 $post_content = xmlrpc_removepostdata( $content );
5153
5154 $post_date = current_time( 'mysql' );
5155 $post_date_gmt = current_time( 'mysql', true );
5156
5157 $post_data = compact(
5158 'post_author',
5159 'post_date',
5160 'post_date_gmt',
5161 'post_content',
5162 'post_title',
5163 'post_category',
5164 'post_status'
5165 );
5166
5167 $post_id = wp_insert_post( $post_data );
5168 if ( is_wp_error( $post_id ) ) {
5169 return new IXR_Error( 500, $post_id->get_error_message() );
5170 }
5171
5172 if ( ! $post_id ) {
5173 return new IXR_Error( 500, __( 'Sorry, the post could not be created.' ) );
5174 }
5175
5176 $this->attach_uploads( $post_id, $post_content );
5177
5178 /**
5179 * Fires after a new post has been successfully created via the XML-RPC Blogger API.
5180 *
5181 * @since 3.4.0
5182 *
5183 * @param int $post_id ID of the new post.
5184 * @param array $args An array of new post arguments.
5185 */
5186 do_action( 'xmlrpc_call_success_blogger_newPost', $post_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
5187
5188 return $post_id;
5189 }
5190
5191 /**
5192 * Edits a post.
5193 *
5194 * @since 1.5.0
5195 *
5196 * @param array $args {
5197 * Method arguments. Note: arguments must be ordered as documented.
5198 *
5199 * @type int $0 Blog ID (unused).
5200 * @type int $1 Post ID.
5201 * @type string $2 Username.
5202 * @type string $3 Password.
5203 * @type string $4 Content
5204 * @type int $5 Publish flag. 0 for draft, 1 for publish.
5205 * }
5206 * @return true|IXR_Error true when done.
5207 */
5208 public function blogger_editPost( $args ) {
5209
5210 $this->escape( $args );
5211
5212 $post_id = (int) $args[1];
5213 $username = $args[2];
5214 $password = $args[3];
5215 $content = $args[4];
5216 $publish = $args[5];
5217
5218 $user = $this->login( $username, $password );
5219 if ( ! $user ) {
5220 return $this->error;
5221 }
5222
5223 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
5224 do_action( 'xmlrpc_call', 'blogger.editPost', $args, $this );
5225
5226 $actual_post = get_post( $post_id, ARRAY_A );
5227
5228 if ( ! $actual_post || 'post' !== $actual_post['post_type'] ) {
5229 return new IXR_Error( 404, __( 'Sorry, no such post.' ) );
5230 }
5231
5232 $this->escape( $actual_post );
5233
5234 if ( ! current_user_can( 'edit_post', $post_id ) ) {
5235 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
5236 }
5237 if ( 'publish' === $actual_post['post_status'] && ! current_user_can( 'publish_posts' ) ) {
5238 return new IXR_Error( 401, __( 'Sorry, you are not allowed to publish this post.' ) );
5239 }
5240
5241 $postdata = array();
5242 $postdata['ID'] = $actual_post['ID'];
5243 $postdata['post_content'] = xmlrpc_removepostdata( $content );
5244 $postdata['post_title'] = xmlrpc_getposttitle( $content );
5245 $postdata['post_category'] = xmlrpc_getpostcategory( $content );
5246 $postdata['post_status'] = $actual_post['post_status'];
5247 $postdata['post_excerpt'] = $actual_post['post_excerpt'];
5248 $postdata['post_status'] = $publish ? 'publish' : 'draft';
5249
5250 $result = wp_update_post( $postdata );
5251
5252 if ( ! $result ) {
5253 return new IXR_Error( 500, __( 'Sorry, the post could not be updated.' ) );
5254 }
5255 $this->attach_uploads( $actual_post['ID'], $postdata['post_content'] );
5256
5257 /**
5258 * Fires after a post has been successfully updated via the XML-RPC Blogger API.
5259 *
5260 * @since 3.4.0
5261 *
5262 * @param int $post_id ID of the updated post.
5263 * @param array $args An array of arguments for the post to edit.
5264 */
5265 do_action( 'xmlrpc_call_success_blogger_editPost', $post_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
5266
5267 return true;
5268 }
5269
5270 /**
5271 * Deletes a post.
5272 *
5273 * @since 1.5.0
5274 *
5275 * @param array $args {
5276 * Method arguments. Note: arguments must be ordered as documented.
5277 *
5278 * @type int $0 Blog ID (unused).
5279 * @type int $1 Post ID.
5280 * @type string $2 Username.
5281 * @type string $3 Password.
5282 * }
5283 * @return true|IXR_Error True when post is deleted.
5284 */
5285 public function blogger_deletePost( $args ) {
5286 $this->escape( $args );
5287
5288 $post_id = (int) $args[1];
5289 $username = $args[2];
5290 $password = $args[3];
5291
5292 $user = $this->login( $username, $password );
5293 if ( ! $user ) {
5294 return $this->error;
5295 }
5296
5297 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
5298 do_action( 'xmlrpc_call', 'blogger.deletePost', $args, $this );
5299
5300 $actual_post = get_post( $post_id, ARRAY_A );
5301
5302 if ( ! $actual_post || 'post' !== $actual_post['post_type'] ) {
5303 return new IXR_Error( 404, __( 'Sorry, no such post.' ) );
5304 }
5305
5306 if ( ! current_user_can( 'delete_post', $post_id ) ) {
5307 return new IXR_Error( 401, __( 'Sorry, you are not allowed to delete this post.' ) );
5308 }
5309
5310 $result = wp_delete_post( $post_id );
5311
5312 if ( ! $result ) {
5313 return new IXR_Error( 500, __( 'Sorry, the post could not be deleted.' ) );
5314 }
5315
5316 /**
5317 * Fires after a post has been successfully deleted via the XML-RPC Blogger API.
5318 *
5319 * @since 3.4.0
5320 *
5321 * @param int $post_id ID of the deleted post.
5322 * @param array $args An array of arguments to delete the post.
5323 */
5324 do_action( 'xmlrpc_call_success_blogger_deletePost', $post_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
5325
5326 return true;
5327 }
5328
5329 /*
5330 * MetaWeblog API functions.
5331 * Specs on wherever Dave Winer wants them to be.
5332 */
5333
5334 /**
5335 * Creates a new post.
5336 *
5337 * The 'content_struct' argument must contain:
5338 * - title
5339 * - description
5340 * - mt_excerpt
5341 * - mt_text_more
5342 * - mt_keywords
5343 * - mt_tb_ping_urls
5344 * - categories
5345 *
5346 * Also, it can optionally contain:
5347 * - wp_slug
5348 * - wp_password
5349 * - wp_page_parent_id
5350 * - wp_page_order
5351 * - wp_author_id
5352 * - post_status | page_status - can be 'draft', 'private', 'publish', or 'pending'
5353 * - mt_allow_comments - can be 'open' or 'closed'
5354 * - mt_allow_pings - can be 'open' or 'closed'
5355 * - date_created_gmt
5356 * - dateCreated
5357 * - wp_post_thumbnail
5358 *
5359 * @since 1.5.0
5360 *
5361 * @param array $args {
5362 * Method arguments. Note: arguments must be ordered as documented.
5363 *
5364 * @type int $0 Blog ID (unused).
5365 * @type string $1 Username.
5366 * @type string $2 Password.
5367 * @type array $3 Content structure.
5368 * @type int $4 Optional. Publish flag. 0 for draft, 1 for publish. Default 0.
5369 * }
5370 * @return int|IXR_Error
5371 */
5372 public function mw_newPost( $args ) {
5373 $this->escape( $args );
5374
5375 $username = $args[1];
5376 $password = $args[2];
5377 $content_struct = $args[3];
5378 $publish = isset( $args[4] ) ? $args[4] : 0;
5379
5380 $user = $this->login( $username, $password );
5381 if ( ! $user ) {
5382 return $this->error;
5383 }
5384
5385 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
5386 do_action( 'xmlrpc_call', 'metaWeblog.newPost', $args, $this );
5387
5388 $page_template = '';
5389 if ( ! empty( $content_struct['post_type'] ) ) {
5390 if ( 'page' === $content_struct['post_type'] ) {
5391 if ( $publish ) {
5392 $cap = 'publish_pages';
5393 } elseif ( isset( $content_struct['page_status'] ) && 'publish' === $content_struct['page_status'] ) {
5394 $cap = 'publish_pages';
5395 } else {
5396 $cap = 'edit_pages';
5397 }
5398 $error_message = __( 'Sorry, you are not allowed to publish pages on this site.' );
5399 $post_type = 'page';
5400 if ( ! empty( $content_struct['wp_page_template'] ) ) {
5401 $page_template = $content_struct['wp_page_template'];
5402 }
5403 } elseif ( 'post' === $content_struct['post_type'] ) {
5404 if ( $publish ) {
5405 $cap = 'publish_posts';
5406 } elseif ( isset( $content_struct['post_status'] ) && 'publish' === $content_struct['post_status'] ) {
5407 $cap = 'publish_posts';
5408 } else {
5409 $cap = 'edit_posts';
5410 }
5411 $error_message = __( 'Sorry, you are not allowed to publish posts on this site.' );
5412 $post_type = 'post';
5413 } else {
5414 // No other 'post_type' values are allowed here.
5415 return new IXR_Error( 401, __( 'Invalid post type.' ) );
5416 }
5417 } else {
5418 if ( $publish ) {
5419 $cap = 'publish_posts';
5420 } elseif ( isset( $content_struct['post_status'] ) && 'publish' === $content_struct['post_status'] ) {
5421 $cap = 'publish_posts';
5422 } else {
5423 $cap = 'edit_posts';
5424 }
5425 $error_message = __( 'Sorry, you are not allowed to publish posts on this site.' );
5426 $post_type = 'post';
5427 }
5428
5429 if ( ! current_user_can( get_post_type_object( $post_type )->cap->create_posts ) ) {
5430 return new IXR_Error( 401, __( 'Sorry, you are not allowed to publish posts on this site.' ) );
5431 }
5432 if ( ! current_user_can( $cap ) ) {
5433 return new IXR_Error( 401, $error_message );
5434 }
5435
5436 // Check for a valid post format if one was given.
5437 if ( isset( $content_struct['wp_post_format'] ) ) {
5438 $content_struct['wp_post_format'] = sanitize_key( $content_struct['wp_post_format'] );
5439 if ( ! array_key_exists( $content_struct['wp_post_format'], get_post_format_strings() ) ) {
5440 return new IXR_Error( 404, __( 'Invalid post format.' ) );
5441 }
5442 }
5443
5444 // Let WordPress generate the 'post_name' (slug) unless
5445 // one has been provided.
5446 $post_name = null;
5447 if ( isset( $content_struct['wp_slug'] ) ) {
5448 $post_name = $content_struct['wp_slug'];
5449 }
5450
5451 // Only use a password if one was given.
5452 $post_password = '';
5453 if ( isset( $content_struct['wp_password'] ) ) {
5454 $post_password = $content_struct['wp_password'];
5455 }
5456
5457 // Only set a post parent if one was given.
5458 $post_parent = 0;
5459 if ( isset( $content_struct['wp_page_parent_id'] ) ) {
5460 $post_parent = $content_struct['wp_page_parent_id'];
5461 }
5462
5463 // Only set the 'menu_order' if it was given.
5464 $menu_order = 0;
5465 if ( isset( $content_struct['wp_page_order'] ) ) {
5466 $menu_order = $content_struct['wp_page_order'];
5467 }
5468
5469 $post_author = $user->ID;
5470
5471 // If an author ID was provided then use it instead.
5472 if ( isset( $content_struct['wp_author_id'] ) && ( $user->ID !== (int) $content_struct['wp_author_id'] ) ) {
5473 switch ( $post_type ) {
5474 case 'post':
5475 if ( ! current_user_can( 'edit_others_posts' ) ) {
5476 return new IXR_Error( 401, __( 'Sorry, you are not allowed to create posts as this user.' ) );
5477 }
5478 break;
5479 case 'page':
5480 if ( ! current_user_can( 'edit_others_pages' ) ) {
5481 return new IXR_Error( 401, __( 'Sorry, you are not allowed to create pages as this user.' ) );
5482 }
5483 break;
5484 default:
5485 return new IXR_Error( 401, __( 'Invalid post type.' ) );
5486 }
5487 $author = get_userdata( $content_struct['wp_author_id'] );
5488 if ( ! $author ) {
5489 return new IXR_Error( 404, __( 'Invalid author ID.' ) );
5490 }
5491 $post_author = $content_struct['wp_author_id'];
5492 }
5493
5494 $post_title = isset( $content_struct['title'] ) ? $content_struct['title'] : '';
5495 $post_content = isset( $content_struct['description'] ) ? $content_struct['description'] : '';
5496
5497 $post_status = $publish ? 'publish' : 'draft';
5498
5499 if ( isset( $content_struct[ "{$post_type}_status" ] ) ) {
5500 switch ( $content_struct[ "{$post_type}_status" ] ) {
5501 case 'draft':
5502 case 'pending':
5503 case 'private':
5504 case 'publish':
5505 $post_status = $content_struct[ "{$post_type}_status" ];
5506 break;
5507 default:
5508 // Deliberably left empty.
5509 break;
5510 }
5511 }
5512
5513 $post_excerpt = isset( $content_struct['mt_excerpt'] ) ? $content_struct['mt_excerpt'] : '';
5514 $post_more = isset( $content_struct['mt_text_more'] ) ? $content_struct['mt_text_more'] : '';
5515
5516 $tags_input = isset( $content_struct['mt_keywords'] ) ? $content_struct['mt_keywords'] : array();
5517
5518 if ( isset( $content_struct['mt_allow_comments'] ) ) {
5519 if ( ! is_numeric( $content_struct['mt_allow_comments'] ) ) {
5520 switch ( $content_struct['mt_allow_comments'] ) {
5521 case 'closed':
5522 $comment_status = 'closed';
5523 break;
5524 case 'open':
5525 $comment_status = 'open';
5526 break;
5527 default:
5528 $comment_status = get_default_comment_status( $post_type );
5529 break;
5530 }
5531 } else {
5532 switch ( (int) $content_struct['mt_allow_comments'] ) {
5533 case 0:
5534 case 2:
5535 $comment_status = 'closed';
5536 break;
5537 case 1:
5538 $comment_status = 'open';
5539 break;
5540 default:
5541 $comment_status = get_default_comment_status( $post_type );
5542 break;
5543 }
5544 }
5545 } else {
5546 $comment_status = get_default_comment_status( $post_type );
5547 }
5548
5549 if ( isset( $content_struct['mt_allow_pings'] ) ) {
5550 if ( ! is_numeric( $content_struct['mt_allow_pings'] ) ) {
5551 switch ( $content_struct['mt_allow_pings'] ) {
5552 case 'closed':
5553 $ping_status = 'closed';
5554 break;
5555 case 'open':
5556 $ping_status = 'open';
5557 break;
5558 default:
5559 $ping_status = get_default_comment_status( $post_type, 'pingback' );
5560 break;
5561 }
5562 } else {
5563 switch ( (int) $content_struct['mt_allow_pings'] ) {
5564 case 0:
5565 $ping_status = 'closed';
5566 break;
5567 case 1:
5568 $ping_status = 'open';
5569 break;
5570 default:
5571 $ping_status = get_default_comment_status( $post_type, 'pingback' );
5572 break;
5573 }
5574 }
5575 } else {
5576 $ping_status = get_default_comment_status( $post_type, 'pingback' );
5577 }
5578
5579 if ( $post_more ) {
5580 $post_content .= '<!--more-->' . $post_more;
5581 }
5582
5583 $to_ping = '';
5584 if ( isset( $content_struct['mt_tb_ping_urls'] ) ) {
5585 $to_ping = $content_struct['mt_tb_ping_urls'];
5586 if ( is_array( $to_ping ) ) {
5587 $to_ping = implode( ' ', $to_ping );
5588 }
5589 }
5590
5591 // Do some timestamp voodoo.
5592 if ( ! empty( $content_struct['date_created_gmt'] ) ) {
5593 // We know this is supposed to be GMT, so we're going to slap that Z on there by force.
5594 $date_created = rtrim( $content_struct['date_created_gmt']->getIso(), 'Z' ) . 'Z';
5595 } elseif ( ! empty( $content_struct['dateCreated'] ) ) {
5596 $date_created = $content_struct['dateCreated']->getIso();
5597 }
5598
5599 $post_date = '';
5600 $post_date_gmt = '';
5601 if ( ! empty( $date_created ) ) {
5602 $post_date = iso8601_to_datetime( $date_created );
5603 $post_date_gmt = iso8601_to_datetime( $date_created, 'gmt' );
5604 }
5605
5606 $post_category = array();
5607 if ( isset( $content_struct['categories'] ) ) {
5608 $catnames = $content_struct['categories'];
5609
5610 if ( is_array( $catnames ) ) {
5611 foreach ( $catnames as $cat ) {
5612 $post_category[] = get_cat_ID( $cat );
5613 }
5614 }
5615 }
5616
5617 $postdata = compact(
5618 'post_author',
5619 'post_date',
5620 'post_date_gmt',
5621 'post_content',
5622 'post_title',
5623 'post_category',
5624 'post_status',
5625 'post_excerpt',
5626 'comment_status',
5627 'ping_status',
5628 'to_ping',
5629 'post_type',
5630 'post_name',
5631 'post_password',
5632 'post_parent',
5633 'menu_order',
5634 'tags_input',
5635 'page_template'
5636 );
5637
5638 $post_id = get_default_post_to_edit( $post_type, true )->ID;
5639 $postdata['ID'] = $post_id;
5640
5641 // Only posts can be sticky.
5642 if ( 'post' === $post_type && isset( $content_struct['sticky'] ) ) {
5643 $data = $postdata;
5644 $data['sticky'] = $content_struct['sticky'];
5645 $error = $this->_toggle_sticky( $data );
5646 if ( $error ) {
5647 return $error;
5648 }
5649 }
5650
5651 if ( isset( $content_struct['custom_fields'] ) ) {
5652 $this->set_custom_fields( $post_id, $content_struct['custom_fields'] );
5653 }
5654
5655 if ( isset( $content_struct['wp_post_thumbnail'] ) ) {
5656 if ( set_post_thumbnail( $post_id, $content_struct['wp_post_thumbnail'] ) === false ) {
5657 return new IXR_Error( 404, __( 'Invalid attachment ID.' ) );
5658 }
5659
5660 unset( $content_struct['wp_post_thumbnail'] );
5661 }
5662
5663 // Handle enclosures.
5664 $enclosure = isset( $content_struct['enclosure'] ) ? $content_struct['enclosure'] : null;
5665 $this->add_enclosure_if_new( $post_id, $enclosure );
5666
5667 $this->attach_uploads( $post_id, $post_content );
5668
5669 /*
5670 * Handle post formats if assigned, value is validated earlier
5671 * in this function.
5672 */
5673 if ( isset( $content_struct['wp_post_format'] ) ) {
5674 set_post_format( $post_id, $content_struct['wp_post_format'] );
5675 }
5676
5677 $post_id = wp_insert_post( $postdata, true );
5678 if ( is_wp_error( $post_id ) ) {
5679 return new IXR_Error( 500, $post_id->get_error_message() );
5680 }
5681
5682 if ( ! $post_id ) {
5683 return new IXR_Error( 500, __( 'Sorry, the post could not be created.' ) );
5684 }
5685
5686 /**
5687 * Fires after a new post has been successfully created via the XML-RPC MovableType API.
5688 *
5689 * @since 3.4.0
5690 *
5691 * @param int $post_id ID of the new post.
5692 * @param array $args An array of arguments to create the new post.
5693 */
5694 do_action( 'xmlrpc_call_success_mw_newPost', $post_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
5695
5696 return (string) $post_id;
5697 }
5698
5699 /**
5700 * Adds an enclosure to a post if it's new.
5701 *
5702 * @since 2.8.0
5703 *
5704 * @param int $post_id Post ID.
5705 * @param array $enclosure Enclosure data.
5706 */
5707 public function add_enclosure_if_new( $post_id, $enclosure ) {
5708 if ( is_array( $enclosure ) && isset( $enclosure['url'] ) && isset( $enclosure['length'] ) && isset( $enclosure['type'] ) ) {
5709 $encstring = $enclosure['url'] . "\n" . $enclosure['length'] . "\n" . $enclosure['type'] . "\n";
5710 $found = false;
5711 $enclosures = get_post_meta( $post_id, 'enclosure' );
5712 if ( $enclosures ) {
5713 foreach ( $enclosures as $enc ) {
5714 // This method used to omit the trailing new line. #23219
5715 if ( rtrim( $enc, "\n" ) === rtrim( $encstring, "\n" ) ) {
5716 $found = true;
5717 break;
5718 }
5719 }
5720 }
5721 if ( ! $found ) {
5722 add_post_meta( $post_id, 'enclosure', $encstring );
5723 }
5724 }
5725 }
5726
5727 /**
5728 * Attaches an upload to a post.
5729 *
5730 * @since 2.1.0
5731 *
5732 * @global wpdb $wpdb WordPress database abstraction object.
5733 *
5734 * @param int $post_id Post ID.
5735 * @param string $post_content Post Content for attachment.
5736 */
5737 public function attach_uploads( $post_id, $post_content ) {
5738 global $wpdb;
5739
5740 // Find any unattached files.
5741 $attachments = $wpdb->get_results( "SELECT ID, guid FROM {$wpdb->posts} WHERE post_parent = '0' AND post_type = 'attachment'" );
5742 if ( is_array( $attachments ) ) {
5743 foreach ( $attachments as $file ) {
5744 if ( ! empty( $file->guid ) && str_contains( $post_content, $file->guid ) ) {
5745 $wpdb->update( $wpdb->posts, array( 'post_parent' => $post_id ), array( 'ID' => $file->ID ) );
5746 }
5747 }
5748 }
5749 }
5750
5751 /**
5752 * Edits a post.
5753 *
5754 * @since 1.5.0
5755 *
5756 * @param array $args {
5757 * Method arguments. Note: arguments must be ordered as documented.
5758 *
5759 * @type int $0 Post ID.
5760 * @type string $1 Username.
5761 * @type string $2 Password.
5762 * @type array $3 Content structure.
5763 * @type int $4 Optional. Publish flag. 0 for draft, 1 for publish. Default 0.
5764 * }
5765 * @return true|IXR_Error True on success.
5766 */
5767 public function mw_editPost( $args ) {
5768 $this->escape( $args );
5769
5770 $post_id = (int) $args[0];
5771 $username = $args[1];
5772 $password = $args[2];
5773 $content_struct = $args[3];
5774 $publish = isset( $args[4] ) ? $args[4] : 0;
5775
5776 $user = $this->login( $username, $password );
5777 if ( ! $user ) {
5778 return $this->error;
5779 }
5780
5781 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
5782 do_action( 'xmlrpc_call', 'metaWeblog.editPost', $args, $this );
5783
5784 $postdata = get_post( $post_id, ARRAY_A );
5785
5786 /*
5787 * If there is no post data for the give post ID, stop now and return an error.
5788 * Otherwise a new post will be created (which was the old behavior).
5789 */
5790 if ( ! $postdata || empty( $postdata['ID'] ) ) {
5791 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
5792 }
5793
5794 if ( ! current_user_can( 'edit_post', $post_id ) ) {
5795 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
5796 }
5797
5798 // Use wp.editPost to edit post types other than post and page.
5799 if ( ! in_array( $postdata['post_type'], array( 'post', 'page' ), true ) ) {
5800 return new IXR_Error( 401, __( 'Invalid post type.' ) );
5801 }
5802
5803 // Thwart attempt to change the post type.
5804 if ( ! empty( $content_struct['post_type'] ) && ( $content_struct['post_type'] !== $postdata['post_type'] ) ) {
5805 return new IXR_Error( 401, __( 'The post type may not be changed.' ) );
5806 }
5807
5808 // Check for a valid post format if one was given.
5809 if ( isset( $content_struct['wp_post_format'] ) ) {
5810 $content_struct['wp_post_format'] = sanitize_key( $content_struct['wp_post_format'] );
5811 if ( ! array_key_exists( $content_struct['wp_post_format'], get_post_format_strings() ) ) {
5812 return new IXR_Error( 404, __( 'Invalid post format.' ) );
5813 }
5814 }
5815
5816 $this->escape( $postdata );
5817
5818 $post_id = $postdata['ID'];
5819 $post_content = $postdata['post_content'];
5820 $post_title = $postdata['post_title'];
5821 $post_excerpt = $postdata['post_excerpt'];
5822 $post_password = $postdata['post_password'];
5823 $post_parent = $postdata['post_parent'];
5824 $post_type = $postdata['post_type'];
5825 $menu_order = $postdata['menu_order'];
5826 $ping_status = $postdata['ping_status'];
5827 $comment_status = $postdata['comment_status'];
5828
5829 // Let WordPress manage slug if none was provided.
5830 $post_name = $postdata['post_name'];
5831 if ( isset( $content_struct['wp_slug'] ) ) {
5832 $post_name = $content_struct['wp_slug'];
5833 }
5834
5835 // Only use a password if one was given.
5836 if ( isset( $content_struct['wp_password'] ) ) {
5837 $post_password = $content_struct['wp_password'];
5838 }
5839
5840 // Only set a post parent if one was given.
5841 if ( isset( $content_struct['wp_page_parent_id'] ) ) {
5842 $post_parent = $content_struct['wp_page_parent_id'];
5843 }
5844
5845 // Only set the 'menu_order' if it was given.
5846 if ( isset( $content_struct['wp_page_order'] ) ) {
5847 $menu_order = $content_struct['wp_page_order'];
5848 }
5849
5850 $page_template = '';
5851 if ( ! empty( $content_struct['wp_page_template'] ) && 'page' === $post_type ) {
5852 $page_template = $content_struct['wp_page_template'];
5853 }
5854
5855 $post_author = $postdata['post_author'];
5856
5857 // If an author ID was provided then use it instead.
5858 if ( isset( $content_struct['wp_author_id'] ) ) {
5859 // Check permissions if attempting to switch author to or from another user.
5860 if ( $user->ID !== (int) $content_struct['wp_author_id'] || $user->ID !== (int) $post_author ) {
5861 switch ( $post_type ) {
5862 case 'post':
5863 if ( ! current_user_can( 'edit_others_posts' ) ) {
5864 return new IXR_Error( 401, __( 'Sorry, you are not allowed to change the post author as this user.' ) );
5865 }
5866 break;
5867 case 'page':
5868 if ( ! current_user_can( 'edit_others_pages' ) ) {
5869 return new IXR_Error( 401, __( 'Sorry, you are not allowed to change the page author as this user.' ) );
5870 }
5871 break;
5872 default:
5873 return new IXR_Error( 401, __( 'Invalid post type.' ) );
5874 }
5875 $post_author = $content_struct['wp_author_id'];
5876 }
5877 }
5878
5879 if ( isset( $content_struct['mt_allow_comments'] ) ) {
5880 if ( ! is_numeric( $content_struct['mt_allow_comments'] ) ) {
5881 switch ( $content_struct['mt_allow_comments'] ) {
5882 case 'closed':
5883 $comment_status = 'closed';
5884 break;
5885 case 'open':
5886 $comment_status = 'open';
5887 break;
5888 default:
5889 $comment_status = get_default_comment_status( $post_type );
5890 break;
5891 }
5892 } else {
5893 switch ( (int) $content_struct['mt_allow_comments'] ) {
5894 case 0:
5895 case 2:
5896 $comment_status = 'closed';
5897 break;
5898 case 1:
5899 $comment_status = 'open';
5900 break;
5901 default:
5902 $comment_status = get_default_comment_status( $post_type );
5903 break;
5904 }
5905 }
5906 }
5907
5908 if ( isset( $content_struct['mt_allow_pings'] ) ) {
5909 if ( ! is_numeric( $content_struct['mt_allow_pings'] ) ) {
5910 switch ( $content_struct['mt_allow_pings'] ) {
5911 case 'closed':
5912 $ping_status = 'closed';
5913 break;
5914 case 'open':
5915 $ping_status = 'open';
5916 break;
5917 default:
5918 $ping_status = get_default_comment_status( $post_type, 'pingback' );
5919 break;
5920 }
5921 } else {
5922 switch ( (int) $content_struct['mt_allow_pings'] ) {
5923 case 0:
5924 $ping_status = 'closed';
5925 break;
5926 case 1:
5927 $ping_status = 'open';
5928 break;
5929 default:
5930 $ping_status = get_default_comment_status( $post_type, 'pingback' );
5931 break;
5932 }
5933 }
5934 }
5935
5936 if ( isset( $content_struct['title'] ) ) {
5937 $post_title = $content_struct['title'];
5938 }
5939
5940 if ( isset( $content_struct['description'] ) ) {
5941 $post_content = $content_struct['description'];
5942 }
5943
5944 $post_category = array();
5945 if ( isset( $content_struct['categories'] ) ) {
5946 $catnames = $content_struct['categories'];
5947 if ( is_array( $catnames ) ) {
5948 foreach ( $catnames as $cat ) {
5949 $post_category[] = get_cat_ID( $cat );
5950 }
5951 }
5952 }
5953
5954 if ( isset( $content_struct['mt_excerpt'] ) ) {
5955 $post_excerpt = $content_struct['mt_excerpt'];
5956 }
5957
5958 $post_more = isset( $content_struct['mt_text_more'] ) ? $content_struct['mt_text_more'] : '';
5959
5960 $post_status = $publish ? 'publish' : 'draft';
5961 if ( isset( $content_struct[ "{$post_type}_status" ] ) ) {
5962 switch ( $content_struct[ "{$post_type}_status" ] ) {
5963 case 'draft':
5964 case 'pending':
5965 case 'private':
5966 case 'publish':
5967 $post_status = $content_struct[ "{$post_type}_status" ];
5968 break;
5969 default:
5970 $post_status = $publish ? 'publish' : 'draft';
5971 break;
5972 }
5973 }
5974
5975 $tags_input = isset( $content_struct['mt_keywords'] ) ? $content_struct['mt_keywords'] : array();
5976
5977 if ( 'publish' === $post_status || 'private' === $post_status ) {
5978 if ( 'page' === $post_type && ! current_user_can( 'publish_pages' ) ) {
5979 return new IXR_Error( 401, __( 'Sorry, you are not allowed to publish this page.' ) );
5980 } elseif ( ! current_user_can( 'publish_posts' ) ) {
5981 return new IXR_Error( 401, __( 'Sorry, you are not allowed to publish this post.' ) );
5982 }
5983 }
5984
5985 if ( $post_more ) {
5986 $post_content = $post_content . '<!--more-->' . $post_more;
5987 }
5988
5989 $to_ping = '';
5990 if ( isset( $content_struct['mt_tb_ping_urls'] ) ) {
5991 $to_ping = $content_struct['mt_tb_ping_urls'];
5992 if ( is_array( $to_ping ) ) {
5993 $to_ping = implode( ' ', $to_ping );
5994 }
5995 }
5996
5997 // Do some timestamp voodoo.
5998 if ( ! empty( $content_struct['date_created_gmt'] ) ) {
5999 // We know this is supposed to be GMT, so we're going to slap that Z on there by force.
6000 $date_created = rtrim( $content_struct['date_created_gmt']->getIso(), 'Z' ) . 'Z';
6001 } elseif ( ! empty( $content_struct['dateCreated'] ) ) {
6002 $date_created = $content_struct['dateCreated']->getIso();
6003 }
6004
6005 // Default to not flagging the post date to be edited unless it's intentional.
6006 $edit_date = false;
6007
6008 if ( ! empty( $date_created ) ) {
6009 $post_date = iso8601_to_datetime( $date_created );
6010 $post_date_gmt = iso8601_to_datetime( $date_created, 'gmt' );
6011
6012 // Flag the post date to be edited.
6013 $edit_date = true;
6014 } else {
6015 $post_date = $postdata['post_date'];
6016 $post_date_gmt = $postdata['post_date_gmt'];
6017 }
6018
6019 $newpost = array(
6020 'ID' => $post_id,
6021 );
6022
6023 $newpost += compact(
6024 'post_content',
6025 'post_title',
6026 'post_category',
6027 'post_status',
6028 'post_excerpt',
6029 'comment_status',
6030 'ping_status',
6031 'edit_date',
6032 'post_date',
6033 'post_date_gmt',
6034 'to_ping',
6035 'post_name',
6036 'post_password',
6037 'post_parent',
6038 'menu_order',
6039 'post_author',
6040 'tags_input',
6041 'page_template'
6042 );
6043
6044 // We've got all the data -- post it.
6045 $result = wp_update_post( $newpost, true );
6046 if ( is_wp_error( $result ) ) {
6047 return new IXR_Error( 500, $result->get_error_message() );
6048 }
6049
6050 if ( ! $result ) {
6051 return new IXR_Error( 500, __( 'Sorry, the post could not be updated.' ) );
6052 }
6053
6054 // Only posts can be sticky.
6055 if ( 'post' === $post_type && isset( $content_struct['sticky'] ) ) {
6056 $data = $newpost;
6057 $data['sticky'] = $content_struct['sticky'];
6058 $data['post_type'] = 'post';
6059 $error = $this->_toggle_sticky( $data, true );
6060 if ( $error ) {
6061 return $error;
6062 }
6063 }
6064
6065 if ( isset( $content_struct['custom_fields'] ) ) {
6066 $this->set_custom_fields( $post_id, $content_struct['custom_fields'] );
6067 }
6068
6069 if ( isset( $content_struct['wp_post_thumbnail'] ) ) {
6070
6071 // Empty value deletes, non-empty value adds/updates.
6072 if ( empty( $content_struct['wp_post_thumbnail'] ) ) {
6073 delete_post_thumbnail( $post_id );
6074 } else {
6075 if ( set_post_thumbnail( $post_id, $content_struct['wp_post_thumbnail'] ) === false ) {
6076 return new IXR_Error( 404, __( 'Invalid attachment ID.' ) );
6077 }
6078 }
6079 unset( $content_struct['wp_post_thumbnail'] );
6080 }
6081
6082 // Handle enclosures.
6083 $enclosure = isset( $content_struct['enclosure'] ) ? $content_struct['enclosure'] : null;
6084 $this->add_enclosure_if_new( $post_id, $enclosure );
6085
6086 $this->attach_uploads( $post_id, $post_content );
6087
6088 // Handle post formats if assigned, validation is handled earlier in this function.
6089 if ( isset( $content_struct['wp_post_format'] ) ) {
6090 set_post_format( $post_id, $content_struct['wp_post_format'] );
6091 }
6092
6093 /**
6094 * Fires after a post has been successfully updated via the XML-RPC MovableType API.
6095 *
6096 * @since 3.4.0
6097 *
6098 * @param int $post_id ID of the updated post.
6099 * @param array $args An array of arguments to update the post.
6100 */
6101 do_action( 'xmlrpc_call_success_mw_editPost', $post_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
6102
6103 return true;
6104 }
6105
6106 /**
6107 * Retrieves a post.
6108 *
6109 * @since 1.5.0
6110 *
6111 * @param array $args {
6112 * Method arguments. Note: arguments must be ordered as documented.
6113 *
6114 * @type int $0 Post ID.
6115 * @type string $1 Username.
6116 * @type string $2 Password.
6117 * }
6118 * @return array|IXR_Error
6119 */
6120 public function mw_getPost( $args ) {
6121 $this->escape( $args );
6122
6123 $post_id = (int) $args[0];
6124 $username = $args[1];
6125 $password = $args[2];
6126
6127 $user = $this->login( $username, $password );
6128 if ( ! $user ) {
6129 return $this->error;
6130 }
6131
6132 $postdata = get_post( $post_id, ARRAY_A );
6133 if ( ! $postdata ) {
6134 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
6135 }
6136
6137 if ( ! current_user_can( 'edit_post', $post_id ) ) {
6138 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
6139 }
6140
6141 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6142 do_action( 'xmlrpc_call', 'metaWeblog.getPost', $args, $this );
6143
6144 if ( '' !== $postdata['post_date'] ) {
6145 $post_date = $this->_convert_date( $postdata['post_date'] );
6146 $post_date_gmt = $this->_convert_date_gmt( $postdata['post_date_gmt'], $postdata['post_date'] );
6147 $post_modified = $this->_convert_date( $postdata['post_modified'] );
6148 $post_modified_gmt = $this->_convert_date_gmt( $postdata['post_modified_gmt'], $postdata['post_modified'] );
6149
6150 $categories = array();
6151 $cat_ids = wp_get_post_categories( $post_id );
6152 foreach ( $cat_ids as $cat_id ) {
6153 $categories[] = get_cat_name( $cat_id );
6154 }
6155
6156 $tagnames = array();
6157 $tags = wp_get_post_tags( $post_id );
6158 if ( ! empty( $tags ) ) {
6159 foreach ( $tags as $tag ) {
6160 $tagnames[] = $tag->name;
6161 }
6162 $tagnames = implode( ', ', $tagnames );
6163 } else {
6164 $tagnames = '';
6165 }
6166
6167 $post = get_extended( $postdata['post_content'] );
6168 $link = get_permalink( $postdata['ID'] );
6169
6170 // Get the author info.
6171 $author = get_userdata( $postdata['post_author'] );
6172
6173 $allow_comments = ( 'open' === $postdata['comment_status'] ) ? 1 : 0;
6174 $allow_pings = ( 'open' === $postdata['ping_status'] ) ? 1 : 0;
6175
6176 // Consider future posts as published.
6177 if ( 'future' === $postdata['post_status'] ) {
6178 $postdata['post_status'] = 'publish';
6179 }
6180
6181 // Get post format.
6182 $post_format = get_post_format( $post_id );
6183 if ( empty( $post_format ) ) {
6184 $post_format = 'standard';
6185 }
6186
6187 $sticky = false;
6188 if ( is_sticky( $post_id ) ) {
6189 $sticky = true;
6190 }
6191
6192 $enclosure = array();
6193 foreach ( (array) get_post_custom( $post_id ) as $key => $val ) {
6194 if ( 'enclosure' === $key ) {
6195 foreach ( (array) $val as $enc ) {
6196 $encdata = explode( "\n", $enc );
6197 $enclosure['url'] = trim( htmlspecialchars( $encdata[0] ) );
6198 $enclosure['length'] = (int) trim( $encdata[1] );
6199 $enclosure['type'] = trim( $encdata[2] );
6200 break 2;
6201 }
6202 }
6203 }
6204
6205 $resp = array(
6206 'dateCreated' => $post_date,
6207 'userid' => $postdata['post_author'],
6208 'postid' => $postdata['ID'],
6209 'description' => $post['main'],
6210 'title' => $postdata['post_title'],
6211 'link' => $link,
6212 'permaLink' => $link,
6213 // Commented out because no other tool seems to use this.
6214 // 'content' => $entry['post_content'],
6215 'categories' => $categories,
6216 'mt_excerpt' => $postdata['post_excerpt'],
6217 'mt_text_more' => $post['extended'],
6218 'wp_more_text' => $post['more_text'],
6219 'mt_allow_comments' => $allow_comments,
6220 'mt_allow_pings' => $allow_pings,
6221 'mt_keywords' => $tagnames,
6222 'wp_slug' => $postdata['post_name'],
6223 'wp_password' => $postdata['post_password'],
6224 'wp_author_id' => (string) $author->ID,
6225 'wp_author_display_name' => $author->display_name,
6226 'date_created_gmt' => $post_date_gmt,
6227 'post_status' => $postdata['post_status'],
6228 'custom_fields' => $this->get_custom_fields( $post_id ),
6229 'wp_post_format' => $post_format,
6230 'sticky' => $sticky,
6231 'date_modified' => $post_modified,
6232 'date_modified_gmt' => $post_modified_gmt,
6233 );
6234
6235 if ( ! empty( $enclosure ) ) {
6236 $resp['enclosure'] = $enclosure;
6237 }
6238
6239 $resp['wp_post_thumbnail'] = get_post_thumbnail_id( $postdata['ID'] );
6240
6241 return $resp;
6242 } else {
6243 return new IXR_Error( 404, __( 'Sorry, no such post.' ) );
6244 }
6245 }
6246
6247 /**
6248 * Retrieves list of recent posts.
6249 *
6250 * @since 1.5.0
6251 *
6252 * @param array $args {
6253 * Method arguments. Note: arguments must be ordered as documented.
6254 *
6255 * @type int $0 Blog ID (unused).
6256 * @type string $1 Username.
6257 * @type string $2 Password.
6258 * @type int $3 Optional. Number of posts.
6259 * }
6260 * @return array|IXR_Error
6261 */
6262 public function mw_getRecentPosts( $args ) {
6263 $this->escape( $args );
6264
6265 $username = $args[1];
6266 $password = $args[2];
6267 if ( isset( $args[3] ) ) {
6268 $query = array( 'numberposts' => absint( $args[3] ) );
6269 } else {
6270 $query = array();
6271 }
6272
6273 $user = $this->login( $username, $password );
6274 if ( ! $user ) {
6275 return $this->error;
6276 }
6277
6278 if ( ! current_user_can( 'edit_posts' ) ) {
6279 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit posts.' ) );
6280 }
6281
6282 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6283 do_action( 'xmlrpc_call', 'metaWeblog.getRecentPosts', $args, $this );
6284
6285 $posts_list = wp_get_recent_posts( $query );
6286
6287 if ( ! $posts_list ) {
6288 return array();
6289 }
6290
6291 $recent_posts = array();
6292 foreach ( $posts_list as $entry ) {
6293 if ( ! current_user_can( 'edit_post', $entry['ID'] ) ) {
6294 continue;
6295 }
6296
6297 $post_date = $this->_convert_date( $entry['post_date'] );
6298 $post_date_gmt = $this->_convert_date_gmt( $entry['post_date_gmt'], $entry['post_date'] );
6299 $post_modified = $this->_convert_date( $entry['post_modified'] );
6300 $post_modified_gmt = $this->_convert_date_gmt( $entry['post_modified_gmt'], $entry['post_modified'] );
6301
6302 $categories = array();
6303 $cat_ids = wp_get_post_categories( $entry['ID'] );
6304 foreach ( $cat_ids as $cat_id ) {
6305 $categories[] = get_cat_name( $cat_id );
6306 }
6307
6308 $tagnames = array();
6309 $tags = wp_get_post_tags( $entry['ID'] );
6310 if ( ! empty( $tags ) ) {
6311 foreach ( $tags as $tag ) {
6312 $tagnames[] = $tag->name;
6313 }
6314 $tagnames = implode( ', ', $tagnames );
6315 } else {
6316 $tagnames = '';
6317 }
6318
6319 $post = get_extended( $entry['post_content'] );
6320 $link = get_permalink( $entry['ID'] );
6321
6322 // Get the post author info.
6323 $author = get_userdata( $entry['post_author'] );
6324
6325 $allow_comments = ( 'open' === $entry['comment_status'] ) ? 1 : 0;
6326 $allow_pings = ( 'open' === $entry['ping_status'] ) ? 1 : 0;
6327
6328 // Consider future posts as published.
6329 if ( 'future' === $entry['post_status'] ) {
6330 $entry['post_status'] = 'publish';
6331 }
6332
6333 // Get post format.
6334 $post_format = get_post_format( $entry['ID'] );
6335 if ( empty( $post_format ) ) {
6336 $post_format = 'standard';
6337 }
6338
6339 $recent_posts[] = array(
6340 'dateCreated' => $post_date,
6341 'userid' => $entry['post_author'],
6342 'postid' => (string) $entry['ID'],
6343 'description' => $post['main'],
6344 'title' => $entry['post_title'],
6345 'link' => $link,
6346 'permaLink' => $link,
6347 // Commented out because no other tool seems to use this.
6348 // 'content' => $entry['post_content'],
6349 'categories' => $categories,
6350 'mt_excerpt' => $entry['post_excerpt'],
6351 'mt_text_more' => $post['extended'],
6352 'wp_more_text' => $post['more_text'],
6353 'mt_allow_comments' => $allow_comments,
6354 'mt_allow_pings' => $allow_pings,
6355 'mt_keywords' => $tagnames,
6356 'wp_slug' => $entry['post_name'],
6357 'wp_password' => $entry['post_password'],
6358 'wp_author_id' => (string) $author->ID,
6359 'wp_author_display_name' => $author->display_name,
6360 'date_created_gmt' => $post_date_gmt,
6361 'post_status' => $entry['post_status'],
6362 'custom_fields' => $this->get_custom_fields( $entry['ID'] ),
6363 'wp_post_format' => $post_format,
6364 'date_modified' => $post_modified,
6365 'date_modified_gmt' => $post_modified_gmt,
6366 'sticky' => ( 'post' === $entry['post_type'] && is_sticky( $entry['ID'] ) ),
6367 'wp_post_thumbnail' => get_post_thumbnail_id( $entry['ID'] ),
6368 );
6369 }
6370
6371 return $recent_posts;
6372 }
6373
6374 /**
6375 * Retrieves the list of categories on a given blog.
6376 *
6377 * @since 1.5.0
6378 *
6379 * @param array $args {
6380 * Method arguments. Note: arguments must be ordered as documented.
6381 *
6382 * @type int $0 Blog ID (unused).
6383 * @type string $1 Username.
6384 * @type string $2 Password.
6385 * }
6386 * @return array|IXR_Error
6387 */
6388 public function mw_getCategories( $args ) {
6389 $this->escape( $args );
6390
6391 $username = $args[1];
6392 $password = $args[2];
6393
6394 $user = $this->login( $username, $password );
6395 if ( ! $user ) {
6396 return $this->error;
6397 }
6398
6399 if ( ! current_user_can( 'edit_posts' ) ) {
6400 return new IXR_Error( 401, __( 'Sorry, you must be able to edit posts on this site in order to view categories.' ) );
6401 }
6402
6403 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6404 do_action( 'xmlrpc_call', 'metaWeblog.getCategories', $args, $this );
6405
6406 $categories_struct = array();
6407
6408 $cats = get_categories( array( 'get' => 'all' ) );
6409 if ( $cats ) {
6410 foreach ( $cats as $cat ) {
6411 $struct = array();
6412 $struct['categoryId'] = $cat->term_id;
6413 $struct['parentId'] = $cat->parent;
6414 $struct['description'] = $cat->name;
6415 $struct['categoryDescription'] = $cat->description;
6416 $struct['categoryName'] = $cat->name;
6417 $struct['htmlUrl'] = esc_html( get_category_link( $cat->term_id ) );
6418 $struct['rssUrl'] = esc_html( get_category_feed_link( $cat->term_id, 'rss2' ) );
6419
6420 $categories_struct[] = $struct;
6421 }
6422 }
6423
6424 return $categories_struct;
6425 }
6426
6427 /**
6428 * Uploads a file, following your settings.
6429 *
6430 * Adapted from a patch by Johann Richard.
6431 *
6432 * @link http://mycvs.org/archives/2004/06/30/file-upload-to-wordpress-in-ecto/
6433 *
6434 * @since 1.5.0
6435 *
6436 * @param array $args {
6437 * Method arguments. Note: arguments must be ordered as documented.
6438 *
6439 * @type int $0 Blog ID (unused).
6440 * @type string $1 Username.
6441 * @type string $2 Password.
6442 * @type array $3 Data.
6443 * }
6444 * @return array|IXR_Error
6445 */
6446 public function mw_newMediaObject( $args ) {
6447 $username = $this->escape( $args[1] );
6448 $password = $this->escape( $args[2] );
6449 $data = $args[3];
6450
6451 $name = sanitize_file_name( $data['name'] );
6452 $type = $data['type'];
6453 $bits = $data['bits'];
6454
6455 $user = $this->login( $username, $password );
6456 if ( ! $user ) {
6457 return $this->error;
6458 }
6459
6460 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6461 do_action( 'xmlrpc_call', 'metaWeblog.newMediaObject', $args, $this );
6462
6463 if ( ! current_user_can( 'upload_files' ) ) {
6464 $this->error = new IXR_Error( 401, __( 'Sorry, you are not allowed to upload files.' ) );
6465 return $this->error;
6466 }
6467
6468 if ( is_multisite() && upload_is_user_over_quota( false ) ) {
6469 $this->error = new IXR_Error(
6470 401,
6471 sprintf(
6472 /* translators: %s: Allowed space allocation. */
6473 __( 'Sorry, you have used your space allocation of %s. Please delete some files to upload more files.' ),
6474 size_format( get_space_allowed() * MB_IN_BYTES )
6475 )
6476 );
6477 return $this->error;
6478 }
6479
6480 /**
6481 * Filters whether to preempt the XML-RPC media upload.
6482 *
6483 * Returning a truthy value will effectively short-circuit the media upload,
6484 * returning that value as a 500 error instead.
6485 *
6486 * @since 2.1.0
6487 *
6488 * @param bool $error Whether to pre-empt the media upload. Default false.
6489 */
6490 $upload_err = apply_filters( 'pre_upload_error', false );
6491 if ( $upload_err ) {
6492 return new IXR_Error( 500, $upload_err );
6493 }
6494
6495 $upload = wp_upload_bits( $name, null, $bits );
6496 if ( ! empty( $upload['error'] ) ) {
6497 /* translators: 1: File name, 2: Error message. */
6498 $error_string = sprintf( __( 'Could not write file %1$s (%2$s).' ), $name, $upload['error'] );
6499 return new IXR_Error( 500, $error_string );
6500 }
6501
6502 // Construct the attachment array.
6503 $post_id = 0;
6504 if ( ! empty( $data['post_id'] ) ) {
6505 $post_id = (int) $data['post_id'];
6506
6507 if ( ! current_user_can( 'edit_post', $post_id ) ) {
6508 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
6509 }
6510 }
6511
6512 $attachment = array(
6513 'post_title' => $name,
6514 'post_content' => '',
6515 'post_type' => 'attachment',
6516 'post_parent' => $post_id,
6517 'post_mime_type' => $type,
6518 'guid' => $upload['url'],
6519 );
6520
6521 // Save the data.
6522 $attachment_id = wp_insert_attachment( $attachment, $upload['file'], $post_id );
6523 wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $upload['file'] ) );
6524
6525 /**
6526 * Fires after a new attachment has been added via the XML-RPC MovableType API.
6527 *
6528 * @since 3.4.0
6529 *
6530 * @param int $attachment_id ID of the new attachment.
6531 * @param array $args An array of arguments to add the attachment.
6532 */
6533 do_action( 'xmlrpc_call_success_mw_newMediaObject', $attachment_id, $args ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.NotLowercase
6534
6535 $struct = $this->_prepare_media_item( get_post( $attachment_id ) );
6536
6537 // Deprecated values.
6538 $struct['id'] = $struct['attachment_id'];
6539 $struct['file'] = $struct['title'];
6540 $struct['url'] = $struct['link'];
6541
6542 return $struct;
6543 }
6544
6545 /*
6546 * MovableType API functions.
6547 * Specs archive on https://web.archive.org/web/20050220091302/http://www.movabletype.org/docs/mtmanual_programmatic.html
6548 */
6549
6550 /**
6551 * Retrieves the post titles of recent posts.
6552 *
6553 * @since 1.5.0
6554 *
6555 * @param array $args {
6556 * Method arguments. Note: arguments must be ordered as documented.
6557 *
6558 * @type int $0 Blog ID (unused).
6559 * @type string $1 Username.
6560 * @type string $2 Password.
6561 * @type int $3 Optional. Number of posts.
6562 * }
6563 * @return array|IXR_Error
6564 */
6565 public function mt_getRecentPostTitles( $args ) {
6566 $this->escape( $args );
6567
6568 $username = $args[1];
6569 $password = $args[2];
6570 if ( isset( $args[3] ) ) {
6571 $query = array( 'numberposts' => absint( $args[3] ) );
6572 } else {
6573 $query = array();
6574 }
6575
6576 $user = $this->login( $username, $password );
6577 if ( ! $user ) {
6578 return $this->error;
6579 }
6580
6581 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6582 do_action( 'xmlrpc_call', 'mt.getRecentPostTitles', $args, $this );
6583
6584 $posts_list = wp_get_recent_posts( $query );
6585
6586 if ( ! $posts_list ) {
6587 $this->error = new IXR_Error( 500, __( 'No posts found or an error occurred while retrieving posts.' ) );
6588 return $this->error;
6589 }
6590
6591 $recent_posts = array();
6592
6593 foreach ( $posts_list as $entry ) {
6594 if ( ! current_user_can( 'edit_post', $entry['ID'] ) ) {
6595 continue;
6596 }
6597
6598 $post_date = $this->_convert_date( $entry['post_date'] );
6599 $post_date_gmt = $this->_convert_date_gmt( $entry['post_date_gmt'], $entry['post_date'] );
6600
6601 $recent_posts[] = array(
6602 'dateCreated' => $post_date,
6603 'userid' => $entry['post_author'],
6604 'postid' => (string) $entry['ID'],
6605 'title' => $entry['post_title'],
6606 'post_status' => $entry['post_status'],
6607 'date_created_gmt' => $post_date_gmt,
6608 );
6609 }
6610
6611 return $recent_posts;
6612 }
6613
6614 /**
6615 * Retrieves the list of all categories on a blog.
6616 *
6617 * @since 1.5.0
6618 *
6619 * @param array $args {
6620 * Method arguments. Note: arguments must be ordered as documented.
6621 *
6622 * @type int $0 Blog ID (unused).
6623 * @type string $1 Username.
6624 * @type string $2 Password.
6625 * }
6626 * @return array|IXR_Error
6627 */
6628 public function mt_getCategoryList( $args ) {
6629 $this->escape( $args );
6630
6631 $username = $args[1];
6632 $password = $args[2];
6633
6634 $user = $this->login( $username, $password );
6635 if ( ! $user ) {
6636 return $this->error;
6637 }
6638
6639 if ( ! current_user_can( 'edit_posts' ) ) {
6640 return new IXR_Error( 401, __( 'Sorry, you must be able to edit posts on this site in order to view categories.' ) );
6641 }
6642
6643 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6644 do_action( 'xmlrpc_call', 'mt.getCategoryList', $args, $this );
6645
6646 $categories_struct = array();
6647
6648 $cats = get_categories(
6649 array(
6650 'hide_empty' => 0,
6651 'hierarchical' => 0,
6652 )
6653 );
6654 if ( $cats ) {
6655 foreach ( $cats as $cat ) {
6656 $struct = array();
6657 $struct['categoryId'] = $cat->term_id;
6658 $struct['categoryName'] = $cat->name;
6659
6660 $categories_struct[] = $struct;
6661 }
6662 }
6663
6664 return $categories_struct;
6665 }
6666
6667 /**
6668 * Retrieves post categories.
6669 *
6670 * @since 1.5.0
6671 *
6672 * @param array $args {
6673 * Method arguments. Note: arguments must be ordered as documented.
6674 *
6675 * @type int $0 Post ID.
6676 * @type string $1 Username.
6677 * @type string $2 Password.
6678 * }
6679 * @return array|IXR_Error
6680 */
6681 public function mt_getPostCategories( $args ) {
6682 $this->escape( $args );
6683
6684 $post_id = (int) $args[0];
6685 $username = $args[1];
6686 $password = $args[2];
6687
6688 $user = $this->login( $username, $password );
6689 if ( ! $user ) {
6690 return $this->error;
6691 }
6692
6693 if ( ! get_post( $post_id ) ) {
6694 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
6695 }
6696
6697 if ( ! current_user_can( 'edit_post', $post_id ) ) {
6698 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
6699 }
6700
6701 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6702 do_action( 'xmlrpc_call', 'mt.getPostCategories', $args, $this );
6703
6704 $categories = array();
6705 $cat_ids = wp_get_post_categories( (int) $post_id );
6706 // First listed category will be the primary category.
6707 $is_primary = true;
6708 foreach ( $cat_ids as $cat_id ) {
6709 $categories[] = array(
6710 'categoryName' => get_cat_name( $cat_id ),
6711 'categoryId' => (string) $cat_id,
6712 'isPrimary' => $is_primary,
6713 );
6714 $is_primary = false;
6715 }
6716
6717 return $categories;
6718 }
6719
6720 /**
6721 * Sets categories for a post.
6722 *
6723 * @since 1.5.0
6724 *
6725 * @param array $args {
6726 * Method arguments. Note: arguments must be ordered as documented.
6727 *
6728 * @type int $0 Post ID.
6729 * @type string $1 Username.
6730 * @type string $2 Password.
6731 * @type array $3 Categories.
6732 * }
6733 * @return true|IXR_Error True on success.
6734 */
6735 public function mt_setPostCategories( $args ) {
6736 $this->escape( $args );
6737
6738 $post_id = (int) $args[0];
6739 $username = $args[1];
6740 $password = $args[2];
6741 $categories = $args[3];
6742
6743 $user = $this->login( $username, $password );
6744 if ( ! $user ) {
6745 return $this->error;
6746 }
6747
6748 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6749 do_action( 'xmlrpc_call', 'mt.setPostCategories', $args, $this );
6750
6751 if ( ! get_post( $post_id ) ) {
6752 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
6753 }
6754
6755 if ( ! current_user_can( 'edit_post', $post_id ) ) {
6756 return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) );
6757 }
6758
6759 $cat_ids = array();
6760 foreach ( $categories as $cat ) {
6761 $cat_ids[] = $cat['categoryId'];
6762 }
6763
6764 wp_set_post_categories( $post_id, $cat_ids );
6765
6766 return true;
6767 }
6768
6769 /**
6770 * Retrieves an array of methods supported by this server.
6771 *
6772 * @since 1.5.0
6773 *
6774 * @return array
6775 */
6776 public function mt_supportedMethods() {
6777 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6778 do_action( 'xmlrpc_call', 'mt.supportedMethods', array(), $this );
6779
6780 return array_keys( $this->methods );
6781 }
6782
6783 /**
6784 * Retrieves an empty array because we don't support per-post text filters.
6785 *
6786 * @since 1.5.0
6787 */
6788 public function mt_supportedTextFilters() {
6789 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6790 do_action( 'xmlrpc_call', 'mt.supportedTextFilters', array(), $this );
6791
6792 /**
6793 * Filters the MoveableType text filters list for XML-RPC.
6794 *
6795 * @since 2.2.0
6796 *
6797 * @param array $filters An array of text filters.
6798 */
6799 return apply_filters( 'xmlrpc_text_filters', array() );
6800 }
6801
6802 /**
6803 * Retrieves trackbacks sent to a given post.
6804 *
6805 * @since 1.5.0
6806 *
6807 * @global wpdb $wpdb WordPress database abstraction object.
6808 *
6809 * @param int $post_id
6810 * @return array|IXR_Error
6811 */
6812 public function mt_getTrackbackPings( $post_id ) {
6813 global $wpdb;
6814
6815 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6816 do_action( 'xmlrpc_call', 'mt.getTrackbackPings', $post_id, $this );
6817
6818 $actual_post = get_post( $post_id, ARRAY_A );
6819
6820 if ( ! $actual_post ) {
6821 return new IXR_Error( 404, __( 'Sorry, no such post.' ) );
6822 }
6823
6824 $comments = $wpdb->get_results( $wpdb->prepare( "SELECT comment_author_url, comment_content, comment_author_IP, comment_type FROM $wpdb->comments WHERE comment_post_ID = %d", $post_id ) );
6825
6826 if ( ! $comments ) {
6827 return array();
6828 }
6829
6830 $trackback_pings = array();
6831 foreach ( $comments as $comment ) {
6832 if ( 'trackback' === $comment->comment_type ) {
6833 $content = $comment->comment_content;
6834 $title = substr( $content, 8, ( strpos( $content, '</strong>' ) - 8 ) );
6835 $trackback_pings[] = array(
6836 'pingTitle' => $title,
6837 'pingURL' => $comment->comment_author_url,
6838 'pingIP' => $comment->comment_author_IP,
6839 );
6840 }
6841 }
6842
6843 return $trackback_pings;
6844 }
6845
6846 /**
6847 * Sets a post's publish status to 'publish'.
6848 *
6849 * @since 1.5.0
6850 *
6851 * @param array $args {
6852 * Method arguments. Note: arguments must be ordered as documented.
6853 *
6854 * @type int $0 Post ID.
6855 * @type string $1 Username.
6856 * @type string $2 Password.
6857 * }
6858 * @return int|IXR_Error
6859 */
6860 public function mt_publishPost( $args ) {
6861 $this->escape( $args );
6862
6863 $post_id = (int) $args[0];
6864 $username = $args[1];
6865 $password = $args[2];
6866
6867 $user = $this->login( $username, $password );
6868 if ( ! $user ) {
6869 return $this->error;
6870 }
6871
6872 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6873 do_action( 'xmlrpc_call', 'mt.publishPost', $args, $this );
6874
6875 $postdata = get_post( $post_id, ARRAY_A );
6876 if ( ! $postdata ) {
6877 return new IXR_Error( 404, __( 'Invalid post ID.' ) );
6878 }
6879
6880 if ( ! current_user_can( 'publish_posts' ) || ! current_user_can( 'edit_post', $post_id ) ) {
6881 return new IXR_Error( 401, __( 'Sorry, you are not allowed to publish this post.' ) );
6882 }
6883
6884 $postdata['post_status'] = 'publish';
6885
6886 // Retain old categories.
6887 $postdata['post_category'] = wp_get_post_categories( $post_id );
6888 $this->escape( $postdata );
6889
6890 return wp_update_post( $postdata );
6891 }
6892
6893 /*
6894 * Pingback functions.
6895 * Specs on www.hixie.ch/specs/pingback/pingback
6896 */
6897
6898 /**
6899 * Retrieves a pingback and registers it.
6900 *
6901 * @since 1.5.0
6902 *
6903 * @global wpdb $wpdb WordPress database abstraction object.
6904 *
6905 * @param array $args {
6906 * Method arguments. Note: arguments must be ordered as documented.
6907 *
6908 * @type string $0 URL of page linked from.
6909 * @type string $1 URL of page linked to.
6910 * }
6911 * @return string|IXR_Error
6912 */
6913 public function pingback_ping( $args ) {
6914 global $wpdb;
6915
6916 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
6917 do_action( 'xmlrpc_call', 'pingback.ping', $args, $this );
6918
6919 $this->escape( $args );
6920
6921 $pagelinkedfrom = str_replace( '&', '&', $args[0] );
6922 $pagelinkedto = str_replace( '&', '&', $args[1] );
6923 $pagelinkedto = str_replace( '&', '&', $pagelinkedto );
6924
6925 /**
6926 * Filters the pingback source URI.
6927 *
6928 * @since 3.6.0
6929 *
6930 * @param string $pagelinkedfrom URI of the page linked from.
6931 * @param string $pagelinkedto URI of the page linked to.
6932 */
6933 $pagelinkedfrom = apply_filters( 'pingback_ping_source_uri', $pagelinkedfrom, $pagelinkedto );
6934
6935 if ( ! $pagelinkedfrom ) {
6936 return $this->pingback_error( 0, __( 'A valid URL was not provided.' ) );
6937 }
6938
6939 // Check if the page linked to is on our site.
6940 $pos1 = strpos( $pagelinkedto, str_replace( array( 'http://www.', 'http://', 'https://www.', 'https://' ), '', get_option( 'home' ) ) );
6941 if ( ! $pos1 ) {
6942 return $this->pingback_error( 0, __( 'Is there no link to us?' ) );
6943 }
6944
6945 /*
6946 * Let's find which post is linked to.
6947 * FIXME: Does url_to_postid() cover all these cases already?
6948 * If so, then let's use it and drop the old code.
6949 */
6950 $urltest = parse_url( $pagelinkedto );
6951 $post_id = url_to_postid( $pagelinkedto );
6952
6953 if ( $post_id ) {
6954 // $way
6955 } elseif ( isset( $urltest['path'] ) && preg_match( '#p/[0-9]{1,}#', $urltest['path'], $match ) ) {
6956 // The path defines the post_ID (archives/p/XXXX).
6957 $blah = explode( '/', $match[0] );
6958 $post_id = (int) $blah[1];
6959 } elseif ( isset( $urltest['query'] ) && preg_match( '#p=[0-9]{1,}#', $urltest['query'], $match ) ) {
6960 // The query string defines the post_ID (?p=XXXX).
6961 $blah = explode( '=', $match[0] );
6962 $post_id = (int) $blah[1];
6963 } elseif ( isset( $urltest['fragment'] ) ) {
6964 // An #anchor is there, it's either...
6965 if ( (int) $urltest['fragment'] ) {
6966 // ...an integer #XXXX (simplest case),
6967 $post_id = (int) $urltest['fragment'];
6968 } elseif ( preg_match( '/post-[0-9]+/', $urltest['fragment'] ) ) {
6969 // ...a post ID in the form 'post-###',
6970 $post_id = preg_replace( '/[^0-9]+/', '', $urltest['fragment'] );
6971 } elseif ( is_string( $urltest['fragment'] ) ) {
6972 // ...or a string #title, a little more complicated.
6973 $title = preg_replace( '/[^a-z0-9]/i', '.', $urltest['fragment'] );
6974 $sql = $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE post_title RLIKE %s", $title );
6975 $post_id = $wpdb->get_var( $sql );
6976 if ( ! $post_id ) {
6977 // Returning unknown error '0' is better than die()'ing.
6978 return $this->pingback_error( 0, '' );
6979 }
6980 }
6981 } else {
6982 // TODO: Attempt to extract a post ID from the given URL.
6983 return $this->pingback_error( 33, __( 'The specified target URL cannot be used as a target. It either does not exist, or it is not a pingback-enabled resource.' ) );
6984 }
6985
6986 $post_id = (int) $post_id;
6987 $post = get_post( $post_id );
6988
6989 if ( ! $post ) { // Post not found.
6990 return $this->pingback_error( 33, __( 'The specified target URL cannot be used as a target. It either does not exist, or it is not a pingback-enabled resource.' ) );
6991 }
6992
6993 if ( url_to_postid( $pagelinkedfrom ) === $post_id ) {
6994 return $this->pingback_error( 0, __( 'The source URL and the target URL cannot both point to the same resource.' ) );
6995 }
6996
6997 // Check if pings are on.
6998 if ( ! pings_open( $post ) ) {
6999 return $this->pingback_error( 33, __( 'The specified target URL cannot be used as a target. It either does not exist, or it is not a pingback-enabled resource.' ) );
7000 }
7001
7002 // Let's check that the remote site didn't already pingback this entry.
7003 if ( $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_author_url = %s", $post_id, $pagelinkedfrom ) ) ) {
7004 return $this->pingback_error( 48, __( 'The pingback has already been registered.' ) );
7005 }
7006
7007 /*
7008 * The remote site may have sent the pingback before it finished publishing its own content
7009 * containing this pingback URL. If that happens then it won't be immediately possible to fetch
7010 * the pinging post; adding a small delay reduces the likelihood of this happening.
7011 *
7012 * While there are more robust methods than calling `sleep()` here (because `sleep()` merely
7013 * mitigates the risk of requesting the remote post before it's available), this is effective
7014 * enough for most cases and avoids introducing more complexity into this code.
7015 *
7016 * One way to improve the reliability of this code might be to add failure-handling to the remote
7017 * fetch and retry up to a set number of times if it receives a 404. This could also handle 401 and
7018 * 403 responses to differentiate the "does not exist" failure from the "may not access" failure.
7019 */
7020 sleep( 1 );
7021
7022 $remote_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );
7023
7024 /** This filter is documented in wp-includes/class-wp-http.php */
7025 $user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ), $pagelinkedfrom );
7026
7027 // Let's check the remote site.
7028 $http_api_args = array(
7029 'timeout' => 10,
7030 'redirection' => 0,
7031 'limit_response_size' => 153600, // 150 KB
7032 'user-agent' => "$user_agent; verifying pingback from $remote_ip",
7033 'headers' => array(
7034 'X-Pingback-Forwarded-For' => $remote_ip,
7035 ),
7036 );
7037
7038 $request = wp_safe_remote_get( $pagelinkedfrom, $http_api_args );
7039 $remote_source = wp_remote_retrieve_body( $request );
7040 $remote_source_original = $remote_source;
7041
7042 if ( ! $remote_source ) {
7043 return $this->pingback_error( 16, __( 'The source URL does not exist.' ) );
7044 }
7045
7046 /**
7047 * Filters the pingback remote source.
7048 *
7049 * @since 2.5.0
7050 *
7051 * @param string $remote_source Response source for the page linked from.
7052 * @param string $pagelinkedto URL of the page linked to.
7053 */
7054 $remote_source = apply_filters( 'pre_remote_source', $remote_source, $pagelinkedto );
7055
7056 // Work around bug in strip_tags():
7057 $remote_source = str_replace( '<!DOC', '<DOC', $remote_source );
7058 $remote_source = preg_replace( '/[\r\n\t ]+/', ' ', $remote_source ); // normalize spaces
7059 $remote_source = preg_replace( '/<\/*(h1|h2|h3|h4|h5|h6|p|th|td|li|dt|dd|pre|caption|input|textarea|button|body)[^>]*>/', "\n\n", $remote_source );
7060
7061 preg_match( '|<title>([^<]*?)</title>|is', $remote_source, $matchtitle );
7062 $title = isset( $matchtitle[1] ) ? $matchtitle[1] : '';
7063 if ( empty( $title ) ) {
7064 return $this->pingback_error( 32, __( 'A title on that page cannot be found.' ) );
7065 }
7066
7067 // Remove all script and style tags including their content.
7068 $remote_source = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $remote_source );
7069 // Just keep the tag we need.
7070 $remote_source = strip_tags( $remote_source, '<a>' );
7071
7072 $p = explode( "\n\n", $remote_source );
7073
7074 $preg_target = preg_quote( $pagelinkedto, '|' );
7075
7076 foreach ( $p as $para ) {
7077 if ( str_contains( $para, $pagelinkedto ) ) { // It exists, but is it a link?
7078 preg_match( '|<a[^>]+?' . $preg_target . '[^>]*>([^>]+?)</a>|', $para, $context );
7079
7080 // If the URL isn't in a link context, keep looking.
7081 if ( empty( $context ) ) {
7082 continue;
7083 }
7084
7085 /*
7086 * We're going to use this fake tag to mark the context in a bit.
7087 * The marker is needed in case the link text appears more than once in the paragraph.
7088 */
7089 $excerpt = preg_replace( '|\</?wpcontext\>|', '', $para );
7090
7091 // prevent really long link text
7092 if ( strlen( $context[1] ) > 100 ) {
7093 $context[1] = substr( $context[1], 0, 100 ) . '…';
7094 }
7095
7096 $marker = '<wpcontext>' . $context[1] . '</wpcontext>'; // Set up our marker.
7097 $excerpt = str_replace( $context[0], $marker, $excerpt ); // Swap out the link for our marker.
7098 $excerpt = strip_tags( $excerpt, '<wpcontext>' ); // Strip all tags but our context marker.
7099 $excerpt = trim( $excerpt );
7100 $preg_marker = preg_quote( $marker, '|' );
7101 $excerpt = preg_replace( "|.*?\s(.{0,100}$preg_marker.{0,100})\s.*|s", '$1', $excerpt );
7102 $excerpt = strip_tags( $excerpt ); // YES, again, to remove the marker wrapper.
7103 break;
7104 }
7105 }
7106
7107 if ( empty( $context ) ) { // Link to target not found.
7108 return $this->pingback_error( 17, __( 'The source URL does not contain a link to the target URL, and so cannot be used as a source.' ) );
7109 }
7110
7111 $pagelinkedfrom = str_replace( '&', '&', $pagelinkedfrom );
7112
7113 $context = '[…] ' . esc_html( $excerpt ) . ' […]';
7114 $pagelinkedfrom = $this->escape( $pagelinkedfrom );
7115
7116 $comment_post_id = (int) $post_id;
7117 $comment_author = $title;
7118 $comment_author_email = '';
7119 $this->escape( $comment_author );
7120 $comment_author_url = $pagelinkedfrom;
7121 $comment_content = $context;
7122 $this->escape( $comment_content );
7123 $comment_type = 'pingback';
7124
7125 $commentdata = array(
7126 'comment_post_ID' => $comment_post_id,
7127 );
7128
7129 $commentdata += compact(
7130 'comment_author',
7131 'comment_author_url',
7132 'comment_author_email',
7133 'comment_content',
7134 'comment_type',
7135 'remote_source',
7136 'remote_source_original'
7137 );
7138
7139 $comment_id = wp_new_comment( $commentdata );
7140
7141 if ( is_wp_error( $comment_id ) ) {
7142 return $this->pingback_error( 0, $comment_id->get_error_message() );
7143 }
7144
7145 /**
7146 * Fires after a post pingback has been sent.
7147 *
7148 * @since 0.71
7149 *
7150 * @param int $comment_id Comment ID.
7151 */
7152 do_action( 'pingback_post', $comment_id );
7153
7154 /* translators: 1: URL of the page linked from, 2: URL of the page linked to. */
7155 return sprintf( __( 'Pingback from %1$s to %2$s registered. Keep the web talking! :-)' ), $pagelinkedfrom, $pagelinkedto );
7156 }
7157
7158 /**
7159 * Retrieves an array of URLs that pingbacked the given URL.
7160 *
7161 * Specs on http://www.aquarionics.com/misc/archives/blogite/0198.html
7162 *
7163 * @since 1.5.0
7164 *
7165 * @global wpdb $wpdb WordPress database abstraction object.
7166 *
7167 * @param string $url
7168 * @return array|IXR_Error
7169 */
7170 public function pingback_extensions_getPingbacks( $url ) {
7171 global $wpdb;
7172
7173 /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */
7174 do_action( 'xmlrpc_call', 'pingback.extensions.getPingbacks', $url, $this );
7175
7176 $url = $this->escape( $url );
7177
7178 $post_id = url_to_postid( $url );
7179 if ( ! $post_id ) {
7180 // We aren't sure that the resource is available and/or pingback enabled.
7181 return $this->pingback_error( 33, __( 'The specified target URL cannot be used as a target. It either does not exist, or it is not a pingback-enabled resource.' ) );
7182 }
7183
7184 $actual_post = get_post( $post_id, ARRAY_A );
7185
7186 if ( ! $actual_post ) {
7187 // No such post = resource not found.
7188 return $this->pingback_error( 32, __( 'The specified target URL does not exist.' ) );
7189 }
7190
7191 $comments = $wpdb->get_results( $wpdb->prepare( "SELECT comment_author_url, comment_content, comment_author_IP, comment_type FROM $wpdb->comments WHERE comment_post_ID = %d", $post_id ) );
7192
7193 if ( ! $comments ) {
7194 return array();
7195 }
7196
7197 $pingbacks = array();
7198 foreach ( $comments as $comment ) {
7199 if ( 'pingback' === $comment->comment_type ) {
7200 $pingbacks[] = $comment->comment_author_url;
7201 }
7202 }
7203
7204 return $pingbacks;
7205 }
7206
7207 /**
7208 * Sends a pingback error based on the given error code and message.
7209 *
7210 * @since 3.6.0
7211 *
7212 * @param int $code Error code.
7213 * @param string $message Error message.
7214 * @return IXR_Error Error object.
7215 */
7216 protected function pingback_error( $code, $message ) {
7217 /**
7218 * Filters the XML-RPC pingback error return.
7219 *
7220 * @since 3.5.1
7221 *
7222 * @param IXR_Error $error An IXR_Error object containing the error code and message.
7223 */
7224 return apply_filters( 'xmlrpc_pingback_error', new IXR_Error( $code, $message ) );
7225 }
7226}
7227