ShortcodeParser::enable()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 4
rs 10
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 CMS\Shortcode;
13
14
use Cake\Event\Event;
15
use Cake\Event\EventManager;
16
use Cake\Routing\Router;
17
use CMS\Core\StaticCacheTrait;
18
use CMS\Event\EventDispatcher;
19
use CMS\View\View;
20
21
/**
22
 * Provides methods for shortcode parsing.
23
 *
24
 * Shortcodes looks as follow:
25
 *
26
 * 1. Self-closing form:
27
 *
28
 *     {my_shortcode attr1=val1 attr2=val2 ... /}
29
 *
30
 * 2. Enclosed form:
31
 *
32
 *     {my_shortcode attr1=val1 attr2=val2 ... } content {/my_shortcode}
33
 *
34
 * Shortcodes can be escaped by using an additional `{` symbol, for instance:
35
 *
36
 *     {{ something }}
37
 *     // this will actually prints `{ something }`
38
 *
39
 *     {{something} dummy {/something}}
40
 *     // this will actually prints `{something} dummy {/something}`
41
 *
42
 */
43
class ShortcodeParser
44
{
45
46
    use StaticCacheTrait;
47
48
    /**
49
     * Default context to use.
50
     *
51
     * @var object
52
     */
53
    protected static $_defaultContext = null;
54
55
    /**
56
     * Holds a list of all registered shortcodes.
57
     *
58
     * @var array
59
     */
60
    protected static $_listeners = [];
61
62
    /**
63
     * Parser status.
64
     *
65
     * The `parse()` method will not work when set to false.
66
     *
67
     * @var boolean
68
     */
69
    protected static $_enabled = true;
70
71
    /**
72
     * Look for shortcodes in the given $text.
73
     *
74
     * @param string $text The content to parse
75
     * @param object $context The context for \Cake\Event\Event::$subject, if not
76
     *  given an instance of this class will be used
77
     * @return string
78
     */
79
    public static function parse($text, $context = null)
80
    {
81
        if (!static::$_enabled || strpos($text, '{') === false) {
82
            return $text;
83
        }
84
85
        if ($context === null) {
86
            $context = static::_getDefaultContext();
87
        }
88
89
        static::cache('context', $context);
90
        $pattern = static::_regex();
91
92
        return preg_replace_callback("/{$pattern}/s", 'static::_doShortcode', $text);
93
    }
94
95
    /**
96
     * Removes all shortcodes from the given content. Useful when converting a
97
     * string to plain text.
98
     *
99
     * @param string $text Text from which to remove shortcodes
100
     * @return string Content without shortcodes markers
101
     */
102
    public static function strip($text)
103
    {
104
        $tagregexp = implode('|', array_map('preg_quote', static::_list()));
105
106
        return preg_replace('/(.?){(' . $tagregexp . ')\b(.*?)(?:(\/))?}(?:(.+?){\/\2})?(.?)/s', '$1$6', $text);
107
    }
108
109
    /**
110
     * Escapes all shortcodes from the given content.
111
     *
112
     * @param string $text Text from which to escape shortcodes
113
     * @return string Content with all shortcodes escaped
114
     */
115
    public static function escape($text)
116
    {
117
        $tagregexp = implode('|', array_map('preg_quote', static::_list()));
118
        preg_match_all('/(.?){(' . $tagregexp . ')\b(.*?)(?:(\/))?}(?:(.+?){\/\2})?(.?)/s', $text, $matches);
119
120
        foreach ($matches[0] as $ht) {
121
            $replace = str_replace_once('{', '{{', $ht);
122
            $replace = str_replace_last('}', '}}', $replace);
123
            $text = str_replace($ht, $replace, $text);
124
        }
125
126
        return $text;
127
    }
128
129
    /**
130
     * Enables shortcode parser.
131
     *
132
     * @return void
133
     */
134
    public static function enable()
135
    {
136
        static::$_enabled = true;
137
    }
138
139
    /**
140
     * Globally disables shortcode parser.
141
     *
142
     * The `parser()` method will not work when disabled.
143
     *
144
     * @return void
145
     */
146
    public static function disable()
147
    {
148
        static::$_enabled = false;
149
    }
150
151
    /**
152
     * Returns a list of all registered shortcodes.
153
     *
154
     * @return array
155
     */
156
    protected static function _list()
157
    {
158
        if (empty(static::$_listeners)) {
159
            $manager = EventDispatcher::instance('Shortcode')->eventManager();
160
            static::$_listeners = listeners($manager);
161
        }
162
163
        return static::$_listeners;
164
    }
165
166
    /**
167
     * Gets default context to use.
168
     *
169
     * @return \CMS\View\View
170
     */
171
    protected static function _getDefaultContext()
172
    {
173
        if (!static::$_defaultContext) {
174
            static::$_defaultContext = new View(Router::getRequest(), null, EventManager::instance(), []);
175
        }
176
177
        return static::$_defaultContext;
178
    }
179
180
    /**
181
     * Retrieve the shortcode regular expression for searching.
182
     *
183
     * The regular expression combines the shortcode tags in the regular expression
184
     * in a regex class.
185
     *
186
     * The regular expression contains 6 different sub matches to help with parsing.
187
     *
188
     * 1 - An extra { to allow for escaping shortcodes: {{ something }}
189
     * 2 - The shortcode name
190
     * 3 - The shortcode argument list
191
     * 4 - The self closing /
192
     * 5 - The content of a shortcode when it wraps some content.
193
     * 6 - An extra } to allow for escaping shortcode
194
     *
195
     * @author WordPress
196
     * @return string The shortcode search regular expression
197
     */
198
    protected static function _regex()
199
    {
200
        $tagregexp = implode('|', array_map('preg_quote', static::_list()));
201
        // @codingStandardsIgnoreStart
202
        return
203
            '\\{'                                // Opening bracket
204
            . '(\\{?)'                           // 1: Optional second opening bracket for escaping shortcodes: {{tag}}
205
            . "($tagregexp)"                     // 2: Shortcode name
206
            . '(?![\\w-])'                       // Not followed by word character or hyphen
207
            . '('                                // 3: Unroll the loop: Inside the opening shortcode tag
208
            .     '[^\\}\\/]*'                   // Not a closing bracket or forward slash
209
            .     '(?:'
210
            .         '\\/(?!\\})'               // A forward slash not followed by a closing bracket
211
            .         '[^\\}\\/]*'               // Not a closing bracket or forward slash
212
            .     ')*?'
213
            . ')'
214
            . '(?:'
215
            .     '(\\/)'                        // 4: Self closing tag ...
216
            .     '\\}'                          // ... and closing bracket
217
            . '|'
218
            .     '\\}'                          // Closing bracket
219
            .     '(?:'
220
            .         '('                        // 5: Unroll the loop: Optionally, anything between the opening and closing shortcode tags
221
            .             '[^\\{]*+'             // Not an opening bracket
222
            .             '(?:'
223
            .                 '\\{(?!\\/\\2\\})' // An opening bracket not followed by the closing shortcode tag
224
            .                 '[^\\{]*+'         // Not an opening bracket
225
            .             ')*+'
226
            .         ')'
227
            .         '\\{\\/\\2\\}'             // Closing shortcode tag
228
            .     ')?'
229
            . ')'
230
            . '(\\}?)';                          // 6: Optional second closing brocket for escaping shortcodes: {{tag}}
231
        // @codingStandardsIgnoreEnd
232
    }
233
234
    /**
235
     * Invokes shortcode lister method for the given shortcode.
236
     *
237
     * @param array $m Shortcode as preg array
238
     * @return string
239
     * @author WordPress
240
     */
241
    protected static function _doShortcode($m)
242
    {
243
        // allow {{foo}} syntax for escaping a tag
244
        if ($m[1] == '{' && $m[6] == '}') {
245
            return substr($m[0], 1, -1);
246
        }
247
248
        $tag = $m[2];
249
        $atts = static::_parseAttributes($m[3]);
250
        $listeners = EventDispatcher::instance('Shortcode')
251
            ->eventManager()
252
            ->listeners($tag);
253
254
        if (!empty($listeners)) {
255
            $options = [
256
                'atts' => (array)$atts,
257
                'content' => null,
258
                'tag' => $tag
259
            ];
260
261
            if (isset($m[5])) {
262
                $options['content'] = $m[5];
263
            }
264
265
            $result = EventDispatcher::instance('Shortcode')
266
                ->triggerArray([$tag, static::cache('context')], $options)
267
                ->result;
268
269
            return $m[1] . $result . $m[6];
270
        }
271
272
        return '';
273
    }
274
275
    /**
276
     * Looks for shortcode's attributes.
277
     *
278
     * Attribute names are always converted to lowercase. Values are untouched.
279
     *
280
     * ## Example:
281
     *
282
     *     {shortcode_name attr1="value1" aTTr2=value2 CamelAttr=Val1 /}
283
     *
284
     * Produces:
285
     *
286
     * ```php
287
     * [
288
     *     'attr1' => 'value1',
289
     *     'attr2' => 'value2',
290
     *     'camelattr' => 'Val1',
291
     * ]
292
     * ```
293
     *
294
     * @param string $text The text where to look for shortcodes
295
     * @return array Associative array of attributes as `tag_name` => `value`
296
     * @author WordPress
297
     */
298
    protected static function _parseAttributes($text)
299
    {
300
        $atts = [];
301
        $pattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
302
        $text = preg_replace("/[\x{00a0}\x{200b}]+/u", ' ', $text);
303
304
        if (preg_match_all($pattern, $text, $match, PREG_SET_ORDER)) {
305
            foreach ($match as $m) {
306
                if (!empty($m[1])) {
307
                    $atts[strtolower($m[1])] = stripcslashes($m[2]);
308
                } elseif (!empty($m[3])) {
309
                    $atts[strtolower($m[3])] = stripcslashes($m[4]);
310
                } elseif (!empty($m[5])) {
311
                    $atts[strtolower($m[5])] = stripcslashes($m[6]);
312
                } elseif (isset($m[7]) && strlen($m[7])) {
313
                    $atts[] = stripcslashes($m[7]);
314
                } elseif (isset($m[8])) {
315
                    $atts[] = stripcslashes($m[8]);
316
                }
317
            }
318
        } else {
319
            $atts = ltrim($text);
320
        }
321
322
        return $atts;
323
    }
324
}
325