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