Completed
Pull Request — 2.x (#4569)
by Scott Kingsley
06:54
created

PodsField_OEmbed::strip_html()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 4
nop 2
dl 0
loc 21
rs 9.3142
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * @package Pods\Fields
5
 */
6
class PodsField_OEmbed extends PodsField {
7
8
	/**
9
	 * {@inheritdoc}
10
	 */
11
	public static $group = 'Relationships / Media';
12
13
	/**
14
	 * {@inheritdoc}
15
	 */
16
	public static $type = 'oembed';
17
18
	/**
19
	 * {@inheritdoc}
20
	 */
21
	public static $label = 'oEmbed';
22
23
	/**
24
	 * {@inheritdoc}
25
	 */
26
	public static $prepare = '%s';
27
28
	/**
29
	 * Available oEmbed providers
30
	 *
31
	 * @var array
32
	 * @since 2.7
33
	 */
34
	private $providers = array();
35
36
	/**
37
	 * Current embed width
38
	 *
39
	 * @var int
40
	 * @since 2.7
41
	 */
42
	private $width = 0;
43
44
	/**
45
	 * Current embed height
46
	 *
47
	 * @var int
48
	 * @since 2.7
49
	 */
50
	private $height = 0;
51
52
	/**
53
	 * {@inheritdoc}
54
	 */
55
	public function setup() {
56
57
		self::$label = __( 'oEmbed', 'pods' );
58
	}
59
60
	/**
61
	 * {@inheritdoc}
62
	 */
63
	public function admin_init() {
64
65
		// AJAX for Uploads
66
		add_action( 'wp_ajax_oembed_update_preview', array( $this, 'admin_ajax_oembed_update_preview' ) );
67
	}
68
69
	/**
70
	 * {@inheritdoc}
71
	 */
72
	public function options() {
73
74
		$options = array(
75
			static::$type . '_repeatable'   => array(
76
				'label'             => __( 'Repeatable Field', 'pods' ),
77
				'default'           => 0,
78
				'type'              => 'boolean',
79
				'help'              => __( 'Making a field repeatable will add controls next to the field which allows users to Add/Remove/Reorder additional values. These values are saved in the database as an array, so searching and filtering by them may require further adjustments".', 'pods' ),
80
				'boolean_yes_label' => '',
81
				'dependency'        => true,
82
				'developer_mode'    => true,
83
			),
84
			static::$type . '_width'        => array(
85
				'label'   => __( 'Embed Width', 'pods' ),
86
				'default' => 0,
87
				'type'    => 'number',
88
				'help'    => __( 'Optional width to use for this oEmbed. Leave as 0 (zero) to default to none.', 'pods' ),
89
			),
90
			static::$type . '_height'       => array(
91
				'label'   => __( 'Embed Height', 'pods' ),
92
				'default' => 0,
93
				'type'    => 'number',
94
				'help'    => __( 'Optional height to use for this oEmbed. Leave as 0 (zero) to default to none.', 'pods' ),
95
			),
96
			static::$type . '_show_preview' => array(
97
				'label'   => __( 'Show preview', 'pods' ),
98
				'default' => 0,
99
				'type'    => 'boolean',
100
			),
101
		);
102
103
		// Get all unique provider host names
104
		$unique_providers = array();
105
		foreach ( $this->get_providers() as $provider ) {
106
			if ( ! in_array( $provider['host'], $unique_providers, true ) ) {
107
				$unique_providers[] = $provider['host'];
108
			}
109
		}
110
		sort( $unique_providers );
111
112
		// Only add the options if we have data
113
		if ( ! empty( $unique_providers ) ) {
114
			$options[ static::$type . '_restrict_providers' ] = array(
115
				'label'      => __( 'Restrict to providers', 'pods' ),
116
				'help'       => __( 'Restrict input to specific WordPress oEmbed compatible providers.', 'pods' ),
117
				'type'       => 'boolean',
118
				'default'    => 0,
119
				'dependency' => true,
120
			);
121
			$options[ static::$type . '_enable_providers' ]   = array(
122
				'label'      => __( 'Select enabled providers', 'pods' ),
123
				'depends-on' => array( static::$type . '_restrict_providers' => true ),
124
				'group'      => array(),
125
			);
126
			// Add all the oEmbed providers
127
			foreach ( $unique_providers as $provider ) {
128
				$options[ static::$type . '_enable_providers' ]['group'][ static::$type . '_enabled_providers_' . tag_escape( $provider ) ] = array(
129
					'label'   => $provider,
130
					'type'    => 'boolean',
131
					'default' => 0,
132
				);
133
			}
134
		}//end if
135
136
		return $options;
137
	}
138
139
	/**
140
	 * {@inheritdoc}
141
	 */
142
	public function schema( $options = null ) {
143
144
		$schema = 'LONGTEXT';
145
146
		return $schema;
147
	}
148
149
	/**
150
	 * {@inheritdoc}
151
	 */
152
	public function display( $value = null, $name = null, $options = null, $pod = null, $id = null ) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
153
154
		$value = $this->pre_save( $value, $id, $name, $options, null, $pod );
155
156
		$width  = (int) pods_v( static::$type . '_width', $options );
157
		$height = (int) pods_v( static::$type . '_height', $options );
158
		$args   = array();
159
		if ( $width > 0 ) {
160
			$args['width'] = $width;
161
		}
162
		if ( $height > 0 ) {
163
			$args['height'] = $height;
164
		}
165
166
		$value = wp_oembed_get( $value, $args );
167
168
		return $value;
169
	}
170
171
	/**
172
	 * {@inheritdoc}
173
	 */
174
	public function input( $name, $value = null, $options = null, $pod = null, $id = null ) {
175
176
		$options         = (array) $options;
177
		$form_field_type = PodsForm::$field_type;
178
179
		if ( is_array( $value ) ) {
180
			$value = implode( ' ', $value );
181
		}
182
183
		if ( isset( $options['name'] ) && false === PodsForm::permission( static::$type, $options['name'], $options, null, $pod, $id ) ) {
184
			if ( pods_v( 'read_only', $options, false ) ) {
185
				$options['readonly'] = true;
186
			} else {
187
				return;
188
			}
189
		} elseif ( ! pods_has_permissions( $options ) && pods_v( 'read_only', $options, false ) ) {
190
			$options['readonly'] = true;
191
		}
192
193
		pods_view( PODS_DIR . 'ui/fields/oembed.php', compact( array_keys( get_defined_vars() ) ) );
194
	}
195
196
	/**
197
	 * {@inheritdoc}
198
	 */
199
	public function validate( $value, $name = null, $options = null, $fields = null, $pod = null, $id = null, $params = null ) {
200
201
		$errors = array();
202
203
		$check = $this->pre_save( $value, $id, $name, $options, $fields, $pod, $params );
204
205
		if ( is_array( $check ) ) {
206
			$errors = $check;
207
		} else {
208
			if ( 0 < strlen( $value ) && '' === $check ) {
209
				if ( 1 === (int) pods_v( 'required', $options ) ) {
210
					$errors[] = __( 'This field is required.', 'pods' );
211
				}
212
			}
213
		}
214
215
		if ( ! empty( $errors ) ) {
216
			return $errors;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $errors; (array) is incompatible with the return type of the parent method PodsField::validate of type boolean.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
217
		}
218
219
		return true;
220
	}
221
222
	/**
223
	 * {@inheritdoc}
224
	 */
225
	public function pre_save( $value, $id = null, $name = null, $options = null, $fields = null, $pod = null, $params = null ) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
226
227
		$value = $this->strip_html( $value, $options );
228
229
		// Only allow ONE URL
230
		if ( ! empty( $value ) ) {
231
			$value = explode( ' ', $value );
232
			$value = esc_url( $value[0] );
233
		}
234
235
		if ( $this->validate_provider( $value, $options ) ) {
236
			return $value;
237
		} else {
238
			return false;
239
		}
240
241
	}
