diff --git a/3rdparty b/3rdparty index bbef51b59e027..dff719813f5d6 160000 --- a/3rdparty +++ b/3rdparty @@ -1 +1 @@ -Subproject commit bbef51b59e027569b9eebf0805342fcb2568848e +Subproject commit dff719813f5d6d8c31634a7b504063c20a093f13 diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 2a11eacd9bece..a0bf84f610af7 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -5,7 +5,7 @@ WebDAV WebDAV endpoint WebDAV endpoint - 1.10.0 + 1.10.1 agpl owncloud.org DAV @@ -23,6 +23,7 @@ OCA\DAV\BackgroundJob\CleanupDirectLinksJob OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob OCA\DAV\BackgroundJob\CleanupInvitationTokenJob + OCA\DAV\BackgroundJob\CleanupPaginateCacheJob diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 6452cd298d4b8..9967d615166ea 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -13,6 +13,7 @@ 'OCA\\DAV\\Avatars\\RootCollection' => $baseDir . '/../lib/Avatars/RootCollection.php', 'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => $baseDir . '/../lib/BackgroundJob/CleanupDirectLinksJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', + 'OCA\\DAV\\BackgroundJob\\CleanupPaginateCacheJob' => $baseDir . '/../lib/BackgroundJob/CleanupPaginateCacheJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => $baseDir . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', @@ -178,6 +179,10 @@ 'OCA\\DAV\\Migration\\Version1008Date20181105110300' => $baseDir . '/../lib/Migration/Version1008Date20181105110300.php', 'OCA\\DAV\\Migration\\Version1008Date20181105112049' => $baseDir . '/../lib/Migration/Version1008Date20181105112049.php', 'OCA\\DAV\\Migration\\Version1008Date20181114084440' => $baseDir . '/../lib/Migration/Version1008Date20181114084440.php', + 'OCA\\DAV\\Migration\\Version1009Date20181108161232' => $baseDir . '/../lib/Migration/Version1009Date20181108161232.php', + 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php', + 'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php', + 'OCA\\DAV\\Paginate\\PaginatePlugin' => $baseDir . '/../lib/Paginate/PaginatePlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => $baseDir . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => $baseDir . '/../lib/RootCollection.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 19c74d3be0752..cdf651be29f1c 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -28,6 +28,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Avatars\\RootCollection' => __DIR__ . '/..' . '/../lib/Avatars/RootCollection.php', 'OCA\\DAV\\BackgroundJob\\CleanupDirectLinksJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupDirectLinksJob.php', 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', + 'OCA\\DAV\\BackgroundJob\\CleanupPaginateCacheJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupPaginateCacheJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', @@ -193,6 +194,10 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1008Date20181105110300' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20181105110300.php', 'OCA\\DAV\\Migration\\Version1008Date20181105112049' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20181105112049.php', 'OCA\\DAV\\Migration\\Version1008Date20181114084440' => __DIR__ . '/..' . '/../lib/Migration/Version1008Date20181114084440.php', + 'OCA\\DAV\\Migration\\Version1009Date20181108161232' => __DIR__ . '/..' . '/../lib/Migration/Version1009Date20181108161232.php', + 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php', + 'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php', + 'OCA\\DAV\\Paginate\\PaginatePlugin' => __DIR__ . '/..' . '/../lib/Paginate/PaginatePlugin.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningNode' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningNode.php', 'OCA\\DAV\\Provisioning\\Apple\\AppleProvisioningPlugin' => __DIR__ . '/..' . '/../lib/Provisioning/Apple/AppleProvisioningPlugin.php', 'OCA\\DAV\\RootCollection' => __DIR__ . '/..' . '/../lib/RootCollection.php', diff --git a/apps/dav/lib/BackgroundJob/CleanupPaginateCacheJob.php b/apps/dav/lib/BackgroundJob/CleanupPaginateCacheJob.php new file mode 100644 index 0000000000000..aa7de37a4741c --- /dev/null +++ b/apps/dav/lib/BackgroundJob/CleanupPaginateCacheJob.php @@ -0,0 +1,40 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\BackgroundJob; + +use OC\BackgroundJob\Job; +use OCA\DAV\Paginate\PaginateCache; + +class CleanupPaginateCacheJob extends Job { + + /** @var PaginateCache */ + private $cache; + + public function __construct(PaginateCache $cache) { + $this->cache = $cache; + } + + public function run($argument) { + $this->cache->cleanup(); + } + +} \ No newline at end of file diff --git a/apps/dav/lib/Migration/Version1009Date20181108161232.php b/apps/dav/lib/Migration/Version1009Date20181108161232.php new file mode 100644 index 0000000000000..6aa805a1750a2 --- /dev/null +++ b/apps/dav/lib/Migration/Version1009Date20181108161232.php @@ -0,0 +1,55 @@ +hasTable('dav_page_cache')) { + $table = $schema->createTable('dav_page_cache'); + + $table->addColumn('id', Type::BIGINT, [ + 'autoincrement' => true + ]); + $table->addColumn('url_hash', Type::STRING, [ + 'notnull' => true, + 'length' => 32, + ]); + $table->addColumn('token', Type::STRING, [ + 'notnull' => true, + 'length' => 32 + ]); + $table->addColumn('result_index', Type::INTEGER, [ + 'notnull' => true + ]); + $table->addColumn('result_value', TYPE::TEXT, [ + 'notnull' => false, + ]); + $table->addColumn('insert_time', TYPE::BIGINT, [ + 'notnull' => false, + ]); + + $table->setPrimaryKey(['id'], 'dav_page_cache_id_index'); + $table->addIndex(['token', 'url_hash'], 'dav_page_cache_token_url'); + $table->addUniqueIndex(['token', 'url_hash', 'result_index'], 'dav_page_cache_url_index'); + $table->addIndex(['result_index'], 'dav_page_cache_index'); + $table->addIndex(['insert_time'], 'dav_page_cache_time'); + } + + return $schema; + } +} diff --git a/apps/dav/lib/Paginate/LimitedCopyIterator.php b/apps/dav/lib/Paginate/LimitedCopyIterator.php new file mode 100644 index 0000000000000..2b3d06940238e --- /dev/null +++ b/apps/dav/lib/Paginate/LimitedCopyIterator.php @@ -0,0 +1,54 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Paginate; + +/** + * Save a copy of the first X items into a separate iterator + * + * this allows us to pass the iterator to the cache while keeping a copy + * of the first X items + */ +class LimitedCopyIterator extends \AppendIterator { + /** @var array */ + private $copy; + + public function __construct(\Traversable $iterator, int $count) { + parent::__construct(); + + if (!$iterator instanceof \Iterator) { + $iterator = new \IteratorIterator($iterator); + } + $iterator = new \NoRewindIterator($iterator); + + while($iterator->valid() && count($this->copy) < $count) { + $this->copy[] = $iterator->current(); + $iterator->next(); + } + + $this->append($this->getFirstItems()); + $this->append($iterator); + } + + public function getFirstItems(): \Iterator { + return new \ArrayIterator($this->copy); + } +} \ No newline at end of file diff --git a/apps/dav/lib/Paginate/PaginateCache.php b/apps/dav/lib/Paginate/PaginateCache.php new file mode 100644 index 0000000000000..45bff55764ca9 --- /dev/null +++ b/apps/dav/lib/Paginate/PaginateCache.php @@ -0,0 +1,112 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Paginate; + + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Security\ISecureRandom; + +class PaginateCache { + const TTL = 3600; + + /** @var IDBConnection */ + private $database; + /** @var ISecureRandom */ + private $random; + /** @var ITimeFactory */ + private $timeFactory; + + public function __construct( + IDBConnection $database, + ISecureRandom $random, + ITimeFactory $timeFactory + ) { + $this->database = $database; + $this->random = $random; + $this->timeFactory = $timeFactory; + } + + public function store(string $uri, \Iterator $items): array { + $token = $this->random->generate(32); + $now = $this->timeFactory->getTime(); + + $query = $this->database->getQueryBuilder(); + $query->insert('dav_page_cache') + ->values([ + 'url_hash' => $query->createNamedParameter(md5($uri), IQueryBuilder::PARAM_STR), + 'token' => $query->createNamedParameter($token, IQueryBuilder::PARAM_STR), + 'insert_time' => $query->createNamedParameter($now, IQueryBuilder::PARAM_INT), + 'result_index' => $query->createParameter('index'), + 'result_value' => $query->createParameter('value'), + ]); + + $count = 0; + foreach ($items as $item) { + $value = json_encode($item); + $query->setParameter('index', $count, IQueryBuilder::PARAM_INT); + $query->setParameter('value', $value); + $query->execute(); + $count++; + } + + return [$token, $count]; + } + + /** + * @param string $url + * @param string $token + * @param int $offset + * @param int $count + * @return array|\Traversable + */ + public function get(string $url, string $token, int $offset, int $count) { + $query = $this->database->getQueryBuilder(); + $query->select(['result_value']) + ->from('dav_page_cache') + ->where($query->expr()->eq('token', $query->createNamedParameter($token))) + ->andWhere($query->expr()->eq('url_hash', $query->createNamedParameter(md5($url)))) + ->andWhere($query->expr()->gte('result_index', $query->createNamedParameter($offset, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->lt('result_index', $query->createNamedParameter($offset + $count, IQueryBuilder::PARAM_INT))); + + $result = $query->execute(); + return array_map(function (string $entry) { + return json_decode($entry, true); + }, $result->fetchAll(\PDO::FETCH_COLUMN)); + } + + public function cleanup() { + $now = $this->timeFactory->getTime(); + + $query = $this->database->getQueryBuilder(); + $query->delete('dav_page_cache') + ->where($query->expr()->lt('insert_time', $query->createNamedParameter($now - self::TTL))); + $query->execute(); + } + + public function clear() { + $query = $this->database->getQueryBuilder(); + $query->delete('dav_page_cache'); + $query->execute(); + } +} \ No newline at end of file diff --git a/apps/dav/lib/Paginate/PaginatePlugin.php b/apps/dav/lib/Paginate/PaginatePlugin.php new file mode 100644 index 0000000000000..849d688d4aff5 --- /dev/null +++ b/apps/dav/lib/Paginate/PaginatePlugin.php @@ -0,0 +1,110 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Paginate; + +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; + +class PaginatePlugin extends ServerPlugin { + const PAGINATE_HEADER = 'x-nc-paginate'; + const PAGINATE_TOTAL_HEADER = 'x-nc-paginate-total'; + const PAGINATE_TOKEN_HEADER = 'x-nc-paginate-token'; + const PAGINATE_OFFSET_HEADER = 'x-nc-paginate-offset'; + const PAGINATE_COUNT_HEADER = 'x-nc-paginate-count'; + + /** @var int */ + private $pageSize; + + /** @var Server */ + private $server; + + /** @var PaginateCache */ + private $cache; + + public function __construct(PaginateCache $cache, int $pageSize = 100) { + $this->cache = $cache; + $this->pageSize = $pageSize; + } + + function initialize(Server $server) { + $this->server = $server; + $server->on('beforeMultiStatus', [$this, 'onMultiStatus']); + $server->on('method:SEARCH', [$this, 'onMethod'], 1); + $server->on('method:PROPFIND', [$this, 'onMethod'], 1); + $server->on('method:REPORT', [$this, 'onMethod'], 1); + } + + public function getFeatures() { + return ['nc-paginate']; + } + + function onMultiStatus(&$fileProperties) { + $request = $this->server->httpRequest; + if (is_array($fileProperties)) { + $fileProperties = new \ArrayIterator($fileProperties); + } + if ( + $request->hasHeader(self::PAGINATE_HEADER) && + !$request->hasHeader(self::PAGINATE_TOKEN_HEADER) + ) { + $url = $request->getUrl(); + + $pageSize = (int)$request->getHeader(self::PAGINATE_COUNT_HEADER) ?: $this->pageSize; + $copyIterator = new LimitedCopyIterator($fileProperties, $pageSize); + list($token, $count) = $this->cache->store($url, $copyIterator); + + $fileProperties = $copyIterator->getFirstItems(); + $this->server->httpResponse->addHeader(self::PAGINATE_HEADER, 'true'); + $this->server->httpResponse->addHeader(self::PAGINATE_TOKEN_HEADER, $token); + $this->server->httpResponse->addHeader(self::PAGINATE_TOTAL_HEADER, $count); + } + } + + function onMethod(RequestInterface $request, ResponseInterface $response) { + if ( + $request->hasHeader(self::PAGINATE_TOKEN_HEADER) && + $request->hasHeader(self::PAGINATE_OFFSET_HEADER) + ) { + $url = $this->server->httpRequest->getUrl(); + $token = $request->getHeader(self::PAGINATE_TOKEN_HEADER); + $offset = (int)$request->getHeader(self::PAGINATE_OFFSET_HEADER); + $count = (int)$request->getHeader(self::PAGINATE_COUNT_HEADER) ?: $this->pageSize; + + $items = $this->cache->get($url, $token, $offset, $count); + + $response->setStatus(207); + $response->addHeader(self::PAGINATE_HEADER, 'true'); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Vary', 'Brief,Prefer'); + + $prefer = $this->server->getHTTPPrefer(); + $minimal = $prefer['return'] === 'minimal'; + + $data = $this->server->generateMultiStatus($items, $minimal); + $response->setBody($data); + return false; + } + } + +} \ No newline at end of file diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 7eb68ce58748e..2bf0f7983c736 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -56,6 +56,7 @@ use OCA\DAV\Files\BrowserErrorPagePlugin; use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin; use OCA\DAV\Files\LazySearchBackend; +use OCA\DAV\Paginate\PaginatePlugin; use OCA\DAV\Provisioning\Apple\AppleProvisioningPlugin; use OCA\DAV\SystemTag\SystemTagPlugin; use OCA\DAV\Upload\ChunkingPlugin; @@ -192,6 +193,7 @@ public function __construct(IRequest $request, $baseUri) { $this->server->addPlugin(new CopyEtagHeaderPlugin()); $this->server->addPlugin(new ChunkingPlugin()); + $this->server->addPlugin(\OC::$server->query(PaginatePlugin::class)); // allow setup of additional plugins $dispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event); diff --git a/apps/dav/tests/unit/Paginate/LimitedCopyIteratorTest.php b/apps/dav/tests/unit/Paginate/LimitedCopyIteratorTest.php new file mode 100644 index 0000000000000..c13c84815af6a --- /dev/null +++ b/apps/dav/tests/unit/Paginate/LimitedCopyIteratorTest.php @@ -0,0 +1,38 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Tests\Paginate; + +use OCA\DAV\Paginate\LimitedCopyIterator; +use Test\TestCase; + +class LimitedCopyIteratorTest extends TestCase { + public function testBasic() { + $data = range(0, 100); + + $iterator = new LimitedCopyIterator(new \ArrayIterator($data), 10); + + $this->assertEquals(array_slice($data, 0, 10), iterator_to_array($iterator->getFirstItems())); + $results = iterator_to_array($iterator); + $this->assertEquals($data, $results); + $this->assertEquals(array_slice($data, 0, 10), iterator_to_array($iterator->getFirstItems())); + } +} \ No newline at end of file diff --git a/apps/dav/tests/unit/Paginate/PaginateCacheTest.php b/apps/dav/tests/unit/Paginate/PaginateCacheTest.php new file mode 100644 index 0000000000000..eb7d438a32cf4 --- /dev/null +++ b/apps/dav/tests/unit/Paginate/PaginateCacheTest.php @@ -0,0 +1,121 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Tests\Paginate; + +use OCA\DAV\Paginate\PaginateCache; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Security\ISecureRandom; +use Test\TestCase; + +/** + * @group DB + */ +class PaginateCacheTest extends TestCase { + /** @var PaginateCache */ + private $cache; + + /** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */ + private $timeFactory; + + /** @var ISecureRandom|\PHPUnit_Framework_MockObject_MockObject */ + private $random; + + const SAMPLE_DATA = [ + ['foo'], + ['bar'], + ['asd'] + ]; + + const URL = 'http://example.com'; + + private $now = 1000; + + protected function setUp() { + parent::setUp(); + + $count = 0; + + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->timeFactory->expects($this->any()) + ->method('getTime') + ->willReturnCallback(function() { + return $this->now; + }); + $this->random = $this->createMock(ISecureRandom::class); + $this->random->expects($this->any()) + ->method('generate') + ->willReturnCallback(function () use (&$count) { + $count++; + return 'random_value_' . $count; + }); + $this->cache = new PaginateCache( + \OC::$server->getDatabaseConnection(), + $this->random, + $this->timeFactory + ); + } + + protected function tearDown() { + $this->cache->clear(); + + return parent::tearDown(); + } + + public function testBasic() { + list($token, $count) = $this->cache->store(self::URL, new \ArrayIterator(self::SAMPLE_DATA)); + $this->assertEquals(count(self::SAMPLE_DATA), $count); + + $this->assertEquals(self::SAMPLE_DATA, $this->cache->get(self::URL, $token, 0, 5)); + + $this->assertEquals(array_slice(self::SAMPLE_DATA, 1, 1), $this->cache->get(self::URL, $token, 1, 1)); + } + + public function testWrongUrl() { + list($token) = $this->cache->store(self::URL, new \ArrayIterator(self::SAMPLE_DATA)); + + $this->assertCount(0, $this->cache->get('http://other.url', $token, 0, 5)); + } + + public function testWrongToken() { + $this->cache->store(self::URL, new \ArrayIterator(self::SAMPLE_DATA)); + + $this->assertCount(0, $this->cache->get(self::URL, 'wrong_token', 0, 5)); + } + + public function testCleanup() { + $this->now = 1000; + list($token1) = $this->cache->store(self::URL, new \ArrayIterator(self::SAMPLE_DATA)); + + $this->now = 2000; + list($token2) = $this->cache->store(self::URL, new \ArrayIterator(self::SAMPLE_DATA)); + + $this->assertCount(count(self::SAMPLE_DATA), $this->cache->get(self::URL, $token1, 0, 5)); + $this->assertCount(count(self::SAMPLE_DATA), $this->cache->get(self::URL, $token2, 0, 5)); + + $this->now = 1500 + PaginateCache::TTL; + + $this->cache->cleanup(); + + $this->assertCount(0, $this->cache->get(self::URL, $token1, 0, 5)); + $this->assertCount(count(self::SAMPLE_DATA), $this->cache->get(self::URL, $token2, 0, 5)); + } +} \ No newline at end of file diff --git a/apps/dav/tests/unit/Paginate/PaginatePluginTest.php b/apps/dav/tests/unit/Paginate/PaginatePluginTest.php new file mode 100644 index 0000000000000..bd2a0363533c2 --- /dev/null +++ b/apps/dav/tests/unit/Paginate/PaginatePluginTest.php @@ -0,0 +1,117 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\Tests\Paginate; + +use OCA\DAV\Paginate\PaginateCache; +use OCA\DAV\Paginate\PaginatePlugin; +use Sabre\DAV\Server; +use Sabre\HTTP\Request; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\Response; +use Test\TestCase; + +class PaginatePluginTest extends TestCase { + /** @var PaginateCache|\PHPUnit_Framework_MockObject_MockObject */ + private $cache; + /** @var Server|\PHPUnit_Framework_MockObject_MockObject */ + private $server; + + /** @var PaginatePlugin */ + private $plugin; + + const DATA = [ + [ + 'href' => 'foo', + '200' => [ + 'd:getcontentlength' => 10 + ], + ], + [ + 'href' => 'bar', + '200' => [ + 'd:getcontentlength' => 11 + ], + ], + [ + 'href' => 'asd', + '200' => [ + 'd:getcontentlength' => 11 + ], + ] + ]; + + protected function setUp() { + parent::setUp(); + + $this->cache = $this->createMock(PaginateCache::class); + $this->server = $this->createMock(Server::class); + $this->server->httpRequest = new Request('GET', 'http://example.com'); + $this->server->httpResponse = new Response(); + $this->plugin = new PaginatePlugin($this->cache, 2); + $this->plugin->initialize($this->server); + } + + public function testPaginateRequest() { + $fileProperties = new \ArrayIterator(self::DATA); + $storedProps = []; + $this->cache->expects($this->any()) + ->method('store') + ->willReturnCallback(function ($url, $props) use (&$storedProps) { + $storedProps = $props; + return ['token', iterator_count($storedProps)]; + }); + + $this->server->httpRequest->setHeader(PaginatePlugin::PAGINATE_HEADER, 'true'); + + $this->plugin->onMultiStatus($fileProperties); + $this->assertTrue($this->server->httpResponse->hasHeader(PaginatePlugin::PAGINATE_HEADER)); + $this->assertTrue($this->server->httpResponse->hasHeader(PaginatePlugin::PAGINATE_TOKEN_HEADER)); + $this->assertEquals(3, $this->server->httpResponse->getHeader(PaginatePlugin::PAGINATE_TOTAL_HEADER)); + + $data = self::DATA; + $this->assertEquals(array_splice($data, 0, 2), array_values(iterator_to_array($fileProperties))); + $this->assertEquals(self::DATA, iterator_to_array($storedProps)); + } + + public function testGetCached() { + $this->cache->expects($this->any()) + ->method('get') + ->willReturnCallback(function ($url, $token, $offset, $count) { + return new \LimitIterator(new \ArrayIterator(self::DATA), $offset, $count); + }); + $responseItems = null; + $this->server->expects($this->any()) + ->method('generateMultiStatus') + ->willReturnCallback(function ($items) use (&$responseItems) { + $responseItems = $items; + }); + + $this->server->httpRequest->setHeader(PaginatePlugin::PAGINATE_TOKEN_HEADER, 'foo'); + $this->server->httpRequest->setHeader(PaginatePlugin::PAGINATE_OFFSET_HEADER, '1'); + $this->server->httpRequest->setHeader(PaginatePlugin::PAGINATE_COUNT_HEADER, '1'); + + $this->plugin->onMethod($this->server->httpRequest, $this->server->httpResponse); + + $this->assertTrue($this->server->httpResponse->hasHeader(PaginatePlugin::PAGINATE_HEADER)); + $this->assertEquals(array_slice(self::DATA, 1, 1), array_values(iterator_to_array($responseItems))); + } +} \ No newline at end of file