&w= * * Images are returned as a data URI produced via DokuWiki's own * media_resize_image(), which consults the media cache and only does * work on the first request for a given (file, width) pair. * * Video is returned as a streamUrl pointing at lib/exe/fetch.php. * The browser then streams the file via HTTP range requests — the * same path DokuWiki uses for native {{file.mp4}} embedding — so no * server-side transcoding or base64-inlining happens for video. * * (c) Wizardry and Steamworks 2026 * SPDX-License-Identifier: MIT */ if (!defined('DOKU_INC')) die(); class action_plugin_geoplot extends DokuWiki_Action_Plugin { const CALL_NAME = 'plugin_geoplot_thumb'; const DEFAULT_WIDTH = 640; const MIN_WIDTH = 32; const MAX_WIDTH = 2000; private static $VIDEO_EXT = array( 'mp4', 'mov', 'm4v', 'webm', 'mkv', 'avi', 'ogg', 'ogv', ); public function register(Doku_Event_Handler $controller) { $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjax'); } public function handleAjax(Doku_Event $event, $param) { if ($event->data !== self::CALL_NAME) return; $event->stopPropagation(); $event->preventDefault(); while (ob_get_level() > 0) { ob_end_clean(); } header('Content-Type: application/json; charset=UTF-8'); header('Cache-Control: private, max-age=3600'); header('X-Content-Type-Options: nosniff'); $mediaId = isset($_GET['media']) ? $_GET['media'] : ''; $width = isset($_GET['w']) ? (int) $_GET['w'] : self::DEFAULT_WIDTH; if ($width < self::MIN_WIDTH) { $width = self::MIN_WIDTH; } if ($width > self::MAX_WIDTH) { $width = self::MAX_WIDTH; } if ($mediaId === '') { echo json_encode(array('error' => 'missing media parameter')); return; } $cleanId = cleanID($mediaId); $resolved = mediaFN($cleanId); if ($resolved === false || !is_file($resolved)) { echo json_encode(array('error' => 'media not found', 'id' => $cleanId)); return; } if (auth_quickaclcheck($cleanId) < AUTH_READ) { echo json_encode(array('error' => 'forbidden')); return; } $ext = strtolower(pathinfo($resolved, PATHINFO_EXTENSION)); // Video: hand the browser a streaming URL. DokuWiki's fetch.php // serves the file with range support, so the player can seek // and buffer progressively without loading the whole file into // PHP memory or into the JSON response. if (in_array($ext, self::$VIDEO_EXT)) { echo json_encode(array( 'streamUrl' => DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($cleanId), 'mediaType' => 'video', )); return; } // Image: use DokuWiki's media cache + resizer. $thumb = media_resize_image($resolved, $ext, $width); if ($thumb === false || !is_file($thumb)) { echo json_encode(array('error' => 'thumbnail generation failed')); return; } $bytes = @file_get_contents($thumb); if ($bytes === false) { echo json_encode(array('error' => 'could not read thumbnail')); return; } $mime = self::mimeFor(pathinfo($thumb, PATHINFO_EXTENSION)); echo json_encode(array( 'dataUri' => 'data:' . $mime . ';base64,' . base64_encode($bytes), 'width' => $width, 'mediaType' => 'image', )); } private static function mimeFor($ext) { $ext = strtolower($ext); switch ($ext) { case 'png': return 'image/png'; case 'jpg': case 'jpeg': return 'image/jpeg'; case 'gif': return 'image/gif'; case 'webp': return 'image/webp'; case 'tif': case 'tiff': return 'image/tiff'; case 'mp4': case 'm4v': return 'video/mp4'; case 'mov': return 'video/quicktime'; case 'webm': return 'video/webm'; case 'mkv': return 'video/x-matroska'; case 'avi': return 'video/x-msvideo'; case 'ogg': case 'ogv': return 'video/ogg'; } return 'application/octet-stream'; } }