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


Generated : Sun Sep 27 08:20:30 2026 Cross-referenced by PHPXref