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