Completed
Push — 3.7 ( 81b2d8...ef0909 )
by
unknown
09:42
created

Oembed_Result::forTemplate()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
nc 7
nop 0
dl 0
loc 19
rs 9.0111
c 0
b 0
f 0
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 {
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
		// Fetch the URL (cache for a week by default)
107
		$service = new RestfulService($url, 60 * 60 * 24 * 7);
108
		$body = $service->request();
109
		if (!$body || $body->isError()) {
110
			return false;
111
		}
112
		$body = $body->getBody();
113
		return static::autodiscover_from_body($body);
114
	}
115
116
	/**
117
	 * Given a response body, determine if there is an autodiscover url
118
	 *
119
	 * @param string $body
120
	 * @return bool|string
121
	 */
122
	public static function autodiscover_from_body($body) {
123
		// Look within the body for an oembed link.
124
		$pcreOmbed = '#<link[^>]+?(?:href=[\'"](?<first>[^\'"]+?)[\'"][^>]+?)'
125
			. '?type=["\']application/json\+oembed["\']'
126
			. '(?:[^>]+?href=[\'"](?<second>[^\'"]+?)[\'"])?#';
127
128
		if(preg_match_all($pcreOmbed, $body, $matches, PREG_SET_ORDER)) {
129
			$match = $matches[0];
130
			if(!empty($match['second'])) {
131
				return html_entity_decode($match['second']);
132
			}
133
			if(!empty($match['first'])) {
134
				return html_entity_decode($match['first']);
135
			}
136
		}
137
		return false;
138
	}
139
140
	/**
141
	 * Takes the human-readable URL of an embeddable resource and converts it into an
142
	 * Oembed_Result descriptor (which contains a full Oembed resource URL).
143
	 *
144
	 * @param $url Human-readable URL
145
	 * @param $type ?
146
	 * @param $options array Options to be used for constructing the resulting descriptor.
147
	 * @returns Oembed_Result/bool An Oembed descriptor, or false
148
	 */
149
	public static function get_oembed_from_url($url, $type = false, array $options = array()) {
150
		if(!self::is_enabled()) return false;
151
152
		// Find or build the Oembed URL.
153
		$endpoint = self::find_endpoint($url);
154
		$oembedUrl = false;
155
		if(!$endpoint) {
156
			if(self::get_autodiscover()) {
157
				$oembedUrl = self::autodiscover_from_url($url);
158
			}
159
		} elseif($endpoint === true) {
160
			$oembedUrl = self::autodiscover_from_url($url);
161
		} else {
162
			// Build the url manually - we gave all needed information.
163
			$oembedUrl = Controller::join_links($endpoint, '?format=json&url=' . rawurlencode($url));
164
		}
165
166
		// If autodescovery failed the resource might be a direct link to a file
167
		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...
168
			if(File::get_app_category(File::get_file_extension($url)) == "image") {
169
				return new Oembed_Result($url, $url, $type, $options);
170
			}
171
		}
172
173
		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...
174
			// Inject the options into the Oembed URL.
175
			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...
176
				if(isset($options['width']) && !isset($options['maxwidth'])) {
177
					$options['maxwidth'] = $options['width'];
178
				}
179
				if(isset($options['height']) && !isset($options['maxheight'])) {
180
					$options['maxheight'] = $options['height'];
181
				}
182
				$oembedUrl = Controller::join_links($oembedUrl, '?' . http_build_query($options, '', '&'));
183
			}
184
185
			return new Oembed_Result($oembedUrl, $url, $type, $options);
186
		}
187
188
		// No matching Oembed resource found.
189
		return false;
190
	}
191
192
	public static function handle_shortcode($arguments, $url, $parser, $shortcode) {
0 ignored issues
show
Unused Code introduced by
The parameter $parser is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $shortcode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
193
		if(isset($arguments['type'])) {
194
			$type = $arguments['type'];
195
			unset($arguments['type']);
196
		} else {
197
			$type = false;
198
		}
199
		$oembed = self::get_oembed_from_url($url, $type, $arguments);
200
		if($oembed && $oembed->exists()) {
201
			return $oembed->forTemplate();
202
		} else {
203
			return '<a href="' . $url . '">' . $url . '</a>';
204
		}
205
	}
206
}
207
208
/**
209
 * @package framework
210
 * @subpackage oembed
211
 */
