diff --git a/.github/workflows/appstore-build-publish.yml b/.github/workflows/appstore-build-publish.yml index 84dd91f1..aa8d7dc5 100644 --- a/.github/workflows/appstore-build-publish.yml +++ b/.github/workflows/appstore-build-publish.yml @@ -2,6 +2,9 @@ # # https://github.com/nextcloud/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT name: Build and publish app release @@ -9,9 +12,6 @@ on: release: types: [published] -env: - PHP_VERSION: 8.1 - jobs: build_and_publish: runs-on: ubuntu-latest @@ -21,7 +21,7 @@ jobs: steps: - name: Check actor permission - uses: skjnldsv/check-actor-permission@e591dbfe838300c007028e1219ca82cc26e8d7c5 # v2.1 + uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 with: require: write @@ -32,7 +32,7 @@ jobs: echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV - name: Checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: path: ${{ env.APP_NAME }} @@ -44,38 +44,44 @@ jobs: expression: "//info//dependencies//nextcloud/@min-version" - name: Read package.json node and npm engines version - uses: skjnldsv/read-package-engines-version-actions@0ce2ed60f6df073a62a77c0a4958dd0fc68e32e7 # v2.1 + uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 id: versions # Continue if no package.json continue-on-error: true with: path: ${{ env.APP_NAME }} - fallbackNode: "^16" - fallbackNpm: "^7" + fallbackNode: '^20' + fallbackNpm: '^10' - name: Set up node ${{ steps.versions.outputs.nodeVersion }} # Skip if no package.json if: ${{ steps.versions.outputs.nodeVersion }} - uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3 + uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3 with: node-version: ${{ steps.versions.outputs.nodeVersion }} - name: Set up npm ${{ steps.versions.outputs.npmVersion }} # Skip if no package.json if: ${{ steps.versions.outputs.npmVersion }} - run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" + run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' + + - name: Get php version + id: php-versions + uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 + with: + filename: ${{ env.APP_NAME }}/appinfo/info.xml - - name: Set up php ${{ env.PHP_VERSION }} - uses: shivammathur/setup-php@c5fc0d8281aba02c7fda07d3a70cc5371548067d # v2 + - name: Set up php ${{ steps.php-versions.outputs.php-min }} + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: - php-version: ${{ env.PHP_VERSION }} + php-version: ${{ steps.php-versions.outputs.php-min }} coverage: none env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check composer.json id: check_composer - uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2 + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 with: files: "${{ env.APP_NAME }}/composer.json" @@ -88,14 +94,16 @@ jobs: - name: Build ${{ env.APP_NAME }} # Skip if no package.json if: ${{ steps.versions.outputs.nodeVersion }} + env: + CYPRESS_INSTALL_BINARY: 0 run: | cd ${{ env.APP_NAME }} npm ci - npm run build + npm run build --if-present - name: Check Krankerl config id: krankerl - uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2 + uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 with: files: ${{ env.APP_NAME }}/krankerl.toml @@ -121,12 +129,12 @@ jobs: continue-on-error: true id: server-checkout run: | - NCVERSION=${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} + NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}' wget --quiet https://download.nextcloud.com/server/releases/latest-$NCVERSION.zip unzip latest-$NCVERSION.zip - name: Checkout server master fallback - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 if: ${{ steps.server-checkout.outcome != 'success' }} with: submodules: true @@ -140,7 +148,7 @@ jobs: tar -xvf ${{ env.APP_NAME }}.tar.gz cd ../../../ # Setting up keys - echo "${{ secrets.APP_PRIVATE_KEY }}" > ${{ env.APP_NAME }}.key + echo '${{ secrets.APP_PRIVATE_KEY }}' > ${{ env.APP_NAME }}.key wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt" # Signing php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }} @@ -149,7 +157,7 @@ jobs: tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }} - name: Attach tarball to github release - uses: svenstaro/upload-release-action@2b9d2847a97b04d02ad5c3df2d3a27baa97ce689 # v2 + uses: svenstaro/upload-release-action@04733e069f2d7f7f0b4aebc4fbdbce8613b03ccd # v2 id: attach_to_release with: repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/fixup.yml b/.github/workflows/fixup.yml index 9548d19f..69da2bbb 100644 --- a/.github/workflows/fixup.yml +++ b/.github/workflows/fixup.yml @@ -2,6 +2,9 @@ # # https://github.com/nextcloud/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT name: Block fixup and squash commits @@ -24,10 +27,10 @@ jobs: pull-requests: write name: Block fixup and squash commits - runs-on: ubuntu-latest + runs-on: ubuntu-latest-low steps: - name: Run check - uses: skjnldsv/block-fixup-merge-action@42d26e1b536ce61e5cf467d65fb76caf4aa85acf # v1 + uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/litmus.yml b/.github/workflows/litmus.yml index be5056b9..c5f63ec2 100644 --- a/.github/workflows/litmus.yml +++ b/.github/workflows/litmus.yml @@ -107,7 +107,7 @@ jobs: run: cat data/nextcloud.log - name: Upload litmus logs - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: failure() with: name: Upload litmus log @@ -115,7 +115,7 @@ jobs: retention-days: 5 - name: Upload nextcloud logs - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: failure() with: name: Upload nextcloud log diff --git a/.github/workflows/pr-feedback.yml b/.github/workflows/pr-feedback.yml index e85c4ecd..6a01fa09 100644 --- a/.github/workflows/pr-feedback.yml +++ b/.github/workflows/pr-feedback.yml @@ -3,6 +3,13 @@ # https://github.com/nextcloud/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-FileCopyrightText: 2023 Marcel Klehr +# SPDX-FileCopyrightText: 2023 Joas Schilling <213943+nickvergessen@users.noreply.github.com> +# SPDX-FileCopyrightText: 2023 Daniel Kesselberg +# SPDX-FileCopyrightText: 2023 Florian Steffens +# SPDX-License-Identifier: MIT + name: 'Ask for feedback on PRs' on: schedule: @@ -17,18 +24,27 @@ jobs: id: scrape with: website: 'https://nextcloud.com/team/' - - uses: marcelklehr/pr-feedback-action@601109aa729eb4c8d6d0ece7567b9d4901db4aef + + - name: Get blocklist + id: blocklist + run: | + blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -) + echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT" + + - uses: marcelklehr/pr-feedback-action@1883b38a033fb16f576875e0cf45f98b857655c4 with: feedback-message: | Hello there, - Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. + Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 Thank you for contributing to Nextcloud and we hope to hear from you soon! + + (If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).) days-before-feedback: 14 - start-date: "2023-07-10" - exempt-authors: "${{ steps.scrape.outputs.users }},nextcloud-command,nextcloud-android-bot,skjnldsv" + start-date: '2024-04-30' + exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }}' exempt-bots: true diff --git a/.github/workflows/update-nextcloud-ocp.yml b/.github/workflows/update-nextcloud-ocp.yml index 908eedb0..6e705cc8 100644 --- a/.github/workflows/update-nextcloud-ocp.yml +++ b/.github/workflows/update-nextcloud-ocp.yml @@ -2,6 +2,9 @@ # # https://github.com/nextcloud/.github # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization +# +# SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors +# SPDX-License-Identifier: MIT name: Update nextcloud/ocp @@ -17,23 +20,23 @@ jobs: strategy: fail-fast: false matrix: - branches: ["main", "master", "stable27", "stable26", "stable25"] + branches: ['main', 'master', 'stable29', 'stable28', 'stable27'] name: update-nextcloud-ocp-${{ matrix.branches }} steps: - id: checkout - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: ref: ${{ matrix.branches }} submodules: true continue-on-error: true - - name: Set up php8.1 + - name: Set up php8.2 if: steps.checkout.outcome == 'success' - uses: shivammathur/setup-php@4bd44f22a98a19e0950cbad5f31095157cc9621b # v2 + uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1 with: - php-version: 8.1 + php-version: 8.2 # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite coverage: none @@ -54,15 +57,15 @@ jobs: - name: Composer update nextcloud/ocp id: update_branch if: ${{ steps.checkout.outcome == 'success' && matrix.branches != 'main' }} - run: composer require --dev nextcloud/ocp:dev-${{ matrix.branches }} + run: composer require --dev 'nextcloud/ocp:dev-${{ matrix.branches }}' - name: Raise on issue on failure - uses: dacbd/create-issue-action@ba4d1c45cccf9c483f2720cefb40e437f0ee6f7d # v1.2.1 + uses: dacbd/create-issue-action@cdb57ab6ff8862aa09fee2be6ba77a59581921c2 # v2.0.0 if: ${{ steps.checkout.outcome == 'success' && failure() && steps.update_branch.conclusion == 'failure' }} with: token: ${{ secrets.GITHUB_TOKEN }} - title: Failed to update nextcloud/ocp package on branch ${{ matrix.branches }} - body: Please check the output of the GitHub action and manually resolve the issues
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
${{ steps.codeowners.outputs.codeowners }} + title: 'Failed to update nextcloud/ocp package on branch ${{ matrix.branches }}' + body: 'Please check the output of the GitHub action and manually resolve the issues
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
${{ steps.codeowners.outputs.codeowners }}' - name: Composer update nextcloud/ocp id: update_main @@ -70,12 +73,12 @@ jobs: run: composer require --dev nextcloud/ocp:dev-master - name: Raise on issue on failure - uses: dacbd/create-issue-action@ba4d1c45cccf9c483f2720cefb40e437f0ee6f7d # v1.2.1 + uses: dacbd/create-issue-action@cdb57ab6ff8862aa09fee2be6ba77a59581921c2 # v2.0.0 if: ${{ steps.checkout.outcome == 'success' && failure() && steps.update_main.conclusion == 'failure' }} with: token: ${{ secrets.GITHUB_TOKEN }} - title: Failed to update nextcloud/ocp package on branch ${{ matrix.branches }} - body: Please check the output of the GitHub action and manually resolve the issues
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
${{ steps.codeowners.outputs.codeowners }} + title: 'Failed to update nextcloud/ocp package on branch ${{ matrix.branches }}' + body: 'Please check the output of the GitHub action and manually resolve the issues
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
${{ steps.codeowners.outputs.codeowners }}' - name: Reset checkout 3rdparty if: steps.checkout.outcome == 'success' @@ -100,15 +103,15 @@ jobs: - name: Create Pull Request if: steps.checkout.outcome == 'success' - uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v3 + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c # v6.1.0 with: token: ${{ secrets.COMMAND_BOT_PAT }} - commit-message: "chore(dev-deps): Bump nextcloud/ocp package" + commit-message: 'chore(dev-deps): Bump nextcloud/ocp package' committer: GitHub author: nextcloud-command signoff: true - branch: automated/noid/${{ matrix.branches }}-update-nextcloud-ocp - title: "[${{ matrix.branches }}] Update nextcloud/ocp dependency" + branch: 'automated/noid/${{ matrix.branches }}-update-nextcloud-ocp' + title: '[${{ matrix.branches }}] Update nextcloud/ocp dependency' body: | Auto-generated update of [nextcloud/ocp](https://github.com/nextcloud-deps/ocp/) dependency labels: | diff --git a/lib/DAV/LockBackend.php b/lib/DAV/LockBackend.php index e6450a7b..b13c0ac5 100644 --- a/lib/DAV/LockBackend.php +++ b/lib/DAV/LockBackend.php @@ -39,7 +39,6 @@ use OCP\Files\NotFoundException; use Sabre\DAV\Locks\Backend\BackendInterface; use Sabre\DAV\Locks\LockInfo; -use Sabre\DAV\Server; class LockBackend implements BackendInterface { /** @var FileService */ @@ -52,9 +51,8 @@ class LockBackend implements BackendInterface { private $absolute = false; public function __construct( - Server $server, FileService $fileService, LockService $lockService, bool $absolute + FileService $fileService, LockService $lockService, bool $absolute ) { - $this->server = $server; $this->fileService = $fileService; $this->lockService = $lockService; $this->absolute = $absolute; diff --git a/lib/DAV/LockPlugin.php b/lib/DAV/LockPlugin.php index 93975923..61793f34 100644 --- a/lib/DAV/LockPlugin.php +++ b/lib/DAV/LockPlugin.php @@ -58,7 +58,7 @@ public function initialize(Server $server) { $absolute = true; break; } - $this->locksBackend = new LockBackend($server, $this->fileService, $this->lockService, $absolute); + $this->locksBackend = new LockBackend($this->fileService, $this->lockService, $absolute); $server->on('propFind', [$this, 'customProperties']); parent::initialize($server); } diff --git a/lib/Db/LocksRequest.php b/lib/Db/LocksRequest.php index 92bcf5a8..4c29ecdd 100644 --- a/lib/Db/LocksRequest.php +++ b/lib/Db/LocksRequest.php @@ -49,11 +49,12 @@ class LocksRequest extends LocksRequestBuilder { public function save(FileLock $lock) { $qb = $this->getLocksInsertSql(); $qb->setValue('user_id', $qb->createNamedParameter($lock->getOwner())) - ->setValue('file_id', $qb->createNamedParameter($lock->getFileId())) - ->setValue('token', $qb->createNamedParameter($lock->getToken())) - ->setValue('creation', $qb->createNamedParameter($lock->getCreatedAt())) - ->setValue('type', $qb->createNamedParameter($lock->getType())) - ->setValue('ttl', $qb->createNamedParameter($lock->getTimeout())); + ->setValue('file_id', $qb->createNamedParameter($lock->getFileId())) + ->setValue('token', $qb->createNamedParameter($lock->getToken())) + ->setValue('creation', $qb->createNamedParameter($lock->getCreatedAt())) + ->setValue('type', $qb->createNamedParameter($lock->getType())) + ->setValue('ttl', $qb->createNamedParameter($lock->getTimeout())) + ->setValue('owner', $qb->createNamedParameter($lock->getDisplayName())); try { $qb->execute(); diff --git a/lib/Service/LockService.php b/lib/Service/LockService.php index 33bfa5c1..b167fbbd 100644 --- a/lib/Service/LockService.php +++ b/lib/Service/LockService.php @@ -46,6 +46,7 @@ use OCP\Files\Lock\OwnerLockedException; use OCP\Files\NotFoundException; use OCP\IL10N; +use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; @@ -65,6 +66,7 @@ class LockService { private IAppManager $appManager; private IEventDispatcher $eventDispatcher; private IUserSession $userSession; + private IRequest $request; private array $locks = []; @@ -82,7 +84,8 @@ public function __construct( ConfigService $configService, IAppManager $appManager, IEventDispatcher $eventDispatcher, - IUserSession $userSession + IUserSession $userSession, + IRequest $request ) { $this->l10n = $l10n; $this->userManager = $userManager; @@ -92,6 +95,7 @@ public function __construct( $this->appManager = $appManager; $this->eventDispatcher = $eventDispatcher; $this->userSession = $userSession; + $this->request = $request; $this->setup('app', 'files_lock'); } @@ -184,9 +188,9 @@ public function lock(LockContext $lockScope): FileLock { $this->generateToken($lock); $lock->setCreation(time()); $this->notice('locking file', false, ['fileLock' => $lock]); + $this->injectMetadata($lock); $this->locksRequest->save($lock); $this->propagateEtag($lockScope); - $this->injectMetadata($lock); return $lock; } } @@ -353,6 +357,11 @@ public function injectMetadata(FileLock $lock): FileLock { } else { $displayName = $this->userManager->getDisplayName($lock->getOwner()) ?? $lock->getDisplayName(); } + $clientHint = $this->getClientHint(); + $displayName = $lock->getDisplayName() ?: ( + $displayName . ' ' . + ($clientHint ? ('(' . $clientHint . ')') : '') + ); } if ($displayName) { @@ -361,6 +370,22 @@ public function injectMetadata(FileLock $lock): FileLock { return $lock; } + private function getClientHint(): ?string { + if ($this->request->isUserAgent([IRequest::USER_AGENT_CLIENT_DESKTOP])) { + return $this->l10n->t('Desktop client'); + } + + if ($this->request->isUserAgent([IRequest::USER_AGENT_CLIENT_IOS])) { + return $this->l10n->t('iOS client'); + } + + if ($this->request->isUserAgent([IRequest::USER_AGENT_CLIENT_ANDROID])) { + return $this->l10n->t('Android client'); + } + + return null; + } + /** * @param FileLock $lock */ diff --git a/tests/Feature/LockFeatureTest.php b/tests/Feature/LockFeatureTest.php index 37706d95..aa0497e7 100644 --- a/tests/Feature/LockFeatureTest.php +++ b/tests/Feature/LockFeatureTest.php @@ -318,6 +318,33 @@ public function testLockDifferentAppsPublic() { }); } + public function testUnlockStaleClientLock() { + // Create a file and lock it as the desktop client would + $file = $this->loginAndGetUserFolder(self::TEST_USER1)->newFile('test-file-client', 'AAA'); + $this->shareFileWithUser($file, self::TEST_USER1, self::TEST_USER2); + + $this->lockManager->lock(new LockContext($file, ILock::TYPE_TOKEN, self::TEST_USER1)); + $locks = $this->lockManager->getLocks($file->getId()); + $this->assertCount(1, $locks); + + // Other users cannot unlock + try { + $this->lockManager->unlock(new LockContext($file, ILock::TYPE_TOKEN, self::TEST_USER2)); + $locks = []; + } catch (\OCP\PreConditionNotMetException $e) { + $locks = $this->lockManager->getLocks($file->getId()); + } + $this->assertCount(1, $locks); + + + // The owner can stil force unlock it as done through the OCS controller + \OCP\Server::get(\OCA\FilesLock\Service\LockService::class)->enableUserOverride(); + $this->lockManager->unlock(new LockContext($file, ILock::TYPE_USER, self::TEST_USER1)); + + $locks = $this->lockManager->getLocks($file->getId()); + $this->assertCount(0, $locks); + } + private function loginAndGetUserFolder(string $userId) { $this->loginAsUser($userId); return $this->rootFolder->getUserFolder($userId); diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml index 77ba5683..0e6c655e 100644 --- a/tests/psalm-baseline.xml +++ b/tests/psalm-baseline.xml @@ -1,85 +1,81 @@ - - - - server]]> - - + - CachingTree - Directory - FakeLockerPlugin - File - FilesPlugin - ObjectTree + + + + + + - ExtendedQueryBuilder + - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - CoreQueryBuilder + + + + + + + + + - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb - $qb + + + + + + + + + + + + + + + + + + + + + + + + + + + + - UniqueConstraintViolationException + - CoreQueryBuilder - CoreQueryBuilder - CoreQueryBuilder - CoreQueryBuilder - CoreQueryBuilder - CoreQueryBuilder + + + + + + - $lock + @@ -88,28 +84,28 @@ rootFolder]]> rootFolder]]> rootFolder]]> - IRootFolder - IRootFolder + + - $arr === null - $arr === null - $arr === null - $arr === null - $arr === null - $arr === null - $arr === null + + + + + + + - string - string + + - string +