wp/wp-admin/includes/class-wp-press-this.php
changeset 5 5e2f62d02dcd
equal deleted inserted replaced
4:346c88efed21 5:5e2f62d02dcd
       
     1 <?php
       
     2 /**
       
     3  * Press This class and display functionality
       
     4  *
       
     5  * @package WordPress
       
     6  * @subpackage Press_This
       
     7  * @since 4.2.0
       
     8  */
       
     9 
       
    10 /**
       
    11  * Press This class.
       
    12  *
       
    13  * @since 4.2.0
       
    14  */
       
    15 class WP_Press_This {
       
    16 
       
    17 	// Used to trigger the bookmarklet update notice.
       
    18 	public $version = 8;
       
    19 
       
    20 	private $images = array();
       
    21 
       
    22 	private $embeds = array();
       
    23 
       
    24 	private $domain = '';
       
    25 
       
    26 	/**
       
    27 	 * Constructor.
       
    28 	 *
       
    29 	 * @since 4.2.0
       
    30 	 * @access public
       
    31 	 */
       
    32 	public function __construct() {}
       
    33 
       
    34 	/**
       
    35 	 * App and site settings data, including i18n strings for the client-side.
       
    36 	 *
       
    37 	 * @since 4.2.0
       
    38 	 * @access public
       
    39 	 *
       
    40 	 * @return array Site settings.
       
    41 	 */
       
    42 	public function site_settings() {
       
    43 		return array(
       
    44 			/**
       
    45 			 * Filter whether or not Press This should redirect the user in the parent window upon save.
       
    46 			 *
       
    47 			 * @since 4.2.0
       
    48 			 *
       
    49 			 * @param bool false Whether to redirect in parent window or not. Default false.
       
    50 			 */
       
    51 			'redirInParent' => apply_filters( 'press_this_redirect_in_parent', false ),
       
    52 		);
       
    53 	}
       
    54 
       
    55 	/**
       
    56 	 * Get the source's images and save them locally, for posterity, unless we can't.
       
    57 	 *
       
    58 	 * @since 4.2.0
       
    59 	 * @access public
       
    60 	 *
       
    61 	 * @param int    $post_id Post ID.
       
    62 	 * @param string $content Optional. Current expected markup for Press This. Expects slashed. Default empty.
       
    63 	 * @return string New markup with old image URLs replaced with the local attachment ones if swapped.
       
    64 	 */
       
    65 	public function side_load_images( $post_id, $content = '' ) {
       
    66 		$content = wp_unslash( $content );
       
    67 
       
    68 		if ( preg_match_all( '/<img [^>]+>/', $content, $matches ) && current_user_can( 'upload_files' ) ) {
       
    69 			foreach ( (array) $matches[0] as $image ) {
       
    70 				// This is inserted from our JS so HTML attributes should always be in double quotes.
       
    71 				if ( ! preg_match( '/src="([^"]+)"/', $image, $url_matches ) ) {
       
    72 					continue;
       
    73 				}
       
    74 
       
    75 				$image_src = $url_matches[1];
       
    76 
       
    77 				// Don't try to sideload a file without a file extension, leads to WP upload error.
       
    78 				if ( ! preg_match( '/[^\?]+\.(?:jpe?g|jpe|gif|png)(?:\?|$)/i', $image_src ) ) {
       
    79 					continue;
       
    80 				}
       
    81 
       
    82 				// Sideload image, which gives us a new image src.
       
    83 				$new_src = media_sideload_image( $image_src, $post_id, null, 'src' );
       
    84 
       
    85 				if ( ! is_wp_error( $new_src ) ) {
       
    86 					// Replace the POSTED content <img> with correct uploaded ones.
       
    87 					// Need to do it in two steps so we don't replace links to the original image if any.
       
    88 					$new_image = str_replace( $image_src, $new_src, $image );
       
    89 					$content = str_replace( $image, $new_image, $content );
       
    90 				}
       
    91 			}
       
    92 		}
       
    93 
       
    94 		// Edxpected slashed
       
    95 		return wp_slash( $content );
       
    96 	}
       
    97 
       
    98 	/**
       
    99 	 * AJAX handler for saving the post as draft or published.
       
   100 	 *
       
   101 	 * @since 4.2.0
       
   102 	 * @access public
       
   103 	 */
       
   104 	public function save_post() {
       
   105 		if ( empty( $_POST['post_ID'] ) || ! $post_id = (int) $_POST['post_ID'] ) {
       
   106 			wp_send_json_error( array( 'errorMessage' => __( 'Missing post ID.' ) ) );
       
   107 		}
       
   108 
       
   109 		if ( empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], 'update-post_' . $post_id ) ||
       
   110 			! current_user_can( 'edit_post', $post_id ) ) {
       
   111 
       
   112 			wp_send_json_error( array( 'errorMessage' => __( 'Invalid post.' ) ) );
       
   113 		}
       
   114 
       
   115 		$post = array(
       
   116 			'ID'            => $post_id,
       
   117 			'post_title'    => ( ! empty( $_POST['post_title'] ) ) ? sanitize_text_field( trim( $_POST['post_title'] ) ) : '',
       
   118 			'post_content'  => ( ! empty( $_POST['post_content'] ) ) ? trim( $_POST['post_content'] ) : '',
       
   119 			'post_type'     => 'post',
       
   120 			'post_status'   => 'draft',
       
   121 			'post_format'   => ( ! empty( $_POST['post_format'] ) ) ? sanitize_text_field( $_POST['post_format'] ) : '',
       
   122 			'tax_input'     => ( ! empty( $_POST['tax_input'] ) ) ? $_POST['tax_input'] : array(),
       
   123 			'post_category' => ( ! empty( $_POST['post_category'] ) ) ? $_POST['post_category'] : array(),
       
   124 		);
       
   125 
       
   126 		if ( ! empty( $_POST['post_status'] ) && 'publish' === $_POST['post_status'] ) {
       
   127 			if ( current_user_can( 'publish_posts' ) ) {
       
   128 				$post['post_status'] = 'publish';
       
   129 			} else {
       
   130 				$post['post_status'] = 'pending';
       
   131 			}
       
   132 		}
       
   133 
       
   134 		$post['post_content'] = $this->side_load_images( $post_id, $post['post_content'] );
       
   135 
       
   136 		$updated = wp_update_post( $post, true );
       
   137 
       
   138 		if ( is_wp_error( $updated ) ) {
       
   139 			wp_send_json_error( array( 'errorMessage' => $updated->get_error_message() ) );
       
   140 		} else {
       
   141 			if ( isset( $post['post_format'] ) ) {
       
   142 				if ( current_theme_supports( 'post-formats', $post['post_format'] ) ) {
       
   143 					set_post_format( $post_id, $post['post_format'] );
       
   144 				} elseif ( $post['post_format'] ) {
       
   145 					set_post_format( $post_id, false );
       
   146 				}
       
   147 			}
       
   148 
       
   149 			if ( 'publish' === get_post_status( $post_id ) ) {
       
   150 				$redirect = get_post_permalink( $post_id );
       
   151 			} else {
       
   152 				$redirect = false;
       
   153 			}
       
   154 
       
   155 			/**
       
   156 			 * Filter the URL to redirect to when Press This saves.
       
   157 			 *
       
   158 			 * @since 4.2.0
       
   159 			 *
       
   160 			 * @param string $url     Redirect URL. If `$status` is 'publish', this will be the post permalink.
       
   161 			 *                        Otherwise, the default is false resulting in no redirect.
       
   162 			 * @param int    $post_id Post ID.
       
   163 			 * @param string $status  Post status.
       
   164 			 */
       
   165 			$redirect = apply_filters( 'press_this_save_redirect', $redirect, $post_id, $post['post_status'] );
       
   166 
       
   167 			if ( $redirect ) {
       
   168 				wp_send_json_success( array( 'redirect' => $redirect ) );
       
   169 			} else {
       
   170 				wp_send_json_success( array( 'postSaved' => true ) );
       
   171 			}
       
   172 		}
       
   173 	}
       
   174 
       
   175 	/**
       
   176 	 * AJAX handler for adding a new category.
       
   177 	 *
       
   178 	 * @since 4.2.0
       
   179 	 * @access public
       
   180 	 */
       
   181 	public function add_category() {
       
   182 		if ( false === wp_verify_nonce( $_POST['new_cat_nonce'], 'add-category' ) ) {
       
   183 			wp_send_json_error();
       
   184 		}
       
   185 
       
   186 		$taxonomy = get_taxonomy( 'category' );
       
   187 
       
   188 		if ( ! current_user_can( $taxonomy->cap->edit_terms ) || empty( $_POST['name'] ) ) {
       
   189 			wp_send_json_error();
       
   190 		}
       
   191 
       
   192 		$parent = isset( $_POST['parent'] ) && (int) $_POST['parent'] > 0 ? (int) $_POST['parent'] : 0;
       
   193 		$names = explode( ',', $_POST['name'] );
       
   194 		$added = $data = array();
       
   195 
       
   196 		foreach ( $names as $cat_name ) {
       
   197 			$cat_name = trim( $cat_name );
       
   198 			$cat_nicename = sanitize_title( $cat_name );
       
   199 
       
   200 			if ( empty( $cat_nicename ) ) {
       
   201 				continue;
       
   202 			}
       
   203 
       
   204 			// @todo Find a more performant to check existence, maybe get_term() with a separate parent check.
       
   205 			if ( ! $cat_id = term_exists( $cat_name, $taxonomy->name, $parent ) ) {
       
   206 				$cat_id = wp_insert_term( $cat_name, $taxonomy->name, array( 'parent' => $parent ) );
       
   207 			}
       
   208 
       
   209 			if ( is_wp_error( $cat_id ) ) {
       
   210 				continue;
       
   211 			} elseif ( is_array( $cat_id ) ) {
       
   212 				$cat_id = $cat_id['term_id'];
       
   213 			}
       
   214 
       
   215 			$added[] = $cat_id;
       
   216 		}
       
   217 
       
   218 		if ( empty( $added ) ) {
       
   219 			wp_send_json_error( array( 'errorMessage' => __( 'This category cannot be added. Please change the name and try again.' ) ) );
       
   220 		}
       
   221 
       
   222 		foreach ( $added as $new_cat_id ) {
       
   223 			$new_cat = get_category( $new_cat_id );
       
   224 
       
   225 			if ( is_wp_error( $new_cat ) ) {
       
   226 				wp_send_json_error( array( 'errorMessage' => __( 'Error while adding the category. Please try again later.' ) ) );
       
   227 			}
       
   228 
       
   229 			$data[] = array(
       
   230 				'term_id' => $new_cat->term_id,
       
   231 				'name' => $new_cat->name,
       
   232 				'parent' => $new_cat->parent,
       
   233 			);
       
   234 		}
       
   235 		wp_send_json_success( $data );
       
   236 	}
       
   237 
       
   238 	/**
       
   239 	 * Downloads the source's HTML via server-side call for the given URL.
       
   240 	 *
       
   241 	 * @since 4.2.0
       
   242 	 * @access public
       
   243 	 *
       
   244 	 * @param string $url URL to scan.
       
   245 	 * @return string Source's HTML sanitized markup
       
   246 	 */
       
   247 	public function fetch_source_html( $url ) {
       
   248 		// Download source page to tmp file.
       
   249 		$source_tmp_file = ( ! empty( $url ) ) ? download_url( $url, 30 ) : '';
       
   250 		$source_content  = '';
       
   251 
       
   252 		if ( ! is_wp_error( $source_tmp_file ) && file_exists( $source_tmp_file ) ) {
       
   253 
       
   254 			// Get the content of the source page from the tmp file..
       
   255 			$source_content = wp_kses(
       
   256 				@file_get_contents( $source_tmp_file ),
       
   257 				array(
       
   258 					'img' => array(
       
   259 						'src'      => array(),
       
   260 						'width'    => array(),
       
   261 						'height'   => array(),
       
   262 					),
       
   263 					'iframe' => array(
       
   264 						'src'      => array(),
       
   265 					),
       
   266 					'link' => array(
       
   267 						'rel'      => array(),
       
   268 						'itemprop' => array(),
       
   269 						'href'     => array(),
       
   270 					),
       
   271 					'meta' => array(
       
   272 						'property' => array(),
       
   273 						'name'     => array(),
       
   274 						'content'  => array(),
       
   275 					)
       
   276 				)
       
   277 			);
       
   278 
       
   279 			// All done with backward compatibility. Let's do some cleanup, for good measure :)
       
   280 			unlink( $source_tmp_file );
       
   281 
       
   282 		} else if ( is_wp_error( $source_tmp_file ) ) {
       
   283 			$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() ) ) );
       
   284 		} else if ( ! file_exists( $source_tmp_file ) ) {
       
   285 			$source_content = new WP_Error( 'no-local-file',  sprintf( __( 'Error: %s' ), __( 'Could not save or locate the temporary download file for the source URL.' ) ) );
       
   286 		}
       
   287 
       
   288 		return $source_content;
       
   289 	}
       
   290 
       
   291 	/**
       
   292 	 * Utility method to limit an array to 50 values.
       
   293 	 *
       
   294 	 * @ignore
       
   295 	 * @since 4.2.0
       
   296 	 *
       
   297 	 * @param array $value Array to limit.
       
   298 	 * @return array Original array if fewer than 50 values, limited array, empty array otherwise.
       
   299 	 */
       
   300 	private function _limit_array( $value ) {
       
   301 		if ( is_array( $value ) ) {
       
   302 			if ( count( $value ) > 50 ) {
       
   303 				return array_slice( $value, 0, 50 );
       
   304 			}
       
   305 
       
   306 			return $value;
       
   307 		}
       
   308 
       
   309 		return array();
       
   310 	}
       
   311 
       
   312 	/**
       
   313 	 * Utility method to limit the length of a given string to 5,000 characters.
       
   314 	 *
       
   315 	 * @ignore
       
   316 	 * @since 4.2.0
       
   317 	 *
       
   318 	 * @param string $value String to limit.
       
   319 	 * @return bool|int|string If boolean or integer, that value. If a string, the original value
       
   320 	 *                         if fewer than 5,000 characters, a truncated version, otherwise an
       
   321 	 *                         empty string.
       
   322 	 */
       
   323 	private function _limit_string( $value ) {
       
   324 		$return = '';
       
   325 
       
   326 		if ( is_numeric( $value ) || is_bool( $value ) ) {
       
   327 			$return = $value;
       
   328 		} else if ( is_string( $value ) ) {
       
   329 			if ( mb_strlen( $value ) > 5000 ) {
       
   330 				$return = mb_substr( $value, 0, 5000 );
       
   331 			} else {
       
   332 				$return = $value;
       
   333 			}
       
   334 
       
   335 			$return = html_entity_decode( $return, ENT_QUOTES, 'UTF-8' );
       
   336 			$return = sanitize_text_field( trim( $return ) );
       
   337 		}
       
   338 
       
   339 		return $return;
       
   340 	}
       
   341 
       
   342 	/**
       
   343 	 * Utility method to limit a given URL to 2,048 characters.
       
   344 	 *
       
   345 	 * @ignore
       
   346 	 * @since 4.2.0
       
   347 	 *
       
   348 	 * @param string $url URL to check for length and validity.
       
   349 	 * @return string Escaped URL if of valid length (< 2048) and makeup. Empty string otherwise.
       
   350 	 */
       
   351 	private function _limit_url( $url ) {
       
   352 		if ( ! is_string( $url ) ) {
       
   353 			return '';
       
   354 		}
       
   355 
       
   356 		// HTTP 1.1 allows 8000 chars but the "de-facto" standard supported in all current browsers is 2048.
       
   357 		if ( strlen( $url ) > 2048 ) {
       
   358 			return ''; // Return empty rather than a truncated/invalid URL
       
   359 		}
       
   360 
       
   361 		// Does not look like an URL.
       
   362 		if ( ! preg_match( '/^([!#$&-;=?-\[\]_a-z~]|%[0-9a-fA-F]{2})+$/', $url ) ) {
       
   363 			return '';
       
   364 		}
       
   365 
       
   366 		// If the URL is root-relative, prepend the protocol and domain name
       
   367 		if ( $url && $this->domain && preg_match( '%^/[^/]+%', $url ) ) {
       
   368 			$url = $this->domain . $url;
       
   369 		}
       
   370 
       
   371 		// Not absolute or protocol-relative URL.
       
   372 		if ( ! preg_match( '%^(?:https?:)?//[^/]+%', $url ) ) {
       
   373 			return '';
       
   374 		}
       
   375 
       
   376 		return esc_url_raw( $url, array( 'http', 'https' ) );
       
   377 	}
       
   378 
       
   379 	/**
       
   380 	 * Utility method to limit image source URLs.
       
   381 	 *
       
   382 	 * Excluded URLs include share-this type buttons, loaders, spinners, spacers, WP interface images,
       
   383 	 * tiny buttons or thumbs, mathtag.com or quantserve.com images, or the WP stats gif.
       
   384 	 *
       
   385 	 * @ignore
       
   386 	 * @since 4.2.0
       
   387 	 *
       
   388 	 * @param string $src Image source URL.
       
   389 	 * @return string If not matched an excluded URL type, the original URL, empty string otherwise.
       
   390 	 */
       
   391 	private function _limit_img( $src ) {
       
   392 		$src = $this->_limit_url( $src );
       
   393 
       
   394 		if ( preg_match( '/\/ad[sx]{1}?\//', $src ) ) {
       
   395 			// Ads
       
   396 			return '';
       
   397 		} else if ( preg_match( '/(\/share-?this[^\.]+?\.[a-z0-9]{3,4})(\?.*)?$/', $src ) ) {
       
   398 			// Share-this type button
       
   399 			return '';
       
   400 		} else if ( preg_match( '/\/(spinner|loading|spacer|blank|rss)\.(gif|jpg|png)/', $src ) ) {
       
   401 			// Loaders, spinners, spacers
       
   402 			return '';
       
   403 		} else if ( preg_match( '/\/([^\.\/]+[-_]{1})?(spinner|loading|spacer|blank)s?([-_]{1}[^\.\/]+)?\.[a-z0-9]{3,4}/', $src ) ) {
       
   404 			// Fancy loaders, spinners, spacers
       
   405 			return '';
       
   406 		} else if ( preg_match( '/([^\.\/]+[-_]{1})?thumb[^.]*\.(gif|jpg|png)$/', $src ) ) {
       
   407 			// Thumbnails, too small, usually irrelevant to context
       
   408 			return '';
       
   409 		} else if ( preg_match( '/\/wp-includes\//', $src ) ) {
       
   410 			// Classic WP interface images
       
   411 			return '';
       
   412 		} else if ( preg_match( '/[^\d]{1}\d{1,2}x\d+\.(gif|jpg|png)$/', $src ) ) {
       
   413 			// Most often tiny buttons/thumbs (< 100px wide)
       
   414 			return '';
       
   415 		} else if ( preg_match( '/\/pixel\.(mathtag|quantserve)\.com/', $src ) ) {
       
   416 			// See mathtag.com and https://www.quantcast.com/how-we-do-it/iab-standard-measurement/how-we-collect-data/
       
   417 			return '';
       
   418 		} else if ( preg_match( '/\/[gb]\.gif(\?.+)?$/', $src ) ) {
       
   419 			// Classic WP stats gif
       
   420 			return '';
       
   421 		}
       
   422 
       
   423 		return $src;
       
   424 	}
       
   425 
       
   426 	/**
       
   427 	 * Limit embed source URLs to specific providers.
       
   428 	 *
       
   429 	 * Not all core oEmbed providers are supported. Supported providers include YouTube, Vimeo,
       
   430 	 * Vine, Daily Motion, SoundCloud, and Twitter.
       
   431 	 *
       
   432 	 * @ignore
       
   433 	 * @since 4.2.0
       
   434 	 *
       
   435 	 * @param string $src Embed source URL.
       
   436 	 * @return string If not from a supported provider, an empty string. Otherwise, a reformattd embed URL.
       
   437 	 */
       
   438 	private function _limit_embed( $src ) {
       
   439 		$src = $this->_limit_url( $src );
       
   440 
       
   441 		if ( preg_match( '/\/\/(m|www)\.youtube\.com\/(embed|v)\/([^\?]+)\?.+$/', $src, $src_matches ) ) {
       
   442 			// Embedded Youtube videos (www or mobile)
       
   443 			$src = 'https://www.youtube.com/watch?v=' . $src_matches[3];
       
   444 		} else if ( preg_match( '/\/\/player\.vimeo\.com\/video\/([\d]+)([\?\/]{1}.*)?$/', $src, $src_matches ) ) {
       
   445 			// Embedded Vimeo iframe videos
       
   446 			$src = 'https://vimeo.com/' . (int) $src_matches[1];
       
   447 		} else if ( preg_match( '/\/\/vimeo\.com\/moogaloop\.swf\?clip_id=([\d]+)$/', $src, $src_matches ) ) {
       
   448 			// Embedded Vimeo Flash videos
       
   449 			$src = 'https://vimeo.com/' . (int) $src_matches[1];
       
   450 		} else if ( preg_match( '/\/\/vine\.co\/v\/([^\/]+)\/embed/', $src, $src_matches ) ) {
       
   451 			// Embedded Vine videos
       
   452 			$src = 'https://vine.co/v/' . $src_matches[1];
       
   453 		} else if ( preg_match( '/\/\/(www\.)?dailymotion\.com\/embed\/video\/([^\/\?]+)([\/\?]{1}.+)?/', $src, $src_matches ) ) {
       
   454 			// Embedded Daily Motion videos
       
   455 			$src = 'https://www.dailymotion.com/video/' . $src_matches[2];
       
   456 		} else if ( ! preg_match( '/\/\/(m|www)\.youtube\.com\/watch\?/', $src )          // Youtube video page (www or mobile)
       
   457 		            && ! preg_match( '/\/youtu\.be\/.+$/', $src )                         // Youtu.be video page
       
   458 		            && ! preg_match( '/\/\/vimeo\.com\/[\d]+$/', $src )                   // Vimeo video page
       
   459 		            && ! preg_match( '/\/\/(www\.)?dailymotion\.com\/video\/.+$/', $src ) // Daily Motion video page
       
   460 		            && ! preg_match( '/\/\/soundcloud\.com\/.+$/', $src )                 // SoundCloud audio page
       
   461 		            && ! preg_match( '/\/\/twitter\.com\/[^\/]+\/status\/[\d]+$/', $src ) // Twitter status page
       
   462 		            && ! preg_match( '/\/\/vine\.co\/v\/[^\/]+/', $src ) ) {              // Vine video page
       
   463 			$src = '';
       
   464 		}
       
   465 
       
   466 		return $src;
       
   467 	}
       
   468 
       
   469 	/**
       
   470 	 * Process a meta data entry from the source.
       
   471 	 *
       
   472 	 * @ignore
       
   473 	 * @since 4.2.0
       
   474 	 *
       
   475 	 * @param string $meta_name  Meta key name.
       
   476 	 * @param mixed  $meta_value Meta value.
       
   477 	 * @param array  $data       Associative array of source data.
       
   478 	 * @return array Processed data array.
       
   479 	 */
       
   480 	private function _process_meta_entry( $meta_name, $meta_value, $data ) {
       
   481 		if ( preg_match( '/:?(title|description|keywords|site_name)$/', $meta_name ) ) {
       
   482 			$data['_meta'][ $meta_name ] = $meta_value;
       
   483 		} else {
       
   484 			switch ( $meta_name ) {
       
   485 				case 'og:url':
       
   486 				case 'og:video':
       
   487 				case 'og:video:secure_url':
       
   488 					$meta_value = $this->_limit_embed( $meta_value );
       
   489 
       
   490 					if ( ! isset( $data['_embeds'] ) ) {
       
   491 						$data['_embeds'] = array();
       
   492 					}
       
   493 
       
   494 					if ( ! empty( $meta_value ) && ! in_array( $meta_value, $data['_embeds'] ) ) {
       
   495 						$data['_embeds'][] = $meta_value;
       
   496 					}
       
   497 
       
   498 					break;
       
   499 				case 'og:image':
       
   500 				case 'og:image:secure_url':
       
   501 				case 'twitter:image0:src':
       
   502 				case 'twitter:image0':
       
   503 				case 'twitter:image:src':
       
   504 				case 'twitter:image':
       
   505 					$meta_value = $this->_limit_img( $meta_value );
       
   506 
       
   507 					if ( ! isset( $data['_images'] ) ) {
       
   508 						$data['_images'] = array();
       
   509 					}
       
   510 
       
   511 					if ( ! empty( $meta_value ) && ! in_array( $meta_value, $data['_images'] ) ) {
       
   512 						$data['_images'][] = $meta_value;
       
   513 					}
       
   514 
       
   515 					break;
       
   516 			}
       
   517 		}
       
   518 
       
   519 		return $data;
       
   520 	}
       
   521 
       
   522 	/**
       
   523 	 * Fetches and parses _meta, _images, and _links data from the source.
       
   524 	 *
       
   525 	 * @since 4.2.0
       
   526 	 * @access public
       
   527 	 *
       
   528 	 * @param string $url  URL to scan.
       
   529 	 * @param array  $data Optional. Existing data array if you have one. Default empty array.
       
   530 	 * @return array New data array.
       
   531 	 */
       
   532 	public function source_data_fetch_fallback( $url, $data = array() ) {
       
   533 		if ( empty( $url ) ) {
       
   534 			return array();
       
   535 		}
       
   536 
       
   537 		// Download source page to tmp file.
       
   538 		$source_content = $this->fetch_source_html( $url );
       
   539 		if ( is_wp_error( $source_content ) ) {
       
   540 			return array( 'errors' => $source_content->get_error_messages() );
       
   541 		}
       
   542 
       
   543 		// Fetch and gather <meta> data first, so discovered media is offered 1st to user.
       
   544 		if ( empty( $data['_meta'] ) ) {
       
   545 			$data['_meta'] = array();
       
   546 		}
       
   547 
       
   548 		if ( preg_match_all( '/<meta [^>]+>/', $source_content, $matches ) ) {
       
   549 			$items = $this->_limit_array( $matches[0] );
       
   550 
       
   551 			foreach ( $items as $value ) {
       
   552 				if ( preg_match( '/(property|name)="([^"]+)"[^>]+content="([^"]+)"/', $value, $new_matches ) ) {
       
   553 					$meta_name  = $this->_limit_string( $new_matches[2] );
       
   554 					$meta_value = $this->_limit_string( $new_matches[3] );
       
   555 
       
   556 					// Sanity check. $key is usually things like 'title', 'description', 'keywords', etc.
       
   557 					if ( strlen( $meta_name ) > 100 ) {
       
   558 						continue;
       
   559 					}
       
   560 
       
   561 					$data = $this->_process_meta_entry( $meta_name, $meta_value, $data );
       
   562 				}
       
   563 			}
       
   564 		}
       
   565 
       
   566 		// Fetch and gather <img> data.
       
   567 		if ( empty( $data['_images'] ) ) {
       
   568 			$data['_images'] = array();
       
   569 		}
       
   570 
       
   571 		if ( preg_match_all( '/<img [^>]+>/', $source_content, $matches ) ) {
       
   572 			$items = $this->_limit_array( $matches[0] );
       
   573 
       
   574 			foreach ( $items as $value ) {
       
   575 				if ( ( preg_match( '/width=(\'|")(\d+)\\1/i', $value, $new_matches ) && $new_matches[2] < 256 ) ||
       
   576 					( preg_match( '/height=(\'|")(\d+)\\1/i', $value, $new_matches ) && $new_matches[2] < 128 ) ) {
       
   577 
       
   578 					continue;
       
   579 				}
       
   580 
       
   581 				if ( preg_match( '/src=(\'|")([^\'"]+)\\1/i', $value, $new_matches ) ) {
       
   582 					$src = $this->_limit_img( $new_matches[2] );
       
   583 					if ( ! empty( $src ) && ! in_array( $src, $data['_images'] ) ) {
       
   584 						$data['_images'][] = $src;
       
   585 					}
       
   586 				}
       
   587 			}
       
   588 		}
       
   589 
       
   590 		// Fetch and gather <iframe> data.
       
   591 		if ( empty( $data['_embeds'] ) ) {
       
   592 			$data['_embeds'] = array();
       
   593 		}
       
   594 
       
   595 		if ( preg_match_all( '/<iframe [^>]+>/', $source_content, $matches ) ) {
       
   596 			$items = $this->_limit_array( $matches[0] );
       
   597 
       
   598 			foreach ( $items as $value ) {
       
   599 				if ( preg_match( '/src=(\'|")([^\'"]+)\\1/', $value, $new_matches ) ) {
       
   600 					$src = $this->_limit_embed( $new_matches[2] );
       
   601 
       
   602 					if ( ! empty( $src ) && ! in_array( $src, $data['_embeds'] ) ) {
       
   603 						$data['_embeds'][] = $src;
       
   604 					}
       
   605 				}
       
   606 			}
       
   607 		}
       
   608 
       
   609 		// Fetch and gather <link> data.
       
   610 		if ( empty( $data['_links'] ) ) {
       
   611 			$data['_links'] = array();
       
   612 		}
       
   613 
       
   614 		if ( preg_match_all( '/<link [^>]+>/', $source_content, $matches ) ) {
       
   615 			$items = $this->_limit_array( $matches[0] );
       
   616 
       
   617 			foreach ( $items as $value ) {
       
   618 				if ( preg_match( '/rel=["\'](canonical|shortlink|icon)["\']/i', $value, $matches_rel ) && preg_match( '/href=[\'"]([^\'" ]+)[\'"]/i', $value, $matches_url ) ) {
       
   619 					$rel = $matches_rel[1];
       
   620 					$url = $this->_limit_url( $matches_url[1] );
       
   621 
       
   622 					if ( ! empty( $url ) && empty( $data['_links'][ $rel ] ) ) {
       
   623 						$data['_links'][ $rel ] = $url;
       
   624 					}
       
   625 				}
       
   626 			}
       
   627 		}
       
   628 
       
   629 		return $data;
       
   630 	}
       
   631 
       
   632 	/**
       
   633 	 * Handles backward-compat with the legacy version of Press This by supporting its query string params.
       
   634 	 *
       
   635 	 * @since 4.2.0
       
   636 	 * @access public
       
   637 	 *
       
   638 	 * @return array
       
   639 	 */
       
   640 	public function merge_or_fetch_data() {
       
   641 		// Get data from $_POST and $_GET, as appropriate ($_POST > $_GET), to remain backward compatible.
       
   642 		$data = array();
       
   643 
       
   644 		// Only instantiate the keys we want. Sanity check and sanitize each one.
       
   645 		foreach ( array( 'u', 's', 't', 'v' ) as $key ) {
       
   646 			if ( ! empty( $_POST[ $key ] ) ) {
       
   647 				$value = wp_unslash( $_POST[ $key ] );
       
   648 			} else if ( ! empty( $_GET[ $key ] ) ) {
       
   649 				$value = wp_unslash( $_GET[ $key ] );
       
   650 			} else {
       
   651 				continue;
       
   652 			}
       
   653 
       
   654 			if ( 'u' === $key ) {
       
   655 				$value = $this->_limit_url( $value );
       
   656 
       
   657 				if ( preg_match( '%^(?:https?:)?//[^/]+%i', $value, $domain_match ) ) {
       
   658 					$this->domain = $domain_match[0];
       
   659 				}
       
   660 			} else {
       
   661 				$value = $this->_limit_string( $value );
       
   662 			}
       
   663 
       
   664 			if ( ! empty( $value ) ) {
       
   665 				$data[ $key ] = $value;
       
   666 			}
       
   667 		}
       
   668 
       
   669 		/**
       
   670 		 * Filter whether to enable in-source media discovery in Press This.
       
   671 		 *
       
   672 		 * @since 4.2.0
       
   673 		 *
       
   674 		 * @param bool $enable Whether to enable media discovery.
       
   675 		 */
       
   676 		if ( apply_filters( 'enable_press_this_media_discovery', true ) ) {
       
   677 			/*
       
   678 			 * If no title, _images, _embed, and _meta was passed via $_POST, fetch data from source as fallback,
       
   679 			 * making PT fully backward compatible with the older bookmarklet.
       
   680 			 */
       
   681 			if ( empty( $_POST ) && ! empty( $data['u'] ) ) {
       
   682 				$data = $this->source_data_fetch_fallback( $data['u'], $data );
       
   683 			} else {
       
   684 				foreach ( array( '_images', '_embeds' ) as $type ) {
       
   685 					if ( empty( $_POST[ $type ] ) ) {
       
   686 						continue;
       
   687 					}
       
   688 
       
   689 					$data[ $type ] = array();
       
   690 					$items = $this->_limit_array( $_POST[ $type ] );
       
   691 
       
   692 					foreach ( $items as $key => $value ) {
       
   693 						if ( $type === '_images' ) {
       
   694 							$value = $this->_limit_img( wp_unslash( $value ) );
       
   695 						} else {
       
   696 							$value = $this->_limit_embed( wp_unslash( $value ) );
       
   697 						}
       
   698 
       
   699 						if ( ! empty( $value ) ) {
       
   700 							$data[ $type ][] = $value;
       
   701 						}
       
   702 					}
       
   703 				}
       
   704 
       
   705 				foreach ( array( '_meta', '_links' ) as $type ) {
       
   706 					if ( empty( $_POST[ $type ] ) ) {
       
   707 						continue;
       
   708 					}
       
   709 
       
   710 					$data[ $type ] = array();
       
   711 					$items = $this->_limit_array( $_POST[ $type ] );
       
   712 
       
   713 					foreach ( $items as $key => $value ) {
       
   714 						// Sanity check. These are associative arrays, $key is usually things like 'title', 'description', 'keywords', etc.
       
   715 						if ( empty( $key ) || strlen( $key ) > 100 ) {
       
   716 							continue;
       
   717 						}
       
   718 
       
   719 						if ( $type === '_meta' ) {
       
   720 							$value = $this->_limit_string( wp_unslash( $value ) );
       
   721 
       
   722 							if ( ! empty( $value ) ) {
       
   723 								$data = $this->_process_meta_entry( $key, $value, $data );
       
   724 							}
       
   725 						} else {
       
   726 							if ( in_array( $key, array( 'canonical', 'shortlink', 'icon' ), true ) ) {
       
   727 								$data[ $type ][ $key ] = $this->_limit_url( wp_unslash( $value ) );
       
   728 							}
       
   729 						}
       
   730 					}
       
   731 				}
       
   732 			}
       
   733 		}
       
   734 
       
   735 		/**
       
   736 		 * Filter the Press This data array.
       
   737 		 *
       
   738 		 * @since 4.2.0
       
   739 		 *
       
   740 		 * @param array $data Press This Data array.
       
   741 		 */
       
   742 		return apply_filters( 'press_this_data', $data );
       
   743 	}
       
   744 
       
   745 	/**
       
   746 	 * Adds another stylesheet inside TinyMCE.
       
   747 	 *
       
   748 	 * @since 4.2.0
       
   749 	 * @access public
       
   750 	 *
       
   751 	 * @param string $styles URL to editor stylesheet.
       
   752 	 * @return string Possibly modified stylesheets list.
       
   753 	 */
       
   754 	public function add_editor_style( $styles ) {
       
   755 		if ( ! empty( $styles ) ) {
       
   756 			$styles .= ',';
       
   757 		}
       
   758 
       
   759 		$press_this = admin_url( 'css/press-this-editor.css' );
       
   760 		if ( is_rtl() ) {
       
   761 			$press_this = str_replace( '.css', '-rtl.css', $press_this );
       
   762 		}
       
   763 
       
   764 		return $styles . $press_this;
       
   765 	}
       
   766 
       
   767 	/**
       
   768 	 * Outputs the post format selection HTML.
       
   769 	 *
       
   770 	 * @since 4.2.0
       
   771 	 * @access public
       
   772 	 *
       
   773 	 * @param WP_Post $post Post object.
       
   774 	 */
       
   775 	public function post_formats_html( $post ) {
       
   776 		if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post->post_type, 'post-formats' ) ) {
       
   777 			$post_formats = get_theme_support( 'post-formats' );
       
   778 
       
   779 			if ( is_array( $post_formats[0] ) ) {
       
   780 				$post_format = get_post_format( $post->ID );
       
   781 
       
   782 				if ( ! $post_format ) {
       
   783 					$post_format = '0';
       
   784 				}
       
   785 
       
   786 				// Add in the current one if it isn't there yet, in case the current theme doesn't support it.
       
   787 				if ( $post_format && ! in_array( $post_format, $post_formats[0] ) ) {
       
   788 					$post_formats[0][] = $post_format;
       
   789 				}
       
   790 
       
   791 				?>
       
   792 				<div id="post-formats-select">
       
   793 				<fieldset><legend class="screen-reader-text"><?php _e( 'Post formats' ); ?></legend>
       
   794 					<input type="radio" name="post_format" class="post-format" id="post-format-0" value="0" <?php checked( $post_format, '0' ); ?> />
       
   795 					<label for="post-format-0" class="post-format-icon post-format-standard"><?php echo get_post_format_string( 'standard' ); ?></label>
       
   796 					<?php
       
   797 
       
   798 					foreach ( $post_formats[0] as $format ) {
       
   799 						$attr_format = esc_attr( $format );
       
   800 						?>
       
   801 						<br />
       
   802 						<input type="radio" name="post_format" class="post-format" id="post-format-<?php echo $attr_format; ?>" value="<?php echo $attr_format; ?>" <?php checked( $post_format, $format ); ?> />
       
   803 						<label for="post-format-<?php echo $attr_format ?>" class="post-format-icon post-format-<?php echo $attr_format; ?>"><?php echo esc_html( get_post_format_string( $format ) ); ?></label>
       
   804 						<?php
       
   805 					 }
       
   806 
       
   807 					 ?>
       
   808 				</fieldset>
       
   809 				</div>
       
   810 				<?php
       
   811 			}
       
   812 		}
       
   813 	}
       
   814 
       
   815 	/**
       
   816 	 * Outputs the categories HTML.
       
   817 	 *
       
   818 	 * @since 4.2.0
       
   819 	 * @access public
       
   820 	 *
       
   821 	 * @param WP_Post $post Post object.
       
   822 	 */
       
   823 	public function categories_html( $post ) {
       
   824 		$taxonomy = get_taxonomy( 'category' );
       
   825 
       
   826 		if ( current_user_can( $taxonomy->cap->edit_terms ) ) {
       
   827 			?>
       
   828 			<button type="button" class="add-cat-toggle button-subtle" aria-expanded="false">
       
   829 				<span class="dashicons dashicons-plus"></span><span class="screen-reader-text"><?php _e( 'Toggle add category' ); ?></span>
       
   830 			</button>
       
   831 			<div class="add-category is-hidden">
       
   832 				<label class="screen-reader-text" for="new-category"><?php echo $taxonomy->labels->add_new_item; ?></label>
       
   833 				<input type="text" id="new-category" class="add-category-name" placeholder="<?php echo esc_attr( $taxonomy->labels->new_item_name ); ?>" value="" aria-required="true">
       
   834 				<label class="screen-reader-text" for="new-category-parent"><?php echo $taxonomy->labels->parent_item_colon; ?></label>
       
   835 				<div class="postform-wrapper">
       
   836 					<?php
       
   837 					wp_dropdown_categories( array(
       
   838 						'taxonomy'         => 'category',
       
   839 						'hide_empty'       => 0,
       
   840 						'name'             => 'new-category-parent',
       
   841 						'orderby'          => 'name',
       
   842 						'hierarchical'     => 1,
       
   843 						'show_option_none' => '&mdash; ' . $taxonomy->labels->parent_item . ' &mdash;'
       
   844 					) );
       
   845 					?>
       
   846 				</div>
       
   847 				<button type="button" class="add-cat-submit"><?php _e( 'Add' ); ?></button>
       
   848 			</div>
       
   849 			<?php
       
   850 
       
   851 		}
       
   852 		?>
       
   853 		<div class="categories-search-wrapper">
       
   854 			<input id="categories-search" type="search" class="categories-search" placeholder="<?php esc_attr_e( 'Search categories by name' ) ?>">
       
   855 			<label for="categories-search">
       
   856 				<span class="dashicons dashicons-search"></span><span class="screen-reader-text"><?php _e( 'Search categories' ); ?></span>
       
   857 			</label>
       
   858 		</div>
       
   859 		<div aria-label="<?php esc_attr_e( 'Categories' ); ?>">
       
   860 			<ul class="categories-select">
       
   861 				<?php wp_terms_checklist( $post->ID, array( 'taxonomy' => 'category', 'list_only' => true ) ); ?>
       
   862 			</ul>
       
   863 		</div>
       
   864 		<?php
       
   865 	}
       
   866 
       
   867 	/**
       
   868 	 * Outputs the tags HTML.
       
   869 	 *
       
   870 	 * @since 4.2.0
       
   871 	 * @access public
       
   872 	 *
       
   873 	 * @param WP_Post $post Post object.
       
   874 	 */
       
   875 	public function tags_html( $post ) {
       
   876 		$taxonomy              = get_taxonomy( 'post_tag' );
       
   877 		$user_can_assign_terms = current_user_can( $taxonomy->cap->assign_terms );
       
   878 		$esc_tags              = get_terms_to_edit( $post->ID, 'post_tag' );
       
   879 
       
   880 		if ( ! $esc_tags || is_wp_error( $esc_tags ) ) {
       
   881 			$esc_tags = '';
       
   882 		}
       
   883 
       
   884 		?>
       
   885 		<div class="tagsdiv" id="post_tag">
       
   886 			<div class="jaxtag">
       
   887 			<input type="hidden" name="tax_input[post_tag]" class="the-tags" value="<?php echo $esc_tags; // escaped in get_terms_to_edit() ?>">
       
   888 		 	<?php
       
   889 
       
   890 			if ( $user_can_assign_terms ) {
       
   891 				?>
       
   892 				<div class="ajaxtag hide-if-no-js">
       
   893 					<label class="screen-reader-text" for="new-tag-post_tag"><?php _e( 'Tags' ); ?></label>
       
   894 					<p>
       
   895 						<input type="text" id="new-tag-post_tag" name="newtag[post_tag]" class="newtag form-input-tip" size="16" autocomplete="off" value="" aria-describedby="new-tag-desc" />
       
   896 						<button type="button" class="tagadd"><?php _e( 'Add' ); ?></button>
       
   897 					</p>
       
   898 				</div>
       
   899 				<p class="howto" id="new-tag-desc">
       
   900 					<?php echo $taxonomy->labels->separate_items_with_commas; ?>
       
   901 				</p>
       
   902 				<?php
       
   903 			}
       
   904 
       
   905 			?>
       
   906 			</div>
       
   907 			<div class="tagchecklist"></div>
       
   908 		</div>
       
   909 		<?php
       
   910 
       
   911 		if ( $user_can_assign_terms ) {
       
   912 			?>
       
   913 			<button type="button" class="button-reset button-link tagcloud-link" id="link-post_tag"><?php echo $taxonomy->labels->choose_from_most_used; ?></button>
       
   914 			<?php
       
   915 		}
       
   916 	}
       
   917 
       
   918 	/**
       
   919 	 * Get a list of embeds with no duplicates.
       
   920 	 *
       
   921 	 * @since 4.2.0
       
   922 	 * @access public
       
   923 	 *
       
   924 	 * @param array $data The site's data.
       
   925 	 * @returns array Embeds selected to be available.
       
   926 	 */
       
   927 	public function get_embeds( $data ) {
       
   928 		$selected_embeds = array();
       
   929 
       
   930 		if ( ! empty( $data['_embeds'] ) ) {
       
   931 			foreach( $data['_embeds'] as $src ) {
       
   932 				$prot_relative_src = preg_replace( '/^https?:/', '', $src );
       
   933 
       
   934 				if ( in_array( $prot_relative_src, $this->embeds ) ) {
       
   935 					continue;
       
   936 				}
       
   937 
       
   938 				$selected_embeds[] = $src;
       
   939 				$this->embeds[] = $prot_relative_src;
       
   940 			}
       
   941 		}
       
   942 
       
   943 		return $selected_embeds;
       
   944 	}
       
   945 
       
   946 	/**
       
   947 	 * Get a list of images with no duplicates.
       
   948 	 *
       
   949 	 * @since 4.2.0
       
   950 	 * @access public
       
   951 	 *
       
   952 	 * @param array $data The site's data.
       
   953 	 * @returns array
       
   954 	 */
       
   955 	public function get_images( $data ) {
       
   956 		$selected_images = array();
       
   957 
       
   958 		if ( ! empty( $data['_images'] ) ) {
       
   959 			foreach( $data['_images'] as $src ) {
       
   960 				if ( false !== strpos( $src, 'gravatar.com' ) ) {
       
   961 					$src = preg_replace( '%http://[\d]+\.gravatar\.com/%', 'https://secure.gravatar.com/', $src );
       
   962 				}
       
   963 
       
   964 				$prot_relative_src = preg_replace( '/^https?:/', '', $src );
       
   965 
       
   966 				if ( in_array( $prot_relative_src, $this->images ) ||
       
   967 					( false !== strpos( $src, 'avatar' ) && count( $this->images ) > 15 ) ) {
       
   968 					// Skip: already selected or some type of avatar and we've already gathered more than 15 images.
       
   969 					continue;
       
   970 				}
       
   971 
       
   972 				$selected_images[] = $src;
       
   973 				$this->images[] = $prot_relative_src;
       
   974 			}
       
   975 		}
       
   976 
       
   977 		return $selected_images;
       
   978 	}
       
   979 
       
   980 	/**
       
   981 	 * Gets the source page's canonical link, based on passed location and meta data.
       
   982 	 *
       
   983 	 * @since 4.2.0
       
   984 	 * @access public
       
   985 	 *
       
   986  	 * @param array $data The site's data.
       
   987 	 * @returns string Discovered canonical URL, or empty
       
   988 	 */
       
   989 	public function get_canonical_link( $data ) {
       
   990 		$link = '';
       
   991 
       
   992 		if ( ! empty( $data['_links']['canonical'] ) ) {
       
   993 			$link = $data['_links']['canonical'];
       
   994 		} elseif ( ! empty( $data['u'] ) ) {
       
   995 			$link = $data['u'];
       
   996 		} elseif ( ! empty( $data['_meta'] ) ) {
       
   997 			if ( ! empty( $data['_meta']['twitter:url'] ) ) {
       
   998 				$link = $data['_meta']['twitter:url'];
       
   999 			} else if ( ! empty( $data['_meta']['og:url'] ) ) {
       
  1000 				$link = $data['_meta']['og:url'];
       
  1001 			}
       
  1002 		}
       
  1003 
       
  1004 		if ( empty( $link ) && ! empty( $data['_links']['shortlink'] ) ) {
       
  1005 			$link = $data['_links']['shortlink'];
       
  1006 		}
       
  1007 
       
  1008 		return $link;
       
  1009 	}
       
  1010 
       
  1011 	/**
       
  1012 	 * Gets the source page's site name, based on passed meta data.
       
  1013 	 *
       
  1014 	 * @since 4.2.0
       
  1015 	 * @access public
       
  1016 	 *
       
  1017 	 * @param array $data The site's data.
       
  1018 	 * @returns string Discovered site name, or empty
       
  1019 	 */
       
  1020 	public function get_source_site_name( $data ) {
       
  1021 		$name = '';
       
  1022 
       
  1023 		if ( ! empty( $data['_meta'] ) ) {
       
  1024 			if ( ! empty( $data['_meta']['og:site_name'] ) ) {
       
  1025 				$name = $data['_meta']['og:site_name'];
       
  1026 			} else if ( ! empty( $data['_meta']['application-name'] ) ) {
       
  1027 				$name = $data['_meta']['application-name'];
       
  1028 			}
       
  1029 		}
       
  1030 
       
  1031 		return $name;
       
  1032 	}
       
  1033 
       
  1034 	/**
       
  1035 	 * Gets the source page's title, based on passed title and meta data.
       
  1036 	 *
       
  1037 	 * @since 4.2.0
       
  1038 	 * @access public
       
  1039 	 *
       
  1040 	 * @param array $data The site's data.
       
  1041 	 * @returns string Discovered page title, or empty
       
  1042 	 */
       
  1043 	public function get_suggested_title( $data ) {
       
  1044 		$title = '';
       
  1045 
       
  1046 		if ( ! empty( $data['t'] ) ) {
       
  1047 			$title = $data['t'];
       
  1048 		} elseif( ! empty( $data['_meta'] ) ) {
       
  1049 			if ( ! empty( $data['_meta']['twitter:title'] ) ) {
       
  1050 				$title = $data['_meta']['twitter:title'];
       
  1051 			} else if ( ! empty( $data['_meta']['og:title'] ) ) {
       
  1052 				$title = $data['_meta']['og:title'];
       
  1053 			} else if ( ! empty( $data['_meta']['title'] ) ) {
       
  1054 				$title = $data['_meta']['title'];
       
  1055 			}
       
  1056 		}
       
  1057 
       
  1058 		return $title;
       
  1059 	}
       
  1060 
       
  1061 	/**
       
  1062 	 * Gets the source page's suggested content, based on passed data (description, selection, etc).
       
  1063 	 *
       
  1064 	 * Features a blockquoted excerpt, as well as content attribution, if any.
       
  1065 	 *
       
  1066 	 * @since 4.2.0
       
  1067 	 * @access public
       
  1068 	 *
       
  1069 	 * @param array $data The site's data.
       
  1070 	 * @returns string Discovered content, or empty
       
  1071 	 */
       
  1072 	public function get_suggested_content( $data ) {
       
  1073 		$content = $text = '';
       
  1074 
       
  1075 		if ( ! empty( $data['s'] ) ) {
       
  1076 			$text = $data['s'];
       
  1077 		} else if ( ! empty( $data['_meta'] ) ) {
       
  1078 			if ( ! empty( $data['_meta']['twitter:description'] ) ) {
       
  1079 				$text = $data['_meta']['twitter:description'];
       
  1080 			} else if ( ! empty( $data['_meta']['og:description'] ) ) {
       
  1081 				$text = $data['_meta']['og:description'];
       
  1082 			} else if ( ! empty( $data['_meta']['description'] ) ) {
       
  1083 				$text = $data['_meta']['description'];
       
  1084 			}
       
  1085 
       
  1086 			// If there is an ellipsis at the end, the description is very likely auto-generated. Better to ignore it.
       
  1087 			if ( $text && substr( $text, -3 ) === '...' ) {
       
  1088 				$text = '';
       
  1089 			}
       
  1090 		}
       
  1091 
       
  1092 		$default_html = array( 'quote' => '', 'link' => '', 'embed' => '' );
       
  1093 
       
  1094 		require_once( ABSPATH . WPINC . '/class-oembed.php' );
       
  1095 		$oembed = _wp_oembed_get_object();
       
  1096 
       
  1097 		if ( ! empty( $data['u'] ) && $oembed->get_provider( $data['u'], array( 'discover' => false ) ) ) {
       
  1098 			$default_html['embed'] = '<p>[embed]' . $data['u'] . '[/embed]</p>';
       
  1099 
       
  1100 			if ( ! empty( $data['s'] ) ) {
       
  1101 				// If the user has selected some text, do quote it.
       
  1102 				$default_html['quote'] = '<blockquote>%1$s</blockquote>';
       
  1103 			}
       
  1104 		} else {
       
  1105 			$default_html['quote'] = '<blockquote>%1$s</blockquote>';
       
  1106 			$default_html['link'] = '<p>' . _x( 'Source:', 'Used in Press This to indicate where the content comes from.' ) .
       
  1107 				' <em><a href="%1$s">%2$s</a></em></p>';
       
  1108 		}
       
  1109 
       
  1110 		/**
       
  1111 		 * Filter the default HTML for the Press This editor.
       
  1112 		 *
       
  1113 		 * @since 4.2.0
       
  1114 		 *
       
  1115 		 * @param array $default_html Associative array with two keys: 'quote' where %1$s is replaced with the site description
       
  1116 		 *                            or the selected content, and 'link' there %1$s is link href, %2$s is link text.
       
  1117 		 */
       
  1118 		$default_html = apply_filters( 'press_this_suggested_html', $default_html, $data );
       
  1119 
       
  1120 		if ( ! empty( $default_html['embed'] ) ) {
       
  1121 			$content .= $default_html['embed'];
       
  1122 		}
       
  1123 
       
  1124 		// Wrap suggested content in the specified HTML.
       
  1125 		if ( ! empty( $default_html['quote'] ) && $text ) {
       
  1126 			$content .= sprintf( $default_html['quote'], $text );
       
  1127 		}
       
  1128 
       
  1129 		// Add source attribution if there is one available.
       
  1130 		if ( ! empty( $default_html['link'] ) ) {
       
  1131 			$title = $this->get_suggested_title( $data );
       
  1132 			$url = $this->get_canonical_link( $data );
       
  1133 
       
  1134 			if ( ! $title ) {
       
  1135 				$title = $this->get_source_site_name( $data );
       
  1136 			}
       
  1137 
       
  1138 			if ( $url && $title ) {
       
  1139 				$content .= sprintf( $default_html['link'], $url, $title );
       
  1140 			}
       
  1141 		}
       
  1142 
       
  1143 		return $content;
       
  1144 	}
       
  1145 
       
  1146 	/**
       
  1147 	 * Serves the app's base HTML, which in turns calls the load script.
       
  1148 	 *
       
  1149 	 * @since 4.2.0
       
  1150 	 * @access public
       
  1151 	 */
       
  1152 	public function html() {
       
  1153 		global $wp_locale, $wp_version;
       
  1154 
       
  1155 		// Get data, new (POST) and old (GET).
       
  1156 		$data = $this->merge_or_fetch_data();
       
  1157 
       
  1158 		$post_title = $this->get_suggested_title( $data );
       
  1159 
       
  1160 		if ( empty( $title ) ) {
       
  1161 			$title = __( 'New Post' );
       
  1162 		}
       
  1163 
       
  1164 		$post_content = $this->get_suggested_content( $data );
       
  1165 
       
  1166 		// Get site settings array/data.
       
  1167 		$site_settings = $this->site_settings();
       
  1168 
       
  1169 		// Pass the images and embeds
       
  1170 		$images = $this->get_images( $data );
       
  1171 		$embeds = $this->get_embeds( $data );
       
  1172 
       
  1173 		$site_data = array(
       
  1174 			'v' => ! empty( $data['v'] ) ? $data['v'] : '',
       
  1175 			'u' => ! empty( $data['u'] ) ? $data['u'] : '',
       
  1176 			'hasData' => ! empty( $data ),
       
  1177 		);
       
  1178 
       
  1179 		if ( ! empty( $images ) ) {
       
  1180 			$site_data['_images'] = $images;
       
  1181 		}
       
  1182 
       
  1183 		if ( ! empty( $embeds ) ) {
       
  1184 			$site_data['_embeds'] = $embeds;
       
  1185 		}
       
  1186 
       
  1187 		// Add press-this-editor.css and remove theme's editor-style.css, if any.
       
  1188 		remove_editor_styles();
       
  1189 
       
  1190 		add_filter( 'mce_css', array( $this, 'add_editor_style' ) );
       
  1191 
       
  1192 		if ( ! empty( $GLOBALS['is_IE'] ) ) {
       
  1193 			@header( 'X-UA-Compatible: IE=edge' );
       
  1194 		}
       
  1195 
       
  1196 		@header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
       
  1197 
       
  1198 ?>
       
  1199 <!DOCTYPE html>
       
  1200 <!--[if IE 7]>         <html class="lt-ie9 lt-ie8" <?php language_attributes(); ?>> <![endif]-->
       
  1201 <!--[if IE 8]>         <html class="lt-ie9" <?php language_attributes(); ?>> <![endif]-->
       
  1202 <!--[if gt IE 8]><!--> <html <?php language_attributes(); ?>> <!--<![endif]-->
       
  1203 <head>
       
  1204 	<meta http-equiv="Content-Type" content="<?php echo esc_attr( get_bloginfo( 'html_type' ) ); ?>; charset=<?php echo esc_attr( get_option( 'blog_charset' ) ); ?>" />
       
  1205 	<meta name="viewport" content="width=device-width">
       
  1206 	<title><?php esc_html_e( 'Press This!' ) ?></title>
       
  1207 
       
  1208 	<script>
       
  1209 		window.wpPressThisData   = <?php echo wp_json_encode( $site_data ); ?>;
       
  1210 		window.wpPressThisConfig = <?php echo wp_json_encode( $site_settings ); ?>;
       
  1211 	</script>
       
  1212 
       
  1213 	<script type="text/javascript">
       
  1214 		var ajaxurl = '<?php echo esc_js( admin_url( 'admin-ajax.php', 'relative' ) ); ?>',
       
  1215 			pagenow = 'press-this',
       
  1216 			typenow = 'post',
       
  1217 			adminpage = 'press-this-php',
       
  1218 			thousandsSeparator = '<?php echo addslashes( $wp_locale->number_format['thousands_sep'] ); ?>',
       
  1219 			decimalPoint = '<?php echo addslashes( $wp_locale->number_format['decimal_point'] ); ?>',
       
  1220 			isRtl = <?php echo (int) is_rtl(); ?>;
       
  1221 	</script>
       
  1222 
       
  1223 	<?php
       
  1224 		/*
       
  1225 		 * $post->ID is needed for the embed shortcode so we can show oEmbed previews in the editor.
       
  1226 		 * Maybe find a way without it.
       
  1227 		 */
       
  1228 		$post = get_default_post_to_edit( 'post', true );
       
  1229 		$post_ID = (int) $post->ID;
       
  1230 
       
  1231 		wp_enqueue_media( array( 'post' => $post_ID ) );
       
  1232 		wp_enqueue_style( 'press-this' );
       
  1233 		wp_enqueue_script( 'press-this' );
       
  1234 		wp_enqueue_script( 'json2' );
       
  1235 		wp_enqueue_script( 'editor' );
       
  1236 
       
  1237 		$supports_formats = false;
       
  1238 		$post_format      = 0;
       
  1239 
       
  1240 		if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post->post_type, 'post-formats' ) ) {
       
  1241 			$supports_formats = true;
       
  1242 
       
  1243 			if ( ! ( $post_format = get_post_format( $post_ID ) ) ) {
       
  1244 				$post_format = 0;
       
  1245 			}
       
  1246 		}
       
  1247 
       
  1248 		/** This action is documented in wp-admin/admin-header.php */
       
  1249 		do_action( 'admin_enqueue_scripts', 'press-this.php' );
       
  1250 
       
  1251 		/** This action is documented in wp-admin/admin-header.php */
       
  1252 		do_action( 'admin_print_styles-press-this.php' );
       
  1253 
       
  1254 		/** This action is documented in wp-admin/admin-header.php */
       
  1255 		do_action( 'admin_print_styles' );
       
  1256 
       
  1257 		/** This action is documented in wp-admin/admin-header.php */
       
  1258 		do_action( 'admin_print_scripts-press-this.php' );
       
  1259 
       
  1260 		/** This action is documented in wp-admin/admin-header.php */
       
  1261 		do_action( 'admin_print_scripts' );
       
  1262 
       
  1263 		/** This action is documented in wp-admin/admin-header.php */
       
  1264 		do_action( 'admin_head-press-this.php' );
       
  1265 
       
  1266 		/** This action is documented in wp-admin/admin-header.php */
       
  1267 		do_action( 'admin_head' );
       
  1268 	?>
       
  1269 </head>
       
  1270 <?php
       
  1271 
       
  1272 	$admin_body_class  = 'press-this';
       
  1273 	$admin_body_class .= ( is_rtl() ) ? ' rtl' : '';
       
  1274 	$admin_body_class .= ' branch-' . str_replace( array( '.', ',' ), '-', floatval( $wp_version ) );
       
  1275 	$admin_body_class .= ' version-' . str_replace( '.', '-', preg_replace( '/^([.0-9]+).*/', '$1', $wp_version ) );
       
  1276 	$admin_body_class .= ' admin-color-' . sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' );
       
  1277 	$admin_body_class .= ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
       
  1278 
       
  1279 	/** This filter is documented in wp-admin/admin-header.php */
       
  1280 	$admin_body_classes = apply_filters( 'admin_body_class', '' );
       
  1281 
       
  1282 ?>
       
  1283 <body class="wp-admin wp-core-ui <?php echo $admin_body_classes . ' ' . $admin_body_class; ?>">
       
  1284 	<div id="adminbar" class="adminbar">
       
  1285 		<h1 id="current-site" class="current-site">
       
  1286 			<a class="current-site-link" href="<?php echo esc_url( home_url( '/' ) ); ?>" target="_blank" rel="home">
       
  1287 				<span class="dashicons dashicons-wordpress"></span>
       
  1288 				<span class="current-site-name"><?php bloginfo( 'name' ); ?></span>
       
  1289 			</a>
       
  1290 		</h1>
       
  1291 		<button type="button" class="options button-subtle closed">
       
  1292 			<span class="dashicons dashicons-tag on-closed"></span>
       
  1293 			<span class="screen-reader-text on-closed"><?php _e( 'Show post options' ); ?></span>
       
  1294 			<span aria-hidden="true" class="on-open"><?php _e( 'Done' ); ?></span>
       
  1295 			<span class="screen-reader-text on-open"><?php _e( 'Hide post options' ); ?></span>
       
  1296 		</button>
       
  1297 	</div>
       
  1298 
       
  1299 	<div id="scanbar" class="scan">
       
  1300 		<form method="GET">
       
  1301 			<label for="url-scan" class="screen-reader-text"><?php _e( 'Scan site for content' ); ?></label>
       
  1302 			<input type="url" name="u" id="url-scan" class="scan-url" value="" placeholder="<?php esc_attr_e( 'Enter a URL to scan' ) ?>" />
       
  1303 			<input type="submit" name="url-scan-submit" id="url-scan-submit" class="scan-submit" value="<?php esc_attr_e( 'Scan' ) ?>" />
       
  1304 		</form>
       
  1305 	</div>
       
  1306 
       
  1307 	<form id="pressthis-form" method="post" action="post.php" autocomplete="off">
       
  1308 		<input type="hidden" name="post_ID" id="post_ID" value="<?php echo $post_ID; ?>" />
       
  1309 		<input type="hidden" name="action" value="press-this-save-post" />
       
  1310 		<input type="hidden" name="post_status" id="post_status" value="draft" />
       
  1311 		<input type="hidden" name="wp-preview" id="wp-preview" value="" />
       
  1312 		<input type="hidden" name="post_title" id="post_title" value="" />
       
  1313 		<?php
       
  1314 
       
  1315 		wp_nonce_field( 'update-post_' . $post_ID, '_wpnonce', false );
       
  1316 		wp_nonce_field( 'add-category', '_ajax_nonce-add-category', false );
       
  1317 
       
  1318 		?>
       
  1319 
       
  1320 	<div class="wrapper">
       
  1321 		<div class="editor-wrapper">
       
  1322 			<div class="alerts" role="alert" aria-live="assertive" aria-relevant="all" aria-atomic="true">
       
  1323 				<?php
       
  1324 
       
  1325 				if ( isset( $data['v'] ) && $this->version > $data['v'] ) {
       
  1326 					?>
       
  1327 					<p class="alert is-notice">
       
  1328 						<?php printf( __( 'You should upgrade <a href="%s" target="_blank">your bookmarklet</a> to the latest version!' ), admin_url( 'tools.php' ) ); ?>
       
  1329 					</p>
       
  1330 					<?php
       
  1331 				}
       
  1332 
       
  1333 				?>
       
  1334 			</div>
       
  1335 
       
  1336 			<div id="app-container" class="editor">
       
  1337 				<span id="title-container-label" class="post-title-placeholder" aria-hidden="true"><?php _e( 'Post title' ); ?></span>
       
  1338 				<h2 id="title-container" class="post-title" contenteditable="true" spellcheck="true" aria-label="<?php esc_attr_e( 'Post title' ); ?>" tabindex="0"><?php echo esc_html( $post_title ); ?></h2>
       
  1339 
       
  1340 				<div class="media-list-container">
       
  1341 					<div class="media-list-inner-container">
       
  1342 						<h2 class="screen-reader-text"><?php _e( 'Suggested media' ); ?></h2>
       
  1343 						<ul class="media-list"></ul>
       
  1344 					</div>
       
  1345 				</div>
       
  1346 
       
  1347 				<?php
       
  1348 				wp_editor( $post_content, 'pressthis', array(
       
  1349 					'drag_drop_upload' => true,
       
  1350 					'editor_height'    => 600,
       
  1351 					'media_buttons'    => false,
       
  1352 					'textarea_name'    => 'post_content',
       
  1353 					'teeny'            => true,
       
  1354 					'tinymce'          => array(
       
  1355 						'resize'                => false,
       
  1356 						'wordpress_adv_hidden'  => false,
       
  1357 						'add_unload_trigger'    => false,
       
  1358 						'statusbar'             => false,
       
  1359 						'autoresize_min_height' => 600,
       
  1360 						'wp_autoresize_on'      => true,
       
  1361 						'plugins'               => 'lists,media,paste,tabfocus,fullscreen,wordpress,wpautoresize,wpeditimage,wpgallery,wplink,wpview',
       
  1362 						'toolbar1'              => 'bold,italic,bullist,numlist,blockquote,link,unlink',
       
  1363 						'toolbar2'              => 'undo,redo',
       
  1364 					),
       
  1365 					'quicktags' => false,
       
  1366 				) );
       
  1367 
       
  1368 				?>
       
  1369 			</div>
       
  1370 		</div>
       
  1371 
       
  1372 		<div class="options-panel-back is-hidden" tabindex="-1"></div>
       
  1373 		<div class="options-panel is-off-screen is-hidden" tabindex="-1">
       
  1374 			<div class="post-options">
       
  1375 
       
  1376 				<?php if ( $supports_formats ) : ?>
       
  1377 					<button type="button" class="button-reset post-option">
       
  1378 						<span class="dashicons dashicons-admin-post"></span>
       
  1379 						<span class="post-option-title"><?php _ex( 'Format', 'post format' ); ?></span>
       
  1380 						<span class="post-option-contents" id="post-option-post-format"><?php echo esc_html( get_post_format_string( $post_format ) ); ?></span>
       
  1381 						<span class="dashicons post-option-forward"></span>
       
  1382 					</button>
       
  1383 				<?php endif; ?>
       
  1384 
       
  1385 				<button type="button" class="button-reset post-option">
       
  1386 					<span class="dashicons dashicons-category"></span>
       
  1387 					<span class="post-option-title"><?php _e( 'Categories' ); ?></span>
       
  1388 					<span class="dashicons post-option-forward"></span>
       
  1389 				</button>
       
  1390 
       
  1391 				<button type="button" class="button-reset post-option">
       
  1392 					<span class="dashicons dashicons-tag"></span>
       
  1393 					<span class="post-option-title"><?php _e( 'Tags' ); ?></span>
       
  1394 					<span class="dashicons post-option-forward"></span>
       
  1395 				</button>
       
  1396 			</div>
       
  1397 
       
  1398 			<?php if ( $supports_formats ) : ?>
       
  1399 				<div class="setting-modal is-off-screen is-hidden">
       
  1400 					<button type="button" class="button-reset modal-close">
       
  1401 						<span class="dashicons post-option-back"></span>
       
  1402 						<span class="setting-title" aria-hidden="true"><?php _ex( 'Format', 'post format' ); ?></span>
       
  1403 						<span class="screen-reader-text"><?php _e( 'Back to post options' ) ?></span>
       
  1404 					</button>
       
  1405 					<?php $this->post_formats_html( $post ); ?>
       
  1406 				</div>
       
  1407 			<?php endif; ?>
       
  1408 
       
  1409 			<div class="setting-modal is-off-screen is-hidden">
       
  1410 				<button type="button" class="button-reset modal-close">
       
  1411 					<span class="dashicons post-option-back"></span>
       
  1412 					<span class="setting-title" aria-hidden="true"><?php _e( 'Categories' ); ?></span>
       
  1413 					<span class="screen-reader-text"><?php _e( 'Back to post options' ) ?></span>
       
  1414 				</button>
       
  1415 				<?php $this->categories_html( $post ); ?>
       
  1416 			</div>
       
  1417 
       
  1418 			<div class="setting-modal tags is-off-screen is-hidden">
       
  1419 				<button type="button" class="button-reset modal-close">
       
  1420 					<span class="dashicons post-option-back"></span>
       
  1421 					<span class="setting-title" aria-hidden="true"><?php _e( 'Tags' ); ?></span>
       
  1422 					<span class="screen-reader-text"><?php _e( 'Back to post options' ) ?></span>
       
  1423 				</button>
       
  1424 				<?php $this->tags_html( $post ); ?>
       
  1425 			</div>
       
  1426 		</div><!-- .options-panel -->
       
  1427 	</div><!-- .wrapper -->
       
  1428 
       
  1429 	<div class="press-this-actions">
       
  1430 		<div class="pressthis-media-buttons">
       
  1431 			<button type="button" class="insert-media button-subtle" data-editor="pressthis">
       
  1432 				<span class="dashicons dashicons-admin-media"></span>
       
  1433 				<span class="screen-reader-text"><?php _e( 'Add Media' ); ?></span>
       
  1434 			</button>
       
  1435 		</div>
       
  1436 		<div class="post-actions">
       
  1437 			<span class="spinner">&nbsp;</span>
       
  1438 			<button type="button" class="button-subtle draft-button" aria-live="polite">
       
  1439 				<span class="save-draft"><?php _e( 'Save Draft' ); ?></span>
       
  1440 				<span class="saving-draft"><?php _e( 'Saving...' ); ?></span>
       
  1441 			</button>
       
  1442 			<a href="<?php echo esc_url( get_edit_post_link( $post_ID ) ); ?>" class="edit-post-link" style="display: none;" target="_blank"><?php _e( 'Standard Editor' ); ?></a>
       
  1443 			<button type="button" class="button-subtle preview-button"><?php _e( 'Preview' ); ?></button>
       
  1444 			<button type="button" class="button-primary publish-button"><?php echo ( current_user_can( 'publish_posts' ) ) ? __( 'Publish' ) : __( 'Submit for Review' ); ?></button>
       
  1445 		</div>
       
  1446 	</div>
       
  1447 	</form>
       
  1448 
       
  1449 	<?php
       
  1450 	/** This action is documented in wp-admin/admin-footer.php */
       
  1451 	do_action( 'admin_footer' );
       
  1452 
       
  1453 	/** This action is documented in wp-admin/admin-footer.php */
       
  1454 	do_action( 'admin_print_footer_scripts' );
       
  1455 
       
  1456 	/** This action is documented in wp-admin/admin-footer.php */
       
  1457 	do_action( 'admin_footer-press-this.php' );
       
  1458 	?>
       
  1459 </body>
       
  1460 </html>
       
  1461 <?php
       
  1462 		die();
       
  1463 	}
       
  1464 }
       
  1465 
       
  1466 $GLOBALS['wp_press_this'] = new WP_Press_This;