Passed
Push — develop ( 0ab425...86b1c6 )
by Schlaefer
03:37
created

JbbCodeAutolinkVisitor::hashLink()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 14
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace Plugin\BbcodeParser\src\Lib\jBBCode\Visitors;
14
15
use Plugin\BbcodeParser\src\Lib\Helper\UrlParserTrait;
16
17
/**
18
 * Handles all implicit linking in a text (autolink URLs, tags, ...)
19
 */
20
class JbbCodeAutolinkVisitor extends JbbCodeTextVisitor
21
{
22
    use UrlParserTrait;
23
24
    protected $_disallowedTags = ['code'];
25
26
    /**
27
     * {@inheritDoc}
28
     */
29
    protected function _processTextNode($string, $node)
30
    {
31
        // don't auto-link in url tags; problem is that 'urlWithAttributes' definition
32
        // reuses 'url' tag with ParseContent = true
33
        if ($node->getParent()->getTagName() === 'url') {
34
            return $string;
35
        }
36
        $string = $this->hashLink($string);
37
        $string = $this->atUserLink($string);
38
39
        return $this->autolink($string);
40
    }
41
42
    /**
43
     * Links @<username> to the user's profile.
44
     *
45
     * @param string $string The Text to be parsed.
46
     * @return string The text with usernames linked.
47
     */
48
    protected function atUserLink(string $string): string
49
    {
50
        $tags = [];
51
52
        /*
53
         * - '\pP' all unicode punctuation marks
54
         * - '<' if nl2br has taken place whatchout for <br /> linebreaks
55
         */
56
        $hasTags = preg_match_all('/(\s|^)@([^\s\pP<]+)/m', $string, $tags);
57
        if (!$hasTags) {
58
            return $string;
59
        }
60
61
        // would be cleaner to pass userlist by value, but for performance reasons
62
        // we only query the db if we actually have any @ tags
63
        $users = $this->_sOptions->get('UserList')->get();
64
        sort($users);
65
        $names = [];
66
        if (empty($tags[2]) === false) {
67
            $tags = $tags[2];
68
            foreach ($tags as $tag) {
69
                if (in_array($tag, $users)) {
70
                    $names[$tag] = 1;
71
                } else {
72
                    $continue = 0;
73
                    foreach ($users as $user) {
74
                        if (mb_strpos($user, $tag) === 0) {
75
                            $names[$user] = 1;
76
                            $continue = true;
77
                        }
78
                        if ($continue === false) {
79
                            break;
80
                        } elseif ($continue !== 0) {
81
                            $continue = false;
82
                        }
83
                    }
84
                }
85
            }
86
        }
87
        krsort($names);
88
        $baseUrl = $this->_sOptions->get('webroot') . $this->_sOptions->get('atBaseUrl');
89
        foreach ($names as $name => $v) {
90
            $title = urlencode($name);
91
            $link = $this->_url(
92
                $baseUrl . $title,
93
                "@$name",
94
                false
95
            );
96
            $string = str_replace("@$name", $link, $string);
97
        }
98
99
        return $string;
100
    }
101
102
    /**
103
     * Autolinks URLs not surrounded by explicit URL-tags for user-convenience.
104
     *
105
     * @param string $string The text to be parsed for URLs.
106
     * @return string The text with URLs linked.
107
     */
108
    protected function autolink(string $string): string
109
    {
110
        $replace = function (array $matches): string {
111
            /// don't link locally
112
            if (strpos($matches['element'], 'file://') !== false) {
113
                return $matches['element'];
114
            }
115
116
            /// exclude punctuation at end of sentence from URLs
117
            $ignoredEndChars = implode('|', [',', '\?', ',', '\.', '\)', '!']);
118
            preg_match(
119
                '/(?P<element>.*?)(?P<suffix>' . $ignoredEndChars . ')?$/',
120
                $matches['element'],
121
                $m
122
            );
123
124
            /// exclude ignored end chars if paired in URL foo.com/bar_(baz)
125
            if (!empty($m['suffix'])) {
126
                $ignoredIfNotPaired = [
127
                    ['open' => '(', 'close' => ')'],
128
                ];
129
                foreach ($ignoredIfNotPaired as $pair) {
130
                    $isUnpaired = substr_count($m['element'], $pair['open']) > substr_count($m['element'], $pair['close']);
131
                    if ($isUnpaired) {
132
                        $m['element'] .= $m['suffix'];
133
                        unset($m['suffix']);
134
                    }
135
                }
136
            }
137
138
            /// keep ['element'] and ['suffix'] and include ['prefix']; (array) for phpstan
139
            $matches = (array)($m + $matches);
140
141
            if (strpos($matches['element'], '://') === false) {
142
                $matches['element'] = 'http://' . $matches['element'];
143
            }
144
            $matches += [
145
                'prefix' => '',
146
                'suffix' => '',
147
            ];
148
149
            $url = $this->_url(
150
                $matches['element'],
151
                $matches['element'],
152
                false,
153
                true
154
            );
155
156
            return $matches['prefix'] . $url . $matches['suffix'];
157
        };
158
159
        //# autolink http://urls
160
        $string = preg_replace_callback(
161
            "#(?<=^|[\n (])(?P<element>[\w]+?://.*?[^ \"\n\r\t<]*)#is",
162
            $replace,
163
            $string
164
        );
165
166
        //# autolink without http://, i.e. www.foo.bar/baz
167
        $string = preg_replace_callback(
168
            "#(?P<prefix>^|[\n (])(?P<element>(www|ftp)\.[\w\-]+\.[\w\-.\~]+(?:/[^ \"\t\n\r<]*)?)#is",
169
            $replace,
170
            $string
171
        );
172
173
        //# autolink email
174
        $string = preg_replace_callback(
175
            "#(?<=^|[\n ])(?P<content>([a-z0-9&\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+))#i",
176
            function ($matches) {
177
                return $this->_email($matches['content']);
178
            },
179
            $string
180
        );
181
182
        return $string;
183
    }
184
185
    /**
186
     * Links #<posting-ID> to that posting.
187
     *
188
     * @param string $string Text to be parsed for #<id>.
189
     * @return string Text containing hash-links.
190
     */
191
    protected function hashLink(string $string): string
192
    {
193
        $baseUrl = $this->_sOptions->get('webroot') . $this->_sOptions->get('hashBaseUrl');
194
        $string = preg_replace_callback(
195
            '/(?<=\s|^|]|\()(?<tag>#)(?<element>\d+)(?!\w)/',
196
            function (array $m) use ($baseUrl): string {
197
                $hash = $m['element'];
198
199
                return $this->_url($baseUrl . $hash, '#' . $hash);
200
            },
201
            $string
202
        );
203
204
        return $string;
205
    }
206
}
207