WP_Press_This::save_post()   F
last analyzed

Complexity

Conditions 22
Paths 9600

Size

Total Lines 86
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 22
eloc 44
c 1
b 0
f 0
nc 9600
nop 0
dl 0
loc 86
rs 2.0431

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
			 * Filters 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 $redirect 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
		// Expected 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 ) ||
0 ignored issues
show
Bug introduced by
The variable $post_id does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
110
			! current_user_can( 'edit_post', $post_id ) ) {
111
112
			wp_send_json_error( array( 'errorMessage' => __( 'Invalid post.' ) ) );
113
		}
114
115
		$post_data = 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_data['post_status'] = 'publish';
129
			} else {
130
				$post_data['post_status'] = 'pending';
131
			}
132
		}
133
134
		$post_data['post_content'] = $this->side_load_images( $post_id, $post_data['post_content'] );
135
136
		/**
137
		 * Filters the post data of a Press This post before saving/updating.
138
		 *
139
		 * The {@see 'side_load_images'} action has already run at this point.
140
		 *
141
		 * @since 4.5.0
142
		 *
143
		 * @param array $post_data The post data.
144
		 */
145
		$post_data = apply_filters( 'press_this_save_post', $post_data );
146
147
		$updated = wp_update_post( $post_data, true );
148
149
		if ( is_wp_error( $updated ) ) {
150
			wp_send_json_error( array( 'errorMessage' => $updated->get_error_message() ) );
151
		} else {
152
			if ( isset( $post_data['post_format'] ) ) {
153
				if ( current_theme_supports( 'post-formats', $post_data['post_format'] ) ) {
154
					set_post_format( $post_id, $post_data['post_format'] );
155
				} elseif ( $post_data['post_format'] ) {
156
					set_post_format( $post_id, false );
0 ignored issues
show
Documentation introduced by
false is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
157
				}
158
			}
159
160
			$forceRedirect = false;
161
162
			if ( 'publish' === get_post_status( $post_id ) ) {
163
				$redirect = get_post_permalink( $post_id );
164
			} elseif ( isset( $_POST['pt-force-redirect'] ) && $_POST['pt-force-redirect'] === 'true' ) {
165
				$forceRedirect = true;
166
				$redirect = get_edit_post_link( $post_id, 'js' );
167
			} else {
168
				$redirect = false;
169
			}
170
171
			/**
172
			 * Filters the URL to redirect to when Press This saves.
173
			 *
174
			 * @since 4.2.0
175
			 *
176
			 * @param string $url     Redirect URL. If `$status` is 'publish', this will be the post permalink.
177
			 *                        Otherwise, the default is false resulting in no redirect.
178
			 * @param int    $post_id Post ID.
179
			 * @param string $status  Post status.
180
			 */
181
			$redirect = apply_filters( 'press_this_save_redirect', $redirect, $post_id, $post_data['post_status'] );
182
183
			if ( $redirect ) {
184
				wp_send_json_success( array( 'redirect' => $redirect, 'force' => $forceRedirect ) );
185
			} else {
186
				wp_send_json_success( array( 'postSaved' => true ) );
187
			}
188
		}
189
	}
190
191
	/**
192
	 * Ajax handler for adding a new category.
193
	 *
194
	 * @since 4.2.0
195
	 * @access public
196
	 */
197
	public function add_category() {
198
		if ( false === wp_verify_nonce( $_POST['new_cat_nonce'], 'add-category' ) ) {
199
			wp_send_json_error();
200
		}
201
202
		$taxonomy = get_taxonomy( 'category' );
203
204
		if ( ! current_user_can( $taxonomy->cap->edit_terms ) || empty( $_POST['name'] ) ) {
205
			wp_send_json_error();
206
		}
207
208
		$parent = isset( $_POST['parent'] ) && (int) $_POST['parent'] > 0 ? (int) $_POST['parent'] : 0;
209
		$names = explode( ',', $_POST['name'] );
210
		$added = $data = array();
211
212
		foreach ( $names as $cat_name ) {
213
			$cat_name = trim( $cat_name );
214
			$cat_nicename = sanitize_title( $cat_name );
215
216
			if ( empty( $cat_nicename ) ) {
217
				continue;
218
			}
219
220
			// @todo Find a more performant way to check existence, maybe get_term() with a separate parent check.
221
			if ( term_exists( $cat_name, $taxonomy->name, $parent ) ) {
222
				if ( count( $names ) === 1 ) {
223
					wp_send_json_error( array( 'errorMessage' => __( 'This category already exists.' ) ) );
224
				} else {
225
					continue;
226
				}
227
			}
228
229
			$cat_id = wp_insert_term( $cat_name, $taxonomy->name, array( 'parent' => $parent ) );
230
231
			if ( is_wp_error( $cat_id ) ) {
232
				continue;
233
			} elseif ( is_array( $cat_id ) ) {
234
				$cat_id = $cat_id['term_id'];
235
			}
236
237
			$added[] = $cat_id;
238
		}
239
240
		if ( empty( $added ) ) {
241
			wp_send_json_error( array( 'errorMessage' => __( 'This category cannot be added. Please change the name and try again.' ) ) );
242
		}
243
244
		foreach ( $added as $new_cat_id ) {
245
			$new_cat = get_category( $new_cat_id );
246
247
			if ( is_wp_error( $new_cat ) ) {
248
				wp_send_json_error( array( 'errorMessage' => __( 'Error while adding the category. Please try again later.' ) ) );
249
			}
250
251
			$data[] = array(
252
				'term_id' => $new_cat->term_id,
253
				'name' => $new_cat->name,
254
				'parent' => $new_cat->parent,
255
			);
256
		}
257
		wp_send_json_success( $data );
258
	}
259
260
	/**
261
	 * Downloads the source's HTML via server-side call for the given URL.
262
	 *
263
	 * @since 4.2.0
264
	 * @access public
265
	 *
266
	 * @param string $url URL to scan.
267
	 * @return string Source's HTML sanitized markup
268
	 */
269
	public function fetch_source_html( $url ) {
270
		global $wp_version;
271
272
		if ( empty( $url ) ) {
273
			return new WP_Error( 'invalid-url', __( 'A valid URL was not provided.' ) );
274
		}
275
276
		$remote_url = wp_safe_remote_get( $url, array(
277
			'timeout' => 30,
278
			// Use an explicit user-agent for Press This
279
			'user-agent' => 'Press This (WordPress/' . $wp_version . '); ' . get_bloginfo( 'url' )
280
		) );
281
282
		if ( is_wp_error( $remote_url ) ) {
283
			return $remote_url;
284
		}
285
286
		$allowed_elements = array(
287
			'img' => array(
288
				'src'      => true,
289
				'width'    => true,
290
				'height'   => true,
291
			),
292
			'iframe' => array(
293
				'src'      => true,
294
			),
295
			'link' => array(
296
				'rel'      => true,
297
				'itemprop' => true,
298
				'href'     => true,
299
			),
300
			'meta' => array(
301
				'property' => true,
302
				'name'     => true,
303
				'content'  => true,
304
			)
305
		);
306
307
		$source_content = wp_remote_retrieve_body( $remote_url );
0 ignored issues
show
Bug introduced by
It seems like $remote_url defined by wp_safe_remote_get($url,.... get_bloginfo('url'))) on line 276 can also be of type object<WP_Error>; however, wp_remote_retrieve_body() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
308
		$source_content = wp_kses( $source_content, $allowed_elements );
