[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ -> class-wp-xmlrpc-server.php (source)

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


Generated : Thu Jun 25 08:20:12 2026 Cross-referenced by PHPXref