242
243
	/**
244
	 * {@inheritdoc}
245
	 */
246
	public function ui( $id, $value, $name = null, $options = null, $fields = null, $pod = null ) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
Coding Style introduced by
This method's name is shorter than the configured minimum length of 3 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
247
248
		$value = $this->pre_save( $value, $id, $name, $options, $fields, $pod );
249
250
		return $value;
251
	}
252
253
	/**
254
	 * {@inheritdoc}
255
	 */
256
	public function strip_html( $value, $options = null ) {
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
257
258
		if ( is_array( $value ) ) {
259
			// @codingStandardsIgnoreLine
260
			$value = @implode( ' ', $value );
261
		}
262
263
		$value = trim( $value );
264
265
		if ( empty( $value ) ) {
266
			return $value;
267
		}
268
269
		// Strip HTML
270
		$value = strip_tags( $value );
271
272
		// Strip shortcodes
273
		$value = strip_shortcodes( $value );
274
275
		return $value;
276
	}
277
278
	/**
279
	 * Passes any unlinked URLs that are on their own line to {@link WP_Embed::shortcode()} for potential embedding.
280
	 *
281
	 * @see   WP_Embed::autoembed()
282
	 * @see   WP_Embed::autoembed_callback()
283
	 *
284
	 * @uses  PodsField_OEmbed::autoembed_callback()
285
	 *
286
	 * @param string $content The content to be searched.
287
	 *
288
	 * @return string Potentially modified $content.
289
	 *
290
	 * @since 2.7
291
	 */
