xref: /plugin/dw2pdf/src/MediaLinkResolver.php (revision 5340eaffbcc1177667ac835e4e719d9eb8959315)
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