Passed
Pull Request — 4 (#10017)
by Thomas
09:46
created

EmbedShortcodeProvider::cleanKeySegment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View\Shortcodes;
4
5
use Embed\Http\DispatcherInterface;
6
use Psr\SimpleCache\CacheInterface;
7
use Psr\SimpleCache\InvalidArgumentException;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Core\Extensible;
10
use SilverStripe\Core\Injector\Injector;
11
use SilverStripe\ORM\ArrayList;
12
use SilverStripe\ORM\FieldType\DBField;
13
use SilverStripe\View\ArrayData;
14
use SilverStripe\View\Embed\Embeddable;
15
use SilverStripe\View\Embed\EmbedResource;
16
use SilverStripe\View\HTML;
17
use SilverStripe\View\Parsers\ShortcodeHandler;
18
use Embed\Adapters\Adapter;
19
use Embed\Exceptions\InvalidUrlException;
20
use SilverStripe\View\Parsers\ShortcodeParser;
21
use SilverStripe\Control\Director;
22
23
/**
24
 * Provider for the [embed] shortcode tag used by the embedding service
25
 * in the HTML Editor field.
26
 * Provides the html needed for the frontend and the editor field itself.
27
 */
28
class EmbedShortcodeProvider implements ShortcodeHandler
29
{
30
31
    /**
32
     * Gets the list of shortcodes provided by this handler
33
     *
34
     * @return mixed
35
     */
36
    public static function get_shortcodes()
37
    {
38
        return ['embed'];
39
    }
40
41
    /**
42
     * Embed shortcode parser from Oembed. This is a temporary workaround.
43
     * Oembed class has been replaced with the Embed external service.
44
     *
45
     * @param array $arguments
46
     * @param string $content
47
     * @param ShortcodeParser $parser
48
     * @param string $shortcode
49
     * @param array $extra
50
     *
51
     * @return string
52
     */
53
    public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = [])
54
    {
55
        // Get service URL
56
        if (!empty($content)) {
57
            $serviceURL = $content;
58
        } elseif (!empty($arguments['url'])) {
59
            $serviceURL = $arguments['url'];
60
        } else {
61
            return '';
62
        }
63
64
        $class = $arguments['class'] ?? '';
65
        $width = $arguments['width'] ?? '';
66
        $height = $arguments['height'] ?? '';
67
68
        // Try to use cached result
69
        $cache = static::getCache();
70
        $key = static::deriveCacheKey($serviceURL, $class, $width, $height);
71
        try {
72
            if ($cache->has($key)) {
73
                return $cache->get($key);
74
            }
75
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
76
        }
77
78
        // See https://github.com/oscarotero/Embed#example-with-all-options for service arguments
79
        $serviceArguments = [];
80
        if (!empty($arguments['width'])) {
81
            $serviceArguments['min_image_width'] = $arguments['width'];
82
        }
83
        if (!empty($arguments['height'])) {
84
            $serviceArguments['min_image_height'] = $arguments['height'];
85
        }
86
87
        /** @var EmbedResource $embed */
88
        $embed = Injector::inst()->create(Embeddable::class, $serviceURL);
89
        if (!empty($serviceArguments)) {
90
            $embed->setOptions(array_merge($serviceArguments, (array) $embed->getOptions()));
91
        }
92
93
        // Allow resolver to be mocked
94
        $dispatcher = null;
95
        if (isset($extra['resolver'])) {
96
            $dispatcher = Injector::inst()->create(
97
                $extra['resolver']['class'],
98
                $serviceURL,
99
                $extra['resolver']['config']
100
            );
101
        } elseif (Injector::inst()->has(DispatcherInterface::class)) {
102
            $dispatcher = Injector::inst()->get(DispatcherInterface::class);
103
        }
104
105
        if ($dispatcher) {
106
            $embed->setDispatcher($dispatcher);
107
        }
108
109
        // Process embed
110
        try {
111
            $embed = $embed->getEmbed();
112
        } catch (InvalidUrlException $e) {
113
            $message = (Director::isDev())
114
                ? $e->getMessage()
115
                : _t(__CLASS__ . '.INVALID_URL', 'There was a problem loading the media.');
116
117
            $attr = [
118
                'class' => 'ss-media-exception embed'
119
            ];
120
121
            $result = HTML::createTag(
122
                'div',
123
                $attr,
124
                HTML::createTag('p', [], $message)
125
            );
126
            return $result;
127
        }
128
129
        // Convert embed object into HTML
130
        if ($embed && $embed instanceof Adapter) {
0 ignored issues
show
introduced by
$embed is always a sub-type of Embed\Adapters\Adapter.
Loading history...
131
            $result = static::embedForTemplate($embed, $arguments);
132
        }
133
        // Fallback to link to service
134
        if (!$result) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
135
            $result = static::linkEmbed($arguments, $serviceURL, $serviceURL);
136
        }
137
        // Cache result
138
        if ($result) {
139
            try {
140
                $cache->set($key, $result);
141
            } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
142
            }
143
        }
