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