Completed
Push — namespace-template ( 7967f2...367a36 )
by Sam
10:48
created

Oembed   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 202
Duplicated Lines 2.97 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 6
loc 202
rs 8.2769
c 1
b 0
f 0
wmc 41
lcom 1
cbo 6

10 Methods

Rating   Name   Duplication   Size   Complexity  
A is_enabled() 0 3 1
A get_autodiscover() 0 3 1
A get_providers() 0 3 1
B find_endpoint() 0 15 6
C matches_scheme() 0 23 7
B autodiscover_from_url() 0 26 3
A autodiscover_from_body() 0 17 4
C get_oembed_from_url() 0 42 13
A get_shortcodes() 0 3 1
A handle_shortcode() 6 14 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Oembed often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Oembed, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Format of the Oembed config. Autodiscover allows discovery of all URLs.
4
 *
5
 * Endpoint set to true means autodiscovery for this specific provider is
6
 * allowed (even if autodiscovery in general has been disabled).
7
 *
8
 * <code>
9
 *
10
 * name: Oembed
11
 * ---
12
 * Oembed:
13
 *   providers:
14
 *     'http://*.youtube.com/watch*':
15
 *     'http://www.youtube.com/oembed/'
16
 *   autodiscover:
17
 *     true
18
 * </code>
19
 *
20
 * @package framework
21
 * @subpackage oembed
22
 */
