Passed
Push — fix-9163 ( 4cfde3...07a516 )
by Ingo
17:14
created

EmbedShortcodeProvider::flushCachedShortcodes()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 15
nc 7
nop 2
dl 0
loc 20
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\View\Shortcodes;
4
5
use Embed\Http\DispatcherInterface;
6
use Psr\SimpleCache\CacheInterface;
7
use Psr\SimpleCache\InvalidArgumentException;
8
use SilverStripe\Core\Convert;
9
use SilverStripe\Core\Extensible;
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\Embed\EmbedResource;
16
use SilverStripe\View\HTML;
17
use SilverStripe\View\Parsers\ShortcodeHandler;
18
use Embed\Adapters\Adapter;
19
use Embed\Exceptions\InvalidUrlException;
20
use SilverStripe\View\Parsers\ShortcodeParser;
21
use SilverStripe\Control\Director;
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
        // Try to use cached result
65
        $cache = static::getCache();
66
        $key = static::deriveCacheKey($serviceURL);
67
        try {
68
            if ($cache->has($key)) {
69
                return $cache->get($key);
70
            }
71
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
72
        }
73
74
        // See https://github.com/oscarotero/Embed#example-with-all-options for service arguments
75
        $serviceArguments = [];
76
        if (!empty($arguments['width'])) {
77
            $serviceArguments['min_image_width'] = $arguments['width'];
78
        }
79
        if (!empty($arguments['height'])) {
80
            $serviceArguments['min_image_height'] = $arguments['height'];
81
        }
82
83
        /** @var EmbedResource $embed */
84
        $embed = Injector::inst()->create(Embeddable::class, $serviceURL);
85
        if (!empty($serviceArguments)) {
86
            $embed->setOptions(array_merge($serviceArguments, (array) $embed->getOptions()));
87
        }
88
89
        // Allow resolver to be mocked
90
        $dispatcher = null;
91
        if (isset($extra['resolver'])) {
92
            $dispatcher = Injector::inst()->create(
93
                $extra['resolver']['class'],
94
                $serviceURL,
95
                $extra['resolver']['config']
96
            );
97
        } elseif (Injector::inst()->has(DispatcherInterface::class)) {
98
            $dispatcher = Injector::inst()->get(DispatcherInterface::class);
99
        }
100
101
        if ($dispatcher) {
102
            $embed->setDispatcher($dispatcher);
103
        }
104
105
        // Process embed
106
        try {
107
            $embed = $embed->getEmbed();
108
        } catch (InvalidUrlException $e) {
109
            $message = (Director::isDev())
110
                ? $e->getMessage()
111
                : _t(__CLASS__ . '.INVALID_URL', 'There was a problem loading the media.');
112
113
            $attr = [
114
                'class' => 'ss-media-exception embed'
115
            ];
116
117
            $result = HTML::createTag(
118
                'div',
119
                $attr,
120
                HTML::createTag('p', [], $message)
121
            );
122
            return $result;
123
        }
124
125
        // Convert embed object into HTML
126
        if ($embed && $embed instanceof Adapter) {
0 ignored issues
show
introduced by
$embed is always a sub-type of Embed\Adapters\Adapter.
Loading history...
127
            $result = static::embedForTemplate($embed, $arguments);
128
        }
129
        // Fallback to link to service
130
        if (!$result) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
131
            $result = static::linkEmbed($arguments, $serviceURL, $serviceURL);
132
        }
133
        // Cache result
