Passed
Push — pulls/manymanylist-add-callbac... ( 7e0693...0d7c5a )
by Ingo
09:03
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 League\Flysystem\Exception;
7
use Psr\SimpleCache\CacheInterface;
8
use Psr\SimpleCache\InvalidArgumentException;
9
use SilverStripe\Core\Convert;
10
use SilverStripe\Core\Extensible;
11
use SilverStripe\Core\Injector\Injector;
12
use SilverStripe\ORM\ArrayList;
13
use SilverStripe\ORM\FieldType\DBField;
14
use SilverStripe\View\ArrayData;
15
use SilverStripe\View\Embed\Embeddable;
16
use SilverStripe\View\Embed\EmbedResource;
17
use SilverStripe\View\HTML;
18
use SilverStripe\View\Parsers\ShortcodeHandler;
19
use Embed\Adapters\Adapter;
20
use Embed\Exceptions\InvalidUrlException;
21
use SilverStripe\View\Parsers\ShortcodeParser;
22
use SilverStripe\Control\Director;
23
24
/**
25
 * Provider for the [embed] shortcode tag used by the embedding service
26
 * in the HTML Editor field.
27
 * Provides the html needed for the frontend and the editor field itself.
28
 */
29
class EmbedShortcodeProvider implements ShortcodeHandler
30
{
31
32
    /**
33
     * Gets the list of shortcodes provided by this handler
34
     *
35
     * @return mixed
36
     */
37
    public static function get_shortcodes()
38
    {
39
        return ['embed'];
40
    }
41
42
    /**
43
     * Embed shortcode parser from Oembed. This is a temporary workaround.
44
     * Oembed class has been replaced with the Embed external service.
45
     *
46
     * @param array $arguments
47
     * @param string $content
48
     * @param ShortcodeParser $parser
49
     * @param string $shortcode
50
     * @param array $extra
51
     *
52
     * @return string
53
     */
54
    public static function handle_shortcode($arguments, $content, $parser, $shortcode, $extra = [])
55
    {
56
        // Get service URL
57
        if (!empty($content)) {
58
            $serviceURL = $content;
59
        } elseif (!empty($arguments['url'])) {
60
            $serviceURL = $arguments['url'];
61
        } else {
62
            return '';
63
        }
64
65
        // Try to use cached result
66
        $cache = static::getCache();
67
        $key = static::deriveCacheKey($serviceURL);
68
        try {
69
            if ($cache->has($key)) {
70
                return $cache->get($key);
71
            }
72
        } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
73
        }
74
75
        // See https://github.com/oscarotero/Embed#example-with-all-options for service arguments
76
        $serviceArguments = [];
77
        if (!empty($arguments['width'])) {
78
            $serviceArguments['min_image_width'] = $arguments['width'];
79
        }
80
        if (!empty($arguments['height'])) {
81
            $serviceArguments['min_image_height'] = $arguments['height'];
82
        }
83
84
        /** @var EmbedResource $embed */
85
        $embed = Injector::inst()->create(Embeddable::class, $serviceURL);
86
        if (!empty($serviceArguments)) {
87
            $embed->setOptions(array_merge($serviceArguments, (array) $embed->getOptions()));
88
        }
89
90
        // Allow resolver to be mocked
91
        $dispatcher = null;
92
        if (isset($extra['resolver'])) {
93
            $dispatcher = Injector::inst()->create(
94
                $extra['resolver']['class'],
95
                $serviceURL,
96
                $extra['resolver']['config']
97
            );
98
        } elseif (Injector::inst()->has(DispatcherInterface::class)) {
99
            $dispatcher = Injector::inst()->get(DispatcherInterface::class);
100
        }
101
102
        if ($dispatcher) {
0 ignored issues
show
introduced by
$dispatcher is of type null, thus it always evaluated to false.
Loading history...
103
            $embed->setDispatcher($dispatcher);
104
        }
105
106
        // Process embed
107
        try {
108
            $embed = $embed->getEmbed();
109
        } catch (InvalidUrlException $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
        if ($embed && $embed instanceof Adapter) {
0 ignored issues
show
introduced by
$embed is always a sub-type of Embed\Adapters\Adapter.
Loading history...
128
            $result = static::embedForTemplate($embed, $arguments);
129
        }
130
        // Fallback to link to service
131
        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...
132
            $result = static::linkEmbed($arguments, $serviceURL, $serviceURL);
133
        }
134
        // Cache result
135
        if ($result) {
136
            try {
137
                $cache->set($key, $result);
138
            } catch (InvalidArgumentException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
139
            }
140
        }
141
        return $result;
142
    }
143
144
    /**
145
     * @param Adapter $embed
146
     * @param array $arguments Additional shortcode params
147
     * @return string
148
     */
149
    public static function embedForTemplate($embed, $arguments)
150
    {
151
        switch ($embed->getType()) {
152
            case 'video':
153
            case 'rich':
154
                // Attempt to inherit width (but leave height auto)
155
                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...
156
                    $arguments['width'] = $embed->getWidth();
157
                }
158
                return static::videoEmbed($arguments, $embed->getCode());
159
            case 'link':
160
                return static::linkEmbed($arguments, $embed->getUrl(), $embed->getTitle());
161
            case 'photo':
162
                return static::photoEmbed($arguments, $embed->getUrl());
163
            default:
164
                return null;
165
        }
166
    }
167
168
    /**
169
     * Build video embed tag
170
     *
171
     * @param array $arguments
172
     * @param string $content Raw HTML content
173
     * @return string
174
     */
175
    protected static function videoEmbed($arguments, $content)
176
    {
177
        // Ensure outer div has given width (but leave height auto)
178
        if (!empty($arguments['width'])) {
179
            $arguments['style'] = 'width: ' . intval($arguments['width']) . 'px;';
180
        }
181
182
        // override iframe dimension attributes provided by webservice with ones specified in shortcode arguments
183
        foreach (['width', 'height'] as $attr) {
184
            if (!($value = $arguments[$attr] ?? false)) {
185
                continue;
186
            }
187
            foreach (['"', "'"] as $quote) {
188
                $rx = "/(<iframe .*?)$attr=$quote([0-9]+)$quote([^>]+>)/";
189
                $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...
190
            }
191
        }
192
193
        $data = [
194
            'Arguments' => $arguments,
195
            '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

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

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