diff -r 346c88efed21 -r 5e2f62d02dcd wp/wp-admin/includes/class-wp-press-this.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wp/wp-admin/includes/class-wp-press-this.php Tue Jun 09 03:35:32 2015 +0200 @@ -0,0 +1,1466 @@ + apply_filters( 'press_this_redirect_in_parent', false ), + ); + } + + /** + * Get the source's images and save them locally, for posterity, unless we can't. + * + * @since 4.2.0 + * @access public + * + * @param int $post_id Post ID. + * @param string $content Optional. Current expected markup for Press This. Expects slashed. Default empty. + * @return string New markup with old image URLs replaced with the local attachment ones if swapped. + */ + public function side_load_images( $post_id, $content = '' ) { + $content = wp_unslash( $content ); + + if ( preg_match_all( '/]+>/', $content, $matches ) && current_user_can( 'upload_files' ) ) { + foreach ( (array) $matches[0] as $image ) { + // This is inserted from our JS so HTML attributes should always be in double quotes. + if ( ! preg_match( '/src="([^"]+)"/', $image, $url_matches ) ) { + continue; + } + + $image_src = $url_matches[1]; + + // Don't try to sideload a file without a file extension, leads to WP upload error. + if ( ! preg_match( '/[^\?]+\.(?:jpe?g|jpe|gif|png)(?:\?|$)/i', $image_src ) ) { + continue; + } + + // Sideload image, which gives us a new image src. + $new_src = media_sideload_image( $image_src, $post_id, null, 'src' ); + + if ( ! is_wp_error( $new_src ) ) { + // Replace the POSTED content with correct uploaded ones. + // Need to do it in two steps so we don't replace links to the original image if any. + $new_image = str_replace( $image_src, $new_src, $image ); + $content = str_replace( $image, $new_image, $content ); + } + } + } + + // Edxpected slashed + return wp_slash( $content ); + } + + /** + * AJAX handler for saving the post as draft or published. + * + * @since 4.2.0 + * @access public + */ + public function save_post() { + if ( empty( $_POST['post_ID'] ) || ! $post_id = (int) $_POST['post_ID'] ) { + wp_send_json_error( array( 'errorMessage' => __( 'Missing post ID.' ) ) ); + } + + if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'update-post_' . $post_id ) || + ! current_user_can( 'edit_post', $post_id ) ) { + + wp_send_json_error( array( 'errorMessage' => __( 'Invalid post.' ) ) ); + } + + $post = array( + 'ID' => $post_id, + 'post_title' => ( ! empty( $_POST['post_title'] ) ) ? sanitize_text_field( trim( $_POST['post_title'] ) ) : '', + 'post_content' => ( ! empty( $_POST['post_content'] ) ) ? trim( $_POST['post_content'] ) : '', + 'post_type' => 'post', + 'post_status' => 'draft', + 'post_format' => ( ! empty( $_POST['post_format'] ) ) ? sanitize_text_field( $_POST['post_format'] ) : '', + 'tax_input' => ( ! empty( $_POST['tax_input'] ) ) ? $_POST['tax_input'] : array(), + 'post_category' => ( ! empty( $_POST['post_category'] ) ) ? $_POST['post_category'] : array(), + ); + + if ( ! empty( $_POST['post_status'] ) && 'publish' === $_POST['post_status'] ) { + if ( current_user_can( 'publish_posts' ) ) { + $post['post_status'] = 'publish'; + } else { + $post['post_status'] = 'pending'; + } + } + + $post['post_content'] = $this->side_load_images( $post_id, $post['post_content'] ); + + $updated = wp_update_post( $post, true ); + + if ( is_wp_error( $updated ) ) { + wp_send_json_error( array( 'errorMessage' => $updated->get_error_message() ) ); + } else { + if ( isset( $post['post_format'] ) ) { + if ( current_theme_supports( 'post-formats', $post['post_format'] ) ) { + set_post_format( $post_id, $post['post_format'] ); + } elseif ( $post['post_format'] ) { + set_post_format( $post_id, false ); + } + } + + if ( 'publish' === get_post_status( $post_id ) ) { + $redirect = get_post_permalink( $post_id ); + } else { + $redirect = false; + } + + /** + * Filter the URL to redirect to when Press This saves. + * + * @since 4.2.0 + * + * @param string $url Redirect URL. If `$status` is 'publish', this will be the post permalink. + * Otherwise, the default is false resulting in no redirect. + * @param int $post_id Post ID. + * @param string $status Post status. + */ + $redirect = apply_filters( 'press_this_save_redirect', $redirect, $post_id, $post['post_status'] ); + + if ( $redirect ) { + wp_send_json_success( array( 'redirect' => $redirect ) ); + } else { + wp_send_json_success( array( 'postSaved' => true ) ); + } + } + } + + /** + * AJAX handler for adding a new category. + * + * @since 4.2.0 + * @access public + */ + public function add_category() { + if ( false === wp_verify_nonce( $_POST['new_cat_nonce'], 'add-category' ) ) { + wp_send_json_error(); + } + + $taxonomy = get_taxonomy( 'category' ); + + if ( ! current_user_can( $taxonomy->cap->edit_terms ) || empty( $_POST['name'] ) ) { + wp_send_json_error(); + } + + $parent = isset( $_POST['parent'] ) && (int) $_POST['parent'] > 0 ? (int) $_POST['parent'] : 0; + $names = explode( ',', $_POST['name'] ); + $added = $data = array(); + + foreach ( $names as $cat_name ) { + $cat_name = trim( $cat_name ); + $cat_nicename = sanitize_title( $cat_name ); + + if ( empty( $cat_nicename ) ) { + continue; + } + + // @todo Find a more performant to check existence, maybe get_term() with a separate parent check. + if ( ! $cat_id = term_exists( $cat_name, $taxonomy->name, $parent ) ) { + $cat_id = wp_insert_term( $cat_name, $taxonomy->name, array( 'parent' => $parent ) ); + } + + if ( is_wp_error( $cat_id ) ) { + continue; + } elseif ( is_array( $cat_id ) ) { + $cat_id = $cat_id['term_id']; + } + + $added[] = $cat_id; + } + + if ( empty( $added ) ) { + wp_send_json_error( array( 'errorMessage' => __( 'This category cannot be added. Please change the name and try again.' ) ) ); + } + + foreach ( $added as $new_cat_id ) { + $new_cat = get_category( $new_cat_id ); + + if ( is_wp_error( $new_cat ) ) { + wp_send_json_error( array( 'errorMessage' => __( 'Error while adding the category. Please try again later.' ) ) ); + } + + $data[] = array( + 'term_id' => $new_cat->term_id, + 'name' => $new_cat->name, + 'parent' => $new_cat->parent, + ); + } + wp_send_json_success( $data ); + } + + /** + * Downloads the source's HTML via server-side call for the given URL. + * + * @since 4.2.0 + * @access public + * + * @param string $url URL to scan. + * @return string Source's HTML sanitized markup + */ + public function fetch_source_html( $url ) { + // Download source page to tmp file. + $source_tmp_file = ( ! empty( $url ) ) ? download_url( $url, 30 ) : ''; + $source_content = ''; + + if ( ! is_wp_error( $source_tmp_file ) && file_exists( $source_tmp_file ) ) { + + // Get the content of the source page from the tmp file.. + $source_content = wp_kses( + @file_get_contents( $source_tmp_file ), + array( + 'img' => array( + 'src' => array(), + 'width' => array(), + 'height' => array(), + ), + 'iframe' => array( + 'src' => array(), + ), + 'link' => array( + 'rel' => array(), + 'itemprop' => array(), + 'href' => array(), + ), + 'meta' => array( + 'property' => array(), + 'name' => array(), + 'content' => array(), + ) + ) + ); + + // All done with backward compatibility. Let's do some cleanup, for good measure :) + unlink( $source_tmp_file ); + + } else if ( is_wp_error( $source_tmp_file ) ) { + $source_content = new WP_Error( 'upload-error', sprintf( __( 'Error: %s' ), sprintf( __( 'Could not download the source URL (native error: %s).' ), $source_tmp_file->get_error_message() ) ) ); + } else if ( ! file_exists( $source_tmp_file ) ) { + $source_content = new WP_Error( 'no-local-file', sprintf( __( 'Error: %s' ), __( 'Could not save or locate the temporary download file for the source URL.' ) ) ); + } + + return $source_content; + } + + /** + * Utility method to limit an array to 50 values. + * + * @ignore + * @since 4.2.0 + * + * @param array $value Array to limit. + * @return array Original array if fewer than 50 values, limited array, empty array otherwise. + */ + private function _limit_array( $value ) { + if ( is_array( $value ) ) { + if ( count( $value ) > 50 ) { + return array_slice( $value, 0, 50 ); + } + + return $value; + } + + return array(); + } + + /** + * Utility method to limit the length of a given string to 5,000 characters. + * + * @ignore + * @since 4.2.0 + * + * @param string $value String to limit. + * @return bool|int|string If boolean or integer, that value. If a string, the original value + * if fewer than 5,000 characters, a truncated version, otherwise an + * empty string. + */ + private function _limit_string( $value ) { + $return = ''; + + if ( is_numeric( $value ) || is_bool( $value ) ) { + $return = $value; + } else if ( is_string( $value ) ) { + if ( mb_strlen( $value ) > 5000 ) { + $return = mb_substr( $value, 0, 5000 ); + } else { + $return = $value; + } + + $return = html_entity_decode( $return, ENT_QUOTES, 'UTF-8' ); + $return = sanitize_text_field( trim( $return ) ); + } + + return $return; + } + + /** + * Utility method to limit a given URL to 2,048 characters. + * + * @ignore + * @since 4.2.0 + * + * @param string $url URL to check for length and validity. + * @return string Escaped URL if of valid length (< 2048) and makeup. Empty string otherwise. + */ + private function _limit_url( $url ) { + if ( ! is_string( $url ) ) { + return ''; + } + + // HTTP 1.1 allows 8000 chars but the "de-facto" standard supported in all current browsers is 2048. + if ( strlen( $url ) > 2048 ) { + return ''; // Return empty rather than a truncated/invalid URL + } + + // Does not look like an URL. + if ( ! preg_match( '/^([!#$&-;=?-\[\]_a-z~]|%[0-9a-fA-F]{2})+$/', $url ) ) { + return ''; + } + + // If the URL is root-relative, prepend the protocol and domain name + if ( $url && $this->domain && preg_match( '%^/[^/]+%', $url ) ) { + $url = $this->domain . $url; + } + + // Not absolute or protocol-relative URL. + if ( ! preg_match( '%^(?:https?:)?//[^/]+%', $url ) ) { + return ''; + } + + return esc_url_raw( $url, array( 'http', 'https' ) ); + } + + /** + * Utility method to limit image source URLs. + * + * Excluded URLs include share-this type buttons, loaders, spinners, spacers, WP interface images, + * tiny buttons or thumbs, mathtag.com or quantserve.com images, or the WP stats gif. + * + * @ignore + * @since 4.2.0 + * + * @param string $src Image source URL. + * @return string If not matched an excluded URL type, the original URL, empty string otherwise. + */ + private function _limit_img( $src ) { + $src = $this->_limit_url( $src ); + + if ( preg_match( '/\/ad[sx]{1}?\//', $src ) ) { + // Ads + return ''; + } else if ( preg_match( '/(\/share-?this[^\.]+?\.[a-z0-9]{3,4})(\?.*)?$/', $src ) ) { + // Share-this type button + return ''; + } else if ( preg_match( '/\/(spinner|loading|spacer|blank|rss)\.(gif|jpg|png)/', $src ) ) { + // Loaders, spinners, spacers + return ''; + } else if ( preg_match( '/\/([^\.\/]+[-_]{1})?(spinner|loading|spacer|blank)s?([-_]{1}[^\.\/]+)?\.[a-z0-9]{3,4}/', $src ) ) { + // Fancy loaders, spinners, spacers + return ''; + } else if ( preg_match( '/([^\.\/]+[-_]{1})?thumb[^.]*\.(gif|jpg|png)$/', $src ) ) { + // Thumbnails, too small, usually irrelevant to context + return ''; + } else if ( preg_match( '/\/wp-includes\//', $src ) ) { + // Classic WP interface images + return ''; + } else if ( preg_match( '/[^\d]{1}\d{1,2}x\d+\.(gif|jpg|png)$/', $src ) ) { + // Most often tiny buttons/thumbs (< 100px wide) + return ''; + } else if ( preg_match( '/\/pixel\.(mathtag|quantserve)\.com/', $src ) ) { + // See mathtag.com and https://www.quantcast.com/how-we-do-it/iab-standard-measurement/how-we-collect-data/ + return ''; + } else if ( preg_match( '/\/[gb]\.gif(\?.+)?$/', $src ) ) { + // Classic WP stats gif + return ''; + } + + return $src; + } + + /** + * Limit embed source URLs to specific providers. + * + * Not all core oEmbed providers are supported. Supported providers include YouTube, Vimeo, + * Vine, Daily Motion, SoundCloud, and Twitter. + * + * @ignore + * @since 4.2.0 + * + * @param string $src Embed source URL. + * @return string If not from a supported provider, an empty string. Otherwise, a reformattd embed URL. + */ + private function _limit_embed( $src ) { + $src = $this->_limit_url( $src ); + + if ( preg_match( '/\/\/(m|www)\.youtube\.com\/(embed|v)\/([^\?]+)\?.+$/', $src, $src_matches ) ) { + // Embedded Youtube videos (www or mobile) + $src = 'https://www.youtube.com/watch?v=' . $src_matches[3]; + } else if ( preg_match( '/\/\/player\.vimeo\.com\/video\/([\d]+)([\?\/]{1}.*)?$/', $src, $src_matches ) ) { + // Embedded Vimeo iframe videos + $src = 'https://vimeo.com/' . (int) $src_matches[1]; + } else if ( preg_match( '/\/\/vimeo\.com\/moogaloop\.swf\?clip_id=([\d]+)$/', $src, $src_matches ) ) { + // Embedded Vimeo Flash videos + $src = 'https://vimeo.com/' . (int) $src_matches[1]; + } else if ( preg_match( '/\/\/vine\.co\/v\/([^\/]+)\/embed/', $src, $src_matches ) ) { + // Embedded Vine videos + $src = 'https://vine.co/v/' . $src_matches[1]; + } else if ( preg_match( '/\/\/(www\.)?dailymotion\.com\/embed\/video\/([^\/\?]+)([\/\?]{1}.+)?/', $src, $src_matches ) ) { + // Embedded Daily Motion videos + $src = 'https://www.dailymotion.com/video/' . $src_matches[2]; + } else if ( ! preg_match( '/\/\/(m|www)\.youtube\.com\/watch\?/', $src ) // Youtube video page (www or mobile) + && ! preg_match( '/\/youtu\.be\/.+$/', $src ) // Youtu.be video page + && ! preg_match( '/\/\/vimeo\.com\/[\d]+$/', $src ) // Vimeo video page + && ! preg_match( '/\/\/(www\.)?dailymotion\.com\/video\/.+$/', $src ) // Daily Motion video page + && ! preg_match( '/\/\/soundcloud\.com\/.+$/', $src ) // SoundCloud audio page + && ! preg_match( '/\/\/twitter\.com\/[^\/]+\/status\/[\d]+$/', $src ) // Twitter status page + && ! preg_match( '/\/\/vine\.co\/v\/[^\/]+/', $src ) ) { // Vine video page + $src = ''; + } + + return $src; + } + + /** + * Process a meta data entry from the source. + * + * @ignore + * @since 4.2.0 + * + * @param string $meta_name Meta key name. + * @param mixed $meta_value Meta value. + * @param array $data Associative array of source data. + * @return array Processed data array. + */ + private function _process_meta_entry( $meta_name, $meta_value, $data ) { + if ( preg_match( '/:?(title|description|keywords|site_name)$/', $meta_name ) ) { + $data['_meta'][ $meta_name ] = $meta_value; + } else { + switch ( $meta_name ) { + case 'og:url': + case 'og:video': + case 'og:video:secure_url': + $meta_value = $this->_limit_embed( $meta_value ); + + if ( ! isset( $data['_embeds'] ) ) { + $data['_embeds'] = array(); + } + + if ( ! empty( $meta_value ) && ! in_array( $meta_value, $data['_embeds'] ) ) { + $data['_embeds'][] = $meta_value; + } + + break; + case 'og:image': + case 'og:image:secure_url': + case 'twitter:image0:src': + case 'twitter:image0': + case 'twitter:image:src': + case 'twitter:image': + $meta_value = $this->_limit_img( $meta_value ); + + if ( ! isset( $data['_images'] ) ) { + $data['_images'] = array(); + } + + if ( ! empty( $meta_value ) && ! in_array( $meta_value, $data['_images'] ) ) { + $data['_images'][] = $meta_value; + } + + break; + } + } + + return $data; + } + + /** + * Fetches and parses _meta, _images, and _links data from the source. + * + * @since 4.2.0 + * @access public + * + * @param string $url URL to scan. + * @param array $data Optional. Existing data array if you have one. Default empty array. + * @return array New data array. + */ + public function source_data_fetch_fallback( $url, $data = array() ) { + if ( empty( $url ) ) { + return array(); + } + + // Download source page to tmp file. + $source_content = $this->fetch_source_html( $url ); + if ( is_wp_error( $source_content ) ) { + return array( 'errors' => $source_content->get_error_messages() ); + } + + // Fetch and gather data first, so discovered media is offered 1st to user. + if ( empty( $data['_meta'] ) ) { + $data['_meta'] = array(); + } + + if ( preg_match_all( '/]+>/', $source_content, $matches ) ) { + $items = $this->_limit_array( $matches[0] ); + + foreach ( $items as $value ) { + if ( preg_match( '/(property|name)="([^"]+)"[^>]+content="([^"]+)"/', $value, $new_matches ) ) { + $meta_name = $this->_limit_string( $new_matches[2] ); + $meta_value = $this->_limit_string( $new_matches[3] ); + + // Sanity check. $key is usually things like 'title', 'description', 'keywords', etc. + if ( strlen( $meta_name ) > 100 ) { + continue; + } + + $data = $this->_process_meta_entry( $meta_name, $meta_value, $data ); + } + } + } + + // Fetch and gather data. + if ( empty( $data['_images'] ) ) { + $data['_images'] = array(); + } + + if ( preg_match_all( '/]+>/', $source_content, $matches ) ) { + $items = $this->_limit_array( $matches[0] ); + + foreach ( $items as $value ) { + if ( ( preg_match( '/width=(\'|")(\d+)\\1/i', $value, $new_matches ) && $new_matches[2] < 256 ) || + ( preg_match( '/height=(\'|")(\d+)\\1/i', $value, $new_matches ) && $new_matches[2] < 128 ) ) { + + continue; + } + + if ( preg_match( '/src=(\'|")([^\'"]+)\\1/i', $value, $new_matches ) ) { + $src = $this->_limit_img( $new_matches[2] ); + if ( ! empty( $src ) && ! in_array( $src, $data['_images'] ) ) { + $data['_images'][] = $src; + } + } + } + } + + // Fetch and gather