144
        return $result;
145
    }
146
147
    /**
148
     * @param Adapter $embed
149
     * @param array $arguments Additional shortcode params
150
     * @return string
151
     */
152
    public static function embedForTemplate($embed, $arguments)
153
    {
154
        switch ($embed->getType()) {
155
            case 'video':
156
            case 'rich':
157
                // Attempt to inherit width (but leave height auto)
158
                if (empty($arguments['width']) && $embed->getWidth()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embed->getWidth() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
159
                    $arguments['width'] = $embed->getWidth();
160
                }
161
                return static::videoEmbed($arguments, $embed->getCode());
162
            case 'link':
163
                return static::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
164
            case 'photo':
165
                return static::photoEmbed($arguments, $embed->getUrl());
166
            default:
167
                return null;
168
        }
169
    }
170
171
    /**
172
     * Build video embed tag
173
     *
174
     * @param array $arguments
175
     * @param string $content Raw HTML content
176
     * @return string
177
     */
178
    protected static function videoEmbed($arguments, $content)
179
    {
180
        // Ensure outer div has given width (but leave height auto)
181
        if (!empty($arguments['width'])) {
182
            $arguments['style'] = 'width: ' . intval($arguments['width']) . 'px;';
183
        }
184
185
        // override iframe dimension attributes provided by webservice with ones specified in shortcode arguments
186
        foreach (['width', 'height'] as $attr) {
187
            if (!($value = $arguments[$attr] ?? false)) {
188
                continue;
189
            }
190
            foreach (['"', "'"] as $quote) {
191
                $rx = "/(<iframe .*?)$attr=$quote([0-9]+)$quote([^>]+>)/";
192
                $content = preg_replace($rx, "$1{$attr}={$quote}{$value}{$quote}$3", $content);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $3 seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $1 seems to be never defined.
Loading history...
193
            }
194
        }
195
196
        $data = [
197
            'Arguments' => $arguments,
198
            'Attributes' => static::buildAttributeListFromArguments($arguments, ['width', 'height', 'url', 'caption']),
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\View\Shortc...buteListFromArguments() has been deprecated: 4.5.0 Use {$Arguments.name} directly in shortcode templates to access argument values ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

198
            'Attributes' => /** @scrutinizer ignore-deprecated */ static::buildAttributeListFromArguments($arguments, ['width', 'height', 'url', 'caption']),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
199
            'Content' => DBField::create_field('HTMLFragment', $content)
200
        ];
201
202
        return ArrayData::create($data)->renderWith(self::class . '_video')->forTemplate();
203
    }
204
205
    /**
206
     * Build <a> embed tag
207
     *
208
     * @param array $arguments
209
     * @param string $href
210
     * @param string $title Default title
211
     * @return string
212
     */
213
    protected static function linkEmbed($arguments, $href, $title)
