Diff: STRATO-apps/wordpress_03/app/wp-includes/class-wp-xmlrpc-server.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
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]
24 + class 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( '&amp;', '&', $args[0] );
6922 + $pagelinkedto = str_replace( '&amp;', '&', $args[1] );
6923 + $pagelinkedto = str_replace( '&', '&amp;', $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 ) . '&#8230;';
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( '&', '&amp;', $pagelinkedfrom );
7112 +
7113 + $context = '[&#8230;] ' . esc_html( $excerpt ) . ' [&#8230;]';
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 +