134
        if ($result) {
135
            try {
136
                $cache->set($key, $result);
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 $result;
141
    }
142
143
    /**
144
     * @param Adapter $embed
145
     * @param array $arguments Additional shortcode params
146
     * @return string
147
     */
148
    public static function embedForTemplate($embed, $arguments)
149
    {
150
        switch ($embed->getType()) {
151
            case 'video':
152
            case 'rich':
153
                // Attempt to inherit width (but leave height auto)
154
                if (empty($arguments['width']) && $embed->getWidth()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $embed->getWidth() of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
155
                    $arguments['width'] = $embed->getWidth();
156
                }
157
                return static::videoEmbed($arguments, $embed->getCode());
158
            case 'link':
159
                return static::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
160
            case 'photo':
161
                return static::photoEmbed($arguments, $embed->getUrl());
162
            default:
163
                return null;
164
        }
165
    }
166
167
    /**
168
     * Build video embed tag
169
     *
170
     * @param array $arguments
171
     * @param string $content Raw HTML content
172
     * @return string
173
     */
174
    protected static function videoEmbed($arguments, $content)
175
    {
176
        // Ensure outer div has given width (but leave height auto)
177
        if (!empty($arguments['width'])) {
178
            $arguments['style'] = 'width: ' . intval($arguments['width']) . 'px;';
179
        }
180
181
        // override iframe dimension attributes provided by webservice with ones specified in shortcode arguments
182
        foreach (['width', 'height'] as $attr) {
183
            if (!($value = $arguments[$attr] ?? false)) {
184
                continue;
185
            }
186
            foreach (['"', "'"] as $quote) {
187
                $rx = "/(<iframe .*?)$attr=$quote([0-9]+)$quote([^>]+>)/";
188
                $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...
189
            }
190
        }
191
192
        $data = [
193
            'Arguments' => $arguments,
194
            '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

194
            '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...
195
            'Content' => DBField::create_field('HTMLFragment', $content)
196
        ];
197
198
        return ArrayData::create($data)->renderWith(self::class . '_video')->forTemplate();
199
    }
200
201
    /**
202
     * Build <a> embed tag
203
     *
204
     * @param array $arguments
205
     * @param string $href
206
     * @param string $title Default title
207
     * @return string
208
     */
209
    protected static function linkEmbed($arguments, $href, $title)
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
            'Href' => $href,
215
            'Title' => !empty($arguments['caption']) ? ($arguments['caption']) : $title
216
        ];
217
218
        return ArrayData::create($data)->renderWith(self::class . '_link')->forTemplate();
219
    }
220
221
    /**
222
     * Build img embed tag
223
     *
224
     * @param array $arguments
225
     * @param string $src
226
     * @return string
227
     */
228
    protected static function photoEmbed($arguments, $src)
229
    {
230
        $data = [
231
            'Arguments' => $arguments,
232
            '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

232
            '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...
233
            'Src' => $src
234
        ];
235
236
        return ArrayData::create($data)->renderWith(self::class . '_photo')->forTemplate();
237
    }
238
239
    /**
240
     * Build a list of HTML attributes from embed arguments - used to preserve backward compatibility
241
     *
242
     * @deprecated 4.5.0 Use {$Arguments.name} directly in shortcode templates to access argument values
243
     * @param array $arguments List of embed arguments
244
     * @param array $exclude List of attribute names to exclude from the resulting list
245
     * @return ArrayList
246
     */
247
    private static function buildAttributeListFromArguments(array $arguments, array $exclude = []): ArrayList
248
    {
249
        $attributes = ArrayList::create();
250
        foreach ($arguments as $key => $value) {
251
            if (in_array($key, $exclude)) {
252
                continue;
253
            }
254
255
            $attributes->push(ArrayData::create([
256
                'Name' => $key,
257
                'Value' => Convert::raw2att($value)
258
            ]));
259
        }
260
261
        return $attributes;
262
    }
263
264
    /**
265
     * @param ShortcodeParser $parser
266
     * @param string $content
267
     */
268
    public static function flushCachedShortcodes(ShortcodeParser $parser, string $content): void
269
    {
270
        $cache = static::getCache();
271
        $tags = $parser->extractTags($content);
272
        foreach ($tags as $tag) {
273
            if (!isset($tag['open']) || $tag['open'] != 'embed') {
274
                continue;
275
            }
276
            $url = $tag['content'] ?? $tag['attrs']['url'] ?? null;
277
            if (!$url) {
278
                continue;
279
            }
280
            $key = static::deriveCacheKey($url);
281
            try {
282
                if (!$cache->has($key)) {
283
                    continue;
284
                }
285
                $cache->delete($key);
286
            } catch (InvalidArgumentException $e) {
287
                continue;
288
            }
289
        }
290
    }
291
292
    /**
293
     * @return CacheInterface
294
     */
295
    private static function getCache(): CacheInterface
296
    {
297
        return Injector::inst()->get(CacheInterface::class . '.EmbedShortcodeProvider');
298
    }
299
300
    /**
301
     * @param string $url
302
     * @return string
303
     */
304
    private static function deriveCacheKey(string $url): string
305
    {
306
        $key = 'embed-shortcode-' . preg_replace('/[^a-zA-Z0-9\-]/', '', $url);
307
        return $key;
308
    }
309
}
310