23
24
class Oembed implements ShortcodeHandler {
25
26
	public static function is_enabled() {
27
		return Config::inst()->get('Oembed', 'enabled');
28
	}
29
30
	/**
31
	 * Gets the autodiscover setting from the config.
32
	 */
33
	public static function get_autodiscover() {
34
		return Config::inst()->get('Oembed', 'autodiscover');
35
	}
36
37
	/**
38
	 * Gets providers from config.
39
	 */
40
	public static function get_providers() {
41
		return Config::inst()->get('Oembed', 'providers');
42
	}
43
44
	/**
45
	 * Returns an endpoint (a base Oembed URL) from first matching provider.
46
	 *
47
	 * @param $url Human-readable URL.
48
	 * @returns string/bool URL of an endpoint, or false if no matching provider exists.
49
	 */
50
	protected static function find_endpoint($url) {
51
		foreach(self::get_providers() as $scheme=>$endpoint) {
0 ignored issues
show
Bug introduced by
The expression self::get_providers() of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
52
			if(self::matches_scheme($url, $scheme)) {
53
				$protocol = Director::is_https() ? 'https' : 'http';
54
55
				if (is_array($endpoint)) {
56
					if (array_key_exists($protocol, $endpoint)) $endpoint = $endpoint[$protocol];
57
					else $endpoint = reset($endpoint);
58
				}
59
60
				return $endpoint;
61
			}
62
		}
63
		return false;
64
	}
65
66
	/**
67
	 * Checks the URL if it matches against the scheme (pattern).
68
	 *
69
	 * @param $url Human-readable URL to be checked.
70
	 * @param $scheme Pattern to be matched against.
71
	 * @returns bool Whether the pattern matches or not.
72
	 */
73
	protected static function matches_scheme($url, $scheme) {
74
		$urlInfo = parse_url($url);
75
		$schemeInfo = parse_url($scheme);
76
77
		foreach($schemeInfo as $k=>$v) {
0 ignored issues
show
Bug introduced by
The expression $schemeInfo of type array<string,string>|false is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
78
			if(!array_key_exists($k, $urlInfo)) {
79
				return false;
80
			}
81
			if(strpos($v, '*') !== false) {
82
				$v = preg_quote($v, '/');
83
				$v = str_replace('\*', '.*', $v);
84
				if($k == 'host') {
85
					$v = str_replace('*\.', '*', $v);
86
				}
87
				if(!preg_match('/' . $v . '/', $urlInfo[$k])) {
88
					return false;
89
				}
90
			} elseif(strcasecmp($urlInfo[$k], $v)) {
91
				return false;
92
			}
93
		}
94
		return true;
95
	}
96
97
	/**
98
	 * Performs a HTTP request to the URL and scans the response for resource links
99
	 * that mention oembed in their type.
100
	 *
101
	 * @param $url Human readable URL.
102
	 * @returns string/bool Oembed URL, or false.
103
	 */
104
	protected static function autodiscover_from_url($url)
105
	{
106
		$timeout   = 5;
107
		$sapphireInfo = new SapphireInfo();
108
		$useragent = 'SilverStripe/' . $sapphireInfo->Version();
109
		$curlRequest = curl_init();
110
		curl_setopt_array(
111
			$curlRequest,
112
			array(
113
				CURLOPT_URL            => $url,
114
				CURLOPT_RETURNTRANSFER => 1,
115
				CURLOPT_USERAGENT      => $useragent,
116
				CURLOPT_CONNECTTIMEOUT => $timeout,
117
				CURLOPT_FOLLOWLOCATION => 1,
118
119
			)
120
		);
121
122
		$response = curl_exec($curlRequest);
123
		$headers = curl_getinfo($curlRequest);
124
		if(!$response || $headers['http_code'] !== 200) {
125
			return false;
126
		}
127
		$body = $response;		
128
		return static::autodiscover_from_body($body);
129
	}
130
131
	/**
132
	 * Given a response body, determine if there is an autodiscover url
133
	 *
134
	 * @param string $body
135
	 * @return bool|string
136
	 */
137
	public static function autodiscover_from_body($body) {
138
		// Look within the body for an oembed link.
139
		$pcreOmbed = '#<link[^>]+?(?:href=[\'"](?<first>[^\'"]+?)[\'"][^>]+?)'
140
			. '?type=["\']application/json\+oembed["\']'
141
			. '(?:[^>]+?href=[\'"](?<second>[^\'"]+?)[\'"])?#';
142
143
		if(preg_match_all($pcreOmbed, $body, $matches, PREG_SET_ORDER)) {
144
			$match = $matches[0];
145
			if(!empty($match['second'])) {
146
				return html_entity_decode($match['second']);
147
			}
148
			if(!empty($match['first'])) {
149
				return html_entity_decode($match['first']);
150
			}
151
		}
152
		return false;
153
	}
154
155
	/**
156
	 * Takes the human-readable URL of an embeddable resource and converts it into an
157
	 * Oembed_Result descriptor (which contains a full Oembed resource URL).
158
	 *
159
	 * @param $url Human-readable URL
160
	 * @param $type ?
161
	 * @param $options array Options to be used for constructing the resulting descriptor.
162
	 * @returns Oembed_Result/bool An Oembed descriptor, or false
163
	 */
164
	public static function get_oembed_from_url($url, $type = false, array $options = array()) {
165
		if(!self::is_enabled()) return false;
166
167
		// Find or build the Oembed URL.
168
		$endpoint = self::find_endpoint($url);
169
		$oembedUrl = false;
170
		if(!$endpoint) {
171
			if(self::get_autodiscover()) {
172
				$oembedUrl = self::autodiscover_from_url($url);
173
			}
174
		} elseif($endpoint === true) {
175
			$oembedUrl = self::autodiscover_from_url($url);
176
		} else {
177
			// Build the url manually - we gave all needed information.
178
			$oembedUrl = Controller::join_links($endpoint, '?format=json&url=' . rawurlencode($url));
179
		}
180
181
		// If autodescovery failed the resource might be a direct link to a file
182
		if(!$oembedUrl) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oembedUrl 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...
183
			if(File::get_app_category(File::get_file_extension($url)) == "image") {
184
				return new Oembed_Result($url, $url, $type, $options);
185
			}
186
		}
187
188
		if($oembedUrl) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $oembedUrl of type false|string is loosely compared to true; 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...
189
			// Inject the options into the Oembed URL.
190
			if($options) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
191
				if(isset($options['width']) && !isset($options['maxwidth'])) {
192
					$options['maxwidth'] = $options['width'];
193
				}
194
				if(isset($options['height']) && !isset($options['maxheight'])) {
195
					$options['maxheight'] = $options['height'];
196
				}
197
				$oembedUrl = Controller::join_links($oembedUrl, '?' . http_build_query($options, '', '&'));
198
			}
