Verzeichnisstruktur phpBB-3.3.15


Veröffentlicht
28.08.2024

So funktioniert es


Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück

Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

Configurator.php

Zuletzt modifiziert: 02.04.2025, 15:04 - Dateigröße: 11.02 KiB


001  <?php
002   
003  /**
004  * @package   s9e\TextFormatter
005  * @copyright Copyright (c) 2010-2022 The s9e authors
006  * @license   http://www.opensource.org/licenses/mit-license.php The MIT License
007  */
008  namespace s9e\TextFormatter\Plugins\Preg;
009   
010  use DOMAttr;
011  use DOMText;
012  use DOMXPath;
013  use Exception;
014  use InvalidArgumentException;
015  use s9e\TextFormatter\Configurator\Helpers\NodeLocator;
016  use s9e\TextFormatter\Configurator\Helpers\RegexpParser;
017  use s9e\TextFormatter\Configurator\Helpers\TemplateLoader;
018  use s9e\TextFormatter\Configurator\Helpers\TemplateModifier;
019  use s9e\TextFormatter\Configurator\Items\Regexp;
020  use s9e\TextFormatter\Configurator\Items\Tag;
021  use s9e\TextFormatter\Configurator\JavaScript\RegexpConvertor;
022  use s9e\TextFormatter\Configurator\Validators\TagName;
023  use s9e\TextFormatter\Plugins\ConfiguratorBase;
024   
025  class Configurator extends ConfiguratorBase
026  {
027      /**
028      * @var array[] Captures from current regexp
029      */
030      protected $captures;
031   
032      /**
033      * @var array[] List of [tagName, regexp, passthroughIdx]
034      */
035      protected $collection = [];
036   
037      /**
038      * @var string Delimiter used in current regexp
039      */
040      protected $delimiter;
041   
042      /**
043      * @var string Non-D modifiers used in current regexp
044      */
045      protected $modifiers;
046   
047      /**
048      * @var array References used in current template
049      */
050      protected $references;
051   
052      /**
053      * @var string Regexp used to find references in the templates. We check that the reference is
054      *             not preceded with an odd number of backslashes
055      */
056      protected $referencesRegexp = '((?<!\\\\)(?:\\\\\\\\)*\\K(?:[$\\\\]\\d+|\\$\\{\\d+\\}))S';
057   
058      /**
059      * {@inheritdoc}
060      */
061      public function asConfig()
062      {
063          if (!count($this->collection))
064          {
065              return;
066          }
067   
068          $pregs = [];
069          foreach ($this->collection as list($tagName, $regexp, $passthroughIdx))
070          {
071              $captures = RegexpParser::getCaptureNames($regexp);
072              $pregs[]  = [$tagName, new Regexp($regexp, true), $passthroughIdx, $captures];
073          }
074   
075          return ['generics' => $pregs];
076      }
077   
078      /**
079      * {@inheritdoc}
080      */
081      public function getJSHints()
082      {
083          $hasPassthrough = false;
084          foreach ($this->collection as list($tagName, $regexp, $passthroughIdx))
085          {
086              if ($passthroughIdx)
087              {
088                  $hasPassthrough = true;
089                  break;
090              }
091          }
092   
093          return ['PREG_HAS_PASSTHROUGH' => $hasPassthrough];
094      }
095   
096      /**
097      * Configure a pattern-based match
098      *
099      * @param  string $regexp   Regexp to be used by the parser
100      * @param  string $tagName  Name of the tag that holds the matched text
101      * @return void
102      */
103      public function match($regexp, $tagName)
104      {
105          $tagName        = TagName::normalize($tagName);
106          $passthroughIdx = 0;
107          $this->parseRegexp($regexp);
108          foreach ($this->captures as $i => $capture)
109          {
110              if (!$this->isCatchAll((string) $capture['expr']))
111              {
112                  continue;
113              }
114              $passthroughIdx = $i;
115          }
116   
117          $this->collection[] = [$tagName, $regexp, $passthroughIdx];
118      }
119   
120      /**
121      * Configure a pattern-based replacement
122      *
123      * @param  string $regexp   Regexp to be used by the parser
124      * @param  string $template Template to be used for rendering
125      * @param  string $tagName  Name of the tag to create. A name based on the regexp is
126      *                          automatically generated if none is provided
127      * @return Tag              The tag created to represent this replacement
128      */
129      public function replace($regexp, $template, $tagName = null)
130      {
131          if (!isset($tagName))
132          {
133              $tagName = 'PREG_' . strtoupper(dechex(crc32($regexp)));
134          }
135          $this->parseRegexp($regexp);
136          $this->parseTemplate($template);
137   
138          $passthroughIdx = $this->getPassthroughCapture();
139          if ($passthroughIdx)
140          {
141              $this->captures[$passthroughIdx]['passthrough'] = true;
142          }
143   
144          $regexp   = $this->fixUnnamedCaptures($regexp);
145          $template = $this->convertTemplate($template, $passthroughIdx);
146   
147          $this->collection[] = [$tagName, $regexp, $passthroughIdx];
148   
149          return $this->createTag($tagName, $template);
150      }
151   
152      /**
153      * Add given attribute to given tag based on parsed captures
154      *
155      * @param  Tag    $tag
156      * @param  string $attrName
157      * @return void
158      */
159      protected function addAttribute(Tag $tag, $attrName)
160      {
161          $isUrl = false;
162          $exprs = [];
163          foreach ($this->captures as $key => $capture)
164          {
165              if ($capture['name'] !== $attrName)
166              {
167                  continue;
168              }
169              $exprs[] = $capture['expr'];
170              if (isset($this->references['asUrl'][$key]))
171              {
172                  $isUrl = true;
173              }
174          }
175          $exprs = array_unique($exprs);
176   
177          $regexp = $this->delimiter . '^';
178          $regexp .= (count($exprs) === 1) ? $exprs[0] : '(?:' . implode('|', $exprs) . ')';
179          $regexp .= '$' . $this->delimiter . 'D' . $this->modifiers;
180   
181          $attribute = $tag->attributes->add($attrName);
182   
183          $filter = $this->configurator->attributeFilters['#regexp'];
184          $filter->setRegexp($regexp);
185          $attribute->filterChain[] = $filter;
186   
187          if ($isUrl)
188          {
189              $filter = $this->configurator->attributeFilters['#url'];
190              $attribute->filterChain[] = $filter;
191          }
192      }
193   
194      /**
195      * Convert a preg-style replacement to a template
196      *
197      * @param  string  $template       Original template
198      * @param  integer $passthroughIdx Index of the passthrough capture
199      * @return string                  Modified template
200      */
201      protected function convertTemplate($template, $passthroughIdx)
202      {
203          // Replace numeric references in the template with the value of the corresponding attribute
204          // values or passthrough
205          $template = TemplateModifier::replaceTokens(
206              $template,
207              $this->referencesRegexp,
208              function ($m, $node) use ($passthroughIdx)
209              {
210                  $key = (int) trim($m[0], '\\${}');
211                  if ($key === 0)
212                  {
213                      // $0 copies the whole textContent
214                      return ['expression', '.'];
215                  }
216                  if ($key === $passthroughIdx && $node instanceof DOMText)
217                  {
218                      // Passthrough capture, does not include start/end tags
219                      return ['passthrough'];
220                  }
221                  if (isset($this->captures[$key]['name']))
222                  {
223                      // Normal capture, replaced by the equivalent expression
224                      return ['expression', '@' . $this->captures[$key]['name']];
225                  }
226   
227                  // Non-existent captures are simply ignored, similarly to preg_replace()
228                  return ['literal', ''];
229              }
230          );
231   
232          // Unescape backslashes and special characters in the template
233          $template = TemplateModifier::replaceTokens(
234              $template,
235              '(\\\\+[0-9${\\\\])',
236              function ($m)
237              {
238                  return ['literal', stripslashes($m[0])];
239              }
240          );
241   
242          return $template;
243      }
244   
245      /**
246      * Create the tag that matches current regexp
247      *
248      * @param  string $tagName
249      * @param  string $template
250      * @return Tag
251      */
252      protected function createTag($tagName, $template)
253      {
254          $tag = new Tag;
255          foreach ($this->captures as $key => $capture)
256          {
257              if (!isset($capture['name']))
258              {
259                  continue;
260              }
261   
262              $attrName = $capture['name'];
263              if (isset($tag->attributes[$attrName]))
264              {
265                  continue;
266              }
267   
268              $this->addAttribute($tag, $attrName);
269          }
270          $tag->template = $template;
271   
272          // Normalize the tag's template
273          $this->configurator->templateNormalizer->normalizeTag($tag);
274   
275          // Check the safeness of this tag
276          $this->configurator->templateChecker->checkTag($tag);
277   
278          return $this->configurator->tags->add($tagName, $tag);
279      }
280   
281      /**
282      * Give a name to unnamed captures that are referenced in current replacement
283      *
284      * @param  string $regexp Original regexp
285      * @return string         Modified regexp
286      */
287      protected function fixUnnamedCaptures($regexp)
288      {
289          $keys = [];
290          foreach ($this->references['anywhere'] as $key)
291          {
292              $capture = $this->captures[$key];
293              if (!$key || isset($capture['name']))
294              {
295                  continue;
296              }
297              // Give the capture a name if it's used as URL or it's not a passthrough
298              if (isset($this->references['asUrl'][$key]) || !isset($capture['passthrough']))
299              {
300                  $keys[] = $key;
301              }
302          }
303   
304          // Alter the original regexp to inject the subpatterns' names. The position is equal to the
305          // subpattern's position plus 2, to account for the delimiter at the start of the regexp and
306          // the opening parenthesis of the subpattern. Also, we need to process them in reverse order
307          // so that replacements don't change the position of subsequent subpatterns
308          rsort($keys);
309          foreach ($keys as $key)
310          {
311              $name   = '_' . $key;
312              $pos    = $this->captures[$key]['pos'];
313              $regexp = substr_replace($regexp, "?'" . $name . "'", 2 + $pos, 0);
314              $this->captures[$key]['name'] = $name;
315          }
316   
317          return $regexp;
318      }
319   
320      /**
321      * Get the index of the capture used for passthrough in current replacement
322      *
323      * @return integer
324      */
325      protected function getPassthroughCapture()
326      {
327          $passthrough = 0;
328          foreach ($this->references['inText'] as $key)
329          {
330              if (!$this->isCatchAll((string) $this->captures[$key]['expr']))
331              {
332                  // Ignore if it's not a catch-all expression such as .*?
333                  continue;
334              }
335              if ($passthrough)
336              {
337                  // Abort if there's more than 1 possible passthrough
338                  $passthrough = 0;
339                  break;
340              }
341              $passthrough = (int) $key;
342          }
343   
344          return $passthrough;
345      }
346   
347      /**
348      * Parse a regexp and return its info
349      *
350      * @param  string $regexp
351      * @return array
352      */
353      protected function getRegexpInfo($regexp)
354      {
355          if (@preg_match_all($regexp, '') === false)
356          {
357              throw new InvalidArgumentException('Invalid regexp');
358          }
359   
360          return RegexpParser::parse($regexp);
361      }
362   
363      /**
364      * Test whether given expression is a catch-all expression such as .*?
365      *
366      * @param  string $expr Subpattern
367      * @return bool
368      */
369      protected function isCatchAll($expr)
370      {
371          return (bool) preg_match('(^\\.[*+]\\??$)D', $expr);
372      }
373   
374      /**
375      * Parse given regexp and store its information
376      *
377      * @param  string  $regexp
378      * @return void
379      */
380      protected function parseRegexp($regexp)
381      {
382          $this->captures = [['name' => null, 'expr' => null]];
383          $regexpInfo = $this->getRegexpInfo($regexp);
384          $this->delimiter = $regexpInfo['delimiter'];
385          $this->modifiers = str_replace('D', '', $regexpInfo['modifiers']);
386          foreach ($regexpInfo['tokens'] as $token)
387          {
388              if ($token['type'] !== 'capturingSubpatternStart')
389              {
390                  continue;
391              }
392              $this->captures[] = [
393                  'pos'    => $token['pos'],
394                  'name'   => $token['name'] ?? null,
395                  'expr'   => $token['content']
396              ];
397          }
398      }
399   
400      /**
401      * Parse given template and store the references it contains
402      *
403      * @param  string $template
404      * @return void
405      */
406      protected function parseTemplate($template)
407      {
408          $this->references = [
409              'anywhere' => [],
410              'asUrl'    => [],
411              'inText'   => []
412          ];
413   
414          preg_match_all($this->referencesRegexp, $template, $matches);
415          foreach ($matches[0] as $match)
416          {
417              $key = trim($match, '\\${}');
418              $this->references['anywhere'][$key] = $key;
419          }
420   
421          $dom   = TemplateLoader::load($template);
422          $xpath = new DOMXPath($dom);
423          foreach ($xpath->query('//text()') as $node)
424          {
425              preg_match_all($this->referencesRegexp, $node->textContent, $matches);
426              foreach ($matches[0] as $match)
427              {
428                  $key = trim($match, '\\${}');
429                  $this->references['inText'][$key] = $key;
430              }
431          }
432   
433          foreach (NodeLocator::getURLNodes($dom) as $node)
434          {
435              // We only bother with literal attributes that start with a capture
436              if ($node instanceof DOMAttr
437               && preg_match('(^(?:[$\\\\]\\d+|\\$\\{\\d+\\}))', trim($node->value), $m))
438              {
439                  $key = trim($m[0], '\\${}');
440                  $this->references['asUrl'][$key] = $key;
441              }
442          }
443   
444          $this->removeUnknownReferences();
445      }
446   
447      /**
448      * Remove references that do not correspond to an existing capture
449      *
450      * @return void
451      */
452      protected function removeUnknownReferences()
453      {
454          foreach ($this->references as &$references)
455          {
456              $references = array_intersect_key($references, $this->captures);
457          }
458      }
459  }