Index: trunk/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php |
— | — | @@ -2,6 +2,7 @@ |
3 | 3 | |
4 | 4 | /** |
5 | 5 | * @group FileRepo |
| 6 | + * @group FileBackend |
6 | 7 | */ |
7 | 8 | class FileBackendTest extends MediaWikiTestCase { |
8 | 9 | private $backend, $multiBackend; |
— | — | @@ -74,6 +75,118 @@ |
75 | 76 | } |
76 | 77 | |
77 | 78 | /** |
| 79 | + * @dataProvider provider_testIsStoragePath |
| 80 | + */ |
| 81 | + public function testIsStoragePath( $path, $isStorePath ) { |
| 82 | + $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ), |
| 83 | + "FileBackend::isStoragePath on path '$path'" ); |
| 84 | + } |
| 85 | + |
| 86 | + function provider_testIsStoragePath() { |
| 87 | + return array( |
| 88 | + array( 'mwstore://', true ), |
| 89 | + array( 'mwstore://backend', true ), |
| 90 | + array( 'mwstore://backend/container', true ), |
| 91 | + array( 'mwstore://backend/container/', true ), |
| 92 | + array( 'mwstore://backend/container/path', true ), |
| 93 | + array( 'mwstore://backend//container/', true ), |
| 94 | + array( 'mwstore://backend//container//', true ), |
| 95 | + array( 'mwstore://backend//container//path', true ), |
| 96 | + array( 'mwstore:///', true ), |
| 97 | + array( 'mwstore:/', false ), |
| 98 | + array( 'mwstore:', false ), |
| 99 | + ); |
| 100 | + } |
| 101 | + |
| 102 | + /** |
| 103 | + * @dataProvider provider_testSplitStoragePath |
| 104 | + */ |
| 105 | + public function testSplitStoragePath( $path, $res ) { |
| 106 | + $this->assertEquals( $res, FileBackend::splitStoragePath( $path ), |
| 107 | + "FileBackend::splitStoragePath on path '$path'" ); |
| 108 | + } |
| 109 | + |
| 110 | + function provider_testSplitStoragePath() { |
| 111 | + return array( |
| 112 | + array( 'mwstore://backend/container', array( 'backend', 'container', '' ) ), |
| 113 | + array( 'mwstore://backend/container/', array( 'backend', 'container', '' ) ), |
| 114 | + array( 'mwstore://backend/container/path', array( 'backend', 'container', 'path' ) ), |
| 115 | + array( 'mwstore://backend/container//path', array( 'backend', 'container', '/path' ) ), |
| 116 | + array( 'mwstore://backend//container/path', array( null, null, null ) ), |
| 117 | + array( 'mwstore://backend//container//path', array( null, null, null ) ), |
| 118 | + array( 'mwstore://', array( null, null, null ) ), |
| 119 | + array( 'mwstore://backend', array( null, null, null ) ), |
| 120 | + array( 'mwstore:///', array( null, null, null ) ), |
| 121 | + array( 'mwstore:/', array( null, null, null ) ), |
| 122 | + array( 'mwstore:', array( null, null, null ) ) |
| 123 | + ); |
| 124 | + } |
| 125 | + |
| 126 | + /** |
| 127 | + * @dataProvider provider_normalizeStoragePath |
| 128 | + */ |
| 129 | + public function testNormalizeStoragePath( $path, $res ) { |
| 130 | + $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ), |
| 131 | + "FileBackend::normalizeStoragePath on path '$path'" ); |
| 132 | + } |
| 133 | + |
| 134 | + function provider_normalizeStoragePath() { |
| 135 | + return array( |
| 136 | + array( 'mwstore://backend/container', 'mwstore://backend/container' ), |
| 137 | + array( 'mwstore://backend/container/', 'mwstore://backend/container' ), |
| 138 | + array( 'mwstore://backend/container/path', 'mwstore://backend/container/path' ), |
| 139 | + array( 'mwstore://backend/container//path', 'mwstore://backend/container/path' ), |
| 140 | + array( 'mwstore://backend/container///path', 'mwstore://backend/container/path' ), |
| 141 | + array( 'mwstore://backend/container///path//to///obj', 'mwstore://backend/container/path/to/obj', |
| 142 | + array( 'mwstore://', null ), |
| 143 | + array( 'mwstore://backend', null ), |
| 144 | + array( 'mwstore://backend//container/path', null ), |
| 145 | + array( 'mwstore://backend//container//path', null ), |
| 146 | + array( 'mwstore:///', null ), |
| 147 | + array( 'mwstore:/', null ), |
| 148 | + array( 'mwstore:', null ), ) |
| 149 | + ); |
| 150 | + } |
| 151 | + |
| 152 | + /** |
| 153 | + * @dataProvider provider_testParentStoragePath |
| 154 | + */ |
| 155 | + public function testParentStoragePath( $path, $res ) { |
| 156 | + $this->assertEquals( $res, FileBackend::parentStoragePath( $path ), |
| 157 | + "FileBackend::parentStoragePath on path '$path'" ); |
| 158 | + } |
| 159 | + |
| 160 | + function provider_testParentStoragePath() { |
| 161 | + return array( |
| 162 | + array( 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ), |
| 163 | + array( 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ), |
| 164 | + array( 'mwstore://backend/container/path', 'mwstore://backend/container' ), |
| 165 | + array( 'mwstore://backend/container', null ), |
| 166 | + array( 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ), |
| 167 | + array( 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ), |
| 168 | + array( 'mwstore://backend/container/path/', 'mwstore://backend/container' ), |
| 169 | + array( 'mwstore://backend/container/', null ), |
| 170 | + ); |
| 171 | + } |
| 172 | + |
| 173 | + /** |
| 174 | + * @dataProvider provider_testExtensionFromPath |
| 175 | + */ |
| 176 | + public function testExtensionFromPath( $path, $res ) { |
| 177 | + $this->assertEquals( $res, FileBackend::extensionFromPath( $path ), |
| 178 | + "FileBackend::extensionFromPath on path '$path'" ); |
| 179 | + } |
| 180 | + |
| 181 | + function provider_testExtensionFromPath() { |
| 182 | + return array( |
| 183 | + array( 'mwstore://backend/container/path.txt', 'txt' ), |
| 184 | + array( 'mwstore://backend/container/path.svg.png', 'png' ), |
| 185 | + array( 'mwstore://backend/container/path', '' ), |
| 186 | + array( 'mwstore://backend/container/path.', '' ), |
| 187 | + ); |
| 188 | + } |
| 189 | + |
| 190 | + /** |
78 | 191 | * @dataProvider provider_testStore |
79 | 192 | */ |
80 | 193 | public function testStore( $op ) { |
— | — | @@ -1060,6 +1173,7 @@ |
1061 | 1174 | foreach ( $iter as $iter ) {} // no errors |
1062 | 1175 | } |
1063 | 1176 | |
| 1177 | + // test helper wrapper for backend prepare() function |
1064 | 1178 | private function prepare( array $params ) { |
1065 | 1179 | $this->dirsToPrune[] = $params['dir']; |
1066 | 1180 | return $this->backend->prepare( $params ); |
Index: trunk/phase3/includes/filerepo/backend/FSFileBackend.php |
— | — | @@ -58,6 +58,16 @@ |
59 | 59 | } |
60 | 60 | |
61 | 61 | /** |
| 62 | + * @see FileBackend::resolveContainerName() |
| 63 | + */ |
| 64 | + protected function resolveContainerName( $container ) { |
| 65 | + if ( $container !== '.' ) { |
| 66 | + return $container; // container is not a traversal |
| 67 | + } |
| 68 | + return null; |
| 69 | + } |
| 70 | + |
| 71 | + /** |
62 | 72 | * @see FileBackend::resolveContainerPath() |
63 | 73 | */ |
64 | 74 | protected function resolveContainerPath( $container, $relStoragePath ) { |
Index: trunk/phase3/includes/filerepo/backend/FileBackend.php |
— | — | @@ -30,9 +30,9 @@ |
31 | 31 | * @since 1.19 |
32 | 32 | */ |
33 | 33 | abstract class FileBackendBase { |
34 | | - protected $name; // unique backend name |
35 | | - protected $wikiId; // unique wiki name |
36 | | - protected $readOnly; // string |
| 34 | + protected $name; // string; unique backend name |
| 35 | + protected $wikiId; // string; unique wiki name |
| 36 | + protected $readOnly; // string; read-only explanation message |
37 | 37 | /** @var LockManager */ |
38 | 38 | protected $lockManager; |
39 | 39 | |
— | — | @@ -265,7 +265,10 @@ |
266 | 266 | } |
267 | 267 | |
268 | 268 | /** |
269 | | - * Concatenate a list of storage files into a single file on the file system |
| 269 | + * Concatenate a list of storage files into a single file system file. |
| 270 | + * The target path should refer to a file that is already locked or |
| 271 | + * otherwise safe from modification from other processes. Normally, |
| 272 | + * the file will be a new temp file, which should be adequate. |
270 | 273 | * $params include: |
271 | 274 | * srcs : ordered source storage paths (e.g. chunk1, chunk2, ...) |
272 | 275 | * dst : file system path to 0-byte temp file |
— | — | @@ -561,6 +564,116 @@ |
562 | 565 | final public function getScopedFileLocks( array $paths, $type, Status $status ) { |
563 | 566 | return ScopedLock::factory( $this->lockManager, $paths, $type, $status ); |
564 | 567 | } |
| 568 | + |
| 569 | + /** |
| 570 | + * Check if a given path is a "mwstore://" path. |
| 571 | + * This does not do any further validation or any existence checks. |
| 572 | + * |
| 573 | + * @param $path string |
| 574 | + * @return bool |
| 575 | + */ |
| 576 | + final public static function isStoragePath( $path ) { |
| 577 | + return ( strpos( $path, 'mwstore://' ) === 0 ); |
| 578 | + } |
| 579 | + |
| 580 | + /** |
| 581 | + * Split a storage path into a backend name, a container name, |
| 582 | + * and a relative file path. The relative path may be the empty string. |
| 583 | + * This does not do any path normalization or traversal checks. |
| 584 | + * |
| 585 | + * @param $storagePath string |
| 586 | + * @return Array (backend, container, rel object) or (null, null, null) |
| 587 | + */ |
| 588 | + final public static function splitStoragePath( $storagePath ) { |
| 589 | + if ( self::isStoragePath( $storagePath ) ) { |
| 590 | + // Remove the "mwstore://" prefix and split the path |
| 591 | + $parts = explode( '/', substr( $storagePath, 10 ), 3 ); |
| 592 | + if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) { |
| 593 | + if ( count( $parts ) == 3 ) { |
| 594 | + return $parts; // e.g. "backend/container/path" |
| 595 | + } else { |
| 596 | + return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" |
| 597 | + } |
| 598 | + } |
| 599 | + } |
| 600 | + return array( null, null, null ); |
| 601 | + } |
| 602 | + |
| 603 | + /** |
| 604 | + * Normalize a storage path by cleaning up directory separators. |
| 605 | + * Returns null if the path is not of the format of a valid storage path. |
| 606 | + * |
| 607 | + * @param $storagePath string |
| 608 | + * @return string|null |
| 609 | + */ |
| 610 | + final public static function normalizeStoragePath( $storagePath ) { |
| 611 | + list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); |
| 612 | + if ( $relPath !== null ) { // must be for this backend |
| 613 | + $relPath = self::normalizeContainerPath( $relPath ); |
| 614 | + if ( $relPath !== null ) { |
| 615 | + return ( $relPath != '' ) |
| 616 | + ? "mwstore://{$backend}/{$container}/{$relPath}" |
| 617 | + : "mwstore://{$backend}/{$container}"; |
| 618 | + } |
| 619 | + } |
| 620 | + return null; |
| 621 | + } |
| 622 | + |
| 623 | + /** |
| 624 | + * Validate and normalize a relative storage path. |
| 625 | + * Null is returned if the path involves directory traversal. |
| 626 | + * Traversal is insecure for FS backends and broken for others. |
| 627 | + * |
| 628 | + * @param $path string Storage path relative to a container |
| 629 | + * @return string|null |
| 630 | + */ |
| 631 | + final protected static function normalizeContainerPath( $path ) { |
| 632 | + // Normalize directory separators |
| 633 | + $path = strtr( $path, '\\', '/' ); |
| 634 | + // Collapse any consecutive directory separators |
| 635 | + $path = preg_replace( '![/]{2,}!', '/', $path ); |
| 636 | + // Remove any leading directory separator |
| 637 | + $path = ltrim( $path, '/' ); |
| 638 | + // Use the same traversal protection as Title::secureAndSplit() |
| 639 | + if ( strpos( $path, '.' ) !== false ) { |
| 640 | + if ( |
| 641 | + $path === '.' || |
| 642 | + $path === '..' || |
| 643 | + strpos( $path, './' ) === 0 || |
| 644 | + strpos( $path, '../' ) === 0 || |
| 645 | + strpos( $path, '/./' ) !== false || |
| 646 | + strpos( $path, '/../' ) !== false |
| 647 | + ) { |
| 648 | + return null; |
| 649 | + } |
| 650 | + } |
| 651 | + return $path; |
| 652 | + } |
| 653 | + |
| 654 | + /** |
| 655 | + * Get the parent storage directory of a storage path. |
| 656 | + * This returns a path like "mwstore://backend/container", |
| 657 | + * "mwstore://backend/container/...", or null if there is no parent. |
| 658 | + * |
| 659 | + * @param $storagePath string |
| 660 | + * @return string|null |
| 661 | + */ |
| 662 | + final public static function parentStoragePath( $storagePath ) { |
| 663 | + $storagePath = dirname( $storagePath ); |
| 664 | + list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath ); |
| 665 | + return ( $rel === null ) ? null : $storagePath; |
| 666 | + } |
| 667 | + |
| 668 | + /** |
| 669 | + * Get the final extension from a storage or FS path |
| 670 | + * |
| 671 | + * @param $path string |
| 672 | + * @return string |
| 673 | + */ |
| 674 | + final public static function extensionFromPath( $path ) { |
| 675 | + $i = strrpos( $path, '.' ); |
| 676 | + return strtolower( $i ? substr( $path, $i + 1 ) : '' ); |
| 677 | + } |
565 | 678 | } |
566 | 679 | |
567 | 680 | /** |
— | — | @@ -1300,71 +1413,6 @@ |
1301 | 1414 | } |
1302 | 1415 | |
1303 | 1416 | /** |
1304 | | - * Get the parent storage directory of a storage path. |
1305 | | - * This returns a path like "mwstore://backend/container", |
1306 | | - * "mwstore://backend/container/...", or null if there is no parent. |
1307 | | - * |
1308 | | - * @param $storagePath string |
1309 | | - * @return string|null |
1310 | | - */ |
1311 | | - final public static function parentStoragePath( $storagePath ) { |
1312 | | - $storagePath = dirname( $storagePath ); |
1313 | | - list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath ); |
1314 | | - return ( $rel === null ) ? null : $storagePath; |
1315 | | - } |
1316 | | - |
1317 | | - /** |
1318 | | - * Check if a given path is a mwstore:// path. |
1319 | | - * This does not do any actual validation or existence checks. |
1320 | | - * |
1321 | | - * @param $path string |
1322 | | - * @return bool |
1323 | | - */ |
1324 | | - final public static function isStoragePath( $path ) { |
1325 | | - return ( strpos( $path, 'mwstore://' ) === 0 ); |
1326 | | - } |
1327 | | - |
1328 | | - /** |
1329 | | - * Normalize a storage path by cleaning up directory separators. |
1330 | | - * Returns null if the path is not of the format of a valid storage path. |
1331 | | - * |
1332 | | - * @param $storagePath string |
1333 | | - * @return string|null |
1334 | | - */ |
1335 | | - final public static function normalizeStoragePath( $storagePath ) { |
1336 | | - list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); |
1337 | | - if ( $relPath !== null ) { // must be for this backend |
1338 | | - $relPath = self::normalizeContainerPath( $relPath ); |
1339 | | - if ( $relPath !== null ) { |
1340 | | - return ( $relPath != '' ) |
1341 | | - ? "mwstore://{$backend}/{$container}/{$relPath}" |
1342 | | - : "mwstore://{$backend}/{$container}"; |
1343 | | - } |
1344 | | - } |
1345 | | - return null; |
1346 | | - } |
1347 | | - |
1348 | | - /** |
1349 | | - * Split a storage path into a backend name, a container name, |
1350 | | - * and a relative file path. The relative path may be the empty string. |
1351 | | - * |
1352 | | - * @param $storagePath string |
1353 | | - * @return Array (backend, container, rel object) or (null, null, null) |
1354 | | - */ |
1355 | | - final public static function splitStoragePath( $storagePath ) { |
1356 | | - if ( self::isStoragePath( $storagePath ) ) { |
1357 | | - // Note: strlen( 'mwstore://' ) = 10 |
1358 | | - $parts = explode( '/', substr( $storagePath, 10 ), 3 ); |
1359 | | - if ( count( $parts ) == 3 ) { |
1360 | | - return $parts; // e.g. "backend/container/path" |
1361 | | - } elseif ( count( $parts ) == 2 ) { |
1362 | | - return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" |
1363 | | - } |
1364 | | - } |
1365 | | - return array( null, null, null ); |
1366 | | - } |
1367 | | - |
1368 | | - /** |
1369 | 1417 | * Check if a container name is valid. |
1370 | 1418 | * This checks for for length and illegal characters. |
1371 | 1419 | * |
— | — | @@ -1380,35 +1428,6 @@ |
1381 | 1429 | } |
1382 | 1430 | |
1383 | 1431 | /** |
1384 | | - * Validate and normalize a relative storage path. |
1385 | | - * Null is returned if the path involves directory traversal. |
1386 | | - * Traversal is insecure for FS backends and broken for others. |
1387 | | - * |
1388 | | - * @param $path string Storage path relative to a container |
1389 | | - * @return string|null |
1390 | | - */ |
1391 | | - final protected static function normalizeContainerPath( $path ) { |
1392 | | - // Normalize directory separators |
1393 | | - $path = strtr( $path, '\\', '/' ); |
1394 | | - // Collapse consecutive directory separators |
1395 | | - $path = preg_replace( '![/]{2,}!', '/', $path ); |
1396 | | - // Use the same traversal protection as Title::secureAndSplit() |
1397 | | - if ( strpos( $path, '.' ) !== false ) { |
1398 | | - if ( |
1399 | | - $path === '.' || |
1400 | | - $path === '..' || |
1401 | | - strpos( $path, './' ) === 0 || |
1402 | | - strpos( $path, '../' ) === 0 || |
1403 | | - strpos( $path, '/./' ) !== false || |
1404 | | - strpos( $path, '/../' ) !== false |
1405 | | - ) { |
1406 | | - return null; |
1407 | | - } |
1408 | | - } |
1409 | | - return $path; |
1410 | | - } |
1411 | | - |
1412 | | - /** |
1413 | 1432 | * Splits a storage path into an internal container name, |
1414 | 1433 | * an internal relative file name, and a container shard suffix. |
1415 | 1434 | * Any shard suffix is already appended to the internal container name. |
— | — | @@ -1565,17 +1584,6 @@ |
1566 | 1585 | protected function resolveContainerPath( $container, $relStoragePath ) { |
1567 | 1586 | return $relStoragePath; |
1568 | 1587 | } |
1569 | | - |
1570 | | - /** |
1571 | | - * Get the final extension from a storage or FS path |
1572 | | - * |
1573 | | - * @param $path string |
1574 | | - * @return string |
1575 | | - */ |
1576 | | - final public static function extensionFromPath( $path ) { |
1577 | | - $i = strrpos( $path, '.' ); |
1578 | | - return strtolower( $i ? substr( $path, $i + 1 ) : '' ); |
1579 | | - } |
1580 | 1588 | } |
1581 | 1589 | |
1582 | 1590 | /** |