PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backups, Restore, Migration & Clone / 4.9.5
WP STAGING – WordPress Backups, Restore, Migration & Clone v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Framework / Filesystem / PathIdentifier.php
wp-staging / Framework / Filesystem Last commit date
Filters 1 week ago Scanning 5 years ago AbstractFileObject.php 1 year ago AbstractFilesystemScanner.php 1 week ago DebugLogReader.php 2 years ago DirectoryListing.php 6 months ago DirectorySize.php 3 weeks ago DiskWriteCheck.php 6 months ago FileObject.php 1 year ago Filesystem.php 8 months ago FilesystemExceptions.php 5 years ago FilesystemScanner.php 5 days ago FilesystemScannerDto.php 1 month ago FilterableDirectoryIterator.php 1 year ago LegacyFileRulesTrait.php 3 weeks ago LogCleanup.php 6 months ago LogFiles.php 1 year ago MissingFileException.php 3 years ago OPcache.php 6 months ago PartIdentifier.php 10 months ago PathChecker.php 2 years ago PathIdentifier.php 5 days ago Permissions.php 1 month ago WpUploadsFolderSymlinker.php 1 month ago
PathIdentifier.php
375 lines
1 <?php
2
3 namespace WPStaging\Framework\Filesystem;
4
5 use WPStaging\Framework\Adapter\Directory;
6 use WPStaging\Framework\Adapter\DirectoryInterface;
7
8 /**
9 * This class is used to shorten the full file path
10 * to reduce the overall file size of the backup file.
11 *
12 * A file like wp-content/uploads/wp-staging-pro/wp-staging-pro.zip turn into
13 * wpstg_p_/wp-staging-pro/wp-staging.zip
14 *
15 * @todo rename this class to PathShortener
16 */
17
18 class PathIdentifier
19 {
20 /** @var string */
21 const IDENTIFIER_ABSPATH = 'wpstg_a_';
22
23 /** @var string */
24 const IDENTIFIER_WP_CONTENT = 'wpstg_c_';
25
26 /** @var string */
27 const IDENTIFIER_PLUGINS = 'wpstg_p_';
28
29 /** @var string */
30 const IDENTIFIER_THEMES = 'wpstg_t_';
31
32 /** @var string */
33 const IDENTIFIER_MUPLUGINS = 'wpstg_m_';
34
35 /** @var string */
36 const IDENTIFIER_UPLOADS = 'wpstg_u_';
37
38 /** @var string */
39 const IDENTIFIER_LANG = 'wpstg_l_';
40
41 /**
42 * @var string|null The identifier of the last match.
43 * We will try to match the path/identifier of the next item starting from this one. It's a form of cache,
44 * making it more efficient to transform long lists of similar paths.
45 */
46 protected $lastIdentifier;
47
48 /** @var DirectoryInterface */
49 protected $directory;
50
51 public function __construct(DirectoryInterface $directory)
52 {
53 $this->directory = $directory;
54 }
55
56 /** @var string */
57 public function getBackupDirectory()
58 {
59 return $this->directory->getBackupDirectory();
60 }
61
62 /**
63 * Convert an absolute file path of a file into an abbreviated path.
64 *
65 * E.g.:
66 *
67 * /var/www/single/wp-content/plugins/index.php => wpstg_p_index.php
68 * /var/www/single/wp-content/mu-plugins/index.php => wpstg_m_index.php
69 * /var/www/single/wp-content/uploads/2019/image.png => wpstg_c_uploads/2019/image.png
70 * /var/www/single/wp-content/themes/twentytwentyone/index.php => wpstg_t_twentytwentyone/index.php
71 *
72 * @param string $path /var/www/single/wp-content/plugins/index.php
73 *
74 * @return string wpstg_p_index.php
75 */
76 public function transformPathToIdentifiable($path)
77 {
78 // Start looking from the same placeholder as the last item, unless it was wp-content, which would cause false-positives.
79 if (isset($this->lastIdentifier) && $this->lastIdentifier !== self::IDENTIFIER_WP_CONTENT) {
80 $basePath = $this->getIdentifierPath($this->lastIdentifier);
81
82 // Early bail: This item has the same type as the previous one.
83 if (strpos($path, $basePath) === 0) {
84 return $this->lastIdentifier . substr($path, strlen($basePath));
85 }
86 }
87
88 // Uploads are usually the largest folders, so let's start with them.
89 if (strpos($path, $this->directory->getUploadsDirectory()) === 0) {
90 $this->lastIdentifier = self::IDENTIFIER_UPLOADS;
91
92 return $this->lastIdentifier . substr($path, strlen($this->directory->getUploadsDirectory()));
93 }
94
95 if ($this->directory->getPluginUploadsDirectory() !== $this->directory->getUploadsDirectory()) {
96 if (strpos($path, $this->directory->getPluginUploadsDirectory()) === 0) {
97 $this->lastIdentifier = self::IDENTIFIER_UPLOADS;
98
99 return $this->lastIdentifier . substr($path, strlen($this->directory->getPluginUploadsDirectory()));
100 }
101 }
102
103 if (strpos($path, $this->directory->getPluginsDirectory()) === 0) {
104 $this->lastIdentifier = self::IDENTIFIER_PLUGINS;
105
106 return $this->lastIdentifier . substr($path, strlen($this->directory->getPluginsDirectory()));
107 }
108
109 foreach ($this->directory->getAllThemesDirectories() as $themesDirectory) {
110 if (strpos($path, $themesDirectory) === 0) {
111 $this->lastIdentifier = self::IDENTIFIER_THEMES;
112
113 return $this->lastIdentifier . substr($path, strlen($themesDirectory));
114 }
115 }
116
117 if (strpos($path, $this->directory->getMuPluginsDirectory()) === 0) {
118 $this->lastIdentifier = self::IDENTIFIER_MUPLUGINS;
119
120 return $this->lastIdentifier . substr($path, strlen($this->directory->getMuPluginsDirectory()));
121 }
122
123 if (strpos($path, $this->directory->getLangsDirectory()) === 0) {
124 $this->lastIdentifier = self::IDENTIFIER_LANG;
125
126 return $this->lastIdentifier . substr($path, strlen($this->directory->getLangsDirectory()));
127 }
128
129 if (strpos($path, $this->directory->getWpContentDirectory()) === 0) {
130 $this->lastIdentifier = self::IDENTIFIER_WP_CONTENT;
131
132 return $this->lastIdentifier . substr($path, strlen($this->directory->getWpContentDirectory()));
133 }
134
135 if (strpos($path, $this->directory->getAbspath()) === 0) {
136 $this->lastIdentifier = self::IDENTIFIER_ABSPATH;
137
138 return $this->lastIdentifier . substr($path, strlen($this->directory->getAbspath()));
139 }
140
141 // This should never happen on Backups, as we only scan the folders above explicitly and don't follow links.
142 throw new \RuntimeException(sprintf(
143 'Could not classify %s for backup: it is not inside any known WordPress content directory (plugins, themes, mu-plugins, uploads, languages, wp-content, or the WordPress root).',
144 $path === '' ? 'an empty path' : "the path \"$path\""
145 ));
146 }
147
148 /**
149 * @param string $path wpstg_p_index.php
150 *
151 * @return string /var/www/single/wp-content/plugins/index.php
152 */
153 public function transformIdentifiableToPath($path)
154 {
155 $identifier = $this->getIdentifierFromPath($path);
156 $pathWithoutIdentifier = $this->getPathWithoutIdentifier($path);
157
158 return $this->getIdentifierPath($identifier) . $pathWithoutIdentifier;
159 }
160
161 /**
162 * @param string $path wpstg_p_index.php
163 *
164 * @return string index.php
165 */
166 public function getPathWithoutIdentifier($path)
167 {
168 return substr($path, 8);
169 }
170
171 /**
172 * @param string $identifiablePath e.g. wpstg_u_2019/image.png
173 *
174 * @return bool True when the remainder is unsafe and the entry must be refused.
175 */
176 public function hasPathTraversal(string $identifiablePath): bool
177 {
178 $relativePath = $this->getPathWithoutIdentifier($identifiablePath);
179 if ($relativePath === '') {
180 return true;
181 }
182
183 if (strpos($relativePath, "\0") !== false) {
184 return true;
185 }
186
187 $normalizedPath = str_replace('\\', '/', $relativePath);
188 if ($normalizedPath[0] === '/' || preg_match('#^[a-zA-Z]:#', $normalizedPath) === 1) {
189 return true;
190 }
191
192 return in_array('..', explode('/', $normalizedPath), true);
193 }
194
195 public function isPathWithinRoot(string $targetPath, string $root): bool
196 {
197 $normalizedTarget = str_replace('\\', '/', $targetPath);
198 if (strpos($normalizedTarget, "\0") !== false || in_array('..', explode('/', $normalizedTarget), true)) {
199 return false;
200 }
201
202 $realRoot = realpath($root);
203 if ($realRoot === false) {
204 return false;
205 }
206
207 if (is_link($targetPath)) {
208 return false;
209 }
210
211 $deepestExisting = $targetPath;
212 while (!file_exists($deepestExisting)) {
213 if (is_link($deepestExisting)) {
214 return false;
215 }
216
217 $parent = dirname($deepestExisting);
218 if ($parent === $deepestExisting) {
219 return false;
220 }
221
222 $deepestExisting = $parent;
223 }
224
225 $realExisting = realpath($deepestExisting);
226 if ($realExisting === false) {
227 return false;
228 }
229
230 $realExisting = rtrim($realExisting, '/\\') . DIRECTORY_SEPARATOR;
231 $realRoot = rtrim($realRoot, '/\\') . DIRECTORY_SEPARATOR;
232
233 return strpos($realExisting, $realRoot) === 0;
234 }
235
236 /**
237 * @param string $path wpstg_p_index.php
238 *
239 * @return string wpstg_p_
240 */
241 public function getIdentifierFromPath($path)
242 {
243 return substr($path, 0, 8);
244 }
245
246 /**
247 * @return string
248 */
249 public function transformIdentifiableToRelativePath(string $string): string
250 {
251 $string = trim($string);
252 if (empty($string)) {
253 return $string;
254 }
255
256 $key = substr($string, 0, 8);
257 $path = $this->getRelativePath($key);
258 if ($path !== $key && is_string($path)) {
259 return substr_replace($string, $path, 0, 8);
260 }
261
262 return $string;
263 }
264
265 /**
266 * @return string
267 */
268 public function getRelativePath(string $identifier): string
269 {
270 static $cache = [];
271
272 if (!empty($cache) && !empty($identifier) && isset($cache[$identifier])) {
273 return $cache[$identifier];
274 }
275
276 $path = [
277 self::IDENTIFIER_ABSPATH => '',
278 self::IDENTIFIER_WP_CONTENT => 'wp-content/',
279 self::IDENTIFIER_PLUGINS => 'wp-content/plugins/',
280 self::IDENTIFIER_THEMES => 'wp-content/themes/',
281 self::IDENTIFIER_MUPLUGINS => 'wp-content/mu-plugins/',
282 self::IDENTIFIER_UPLOADS => 'wp-content/uploads/',
283 self::IDENTIFIER_LANG => 'wp-content/languages/',
284 ];
285
286 if (!empty($identifier) && isset($path[$identifier])) {
287 $cache[$identifier] = $path[$identifier];
288 return $cache[$identifier];
289 }
290
291 // Add __METHOD__ for debugging in wpstg-restore
292 trigger_error(sprintf('[%s] Could not find a path for the placeholder: %s', __METHOD__, filter_var($identifier, FILTER_SANITIZE_SPECIAL_CHARS)));
293 return $identifier;
294 }
295
296 public function getAbsolutePath(string $identifier): string
297 {
298 return $this->getIdentifierPath($identifier);
299 }
300
301 /**
302 * @return string
303 */
304 public function getIdentifierByPartName(string $key): string
305 {
306 static $cache = [];
307
308 if (!empty($cache) && !empty($key) && !empty($cache[$key])) {
309 return $cache[$key];
310 }
311
312 $list = [
313 PartIdentifier::WP_CONTENT_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_WP_CONTENT,
314 PartIdentifier::PLUGIN_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_PLUGINS,
315 PartIdentifier::THEME_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_THEMES,
316 PartIdentifier::MU_PLUGIN_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_MUPLUGINS,
317 PartIdentifier::UPLOAD_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_UPLOADS,
318 PartIdentifier::LANGUAGE_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_LANG,
319 PartIdentifier::DATABASE_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_UPLOADS,
320 PartIdentifier::WP_ROOT_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_ABSPATH,
321 ];
322
323 if (!empty($key) && !empty($list[$key])) {
324 $cache[$key] = $list[$key];
325 return $cache[$key];
326 }
327
328 return '';
329 }
330
331 /**
332 * @param string $identifier wpstg_p_
333 *
334 * @return string /var/www/single/wp-content/plugins/
335 */
336 protected function getIdentifierPath($identifier)
337 {
338 // It is crucial that generic paths are placed last in this list. Eg: wp-content directory must be last.
339 switch ($identifier) {
340 case self::IDENTIFIER_ABSPATH:
341 return $this->directory->getAbspath();
342 case self::IDENTIFIER_UPLOADS:
343 return $this->directory->getUploadsDirectory();
344 case self::IDENTIFIER_PLUGINS:
345 return $this->directory->getPluginsDirectory();
346 case self::IDENTIFIER_THEMES:
347 return $this->directory->getActiveThemeParentDirectory();
348 case self::IDENTIFIER_MUPLUGINS:
349 return $this->directory->getMuPluginsDirectory();
350 case self::IDENTIFIER_LANG:
351 return $this->directory->getLangsDirectory();
352 case self::IDENTIFIER_WP_CONTENT:
353 return $this->directory->getWpContentDirectory();
354 default:
355 throw new \UnexpectedValueException(sprintf("[%s] Could not find a path for the placeholder: %s", __METHOD__, filter_var($identifier, FILTER_SANITIZE_SPECIAL_CHARS)));
356 }
357 }
358
359 /**
360 * @param string $identifiablePath wpstg_p_db.php
361 *
362 * @return bool
363 */
364 public function hasDropinsFile(string $identifiablePath): bool
365 {
366 if (!(strpos($identifiablePath, self::IDENTIFIER_WP_CONTENT) === 0)) {
367 return false;
368 }
369
370 $dropinsFile = implode('|', PartIdentifier::DROP_IN_FILES);
371
372 return preg_match('@^' . self::IDENTIFIER_WP_CONTENT . '(' . $dropinsFile . ')@', $identifiablePath) ? true : false;
373 }
374 }
375