214
    {
215
        $data = [
216
            'Arguments' => $arguments,
217
            'Attributes' => static::buildAttributeListFromArguments($arguments, ['width', 'height', 'url', 'caption']),
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\View\Shortc...buteListFromArguments() has been deprecated: 4.5.0 Use {$Arguments.name} directly in shortcode templates to access argument values ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

217
            'Attributes' => /** @scrutinizer ignore-deprecated */ static::buildAttributeListFromArguments($arguments, ['width', 'height', 'url', 'caption']),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
218
            'Href' => $href,
219
            'Title' => !empty($arguments['caption']) ? ($arguments['caption']) : $title
220
        ];
221
222
        return ArrayData::create($data)->renderWith(self::class . '_link')->forTemplate();
223
    }
224
225
    /**
226
     * Build img embed tag
227
     *
228
     * @param array $arguments
229
     * @param string $src
230
     * @return string
231
     */
232
    protected static function photoEmbed($arguments, $src)
233
    {
234
        $data = [
235
            'Arguments' => $arguments,
236
            'Attributes' => static::buildAttributeListFromArguments($arguments, ['url']),
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\View\Shortc...buteListFromArguments() has been deprecated: 4.5.0 Use {$Arguments.name} directly in shortcode templates to access argument values ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

236
            'Attributes' => /** @scrutinizer ignore-deprecated */ static::buildAttributeListFromArguments($arguments, ['url']),

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
237
            'Src' => $src
238
        ];
239
240
        return ArrayData::create($data)->renderWith(self::class . '_photo')->forTemplate();
241
    }
242
243
    /**
244
     * Build a list of HTML attributes from embed arguments - used to preserve backward compatibility
245
     *
246
     * @deprecated 4.5.0 Use {$Arguments.name} directly in shortcode templates to access argument values
247
     * @param array $arguments List of embed arguments
248
     * @param array $exclude List of attribute names to exclude from the resulting list
249
     * @return ArrayList
250
     */
251
    private static function buildAttributeListFromArguments(array $arguments, array $exclude = []): ArrayList
252
    {
253
        $attributes = ArrayList::create();
254
        foreach ($arguments as $key => $value) {
255
            if (in_array($key, $exclude)) {
256
                continue;
257
            }
258
259
            $attributes->push(ArrayData::create([
260
                'Name' => $key,
261
                'Value' => Convert::raw2att($value)
262
            ]));
263
        }
264
265
        return $attributes;
266
    }
267
268
    /**
269
     * @param ShortcodeParser $parser
270
     * @param string $content
271
     */
272
    public static function flushCachedShortcodes(ShortcodeParser $parser, string $content): void
273
    {
274
        $cache = static::getCache();
275
        $tags = $parser->extractTags($content);
276
        foreach ($tags as $tag) {
277
            if (!isset($tag['open']) || $tag['open'] != 'embed') {
278
                continue;
279
            }
280
            $url = $tag['content'] ?? $tag['attrs']['url'] ?? '';
281
            $class = $tag['attrs']['class'] ?? '';
282
            $width = $tag['attrs']['width'] ?? '';
283
            $height = $tag['attrs']['height'] ?? '';
284
            if (!$url) {
285
                continue;
286
            }
287
            $key = static::deriveCacheKey($url, $class, $width, $height);
288
            try {
289
                if (!$cache->has($key)) {
290
                    continue;
291
                }
292
                $cache->delete($key);
293
            } catch (InvalidArgumentException $e) {
294
                continue;
295
            }
296
        }
297
    }
298
299
    /**
300
     * @return CacheInterface
301
     */
302
    private static function getCache(): CacheInterface
303
    {
304
        return Injector::inst()->get(CacheInterface::class . '.EmbedShortcodeProvider');
305
    }
306
307
    /**
308
     * @param string $url
309
     * @return string
310
     */
311
    private static function deriveCacheKey(string $url, string $class, string $width, string $height): string
312
    {
313
        return implode('-', array_filter([
314
            'embed-shortcode',
315
            self::cleanKeySegment($url),
316
            self::cleanKeySegment($class),
317
            self::cleanKeySegment($width),
318
            self::cleanKeySegment($height)
319
        ]));
320
    }
321
322
    /**
323
     * @param string $str
324
     * @return string
325
     */
326
    private static function cleanKeySegment(string $str): string
327
    {
328
        return preg_replace('/[^a-zA-Z0-9\-]/', '', $str);
329
    }
330
}
331