309
310
		return $source_content;
311
	}
312
313
	/**
314
	 * Utility method to limit an array to 50 values.
315
	 *
316
	 * @ignore
317
	 * @since 4.2.0
318
	 *
319
	 * @param array $value Array to limit.
320
	 * @return array Original array if fewer than 50 values, limited array, empty array otherwise.
321
	 */
322
	private function _limit_array( $value ) {
323
		if ( is_array( $value ) ) {
324
			if ( count( $value ) > 50 ) {
325
				return array_slice( $value, 0, 50 );
326
			}
327
328
			return $value;
329
		}
330
331
		return array();
332
	}
333
334
	/**
335
	 * Utility method to limit the length of a given string to 5,000 characters.
336
	 *
337
	 * @ignore
338
	 * @since 4.2.0
339
	 *
340
	 * @param string $value String to limit.
341
	 * @return bool|int|string If boolean or integer, that value. If a string, the original value
342
	 *                         if fewer than 5,000 characters, a truncated version, otherwise an
343
	 *                         empty string.
344
	 */
345
	private function _limit_string( $value ) {
346
		$return = '';
347
348
		if ( is_numeric( $value ) || is_bool( $value ) ) {
349
			$return = $value;
350
		} else if ( is_string( $value ) ) {
351
			if ( mb_strlen( $value ) > 5000 ) {
352
				$return = mb_substr( $value, 0, 5000 );
353
			} else {
354
				$return = $value;
355
			}
356
357
			$return = html_entity_decode( $return, ENT_QUOTES, 'UTF-8' );
358
			$return = sanitize_text_field( trim( $return ) );
359
		}
360
361
		return $return;
362
	}
363
364
	/**
365
	 * Utility method to limit a given URL to 2,048 characters.
366
	 *
367
	 * @ignore
368
	 * @since 4.2.0
369
	 *
370
	 * @param string $url URL to check for length and validity.
371
	 * @return string Escaped URL if of valid length (< 2048) and makeup. Empty string otherwise.
372
	 */
373
	private function _limit_url( $url ) {
374
		if ( ! is_string( $url ) ) {
375
			return '';
376
		}
377
378
		// HTTP 1.1 allows 8000 chars but the "de-facto" standard supported in all current browsers is 2048.
379
		if ( strlen( $url ) > 2048 ) {
380
			return ''; // Return empty rather than a truncated/invalid URL
381
		}
382
383
		// Does not look like a URL.
384
		if ( ! preg_match( '/^([!#$&-;=?-\[\]_a-z~]|%[0-9a-fA-F]{2})+$/', $url ) ) {
385
			return '';
386
		}
387
388
		// If the URL is root-relative, prepend the protocol and domain name
389
		if ( $url && $this->domain && preg_match( '%^/[^/]+%', $url ) ) {
390
			$url = $this->domain . $url;
391
		}
392
393
		// Not absolute or protocol-relative URL.
394
		if ( ! preg_match( '%^(?:https?:)?//[^/]+%', $url ) ) {
395
			return '';
396
		}
397
398
		return esc_url_raw( $url, array( 'http', 'https' ) );
399
	}
400
401
	/**
402
	 * Utility method to limit image source URLs.
403
	 *
404
	 * Excluded URLs include share-this type buttons, loaders, spinners, spacers, WordPress interface images,
405
	 * tiny buttons or thumbs, mathtag.com or quantserve.com images, or the WordPress.com stats gif.
406
	 *
407
	 * @ignore
408
	 * @since 4.2.0
409
	 *
410
	 * @param string $src Image source URL.
411
	 * @return string If not matched an excluded URL type, the original URL, empty string otherwise.
412
	 */
413
	private function _limit_img( $src ) {
414
		$src = $this->_limit_url( $src );
415
416
		if ( preg_match( '!/ad[sx]?/!i', $src ) ) {
417
			// Ads
418
			return '';
419
		} else if ( preg_match( '!(/share-?this[^.]+?\.[a-z0-9]{3,4})(\?.*)?$!i', $src ) ) {
420
			// Share-this type button
421
			return '';
422
		} else if ( preg_match( '!/(spinner|loading|spacer|blank|rss)\.(gif|jpg|png)!i', $src ) ) {
423
			// Loaders, spinners, spacers
424
			return '';
425
		} else if ( preg_match( '!/([^./]+[-_])?(spinner|loading|spacer|blank)s?([-_][^./]+)?\.[a-z0-9]{3,4}!i', $src ) ) {
426
			// Fancy loaders, spinners, spacers
427
			return '';
428
		} else if ( preg_match( '!([^./]+[-_])?thumb[^.]*\.(gif|jpg|png)$!i', $src ) ) {
429
			// Thumbnails, too small, usually irrelevant to context
430
			return '';
431
		} else if ( false !== stripos( $src, '/wp-includes/' ) ) {
432
			// Classic WordPress interface images
433
			return '';
434
		} else if ( preg_match( '![^\d]\d{1,2}x\d+\.(gif|jpg|png)$!i', $src ) ) {
435
			// Most often tiny buttons/thumbs (< 100px wide)
436
			return '';
437
		} else if ( preg_match( '!/pixel\.(mathtag|quantserve)\.com!i', $src ) ) {
438
			// See mathtag.com and https://www.quantcast.com/how-we-do-it/iab-standard-measurement/how-we-collect-data/
439
			return '';
440
		} else if ( preg_match( '!/[gb]\.gif(\?.+)?$!i', $src ) ) {
441
			// WordPress.com stats gif
442
			return '';
443
		}
444
445
		return $src;
446
	}
447
448
	/**
449
	 * Limit embed source URLs to specific providers.
450
	 *
451
	 * Not all core oEmbed providers are supported. Supported providers include YouTube, Vimeo,
452
	 * Vine, Daily Motion, SoundCloud, and Twitter.
453
	 *
454
	 * @ignore
455
	 * @since 4.2.0
456
	 *
457
	 * @param string $src Embed source URL.
458
	 * @return string If not from a supported provider, an empty string. Otherwise, a reformattd embed URL.
459
	 */
460
	private function _limit_embed( $src ) {
461
		$src = $this->_limit_url( $src );
462
463
		if ( empty( $src ) )
464
			return '';
465
466
		if ( preg_match( '!//(m|www)\.youtube\.com/(embed|v)/([^?]+)\?.+$!i', $src, $src_matches ) ) {
467
			// Embedded Youtube videos (www or mobile)
468
			$src = 'https://www.youtube.com/watch?v=' . $src_matches[3];
469
		} else if ( preg_match( '!//player\.vimeo\.com/video/([\d]+)([?/].*)?$!i', $src, $src_matches ) ) {
470
			// Embedded Vimeo iframe videos
471
			$src = 'https://vimeo.com/' . (int) $src_matches[1];
472
		} else if ( preg_match( '!//vimeo\.com/moogaloop\.swf\?clip_id=([\d]+)$!i', $src, $src_matches ) ) {
473
			// Embedded Vimeo Flash videos
474
			$src = 'https://vimeo.com/' . (int) $src_matches[1];
475
		} else if ( preg_match( '!//vine\.co/v/([^/]+)/embed!i', $src, $src_matches ) ) {
476
			// Embedded Vine videos
477
			$src = 'https://vine.co/v/' . $src_matches[1];
478
		} else if ( preg_match( '!//(www\.)?dailymotion\.com/embed/video/([^/?]+)([/?].+)?!i', $src, $src_matches ) ) {
479
			// Embedded Daily Motion videos
480
			$src = 'https://www.dailymotion.com/video/' . $src_matches[2];
481
		} else {
482
			require_once( ABSPATH . WPINC . '/class-oembed.php' );
483
			$oembed = _wp_oembed_get_object();
484
485
			if ( ! $oembed->get_provider( $src, array( 'discover' => false ) ) ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oembed->get_provider($s...y('discover' => false)) of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
486
				$src = '';
487
			}
488
		}
489
490
		return $src;
491
	}
