ILIAS  trunk Revision v11.0_alpha-1713-gd8962da2f67
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
MakeClickable.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
21 namespace ILIAS\Refinery\String;
22 
27 
28 class MakeClickable implements Transformation
29 {
32 
33  private const URL_PATTERN = '(^|[^[:alnum:]])(((https?:\/\/)|(www.))[^[:cntrl:][:space:]<>\'"]+)([^[:alnum:]]|$)';
34 
35  private bool $open_in_new_tab;
36 
37  public function __construct($open_in_new_tab = true)
38  {
39  $this->open_in_new_tab = $open_in_new_tab;
40  }
41 
42  public function transform($from): string
43  {
44  $this->requireString($from);
45 
46  return $this->replaceMatches($from, fn(int $startOfMatch, int $endOfMatch, string $url, string $protocol): string => (
47  $this->shouldReplace($from, $startOfMatch, $endOfMatch) ?
48  $this->replace($url, $protocol) :
49  $url
50  ));
51  }
52 
53  private function replaceMatches(string $from, callable $replace): string
54  {
55  $endOfLastMatch = 0;
56  $stringParts = [];
57 
58  while (null !== ($matches = $this->match(self::URL_PATTERN, substr($from, $endOfLastMatch)))) {
59  $startOfMatch = $endOfLastMatch + strpos(substr($from, $endOfLastMatch), $matches[0]);
60  $endOfMatch = $startOfMatch + strlen($matches[1] . $matches[2]);
61 
62  $stringParts[] = substr($from, $endOfLastMatch, $startOfMatch - $endOfLastMatch);
63  $stringParts[] = $matches[1] . $replace($startOfMatch, $endOfMatch, $matches[2], $matches[4]);
64 
65  $endOfLastMatch = $endOfMatch;
66  }
67 
68  $stringParts[] = substr($from, $endOfLastMatch);
69 
70  return implode('', $stringParts);
71  }
72 
73  private function regexPos(string $regexp, string $string): int
74  {
75  $matches = $this->match($regexp, $string);
76  if (null !== $matches) {
77  return strpos($string, $matches[0]);
78  }
79 
80  return strlen($string);
81  }
82 
87  private function requireString($maybeHTML): void
88  {
89  if (!is_string($maybeHTML)) {
90  throw new ConstraintViolationException('not a string', 'not_a_string');
91  }
92  }
93 
94  private function shouldReplace(string $maybeHTML, int $startOfMatch, int $endOfMatch): bool
95  {
96  $isNotInAnchor = $this->regexPos('<a.*</a>', substr($maybeHTML, $endOfMatch)) <= $this->regexPos('</a>', substr($maybeHTML, $endOfMatch));
97  $isNotATagAttribute = null === $this->match('^[^>]*[[:space:]][[:alpha:]]+<', strrev(substr($maybeHTML, 0, $startOfMatch)));
98 
99  return $isNotInAnchor && $isNotATagAttribute;
100  }
101 
106  private function match(string $pattern, string $haystack): ?array
107  {
108  $pattern = str_replace('@', '\@', $pattern);
109  return 1 === preg_match('@' . $pattern . '@', $haystack, $matches) ? $matches : null;
110  }
111 
112  private function replace(string $url, string $protocol): string
113  {
114  $maybeProtocol = !$protocol ? 'https://' : '';
115  return sprintf(
116  '<a%s href="%s">%s</a>',
117  $this->additionalAttributes(),
118  $maybeProtocol . $url,
119  $url
120  );
121  }
122 
123  protected function additionalAttributes(): string
124  {
125  if ($this->open_in_new_tab) {
126  return ' target="_blank" rel="noopener"';
127  }
128 
129  return '';
130  }
131 }
transform($from)
Perform the transformation.
$url
Definition: shib_logout.php:66
replace(string $url, string $protocol)
match(string $pattern, string $haystack)
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
replaceMatches(string $from, callable $replace)
__construct($open_in_new_tab=true)
regexPos(string $regexp, string $string)
A transformation is a function from one datatype to another.
shouldReplace(string $maybeHTML, int $startOfMatch, int $endOfMatch)