199
200
			return new Oembed_Result($oembedUrl, $url, $type, $options);
201
		}
202
203
		// No matching Oembed resource found.
204
		return false;
205
	}
206
207
	public static function get_shortcodes() {
208
		return 'embed';
209
	}
210
211
	public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = array()) {
212 View Code Duplication
		if(isset($arguments['type'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
213
			$type = $arguments['type'];
214
			unset($arguments['type']);
215
		} else {
216
			$type = false;
217
		}
218
		$oembed = self::get_oembed_from_url($content, $type, $arguments);
219
		if($oembed && $oembed->exists()) {
220
			return $oembed->forTemplate();
221
		} else {
222
			return '<a href="' . $content . '">' . $content . '</a>';
223
		}
224
	}
225
}
226
227
/**
228
 * @property string $Type Oembed type
229
 * @property string $Title Title
230
 * @property string $URL URL to asset
231
 * @property string $Provider_URL Url for provider
232
 * @property int $Width
233
 * @property int $Height
234
 * @property string $Info Descriptive text for this oembed
235
 *
236
 * @package framework
237
 * @subpackage oembed
238
 */
239
class Oembed_Result extends ViewableData {
240
	/**
241
	 * JSON data fetched from the Oembed URL.
242
	 * This data is accessed dynamically by getField and hasField.
243
	 */
244
	protected $data = false;
245
246
	/**
247
	 * Human readable URL
248
	 */
249
	protected $origin = false;
250
251
	/**
252
	 * ?
253
	 */
254
	protected $type = false;
255
256
	/**
257
	 * Oembed URL
258
	 */
259
	protected $url;
260
261
	/**
262
	 * Class to be injected into the resulting HTML element.
263
	 */
264
	protected $extraClass;
265
266
	private static $casting = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
267
		'html' => 'HTMLText',
268
	);
269
270
	public function __construct($url, $origin = false, $type = false, array $options = array()) {
271
		$this->url = $url;
272
		$this->origin = $origin;
273
		$this->type = $type;
274
275
		if(isset($options['class'])) {
276
			$this->extraClass = $options['class'];
277
		}
278
		if($options) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
279
			if(isset($options['width'])) {
280
				$this->Width = $options['width'];
281
			}
282
			if(isset($options['height'])) {
283
				$this->Height = $options['height'];
284
			}
285
		}
286
		parent::__construct();
287
	}
288
289
	public function getOembedURL() {
290
		return $this->url;
291
	}
292
293
	/**
294
	 * Fetches the JSON data from the Oembed URL (cached).
295
	 * Only sets the internal variable.
296
	 */
