Passed
Pull Request — 4 (#10244)
by Steve
06:41
created

EmbedShortcodeProvider::embeddableToHtml()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 13
nc 6
nop 2
dl 0
loc 22
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View\Shortcodes;
4
5
use Exception;
6
use Embed\Embed;
7
use Embed\Extractor;
8
use Embed\Http\NetworkException;
9
use Embed\Http\RequestException;
10
use Psr\SimpleCache\CacheInterface;
11
use Psr\SimpleCache\InvalidArgumentException;
12
use SilverStripe\Core\Convert;
13
use SilverStripe\Core\Injector\Injector;
14
use SilverStripe\ORM\ArrayList;
15
use SilverStripe\ORM\FieldType\DBField;
16
use SilverStripe\View\ArrayData;
17
use SilverStripe\View\Embed\Embeddable;
18
use SilverStripe\View\HTML;
19
use SilverStripe\View\Parsers\ShortcodeHandler;
20
use SilverStripe\View\Parsers\ShortcodeParser;
21
use SilverStripe\Control\Director;
22
use SilverStripe\Dev\Deprecation;
23
use SilverStripe\View\Embed\EmbedContainer;
24
25
/**
26
 * Provider for the [embed] shortcode tag used by the embedding service
27
 * in the HTML Editor field.
28
 * Provides the html needed for the frontend and the editor field itself.
29
 */
30
class EmbedShortcodeProvider implements ShortcodeHandler
31
{
32
33
    /**
34
     * Gets the list of shortcodes provided by this handler
35
     *
36
     * @return mixed
37
     */
38
    public static function get_shortcodes()
39
    {
40
        return ['embed'];
41
    }
42
43
    /**
44
     * Embed shortcode parser from Oembed. This is a temporary workaround.
45
     * Oembed class has been replaced with the Embed external service.
46
     *
47
     * @param array $arguments
48
     * @param string $content
49
     * @param ShortcodeParser $parser
50
     * @param string $shortcode
51
     * @param array $extra
52
     *
53
     * @return string
54
     */
55
    public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = [])
56
    {
57
        // Get service URL
58
        if (!empty($content)) {
59
            $serviceURL = $content;
60
        } elseif (!empty($arguments['url'])) {
61
            $serviceURL = $arguments['url'];
62
        } else {
63
            return '';
64
        }
65
66
        $class = $arguments['class'] ?? '';
67
        $width = $arguments['width'] ?? '';
68
        $height = $arguments['height'] ?? '';
69
70
        // Try to use cached result
71
        $cache = static::getCache();
72
        $key = static::deriveCacheKey($serviceURL, $class, $width, $height);
73
        try {
74
            if ($cache->has($key)) {
75
                return $cache->get($key);
76
            }
77
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
78
        }
79
80
        // See https://github.com/oscarotero/Embed#example-with-all-options for service arguments
81
        $serviceArguments = [];
82
        if (!empty($arguments['width'])) {
83
            $serviceArguments['min_image_width'] = $arguments['width'];
84
        }
85
        if (!empty($arguments['height'])) {
86
            $serviceArguments['min_image_height'] = $arguments['height'];
87
        }
88
89
        /** @var Embeddable $embeddable */
90
        // Allow mocking of Embeddable
91
        if (($extra['Embeddable'] ?? null) instanceof Embeddable) {
92
            $embeddable = $extra['Embeddable'];
93
        } else {
94
            $embeddable = Injector::inst()->create(Embeddable::class, $serviceURL);
95
        }
96
        if (!empty($serviceArguments)) {
97
            $embeddable->setOptions(array_merge($serviceArguments, (array) $embeddable->getOptions()));
98
        }
99
100
        // Only EmbedContainer is currently supported
101
        if (!($embeddable instanceof EmbedContainer)) {
102
            throw new Exception('Emeddable must extend EmbedContainer');
103
        }
104
105
        // Process embed
106
        try {
107
            // this will trigger a request/response which will then be cached within $embeddable
108
            $embeddable->getExtractor();
109
        } catch (NetworkException | RequestException $e) {
110
            $message = (Director::isDev())
111
                ? $e->getMessage()
112
                : _t(__CLASS__ . '.INVALID_URL', 'There was a problem loading the media.');
113
114
            $attr = [
115
                'class' => 'ss-media-exception embed'
116
            ];
117
118
            $result = HTML::createTag(
119
                'div',
120
                $attr,
121
                HTML::createTag('p', [], $message)
122
            );
123
            return $result;
124
        }
125
126
        // Convert embed object into HTML
127
        $html = static::embeddableToHtml($embeddable, $arguments);
128
        // Fallback to link to service
129
        if (!$html) {
130
            $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...
131
        }
132
        // Cache result
133
        if ($html) {
134
            try {
135
                $cache->set($key, $html);
136
            } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
137
            }
138
        }
139
        return $html;
140
    }
141
142
    public static function embeddableToHtml(Embeddable $embeddable, array $arguments): string
143
    {
144
        // Only EmbedContainer is supported
145
        if (!($embeddable instanceof EmbedContainer)) {
146
            return '';
147
        }
148
        $extractor = $embeddable->getExtractor();
149
        $type = $embeddable->getType();
150
        if ($type === 'video' || $type === 'rich') {
151
            // Attempt to inherit width (but leave height auto)
152
            if (empty($arguments['width']) && $embeddable->getWidth()) {
153
                $arguments['width'] = $embeddable->getWidth();
154
            }
155
            return static::videoEmbed($arguments, $extractor->code->html);
156
        }
157
        if ($type === 'photo') {
158
            return static::photoEmbed($arguments, (string) $extractor->url);
159
        }
160
        if ($type === 'link') {
161
            return static::linkEmbed($arguments, (string) $extractor->url, $extractor->title);
162
        }
163
        return '';
164
    }
165
166
    /**
167
     * @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...
168
     * @param array $arguments Additional shortcode params
169
     * @return string
170
     * @deprecated 4.11..5.0 Use embeddableToHtml instead
171
     */
172
    public static function embedForTemplate($embed, $arguments)
173
    {
174
        Deprecation::notice('4.11', 'Use embeddableToHtml() instead');
175
        switch ($embed->getType()) {
176
            case 'video':
177
            case 'rich':
178
                // Attempt to inherit width (but leave height auto)
179
                if (empty($arguments['width']) && $embed->getWidth()) {
180
                    $arguments['width'] = $embed->getWidth();
181
                }
182
                return static::videoEmbed($arguments, $embed->getCode());
183
            case 'link':
184
                return static::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
185
            case 'photo':
186
                return static::photoEmbed($arguments, $embed->getUrl());
187
            default:
188
                return null;
189
        }
190
    }
191
192
    /**
193
     * Build video embed tag
194
     *
195
     * @param array $arguments
196
     * @param string $content Raw HTML content
197
     * @return string
198
     */
199
    protected static function videoEmbed($arguments, $content)
200
    {
201
        // Ensure outer div has given width (but leave height auto)
202
        if (!empty($arguments['width'])) {
203
            $arguments['style'] = 'width: ' . intval($arguments['width']) . 'px;';
204
        }
205
206
        // override iframe dimension attributes provided by webservice with ones specified in shortcode arguments
207
        foreach (['width', 'height'] as $attr) {
208
            if (!($value = $arguments[$attr] ?? false)) {
209
                continue;
210
            }
211
            foreach (['"', "'"] as $quote) {
212
                $rx = "/(<iframe .*?)$attr=$quote([0-9]+)$quote([^>]+>)/";
213
                $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...
214
            }
215
        }
216
217
        $data = [
218
            'Arguments' => $arguments,
219
            '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

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

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

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