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