292
	public function autoembed( $content ) {
293
294
		// Replace line breaks from all HTML elements with placeholders.
295
		$content = wp_replace_in_html_tags( $content, array( "\n" => '<!-- wp-line-break -->' ) );
296
297
		// Find URLs that are on their own line.
298
		$content = preg_replace_callback(
299
			'|^(\s*)(https?://[^\s"]+)(\s*)$|im', array(
300
				$this,
301
				'autoembed_callback',
302
			), $content
303
		);
304
305
		// Put the line breaks back.
306
		return str_replace( '<!-- wp-line-break -->', "\n", $content );
307
308
	}
309
310
	/**
311
	 * Callback function for {@link WP_Embed::autoembed()}.
312
	 *
313
	 * @param array $match A regex match array.
314
	 *
315
	 * @return string The embed shortcode
316
	 *
317
	 * @since 2.7
318
	 */
319
	public function autoembed_callback( $match ) {
320
321
		$shortcode = '[embed width="' . $this->width . '" height="' . $this->height . '"]' . $match[2] . '[/embed]';
322
323
		return $shortcode;
324
325
	}
326
327
	/**
328
	 * Get a list of available providers from the WP_oEmbed class
329
	 *
330
	 * @see   wp-includes/class-oembed.php
331
	 * @return array $providers {
332
	 *     Array of provider data with regex as key
333
	 *
334
	 * @type string URL for this provider
335
	 * @type int
336
	 * @type string Hostname for this provider
337
	 * }
338
	 *
339
	 * @since 2.7
340
	 */
341
	public function get_providers() {
342
343
		// Return class property if already set
344
		if ( ! empty( $this->providers ) ) {
345
			return $this->providers;
346
		}
347
348
		if ( file_exists( ABSPATH . WPINC . '/class-oembed.php' ) ) {
349
			require_once ABSPATH . WPINC . '/class-oembed.php';
350
		}
351
352
		// Return an empty array if no providers could be found
353
		$providers = array();
354
355
		if ( function_exists( '_wp_oembed_get_object' ) ) {
356
			$wp_oembed = _wp_oembed_get_object();
357
			$providers = $wp_oembed->providers;
358
359
			foreach ( $providers as $key => $provider ) {
360
				$url  = wp_parse_url( $provider[0] );
361
				$host = $url['host'];
362
				$tmp  = explode( '.', $host );
363
364
				if ( count( $tmp ) === 3 ) {
0 ignored issues
show
introduced by
Found "=== 3". Use Yoda Condition checks, you must
Loading history...
365
					// Take domain names like .co.uk in consideration
366
					if ( ! in_array( 'co', $tmp, true ) ) {
367
						unset( $tmp[0] );
368
					}
369
				} elseif ( count( $tmp ) === 4 ) {
0 ignored issues
show
introduced by
Found "=== 4". Use Yoda Condition checks, you must
Loading history...
370
					// Take domain names like .co.uk in consideration
371
					unset( $tmp[0] );
372
				}
373
374
				$host = implode( '.', $tmp );
375
376
				$providers[ $key ]['host'] = $host;
377
			}
378
379
			$this->providers = $providers;
380
		}//end if
381
382
		return $providers;
383
384
	}
385
386
	/**
387
	 * Takes a URL and returns the corresponding oEmbed provider's URL, if there is one.
388
	 * This function is ripped from WP since Pods has support from 3.8 and in the WP core this function is 4.0+
389
	 * We've stripped the autodiscover part from this function to keep it basic
390
	 *
391
	 * @since  2.7
392
	 * @access public
393
	 *
394
	 * @see    WP_oEmbed::get_provider()
395
	 *
396
	 * @param string $url The URL to the content.
397
	 *
398
	 * @return false|string False on failure, otherwise the oEmbed provider URL.
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
399
	 */
