Completed
Push — master ( fdaddd...d64c68 )
by Josh
15:44
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-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\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
		// Create a MEDIA tag
70 25
		$this->createMediaTag();
71
72
		// Create a [MEDIA] BBCode if applicable
73 25
		if ($this->createMediaBBCode)
74
		{
75 24
			$this->configurator->BBCodes->set($this->tagName, ['contentAttributes' => ['url']]);
76
		}
77 25
	}
78
79
	/**
80
	* {@inheritdoc}
81
	*/
82 5
	public function asConfig()
83
	{
84 5
		if (empty($this->sites))
85
		{
86 1
			return;
87
		}
88
89
		return [
90 4
			'quickMatch' => $this->quickMatch,
91 4
			'regexp'     => $this->regexp,
92 4
			'tagName'    => $this->tagName
93
		];
94
	}
95
96
	/**
97
	* Add a media site
98
	*
99
	* @param  string $siteId     Site's ID
100
	* @param  array  $siteConfig Site's config
101
	* @return Tag                Tag created for this site
102
	*/
103 21
	public function add($siteId, array $siteConfig = null)
104
	{
105
		// Normalize or retrieve the site definition
106 21
		$siteId = $this->normalizeId($siteId);
107 20
		if (isset($siteConfig))
108
		{
109 15
			$siteConfig = $this->defaultSites->normalizeValue($siteConfig);
110
		}
111
		else
112
		{
113 5
			$siteConfig = $this->defaultSites->get($siteId);
114
		}
115 19
		$siteConfig['extract'] = $this->convertRegexps($siteConfig['extract']);
116 19
		$siteConfig['scrape']  = $this->convertScrapes($siteConfig['scrape']);
117
118
		// Check the safety of attribute filters
119 19
		$this->checkAttributeFilters($siteConfig['attributes']);
120
121
		// Create the tag for this site
122 18
		$tag = new Tag([
123 18
			'attributes' => $this->getAttributesConfig($siteConfig),
124
			'rules'      => [
125 18
				'allowChild' => 'URL',
126
				'autoClose'  => true,
127 18
				'denyChild'  => [$siteId, $this->tagName]
128
			],
129 18
			'template'   => $this->templateBuilder->build($siteId, $siteConfig)
130
		]);
131
132 18
		$this->configurator->templateNormalizer->normalizeTag($tag);
133 18
		$this->configurator->templateChecker->checkTag($tag);
134 17
		$this->configurator->tags->add($siteId, $tag);
135 17
		$this->sites[$siteId] = $siteConfig;
136
137 17
		return $tag;
138
	}
139
140
	/**
141
	* {@inheritdoc}
142
	*/
143 2
	public function finalize()
144
	{
145 2
		$hosts = [];
146 2
		$sites = [];
147 2
		foreach ($this->sites as $siteId => $siteConfig)
148
		{
149 1
			foreach ($siteConfig['host'] as $host)
150
			{
151 1
				$hosts[$host] = $siteId;
152
			}
153 1
			$sites[$siteId] = [$siteConfig['extract'], $siteConfig['scrape']];
154
		}
155
156 2
		$this->configurator->registeredVars['MediaEmbed.hosts'] = new Dictionary($hosts);
157 2
		$this->configurator->registeredVars['MediaEmbed.sites'] = new Dictionary($sites);
158 2
	}
159
160
	/**
161
	* Return the list of configured sites
162
	*
163
	* @return array Site's ID as keys, site's config as values
164
	*/
165 1
	public function getSites()
166
	{
167 1
		return $this->sites;
168
	}
169
170
	/**
171
	* Check the safety of given attributes
172
	*
173
	* @param  array $attributes
174
	* @return void
175
	*/
176 19
	protected function checkAttributeFilters(array $attributes)
177
	{
178 19
		foreach ($attributes as $attrConfig)
179
		{
180 7
			if (empty($attrConfig['filterChain']))
181
			{
182 3
				continue;
183
			}
184 6
			foreach ($attrConfig['filterChain'] as $filter)
185
			{
186 6
				if (substr($filter, 0, 1) !== '#' && !in_array($filter, $this->allowedFilters, true))
187
				{
188 6
					throw new RuntimeException("Filter '$filter' is not allowed in media sites");
189
				}
190
			}
191
		}
192 18
	}
193
194
	/**
195
	* Convert given regexp to a [regexp, map] pair
196
	*
197
	* @param  string $regexp Original regexp
198
	* @return array          [regexp, [list of captures' names]]
199
	*/
