'categoryDescription'] = $cat->description; $struct['categoryName'] = $cat->name; $struct['htmlUrl'] = esc_html(get_category_link($cat->term_id)); $struct['rssUrl'] = esc_html(get_category_feed_link($cat->term_id, 'rss2')); $categories_struct[] = $struct; } } return $categories_struct; } /** * Uploads a file, following your settings. * * Adapted from a patch by Johann Richard. * * @link http://mycvs.org/archives/2004/06/30/file-upload-to-wordpress-in-ecto/ * * @since 1.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $blog_id (unused) * @type string $username * @type string $password * @type array $data * } * @return array|IXR_Error */ public function mw_newMediaObject( $args ) { global $wpdb; $username = $this->escape( $args[1] ); $password = $this->escape( $args[2] ); $data = $args[3]; $name = sanitize_file_name( $data['name'] ); $type = $data['type']; $bits = $data['bits']; if ( !$user = $this->login($username, $password) ) return $this->error; /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'metaWeblog.newMediaObject' ); if ( !current_user_can('upload_files') ) { $this->error = new IXR_Error( 401, __( 'Sorry, you are not allowed to upload files.' ) ); return $this->error; } if ( is_multisite() && upload_is_user_over_quota( false ) ) { $this->error = new IXR_Error( 401, __( 'Sorry, you have used your space allocation.' ) ); return $this->error; } /** * Filters whether to preempt the XML-RPC media upload. * * Passing a truthy value will effectively short-circuit the media upload, * returning that value as a 500 error instead. * * @since 2.1.0 * * @param bool $error Whether to pre-empt the media upload. Default false. */ if ( $upload_err = apply_filters( 'pre_upload_error', false ) ) { return new IXR_Error( 500, $upload_err ); } $upload = wp_upload_bits($name, null, $bits); if ( ! empty($upload['error']) ) { /* translators: 1: file name, 2: error message */ $errorString = sprintf( __( 'Could not write file %1$s (%2$s).' ), $name, $upload['error'] ); return new IXR_Error( 500, $errorString ); } // Construct the attachment array $post_id = 0; if ( ! empty( $data['post_id'] ) ) { $post_id = (int) $data['post_id']; if ( ! current_user_can( 'edit_post', $post_id ) ) return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) ); } $attachment = array( 'post_title' => $name, 'post_content' => '', 'post_type' => 'attachment', 'post_parent' => $post_id, 'post_mime_type' => $type, 'guid' => $upload[ 'url' ] ); // Save the data $id = wp_insert_attachment( $attachment, $upload[ 'file' ], $post_id ); wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $upload['file'] ) ); /** * Fires after a new attachment has been added via the XML-RPC MovableType API. * * @since 3.4.0 * * @param int $id ID of the new attachment. * @param array $args An array of arguments to add the attachment. */ do_action( 'xmlrpc_call_success_mw_newMediaObject', $id, $args ); $struct = $this->_prepare_media_item( get_post( $id ) ); // Deprecated values $struct['id'] = $struct['attachment_id']; $struct['file'] = $struct['title']; $struct['url'] = $struct['link']; return $struct; } /* MovableType API functions * specs on http://www.movabletype.org/docs/mtmanual_programmatic.html */ /** * Retrieve the post titles of recent posts. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $blog_id (unused) * @type string $username * @type string $password * @type int $numberposts * } * @return array|IXR_Error */ public function mt_getRecentPostTitles( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; if ( isset( $args[3] ) ) $query = array( 'numberposts' => absint( $args[3] ) ); else $query = array(); if ( !$user = $this->login($username, $password) ) return $this->error; /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.getRecentPostTitles' ); $posts_list = wp_get_recent_posts( $query ); if ( !$posts_list ) { $this->error = new IXR_Error(500, __('Either there are no posts, or something went wrong.')); return $this->error; } $recent_posts = array(); foreach ($posts_list as $entry) { if ( !current_user_can( 'edit_post', $entry['ID'] ) ) continue; $post_date = $this->_convert_date( $entry['post_date'] ); $post_date_gmt = $this->_convert_date_gmt( $entry['post_date_gmt'], $entry['post_date'] ); $recent_posts[] = array( 'dateCreated' => $post_date, 'userid' => $entry['post_author'], 'postid' => (string) $entry['ID'], 'title' => $entry['post_title'], 'post_status' => $entry['post_status'], 'date_created_gmt' => $post_date_gmt ); } return $recent_posts; } /** * Retrieve list of all categories on blog. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $blog_id (unused) * @type string $username * @type string $password * } * @return array|IXR_Error */ public function mt_getCategoryList( $args ) { $this->escape( $args ); $username = $args[1]; $password = $args[2]; if ( !$user = $this->login($username, $password) ) return $this->error; if ( !current_user_can( 'edit_posts' ) ) return new IXR_Error( 401, __( 'Sorry, you must be able to edit posts on this site in order to view categories.' ) ); /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.getCategoryList' ); $categories_struct = array(); if ( $cats = get_categories(array('hide_empty' => 0, 'hierarchical' => 0)) ) { foreach ( $cats as $cat ) { $struct = array(); $struct['categoryId'] = $cat->term_id; $struct['categoryName'] = $cat->name; $categories_struct[] = $struct; } } return $categories_struct; } /** * Retrieve post categories. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $post_ID * @type string $username * @type string $password * } * @return array|IXR_Error */ public function mt_getPostCategories( $args ) { $this->escape( $args ); $post_ID = (int) $args[0]; $username = $args[1]; $password = $args[2]; if ( !$user = $this->login($username, $password) ) return $this->error; if ( ! get_post( $post_ID ) ) return new IXR_Error( 404, __( 'Invalid post ID.' ) ); if ( !current_user_can( 'edit_post', $post_ID ) ) return new IXR_Error( 401, __( 'Sorry, you are not allowed to edit this post.' ) ); /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.getPostCategories' ); $categories = array(); $catids = wp_get_post_categories(intval($post_ID)); // first listed category will be the primary category $isPrimary = true; foreach ( $catids as $catid ) { $categories[] = array( 'categoryName' => get_cat_name($catid), 'categoryId' => (string) $catid, 'isPrimary' => $isPrimary ); $isPrimary = false; } return $categories; } /** * Sets categories for a post. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $post_ID * @type string $username * @type string $password * @type array $categories * } * @return true|IXR_Error True on success. */ public function mt_setPostCategories( $args ) { $this->escape( $args ); $post_ID = (int) $args[0]; $username = $args[1]; $password = $args[2]; $categories = $args[3]; if ( !$user = $this->login($username, $password) ) return $this->error; /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.setPostCategories' ); if ( ! get_post( $post_ID ) ) return new IXR_Error( 404, __( 'Invalid post ID.' ) ); if ( !current_user_can('edit_post', $post_ID) ) return new IXR_Error(401, __('Sorry, you are not allowed to edit this post.')); $catids = array(); foreach ( $categories as $cat ) { $catids[] = $cat['categoryId']; } wp_set_post_categories($post_ID, $catids); return true; } /** * Retrieve an array of methods supported by this server. * * @since 1.5.0 * * @return array */ public function mt_supportedMethods() { /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.supportedMethods' ); return array_keys( $this->methods ); } /** * Retrieve an empty array because we don't support per-post text filters. * * @since 1.5.0 */ public function mt_supportedTextFilters() { /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.supportedTextFilters' ); /** * Filters the MoveableType text filters list for XML-RPC. * * @since 2.2.0 * * @param array $filters An array of text filters. */ return apply_filters( 'xmlrpc_text_filters', array() ); } /** * Retrieve trackbacks sent to a given post. * * @since 1.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param int $post_ID * @return array|IXR_Error */ public function mt_getTrackbackPings( $post_ID ) { global $wpdb; /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.getTrackbackPings' ); $actual_post = get_post($post_ID, ARRAY_A); if ( !$actual_post ) return new IXR_Error(404, __('Sorry, no such post.')); $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) ); if ( !$comments ) return array(); $trackback_pings = array(); foreach ( $comments as $comment ) { if ( 'trackback' == $comment->comment_type ) { $content = $comment->comment_content; $title = substr($content, 8, (strpos($content, '') - 8)); $trackback_pings[] = array( 'pingTitle' => $title, 'pingURL' => $comment->comment_author_url, 'pingIP' => $comment->comment_author_IP ); } } return $trackback_pings; } /** * Sets a post's publish status to 'publish'. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type int $post_ID * @type string $username * @type string $password * } * @return int|IXR_Error */ public function mt_publishPost( $args ) { $this->escape( $args ); $post_ID = (int) $args[0]; $username = $args[1]; $password = $args[2]; if ( !$user = $this->login($username, $password) ) return $this->error; /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'mt.publishPost' ); $postdata = get_post($post_ID, ARRAY_A); if ( ! $postdata ) return new IXR_Error( 404, __( 'Invalid post ID.' ) ); if ( !current_user_can('publish_posts') || !current_user_can('edit_post', $post_ID) ) return new IXR_Error(401, __('Sorry, you are not allowed to publish this post.')); $postdata['post_status'] = 'publish'; // retain old cats $cats = wp_get_post_categories($post_ID); $postdata['post_category'] = $cats; $this->escape($postdata); return wp_update_post( $postdata ); } /* PingBack functions * specs on www.hixie.ch/specs/pingback/pingback */ /** * Retrieves a pingback and registers it. * * @since 1.5.0 * * @param array $args { * Method arguments. Note: arguments must be ordered as documented. * * @type string $pagelinkedfrom * @type string $pagelinkedto * } * @return string|IXR_Error */ public function pingback_ping( $args ) { global $wpdb; /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'pingback.ping' ); $this->escape( $args ); $pagelinkedfrom = str_replace( '&', '&', $args[0] ); $pagelinkedto = str_replace( '&', '&', $args[1] ); $pagelinkedto = str_replace( '&', '&', $pagelinkedto ); /** * Filters the pingback source URI. * * @since 3.6.0 * * @param string $pagelinkedfrom URI of the page linked from. * @param string $pagelinkedto URI of the page linked to. */ $pagelinkedfrom = apply_filters( 'pingback_ping_source_uri', $pagelinkedfrom, $pagelinkedto ); if ( ! $pagelinkedfrom ) return $this->pingback_error( 0, __( 'A valid URL was not provided.' ) ); // Check if the page linked to is in our site $pos1 = strpos($pagelinkedto, str_replace(array('http://www.','http://','https://www.','https://'), '', get_option('home'))); if ( !$pos1 ) return $this->pingback_error( 0, __( 'Is there no link to us?' ) ); // let's find which post is linked to // FIXME: does url_to_postid() cover all these cases already? // if so, then let's use it and drop the old code. $urltest = parse_url($pagelinkedto); if ( $post_ID = url_to_postid($pagelinkedto) ) { // $way } elseif ( isset( $urltest['path'] ) && preg_match('#p/[0-9]{1,}#', $urltest['path'], $match) ) { // the path defines the post_ID (archives/p/XXXX) $blah = explode('/', $match[0]); $post_ID = (int) $blah[1]; } elseif ( isset( $urltest['query'] ) && preg_match('#p=[0-9]{1,}#', $urltest['query'], $match) ) { // the querystring defines the post_ID (?p=XXXX) $blah = explode('=', $match[0]); $post_ID = (int) $blah[1]; } elseif ( isset($urltest['fragment']) ) { // an #anchor is there, it's either... if ( intval($urltest['fragment']) ) { // ...an integer #XXXX (simplest case) $post_ID = (int) $urltest['fragment']; } elseif ( preg_match('/post-[0-9]+/',$urltest['fragment']) ) { // ...a post id in the form 'post-###' $post_ID = preg_replace('/[^0-9]+/', '', $urltest['fragment']); } elseif ( is_string($urltest['fragment']) ) { // ...or a string #title, a little more complicated $title = preg_replace('/[^a-z0-9]/i', '.', $urltest['fragment']); $sql = $wpdb->prepare("SELECT ID FROM $wpdb->posts WHERE post_title RLIKE %s", $title ); if (! ($post_ID = $wpdb->get_var($sql)) ) { // returning unknown error '0' is better than die()ing return $this->pingback_error( 0, '' ); } } } else { // TODO: Attempt to extract a post ID from the given URL return $this->pingback_error( 33, __('The specified target URL cannot be used as a target. It either doesn’t exist, or it is not a pingback-enabled resource.' ) ); } $post_ID = (int) $post_ID; $post = get_post($post_ID); if ( !$post ) // Post_ID not found return $this->pingback_error( 33, __( 'The specified target URL cannot be used as a target. It either doesn’t exist, or it is not a pingback-enabled resource.' ) ); if ( $post_ID == url_to_postid($pagelinkedfrom) ) return $this->pingback_error( 0, __( 'The source URL and the target URL cannot both point to the same resource.' ) ); // Check if pings are on if ( !pings_open($post) ) return $this->pingback_error( 33, __( 'The specified target URL cannot be used as a target. It either doesn’t exist, or it is not a pingback-enabled resource.' ) ); // Let's check that the remote site didn't already pingback this entry if ( $wpdb->get_results( $wpdb->prepare("SELECT * FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_author_url = %s", $post_ID, $pagelinkedfrom) ) ) return $this->pingback_error( 48, __( 'The pingback has already been registered.' ) ); // very stupid, but gives time to the 'from' server to publish ! sleep(1); $remote_ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] ); /** This filter is documented in wp-includes/class-http.php */ $user_agent = apply_filters( 'http_headers_useragent', 'WordPress/' . get_bloginfo( 'version' ) . '; ' . get_bloginfo( 'url' ) ); // Let's check the remote site $http_api_args = array( 'timeout' => 10, 'redirection' => 0, 'limit_response_size' => 153600, // 150 KB 'user-agent' => "$user_agent; verifying pingback from $remote_ip", 'headers' => array( 'X-Pingback-Forwarded-For' => $remote_ip, ), ); $request = wp_safe_remote_get( $pagelinkedfrom, $http_api_args ); $remote_source = $remote_source_original = wp_remote_retrieve_body( $request ); if ( ! $remote_source ) { return $this->pingback_error( 16, __( 'The source URL does not exist.' ) ); } /** * Filters the pingback remote source. * * @since 2.5.0 * * @param string $remote_source Response source for the page linked from. * @param string $pagelinkedto URL of the page linked to. */ $remote_source = apply_filters( 'pre_remote_source', $remote_source, $pagelinkedto ); // Work around bug in strip_tags(): $remote_source = str_replace( ']*>/", "\n\n", $remote_source ); preg_match( '|([^<]*?)|is', $remote_source, $matchtitle ); $title = isset( $matchtitle[1] ) ? $matchtitle[1] : ''; if ( empty( $title ) ) { return $this->pingback_error( 32, __( 'We cannot find a title on that page.' ) ); } $remote_source = strip_tags( $remote_source, '' ); // just keep the tag we need $p = explode( "\n\n", $remote_source ); $preg_target = preg_quote($pagelinkedto, '|'); foreach ( $p as $para ) { if ( strpos($para, $pagelinkedto) !== false ) { // it exists, but is it a link? preg_match("|]+?".$preg_target."[^>]*>([^>]+?)|", $para, $context); // If the URL isn't in a link context, keep looking if ( empty($context) ) continue; // We're going to use this fake tag to mark the context in a bit // the marker is needed in case the link text appears more than once in the paragraph $excerpt = preg_replace('|\|', '', $para); // prevent really long link text if ( strlen($context[1]) > 100 ) $context[1] = substr($context[1], 0, 100) . '…'; $marker = ''.$context[1].''; // set up our marker $excerpt= str_replace($context[0], $marker, $excerpt); // swap out the link for our marker $excerpt = strip_tags($excerpt, ''); // strip all tags but our context marker $excerpt = trim($excerpt); $preg_marker = preg_quote($marker, '|'); $excerpt = preg_replace("|.*?\s(.{0,100}$preg_marker.{0,100})\s.*|s", '$1', $excerpt); $excerpt = strip_tags($excerpt); // YES, again, to remove the marker wrapper break; } } if ( empty($context) ) // Link to target not found 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.' ) ); $pagelinkedfrom = str_replace('&', '&', $pagelinkedfrom); $context = '[…] ' . esc_html( $excerpt ) . ' […]'; $pagelinkedfrom = $this->escape( $pagelinkedfrom ); $comment_post_ID = (int) $post_ID; $comment_author = $title; $comment_author_email = ''; $this->escape($comment_author); $comment_author_url = $pagelinkedfrom; $comment_content = $context; $this->escape($comment_content); $comment_type = 'pingback'; $commentdata = compact( 'comment_post_ID', 'comment_author', 'comment_author_url', 'comment_author_email', 'comment_content', 'comment_type', 'remote_source', 'remote_source_original' ); $comment_ID = wp_new_comment($commentdata); /** * Fires after a post pingback has been sent. * * @since 0.71 * * @param int $comment_ID Comment ID. */ do_action( 'pingback_post', $comment_ID ); /* translators: 1: URL of the page linked from, 2: URL of the page linked to */ return sprintf( __( 'Pingback from %1$s to %2$s registered. Keep the web talking! :-)' ), $pagelinkedfrom, $pagelinkedto ); } /** * Retrieve array of URLs that pingbacked the given URL. * * Specs on http://www.aquarionics.com/misc/archives/blogite/0198.html * * @since 1.5.0 * * @global wpdb $wpdb WordPress database abstraction object. * * @param string $url * @return array|IXR_Error */ public function pingback_extensions_getPingbacks( $url ) { global $wpdb; /** This action is documented in wp-includes/class-wp-xmlrpc-server.php */ do_action( 'xmlrpc_call', 'pingback.extensions.getPingbacks' ); $url = $this->escape( $url ); $post_ID = url_to_postid($url); if ( !$post_ID ) { // We aren't sure that the resource is available and/or pingback enabled return $this->pingback_error( 33, __( 'The specified target URL cannot be used as a target. It either doesn’t exist, or it is not a pingback-enabled resource.' ) ); } $actual_post = get_post($post_ID, ARRAY_A); if ( !$actual_post ) { // No such post = resource not found return $this->pingback_error( 32, __('The specified target URL does not exist.' ) ); } $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) ); if ( !$comments ) return array(); $pingbacks = array(); foreach ( $comments as $comment ) { if ( 'pingback' == $comment->comment_type ) $pingbacks[] = $comment->comment_author_url; } return $pingbacks; } /** * Sends a pingback error based on the given error code and message. * * @since 3.6.0 * * @param int $code Error code. * @param string $message Error message. * @return IXR_Error Error object. */ protected function pingback_error( $code, $message ) { /** * Filters the XML-RPC pingback error return. * * @since 3.5.1 * * @param IXR_Error $error An IXR_Error object containing the error code and message. */ return apply_filters( 'xmlrpc_pingback_error', new IXR_Error( $code, $message ) ); } }