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


Generated : Sat Sep 19 08:20:30 2026 Cross-referenced by PHPXref