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

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

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

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