492
493
	/**
494
	 * Process a meta data entry from the source.
495
	 *
496
	 * @ignore
497
	 * @since 4.2.0
498
	 *
499
	 * @param string $meta_name  Meta key name.
500
	 * @param mixed  $meta_value Meta value.
501
	 * @param array  $data       Associative array of source data.
502
	 * @return array Processed data array.
503
	 */
504
	private function _process_meta_entry( $meta_name, $meta_value, $data ) {
505
		if ( preg_match( '/:?(title|description|keywords|site_name)$/', $meta_name ) ) {
506
			$data['_meta'][ $meta_name ] = $meta_value;
507
		} else {
508
			switch ( $meta_name ) {
509
				case 'og:url':
510
				case 'og:video':
511 View Code Duplication
				case 'og:video:secure_url':
512
					$meta_value = $this->_limit_embed( $meta_value );
513
514
					if ( ! isset( $data['_embeds'] ) ) {
515
						$data['_embeds'] = array();
516
					}
517
518
					if ( ! empty( $meta_value ) && ! in_array( $meta_value, $data['_embeds'] ) ) {
519
						$data['_embeds'][] = $meta_value;
520
					}
521
522
					break;
523
				case 'og:image':
524
				case 'og:image:secure_url':
525
				case 'twitter:image0:src':
526
				case 'twitter:image0':
527
				case 'twitter:image:src':
528 View Code Duplication
				case 'twitter:image':
529
					$meta_value = $this->_limit_img( $meta_value );
530
531
					if ( ! isset( $data['_images'] ) ) {
532
						$data['_images'] = array();
533
					}
534
535
					if ( ! empty( $meta_value ) && ! in_array( $meta_value, $data['_images'] ) ) {
536
						$data['_images'][] = $meta_value;
537
					}
538
539
					break;
540
			}
541
		}
542
543
		return $data;
544
	}
545
546
	/**
547
	 * Fetches and parses _meta, _images, and _links data from the source.
548
	 *
549
	 * @since 4.2.0
550
	 * @access public
551
	 *
552
	 * @param string $url  URL to scan.
553
	 * @param array  $data Optional. Existing data array if you have one. Default empty array.
554
	 * @return array New data array.
555
	 */
556
	public function source_data_fetch_fallback( $url, $data = array() ) {
557
		if ( empty( $url ) ) {
558
			return array();
559
		}
560
561
		// Download source page to tmp file.
562
		$source_content = $this->fetch_source_html( $url );
563
		if ( is_wp_error( $source_content ) ) {
564
			return array( 'errors' => $source_content->get_error_messages() );
565
		}
566
567
		// Fetch and gather <meta> data first, so discovered media is offered 1st to user.
568
		if ( empty( $data['_meta'] ) ) {
569
			$data['_meta'] = array();
570
		}
571
572
		if ( preg_match_all( '/<meta [^>]+>/', $source_content, $matches ) ) {
573
			$items = $this->_limit_array( $matches[0] );
574
575
			foreach ( $items as $value ) {
576
				if ( preg_match( '/(property|name)="([^"]+)"[^>]+content="([^"]+)"/', $value, $new_matches ) ) {
577
					$meta_name  = $this->_limit_string( $new_matches[2] );
578
					$meta_value = $this->_limit_string( $new_matches[3] );
579
580
					// Sanity check. $key is usually things like 'title', 'description', 'keywords', etc.
581
					if ( strlen( $meta_name ) > 100 ) {
582
						continue;
583
					}
584
585
					$data = $this->_process_meta_entry( $meta_name, $meta_value, $data );
586
				}
587
			}
588
		}
589
590
		// Fetch and gather <img> data.
591
		if ( empty( $data['_images'] ) ) {
592
			$data['_images'] = array();
593
		}
594
595
		if ( preg_match_all( '/<img [^>]+>/', $source_content, $matches ) ) {
596
			$items = $this->_limit_array( $matches[0] );
597
598
			foreach ( $items as $value ) {
599
				if ( ( preg_match( '/width=(\'|")(\d+)\\1/i', $value, $new_matches ) && $new_matches[2] < 256 ) ||
600
					( preg_match( '/height=(\'|")(\d+)\\1/i', $value, $new_matches ) && $new_matches[2] < 128 ) ) {
601
602
					continue;
603
				}
604
605 View Code Duplication
				if ( preg_match( '/src=(\'|")([^\'"]+)\\1/i', $value, $new_matches ) ) {
606
					$src = $this->_limit_img( $new_matches[2] );
607
					if ( ! empty( $src ) && ! in_array( $src, $data['_images'] ) ) {
608
						$data['_images'][] = $src;
609
					}
610
				}
611
			}
612
		}
613
614
		// Fetch and gather <iframe> data.
615
		if ( empty( $data['_embeds'] ) ) {
616
			$data['_embeds'] = array();
617
		}
618
619
		if ( preg_match_all( '/<iframe [^>]+>/', $source_content, $matches ) ) {
620
			$items = $this->_limit_array( $matches[0] );
621
622
			foreach ( $items as $value ) {
623 View Code Duplication
				if ( preg_match( '/src=(\'|")([^\'"]+)\\1/', $value, $new_matches ) ) {
624
					$src = $this->_limit_embed( $new_matches[2] );
625
626
					if ( ! empty( $src ) && ! in_array( $src, $data['_embeds'] ) ) {
627
						$data['_embeds'][] = $src;
628
					}
629
				}
630
			}
631
		}
632
633
		// Fetch and gather <link> data.
634
		if ( empty( $data['_links'] ) ) {
635
			$data['_links'] = array();
636
		}
637
638
		if ( preg_match_all( '/<link [^>]+>/', $source_content, $matches ) ) {
639
			$items = $this->_limit_array( $matches[0] );
640
641
			foreach ( $items as $value ) {
642
				if ( preg_match( '/rel=["\'](canonical|shortlink|icon)["\']/i', $value, $matches_rel ) && preg_match( '/href=[\'"]([^\'" ]+)[\'"]/i', $value, $matches_url ) ) {
643
					$rel = $matches_rel[1];
644
					$url = $this->_limit_url( $matches_url[1] );
645
646 View Code Duplication
					if ( ! empty( $url ) && empty( $data['_links'][ $rel ] ) ) {
647
						$data['_links'][ $rel ] = $url;
648
					}
649
				}
650
			}
651
		}
652
653
		return $data;
654
	}
655
656
	/**
657
	 * Handles backward-compat with the legacy version of Press This by supporting its query string params.
658
	 *
659
	 * @since 4.2.0
660
	 * @access public
661
	 *
662
	 * @return array
663
	 */
