Passed
Pull Request — 4 (#10244)
by Steve
15:32
created

EmbedShortcodeProvider::flushCachedShortcodes()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 23
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 18
nc 7
nop 2
dl 0
loc 23
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View\Shortcodes;
4
5
use Embed\Http\Crawler;
6
use Embed\Http\NetworkException;
7
use Embed\Http\RequestException;
8
use Psr\SimpleCache\CacheInterface;
9
use Psr\SimpleCache\InvalidArgumentException;
10
use SilverStripe\Core\Convert;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\FieldType\DBField;
14
use SilverStripe\View\ArrayData;
15
use SilverStripe\View\Embed\Embeddable;
16
use SilverStripe\View\HTML;
17
use SilverStripe\View\Parsers\ShortcodeHandler;
18
use SilverStripe\View\Parsers\ShortcodeParser;
19
use SilverStripe\Control\Director;
20
use SilverStripe\Dev\Deprecation;
21
use SilverStripe\View\Embed\EmbedContainer;
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 (false && $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 EmbedContainer $embeddable */
88
        $embeddable = Injector::inst()->create(Embeddable::class, $serviceURL);
89
90
        // Only EmbedContainer is currently supported
91
        if (!($embeddable instanceof EmbedContainer)) {
0 ignored issues
show
introduced by
$embeddable is always a sub-type of SilverStripe\View\Embed\EmbedContainer.
Loading history...
92
            throw new \RuntimeException('Emeddable must extend EmbedContainer');
93
        }
94
95
        $crawler = Injector::inst()->get(Crawler::class);
96
        $embeddable->setCrawler($crawler);
97
98
        if (!empty($serviceArguments)) {
99
            $embeddable->setOptions(array_merge($serviceArguments, (array) $embeddable->getOptions()));
100
        }
101
102
        // Process embed
103
        try {
104
            // this will trigger a request/response which will then be cached within $embeddable
105
            $embeddable->getExtractor();
106
        } catch (NetworkException | RequestException $e) {
107
            $message = (Director::isDev())
108
                ? $e->getMessage()
109
                : _t(__CLASS__ . '.INVALID_URL', 'There was a problem loading the media.');
110
111
            $attr = [
112
                'class' => 'ss-media-exception embed'
113
            ];
114
115
            $result = HTML::createTag(
116
                'div',
117
                $attr,
118
                HTML::createTag('p', [], $message)
119
            );
120
            return $result;
121
        }
122
123
        // Convert embed object into HTML
124
        $html = static::embeddableToHtml($embeddable, $arguments);
125
        // Fallback to link to service
126
        if (!$html) {
127
            $result = static::linkEmbed($arguments, $serviceURL, $serviceURL);
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
128
        }
129
        // Cache result
130
        if ($html) {
131
            try {
132
                $cache->set($key, $html);
133
            } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
134
            }
135
        }
136
        return $html;
137
    }
138
139
    public static function embeddableToHtml(Embeddable $embeddable, array $arguments): string
140
    {
141
        // Only EmbedContainer is supported
142
        if (!($embeddable instanceof EmbedContainer)) {
143
            return '';
144
        }
145
        $extractor = $embeddable->getExtractor();
146
        $type = $embeddable->getType();
147
        if ($type === 'video' || $type === 'rich') {
148
            // Attempt to inherit width (but leave height auto)
149
            if (empty($arguments['width']) && $embeddable->getWidth()) {
150
                $arguments['width'] = $embeddable->getWidth();
151
            }
152
            return static::videoEmbed($arguments, $extractor->code->html);
153
        }
154
        if ($type === 'photo') {
155
            return static::photoEmbed($arguments, (string) $extractor->url);
156
        }
157
        if ($type === 'link') {
158
            return static::linkEmbed($arguments, (string) $extractor->url, $extractor->title);
159
        }
160
        return '';
161
    }
162
163
    /**
164
     * @param Adapter $embed
0 ignored issues
show
Bug introduced by
The type SilverStripe\View\Shortcodes\Adapter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
165
     * @param array $arguments Additional shortcode params
166
     * @return string
167
     * @deprecated 4.11..5.0 Use embeddableToHtml instead
168
     */
169
    public static function embedForTemplate($embed, $arguments)
170
    {
171
        Deprecation::notice('4.11', 'Use embeddableToHtml() instead');
172
        switch ($embed->getType()) {
173
            case 'video':
174
            case 'rich':
175
                // Attempt to inherit width (but leave height auto)
176
                if (empty($arguments['width']) && $embed->getWidth()) {
177
                    $arguments['width'] = $embed->getWidth();
178
                }
179
                return static::videoEmbed($arguments, $embed->getCode());
180
            case 'link':
181
                return static::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
182
            case 'photo':
183
                return static::photoEmbed($arguments, $embed->getUrl());
184
            default:
185
                return null;
186
        }
187
    }
188
189
    /**
190
     * Build video embed tag
191
     *
192
     * @param array $arguments
193
     * @param string $content Raw HTML content
194
     * @return string
195
     */
196
    protected static function videoEmbed($arguments, $content)
197
    {
198
        // Ensure outer div has given width (but leave height auto)
199
        if (!empty($arguments['width'])) {
200
            $arguments['style'] = 'width: ' . intval($arguments['width']) . 'px;';
201
        }
202
203
        // override iframe dimension attributes provided by webservice with ones specified in shortcode arguments
204
        foreach (['width', 'height'] as $attr) {
205
            if (!($value = $arguments[$attr] ?? false)) {
206
                continue;
207
            }
208
            foreach (['"', "'"] as $quote) {
209
                $rx = "/(<iframe .*?)$attr=$quote([0-9]+)$quote([^>]+>)/";
210
                $content = preg_replace($rx, "$1{$attr}={$quote}{$value}{$quote}$3", $content);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $1 seems to be never defined.
Loading history...
Comprehensibility Best Practice introduced by
The variable $3 seems to be never defined.
Loading history...
211
            }
212
        }
213
214
        $data = [
215
            'Arguments' => $arguments,
216
            '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

216
            '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...
217
            'Content' => DBField::create_field('HTMLFragment', $content)
218
        ];
219
220
        return ArrayData::create($data)->renderWith(self::class . '_video')->forTemplate();
221
    }
222
223
    /**
224
     * Build <a> embed tag
225
     *
226
     * @param array $arguments
227
     * @param string $href
228
     * @param string $title Default title
229
     * @return string
230
     */
231
    protected static function linkEmbed($arguments, $href, $title)
232
    {
233
        $data = [
234
            'Arguments' => $arguments,
235
            '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

235
            '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...
236
            'Href' => $href,
237
            'Title' => !empty($arguments['caption']) ? ($arguments['caption']) : $title
238
        ];
239
240
        return ArrayData::create($data)->renderWith(self::class . '_link')->forTemplate();
241
    }
242
243
    /**
244
     * Build img embed tag
245
     *
246
     * @param array $arguments
247
     * @param string $src
248
     * @return string
249
     */
250
    protected static function photoEmbed($arguments, $src)
251
    {
252
        $data = [
253
            'Arguments' => $arguments,
254
            '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

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