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

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

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

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