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