Completed
Push — master ( f6e833...c164ec )
by Josh
16:26
created

Configurator::finalize()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

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