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


Generated : Sun Apr 26 08:20:11 2026 Cross-referenced by PHPXref