Completed
Branch MediaEmbedRefactor (b4e2db)
by Josh
14:26
created

Configurator::setUp()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 0
crap 2
1
<?php
2
3
/**
4
* @package   s9e\TextFormatter
5
* @copyright Copyright (c) 2010-2018 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\TextFormatter\Plugins\MediaEmbed;
9
10
use InvalidArgumentException;
11
use RuntimeException;
12
use s9e\TextFormatter\Configurator\Exceptions\UnsafeFilterException;
13
use s9e\TextFormatter\Configurator\Items\Regexp;
14
use s9e\TextFormatter\Configurator\Items\Tag;
15
use s9e\TextFormatter\Configurator\JavaScript\Dictionary;
16
use s9e\TextFormatter\Plugins\ConfiguratorBase;
17
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\Collections\CachedDefinitionCollection;
18
use s9e\TextFormatter\Plugins\MediaEmbed\Configurator\TemplateBuilder;
19
20
class Configurator extends ConfiguratorBase
21
{
22
	/**
23
	* @var array List of filters that are explicitly allowed in attribute definitions
24
	*/
25
	public $allowedFilters = ['stripslashes', 'urldecode'];
26
27
	/**
28
	* @var bool Whether to create the MEDIA BBCode
29
	*/
30
	protected $createMediaBBCode = true;
31
32
	/**
33
	* @var Configurator\Collections\SiteDefinitionCollection Default sites
34
	*/
35
	public $defaultSites;
36
37
	/**
38
	* {@inheritdoc}
39
	*/
40
	protected $quickMatch = '://';
41
42
	/**
43
	* {@inheritdoc}
44
	*/
45
	protected $regexp = '/\\bhttps?:\\/\\/[^["\'\\s]+/Si';
46
47
	/**
48
	* @var array Configured sites
49
	*/
50
	protected $sites = [];
51
52
	/**
53
	* @var string Name of the tag used to handle embeddable URLs
54
	*/
55
	protected $tagName = 'MEDIA';
56
57
	/**
58
	* @var TemplateBuilder
59
	*/
60
	protected $templateBuilder;
61
62
	/**
63
	* {@inheritdoc}
64
	*/
65 24
	protected function setUp()
66
	{
67 24
		$this->defaultSites    = new CachedDefinitionCollection;
68 24
		$this->templateBuilder = new TemplateBuilder;
69
70
		// Create a MEDIA tag
71 24
		$this->createMediaTag();
72
73
		// Create a [MEDIA] BBCode if applicable
74 24
		if ($this->createMediaBBCode)
75
		{
76 23
			$this->configurator->BBCodes->set($this->tagName, ['contentAttributes' => ['url']]);
77
		}
78 24
	}
79
80
	/**
81
	* {@inheritdoc}
82
	*/
83 5
	public function asConfig()
84
	{
85 5
		if (empty($this->sites))
86
		{
87 1
			return;
88
		}
89
90
		return [
91 4
			'quickMatch' => $this->quickMatch,
92 4
			'regexp'     => $this->regexp,
93 4
			'tagName'    => $this->tagName
94
		];
95
	}
96
97
	/**
98
	* Add a media site
99
	*
100
	* @param  string $siteId     Site's ID
101
	* @param  array  $siteConfig Site's config
102
	* @return Tag                Tag created for this site
103
	*/
104 20
	public function add($siteId, array $siteConfig = null)
105
	{
106
		// Normalize or retrieve the site definition
107 20
		$siteId = $this->normalizeId($siteId);
108 19
		if (isset($siteConfig))
109
		{
110 15
			$siteConfig = $this->defaultSites->normalizeValue($siteConfig);
111
		}
112
		else
113
		{
114 4
			$siteConfig = $this->defaultSites->get($siteId);
115
		}
116 18
		$siteConfig['extract'] = $this->convertRegexps($siteConfig['extract']);
117 18
		$siteConfig['scrape']  = $this->convertScrapes($siteConfig['scrape']);
118
119
		// Check the safety of attribute filters
120 18
		$this->checkAttributeFilters($siteConfig['attributes']);
121
122
		// Create the tag for this site
123 17
		$tag = new Tag([
124 17
			'attributes' => $this->getAttributesConfig($siteConfig),
125
			'rules'      => [
126 17
				'allowChild' => 'URL',
127
				'autoClose'  => true,
128 17
				'denyChild'  => [$siteId, $this->tagName]
129
			],
130 17
			'template'   => $this->templateBuilder->build($siteId, $siteConfig)
131
		]);
132
133 17
		$this->configurator->templateNormalizer->normalizeTag($tag);
134 17
		$this->configurator->templateChecker->checkTag($tag);
135 16
		$this->configurator->tags->add($siteId, $tag);
136 16
		$this->sites[$siteId] = $siteConfig;
137
138 16
		return $tag;
139
	}
140
141
	/**
142
	* {@inheritdoc}
143
	*/
144 2
	public function finalize()
145
	{
146 2
		$hosts = [];
147 2
		$sites = [];
148 2
		foreach ($this->sites as $siteId => $siteConfig)
149
		{
150 1
			foreach ($siteConfig['host'] as $host)
151
			{
152 1
				$hosts[$host] = $siteId;
153
			}
154 1
			$sites[$siteId] = [$siteConfig['extract'], $siteConfig['scrape']];
155
		}
156
157 2
		$this->configurator->registeredVars['MediaEmbed.hosts'] = new Dictionary($hosts);
158 2
		$this->configurator->registeredVars['MediaEmbed.sites'] = new Dictionary($sites);
159 2
	}