212
class Oembed_Result extends ViewableData {
213
	/**
214
	 * JSON data fetched from the Oembed URL.
215
	 * This data is accessed dynamically by getField and hasField.
216
	 */
217
	protected $data = false;
218
219
	/**
220
	 * Human readable URL
221
	 */
222
	protected $origin = false;
223
224
	/**
225
	 * ?
226
	 */
227
	protected $type = false;
228
229
	/**
230
	 * Oembed URL
231
	 */
232
	protected $url;
233
234
	/**
235
	 * Class to be injected into the resulting HTML element.
236
	 */
237
	protected $extraClass;
238
239
	private static $casting = array(
240
		'html' => 'HTMLText',
241
	);
242
243
	public function __construct($url, $origin = false, $type = false, array $options = array()) {
244
		$this->url = $url;
245
		$this->origin = $origin;
246
		$this->type = $type;
247
248
		if(isset($options['class'])) {
249
			$this->extraClass = $options['class'];
250
		}
251
		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...
252
			if(isset($options['width'])) {
253
				$this->Width = $options['width'];
0 ignored issues
show
Documentation introduced by
The property Width does not exist on object<Oembed_Result>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
254
			}
255
			if(isset($options['height'])) {
256
				$this->Height = $options['height'];
0 ignored issues
show
Documentation introduced by
The property Height does not exist on object<Oembed_Result>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
257
			}
258
		}
259
		parent::__construct();
260
	}
261
262
	public function getOembedURL() {
263
		return $this->url;
264
	}
265
266
	/**
267
	 * Fetches the JSON data from the Oembed URL (cached).
268
	 * Only sets the internal variable.
269
	 */
270
	protected function loadData() {
271
		if($this->data !== false) {
272
			return;
273
		}
274
275
		// Fetch from Oembed URL (cache for a week by default)
276
		$service = new RestfulService($this->url, 60*60*24*7);
277
		$body = $service->request();
278
		if(!$body || $body->isError()) {
279
			$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...
280
			return;
281
		}
282
		$body = $body->getBody();
283
		$data = json_decode($body, true);
284
		if(!$data) {
285
			// if the response is no valid JSON we might have received a binary stream to an image
286
			$data = array();
287
			if (!function_exists('imagecreatefromstring')) {
288
				throw new LogicException('imagecreatefromstring function does not exist - Please make sure GD is installed');
289
			}
290
			$image = imagecreatefromstring($body);
291
			if($image !== FALSE) {
292
				preg_match("/^(http:\/\/)?([^\/]+)/i", $this->url, $matches);
293
				$protocoll = $matches[1];
294
				$host = $matches[2];
295
				$data['type'] = "photo";
296
				$data['title'] = basename($this->url) . " ($host)";
297
				$data['url'] = $this->url;
298
				$data['provider_url'] = $protocoll.$host;
299
				$data['width'] = imagesx($image);
300
				$data['height'] = imagesy($image);
301
				$data['info'] = _t('UploadField.HOTLINKINFO',
302
					'Info: This image will be hotlinked. Please ensure you have permissions from the'
303
					. ' original site creator to do so.');
304
			}
305
		}
306
307
		// Convert all keys to lowercase
308
		$data = array_change_key_case($data, CASE_LOWER);
309
310
		// Check if we can guess thumbnail
311
		if(empty($data['thumbnail_url']) && $thumbnail = $this->findThumbnail($data)) {
312
			$data['thumbnail_url'] = $thumbnail;
313
		}
314
315
		// Purge everything if the type does not match.
316
		if($this->type && $this->type != $data['type']) {
317
			$data = array();
318
		}
319
320
		$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...
321
	}
322
323
	/**
324
	 * Find thumbnail if omitted from data
325
	 *
326
	 * @param array $data
327
	 * @return string
328
	 */
329
	public function findThumbnail($data) {
330
		if(!empty($data['thumbnail_url'])) {
331
			return $data['thumbnail_url'];
332
		}
333
334
		// Hack in facebook graph thumbnail
335
		if(!empty($data['provider_name']) && $data['provider_name'] === 'Facebook') {
336
			$id = preg_replace("/.*\\/(\\d+?)\\/?($|\\?.*)/", "$1", $data["url"]);
337
			return "https://graph.facebook.com/{$id}/picture";
338
		}
339
340
		// no thumbnail found
341
		return null;
342
	}
343
344
	/**
345
	 * Wrap the check for looking into Oembed JSON within $this->data.
346
	 */
347
	public function hasField($field) {
348
		$this->loadData();
349
		return array_key_exists(strtolower($field), $this->data);
350
	}
351
352
	/**
353
	 * Wrap the field calls to fetch data from Oembed JSON (within $this->data)
354
	 */
355
	public function getField($field) {
356
		$field = strtolower($field);
357
		if($this->hasField($field)) {
358
			return $this->data[$field];
359
		}
360
	}
361
362
	public function forTemplate() {
363
		$this->loadData();
364
		switch($this->Type) {
365
		case 'video':
366
		case 'rich':
367
			if($this->extraClass) {
368
				return "<div class='media $this->extraClass'>$this->HTML</div>";
369
			} else {
370
				return "<div class='media'>$this->HTML</div>";
371
			}
372
			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...
373
		case 'link':
374
			return '<a class="' . $this->extraClass . '" href="' . $this->origin . '">' . $this->Title . '</a>';
375
			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...
376
		case 'photo':
377
			return "<img src='$this->URL' width='$this->Width' height='$this->Height' class='$this->extraClass' />";
378
			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...
379
		}
380
	}
381
382
	public function exists() {
383
		$this->loadData();
384
		return count($this->data) > 0;
385
	}
386
}
387
388