400
	public function get_provider( $url ) {
401
402
		$provider = false;
403
404
		foreach ( $this->providers as $matchmask => $data ) {
405
			if ( isset( $data['host'] ) ) {
406
				unset( $data['host'] );
407
			}
408
			reset( $data );
409
410
			list( $providerurl, $regex ) = $data;
411
412
			$match = $matchmask;
413
414
			// Turn the asterisk-type provider URLs into regex
415
			if ( ! $regex ) {
416
				$matchmask = '#' . str_replace( '___wildcard___', '(.+)', preg_quote( str_replace( '*', '___wildcard___', $matchmask ), '#' ) ) . '#i';
417
				$matchmask = preg_replace( '|^#http\\\://|', '#https?\://', $matchmask );
418
			}
419
420
			if ( preg_match( $matchmask, $url ) ) {
421
				$provider = $match;
422
423
				break;
424
			}
425
		}//end foreach
426
427
		return $provider;
428
	}
429
430
	/**
431
	 * Validate a value with the enabled oEmbed providers (if required).
432
	 *
433
	 * @since 2.7
434
	 *
435
	 * @param string $value   Field value.
436
	 * @param array  $options Field options.
437
	 *
438
	 * @return bool
439
	 */
440
	public function validate_provider( $value, $options ) {
441
442
		// Check if we need to validate.
443
		if ( 0 === (int) pods_v( static::$type . '_restrict_providers', $options ) ) {
444
			return true;
445
		}
446
447
		$providers = $this->get_providers();
448
449
		// Filter existing providers.
450
		foreach ( $providers as $key => $provider ) {
451
			$fieldname = static::$type . '_enabled_providers_' . tag_escape( $provider['host'] );
452
453
			/**
454
			 * @todo Future compat to enable serialised strings as field options
455
			 */
456
457
			/**
458
			 * Current solution: all separate field options.
459
			 */
460
			if ( empty( $options[ $fieldname ] ) ) {
461
				unset( $providers[ $key ] );
462
			}
463
		}
464
465
		// Value validation.
466
		$provider_match = $this->get_provider( $value );
467
468
		foreach ( $providers as $match => $provider ) {
469
			if ( $provider_match === $match ) {
470
				return true;
471
			}
472
		}
473
474
		return false;
475
	}
476
477
	/**
478
	 * Handle update preview AJAX.
479
	 *
480
	 * @since 2.7
481
	 */
482
	public function admin_ajax_oembed_update_preview() {
483
484
		// Sanitize input.
485
		// @codingStandardsIgnoreLine
486
		$params = pods_unslash( (array) $_POST );
487
488
		if ( ! empty( $params['_nonce_pods_oembed'] ) && ! empty( $params['pods_field_oembed_value'] ) && wp_verify_nonce( $params['_nonce_pods_oembed'], 'pods_field_oembed_preview' ) ) {
489
			$value = $this->strip_html( $params['pods_field_oembed_value'] );
490
491
			$name    = '';
492
			$options = array();
493
494
			if ( ! empty( $params['pods_field_oembed_name'] ) ) {
495
				$name = $this->strip_html( $params['pods_field_oembed_name'] );
496
			}
497
498
			if ( ! empty( $params['pods_field_oembed_options'] ) ) {
499
				$options = $params['pods_field_oembed_options'];
500
			}
501
502
			// Load the field to get it's options.
503
			$options = pods_api()->load_field( (object) $options );
504
505
			// Field options are stored here, if not, just stay with the full options array.
506
			if ( ! empty( $options['options'] ) ) {
507
				$options = $options['options'];
508
			}
509
510
			// Run display function to run oEmbed.
511
			$value = $this->display( $value, $name, $options );
512
513
			if ( empty( $value ) ) {
514
				$value = __( 'Please choose a valid oEmbed URL.', 'pods' );
515
				wp_send_json_error( $value );
516
			} else {
517
				wp_send_json_success( $value );
518
			}
519
		}//end if
520
		wp_send_json_error( __( 'Unauthorized request', 'pods' ) );
521
522
		die();
523
		// Kill it!
524
	}
525
526
}
527