Configurator::add()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 30
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

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