160
161
	/**
162
	* Check the safety of given attributes
163
	*
164
	* @param  array $attributes
165
	* @return void
166
	*/
167 18
	protected function checkAttributeFilters(array $attributes)
168
	{
169 18
		foreach ($attributes as $attrConfig)
170
		{
171 6
			if (empty($attrConfig['filterChain']))
172
			{
173 3
				continue;
174
			}
175 5
			foreach ($attrConfig['filterChain'] as $filter)
176
			{
177 5
				if (substr($filter, 0, 1) !== '#' && !in_array($filter, $this->allowedFilters, true))
178
				{
179 5
					throw new UnsafeFilterException("Filter '$filter' is not allowed in media sites");
180
				}
181
			}
182
		}
183 17
	}
184
185
	/**
186
	* Convert given regexp to a [regexp, map] pair
187
	*
188
	* @param  string $regexp Original regexp
189
	* @return array          [regexp, [list of captures' names]]
190
	*/
191 16
	protected function convertRegexp($regexp)
192
	{
193 16
		$regexp = new Regexp($regexp);
194
195 16
		return [$regexp, $regexp->getCaptureNames()];
196
	}
197
198
	/**
199
	* Convert a list of regexps
200
	*
201
	* @param  string[] $regexps Original list
202
	* @return array[]           Converted list
203
	*/
204 18
	protected function convertRegexps(array $regexps)
205
	{
206 18
		return array_map([$this, 'convertRegexp'], $regexps);
207
	}
208
209
	/**
210
	* Convert all regexps in a scraping config
211
	*
212
	* @param  array $config Original config
213
	* @return array         Converted config
214
	*/
215 4
	protected function convertScrapeConfig(array $config)
216
	{
217 4
		$config['extract'] = $this->convertRegexps($config['extract']);
218 4
		$config['match']   = $this->convertRegexps($config['match']);
219
220 4
		return $config;
221
	}
222
223
	/**
224
	* Convert all regexps in a list of scraping configs
225
	*
226
	* @param  array[] $scrapes Original config
227
	* @return array[]          Converted config
228
	*/
229 18
	protected function convertScrapes(array $scrapes)
230
	{
231 18
		return array_map([$this, 'convertScrapeConfig'], $scrapes);
232
	}
233
234
	/**
235
	* Create the default MEDIA tag
236
	*
237
	* @return void
238
	*/
239 24
	protected function createMediaTag()
240
	{
241 24
		$tag = $this->configurator->tags->add($this->tagName);
242
243
		// This tag should not need to be closed and should not contain itself
244 24
		$tag->rules->autoClose();
245 24
		$tag->rules->denyChild($this->tagName);
246
247
		// Empty this tag's filter chain and add our tag filter
248 24
		$tag->filterChain->clear();
249 24
		$tag->filterChain
250 24
		    ->append(__NAMESPACE__ . '\\Parser::filterTag')
251 24
		    ->resetParameters()
252 24
		    ->addParameterByName('tag')
253 24
		    ->addParameterByName('parser')
254 24
		    ->addParameterByName('MediaEmbed.hosts')
255 24
		    ->addParameterByName('MediaEmbed.sites')
256 24
		    ->addParameterByName('cacheDir')
257 24
		    ->setJS(file_get_contents(__DIR__ . '/Parser/tagFilter.js'));
258 24
	}
259
260
	/**
261
	* Return the list of named captures from a list of [regexp, map] pairs
262
	*
263
	* @param  array[] $regexps List of [regexp, map] pairs
264
	* @return string[]
265
	*/
266 17
	protected function getAttributeNamesFromRegexps(array $regexps)
267
	{
268 17
		$attrNames = [];
269 17
		foreach ($regexps as list($regexp, $map))
270
		{
271 15
			$attrNames += array_flip(array_filter($map));
272
		}
273
274 17
		return $attrNames;
275
	}
276
277
	/**
278
	* Get the attributes config for given site config
279
	*
280
	* @param  array $siteConfig Site's config
281
	* @return array             Map of [attrName => attrConfig]
282
	*/
283 17
	protected function getAttributesConfig(array $siteConfig)
284
	{
285 17
		$attrNames = $this->getAttributeNamesFromRegexps($siteConfig['extract']);
286 17
		foreach ($siteConfig['scrape'] as $scrapeConfig)
287
		{
288 4
			$attrNames += $this->getAttributeNamesFromRegexps($scrapeConfig['extract']);
289
		}
290
291 17
		$attributes = $siteConfig['attributes'] + array_fill_keys(array_keys($attrNames), []);
292 17
		foreach ($attributes as &$attrConfig)
293
		{
294 15
			$attrConfig += ['required' => false];
295
		}
296 17
		unset($attrConfig);
297
298 17
		return $attributes;
299
	}
300
301
	/**
302
	* Validate and normalize a site ID
303
	*
304
	* @param  string $siteId
305
	* @return string
306
	*/
307 20
	protected function normalizeId($siteId)
308
	{
309 20
		$siteId = strtolower($siteId);
310
311 20
		if (!preg_match('(^[a-z0-9]+$)', $siteId))
312
		{
313 1
			throw new InvalidArgumentException('Invalid site ID');
314
		}
315
316 19
		return $siteId;
317
	}
318
}