297
	protected function loadData() {
298
		if($this->data !== false) {
299
			return;
300
		}
301
		$timeout   = 5;
302
		$sapphireInfo = new SapphireInfo();
303
		$useragent = 'SilverStripe/' . $sapphireInfo->Version();
304
		$curlRequest = curl_init();
305
		curl_setopt_array(
306
			$curlRequest,
307
			array(
308
				CURLOPT_URL            => $this->url,
309
				CURLOPT_RETURNTRANSFER => 1,
310
				CURLOPT_USERAGENT      => $useragent,
311
				CURLOPT_CONNECTTIMEOUT => $timeout,
312
				CURLOPT_FOLLOWLOCATION => 1,
313
314
			)
315
		);
316
317
		$response = curl_exec($curlRequest);
318
		$headers = curl_getinfo($curlRequest);
319
		if(!$response || $headers['http_code'] !== 200) {
320
			$this->data = array();
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
321
			return;
322
		}
323
		$body = $response;
324
		$data = json_decode($body, true);
325
		if(!$data) {
326
			// if the response is no valid JSON we might have received a binary stream to an image
327
			$data = array();
328
			if (!function_exists('imagecreatefromstring')) {
329
				throw new LogicException('imagecreatefromstring function does not exist - Please make sure GD is installed');
330
			}
331
			$image = imagecreatefromstring($body);
332
			if($image !== FALSE) {
333
				preg_match("/^(http:\/\/)?([^\/]+)/i", $this->url, $matches);
334
				$protocoll = $matches[1];
335
				$host = $matches[2];
336
				$data['type'] = "photo";
337
				$data['title'] = basename($this->url) . " ($host)";
338
				$data['url'] = $this->url;
339
				$data['provider_url'] = $protocoll.$host;
340
				$data['width'] = imagesx($image);
341
				$data['height'] = imagesy($image);
342
				$data['info'] = _t('UploadField.HOTLINKINFO',
343
					'Info: This image will be hotlinked. Please ensure you have permissions from the'
344
					. ' original site creator to do so.');
345
			}
346
		}
347
348
		// Convert all keys to lowercase
349
		$data = array_change_key_case($data, CASE_LOWER);
350
351
		// Check if we can guess thumbnail
352
		if(empty($data['thumbnail_url']) && $thumbnail = $this->findThumbnail($data)) {
353
			$data['thumbnail_url'] = $thumbnail;
354
		}
355
356
		// Purge everything if the type does not match.
357
		if($this->type && $this->type != $data['type']) {
358
			$data = array();
359
		}
360
361
		$this->data = $data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $data of type array is incompatible with the declared type boolean of property $data.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
362
	}
363
364
	/**
365
	 * Find thumbnail if omitted from data
366
	 *
367
	 * @param array $data
368
	 * @return string
369
	 */
370
	public function findThumbnail($data) {
371
		if(!empty($data['thumbnail_url'])) {
372
			return $data['thumbnail_url'];
373
		}
374
375
		// Hack in facebook graph thumbnail
376
		if(!empty($data['provider_name']) && $data['provider_name'] === 'Facebook') {
377
			$id = preg_replace("/.*\\/(\\d+?)\\/?($|\\?.*)/", "$1", $data["url"]);
378
			return "https://graph.facebook.com/{$id}/picture";
379
		}
380
381
		// no thumbnail found
382
		return null;
383
	}
384
385
	/**
386
	 * Wrap the check for looking into Oembed JSON within $this->data.
387
	 */
388
	public function hasField($field) {
389
		$this->loadData();
390
		return array_key_exists(strtolower($field), $this->data);
391
	}
392
393
	/**
394
	 * Wrap the field calls to fetch data from Oembed JSON (within $this->data)
395
	 */
396
	public function getField($field) {
397
		$field = strtolower($field);
398
		if($this->hasField($field)) {
399
			return $this->data[$field];
400
		}
401
	}
402
403
	public function forTemplate() {
404
		$this->loadData();
405
		switch($this->Type) {
406
		case 'video':
407
		case 'rich':
408
			if($this->extraClass) {
409
				return "<div class='media $this->extraClass'>$this->HTML</div>";
410
			} else {
411
				return "<div class='media'>$this->HTML</div>";
412
			}
413
			break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
414
		case 'link':
415
			return '<a class="' . $this->extraClass . '" href="' . $this->origin . '">' . $this->Title . '</a>';
416
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
417
		case 'photo':
418
			return "<img src='$this->URL' width='$this->Width' height='$this->Height' class='$this->extraClass' />";
419
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
420
		}
421
	}
422
423
	public function exists() {
424
		$this->loadData();
425
		return count($this->data) > 0;
426
	}
427
}
428
429