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