LinkHelper::_crumbs()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 12
rs 9.4285
1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace Menu\View\Helper;
13
14
use Cake\Datasource\EntityInterface;
15
use Cake\I18n\I18n;
16
use Cake\Routing\Router;
17
use Cake\View\View;
18
use CMS\View\Helper;
19
use Menu\View\BreadcrumbRegistry;
20
21
/**
22
 * Link helper.
23
 *
24
 * Utility helper used by MenuHelper. Provides some utility methods for working with
25
 * menu links, such as `isActive()` method which check if the given link matches
26
 * current URL.
27
 */
28
class LinkHelper extends Helper
29
{
30
31
    /**
32
     * Default configuration for this class.
33
     *
34
     * - `breadcrumbGuessing`: Whether to mark an item as "active" if its URL is on
35
     *   the breadcrumb stack. Defaults to true
36
     *
37
     * @var array
38
     */
39
    protected $_defaultConfig = [
40
        'breadcrumbGuessing' => true,
41
    ];
42
43
    /**
44
     * Returns a safe URL string for later use with HtmlHelper.
45
     *
46
     * @param string|array $url URL given as string or an array compatible
47
     *  with `Router::url()`
48
     * @return string
49
     */
50
    public function url($url)
51
    {
52
        if (is_string($url)) {
53
            $url = $this->localePrefix($url);
54
        }
55
56
        try {
57
            $url = Router::url($url, true);
58
        } catch (\Exception $ex) {
59
            $url = '';
60
        }
61
62
        return $url;
63
    }
64
65
    /**
66
     * Checks if the given menu link should be marked as active.
67
     *
68
     * If `$item->activation` is a callable function it will be used to determinate
69
     * if the link should be active or not, returning true from callable indicates
70
     * link should be active, false indicates it should not be marked as active.
71
     * Callable receives current request object as first argument and $item as second.
72
     *
73
     * `$item->url` property MUST exists if "activation" is not a callable, and can
74
     * be either:
75
     *
76
     * - A string representing an external or internal URL (all internal links must
77
     *   starts with "/"). e.g. `/user/login`
78
     *
79
     * - An array compatible with \Cake\Routing\Router::url(). e.g. `['controller'
80
     *   => 'users', 'action' => 'login']`
81
     *
82
     * Both examples are equivalent.
83
     *
84
     * @param \Cake\Datasource\EntityInterface $item A menu's item
85
     * @return bool
86
     */
87
    public function isActive(EntityInterface $item)
88
    {
89
        if ($item->has('activation') && is_callable($item->get('activation'))) {
90
            $callable = $item->get('activation');
91
92
            return $callable($this->_View->request, $item);
93
        }
94
95
        $itemUrl = $this->sanitize($item->get('url'));
96
        if (!str_starts_with($itemUrl, '/')) {
97
            return false;
98
        }
99
100
        switch ($item->get('activation')) {
101
            case 'any':
102
                return $this->_requestMatches($item->get('active'));
103
            case 'none':
104
                return !$this->_requestMatches($item->get('active'));
105
            case 'php':
106
                return php_eval($item->get('active'), [
107
                    'view', &$this->_View,
108
                    'item', &$item,
109
                ]) === true;
110
            case 'auto':
111
            default:
112
                static $requestUri = null;
113
                static $requestUrl = null;
114
115
                if ($requestUri === null) {
116
                    $requestUri = urldecode(env('REQUEST_URI'));
117
                    $requestUrl = str_replace('//', '/', '/' . urldecode($this->_View->request->url) . '/');
118
                }
119
120
                $isInternal =
121
                    $itemUrl !== '/' &&
122
                    str_ends_with($itemUrl, str_replace_once($this->baseUrl(), '', $requestUri));
123
                $isIndex =
124
                    $itemUrl === '/' &&
125
                    $this->_View->request->isHome();
126
                $isExact =
127
                    str_replace('//', '/', "{$itemUrl}/") === $requestUrl ||
128
                    $itemUrl == $requestUri;
129
130
                if ($this->config('breadcrumbGuessing')) {
131
                    return ($isInternal || $isIndex || $isExact || in_array($itemUrl, $this->_crumbs()));
132
                }
133
134
                return ($isInternal || $isIndex || $isExact);
135
        }
136
    }
137
138
    /**
139
     * Sanitizes the given URL by making sure it's suitable for menu links.
140
     *
141
     * @param string $url Item's URL to sanitize
142
     * @return string Valid URL, empty string on error
143
     */
144
    public function sanitize($url)
145
    {
146
        try {
147
            $url = Router::url($url);
148
        } catch (\Exception $ex) {
149
            return '';
150
        }
151
152
        if (!str_starts_with($url, '/')) {
153
            return $url;
154
        }
155
156
        if (str_starts_with($url, $this->baseUrl())) {
157
            $url = str_replace_once($this->baseUrl(), '', $url);
158
        }
159
160
        return $this->localePrefix($url);
161
    }
162
163
    /**
164
     * Prepends language code to the given URL if the "url_locale_prefix" directive
165
     * is enabled.
166
     *
167
     * @param string $url The URL to fix
168
     * @return string Locale prefixed URL
169
     */
170
    public function localePrefix($url)
171
    {
172
        if (option('url_locale_prefix') &&
173
            str_starts_with($url, '/') &&
174
            !preg_match('/^\/' . $this->_localesPattern() . '/', $url)
175
        ) {
176
            $url = '/' . I18n::locale() . $url;
177
        }
178
179
        return $url;
180
    }
181
182
    /**
183
     * Calculates site's base URL.
184
     *
185
     * @return string Site's base URL
186
     */
187
    public function baseUrl()
188
    {
189
        static $base = null;
190
        if ($base === null) {
191
            $base = $this->_View->request->base;
192
        }
193
194
        return $base;
195
    }
196
197
    /**
198
     * Gets a list of all URLs present in current crumbs stack.
199
     *
200
     * @return array List of URLs
201
     */
202
    protected function _crumbs()
203
    {
204
        static $crumbs = null;
205
        if ($crumbs === null) {
206
            $crumbs = BreadcrumbRegistry::getUrls();
207
            foreach ($crumbs as &$crumb) {
208
                $crumb = $this->sanitize($crumb);
209
            }
210
        }
211
212
        return $crumbs;
213
    }
214
215
    /**
216
     * Check if current request path matches any pattern in a set of patterns.
217
     *
218
     * @param string $patterns String containing a set of patterns separated by \n,
219
     *  \r or \r\n
220
     * @return bool TRUE if the path matches a pattern, FALSE otherwise
221
     */
222
    protected function _requestMatches($patterns)
223
    {
224
        if (empty($patterns)) {
225
            return false;
226
        }
227
228
        $request = $this->_View->request;
229
        $path = '/' . urldecode($request->url);
230
        $patterns = explode("\n", $patterns);
231
232
        foreach ($patterns as &$p) {
233
            $p = $this->_View->Url->build('/') . $p;
234
            $p = str_replace('//', '/', $p);
235
            $p = str_replace($request->base, '', $p);
236
            $p = $this->localePrefix($p);
237
        }
238
239
        $patterns = implode("\n", $patterns);
240
241
        // Convert path settings to a regular expression.
242
        // Therefore replace newlines with a logical or, /* with asterisks and "/" with the front page.
243
        $toReplace = [
244
            '/(\r\n?|\n)/', // newlines
245
            '/\\\\\*/', // asterisks
246
            '/(^|\|)\/($|\|)/' // front '/'
247
        ];
248
249
        $replacements = [
250
            '|',
251
            '.*',
252
            '\1' . preg_quote($this->_View->Url->build('/'), '/') . '\2'
253
        ];
254
255
        $patternsQuoted = preg_quote($patterns, '/');
256
        $patterns = '/^(' . preg_replace($toReplace, $replacements, $patternsQuoted) . ')$/';
257
258
        return (bool)preg_match($patterns, $path);
259
    }
260
261
    /**
262
     * Returns a regular expression that is used to verify if an URL starts
263
     * or not with a language prefix.
264
     *
265
     * ## Example:
266
     *
267
     * ```
268
     * (en\-us|fr|es|it)
269
     * ```
270
     *
271
     * @return string
272
     */
273
    protected function _localesPattern()
274
    {
275
        $cacheKey = '_localesPattern';
276
        $cache = static::cache($cacheKey);
277
        if ($cache) {
278
            return $cache;
279
        }
280
281
        $pattern = '(' . implode(
282
            '|',
283
            array_map(
284
                'preg_quote',
285
                array_keys(
286
                    quickapps('languages')
287
                )
288
            )
289
        ) . ')';
290
291
        return static::cache($cacheKey, $pattern);
292
    }
293
}
294