Passed
Pull Request — 4 (#10244)
by Steve
08:09
created

EmbedShortcodeProvider::getCache()   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 0
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\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 ($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
        if (!empty($serviceArguments)) {
96
            $embeddable->setOptions(array_merge($serviceArguments, (array) $embeddable->getOptions()));
97
        }
98
99
        // Process embed
100
        try {
101
            // this will trigger a request/response which will then be cached within $embeddable
102
            $embeddable->getExtractor();
103
        } catch (NetworkException | RequestException $e) {
104
            $message = (Director::isDev())
105
                ? $e->getMessage()
106
                : _t(__CLASS__ . '.INVALID_URL', 'There was a problem loading the media.');
107
108
            $attr = [
109
                'class' => 'ss-media-exception embed'
110
            ];
111
112
            $result = HTML::createTag(
113
                'div',
114
                $attr,
115
                HTML::createTag('p', [], $message)
116
            );
117
            return $result;
118
        }
119
120
        // Convert embed object into HTML
121
        $html = static::embeddableToHtml($embeddable, $arguments);
122
        // Fallback to link to service
123
        if (!$html) {
124
            $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...
125
        }
126
        // Cache result
127
        if ($html) {
128
            try {
129
                $cache->set($key, $html);
130
            } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
131
            }
132
        }
133
        return $html;
134
    }
135
136
    public static function embeddableToHtml(Embeddable $embeddable, array $arguments): string
137
    {
138
        // Only EmbedContainer is supported
139
        if (!($embeddable instanceof EmbedContainer)) {
140
            return '';
141
        }
142
        $extractor = $embeddable->getExtractor();
143
        $type = $embeddable->getType();
144
        if ($type === 'video' || $type === 'rich') {
145
            // Attempt to inherit width (but leave height auto)
146
            if (empty($arguments['width']) && $embeddable->getWidth()) {
147
                $arguments['width'] = $embeddable->getWidth();
148
            }
149
            return static::videoEmbed($arguments, $extractor->code->html);
150
        }
151
        if ($type === 'photo') {
152
            return static::photoEmbed($arguments, (string) $extractor->url);
153
        }
154
        if ($type === 'link') {
155
            return static::linkEmbed($arguments, (string) $extractor->url, $extractor->title);
156
        }
157
        return '';
158
    }
159
160
    /**
161
     * @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...
162
     * @param array $arguments Additional shortcode params
163
     * @return string
164
     * @deprecated 4.11..5.0 Use embeddableToHtml instead
165
     */
166
    public static function embedForTemplate($embed, $arguments)
167
    {
168
        Deprecation::notice('4.11', 'Use embeddableToHtml() instead');
169
        switch ($embed->getType()) {
170
            case 'video':
171
            case 'rich':
172
                // Attempt to inherit width (but leave height auto)
173
                if (empty($arguments['width']) && $embed->getWidth()) {
174
                    $arguments['width'] = $embed->getWidth();
175
                }
176
                return static::videoEmbed($arguments, $embed->getCode());
177
            case 'link':
178
                return static::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
179
            case 'photo':
180
                return static::photoEmbed($arguments, $embed->getUrl());
181
            default:
182
                return null;
183
        }
184
    }
185
186
    /**
187
     * Build video embed tag
188
     *
189
     * @param array $arguments
190
     * @param string $content Raw HTML content
191
     * @return string
192
     */
193
    protected static function videoEmbed($arguments, $content)
194
    {
195
        // Ensure outer div has given width (but leave height auto)
196
        if (!empty($arguments['width'])) {
197
            $arguments['style'] = 'width: ' . intval($arguments['width']) . 'px;';
198
        }
199
200
        // override iframe dimension attributes provided by webservice with ones specified in shortcode arguments
201
        foreach (['width', 'height'] as $attr) {
202
            if (!($value = $arguments[$attr] ?? false)) {
203
                continue;
204
            }
205
            foreach (['"', "'"] as $quote) {
206
                $rx = "/(<iframe .*?)$attr=$quote([0-9]+)$quote([^>]+>)/";
207
                $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...
208
            }
209
        }
210
211
        $data = [
212
            'Arguments' => $arguments,
213
            '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

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

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

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