[FEATURE] Introduce a stream wrapper to overlay file paths
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Tests / FileStreamWrapper.php
1 <?php
2 namespace TYPO3\CMS\Core\Tests;
3
4 /**
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Stream wrapper for the file:// protocol
19 *
20 * Implementation details:
21 * Due to the nature of PHP, it is not possible to switch to the default handler
22 * other then restoring the default handler for file:// and registering it again
23 * around each call.
24 * It is important that the default handler is restored to allow autoloading (including)
25 * of files during the test run.
26 * For each method allowed to pass paths, the passed path is checked against the
27 * the list of paths to overlay and rewritten if needed.
28 *
29 * = Usage =
30 * <code title="Add use statements">
31 * use org\bovigo\vfs\vfsStream;
32 * use org\bovigo\vfs\visitor\vfsStreamStructureVisitor;
33 * </code>
34 *
35 * <code title="Usage in test">
36 * $root = \org\bovigo\vfs\vfsStream::setup('root');
37 * $subfolder = \org\bovigo\vfs\vfsStream::newDirectory('fileadmin');
38 * $root->addChild($subfolder);
39 * // Load fixture files and folders from disk
40 * \org\bovigo\vfs\vfsStream::copyFromFileSystem(__DIR__ . '/Fixture/Files', $subfolder, 1024*1024);
41 * FileStreamWrapper::init(PATH_site);
42 * FileStreamWrapper::registerOverlayPath('fileadmin', 'vfs://root/fileadmin');
43 *
44 * // Use file functions as usual
45 * mkdir(PATH_site . 'fileadmin/test/');
46 * $file = PATH_site . 'fileadmin/test/Foo.bar';
47 * file_put_contents($file, 'Baz');
48 * $content = file_get_contents($file);
49 * $this->assertSame('Baz', $content);
50 *
51 * $this->assertEqual(**array(file system structure as array**), vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure());
52 *
53 * FileStreamWrapper::destroy();
54 * </code>
55 *
56 * @see http://www.php.net/manual/en/class.streamwrapper.php
57 */
58 class FileStreamWrapper {
59
60 /**
61 * @var resource
62 */
63 protected $dirHandle = NULL;
64
65 /**
66 * @var resource
67 */
68 protected $fileHandle = NULL;
69
70 /**
71 * Switch whether class has already been registered as stream wrapper or not
72 *
73 * @type bool
74 */
75 protected static $registered = FALSE;
76
77 /**
78 * Array of paths to overlay
79 *
80 * @var array
81 */
82 protected static $overlayPaths = array();
83
84 /**
85 * The first part of each (absolute) path that shall be ignored
86 *
87 * @var string
88 */
89 protected static $rootPath = '';
90
91 /**
92 * Initialize the stream wrapper with a root path and register itself
93 *
94 * @param $rootPath
95 * @return void
96 */
97 public static function init($rootPath) {
98 self::$rootPath = rtrim(str_replace('\\', '/', $rootPath), '/') . '/';
99 self::register();
100 }
101
102 /**
103 * Unregister the stream wrapper and reset all static members to their default values
104 * @return void
105 */
106 public static function destroy() {
107 self::$overlayPaths = array();
108 self::$rootPath = '';
109 if (self::$registered) {
110 self::restore();
111 }
112 }
113
114 /**
115 * Register a path relative to the root path (set in init) to be overlaid
116 *
117 * @param string $overlay Relative path to the root folder
118 * @param string $replace The path that should replace the overlay path
119 * @param bool $createFolder TRUE of the folder should be created (mkdir)
120 * @return void
121 */
122 public static function registerOverlayPath($overlay, $replace, $createFolder = TRUE) {
123 $overlay = trim(str_replace('\\', '/', $overlay), '/') . '/';
124 $replace = rtrim(str_replace('\\', '/', $replace), '/') . '/';
125 self::$overlayPaths[$overlay] = $replace;
126 if ($createFolder) {
127 mkdir($replace);
128 }
129 }
130
131 /**
132 * Checks and overlays a path
133 *
134 * @param string $path The path to check
135 * @return string The potentially overlaid path
136 */
137 protected static function overlayPath($path) {
138 $path = str_replace('\\', '/', $path);
139 $hasOverlay = FALSE;
140 if (strpos($path, self::$rootPath) !== 0) {
141 // Path is not below root path, ignore it
142 return $path;
143 }
144
145 $newPath = substr($path, strlen(self::$rootPath));
146 foreach (self::$overlayPaths as $overlay => $replace) {
147 if (strpos($newPath, $overlay) === 0) {
148 $newPath = $replace . substr($newPath, strlen($overlay));
149 $hasOverlay = TRUE;
150 break;
151 }
152 }
153 return $hasOverlay ? $newPath : $path;
154 }
155
156 /**
157 * Method to register the stream wrapper
158 *
159 * If the stream is already registered the method returns silently. If there
160 * is already another stream wrapper registered for the scheme used by
161 * file:// scheme a \BadFunctionCallException will be thrown.
162 *
163 * @throws \BadFunctionCallException
164 * @return void
165 */
166 protected static function register() {
167 if (self::$registered) {
168 return;
169 }
170
171 if (@stream_wrapper_unregister('file') === FALSE) {
172 throw new \BadFunctionCallException('Cannot unregister file:// stream wrapper.', 1396340331);
173 }
174 if (@stream_wrapper_register('file', __CLASS__) === FALSE) {
175 throw new \BadFunctionCallException('A handler has already been registered for the file:// scheme.', 1396340332);
176 }
177
178 self::$registered = TRUE;
179 }
180
181 /**
182 * Restore the file handler
183 *
184 * @return void
185 */
186 protected static function restore() {
187 if (!self::$registered) {
188 return;
189 }
190 if (@stream_wrapper_restore('file') === FALSE) {
191 throw new \BadFunctionCallException('Cannot restore the default file:// stream handler.', 1396340333);
192 }
193 self::$registered = FALSE;
194 }
195
196
197 /*
198 * The following list of functions is implemented as of
199 * @see http://www.php.net/manual/en/streamwrapper.dir-closedir.php
200 */
201
202 /**
203 * Close the directory
204 *
205 * @return bool
206 */
207 public function dir_closedir() {
208 if ($this->dirHandle === NULL) {
209 return FALSE;
210 } else {
211 self::restore();
212 closedir($this->dirHandle);
213 self::register();
214 $this->dirHandle = NULL;
215 return TRUE;
216 }
217 }
218
219 /**
220 * Opens a directory for reading
221 *
222 * @param string $path
223 * @param int $options
224 * @return bool
225 */
226 public function dir_opendir($path, $options = 0) {
227 if ($this->dirHandle !== NULL) {
228 return FALSE;
229 }
230 self::restore();
231 $path = self::overlayPath($path);
232 $this->dirHandle = opendir($path);
233 self::register();
234 return $this->dirHandle !== FALSE;
235 }
236
237 /**
238 * Read a single filename of a directory
239 *
240 * @return string|bool
241 */
242 public function dir_readdir() {
243 if ($this->dirHandle === NULL) {
244 return FALSE;
245 }
246 self::restore();
247 $success = readdir($this->dirHandle);
248 self::register();
249 return $success;
250 }
251
252 /**
253 * Reset directory name pointer
254 *
255 * @return bool
256 */
257 public function dir_rewinddir() {
258 if ($this->dirHandle === NULL) {
259 return FALSE;
260 }
261 self::restore();
262 rewinddir($this->dirHandle);
263 self::register();
264 return TRUE;
265 }
266
267 /**
268 * Create a directory
269 *
270 * @param string $path
271 * @param int $mode
272 * @param int $options
273 * @return bool
274 */
275 public function mkdir($path, $mode, $options = 0) {
276 self::restore();
277 $path = self::overlayPath($path);
278 $success = mkdir($path, $mode, (bool)($options & STREAM_MKDIR_RECURSIVE));
279 self::register();
280 return $success;
281 }
282
283 /**
284 * Rename a file
285 *
286 * @param string $pathFrom
287 * @param string $pathTo
288 * @return bool
289 */
290 public function rename($pathFrom, $pathTo) {
291 self::restore();
292 $pathFrom = self::overlayPath($pathFrom);
293 $pathTo = self::overlayPath($pathTo);
294 $success = rename($pathFrom, $pathTo);
295 self::register();
296 return $success;
297 }
298
299 /**
300 * Remove a directory
301 *
302 * @param string $path
303 * @return bool
304 */
305 public function rmdir($path) {
306 self::restore();
307 $path = self::overlayPath($path);
308 $success = rmdir($path);
309 self::register();
310 return $success;
311 }
312
313 /**
314 * Close a file
315 *
316 */
317 public function stream_close() {
318 self::restore();
319 if ($this->fileHandle !== NULL) {
320 fclose($this->fileHandle);
321 $this->fileHandle = NULL;
322 }
323 self::register();
324 }
325
326 /**
327 * Test for end-of-file on a file pointer
328 *
329 * @return bool
330 */
331 public function stream_eof() {
332 if ($this->fileHandle === NULL) {
333 return FALSE;
334 }
335 self::restore();
336 $success = feof($this->fileHandle);
337 self::register();
338 return $success;
339 }
340
341 /**
342 * Flush the output
343 *
344 * @return bool
345 */
346 public function stream_flush() {
347 if ($this->fileHandle === NULL) {
348 return FALSE;
349 }
350 self::restore();
351 $success = fflush($this->fileHandle);
352 self::register();
353 return $success;
354 }
355
356 /**
357 * Advisory file locking
358 *
359 * @param int $operation
360 * @return bool
361 */
362 public function stream_lock($operation) {
363 if ($this->fileHandle === NULL) {
364 return FALSE;
365 }
366 self::restore();
367 $success = flock($this->fileHandle, $operation);
368 self::register();
369 return $success;
370 }
371
372 /**
373 * Change file options
374 *
375 * @param string $path
376 * @param int $options
377 * @param mixed $value
378 * @return bool
379 */
380 public function stream_metadata($path, $options, $value) {
381 self::restore();
382 $path = self::overlayPath($path);
383 switch ($options) {
384 case STREAM_META_TOUCH:
385 $success = touch($path, $value[0], $value[1]);
386 break;
387 case STREAM_META_OWNER_NAME:
388 // Fall through
389 case STREAM_META_OWNER:
390 $success = chown($path, $value);
391 break;
392 case STREAM_META_GROUP_NAME:
393 // Fall through
394 case STREAM_META_GROUP:
395 $success = chgrp($path, $value);
396 break;
397 case STREAM_META_ACCESS:
398 $success = chmod($path, $value);
399 break;
400 default:
401 $success = FALSE;
402 }
403 self::register();
404 return $success;
405 }
406
407 /**
408 * Open a file
409 *
410 * @param string $path
411 * @param string $mode
412 * @param int $options
413 * @param string &$opened_path
414 * @return bool
415 */
416 public function stream_open($path, $mode, $options, &$opened_path) {
417 if ($this->fileHandle !== NULL) {
418 return FALSE;
419 }
420 self::restore();
421 $path = self::overlayPath($path);
422 $this->fileHandle = fopen($path, $mode, (bool)($options & STREAM_USE_PATH));
423 self::register();
424 return $this->fileHandle !== FALSE;
425 }
426
427 /**
428 * Read from a file
429 *
430 * @param int $length
431 * @return string
432 */
433 public function stream_read($length) {
434 if ($this->fileHandle === NULL) {
435 return FALSE;
436 }
437 self::restore();
438 $content = fread($this->fileHandle, $length);
439 self::register();
440 return $content;
441 }
442
443 /**
444 * Seek to specific location in a stream
445 *
446 * @param int $offset
447 * @param int $whence = SEEK_SET
448 * @return bool
449 */
450 public function stream_seek($offset, $whence = SEEK_SET) {
451 if ($this->fileHandle === NULL) {
452 return FALSE;
453 }
454 self::restore();
455 $success = fseek($this->fileHandle, $offset, $whence);
456 self::register();
457 return $success;
458 }
459
460 /**
461 * Change stream options (not implemented)
462 *
463 * @param int $option
464 * @param int $arg1
465 * @param int $arg2
466 * @return bool
467 */
468 public function stream_set_option($option, $arg1, $arg2) {
469 return FALSE;
470 }
471
472 /**
473 * Retrieve information about a file resource
474 *
475 * @return array
476 */
477 public function stream_stat() {
478 if ($this->fileHandle === NULL) {
479 return FALSE;
480 }
481 self::restore();
482 $stats = fstat($this->fileHandle);
483 self::register();
484 return $stats;
485 }
486
487 /**
488 * Retrieve the current position of a stream
489 *
490 * @return int
491 */
492 public function stream_tell() {
493 if ($this->fileHandle === NULL) {
494 return FALSE;
495 }
496 self::restore();
497 $position = ftell($this->fileHandle);
498 self::register();
499 return $position;
500 }
501
502 /**
503 * Truncates a file to the given size
504 *
505 * @param int $size Truncate to this size
506 * @return bool
507 */
508 public function stream_truncate($size) {
509 if ($this->fileHandle === NULL) {
510 return FALSE;
511 }
512 self::restore();
513 $success = ftruncate($this->fileHandle, $size);
514 self::register();
515 return $success;
516 }
517
518 /**
519 * Write to stream
520 *
521 * @param string $data
522 * @return int
523 */
524 public function stream_write($data) {
525 if ($this->fileHandle === NULL) {
526 return FALSE;
527 }
528 self::restore();
529 $length = fwrite($this->fileHandle, $data);
530 self::register();
531 return $length;
532 }
533
534 /**
535 * Unlink a file
536 *
537 * @param string $path
538 * @return bool
539 */
540 public function unlink($path) {
541 self::restore();
542 $path = self::overlayPath($path);
543 $success = unlink($path);
544 self::register();
545 return $success;
546 }
547
548 /**
549 * Retrieve information about a file
550 *
551 * @param string $path
552 * @param int $flags
553 * @return array
554 */
555 public function url_stat($path, $flags) {
556 self::restore();
557 $path = self::overlayPath($path);
558 if ($flags & STREAM_URL_STAT_LINK) {
559 $information = @lstat($path);
560 } else {
561 $information = @stat($path);
562 }
563 self::register();
564 return $information;
565 }
566
567 }