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


Generated : Sun Aug 9 08:20:21 2026 Cross-referenced by PHPXref