1<?php 2 3namespace dokuwiki\plugin\dw2pdf\src; 4 5/** 6 * Translates Dokuwiki-specific media URLs into local cached files. 7 * 8 * This consolidates the logic previously handled inside the custom ImageProcessor. 9 */ 10class MediaLinkResolver 11{ 12 /** 13 * Resolve a Dokuwiki media URL or local path to a cached file path. 14 * 15 * The given file might be a Dokuwiki media reference (fetch.php call or /media/ URL) or an URL 16 * pointing to a static resource. Instead of performing an HTTP request, this method is used to 17 * resolve the file to a local cached copy whenever possible. It only handles images. 18 * 19 * When null is returned, the caller will fall back to a standard HTTP request. 20 * 21 * @param string $file Original media reference or URL. 22 * @return array|null ['path' => string, 'mime' => string] when resolution succeeds, null otherwise. 23 */ 24 public function resolve(string $file): ?array 25 { 26 $mediaID = $this->extractMediaID($file); 27 if ($mediaID !== null) { 28 [$w, $h, $rev] = $this->extractMediaParams($file); 29 [$ext, $mime] = mimetype($mediaID); 30 if (!$ext) return null; 31 $localFile = $this->localMediaFile($mediaID, $ext, $rev); 32 if (!$localFile) return null; 33 if (str_starts_with($mime, 'image/')) { 34 $localFile = $this->resizedMedia($localFile, $ext, $w, $h); 35 } 36 } else { 37 [, $mime] = mimetype($file); 38 if (!str_starts_with($mime, 'image/')) return null; 39 $localFile = $this->extractLocalImage($file); 40 } 41 42 if (!$localFile) return null; 43 return ['path' => $localFile, 'mime' => $mime]; 44 } 45 46 /** 47 * Check if the given file URL corresponds to a Dokuwiki media ID and extract it. 48 * 49 * Handles rewritten media URLs (/media/*) and fetch.php calls by building a regex 50 * from the result of calling ml() for a fake media ID. 51 * 52 * Note that the returned media ID could still be an external URL! 53 * 54 * @param string $file 55 * @return string|null The extracted media ID, or null if not found. 56 */ 57 protected function extractMediaID(string $file): ?string 58 { 59 // build regex to parse URL back to media info (matches fetch.php calls) 60 $fetchRegex = preg_quote(ml('xxx123yyy', '', true, '&', true), '/'); 61 $fetchRegex = str_replace('xxx123yyy', '([^&\?]*)', $fetchRegex); 62 63 // extract the real media from a fetch.php URI and determine mime 64 if ( 65 preg_match("/^$fetchRegex/", $file, $matches) || 66 preg_match('/[&?]media=([^&?]*)/', $file, $matches) 67 ) { 68 return rawurldecode($matches[1]); 69 } 70 71 return null; 72 } 73 74 /** 75 * Extract media parameters (width, height, revision) from the given file URL. 76 * 77 * When a parameter is not found, its value will be 0. 78 * 79 * @param string $file Source string (fetch call) 80 * @return array{int,int,int} Array containing width, height, and revision. 81 */ 82 protected function extractMediaParams(string $file): array 83 { 84 $width = $this->extractInt($file, 'w'); 85 $height = $this->extractInt($file, 'h'); 86 $rev = $this->extractInt($file, 'rev'); 87 return [$width, $height, $rev]; 88 } 89 90 /** 91 * Returns a local, absolute path for the given media ID and revision 92 * 93 * This method will download external media files to the local cache if needed. ACLs are 94 * checked here as well. 95 * 96 * Returns null when the media file is not accessible. 97 * 98 * @param string $mediaID A media ID or external URL. 99 * @param string $ext File extension (used for external media caching). 100 * @param int $rev Revision number (0 for latest). 101 * @return string|null Absolute path to the local media file, or null when not accessible. 102 */ 103 protected function localMediaFile(string $mediaID, string $ext, int $rev): ?string 104 { 105 global $conf; 106 107 if (media_isexternal($mediaID)) { 108 $local = media_get_from_URL($mediaID, $ext, $conf['cachetime']); 109 if (!$local) return null; 110 } else { 111 $mediaID = cleanID($mediaID); 112 // check permissions (namespace only) 113 if (auth_quickaclcheck(getNS($mediaID) . ':X') < AUTH_READ) { 114 return null; 115 } 116 $local = mediaFN($mediaID, $rev ?: ''); 117 } 118 if (!file_exists($local)) return null; 119 120 return $local; 121 } 122 123 /** 124 * Resize or crop the given media file as needed. 125 * 126 * @param string $mediaFile Absolute path to the local media file. 127 * @param string $ext File extension. 128 * @param int $width Desired width 129 * @param int $height Desired height 130 * @return string|null Absolute path to the resized/cropped media file, or null on failure. 131 */ 132 protected function resizedMedia($mediaFile, $ext, $width, $height) 133 { 134 if ($width && $height) { 135 $mediaFile = media_crop_image($mediaFile, $ext, $width, $height); 136 } elseif ($width || $height) { 137 $mediaFile = media_resize_image($mediaFile, $ext, $width, $height); 138 } 139 if (!file_exists($mediaFile)) return null; 140 return $mediaFile; 141 } 142 143 /** 144 * Extract an integer parameter from the given subject URL. 145 * 146 * @param string $subject Source string, usually the media URL. 147 * @param string $param Name of the parameter to extract. 148 * @return int 149 */ 150 protected function extractInt(string $subject, string $param): int 151 { 152 $pattern = '/[?&]' . $param . '=(\d+)/'; 153 if (preg_match($pattern, $subject, $match)) { 154 return (int)$match[1]; 155 } 156 157 return 0; 158 } 159 160 /** 161 * Attempt to extract a local file path from the given URL. 162 * 163 * This only works for static files that are directly accessible on disk. Or our 164 * custom dw2pdf:// scheme for local files passed from plugins. 165 * 166 * @param string $file Source URL. 167 * @return string|null Absolute path to the local file, or null when not accessible. 168 */ 169 protected function extractLocalImage($file) 170 { 171 $local = null; 172 if (str_starts_with($file, 'dw2pdf://')) { 173 // support local files passed from plugins 174 $local = substr($file, 9); 175 } elseif (!preg_match('/(\.php|\?)/', $file)) { 176 // directly access local files instead of using HTTP (skipping dynamic content) 177 $base = preg_quote(DOKU_URL, '/'); 178 $local = preg_replace("/^$base/i", DOKU_INC, $file, 1); 179 } 180 181 if (!file_exists($local)) return null; 182 if (!is_readable($local)) return null; 183 184 return $local; 185 } 186} 187