200 17
	protected function convertRegexp($regexp)
201
	{
202 17
		$regexp = new Regexp($regexp);
203
204 17
		return [$regexp, $regexp->getCaptureNames()];
205
	}
206
207
	/**
208
	* Convert a list of regexps
209
	*
210
	* @param  string[] $regexps Original list
211
	* @return array[]           Converted list
212
	*/
213 19
	protected function convertRegexps(array $regexps)
214
	{
215 19
		return array_map([$this, 'convertRegexp'], $regexps);
216
	}
217
218
	/**
219
	* Convert all regexps in a scraping config
220
	*
221
	* @param  array $config Original config
222
	* @return array         Converted config
223
	*/
224 5
	protected function convertScrapeConfig(array $config)
225
	{
226 5
		$config['extract'] = $this->convertRegexps($config['extract']);
227 5
		$config['match']   = $this->convertRegexps($config['match']);
228
229 5
		return $config;
230
	}
231
232
	/**
233
	* Convert all regexps in a list of scraping configs
234
	*
235
	* @param  array[] $scrapes Original config
236
	* @return array[]          Converted config
237
	*/
238 19
	protected function convertScrapes(array $scrapes)
239
	{
240 19
		return array_map([$this, 'convertScrapeConfig'], $scrapes);
241
	}
242
243
	/**
244
	* Create the default MEDIA tag
245
	*
246
	* @return void
247
	*/
248 25
	protected function createMediaTag()
249
	{
250 25
		$tag = $this->configurator->tags->add($this->tagName);
251
252
		// This tag should not need to be closed and should not contain itself
253 25
		$tag->rules->autoClose();
254 25
		$tag->rules->denyChild($this->tagName);
255
256
		// Empty this tag's filter chain and add our tag filter
257 25
		$tag->filterChain->clear();
258 25
		$tag->filterChain
259 25
		    ->append(__NAMESPACE__ . '\\Parser::filterTag')
260 25
		    ->resetParameters()
261 25
		    ->addParameterByName('tag')
262 25
		    ->addParameterByName('parser')
263 25
		    ->addParameterByName('MediaEmbed.hosts')
264 25
		    ->addParameterByName('MediaEmbed.sites')
265 25
		    ->addParameterByName('cacheDir')
266 25
		    ->setJS(file_get_contents(__DIR__ . '/Parser/tagFilter.js'));
267 25
	}
268
269
	/**
270
	* Return the list of named captures from a list of [regexp, map] pairs
271
	*
272
	* @param  array[] $regexps List of [regexp, map] pairs
273
	* @return string[]
274
	*/
275 18
	protected function getAttributeNamesFromRegexps(array $regexps)
276
	{
277 18
		$attrNames = [];
278 18
		foreach ($regexps as list($regexp, $map))
279
		{
280 16
			$attrNames += array_flip(array_filter($map));
281
		}
282
283 18
		return $attrNames;
284
	}
285
286
	/**
287
	* Get the attributes config for given site config
288
	*
289
	* @param  array $siteConfig Site's config
290
	* @return array             Map of [attrName => attrConfig]
291
	*/
292 18
	protected function getAttributesConfig(array $siteConfig)
293
	{
294 18
		$attrNames = $this->getAttributeNamesFromRegexps($siteConfig['extract']);
295 18
		foreach ($siteConfig['scrape'] as $scrapeConfig)
296
		{
297 5
			$attrNames += $this->getAttributeNamesFromRegexps($scrapeConfig['extract']);
298
		}
299
300 18
		$attributes = $siteConfig['attributes'] + array_fill_keys(array_keys($attrNames), []);
301 18
		foreach ($attributes as &$attrConfig)
302
		{
303 16
			$attrConfig += ['required' => false];
304
		}
305 18
		unset($attrConfig);
306
307 18
		return $attributes;
308
	}
309
310
	/**
311
	* Validate and normalize a site ID
312
	*
313
	* @param  string $siteId
314
	* @return string
315
	*/
316 21
	protected function normalizeId($siteId)
317
	{
318 21
		$siteId = strtolower($siteId);
319
320 21
		if (!preg_match('(^[a-z0-9]+$)', $siteId))
321
		{
322 1
			throw new InvalidArgumentException('Invalid site ID');
323
		}
324
325 20
		return $siteId;
326
	}
327
}