Passed
Push — master ( a025c6...613f63 )
by Josh
03:19
created

Configurator::getSites()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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