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