PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
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 7 months ago Scanning 5 years ago AbstractFileObject.php 1 year ago AbstractFilesystemScanner.php 2 months ago DebugLogReader.php 2 years ago DirectoryListing.php 5 months ago DiskWriteCheck.php 5 months ago FileObject.php 1 year ago Filesystem.php 7 months ago FilesystemExceptions.php 5 years ago FilesystemScanner.php 2 weeks ago FilesystemScannerDto.php 2 weeks ago FilterableDirectoryIterator.php 1 year ago LegacyFileRulesTrait.php 2 weeks ago LogCleanup.php 5 months ago LogFiles.php 1 year ago MissingFileException.php 3 years ago OPcache.php 5 months ago PartIdentifier.php 9 months ago PathChecker.php 2 years ago PathIdentifier.php 1 day ago Permissions.php 1 week ago WpUploadsFolderSymlinker.php 2 weeks ago
PathIdentifier.php
370 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 $realRoot = realpath($root);
198 if ($realRoot === false) {
199 return false;
200 }
201
202 if (is_link($targetPath)) {
203 return false;
204 }
205
206 $deepestExisting = $targetPath;
207 while (!file_exists($deepestExisting)) {
208 if (is_link($deepestExisting)) {
209 return false;
210 }
211
212 $parent = dirname($deepestExisting);
213 if ($parent === $deepestExisting) {
214 return false;
215 }
216
217 $deepestExisting = $parent;
218 }
219
220 $realExisting = realpath($deepestExisting);
221 if ($realExisting === false) {
222 return false;
223 }
224
225 $realExisting = rtrim($realExisting, '/\\') . DIRECTORY_SEPARATOR;
226 $realRoot = rtrim($realRoot, '/\\') . DIRECTORY_SEPARATOR;
227
228 return strpos($realExisting, $realRoot) === 0;
229 }
230
231 /**
232 * @param string $path wpstg_p_index.php
233 *
234 * @return string wpstg_p_
235 */
236 public function getIdentifierFromPath($path)
237 {
238 return substr($path, 0, 8);
239 }
240
241 /**
242 * @return string
243 */
244 public function transformIdentifiableToRelativePath(string $string): string
245 {
246 $string = trim($string);
247 if (empty($string)) {
248 return $string;
249 }
250
251 $key = substr($string, 0, 8);
252 $path = $this->getRelativePath($key);
253 if ($path !== $key && is_string($path)) {
254 return substr_replace($string, $path, 0, 8);
255 }
256
257 return $string;
258 }
259
260 /**
261 * @return string
262 */
263 public function getRelativePath(string $identifier): string
264 {
265 static $cache = [];
266
267 if (!empty($cache) && !empty($identifier) && isset($cache[$identifier])) {
268 return $cache[$identifier];
269 }
270
271 $path = [
272 self::IDENTIFIER_ABSPATH => '',
273 self::IDENTIFIER_WP_CONTENT => 'wp-content/',
274 self::IDENTIFIER_PLUGINS => 'wp-content/plugins/',
275 self::IDENTIFIER_THEMES => 'wp-content/themes/',
276 self::IDENTIFIER_MUPLUGINS => 'wp-content/mu-plugins/',
277 self::IDENTIFIER_UPLOADS => 'wp-content/uploads/',
278 self::IDENTIFIER_LANG => 'wp-content/languages/',
279 ];
280
281 if (!empty($identifier) && isset($path[$identifier])) {
282 $cache[$identifier] = $path[$identifier];
283 return $cache[$identifier];
284 }
285
286 // Add __METHOD__ for debugging in wpstg-restore
287 trigger_error(sprintf('[%s] Could not find a path for the placeholder: %s', __METHOD__, filter_var($identifier, FILTER_SANITIZE_SPECIAL_CHARS)));
288 return $identifier;
289 }
290
291 public function getAbsolutePath(string $identifier): string
292 {
293 return $this->getIdentifierPath($identifier);
294 }
295
296 /**
297 * @return string
298 */
299 public function getIdentifierByPartName(string $key): string
300 {
301 static $cache = [];
302
303 if (!empty($cache) && !empty($key) && !empty($cache[$key])) {
304 return $cache[$key];
305 }
306
307 $list = [
308 PartIdentifier::WP_CONTENT_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_WP_CONTENT,
309 PartIdentifier::PLUGIN_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_PLUGINS,
310 PartIdentifier::THEME_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_THEMES,
311 PartIdentifier::MU_PLUGIN_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_MUPLUGINS,
312 PartIdentifier::UPLOAD_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_UPLOADS,
313 PartIdentifier::LANGUAGE_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_LANG,
314 PartIdentifier::DATABASE_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_UPLOADS,
315 PartIdentifier::WP_ROOT_PART_IDENTIFIER => PathIdentifier::IDENTIFIER_ABSPATH,
316 ];
317
318 if (!empty($key) && !empty($list[$key])) {
319 $cache[$key] = $list[$key];
320 return $cache[$key];
321 }
322
323 return '';
324 }
325
326 /**
327 * @param string $identifier wpstg_p_
328 *
329 * @return string /var/www/single/wp-content/plugins/
330 */
331 protected function getIdentifierPath($identifier)
332 {
333 // It is crucial that generic paths are placed last in this list. Eg: wp-content directory must be last.
334 switch ($identifier) {
335 case self::IDENTIFIER_ABSPATH:
336 return $this->directory->getAbspath();
337 case self::IDENTIFIER_UPLOADS:
338 return $this->directory->getUploadsDirectory();
339 case self::IDENTIFIER_PLUGINS:
340 return $this->directory->getPluginsDirectory();
341 case self::IDENTIFIER_THEMES:
342 return $this->directory->getActiveThemeParentDirectory();
343 case self::IDENTIFIER_MUPLUGINS:
344 return $this->directory->getMuPluginsDirectory();
345 case self::IDENTIFIER_LANG:
346 return $this->directory->getLangsDirectory();
347 case self::IDENTIFIER_WP_CONTENT:
348 return $this->directory->getWpContentDirectory();
349 default:
350 throw new \UnexpectedValueException(sprintf("[%s] Could not find a path for the placeholder: %s", __METHOD__, filter_var($identifier, FILTER_SANITIZE_SPECIAL_CHARS)));
351 }
352 }
353
354 /**
355 * @param string $identifiablePath wpstg_p_db.php
356 *
357 * @return bool
358 */
359 public function hasDropinsFile(string $identifiablePath): bool
360 {
361 if (!(strpos($identifiablePath, self::IDENTIFIER_WP_CONTENT) === 0)) {
362 return false;
363 }
364
365 $dropinsFile = implode('|', PartIdentifier::DROP_IN_FILES);
366
367 return preg_match('@^' . self::IDENTIFIER_WP_CONTENT . '(' . $dropinsFile . ')@', $identifiablePath) ? true : false;
368 }
369 }
370