664
	public function merge_or_fetch_data() {
665
		// Get data from $_POST and $_GET, as appropriate ($_POST > $_GET), to remain backward compatible.
666
		$data = array();
667
668
		// Only instantiate the keys we want. Sanity check and sanitize each one.
669
		foreach ( array( 'u', 's', 't', 'v' ) as $key ) {
670
			if ( ! empty( $_POST[ $key ] ) ) {
671
				$value = wp_unslash( $_POST[ $key ] );
672
			} else if ( ! empty( $_GET[ $key ] ) ) {
673
				$value = wp_unslash( $_GET[ $key ] );
674
			} else {
675
				continue;
676
			}
677
678
			if ( 'u' === $key ) {
679
				$value = $this->_limit_url( $value );
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type array; however, WP_Press_This::_limit_url() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
680
681
				if ( preg_match( '%^(?:https?:)?//[^/]+%i', $value, $domain_match ) ) {
682
					$this->domain = $domain_match[0];
683
				}
684
			} else {
685
				$value = $this->_limit_string( $value );
0 ignored issues
show
Bug introduced by
It seems like $value defined by $this->_limit_string($value) on line 685 can also be of type array; however, WP_Press_This::_limit_string() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
686
			}
687
688
			if ( ! empty( $value ) ) {
689
				$data[ $key ] = $value;
690
			}
691
		}
692
693
		/**
694
		 * Filters whether to enable in-source media discovery in Press This.
695
		 *
696
		 * @since 4.2.0
697
		 *
698
		 * @param bool $enable Whether to enable media discovery.
699
		 */
700
		if ( apply_filters( 'enable_press_this_media_discovery', true ) ) {
701
			/*
702
			 * If no title, _images, _embed, and _meta was passed via $_POST, fetch data from source as fallback,
703
			 * making PT fully backward compatible with the older bookmarklet.
704
			 */
705
			if ( empty( $_POST ) && ! empty( $data['u'] ) ) {
706
				$data = $this->source_data_fetch_fallback( $data['u'], $data );
707
			} else {
708
				foreach ( array( '_images', '_embeds' ) as $type ) {
709
					if ( empty( $_POST[ $type ] ) ) {
710
						continue;
711
					}
712
713
					$data[ $type ] = array();
714
					$items = $this->_limit_array( $_POST[ $type ] );
715
716
					foreach ( $items as $key => $value ) {
717
						if ( $type === '_images' ) {
718
							$value = $this->_limit_img( wp_unslash( $value ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($value) targeting wp_unslash() can also be of type array; however, WP_Press_This::_limit_img() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
719
						} else {
720
							$value = $this->_limit_embed( wp_unslash( $value ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($value) targeting wp_unslash() can also be of type array; however, WP_Press_This::_limit_embed() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
721
						}
722
723
						if ( ! empty( $value ) ) {
724
							$data[ $type ][] = $value;
725
						}
726
					}
727
				}
728
729
				foreach ( array( '_meta', '_links' ) as $type ) {
730
					if ( empty( $_POST[ $type ] ) ) {
731
						continue;
732
					}
733
734
					$data[ $type ] = array();
735
					$items = $this->_limit_array( $_POST[ $type ] );
736
737
					foreach ( $items as $key => $value ) {
738
						// Sanity check. These are associative arrays, $key is usually things like 'title', 'description', 'keywords', etc.
739
						if ( empty( $key ) || strlen( $key ) > 100 ) {
740
							continue;
741
						}
742
743
						if ( $type === '_meta' ) {
744
							$value = $this->_limit_string( wp_unslash( $value ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($value) targeting wp_unslash() can also be of type array; however, WP_Press_This::_limit_string() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
745
746
							if ( ! empty( $value ) ) {
747
								$data = $this->_process_meta_entry( $key, $value, $data );
748
							}
749
						} else {
750
							if ( in_array( $key, array( 'canonical', 'shortlink', 'icon' ), true ) ) {
751
								$data[ $type ][ $key ] = $this->_limit_url( wp_unslash( $value ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($value) targeting wp_unslash() can also be of type array; however, WP_Press_This::_limit_url() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
752
							}
753
						}
754
					}
755
				}
756
			}
757
758
			// Support passing a single image src as `i`
759
			if ( ! empty( $_REQUEST['i'] ) && ( $img_src = $this->_limit_img( wp_unslash( $_REQUEST['i'] ) ) ) ) {
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_REQUEST['i']) targeting wp_unslash() can also be of type array; however, WP_Press_This::_limit_img() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
760
				if ( empty( $data['_images'] ) ) {
761
					$data['_images'] = array( $img_src );
762
				} elseif ( ! in_array( $img_src, $data['_images'], true ) ) {
763
					array_unshift( $data['_images'], $img_src );
764
				}
765
			}
766
		}
767
768
		/**
769
		 * Filters the Press This data array.
770
		 *
771
		 * @since 4.2.0
772
		 *
773
		 * @param array $data Press This Data array.
774
		 */
775
		return apply_filters( 'press_this_data', $data );
776
	}
777
778
	/**
779
	 * Adds another stylesheet inside TinyMCE.
780
	 *
781
	 * @since 4.2.0
782
	 * @access public
783
	 *
784
	 * @param string $styles URL to editor stylesheet.
785
	 * @return string Possibly modified stylesheets list.
786
	 */
787
	public function add_editor_style( $styles ) {
788
		if ( ! empty( $styles ) ) {
789
			$styles .= ',';
790
		}
791
792
		$press_this = admin_url( 'css/press-this-editor.css' );
793
		if ( is_rtl() ) {
794
			$press_this = str_replace( '.css', '-rtl.css', $press_this );
795
		}
796
797
		return $styles . $press_this;
798
	}
799
800
	/**
801
	 * Outputs the post format selection HTML.
802
	 *
803
	 * @since 4.2.0
804
	 * @access public
805
	 *
806
	 * @param WP_Post $post Post object.
807
	 */
808
	public function post_formats_html( $post ) {
809
		if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post->post_type, 'post-formats' ) ) {
810
			$post_formats = get_theme_support( 'post-formats' );
811
812
			if ( is_array( $post_formats[0] ) ) {
813
				$post_format = get_post_format( $post->ID );
814
815
				if ( ! $post_format ) {
816
					$post_format = '0';
817
				}
818
819
				// Add in the current one if it isn't there yet, in case the current theme doesn't support it.
820 View Code Duplication
				if ( $post_format && ! in_array( $post_format, $post_formats[0] ) ) {
821
					$post_formats[0][] = $post_format;
822
				}
823
824
				?>
825
				<div id="post-formats-select">
826
				<fieldset><legend class="screen-reader-text"><?php _e( 'Post Formats' ); ?></legend>
827
					<input type="radio" name="post_format" class="post-format" id="post-format-0" value="0" <?php checked( $post_format, '0' ); ?> />
828
					<label for="post-format-0" class="post-format-icon post-format-standard"><?php echo get_post_format_string( 'standard' ); ?></label>
829
					<?php
830
831
					foreach ( $post_formats[0] as $format ) {
832
						$attr_format = esc_attr( $format );
833
						?>
834
						<br />
835
						<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 ); ?> />
836
						<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>
837
						<?php
838
					 }
839
840
					 ?>
841
				</fieldset>
842
				</div>
843
				<?php
844
			}
845
		}
846
	}
847
848
	/**
849
	 * Outputs the categories HTML.
850
	 *
851
	 * @since 4.2.0
852
	 * @access public
853
	 *
854
	 * @param WP_Post $post Post object.
855
	 */
856
	public function categories_html( $post ) {
857
		$taxonomy = get_taxonomy( 'category' );
858
859
		if ( current_user_can( $taxonomy->cap->edit_terms ) ) {
860
			?>
861
			<button type="button" class="add-cat-toggle button-link" aria-expanded="false">
862
				<span class="dashicons dashicons-plus"></span><span class="screen-reader-text"><?php _e( 'Toggle add category' ); ?></span>
863
			</button>
864
			<div class="add-category is-hidden">
865
				<label class="screen-reader-text" for="new-category"><?php echo $taxonomy->labels->add_new_item; ?></label>
866
				<input type="text" id="new-category" class="add-category-name" placeholder="<?php echo esc_attr( $taxonomy->labels->new_item_name ); ?>" value="" aria-required="true">
867
				<label class="screen-reader-text" for="new-category-parent"><?php echo $taxonomy->labels->parent_item_colon; ?></label>
868
				<div class="postform-wrapper">
869
					<?php
870
					wp_dropdown_categories( array(
871
						'taxonomy'         => 'category',
872
						'hide_empty'       => 0,
873
						'name'             => 'new-category-parent',
874
						'orderby'          => 'name',
875
						'hierarchical'     => 1,
876
						'show_option_none' => '&mdash; ' . $taxonomy->labels->parent_item . ' &mdash;'
877
					) );
878
					?>
879
				</div>
880
				<button type="button" class="add-cat-submit"><?php _e( 'Add' ); ?></button>
881
			</div>
882
			<?php
883
884
		}
885
		?>
886
		<div class="categories-search-wrapper">
887
			<input id="categories-search" type="search" class="categories-search" placeholder="<?php esc_attr_e( 'Search categories by name' ) ?>">
888
			<label for="categories-search">
889
				<span class="dashicons dashicons-search"></span><span class="screen-reader-text"><?php _e( 'Search categories' ); ?></span>
890
			</label>
891
		</div>
892
		<div aria-label="<?php esc_attr_e( 'Categories' ); ?>">
893
			<ul class="categories-select">
894
				<?php wp_terms_checklist( $post->ID, array( 'taxonomy' => 'category', 'list_only' => true ) ); ?>
895
			</ul>
896
		</div>
897
		<?php
898
	}
899
900
	/**
901
	 * Outputs the tags HTML.
902
	 *
903
	 * @since 4.2.0
904
	 * @access public
905
	 *
906
	 * @param WP_Post $post Post object.
907
	 */
908
	public function tags_html( $post ) {
909
		$taxonomy              = get_taxonomy( 'post_tag' );
910
		$user_can_assign_terms = current_user_can( $taxonomy->cap->assign_terms );
911
		$esc_tags              = get_terms_to_edit( $post->ID, 'post_tag' );
912
913
		if ( ! $esc_tags || is_wp_error( $esc_tags ) ) {
914
			$esc_tags = '';
915
		}
916
917
		?>
918
		<div class="tagsdiv" id="post_tag">
919
			<div class="jaxtag">
920
			<input type="hidden" name="tax_input[post_tag]" class="the-tags" value="<?php echo $esc_tags; // escaped in get_terms_to_edit() ?>">
921
		 	<?php
922
923
			if ( $user_can_assign_terms ) {
924
				?>
925
				<div class="ajaxtag hide-if-no-js">
926
					<label class="screen-reader-text" for="new-tag-post_tag"><?php _e( 'Tags' ); ?></label>
927
					<p>
928
						<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" />
929
						<button type="button" class="tagadd"><?php _e( 'Add' ); ?></button>
930
					</p>
931
				</div>
932
				<p class="howto" id="new-tag-desc">
933
					<?php echo $taxonomy->labels->separate_items_with_commas; ?>
934
				</p>
935
				<?php
936
			}
937
938
			?>
939
			</div>
940
			<div class="tagchecklist"></div>
941
		</div>
942
		<?php
943
944
		if ( $user_can_assign_terms ) {
945
			?>
946
			<button type="button" class="button-link tagcloud-link" id="link-post_tag"><?php echo $taxonomy->labels->choose_from_most_used; ?></button>
947
			<?php
948
		}
949
	}
950
951
	/**
952
	 * Get a list of embeds with no duplicates.
953
	 *
954
	 * @since 4.2.0
955
	 * @access public
956
	 *
957
	 * @param array $data The site's data.
958
	 * @return array Embeds selected to be available.
959
	 */
960
	public function get_embeds( $data ) {
961
		$selected_embeds = array();
962
963
		// Make sure to add the Pressed page if it's a valid oembed itself
964
		if ( ! empty ( $data['u'] ) && $this->_limit_embed( $data['u'] ) ) {
965
			$data['_embeds'][] = $data['u'];
966
		}
967
968
		if ( ! empty( $data['_embeds'] ) ) {
969
			foreach ( $data['_embeds'] as $src ) {
970
				$prot_relative_src = preg_replace( '/^https?:/', '', $src );
971
972
				if ( in_array( $prot_relative_src, $this->embeds ) ) {
973
					continue;
974
				}
975
976
				$selected_embeds[] = $src;
977
				$this->embeds[] = $prot_relative_src;
978
			}
979
		}
980
981
		return $selected_embeds;
982
	}
983
984
	/**
985
	 * Get a list of images with no duplicates.
986
	 *
987
	 * @since 4.2.0
988
	 * @access public
989
	 *
990
	 * @param array $data The site's data.
991
	 * @return array
992
	 */
993
	public function get_images( $data ) {
994
		$selected_images = array();
995
996
		if ( ! empty( $data['_images'] ) ) {
997
			foreach ( $data['_images'] as $src ) {
998
				if ( false !== strpos( $src, 'gravatar.com' ) ) {
999
					$src = preg_replace( '%http://[\d]+\.gravatar\.com/%', 'https://secure.gravatar.com/', $src );
1000
				}
1001
1002
				$prot_relative_src = preg_replace( '/^https?:/', '', $src );
1003
1004
				if ( in_array( $prot_relative_src, $this->images ) ||
1005
					( false !== strpos( $src, 'avatar' ) && count( $this->images ) > 15 ) ) {
1006
					// Skip: already selected or some type of avatar and we've already gathered more than 15 images.
1007
					continue;
1008
				}
1009
1010
				$selected_images[] = $src;
1011
				$this->images[] = $prot_relative_src;
1012
			}
1013
		}
1014
1015
		return $selected_images;
1016
	}
1017
1018
	/**
1019
	 * Gets the source page's canonical link, based on passed location and meta data.
1020
	 *
1021
	 * @since 4.2.0
1022
	 * @access public
1023
	 *
1024
 	 * @param array $data The site's data.
1025
	 * @return string Discovered canonical URL, or empty
1026
	 */
1027
	public function get_canonical_link( $data ) {
1028
		$link = '';
1029
1030
		if ( ! empty( $data['_links']['canonical'] ) ) {
1031
			$link = $data['_links']['canonical'];
1032
		} elseif ( ! empty( $data['u'] ) ) {
1033
			$link = $data['u'];
1034
		} elseif ( ! empty( $data['_meta'] ) ) {
1035
			if ( ! empty( $data['_meta']['twitter:url'] ) ) {
1036
				$link = $data['_meta']['twitter:url'];
1037
			} else if ( ! empty( $data['_meta']['og:url'] ) ) {
1038
				$link = $data['_meta']['og:url'];
1039
			}
1040
		}
1041
1042 View Code Duplication
		if ( empty( $link ) && ! empty( $data['_links']['shortlink'] ) ) {
1043
			$link = $data['_links']['shortlink'];
1044
		}
1045
1046
		return $link;
1047
	}
1048
1049
	/**
1050
	 * Gets the source page's site name, based on passed meta data.
1051
	 *
1052
	 * @since 4.2.0
1053
	 * @access public
1054
	 *
1055
	 * @param array $data The site's data.
1056
	 * @return string Discovered site name, or empty
1057
	 */
1058
	public function get_source_site_name( $data ) {
1059
		$name = '';
1060
1061
		if ( ! empty( $data['_meta'] ) ) {
1062
			if ( ! empty( $data['_meta']['og:site_name'] ) ) {
1063
				$name = $data['_meta']['og:site_name'];
1064
			} else if ( ! empty( $data['_meta']['application-name'] ) ) {
1065
				$name = $data['_meta']['application-name'];
1066
			}
1067
		}
1068
1069
		return $name;
1070
	}
1071
1072
	/**
1073
	 * Gets the source page's title, based on passed title and meta data.
1074
	 *
1075
	 * @since 4.2.0
1076
	 * @access public
1077
	 *
1078
	 * @param array $data The site's data.
1079
	 * @return string Discovered page title, or empty
1080
	 */
1081
	public function get_suggested_title( $data ) {
1082
		$title = '';
1083
1084
		if ( ! empty( $data['t'] ) ) {
1085
			$title = $data['t'];
1086 View Code Duplication
		} elseif ( ! empty( $data['_meta'] ) ) {
1087
			if ( ! empty( $data['_meta']['twitter:title'] ) ) {
1088
				$title = $data['_meta']['twitter:title'];
1089
			} else if ( ! empty( $data['_meta']['og:title'] ) ) {
1090
				$title = $data['_meta']['og:title'];
1091
			} else if ( ! empty( $data['_meta']['title'] ) ) {
1092
				$title = $data['_meta']['title'];
1093
			}
1094
		}
1095
1096
		return $title;
1097
	}
1098
1099
	/**
1100
	 * Gets the source page's suggested content, based on passed data (description, selection, etc).
1101
	 *
1102
	 * Features a blockquoted excerpt, as well as content attribution, if any.
1103
	 *
1104
	 * @since 4.2.0
1105
	 * @access public
1106
	 *
1107
	 * @param array $data The site's data.
1108
	 * @return string Discovered content, or empty
1109
	 */
1110
	public function get_suggested_content( $data ) {
1111
		$content = $text = '';
1112
1113
		if ( ! empty( $data['s'] ) ) {
1114
			$text = $data['s'];
1115
		} else if ( ! empty( $data['_meta'] ) ) {
1116 View Code Duplication
			if ( ! empty( $data['_meta']['twitter:description'] ) ) {
1117
				$text = $data['_meta']['twitter:description'];
1118
			} else if ( ! empty( $data['_meta']['og:description'] ) ) {
1119
				$text = $data['_meta']['og:description'];
1120
			} else if ( ! empty( $data['_meta']['description'] ) ) {
1121
				$text = $data['_meta']['description'];
1122
			}
1123
1124
			// If there is an ellipsis at the end, the description is very likely auto-generated. Better to ignore it.
1125
			if ( $text && substr( $text, -3 ) === '...' ) {
1126
				$text = '';
1127
			}
1128
		}
1129
1130
		$default_html = array( 'quote' => '', 'link' => '', 'embed' => '' );
1131
1132
		if ( ! empty( $data['u'] ) && $this->_limit_embed( $data['u'] ) ) {
1133
			$default_html['embed'] = '<p>[embed]' . $data['u'] . '[/embed]</p>';
1134
1135
			if ( ! empty( $data['s'] ) ) {
1136
				// If the user has selected some text, do quote it.
1137
				$default_html['quote'] = '<blockquote>%1$s</blockquote>';
1138
			}
1139
		} else {
1140
			$default_html['quote'] = '<blockquote>%1$s</blockquote>';
1141
			$default_html['link'] = '<p>' . _x( 'Source:', 'Used in Press This to indicate where the content comes from.' ) .
1142
				' <em><a href="%1$s">%2$s</a></em></p>';
1143
		}
1144
1145
		/**
1146
		 * Filters the default HTML tags used in the suggested content for the editor.
1147
		 *
1148
		 * The HTML strings use printf format. After filtering the content is added at the specified places with `sprintf()`.
1149
		 *
1150
		 * @since 4.2.0
1151
		 *
1152
		 * @param array $default_html Associative array with three possible keys:
1153
		 *                                - 'quote' where %1$s is replaced with the site description or the selected content.
1154
		 *                                - 'link' where %1$s is link href, %2$s is link text, usually the source page title.
1155
		 *                                - 'embed' which contains an [embed] shortcode when the source page offers embeddable content.
1156
		 * @param array $data         Associative array containing the data from the source page.
1157
		 */
1158
		$default_html = apply_filters( 'press_this_suggested_html', $default_html, $data );
1159
1160
		if ( ! empty( $default_html['embed'] ) ) {
1161
			$content .= $default_html['embed'];
1162
		}
1163
1164
		// Wrap suggested content in the specified HTML.
1165
		if ( ! empty( $default_html['quote'] ) && $text ) {
1166
			$content .= sprintf( $default_html['quote'], $text );
1167
		}
1168
1169
		// Add source attribution if there is one available.
1170
		if ( ! empty( $default_html['link'] ) ) {
1171
			$title = $this->get_suggested_title( $data );
1172
			$url = $this->get_canonical_link( $data );
1173
1174
			if ( ! $title ) {
1175
				$title = $this->get_source_site_name( $data );
1176
			}
1177
1178
			if ( $url && $title ) {
1179
				$content .= sprintf( $default_html['link'], $url, $title );
1180
			}
1181
		}
1182
1183
		return $content;
1184
	}
1185
1186
	/**
1187
	 * Serves the app's base HTML, which in turns calls the load script.
1188
	 *
1189
	 * @since 4.2.0
1190
	 * @access public
1191
	 *
1192
	 * @global WP_Locale $wp_locale
1193
	 * @global string    $wp_version
1194
	 * @global bool      $is_IE
1195
	 */
1196
	public function html() {
1197
		global $wp_locale, $wp_version;
1198
1199
		// Get data, new (POST) and old (GET).
1200
		$data = $this->merge_or_fetch_data();
1201
1202
		$post_title = $this->get_suggested_title( $data );
1203
1204
		$post_content = $this->get_suggested_content( $data );
1205
1206
		// Get site settings array/data.
1207
		$site_settings = $this->site_settings();
1208
1209
		// Pass the images and embeds
1210
		$images = $this->get_images( $data );
1211
		$embeds = $this->get_embeds( $data );
1212
1213
		$site_data = array(
1214
			'v' => ! empty( $data['v'] ) ? $data['v'] : '',
1215
			'u' => ! empty( $data['u'] ) ? $data['u'] : '',
1216
			'hasData' => ! empty( $data ),
1217
		);
1218
1219
		if ( ! empty( $images ) ) {
1220
			$site_data['_images'] = $images;
1221
		}
1222
1223
		if ( ! empty( $embeds ) ) {
1224
			$site_data['_embeds'] = $embeds;
1225
		}
1226
1227
		// Add press-this-editor.css and remove theme's editor-style.css, if any.
1228
		remove_editor_styles();
1229
1230
		add_filter( 'mce_css', array( $this, 'add_editor_style' ) );
1231
1232
		if ( ! empty( $GLOBALS['is_IE'] ) ) {
1233
			@header( 'X-UA-Compatible: IE=edge' );
1234
		}
1235
1236
		@header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
1237
1238
?>
1239
<!DOCTYPE html>
1240
<!--[if IE 7]>         <html class="lt-ie9 lt-ie8" <?php language_attributes(); ?>> <![endif]-->
1241
<!--[if IE 8]>         <html class="lt-ie9" <?php language_attributes(); ?>> <![endif]-->
1242
<!--[if gt IE 8]><!--> <html <?php language_attributes(); ?>> <!--<![endif]-->
1243
<head>
1244
	<meta http-equiv="Content-Type" content="<?php echo esc_attr( get_bloginfo( 'html_type' ) ); ?>; charset=<?php echo esc_attr( get_option( 'blog_charset' ) ); ?>" />
1245
	<meta name="viewport" content="width=device-width">
1246
	<title><?php esc_html_e( 'Press This!' ) ?></title>
1247
1248
	<script>
1249
		window.wpPressThisData   = <?php echo wp_json_encode( $site_data ); ?>;
1250
		window.wpPressThisConfig = <?php echo wp_json_encode( $site_settings ); ?>;
1251
	</script>
1252
1253
	<script type="text/javascript">
1254
		var ajaxurl = '<?php echo esc_js( admin_url( 'admin-ajax.php', 'relative' ) ); ?>',
1255
			pagenow = 'press-this',
1256
			typenow = 'post',
1257
			adminpage = 'press-this-php',
1258
			thousandsSeparator = '<?php echo addslashes( $wp_locale->number_format['thousands_sep'] ); ?>',
1259
			decimalPoint = '<?php echo addslashes( $wp_locale->number_format['decimal_point'] ); ?>',
1260
			isRtl = <?php echo (int) is_rtl(); ?>;
1261
	</script>
1262
1263
	<?php
1264
		/*
1265
		 * $post->ID is needed for the embed shortcode so we can show oEmbed previews in the editor.
1266
		 * Maybe find a way without it.
1267
		 */
1268
		$post = get_default_post_to_edit( 'post', true );
1269
		$post_ID = (int) $post->ID;
1270
1271
		wp_enqueue_media( array( 'post' => $post_ID ) );
1272
		wp_enqueue_style( 'press-this' );
1273
		wp_enqueue_script( 'press-this' );
1274
		wp_enqueue_script( 'json2' );
1275
		wp_enqueue_script( 'editor' );
1276
1277
		$supports_formats = false;
1278
		$post_format      = 0;
1279
1280
		if ( current_theme_supports( 'post-formats' ) && post_type_supports( $post->post_type, 'post-formats' ) ) {
1281
			$supports_formats = true;
1282
1283
			if ( ! ( $post_format = get_post_format( $post_ID ) ) ) {
1284
				$post_format = 0;
1285
			}
1286
		}
1287
1288
		/** This action is documented in wp-admin/admin-header.php */
1289
		do_action( 'admin_enqueue_scripts', 'press-this.php' );
1290
1291
		/** This action is documented in wp-admin/admin-header.php */
1292
		do_action( 'admin_print_styles-press-this.php' );
1293
1294
		/** This action is documented in wp-admin/admin-header.php */
1295
		do_action( 'admin_print_styles' );
1296
1297
		/** This action is documented in wp-admin/admin-header.php */
1298
		do_action( 'admin_print_scripts-press-this.php' );
1299
1300
		/** This action is documented in wp-admin/admin-header.php */
1301
		do_action( 'admin_print_scripts' );
1302
1303
		/** This action is documented in wp-admin/admin-header.php */
1304
		do_action( 'admin_head-press-this.php' );
1305
1306
		/** This action is documented in wp-admin/admin-header.php */
1307
		do_action( 'admin_head' );
1308
	?>
1309
</head>
1310
<?php
1311
1312
	$admin_body_class  = 'press-this';
1313
	$admin_body_class .= ( is_rtl() ) ? ' rtl' : '';
1314
	$admin_body_class .= ' branch-' . str_replace( array( '.', ',' ), '-', floatval( $wp_version ) );
1315
	$admin_body_class .= ' version-' . str_replace( '.', '-', preg_replace( '/^([.0-9]+).*/', '$1', $wp_version ) );
1316
	$admin_body_class .= ' admin-color-' . sanitize_html_class( get_user_option( 'admin_color' ), 'fresh' );
1317
	$admin_body_class .= ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_locale() ) ) );
1318
1319
	/** This filter is documented in wp-admin/admin-header.php */
1320
	$admin_body_classes = apply_filters( 'admin_body_class', '' );
1321
1322
?>
1323
<body class="wp-admin wp-core-ui <?php echo $admin_body_classes . ' ' . $admin_body_class; ?>">
1324
	<div id="adminbar" class="adminbar">
1325
		<h1 id="current-site" class="current-site">
1326
			<a class="current-site-link" href="<?php echo esc_url( home_url( '/' ) ); ?>" target="_blank" rel="home">
1327
				<span class="dashicons dashicons-wordpress"></span>
1328
				<span class="current-site-name"><?php bloginfo( 'name' ); ?></span>
1329
			</a>
1330
		</h1>
1331
		<button type="button" class="options button-link closed">
1332
			<span class="dashicons dashicons-tag on-closed"></span>
1333
			<span class="screen-reader-text on-closed"><?php _e( 'Show post options' ); ?></span>
1334
			<span aria-hidden="true" class="on-open"><?php _e( 'Done' ); ?></span>
1335
			<span class="screen-reader-text on-open"><?php _e( 'Hide post options' ); ?></span>
1336
		</button>
1337
	</div>
1338
1339
	<div id="scanbar" class="scan">
1340
		<form method="GET">
1341
			<label for="url-scan" class="screen-reader-text"><?php _e( 'Scan site for content' ); ?></label>
1342
			<input type="url" name="u" id="url-scan" class="scan-url" value="" placeholder="<?php esc_attr_e( 'Enter a URL to scan' ) ?>" />
1343
			<input type="submit" name="url-scan-submit" id="url-scan-submit" class="scan-submit" value="<?php esc_attr_e( 'Scan' ) ?>" />
1344
		</form>
1345
	</div>
1346
1347
	<form id="pressthis-form" method="post" action="post.php" autocomplete="off">
1348
		<input type="hidden" name="post_ID" id="post_ID" value="<?php echo $post_ID; ?>" />
1349
		<input type="hidden" name="action" value="press-this-save-post" />
1350
		<input type="hidden" name="post_status" id="post_status" value="draft" />
1351
		<input type="hidden" name="wp-preview" id="wp-preview" value="" />
1352
		<input type="hidden" name="post_title" id="post_title" value="" />
1353
		<input type="hidden" name="pt-force-redirect" id="pt-force-redirect" value="" />
1354
		<?php
1355
1356
		wp_nonce_field( 'update-post_' . $post_ID, '_wpnonce', false );
1357
		wp_nonce_field( 'add-category', '_ajax_nonce-add-category', false );
1358
1359
		?>
1360
1361
	<div class="wrapper">
1362
		<div class="editor-wrapper">
1363
			<div class="alerts" role="alert" aria-live="assertive" aria-relevant="all" aria-atomic="true">
1364
				<?php
1365
1366
				if ( isset( $data['v'] ) && $this->version > $data['v'] ) {
1367
					?>
1368
					<p class="alert is-notice">
1369
						<?php printf( __( 'You should upgrade <a href="%s" target="_blank">your bookmarklet</a> to the latest version!' ), admin_url( 'tools.php' ) ); ?>
1370
					</p>
1371
					<?php
1372
				}
1373
1374
				?>
1375
			</div>
1376
1377
			<div id="app-container" class="editor">
1378
				<span id="title-container-label" class="post-title-placeholder" aria-hidden="true"><?php _e( 'Post title' ); ?></span>
1379
				<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>
1380
1381
				<div class="media-list-container">
1382
					<div class="media-list-inner-container">
1383
						<h2 class="screen-reader-text"><?php _e( 'Suggested media' ); ?></h2>
1384
						<ul class="media-list"></ul>
1385
					</div>
1386
				</div>
1387
1388
				<?php
1389
				wp_editor( $post_content, 'pressthis', array(
1390
					'drag_drop_upload' => true,
1391
					'editor_height'    => 600,
1392
					'media_buttons'    => false,
1393
					'textarea_name'    => 'post_content',
1394
					'teeny'            => true,
1395
					'tinymce'          => array(
1396
						'resize'                => false,
1397
						'wordpress_adv_hidden'  => false,
1398
						'add_unload_trigger'    => false,
1399
						'statusbar'             => false,
1400
						'autoresize_min_height' => 600,
1401
						'wp_autoresize_on'      => true,
1402
						'plugins'               => 'lists,media,paste,tabfocus,fullscreen,wordpress,wpautoresize,wpeditimage,wpgallery,wplink,wptextpattern,wpview',
1403
						'toolbar1'              => 'bold,italic,bullist,numlist,blockquote,link,unlink',
1404
						'toolbar2'              => 'undo,redo',
1405
					),
1406
					'quicktags' => array(
1407
						'buttons' => 'strong,em,link,block,del,ins,img,ul,ol,li,code,more',
1408
					),
1409
				) );
1410
1411
				?>
1412
			</div>
1413
		</div>
1414
1415
		<div class="options-panel-back is-hidden" tabindex="-1"></div>
1416
		<div class="options-panel is-off-screen is-hidden" tabindex="-1">
1417
			<div class="post-options">
1418
1419
				<?php if ( $supports_formats ) : ?>
1420
					<button type="button" class="button-link post-option">
1421
						<span class="dashicons dashicons-admin-post"></span>
1422
						<span class="post-option-title"><?php _ex( 'Format', 'post format' ); ?></span>
1423
						<span class="post-option-contents" id="post-option-post-format"><?php echo esc_html( get_post_format_string( $post_format ) ); ?></span>
1424
						<span class="dashicons post-option-forward"></span>
1425
					</button>
1426
				<?php endif; ?>
1427
1428
				<button type="button" class="button-link post-option">
1429
					<span class="dashicons dashicons-category"></span>
1430
					<span class="post-option-title"><?php _e( 'Categories' ); ?></span>
1431
					<span class="dashicons post-option-forward"></span>
1432
				</button>
1433
1434
				<button type="button" class="button-link post-option">
1435
					<span class="dashicons dashicons-tag"></span>
1436
					<span class="post-option-title"><?php _e( 'Tags' ); ?></span>
1437
					<span class="dashicons post-option-forward"></span>
1438
				</button>
1439
			</div>
1440
1441
			<?php if ( $supports_formats ) : ?>
1442
				<div class="setting-modal is-off-screen is-hidden">
1443
					<button type="button" class="button-link modal-close">
1444
						<span class="dashicons post-option-back"></span>
1445
						<span class="setting-title" aria-hidden="true"><?php _ex( 'Format', 'post format' ); ?></span>
1446
						<span class="screen-reader-text"><?php _e( 'Back to post options' ) ?></span>
1447
					</button>
1448
					<?php $this->post_formats_html( $post ); ?>
1449
				</div>
1450
			<?php endif; ?>
1451
1452
			<div class="setting-modal is-off-screen is-hidden">
1453
				<button type="button" class="button-link modal-close">
1454
					<span class="dashicons post-option-back"></span>
1455
					<span class="setting-title" aria-hidden="true"><?php _e( 'Categories' ); ?></span>
1456
					<span class="screen-reader-text"><?php _e( 'Back to post options' ) ?></span>
1457
				</button>
1458
				<?php $this->categories_html( $post ); ?>
1459
			</div>
1460
1461
			<div class="setting-modal tags is-off-screen is-hidden">
1462
				<button type="button" class="button-link modal-close">
1463
					<span class="dashicons post-option-back"></span>
1464
					<span class="setting-title" aria-hidden="true"><?php _e( 'Tags' ); ?></span>
1465
					<span class="screen-reader-text"><?php _e( 'Back to post options' ) ?></span>
1466
				</button>
1467
				<?php $this->tags_html( $post ); ?>
1468
			</div>
1469
		</div><!-- .options-panel -->
1470
	</div><!-- .wrapper -->
1471
1472
	<div class="press-this-actions">
1473
		<div class="pressthis-media-buttons">
1474
			<button type="button" class="insert-media button-link" data-editor="pressthis">
1475
				<span class="dashicons dashicons-admin-media"></span>
1476
				<span class="screen-reader-text"><?php _e( 'Add Media' ); ?></span>
1477
			</button>
1478
		</div>
1479
		<div class="post-actions">
1480
			<span class="spinner">&nbsp;</span>
1481
			<div class="split-button">
1482
				<div class="split-button-head">
1483
					<button type="button" class="publish-button split-button-primary" aria-live="polite">
1484
						<span class="publish"><?php echo ( current_user_can( 'publish_posts' ) ) ? __( 'Publish' ) : __( 'Submit for Review' ); ?></span>
1485
						<span class="saving-draft"><?php _e( 'Saving&hellip;' ); ?></span>
1486
					</button><button type="button" class="split-button-toggle" aria-haspopup="true" aria-expanded="false">
1487
						<i class="dashicons dashicons-arrow-down-alt2"></i>
1488
						<span class="screen-reader-text"><?php _e('More actions'); ?></span>
1489
					</button>
1490
				</div>
1491
				<ul class="split-button-body">
1492
					<li><button type="button" class="button-link draft-button split-button-option"><?php _e( 'Save Draft' ); ?></button></li>
1493
					<li><button type="button" class="button-link standard-editor-button split-button-option"><?php _e( 'Standard Editor' ); ?></button></li>
1494
					<li><button type="button" class="button-link preview-button split-button-option"><?php _e( 'Preview' ); ?></button></li>
1495
				</ul>
1496
			</div>
1497
		</div>
1498
	</div>
1499
	</form>
1500
1501
	<?php
1502
	/** This action is documented in wp-admin/admin-footer.php */
1503
	do_action( 'admin_footer' );
1504
1505
	/** This action is documented in wp-admin/admin-footer.php */
1506
	do_action( 'admin_print_footer_scripts-press-this.php' );
1507
1508
	/** This action is documented in wp-admin/admin-footer.php */
1509
	do_action( 'admin_print_footer_scripts' );
1510
1511
	/** This action is documented in wp-admin/admin-footer.php */
1512
	do_action( 'admin_footer-press-this.php' );
1513
	?>
1514
</body>
1515
</html>
1516
<?php
1517
		die();
1518
	}
1519
}
1520
1521
/**
1522
 *
1523
 * @global WP_Press_This $wp_press_this
1524
 */
1525
$GLOBALS['wp_press_this'] = new WP_Press_This;
1526