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 'loading' => 'lazy', 143 ]; 144 if ($this->getConf('extrahard')) { 145 $attributes = array_merge($attributes, $this->hardenedIframeAttributes()); 146 } 147 148 return "<$element " 149 . buildAttributes($attributes) 150 . '><h3>' . hsc($data['title']) . "</h3></$element>"; 151 } 152 153 /** 154 * Create a style attribute for the given size 155 * 156 * @param int|string $width 157 * @param int|string $height 158 * @return string 159 */ 160 public function sizeToStyle($width, $height) 161 { 162 // no unit? use px 163 if ($width && $width == (int)$width) { 164 $width .= 'px'; 165 } 166 // no unit? use px 167 if ($height && $height == (int)$height) { 168 $height .= 'px'; 169 } 170 171 $style = ''; 172 if ($width) $style .= 'width:' . $width . ';'; 173 if ($height) $style .= 'height:' . $height . ';'; 174 return $style; 175 } 176 177 /** 178 * Prepare the HTML for output in PDF exports 179 * 180 * @param array $data 181 * @return string 182 */ 183 public function pdf($data) 184 { 185 $html = '<div class="vshare vshare__' . $data['align'] . '" 186 width="' . $data['width'] . '" 187 height="' . $data['height'] . '">'; 188 189 $html .= '<a href="' . $data['url'] . '" class="vshare">'; 190 $html .= '<img src="' . DOKU_BASE . 'lib/plugins/vshare/video.png" />'; 191 $html .= '</a>'; 192 193 $html .= '<br />'; 194 195 $html .= '<a href="' . $data['url'] . '" class="vshare">'; 196 $html .= ($data['title'] ? hsc($data['title']) : 'Video'); 197 $html .= '</a>'; 198 199 $html .= '</div>'; 200 201 return $html; 202 } 203 204 /** 205 * Fill the placeholders in the given URL 206 * 207 * @param string $url 208 * @param string $vid 209 * @param int|string $width 210 * @param int|string $height 211 * @return string 212 */ 213 public function insertPlaceholders($url, $vid, $width, $height) 214 { 215 global $INPUT; 216 $url = str_replace('@VIDEO@', rawurlencode($vid), $url); 217 $url = str_replace('@DOMAIN@', rawurlencode($INPUT->server->str('HTTP_HOST')), $url); 218 $url = str_replace('@WIDTH@', $width, $url); 219 $url = str_replace('@HEIGHT@', $height, $url); 220 221 return $url; 222 } 223 224 /** 225 * Extract the wanted size from the parameter list 226 * 227 * @param array $params 228 * @return int[] 229 */ 230 public function parseSize(&$params) 231 { 232 $known = implode('|', array_keys($this->sizes)); 233 234 foreach (array_keys($params) as $key) { 235 if (preg_match("/^((\d+)x(\d+))|($known)\$/i", $key, $m)) { 236 unset($params[$key]); 237 if (isset($m[4])) { 238 return $this->sizes[strtolower($m[4])]; 239 } else { 240 return [$m[2], $m[3]]; 241 } 242 } 243 } 244 245 // default 246 return $this->sizes['medium']; 247 } 248 249 /** 250 * Get additional attributes to set on the iframe to harden 251 * 252 * @link https://dustri.org/b/youtube-video-embedding-harm-reduction.html 253 * @return array 254 */ 255 protected function hardenedIframeAttributes() 256 { 257 $disallow = [ 258 'accelerometer', 259 'ambient-light-sensor', 260 'autoplay', 261 'battery', 262 'browsing-topics', 263 'camera', 264 'display-capture', 265 'domain-agent', 266 'document-domain', 267 'encrypted-media', 268 'execution-while-not-rendered', 269 'execution-while-out-of-viewport', 270 'gamepad', 271 'geolocation', 272 'gyroscope', 273 'hid', 274 'identity-credentials-get', 275 'idle-detection', 276 'local-fonts', 277 'magnetometer', 278 'microphone', 279 'midi', 280 'otp-credentials', 281 'payment', 282 'picture-in-picture', 283 'publickey-credentials-create', 284 'publickey-credentials-get', 285 'screen-wake-lock', 286 'serial', 287 'speaker-selection', 288 'usb', 289 'window-management', 290 'xr-spatial-tracking', 291 ]; 292 293 $disallow = implode('; ', array_map(static fn($v) => "$v 'none'", $disallow)); 294 295 return [ 296 'credentialless' => '', 297 'sandbox' => 'allow-scripts allow-same-origin', 298 'allow' => $disallow, 299 'csp' => 'sandbox allow-scripts allow-same-origin', 300 'referrerpolicy' => 'no-referrer', 301 ]; 302 } 303} 304