diff --git a/lib/FilesHooks.php b/lib/FilesHooks.php
index b073dd44e..62859f6a5 100644
--- a/lib/FilesHooks.php
+++ b/lib/FilesHooks.php
@@ -17,7 +17,11 @@
use OCP\Activity\IManager;
use OCP\BackgroundJob\IJobList;
use OCP\Constants;
+use OCP\Files\Cache\IFileAccess;
+use OCP\Files\Config\ICachedMountFileInfo;
+use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IUserMountCache;
+use OCP\Files\Config\MountProviderArgs;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
@@ -66,6 +70,8 @@ public function __construct(
protected NotificationGenerator $notificationGenerator,
protected ITagManager $tagManager,
protected ?CirclesManager $teamManager,
+ private IFileAccess $fileAccess,
+ private IMountProviderCollection $mountProviderCollection,
) {
}
@@ -1273,18 +1279,9 @@ private function getAffectedUsersFromCachedMounts(int $fileId): array {
$mountsForFile = $this->userMountCache->getMountsForFileId($fileId);
foreach ($mountsForFile as $mount) {
$affectedUsers[$mount->getUser()->getUID()] = $this->getVisiblePath($mount->getPath());
- $cachedMounts[] = [
- 'userId' => $mount->getUser()->getUID(),
- 'provider' => str_replace('\\\\', '\\', $mount->getMountProvider()),
- 'path' => $mount->getPath(),
- 'visiblePath' => $this->getVisiblePath($mount->getPath()),
- 'storageId' => $mount->getStorageId(),
- 'internalPath' => $mount->getInternalPath(),
- 'rootInternalPath' => $mount->getRootInternalPath(),
- ];
}
- $unrelatedUsers = $this->getUnrelatedUsers($cachedMounts);
+ $unrelatedUsers = $this->getBlockedGroupfoldersAclUsers($mountsForFile);
return array_filter($affectedUsers, fn (string $userId): bool => !in_array($userId, $unrelatedUsers, true), ARRAY_FILTER_USE_KEY);
}
@@ -1292,10 +1289,11 @@ private function getAffectedUsersFromCachedMounts(int $fileId): array {
/**
* returns an array of users that have confirmed no access to fileId
*
+ * @param ICachedMountFileInfo[] $cachedMounts
*
- * @return string[] list of unrelated userIds
+ * @return string[] list of blocked userIds
*/
- private function getUnrelatedUsers(array $cachedMounts): array {
+ private function getBlockedGroupfoldersAclUsers(array $cachedMounts): array {
/** @var \OCA\GroupFolders\ACL\RuleManager $ruleManager */
/** @var \OCA\GroupFolders\Folder\FolderManager $folderManager */
try {
@@ -1305,46 +1303,58 @@ private function getUnrelatedUsers(array $cachedMounts): array {
return []; // if we have no access to RuleManager, we cannot filter unrelated users
}
- $groupFolderAclStatus = [];
-
/** @var \OCA\GroupFolders\ACL\Rule[] $rules */
$rules = $knownRules = $knownGroupRules = $usersToCheck = $cachedPath = [];
+ $groupfolderId = null;
foreach ($cachedMounts as $cachedMount) {
// we are only interested in filtering GroupFolders ACL
- if ($cachedMount['provider'] !== 'OCA\GroupFolders\Mount\MountProvider') {
+ if ($cachedMount->getMountProvider() !== 'OCA\GroupFolders\Mount\MountProvider') {
continue;
}
// caching rules based on storage id
- $storageId = $cachedMount['storageId'];
+ $storageId = $cachedMount->getStorageId();
if (!array_key_exists($storageId, $knownRules)) {
$knownRules[$storageId] = [];
}
- $cachedPath[$cachedMount['userId']] = $fullPath = $cachedMount['path'];
+ $cachedPath[$cachedMount->getUser()->getUID()] = $fullPath = $cachedMount->getPath();
// caching rules based on storage+path to file
- if (!array_key_exists($cachedMount['visiblePath'], $knownRules[$storageId])) {
+ $visiblePath = $this->getVisiblePath($cachedMount->getPath());
+ if (!array_key_exists($visiblePath, $knownRules[$storageId])) {
// we need mountPoint and folderId to generate the correct path
try {
- // only check for groupfolders
- if (!str_starts_with($cachedMount['rootInternalPath'], '__groupfolders')) {
- continue;
- }
-
- $folderId = (int)basename($cachedMount['rootInternalPath']);
- if (!isset($groupFolderAclStatus[$folderId])) {
- $groupFolderAclStatus[$folderId] = $folderManager->getFolderAclEnabled($folderId);
- }
- if (!$groupFolderAclStatus[$folderId]) {
- continue; // acl are disable
+ if ($groupfolderId === null) {
+ $rootId = $cachedMount->getRootId();
+ $rootMetadata = $this->fileAccess->getByFileId($rootId);
+ if (!$rootMetadata) {
+ continue;
+ }
+ $providerArgs = new MountProviderArgs($cachedMount, $rootMetadata);
+ /** @psalm-suppress UndefinedInterfaceMethod Could not find a public API for this */
+ $mounts = $this->mountProviderCollection->getUserMountsFromProviderByPath(
+ $cachedMount->getMountProvider(),
+ '',
+ false,
+ [$providerArgs]
+ );
+ $mount = reset($mounts);
+ if (!($mount instanceof \OCA\GroupFolders\Mount\GroupMountPoint)) {
+ continue;
+ }
+ $groupfolderId = $mount->getFolderId();
+ if (!$folderManager->getFolderAclEnabled($groupfolderId)) {
+ // ACL are disabled for the groupfolder, we can stop here
+ break;
+ }
}
- $folderPath = '/' . $cachedMount['rootInternalPath'];
- $path = $cachedMount['internalPath'];
+ $folderPath = '/' . $cachedMount->getRootInternalPath();
+ $path = $cachedMount->getInternalPath();
} catch (\Exception) {
// in case of issue during the process, we can imagine the user have no access to the file
- $usersToCheck[] = $cachedMount['userId'];
+ $usersToCheck[] = $cachedMount->getUser()->getUID();
continue; // we'll catch rules on next user with access to the file
}
@@ -1352,7 +1362,7 @@ private function getUnrelatedUsers(array $cachedMounts): array {
$paths = [$folderPath];
while ($path !== '') {
$paths[] = $folderPath . '/' . $path;
- $path = dirname((string)$path);
+ $path = dirname($path);
if ($path === '.' || $path === '/') {
$path = '';
}
@@ -1373,7 +1383,7 @@ private function getUnrelatedUsers(array $cachedMounts): array {
$rules = array_merge($rules, $rulesPerPath[$path]);
}
- $knownRules[$storageId][$cachedMount['visiblePath']] = true;
+ $knownRules[$storageId][$visiblePath] = true;
}
}
@@ -1412,7 +1422,7 @@ private function getUnrelatedUsers(array $cachedMounts): array {
// now that we have a list of eventuals filtered users, we confirm they have no access to the file
$filteredUsers = [];
foreach ($usersToCheck as $userId) {
- if (!array_key_exists($userId, $cachedPath) || $cachedPath[$userId] === null) {
+ if (!array_key_exists($userId, $cachedPath)) {
$this->logger->notice('could not find user in list of cached path', ['cachePath' => $cachedPath, 'usersToCheck' => $usersToCheck, 'current' => $userId]);
$filteredUsers[] = $userId;
continue;
diff --git a/psalm.xml b/psalm.xml
index 7882e8b5c..73f728fa0 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -25,13 +25,29 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/FilesHooksTest.php b/tests/FilesHooksTest.php
index a90a70df4..570a22f95 100644
--- a/tests/FilesHooksTest.php
+++ b/tests/FilesHooksTest.php
@@ -33,6 +33,8 @@
use OCA\Files_Sharing\SharedMount;
use OCP\Activity\IEvent;
use OCP\Activity\IManager;
+use OCP\Files\Cache\IFileAccess;
+use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\File;
use OCP\Files\Folder;
@@ -77,6 +79,8 @@ class FilesHooksTest extends TestCase {
protected TagManager&MockObject $tagManager;
protected Tags&MockObject $tags;
protected (OCA\Circles\CirclesManager&MockObject)|null $teamManager = null;
+ private IFileAccess&MockObject $fileAccess;
+ private IMountProviderCollection&MockObject $mountProviderCollection;
protected function setUp(): void {
parent::setUp();
@@ -98,6 +102,8 @@ protected function setUp(): void {
->willReturn([]);
$this->tagManager->method('load')
->willReturn($this->tags);
+ $this->fileAccess = $this->createMock(IFileAccess::class);
+ $this->mountProviderCollection = $this->createMock(IMountProviderCollection::class);
$this->filesHooks = $this->getFilesHooks();
}
@@ -131,6 +137,8 @@ protected function getFilesHooks(array $mockedMethods = [], ?string $user = 'use
$this->notificationGenerator,
$this->tagManager,
$this->teamManager,
+ $this->fileAccess,
+ $this->mountProviderCollection,
])
->onlyMethods($mockedMethods)
->getMock();
@@ -152,6 +160,8 @@ protected function getFilesHooks(array $mockedMethods = [], ?string $user = 'use
$this->notificationGenerator,
$this->tagManager,
$this->teamManager,
+ $this->fileAccess,
+ $this->mountProviderCollection,
);
}
@@ -199,6 +209,8 @@ public function testFileCreate(string $currentUser, bool $isPublicShare, string
$this->notificationGenerator,
$this->tagManager,
$this->teamManager,
+ $this->fileAccess,
+ $this->mountProviderCollection,
])
->onlyMethods(['addNotificationsForFileAction'])
->getMock();
diff --git a/tests/stubs/oc_core_command_base.php b/tests/stubs/oc_core_command_base.php
index c062e2fc7..504d82339 100644
--- a/tests/stubs/oc_core_command_base.php
+++ b/tests/stubs/oc_core_command_base.php
@@ -1,49 +1,89 @@
*/
+ protected string $class;
+ protected ?string $storageId = null;
+ protected ?int $numericStorageId = null;
+ protected ?int $rootId = null;
+
+ /**
+ * Configuration options for the storage backend
+ */
+ protected array $arguments = [];
+ protected string $mountPoint;
+
+ /**
+ * Mount specific options
+ */
+ protected array $mountOptions = [];
+ protected string $mountProvider;
+
+ /**
+ * @param IStorage|class-string $storage
+ * @param array $arguments (optional) configuration for the storage backend
+ * @param ?array $mountOptions mount specific options
+ * @param ?int $mountId
+ * @param ?string $mountProvider
+ * @throws \Exception
+ */
+ public function __construct(
+ string|IStorage $storage,
+ string $mountpoint,
+ ?array $arguments = null,
+ ?IStorageFactory $loader = null,
+ ?array $mountOptions = null,
+ protected ?int $mountId = null,
+ ?string $mountProvider = null,
+ ) {
+ }
+
+ /**
+ * get complete path to the mount point, relative to data/
+ *
+ * @return string
+ */
+ #[\Override]
+ public function getMountPoint() {
+ }
+
+ /**
+ * Sets the mount point path, relative to data/
+ *
+ * @param string $mountPoint new mount point
+ */
+ #[\Override]
+ public function setMountPoint($mountPoint) {
+ }
+
+ /**
+ * @return IStorage|null
+ */
+ #[\Override]
+ public function getStorage() {
+ }
+
+ /**
+ * @return string|null
+ */
+ #[\Override]
+ public function getStorageId() {
+ }
+
+ /**
+ * @return int
+ */
+ #[\Override]
+ public function getNumericStorageId() {
+ }
+
+ /**
+ * @param string $path
+ * @return string
+ */
+ #[\Override]
+ public function getInternalPath($path) {
+ }
+
+ /**
+ * @param callable $wrapper
+ */
+ #[\Override]
+ public function wrapStorage($wrapper) {
+ }
+
+ /**
+ * Get a mount option
+ *
+ * @param string $name Name of the mount option to get
+ * @param mixed $default Default value for the mount option
+ * @return mixed
+ */
+ #[\Override]
+ public function getOption($name, $default) {
+ }
+
+ /**
+ * Get all options for the mount
+ *
+ * @return array
+ */
+ #[\Override]
+ public function getOptions() {
+ }
+
+ /**
+ * Get the file id of the root of the storage
+ *
+ * @return int
+ */
+ #[\Override]
+ public function getStorageRootId() {
+ }
+
+ #[\Override]
+ public function getMountId() {
+ }
+
+ #[\Override]
+ public function getMountType() {
+ }
+
+ #[\Override]
+ public function getMountProvider(): string {
+ }
+}
diff --git a/tests/stubs/oc_files_storage_common.php b/tests/stubs/oc_files_storage_common.php
new file mode 100644
index 000000000..afd4f59fd
--- /dev/null
+++ b/tests/stubs/oc_files_storage_common.php
@@ -0,0 +1,288 @@
+stat() .
+ */
+abstract class Common implements Storage, ILockingStorage, IWriteStreamStorage, IConstructableStorage {
+ use LocalTempFileTrait;
+
+ protected ?Cache $cache = null;
+ protected ?Scanner $scanner = null;
+ protected ?Watcher $watcher = null;
+ protected ?Propagator $propagator = null;
+ protected $storageCache;
+ protected ?Updater $updater = null;
+
+ protected array $mountOptions = [];
+ protected $owner = null;
+
+ public function __construct(array $parameters) {
+ }
+
+ protected function remove(string $path): bool {
+ }
+
+ #[\Override]
+ public function is_dir(string $path): bool {
+ }
+
+ #[\Override]
+ public function is_file(string $path): bool {
+ }
+
+ #[\Override]
+ public function filesize(string $path): int|float|false {
+ }
+
+ #[\Override]
+ public function isReadable(string $path): bool {
+ }
+
+ #[\Override]
+ public function isUpdatable(string $path): bool {
+ }
+
+ #[\Override]
+ public function isCreatable(string $path): bool {
+ }
+
+ #[\Override]
+ public function isDeletable(string $path): bool {
+ }
+
+ #[\Override]
+ public function isSharable(string $path): bool {
+ }
+
+ #[\Override]
+ public function getPermissions(string $path): int {
+ }
+
+ #[\Override]
+ public function filemtime(string $path): int|false {
+ }
+
+ #[\Override]
+ public function file_get_contents(string $path): string|false {
+ }
+
+ #[\Override]
+ public function file_put_contents(string $path, mixed $data): int|float|false {
+ }
+
+ #[\Override]
+ public function rename(string $source, string $target): bool {
+ }
+
+ #[\Override]
+ public function copy(string $source, string $target): bool {
+ }
+
+ #[\Override]
+ public function getMimeType(string $path): string|false {
+ }
+
+ #[\Override]
+ public function hash(string $type, string $path, bool $raw = false): string|false {
+ }
+
+ #[\Override]
+ public function getLocalFile(string $path): string|false {
+ }
+
+ protected function searchInDir(string $query, string $dir = ''): array {
+ }
+
+ /**
+ * @inheritDoc
+ * Check if a file or folder has been updated since $time
+ *
+ * The method is only used to check if the cache needs to be updated. Storage backends that don't support checking
+ * the mtime should always return false here. As a result storage implementations that always return false expect
+ * exclusive access to the backend and will not pick up files that have been added in a way that circumvents
+ * Nextcloud filesystem.
+ */
+ #[\Override]
+ public function hasUpdated(string $path, int $time): bool {
+ }
+
+ protected function getCacheDependencies(): CacheDependencies {
+ }
+
+ #[\Override]
+ public function getCache(string $path = '', ?IStorage $storage = null): ICache {
+ }
+
+ #[\Override]
+ public function getScanner(string $path = '', ?IStorage $storage = null): IScanner {
+ }
+
+ #[\Override]
+ public function getWatcher(string $path = '', ?IStorage $storage = null): IWatcher {
+ }
+
+ #[\Override]
+ public function getPropagator(?IStorage $storage = null): IPropagator {
+ }
+
+ #[\Override]
+ public function getUpdater(?IStorage $storage = null): IUpdater {
+ }
+
+ #[\Override]
+ public function getStorageCache(?IStorage $storage = null): \OC\Files\Cache\Storage {
+ }
+
+ #[\Override]
+ public function getOwner(string $path): string|false {
+ }
+
+ #[\Override]
+ public function getETag(string $path): string|false {
+ }
+
+ /**
+ * clean a path, i.e. remove all redundant '.' and '..'
+ * making sure that it can't point to higher than '/'
+ *
+ * @param string $path The path to clean
+ * @return string cleaned path
+ */
+ public function cleanPath(string $path): string {
+ }
+
+ /**
+ * Test a storage for availability
+ */
+ #[\Override]
+ public function test(): bool {
+ }
+
+ #[\Override]
+ public function free_space(string $path): int|float|false {
+ }
+
+ #[\Override]
+ public function isLocal(): bool {
+ }
+
+ /**
+ * Check if the storage is an instance of $class or is a wrapper for a storage that is an instance of $class
+ */
+ #[\Override]
+ public function instanceOfStorage(string $class): bool {
+ }
+
+ #[Override]
+ public function getDirectDownload(string $path): array|false {
+ }
+
+ #[Override]
+ public function getDirectDownloadById(string $fileId): array|false {
+ }
+
+ #[\Override]
+ public function verifyPath(string $path, string $fileName): void {
+ }
+
+ /**
+ * Get the filename validator
+ * (cached for performance)
+ */
+ protected function getFilenameValidator(): IFilenameValidator {
+ }
+
+ public function setMountOptions(array $options): void {
+ }
+
+ public function getMountOption(string $name, mixed $default = null): mixed {
+ }
+
+ #[\Override]
+ public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath, bool $preserveMtime = false): bool {
+ }
+
+ #[\Override]
+ public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
+ }
+
+ #[\Override]
+ public function getMetaData(string $path): ?array {
+ }
+
+ #[\Override]
+ public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
+ }
+
+ #[\Override]
+ public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
+ }
+
+ #[\Override]
+ public function changeLock(string $path, int $type, ILockingProvider $provider): void {
+ }
+
+ /**
+ * @return array{available: bool, last_checked: int}
+ */
+ #[\Override]
+ public function getAvailability(): array {
+ }
+
+ #[\Override]
+ public function setAvailability(bool $isAvailable): void {
+ }
+
+ #[\Override]
+ public function setOwner(?string $user): void {
+ }
+
+ #[\Override]
+ public function needsPartFile(): bool {
+ }
+
+ #[\Override]
+ public function writeStream(string $path, $stream, ?int $size = null): int {
+ }
+
+ #[\Override]
+ public function getDirectoryContent(string $directory): \Traversable {
+ }
+}
diff --git a/tests/stubs/oc_files_storage_dav.php b/tests/stubs/oc_files_storage_dav.php
new file mode 100644
index 000000000..926966602
--- /dev/null
+++ b/tests/stubs/oc_files_storage_dav.php
@@ -0,0 +1,396 @@
+applyBearerToken((string)$settings['bearerToken']);
+ }
+ }
+
+ /**
+ * Register a callback invoked when a request comes back with HTTP 401. The
+ * callback should return a fresh bearer token, or null to give up. When a
+ * non-empty token is returned the failing request is replayed once.
+ *
+ * @param (callable(): ?string)|null $callback
+ */
+ public function setRefreshTokenCallback(?callable $callback): void {
+ $this->refreshTokenCallback = $callback === null ? null : \Closure::fromCallable($callback);
+ }
+
+ #[\Override]
+ public function send(RequestInterface $request): SabreResponseInterface {
+ try {
+ return parent::send($request);
+ } catch (ClientHttpException $e) {
+ if ($e->getHttpStatus() !== 401 || $this->retrying || $this->refreshTokenCallback === null) {
+ throw $e;
+ }
+ $this->retrying = true;
+ try {
+ $newToken = ($this->refreshTokenCallback)();
+ if (!is_string($newToken) || $newToken === '') {
+ throw $e;
+ }
+ $this->applyBearerToken($newToken);
+ return parent::send($request);
+ } finally {
+ $this->retrying = false;
+ }
+ }
+ }
+
+ private function applyBearerToken(string $token): void {
+ /** @psalm-suppress InvalidArrayOffset */
+ $curlType = $this->curlSettings[CURLOPT_HTTPAUTH] ?? 0;
+ $curlType |= CURLAUTH_BEARER;
+ $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType);
+ $this->addCurlSetting(CURLOPT_XOAUTH2_BEARER, $token);
+ }
+}
+
+/**
+ * Class DAV
+ *
+ * @package OC\Files\Storage
+ */
+class DAV extends Common {
+ /** @var string */
+ protected $password;
+ /** @var string */
+ protected $user;
+ protected ?int $authType = null;
+ /** @var string */
+ protected $host;
+ /** @var bool */
+ protected $secure;
+ protected bool $verify;
+ /** @var string */
+ protected $root;
+ /** @var string */
+ protected $certPath;
+ /** @var bool */
+ protected $ready;
+ /** The resolved bearer token for AUTH_BEARER (access token or exchanged token) */
+ protected string $bearerToken = '';
+ /** @var Client */
+ protected $client;
+ /** @var ArrayCache */
+ protected $statCache;
+ /** @var IClientService */
+ protected $httpClientService;
+ /** @var ICertificateManager */
+ protected $certManager;
+ protected LoggerInterface $logger;
+ protected IEventLogger $eventLogger;
+ protected IMimeTypeDetector $mimeTypeDetector;
+ protected IOCMDiscoveryService $discoveryService;
+ protected ISignatureManager $signatureManager;
+ protected OCMSignatoryManager $signatoryManager;
+ protected IAppConfig $appConfig;
+ protected IURLGenerator $urlGenerator;
+
+ protected const PROPFIND_PROPS = [
+ '{DAV:}getlastmodified',
+ '{DAV:}getcontentlength',
+ '{DAV:}getcontenttype',
+ '{http://owncloud.org/ns}permissions',
+ '{http://open-collaboration-services.org/ns}share-permissions',
+ '{http://open-cloud-mesh.org/ns}share-permissions',
+ '{DAV:}resourcetype',
+ '{DAV:}getetag',
+ '{DAV:}quota-available-bytes',
+ ];
+
+ /**
+ * @param array $parameters
+ * @throws \Exception
+ */
+ public function __construct(array $parameters) {
+ }
+
+ protected function init(): void {
+ }
+
+ /**
+ * Exchange refresh token for access token via the remote server's token endpoint
+ *
+ * @return string The access token
+ * @throws StorageNotAvailableException If token exchange fails
+ */
+ protected function exchangeRefreshToken(): string {
+ }
+
+ /**
+ * Check if bearer authentication is being used
+ */
+ protected function isBearerAuth(): bool {
+ }
+
+ /**
+ * Wrap a Guzzle-based operation with retry-on-401 using the bearer-token
+ * refresh. Sabre-based operations don't need this — {@see BearerAuthAwareSabreClient}
+ * handles 401 transparently on its own.
+ *
+ * @template T
+ * @param callable(): T $operation
+ * @return T
+ * @throws \GuzzleHttp\Exception\ClientException
+ */
+ protected function withAuthRetry(callable $operation): mixed {
+ }
+
+ /**
+ * Exchange the long-lived refresh token for a new short-lived access token
+ * and update {@see $bearerToken} (and the stored password so subsequent
+ * init() calls reuse the same token). Used as the refresh callback for the
+ * Sabre client and by {@see withAuthRetry} for the Guzzle paths.
+ *
+ * @return string|null new access token, or null if the exchange failed
+ */
+ protected function refreshAccessToken(): ?string {
+ }
+
+ /**
+ * Clear the stat cache
+ */
+ public function clearStatCache(): void {
+ }
+
+ #[\Override]
+ public function getId(): string {
+ }
+
+ public function createBaseUri(): string {
+ }
+
+ #[\Override]
+ public function mkdir(string $path): bool {
+ }
+
+ #[\Override]
+ public function rmdir(string $path): bool {
+ }
+
+ #[\Override]
+ public function opendir(string $path) {
+ }
+
+ /**
+ * @return array
+ */
+ protected function getPropfindProperties(): array {
+ }
+
+ /**
+ * Get property value from cached PROPFIND response.
+ * For accessing app-specific properties not included in getMetaData().
+ *
+ * @param string $path
+ * @param string $propertyName
+ * @return mixed
+ */
+ public function getPropfindPropertyValue(string $path, string $propertyName): mixed {
+ }
+
+ /**
+ * Propfind call with cache handling.
+ *
+ * First checks if information is cached.
+ * If not, request it from the server then store to cache.
+ *
+ * @param string $path path to propfind
+ *
+ * @return array|false propfind response or false if the entry was not found
+ *
+ * @throws ClientHttpException
+ */
+ protected function propfind(string $path): array|false {
+ }
+
+ #[\Override]
+ public function filetype(string $path): string|false {
+ }
+
+ #[\Override]
+ public function file_exists(string $path): bool {
+ }
+
+ #[\Override]
+ public function unlink(string $path): bool {
+ }
+
+ #[\Override]
+ public function fopen(string $path, string $mode) {
+ }
+
+ public function writeBack(string $tmpFile, string $path): void {
+ }
+
+ #[\Override]
+ public function free_space(string $path): int|float|false {
+ }
+
+ #[\Override]
+ public function touch(string $path, ?int $mtime = null): bool {
+ }
+
+ #[\Override]
+ public function file_put_contents(string $path, mixed $data): int|float|false {
+ }
+
+ protected function uploadFile(string $path, string $target): void {
+ }
+
+ #[\Override]
+ public function rename(string $source, string $target): bool {
+ }
+
+ #[\Override]
+ public function copy(string $source, string $target): bool {
+ }
+
+ #[\Override]
+ public function getMetaData(string $path): ?array {
+ }
+
+ #[\Override]
+ public function stat(string $path): array|false {
+ }
+
+ #[\Override]
+ public function getMimeType(string $path): string|false {
+ }
+
+ #[\Override]
+ public function cleanPath(string $path): string {
+ }
+
+ /**
+ * URL encodes the given path but keeps the slashes
+ *
+ * @param string $path to encode
+ * @return string encoded path
+ */
+ protected function encodePath(string $path): string {
+ }
+
+ /**
+ * @return bool
+ * @throws StorageInvalidException
+ * @throws StorageNotAvailableException
+ */
+ protected function simpleResponse(string $method, string $path, ?string $body, int $expected): bool {
+ }
+
+ /**
+ * check if curl is installed
+ */
+ public static function checkDependencies(): bool {
+ }
+
+ #[\Override]
+ public function isUpdatable(string $path): bool {
+ }
+
+ #[\Override]
+ public function isCreatable(string $path): bool {
+ }
+
+ #[\Override]
+ public function isSharable(string $path): bool {
+ }
+
+ #[\Override]
+ public function isDeletable(string $path): bool {
+ }
+
+ #[\Override]
+ public function getPermissions(string $path): int {
+ }
+
+ #[\Override]
+ public function getETag(string $path): string|false {
+ }
+
+ protected function parsePermissions(string $permissionsString): int {
+ }
+
+ #[\Override]
+ public function hasUpdated(string $path, int $time): bool {
+ }
+
+ /**
+ * Interpret the given exception and decide whether it is due to an
+ * unavailable storage, invalid storage or other.
+ * This will either throw StorageInvalidException, StorageNotAvailableException
+ * or do nothing.
+ *
+ * @param Exception $e sabre exception
+ * @param string $path optional path from the operation
+ *
+ * @throws StorageInvalidException if the storage is invalid, for example
+ * when the authentication expired or is invalid
+ * @throws StorageNotAvailableException if the storage is not available,
+ * which might be temporary
+ * @throws ForbiddenException if the action is not allowed
+ */
+ protected function convertException(Exception $e, string $path = ''): void {
+ }
+
+ #[\Override]
+ public function getDirectoryContent(string $directory): \Traversable {
+ }
+}
diff --git a/tests/stubs/oc_files_storage_storage.php b/tests/stubs/oc_files_storage_storage.php
new file mode 100644
index 000000000..d53b1140c
--- /dev/null
+++ b/tests/stubs/oc_files_storage_storage.php
@@ -0,0 +1,65 @@
+
+ */
+ public function getMetaData(string $path): ?array;
+
+ /**
+ * Get the contents of a directory with metadata
+ *
+ * The metadata array will contain the following fields
+ *
+ * - name
+ * - mimetype
+ * - mtime
+ * - size
+ * - etag
+ * - storage_mtime
+ * - permissions
+ *
+ * @return \Traversable>
+ */
+ public function getDirectoryContent(string $directory): \Traversable;
+}
diff --git a/tests/stubs/oc_files_storage_wrapper_wrapper.php b/tests/stubs/oc_files_storage_wrapper_wrapper.php
new file mode 100644
index 000000000..d4148178c
--- /dev/null
+++ b/tests/stubs/oc_files_storage_wrapper_wrapper.php
@@ -0,0 +1,291 @@
+ $class
+ * @psalm-return T|null
+ */
+ public function getInstanceOfStorage(string $class): ?IStorage {
+ }
+
+ /**
+ * Pass any methods custom to specific storage implementations to the wrapped storage
+ *
+ * @return mixed
+ */
+ public function __call(string $method, array $args) {
+ }
+
+ #[Override]
+ public function getDirectDownload(string $path): array|false {
+ }
+
+ #[Override]
+ public function getDirectDownloadById(string $fileId): array|false {
+ }
+
+ #[\Override]
+ public function getAvailability(): array {
+ }
+
+ #[\Override]
+ public function setAvailability(bool $isAvailable): void {
+ }
+
+ #[\Override]
+ public function verifyPath(string $path, string $fileName): void {
+ }
+
+ #[\Override]
+ public function copyFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
+ }
+
+ #[\Override]
+ public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
+ }
+
+ #[\Override]
+ public function getMetaData(string $path): ?array {
+ }
+
+ #[\Override]
+ public function acquireLock(string $path, int $type, ILockingProvider $provider): void {
+ }
+
+ #[\Override]
+ public function releaseLock(string $path, int $type, ILockingProvider $provider): void {
+ }
+
+ #[\Override]
+ public function changeLock(string $path, int $type, ILockingProvider $provider): void {
+ }
+
+ #[\Override]
+ public function needsPartFile(): bool {
+ }
+
+ #[\Override]
+ public function writeStream(string $path, $stream, ?int $size = null): int {
+ }
+
+ /**
+ * @param resource $stream
+ */
+ protected function writeStreamFallback(string $path, $stream): int {
+ }
+
+ #[\Override]
+ public function getDirectoryContent(string $directory): \Traversable {
+ }
+
+ public function isWrapperOf(IStorage $storage): bool {
+ }
+
+ #[\Override]
+ public function setOwner(?string $user): void {
+ }
+}
diff --git a/tests/stubs/oca_files_event.php b/tests/stubs/oca_files_event.php
deleted file mode 100644
index 18c0503e3..000000000
--- a/tests/stubs/oca_files_event.php
+++ /dev/null
@@ -1,6 +0,0 @@
-
- */
- public function getAllRulesForPaths(int $storageId, array $filePaths): array {
- }
- }
-}
-
-namespace OCA\GroupFolders\Folder {
- class FolderManager {
- public function getFolderAclEnabled(int $id): bool {
- }
- }
-}
diff --git a/tests/stubs/oca_groupfolders_acl_rule.php b/tests/stubs/oca_groupfolders_acl_rule.php
new file mode 100644
index 000000000..64f333ae8
--- /dev/null
+++ b/tests/stubs/oca_groupfolders_acl_rule.php
@@ -0,0 +1,123 @@
+ Constants::PERMISSION_READ,
+ 'write' => Constants::PERMISSION_UPDATE,
+ 'create' => Constants::PERMISSION_CREATE,
+ 'delete' => Constants::PERMISSION_DELETE,
+ 'share' => Constants::PERMISSION_SHARE,
+ ];
+
+ /**
+ * @param int $mask for every permission type a rule can either allow, deny or inherit
+ * these 3 values are stored as 2 bitmaps, one that masks out all inherit values (1 -> set permission, 0 -> inherit)
+ * and one that specifies the permissions to set for non inherited values (1-> allow, 0 -> deny)
+ */
+ public function __construct(
+ private readonly IUserMapping $userMapping,
+ private readonly int $fileId,
+ private int $mask,
+ int $permissions,
+ ) {
+ }
+
+ public function getUserMapping(): IUserMapping {
+ }
+
+ public function getFileId(): int {
+ }
+
+ public function getMask(): int {
+ }
+
+ public function getPermissions(): int {
+ }
+
+ /**
+ * Apply this rule to an existing permission set, returning the resulting permissions
+ *
+ * All permissions included in the current mask will overwrite the existing permissions
+ */
+ public function applyPermissions(int $permissions): int {
+ }
+
+ /**
+ * Apply the deny permissions this rule to an existing permission set, returning the resulting permissions
+ *
+ * Only the deny permissions included in the current mask will overwrite the existing permissions
+ */
+ public function applyDenyPermissions(int $permissions): int {
+ }
+
+ #[\Override]
+ public function xmlSerialize(Writer $writer): void {
+ }
+
+ /**
+ * @return array{
+ * mapping: array{
+ * type: 'user'|'group'|'dummy'|'circle',
+ * id: string,
+ * },
+ * mask: int,
+ * permissions: int,
+ * }
+ */
+ #[\Override]
+ public function jsonSerialize(): array {
+ }
+
+ #[\Override]
+ public static function xmlDeserialize(Reader $reader): Rule {
+ }
+
+ /**
+ * merge multiple rules that apply on the same file where allow overwrites deny
+ * @param Rule[] $rules
+ */
+ public static function mergeRules(array $rules): Rule {
+ }
+
+ /**
+ * apply a new rule on top of the existing
+ *
+ * All non-inherit fields of the new rule will overwrite the current permissions
+ */
+ public function applyRule(Rule $rule): void {
+ }
+
+ /**
+ * Create a default, no-op rule
+ */
+ public static function defaultRule(): Rule {
+ }
+
+ public static function formatRulePermissions(int $mask, int $permissions): string {
+ }
+
+ public function formatPermissions(): string {
+ }
+}
diff --git a/tests/stubs/oca_groupfolders_acl_rulemanager.php b/tests/stubs/oca_groupfolders_acl_rulemanager.php
new file mode 100644
index 000000000..cbb8db56c
--- /dev/null
+++ b/tests/stubs/oca_groupfolders_acl_rulemanager.php
@@ -0,0 +1,75 @@
+
+ */
+ public function getRulesForFilesById(IUser $user, array $fileIds): array {
+ }
+
+ /**
+ * @param string[] $filePaths
+ * @return array
+ */
+ public function getRulesForFilesByPath(IUser $user, int $storageId, array $filePaths): array {
+ }
+
+ /**
+ * @param int[] $fileIds
+ * @return array>
+ */
+ public function getRulesForFilesByIds(IUser $user, array $fileIds): array {
+ }
+
+ /**
+ * @return array
+ */
+ public function getRulesForFilesByParent(IUser $user, int $storageId, int $parentId): array {
+ }
+
+ /**
+ * @param string[] $filePaths
+ * @return array
+ */
+ public function getAllRulesForPaths(int $storageId, array $filePaths): array {
+ }
+
+ /**
+ * @return array
+ */
+ public function getAllRulesForPrefix(int $storageId, string $prefix): array {
+ }
+
+ /**
+ * @return array
+ */
+ public function getRulesForPrefix(IUser $user, int $storageId, string $prefix): array {
+ }
+
+ public function saveRule(Rule $rule): void {
+ }
+
+ public function deleteRule(Rule $rule): void {
+ }
+}
diff --git a/tests/stubs/oca_groupfolders_acl_usermapping_iusermapping.php b/tests/stubs/oca_groupfolders_acl_usermapping_iusermapping.php
new file mode 100644
index 000000000..bf5edd1b3
--- /dev/null
+++ b/tests/stubs/oca_groupfolders_acl_usermapping_iusermapping.php
@@ -0,0 +1,20 @@
+
+ * @throws Exception
+ */
+ public function getAllFolders(): array {
+ }
+
+ /**
+ * @return array
+ * @throws Exception
+ */
+ public function getAllFoldersWithSize(int $offset = 0, ?int $limit = null, string $orderBy = 'mount_point', \SortDirection $order = \SortDirection::Ascending, ?string $mountPoint = null): array {
+ }
+
+ /**
+ * @return array
+ * @throws Exception
+ */
+ public function getAllFoldersForUserWithSize(IUser $user): array {
+ }
+
+ public function getFolder(int $id): ?FolderWithMappingsAndCache {
+ }
+
+ /**
+ * Return just the ACL for the folder.
+ *
+ * @throws Exception
+ */
+ public function getFolderAclEnabled(int $id): bool {
+ }
+
+ public function getFolderByPath(string $path): int {
+ }
+
+ /**
+ * Check if the user is able to configure the advanced folder permissions. This
+ * is the case if the user is an admin, has admin permissions for the group folder
+ * app or is member of a group that can manage permissions for the specific folder.
+ *
+ * @throws Exception
+ */
+ public function canManageACL(int $folderId, IUser $user, bool $excludeAdmins = false): bool {
+ }
+
+ public function mountPointExists(string $mountPoint): bool {
+ }
+
+ /**
+ * @return list
+ * @throws Exception
+ */
+ public function searchGroups(int $id, string $search = ''): array {
+ }
+
+ /**
+ * @return list
+ * @throws Exception
+ */
+ public function searchCircles(int $id, string $search = ''): array {
+ }
+
+ /**
+ * @return list
+ * @throws Exception
+ */
+ public function searchUsers(int $id, string $search = '', int $limit = 10, int $offset = 0): array {
+ }
+
+ /**
+ * @param string[] $groupIds
+ * @param list $paths
+ * @return list
+ * @throws Exception
+ */
+ public function getFoldersForGroups(array $groupIds, ?int $folderId = null, ?array $paths = null): array {
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ * @throws Exception
+ */
+ public function hasFolderForGroup(string $groupId): bool {
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ * @throws Exception
+ */
+ public function hasFolderForCircle(string $circleId): bool {
+ }
+
+ /**
+ * Return all group folders directly assigned to or owned by a circle.
+ *
+ * @return list
+ * @throws Exception
+ */
+ public function getFoldersForCircle(string $circleId): array {
+ }
+
+ /**
+ * @param list $paths
+ * @return list
+ * @throws Exception
+ */
+ public function getFoldersFromCircleMemberships(IUser $user, ?int $folderId = null, ?array $paths = null): array {
+ }
+
+ public function trimMountpoint(string $mountpoint): string {
+ }
+
+ /**
+ * @param array{separate-storage?: bool} $options
+ * @throws Exception
+ */
+ public function createFolder(string $mountPoint, array $options = [], bool $aclDefaultNoPermission = false): int {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function addApplicableGroup(int $folderId, string $groupId): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function removeApplicableGroup(int $folderId, string $groupId): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function setGroupPermissions(int $folderId, string $groupId, int $permissions): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function setManageACL(int $folderId, string $type, string $id, bool $manageAcl): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function removeFolder(int $folderId): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function setFolderQuota(int $folderId, int $quota): void {
+ }
+
+ /**
+ * Update the JSON-encoded options of a folder.
+ *
+ * @param int $folderId
+ * @param array{separate-storage?: bool} $options
+ * @throws Exception
+ */
+ public function setFolderOptions(int $folderId, array $options): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function renameFolder(int $folderId, string $newMountPoint): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function deleteGroup(string $groupId): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function deleteUser(string $userId): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function deleteCircle(string $circleId): void {
+ }
+
+ /**
+ * Look up the team folder that belongs to the given team (circle single id)
+ * via the `team_circle_id` column.
+ *
+ * This is the single source of truth for the "folder belongs to team"
+ * relationship. The legacy `group_folders_groups.circle_id` column tracks
+ * applicable-group membership (a separate concern) and must not be used to
+ * determine team ownership.
+ *
+ * @return int|null The folder id, or null if no folder belongs to this team.
+ */
+ public function getFolderIdByTeamCircleId(string $circleId): ?int {
+ }
+
+ /**
+ * Mark a team folder as belonging to a team by setting the `team_circle_id`
+ * column.
+ */
+ public function setTeamCircleId(int $folderId, string $circleId): void {
+ }
+
+ /**
+ * Whether the folder is assigned exclusively to the given circle. A folder
+ * must have precisely one applicable mapping, and that mapping must be the
+ * target team; group mappings and additional teams make it ineligible.
+ *
+ * @throws Exception
+ */
+ public function isExclusivelyAssignedToCircle(int $folderId, string $circleId): bool {
+ }
+
+ /**
+ * Clear the team ownership of a team folder by resetting the
+ * `team_circle_id` column to null.
+ */
+ public function clearTeamCircleId(int $folderId): void {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function setFolderACL(int $folderId, bool $acl): void {
+ }
+
+ /**
+ * @param list $paths
+ * @return list
+ * @throws Exception
+ */
+ public function getFoldersForUser(IUser $user, ?int $folderId = null, ?array $paths = null): array {
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function getFolderPermissionsForUser(IUser $user, int $folderId): int {
+ }
+
+ /**
+ * returns if the groupId is in fact the singleId of an existing Circle
+ */
+ public function isACircle(string $groupId): bool {
+ }
+
+ /**
+ * returns the Circle from its single Id, or NULL if not available
+ */
+ public function getCircle(string $groupId): ?Circle {
+ }
+
+ public function getCirclesManager(): ?CirclesManager {
+ }
+
+ public function updateOverwriteHomeFolders(): void {
+ }
+
+ public function hasFolderACLDefaultNoPermission(int $folderId): bool {
+ }
+
+ /**
+ * Seed the cache from an already-loaded folder so bulk callers (the mount
+ * provider) skip one `getBasePermission()` lookup per folder.
+ */
+ public function primeAclDefaultNoPermission(FolderDefinition $folder): void {
+ }
+
+ public function countAllFolders(): int {
+ }
+}
diff --git a/tests/stubs/oca_groupfolders_mount_groupmountpoint.php b/tests/stubs/oca_groupfolders_mount_groupmountpoint.php
new file mode 100644
index 000000000..631777799
--- /dev/null
+++ b/tests/stubs/oca_groupfolders_mount_groupmountpoint.php
@@ -0,0 +1,47 @@
+ $arguments
+ * @param ?array $mountOptions
+ */
+ public function __construct(
+ private readonly FolderDefinition $folder,
+ IStorage $storage,
+ string $mountpoint,
+ ?array $arguments = null,
+ ?IStorageFactory $loader = null,
+ ?array $mountOptions = null,
+ ?int $mountId = null,
+ ?int $rootId = null,
+ ) {
+ }
+
+ #[\Override]
+ public function getMountType(): string {
+ }
+
+ public function getFolderId(): int {
+ }
+
+ public function getFolder(): FolderDefinition {
+ }
+
+ public function getSourcePath(): string {
+ }
+}
diff --git a/tests/stubs/oca_theming_defaults.php b/tests/stubs/oca_theming_defaults.php
deleted file mode 100644
index f3f4c5e6b..000000000
--- a/tests/stubs/oca_theming_defaults.php
+++ /dev/null
@@ -1,21 +0,0 @@
-next();
+ *
+ * $reader->parseInnerTree() will parse the entire sub-tree, and advance to
+ * the next element.
+ */
+ public static function xmlDeserialize(Reader $reader);
+}
diff --git a/tests/stubs/sabre_xml_xmlserializable.php b/tests/stubs/sabre_xml_xmlserializable.php
new file mode 100644
index 000000000..fef237ffe
--- /dev/null
+++ b/tests/stubs/sabre_xml_xmlserializable.php
@@ -0,0 +1,33 @@
+commandLine
+ *
+ * Bash equivalent: COMP_POINT
+ *
+ * @var int
+ */
+ protected $charIndex = 0;
+
+ /**
+ * An array of the individual words in the current command line.
+ *
+ * This is not set until $this->splitCommand() is called, when it is populated by
+ * $commandLine exploded by $wordBreaks
+ *
+ * Bash equivalent: COMP_WORDS
+ *
+ * @var string[]|null
+ */
+ protected $words = null;
+
+ /**
+ * Words from the currently command-line before quotes and escaping is processed
+ *
+ * This is indexed the same as $this->words, but in their raw input terms are in their input form, including
+ * quotes and escaping.
+ *
+ * @var string[]|null
+ */
+ protected $rawWords = null;
+
+ /**
+ * The index in $this->words containing the word at the current cursor position.
+ *
+ * This is not set until $this->splitCommand() is called.
+ *
+ * Bash equivalent: COMP_CWORD
+ *
+ * @var int|null
+ */
+ protected $wordIndex = null;
+
+ /**
+ * Characters that $this->commandLine should be split on to get a list of individual words
+ *
+ * Bash equivalent: COMP_WORDBREAKS
+ *
+ * @var string
+ */
+ protected $wordBreaks = "= \t\n";
+
+ /**
+ * Set the whole contents of the command line as a string
+ *
+ * @param string $commandLine
+ */
+ public function setCommandLine($commandLine) {
+ }
+
+ /**
+ * Return the current command line verbatim as a string
+ *
+ * @return string
+ */
+ public function getCommandLine() {
+ }
+
+ /**
+ * Return the word from the command line that the cursor is currently in
+ *
+ * Most of the time this will be a partial word. If the cursor has a space before it,
+ * this will return an empty string, indicating a new word.
+ *
+ * @return string
+ */
+ public function getCurrentWord() {
+ }
+
+ /**
+ * Return the unprocessed string for the word under the cursor
+ *
+ * This preserves any quotes and escaping that are present in the input command line.
+ *
+ * @return string
+ */
+ public function getRawCurrentWord() {
+ }
+
+ /**
+ * Return a word by index from the command line
+ *
+ * @see $words, $wordBreaks
+ * @param int $index
+ * @return string
+ */
+ public function getWordAtIndex($index) {
+ }
+
+ /**
+ * Get the contents of the command line, exploded into words based on the configured word break characters
+ *
+ * @see $wordBreaks, setWordBreaks
+ * @return array
+ */
+ public function getWords() {
+ }
+
+ /**
+ * Get the unprocessed/literal words from the command line
+ *
+ * This is indexed the same as getWords(), but preserves any quoting and escaping from the command line
+ *
+ * @return string[]
+ */
+ public function getRawWords() {
+ }
+
+ /**
+ * Get the index of the word the cursor is currently in
+ *
+ * @see getWords, getCurrentWord
+ * @return int
+ */
+ public function getWordIndex() {
+ }
+
+ /**
+ * Get the character index of the user's cursor on the command line
+ *
+ * This is in the context of the full command line string, so includes word break characters.
+ * Note that some shells can only provide an approximation for character index. Under ZSH for
+ * example, this will always be the character at the start of the current word.
+ *
+ * @return int
+ */
+ public function getCharIndex() {
+ }
+
+ /**
+ * Set the cursor position as a character index relative to the start of the command line
+ *
+ * @param int $index
+ */
+ public function setCharIndex($index) {
+ }
+
+ /**
+ * Set characters to use as split points when breaking the command line into words
+ *
+ * This defaults to a sane value based on BASH's word break characters and shouldn't
+ * need to be changed unless your completions contain the default word break characters.
+ *
+ * @deprecated This is becoming an internal setting that doesn't make sense to expose publicly.
+ *
+ * @see wordBreaks
+ * @param string $charList - a single string containing all of the characters to break words on
+ */
+ public function setWordBreaks($charList) {
+ }
+
+ /**
+ * Split the command line into words using the configured word break characters
+ *
+ * @return string[]
+ */
+ protected function splitCommand() {
+ }
+
+ /**
+ * Return a token's value with escaping and quotes removed
+ *
+ * @see self::tokenizeString()
+ * @param array $token
+ * @return string
+ */
+ protected function getTokenValue($token) {
+ }
+
+ /**
+ * Break a string into words, quoted strings and non-words (breaks)
+ *
+ * Returns an array of unmodified segments of $string with offset and type information.
+ *
+ * @param string $string
+ * @return array as [ [type => string, value => string, offset => int], ... ]
+ */
+ protected function tokenizeString($string) {
+ }
+
+ /**
+ * Reset the computed words so that $this->splitWords is forced to run again
+ */
+ protected function reset() {
+ }
+}