14c3a95e3SAndreas Gohr<?php 24c3a95e3SAndreas Gohr 34c3a95e3SAndreas Gohrnamespace dokuwiki\plugin\dw2pdf\src; 44c3a95e3SAndreas Gohr 54c3a95e3SAndreas Gohr/** 64c3a95e3SAndreas Gohr * Translates Dokuwiki-specific media URLs into local cached files. 74c3a95e3SAndreas Gohr * 84c3a95e3SAndreas Gohr * This consolidates the logic previously handled inside the custom ImageProcessor. 94c3a95e3SAndreas Gohr */ 104c3a95e3SAndreas Gohrclass MediaLinkResolver 114c3a95e3SAndreas Gohr{ 124c3a95e3SAndreas Gohr /** 134c3a95e3SAndreas Gohr * Resolve a Dokuwiki media URL or local path to a cached file path. 144c3a95e3SAndreas Gohr * 154c3a95e3SAndreas Gohr * The given file might be a Dokuwiki media reference (fetch.php call or /media/ URL) or an URL 164c3a95e3SAndreas Gohr * pointing to a static resource. Instead of performing an HTTP request, this method is used to 174c3a95e3SAndreas Gohr * resolve the file to a local cached copy whenever possible. It only handles images. 184c3a95e3SAndreas Gohr * 194c3a95e3SAndreas Gohr * When null is returned, the caller will fall back to a standard HTTP request. 204c3a95e3SAndreas Gohr * 214c3a95e3SAndreas Gohr * @param string $file Original media reference or URL. 224c3a95e3SAndreas Gohr * @return array|null ['path' => string, 'mime' => string] when resolution succeeds, null otherwise. 234c3a95e3SAndreas Gohr */ 244c3a95e3SAndreas Gohr public function resolve(string $file): ?array 254c3a95e3SAndreas Gohr { 264c3a95e3SAndreas Gohr $mediaID = $this->extractMediaID($file); 274c3a95e3SAndreas Gohr if ($mediaID !== null) { 284c3a95e3SAndreas Gohr [$w, $h, $rev] = $this->extractMediaParams($file); 294c3a95e3SAndreas Gohr [$ext, $mime] = mimetype($mediaID); 304c3a95e3SAndreas Gohr if (!$ext) return null; 314c3a95e3SAndreas Gohr $localFile = $this->localMediaFile($mediaID, $ext, $rev); 324c3a95e3SAndreas Gohr if (!$localFile) return null; 33*5340eaffSAndreas Gohr if (str_starts_with($mime, 'image/')) { 344c3a95e3SAndreas Gohr $localFile = $this->resizedMedia($localFile, $ext, $w, $h); 354c3a95e3SAndreas Gohr } 364c3a95e3SAndreas Gohr } else { 374c3a95e3SAndreas Gohr [, $mime] = mimetype($file); 38*5340eaffSAndreas Gohr if (!str_starts_with($mime, 'image/')) return null; 394c3a95e3SAndreas Gohr $localFile = $this->extractLocalImage($file); 404c3a95e3SAndreas Gohr } 414c3a95e3SAndreas Gohr 424c3a95e3SAndreas Gohr if (!$localFile) return null; 434c3a95e3SAndreas Gohr return ['path' => $localFile, 'mime' => $mime]; 444c3a95e3SAndreas Gohr } 454c3a95e3SAndreas Gohr 464c3a95e3SAndreas Gohr /** 474c3a95e3SAndreas Gohr * Check if the given file URL corresponds to a Dokuwiki media ID and extract it. 484c3a95e3SAndreas Gohr * 494c3a95e3SAndreas Gohr * Handles rewritten media URLs (/media/*) and fetch.php calls by building a regex 504c3a95e3SAndreas Gohr * from the result of calling ml() for a fake media ID. 514c3a95e3SAndreas Gohr * 524c3a95e3SAndreas Gohr * Note that the returned media ID could still be an external URL! 534c3a95e3SAndreas Gohr * 544c3a95e3SAndreas Gohr * @param string $file 554c3a95e3SAndreas Gohr * @return string|null The extracted media ID, or null if not found. 564c3a95e3SAndreas Gohr */ 574c3a95e3SAndreas Gohr protected function extractMediaID(string $file): ?string 584c3a95e3SAndreas Gohr { 594c3a95e3SAndreas Gohr // build regex to parse URL back to media info (matches fetch.php calls) 604c3a95e3SAndreas Gohr $fetchRegex = preg_quote(ml('xxx123yyy', '', true, '&', true), '/'); 614c3a95e3SAndreas Gohr $fetchRegex = str_replace('xxx123yyy', '([^&\?]*)', $fetchRegex); 624c3a95e3SAndreas Gohr 634c3a95e3SAndreas Gohr // extract the real media from a fetch.php URI and determine mime 644c3a95e3SAndreas Gohr if ( 654c3a95e3SAndreas Gohr preg_match("/^$fetchRegex/", $file, $matches) || 664c3a95e3SAndreas Gohr preg_match('/[&?]media=([^&?]*)/', $file, $matches) 674c3a95e3SAndreas Gohr ) { 684c3a95e3SAndreas Gohr return rawurldecode($matches[1]); 694c3a95e3SAndreas Gohr } 704c3a95e3SAndreas Gohr 714c3a95e3SAndreas Gohr return null; 724c3a95e3SAndreas Gohr } 734c3a95e3SAndreas Gohr 744c3a95e3SAndreas Gohr /** 754c3a95e3SAndreas Gohr * Extract media parameters (width, height, revision) from the given file URL. 764c3a95e3SAndreas Gohr * 774c3a95e3SAndreas Gohr * When a parameter is not found, its value will be 0. 784c3a95e3SAndreas Gohr * 794c3a95e3SAndreas Gohr * @param string $file Source string (fetch call) 804c3a95e3SAndreas Gohr * @return array{int,int,int} Array containing width, height, and revision. 814c3a95e3SAndreas Gohr */ 824c3a95e3SAndreas Gohr protected function extractMediaParams(string $file): array 834c3a95e3SAndreas Gohr { 844c3a95e3SAndreas Gohr $width = $this->extractInt($file, 'w'); 854c3a95e3SAndreas Gohr $height = $this->extractInt($file, 'h'); 864c3a95e3SAndreas Gohr $rev = $this->extractInt($file, 'rev'); 874c3a95e3SAndreas Gohr return [$width, $height, $rev]; 884c3a95e3SAndreas Gohr } 894c3a95e3SAndreas Gohr 904c3a95e3SAndreas Gohr /** 914c3a95e3SAndreas Gohr * Returns a local, absolute path for the given media ID and revision 924c3a95e3SAndreas Gohr * 934c3a95e3SAndreas Gohr * This method will download external media files to the local cache if needed. ACLs are 944c3a95e3SAndreas Gohr * checked here as well. 954c3a95e3SAndreas Gohr * 964c3a95e3SAndreas Gohr * Returns null when the media file is not accessible. 974c3a95e3SAndreas Gohr * 984c3a95e3SAndreas Gohr * @param string $mediaID A media ID or external URL. 994c3a95e3SAndreas Gohr * @param string $ext File extension (used for external media caching). 1004c3a95e3SAndreas Gohr * @param int $rev Revision number (0 for latest). 1014c3a95e3SAndreas Gohr * @return string|null Absolute path to the local media file, or null when not accessible. 1024c3a95e3SAndreas Gohr */ 1034c3a95e3SAndreas Gohr protected function localMediaFile(string $mediaID, string $ext, int $rev): ?string 1044c3a95e3SAndreas Gohr { 1054c3a95e3SAndreas Gohr global $conf; 1064c3a95e3SAndreas Gohr 1074c3a95e3SAndreas Gohr if (media_isexternal($mediaID)) { 1084c3a95e3SAndreas Gohr $local = media_get_from_URL($mediaID, $ext, $conf['cachetime']); 1094c3a95e3SAndreas Gohr if (!$local) return null; 1104c3a95e3SAndreas Gohr } else { 1114c3a95e3SAndreas Gohr $mediaID = cleanID($mediaID); 1124c3a95e3SAndreas Gohr // check permissions (namespace only) 1134c3a95e3SAndreas Gohr if (auth_quickaclcheck(getNS($mediaID) . ':X') < AUTH_READ) { 1144c3a95e3SAndreas Gohr return null; 1154c3a95e3SAndreas Gohr } 1164c3a95e3SAndreas Gohr $local = mediaFN($mediaID, $rev ?: ''); 1174c3a95e3SAndreas Gohr } 1184c3a95e3SAndreas Gohr if (!file_exists($local)) return null; 1194c3a95e3SAndreas Gohr 1204c3a95e3SAndreas Gohr return $local; 1214c3a95e3SAndreas Gohr } 1224c3a95e3SAndreas Gohr 1234c3a95e3SAndreas Gohr /** 1244c3a95e3SAndreas Gohr * Resize or crop the given media file as needed. 1254c3a95e3SAndreas Gohr * 1264c3a95e3SAndreas Gohr * @param string $mediaFile Absolute path to the local media file. 1274c3a95e3SAndreas Gohr * @param string $ext File extension. 1284c3a95e3SAndreas Gohr * @param int $width Desired width 1294c3a95e3SAndreas Gohr * @param int $height Desired height 1304c3a95e3SAndreas Gohr * @return string|null Absolute path to the resized/cropped media file, or null on failure. 1314c3a95e3SAndreas Gohr */ 1324c3a95e3SAndreas Gohr protected function resizedMedia($mediaFile, $ext, $width, $height) 1334c3a95e3SAndreas Gohr { 1344c3a95e3SAndreas Gohr if ($width && $height) { 1354c3a95e3SAndreas Gohr $mediaFile = media_crop_image($mediaFile, $ext, $width, $height); 1364c3a95e3SAndreas Gohr } elseif ($width || $height) { 1374c3a95e3SAndreas Gohr $mediaFile = media_resize_image($mediaFile, $ext, $width, $height); 1384c3a95e3SAndreas Gohr } 1394c3a95e3SAndreas Gohr if (!file_exists($mediaFile)) return null; 1404c3a95e3SAndreas Gohr return $mediaFile; 1414c3a95e3SAndreas Gohr } 1424c3a95e3SAndreas Gohr 1434c3a95e3SAndreas Gohr /** 1444c3a95e3SAndreas Gohr * Extract an integer parameter from the given subject URL. 1454c3a95e3SAndreas Gohr * 1464c3a95e3SAndreas Gohr * @param string $subject Source string, usually the media URL. 1474c3a95e3SAndreas Gohr * @param string $param Name of the parameter to extract. 1484c3a95e3SAndreas Gohr * @return int 1494c3a95e3SAndreas Gohr */ 1504c3a95e3SAndreas Gohr protected function extractInt(string $subject, string $param): int 1514c3a95e3SAndreas Gohr { 1524c3a95e3SAndreas Gohr $pattern = '/[?&]' . $param . '=(\d+)/'; 1534c3a95e3SAndreas Gohr if (preg_match($pattern, $subject, $match)) { 1544c3a95e3SAndreas Gohr return (int)$match[1]; 1554c3a95e3SAndreas Gohr } 1564c3a95e3SAndreas Gohr 1574c3a95e3SAndreas Gohr return 0; 1584c3a95e3SAndreas Gohr } 1594c3a95e3SAndreas Gohr 1604c3a95e3SAndreas Gohr /** 1614c3a95e3SAndreas Gohr * Attempt to extract a local file path from the given URL. 1624c3a95e3SAndreas Gohr * 1634c3a95e3SAndreas Gohr * This only works for static files that are directly accessible on disk. Or our 1644c3a95e3SAndreas Gohr * custom dw2pdf:// scheme for local files passed from plugins. 1654c3a95e3SAndreas Gohr * 1664c3a95e3SAndreas Gohr * @param string $file Source URL. 1674c3a95e3SAndreas Gohr * @return string|null Absolute path to the local file, or null when not accessible. 1684c3a95e3SAndreas Gohr */ 1694c3a95e3SAndreas Gohr protected function extractLocalImage($file) 1704c3a95e3SAndreas Gohr { 1714c3a95e3SAndreas Gohr $local = null; 172*5340eaffSAndreas Gohr if (str_starts_with($file, 'dw2pdf://')) { 1734c3a95e3SAndreas Gohr // support local files passed from plugins 1744c3a95e3SAndreas Gohr $local = substr($file, 9); 1754c3a95e3SAndreas Gohr } elseif (!preg_match('/(\.php|\?)/', $file)) { 1764c3a95e3SAndreas Gohr // directly access local files instead of using HTTP (skipping dynamic content) 1774c3a95e3SAndreas Gohr $base = preg_quote(DOKU_URL, '/'); 1784c3a95e3SAndreas Gohr $local = preg_replace("/^$base/i", DOKU_INC, $file, 1); 1794c3a95e3SAndreas Gohr } 1804c3a95e3SAndreas Gohr 1814c3a95e3SAndreas Gohr if (!file_exists($local)) return null; 1824c3a95e3SAndreas Gohr if (!is_readable($local)) return null; 1834c3a95e3SAndreas Gohr 1844c3a95e3SAndreas Gohr return $local; 1854c3a95e3SAndreas Gohr } 1864c3a95e3SAndreas Gohr} 187