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

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

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

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