xref: /plugin/vshare/syntax/video.php (revision e7e00d3309649016073788c654878bc419207365)
1<?php
2
3use dokuwiki\Extension\SyntaxPlugin;
4
5/**
6 * Easily embed videos from various Video Sharing sites
7 *
8 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @author     Andreas Gohr <andi@splitbrain.org>
10 */
11class syntax_plugin_vshare_video extends SyntaxPlugin
12{
13    protected $sites;
14
15    protected $sizes = [
16        'small' => [255, 143],
17        'medium' => [425, 239],
18        'large' => [520, 293],
19        'full' => ['100%', ''],
20        'half' => ['50%', ''],
21    ];
22
23    protected $alignments = [
24        0 => 'none',
25        1 => 'right',
26        2 => 'left',
27        3 => 'center',
28    ];
29
30    /**
31     * Constructor.
32     * Intitalizes the supported video sites
33     */
34    public function __construct()
35    {
36        $this->sites = helper_plugin_vshare::loadSites();
37    }
38
39    /** @inheritdoc */
40    public function getType()
41    {
42        return 'substition';
43    }
44
45    /** @inheritdoc */
46    public function getPType()
47    {
48        return 'block';
49    }
50
51    /** @inheritdoc */
52    public function getSort()
53    {
54        return 159;
55    }
56
57    /** @inheritdoc */
58    public function connectTo($mode)
59    {
60        $pattern = implode('|', array_keys($this->sites));
61        $this->Lexer->addSpecialPattern('\{\{\s?(?:' . $pattern . ')>[^}]*\}\}', $mode, 'plugin_vshare_video');
62    }
63
64    /** @inheritdoc */
65    public function handle($match, $state, $pos, Doku_Handler $handler)
66    {
67        $command = substr($match, 2, -2);
68
69        // title
70        [$command, $title] = sexplode('|', $command, 2, '');
71        $title = trim($title);
72
73        // alignment
74        $align = 0;
75        if (substr($command, 0, 1) == ' ') ++$align;
76        if (substr($command, -1) == ' ') $align += 2;
77        $command = trim($command);
78
79        // get site and video
80        [$site, $vid] = explode('>', $command);
81        if (!$this->sites[$site]) return null; // unknown site
82        if (!$vid) return null; // no video!?
83
84        // what size?
85        [$vid, $pstr] = sexplode('?', $vid, 2, '');
86        parse_str($pstr, $userparams);
87        [$width, $height] = $this->parseSize($userparams);
88
89        // get URL
90        $url = $this->insertPlaceholders($this->sites[$site]['url'], $vid, $width, $height);
91        [$url, $urlpstr] = sexplode('?', $url, 2, '');
92        parse_str($urlpstr, $urlparams);
93
94        // merge parameters
95        $params = array_merge($urlparams, $userparams);
96        $url = $url . '?' . buildURLparams($params, '&');
97
98        return [
99            'site' => $site,
100            'domain' => parse_url($url, PHP_URL_HOST),
101            'video' => $vid,
102            'url' => $url,
103            'align' => $this->alignments[$align],
104            'width' => $width,
105            'height' => $height,
106            'title' => $title
107        ];
108    }
109
110    /** @inheritdoc */
111    public function render($mode, Doku_Renderer $R, $data)
112    {
113        if ($mode != 'xhtml') return false;
114        if (is_null($data)) return false;
115
116        if (is_a($R, 'renderer_plugin_dw2pdf')) {
117            $R->doc .= $this->pdf($data);
118        } else {
119            $R->doc .= $this->iframe($data, $this->getConf('gdpr') ? 'div' : 'iframe');
120        }
121        return true;
122    }
123
124    /**
125     * Prepare the HTML for output of the embed iframe
126     * @param array $data
127     * @param string $element Can be used to not directly embed the iframe
128     * @return string
129     */
130    public function iframe($data, $element = 'iframe')
131    {
132        $attributes = [
133            'src' => $data['url'],
134            'width' => $data['width'],
135            'height' => $data['height'],
136            'style' => $this->sizeToStyle($data['width'], $data['height']),
137            'class' => 'vshare vshare__' . $data['align'],
138            'allowfullscreen' => '',
139            'frameborder' => 0,
140            'scrolling' => 'no',
141            'data-domain' => $data['domain'],
142            'referrerpolicy' => 'no-referrer',
143            'loading' => 'lazy',
144        ];
145        if ($this->getConf('extrahard')) {
146            $attributes = array_merge($attributes, $this->hardenedIframeAttributes());
147        }
148
149        return "<$element "
150            . buildAttributes($attributes)
151            . '><h3>' . hsc($data['title']) . "</h3></$element>";
152    }
153
154    /**
155     * Create a style attribute for the given size
156     *
157     * @param int|string $width
158     * @param int|string $height
159     * @return string
160     */
161    public function sizeToStyle($width, $height)
162    {
163        // no unit? use px
164        if ($width && $width == (int)$width) {
165            $width .= 'px';
166        }
167        // no unit? use px
168        if ($height && $height == (int)$height) {
169            $height .= 'px';
170        }
171
172        $style = '';
173        if ($width) $style .= 'width:' . $width . ';';
174        if ($height) $style .= 'height:' . $height . ';';
175        return $style;
176    }
177
178    /**
179     * Prepare the HTML for output in PDF exports
180     *
181     * @param array $data
182     * @return string
183     */
184    public function pdf($data)
185    {
186        $html = '<div class="vshare vshare__' . $data['align'] . '"
187                      width="' . $data['width'] . '"
188                      height="' . $data['height'] . '">';
189
190        $html .= '<a href="' . $data['url'] . '" class="vshare">';
191        $html .= '<img src="' . DOKU_BASE . 'lib/plugins/vshare/video.png" />';
192        $html .= '</a>';
193
194        $html .= '<br />';
195
196        $html .= '<a href="' . $data['url'] . '" class="vshare">';
197        $html .= ($data['title'] ? hsc($data['title']) : 'Video');
198        $html .= '</a>';
199
200        $html .= '</div>';
201
202        return $html;
203    }
204
205    /**
206     * Fill the placeholders in the given URL
207     *
208     * @param string $url
209     * @param string $vid
210     * @param int|string $width
211     * @param int|string $height
212     * @return string
213     */
214    public function insertPlaceholders($url, $vid, $width, $height)
215    {
216        global $INPUT;
217        $url = str_replace('@VIDEO@', rawurlencode($vid), $url);
218        $url = str_replace('@DOMAIN@', rawurlencode($INPUT->server->str('HTTP_HOST')), $url);
219        $url = str_replace('@WIDTH@', $width, $url);
220        $url = str_replace('@HEIGHT@', $height, $url);
221
222        return $url;
223    }
224
225    /**
226     * Extract the wanted size from the parameter list
227     *
228     * @param array $params
229     * @return int[]
230     */
231    public function parseSize(&$params)
232    {
233        $known = implode('|', array_keys($this->sizes));
234
235        foreach (array_keys($params) as $key) {
236            if (preg_match("/^((\d+)x(\d+))|($known)\$/i", $key, $m)) {
237                unset($params[$key]);
238                if (isset($m[4])) {
239                    return $this->sizes[strtolower($m[4])];
240                } else {
241                    return [$m[2], $m[3]];
242                }
243            }
244        }
245
246        // default
247        return $this->sizes['medium'];
248    }
249
250    /**
251     * Get additional attributes to set on the iframe to harden
252     *
253     * @link https://dustri.org/b/youtube-video-embedding-harm-reduction.html
254     * @return array
255     */
256    protected function hardenedIframeAttributes()
257    {
258        $disallow = [
259            'accelerometer',
260            'ambient-light-sensor',
261            'autoplay',
262            'battery',
263            'browsing-topics',
264            'camera',
265            'display-capture',
266            'domain-agent',
267            'document-domain',
268            'encrypted-media',
269            'execution-while-not-rendered',
270            'execution-while-out-of-viewport',
271            'gamepad',
272            'geolocation',
273            'gyroscope',
274            'hid',
275            'identity-credentials-get',
276            'idle-detection',
277            'local-fonts',
278            'magnetometer',
279            'microphone',
280            'midi',
281            'otp-credentials',
282            'payment',
283            'picture-in-picture',
284            'publickey-credentials-create',
285            'publickey-credentials-get',
286            'screen-wake-lock',
287            'serial',
288            'speaker-selection',
289            'usb',
290            'window-management',
291            'xr-spatial-tracking',
292        ];
293
294        $disallow = implode('; ', array_map(static fn($v) => "$v 'none'", $disallow));
295
296        return [
297            'credentialless' => '',
298            'sandbox' => 'allow-scripts allow-same-origin',
299            'allow' => $disallow,
300            'csp' => 'sandbox allow-scripts allow-same-origin'
301        ];
302    }
303}
304