Completed
Pull Request — master (#532)
by Daniel
04:11
created

Link::getPathFromInternalUrl()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php
2
3
namespace Coyote\Services\Parser\Parsers;
4
5
use Collective\Html\HtmlBuilder;
6
use Coyote\Repositories\Contracts\PageRepositoryInterface as PageRepository;
7
use Coyote\Services\Parser\Parsers\Parentheses\ParenthesesParser;
8
use Coyote\Services\Parser\Parsers\Parentheses\SymmetricParenthesesChunks;
9
10
class Link extends Parser implements ParserInterface
11
{
12
    const LINK_TAG_REGEXP = "<a\s[^>]*href=(\"??)([^\" >]*?)\\1[^>]*>(.*)<\/a>";
13
    const LINK_INTERNAL_REGEXP = '\[\[(.*?)(\|(.*?))*\]\]';
14
15
    const REGEXP_EMAIL = '#(^|[\n \[\]\:<>&;]|\()([a-z0-9&\-_.]+?@[\w\-]+\.(?:[\w\-\.]+\.)?[\w]+)#i';
16
17
    private const PARENTHESES_LEVEL = 3;
18
19
    /**
20
     * @var PageRepository
21
     */
22
    private $page;
23
24
    /**
25
     * @var string
26
     */
27
    private $host;
28
29
    /**
30
     * @var HtmlBuilder|null
31
     */
32
    private $html;
33
34
    /** @var UrlFormatter */
35
    private $urlParser;
36
37
    /**
38
     * Link constructor.
39
     *
40
     * @param PageRepository $page
41
     * @param string $host
42
     * @param HtmlBuilder|null $html
43
     */
44
    public function __construct(PageRepository $page, string $host, HtmlBuilder $html = null)
45
    {
46
        $this->page = $page;
47
        $this->host = $host;
48
        $this->html = $html;
49
        $this->urlParser = new UrlFormatter($host, app('html'), new ParenthesesParser(new SymmetricParenthesesChunks(), self::PARENTHESES_LEVEL));
50
    }
51
52
    /**
53
     * @param string $text
54
     * @return string
55
     */
56
    public function parse($text)
57
    {
58
        // first, make <a> from plain URL's
59
        // -----------------------------------------
60
        $text = $this->hashBlock($text, ['code', 'a']);
61
        $text = $this->hashInline($text, 'img');
62
63
        $text = $this->parseUrl($text);
64
        $text = $this->parseEmail($text);
65
66
        $text = $this->unhash($text);
67
        // ------------------------------------------
68
69
        $text = $this->hashBlock($text, 'code');
70
        $text = $this->hashInline($text, 'img');
71
72
        // then, parse internal links and youtube video links
73
        // --------------------------------------------------
74
        $text = $this->parseLinks($text);
75
76
        $text = $this->hashBlock($text, 'a');
77
78
        // at last, parse coyote markup
79
        $text = $this->parseInternalAccessors($text);
80
81
        $text = $this->unhash($text);
82
83
        return $text;
84
    }
85
86
    /**
87
     * @param string $text
88
     * @return string
89
     */
90
    protected function parseLinks($text)
91
    {
92
        if (!preg_match_all('/' . self::LINK_TAG_REGEXP . '/siU', $text, $matches, PREG_SET_ORDER)) {
93
            return $text;
94
        }
95
96
        for ($i = 0, $count = count($matches); $i < $count; $i++) {
97
            $link = $matches[$i][2];
98
            $title = $matches[$i][3];
99
            $match = $matches[$i][0];
100
101
            $text = $this->parseInternalLink($text, $match, $link, $title);
102
            $text = $this->parseYoutubeLinks($text, $match, $link, $title);
103
        }
104
105
        return $text;
106
    }
107
108
    /**
109
     * @param string $text
110
     * @param string $match
111
     * @param string $url
112
     * @param string $title
113
     * @return string
114
     */
115
    protected function parseInternalLink($text, $match, $url, $title)
116
    {
117
        if (urldecode($title) === urldecode($url) && ($path = $this->getPathFromInternalUrl($url)) !== false) {
118
            $page = $this->page->findByPath($path);
119
120
            if ($page) {
121
                $text = str_replace($match, link_to($url, $page->title), $text);
122
            }
123
        }
124
125
        return $text;
126
    }
127
128
    /**
129
     * @param string $text
130
     * @param string $match
131
     * @param string $url
132
     * @param string $title
133
     * @return string
134
     */
135
    protected function parseYoutubeLinks($text, $match, $url, $title)
136
    {
137
        if ($this->html === null) {
138
            return $text;
139
        }
140
141
        if (urldecode($title) !== urldecode($url)) {
142
            return $text;
143
        }
144
145
        $components = parse_url($url);
146
147
        if ($this->isUrl($components)) {
148
            // get host without "www"
149
            $host = $this->getHost($components['host']);
150
            $path = trim($components['path'], '/');
151
152
            if ($host === 'youtube.com' && $path === 'watch') {
153
                parse_str($components['query'], $query);
154
155
                if (!empty($query['v'])) {
156
                    parse_str($components['fragment'] ?? '', $fragments);
157
158
                    $text = str_replace(
159
                        $match,
160
                        $this->makeIframe($query['v'], $this->timeToSeconds($fragments['t'] ?? null)),
161
                        $text
162
                    );
163
                }
164
            }
165
166
            if ($host === 'youtu.be' && $path !== '') {
167
                parse_str($components['query'] ?? '', $query);
168
169
                $text = str_replace($match, $this->makeIframe($path, $this->timeToSeconds($query['t'] ?? null)), $text);
170
            }
171
        }
172
173
        return $text;
174
    }
175
176
    /**
177
     * Parse "old" coyote links like [[Foo/Bar]] to http://4programmers.net/Foo/Bar
178
     *
179
     * @param string $text
180
     * @return string
181
     */
182
    protected function parseInternalAccessors($text)
183
    {
184
        if (!preg_match_all('/' . self::LINK_INTERNAL_REGEXP . '/i', $text, $matches, PREG_SET_ORDER)) {
185
            return $text;
186
        }
187
188
        for ($i = 0, $count = count($matches); $i < $count; $i++) {
189
            $origin = $matches[$i][0];
190
191
            $path = '/' . str_replace(' ', '_', trim($matches[$i][1], '/?&'));
192
193
            $title = $matches[$i][3] ?? null;
194
            $hash = $this->getHashFromPath($path);
195
196
            $page = $this->page->findByPath($path);
197
            $attr = [];
198
199
            if (empty($page)) {
200
                $attr = ['class' => 'link-broken', 'title' => 'Dokument nie istnieje'];
201
                $path = 'Create' . $path;
202
203
                if (empty($title)) {
204
                    $title = str_replace('_', ' ', last(explode('/', $path)));
205
                }
206
            } else {
207
                $path = $page->path;
208
                $title = $title ?: $page->title;
209
            }
210
211
            $text = str_replace($origin, link_to($path . ($hash ? '#' . $hash : ''), $title, $attr), $text);
212
        }
213
214
        return $text;
215
    }
216
217
    /**
218
     * @param string $text
219
     * @return string
220
     */
221
    protected function parseUrl(string $text): string
222
    {
223
        return $this->urlParser->parse($text);
224
    }
225
226
    /**
227
     * @param string $text
228
     * @return string
229
     */
230
    protected function parseEmail(string $text): string
231
    {
232
        return preg_replace(self::REGEXP_EMAIL, "\$1<a href=\"mailto:\$2\">$2</a>", $text);
233
    }
234
235
    /**
236
     * @param string|null $time
237
     * @return null|string
238
     */
239
    private function timeToSeconds($time)
240
    {
241
        if (!$time) {
242
            return null;
243
        }
244
245
        if (preg_match('/(\d+)m(\d+)s/', $time, $match)) {
246
            return ($match[1] * 60) + $match[2];
247
        }
248
249
        return $time;
250
    }
251
252
    /**
253
     * @param string $videoId
254
     * @param string $start
255
     * @return string
256
     */
257
    private function makeIframe(string $videoId, string $start = null): string
258
    {
259
        $iframe = (string) $this->html->tag('iframe', '', [
260
            'src'             => 'https://youtube.com/embed/' . $videoId . ($start !== null ? "?start=$start" : ''),
261
            'class'           => 'embed-responsive-item',
262
            'allowfullscreen' => 'allowfullscreen'
263
        ]);
264
265
        return (string) $this->html->tag('div', $iframe, ['class' => 'embed-responsive embed-responsive-16by9']);
266
    }
267
268
    /**
269
     * Get path from url only if it's internal link (false if it's NOT internal link)
270
     *
271
     * @example http://4programmers.net/Foo/Bar => /Foo/Bar
272
     * @param string $url
273
     * @return string|false
274
     */
275
    private function getPathFromInternalUrl($url)
276
    {
277
        $components = parse_url($url);
278
        $path = false;
279
280
        if ($this->isUrl($components)) {
281
            // sprawdzamy, czy mamy do czynienia z linkiem wewnetrznym
282
            if ($this->host === $this->getHost($components['host'])) {
283
                $path = urldecode($components['path']);
284
            }
285
        }
286
287
        return $path;
288
    }
289
290
    /**
291
     * @param array|false $components
292
     * @return bool
293
     */
294
    private function isUrl($components)
295
    {
296
        if (!is_array($components)) {
297
            return false;
298
        }
299
300
        return (!empty($components['path']) && !empty($components['host']));
301
    }
302
303
    /**
304
     * @param string $path
305
     * @return string
306
     */
307
    private function getHashFromPath(&$path)
308
    {
309
        $hash = '';
310
311
        if (($pos = strpos($path, '#')) !== false) {
312
            $hash = htmlspecialchars(substr($path, $pos + 1));
313
            $path = substr($path, 0, $pos);
314
        }
315
316
        return $hash;
317
    }
318
319
    /**
320
     * Get host without "www" at the beginning.
321
     *
322
     * @param string $host
323
     * @return string
324
     */
325
    private function getHost(string $host): string
326
    {
327
        $parts = explode('.', $host);
328
329
        if ($parts[0] === 'www') {
330
            array_shift($parts);
331
        }
332
333
        return implode('.', $parts);
334
    }
335
}
336