@@ -44,200 +44,200 @@ |
||
44 | 44 | |
45 | 45 | class ServerFactory { |
46 | 46 | |
47 | - public function __construct( |
|
48 | - private IConfig $config, |
|
49 | - private LoggerInterface $logger, |
|
50 | - private IDBConnection $databaseConnection, |
|
51 | - private IUserSession $userSession, |
|
52 | - private IMountManager $mountManager, |
|
53 | - private ITagManager $tagManager, |
|
54 | - private IRequest $request, |
|
55 | - private IPreview $previewManager, |
|
56 | - private IEventDispatcher $eventDispatcher, |
|
57 | - private IL10N $l10n, |
|
58 | - ) { |
|
59 | - } |
|
60 | - |
|
61 | - /** |
|
62 | - * @param callable $viewCallBack callback that should return the view for the dav endpoint |
|
63 | - */ |
|
64 | - public function createServer( |
|
65 | - bool $isPublicShare, |
|
66 | - string $baseUri, |
|
67 | - string $requestUri, |
|
68 | - Plugin $authPlugin, |
|
69 | - callable $viewCallBack, |
|
70 | - ): Server { |
|
71 | - // Fire up server |
|
72 | - if ($isPublicShare) { |
|
73 | - $rootCollection = new SimpleCollection('root'); |
|
74 | - $tree = new CachingTree($rootCollection); |
|
75 | - } else { |
|
76 | - $rootCollection = null; |
|
77 | - $tree = new ObjectTree(); |
|
78 | - } |
|
79 | - $server = new Server($tree); |
|
80 | - // Set URL explicitly due to reverse-proxy situations |
|
81 | - $server->httpRequest->setUrl($requestUri); |
|
82 | - $server->setBaseUri($baseUri); |
|
83 | - |
|
84 | - // Load plugins |
|
85 | - $server->addPlugin(new MaintenancePlugin($this->config, $this->l10n)); |
|
86 | - $server->addPlugin(new BlockLegacyClientPlugin( |
|
87 | - $this->config, |
|
88 | - \OCP\Server::get(ThemingDefaults::class), |
|
89 | - )); |
|
90 | - $server->addPlugin(new AnonymousOptionsPlugin()); |
|
91 | - $server->addPlugin($authPlugin); |
|
92 | - // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / |
|
93 | - $server->addPlugin(new DummyGetResponsePlugin()); |
|
94 | - $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger)); |
|
95 | - $server->addPlugin(new LockPlugin()); |
|
96 | - |
|
97 | - $server->addPlugin(new RequestIdHeaderPlugin($this->request)); |
|
98 | - |
|
99 | - $server->addPlugin(new ZipFolderPlugin( |
|
100 | - $tree, |
|
101 | - $this->logger, |
|
102 | - $this->eventDispatcher, |
|
103 | - )); |
|
104 | - |
|
105 | - // Some WebDAV clients do require Class 2 WebDAV support (locking), since |
|
106 | - // we do not provide locking we emulate it using a fake locking plugin. |
|
107 | - if ($this->request->isUserAgent([ |
|
108 | - '/WebDAVFS/', |
|
109 | - '/OneNote/', |
|
110 | - '/Microsoft-WebDAV-MiniRedir/', |
|
111 | - ])) { |
|
112 | - $server->addPlugin(new FakeLockerPlugin()); |
|
113 | - } |
|
114 | - |
|
115 | - if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) { |
|
116 | - $server->addPlugin(new BrowserErrorPagePlugin()); |
|
117 | - } |
|
118 | - |
|
119 | - // wait with registering these until auth is handled and the filesystem is setup |
|
120 | - $server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void { |
|
121 | - // ensure the skeleton is copied |
|
122 | - $userFolder = \OC::$server->getUserFolder(); |
|
123 | - |
|
124 | - /** @var View $view */ |
|
125 | - $view = $viewCallBack($server); |
|
126 | - if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) { |
|
127 | - $rootInfo = $userFolder; |
|
128 | - } else { |
|
129 | - $rootInfo = $view->getFileInfo(''); |
|
130 | - } |
|
131 | - |
|
132 | - // Create Nextcloud Dir |
|
133 | - if ($rootInfo->getType() === 'dir') { |
|
134 | - $root = new Directory($view, $rootInfo, $tree); |
|
135 | - } else { |
|
136 | - $root = new File($view, $rootInfo); |
|
137 | - } |
|
138 | - |
|
139 | - if ($isPublicShare) { |
|
140 | - $userPrincipalBackend = new Principal( |
|
141 | - \OCP\Server::get(IUserManager::class), |
|
142 | - \OCP\Server::get(IGroupManager::class), |
|
143 | - \OCP\Server::get(IAccountManager::class), |
|
144 | - \OCP\Server::get(\OCP\Share\IManager::class), |
|
145 | - \OCP\Server::get(IUserSession::class), |
|
146 | - \OCP\Server::get(IAppManager::class), |
|
147 | - \OCP\Server::get(ProxyMapper::class), |
|
148 | - \OCP\Server::get(KnownUserService::class), |
|
149 | - \OCP\Server::get(IConfig::class), |
|
150 | - \OC::$server->getL10NFactory(), |
|
151 | - ); |
|
152 | - |
|
153 | - // Mount the share collection at /public.php/dav/shares/<share token> |
|
154 | - $rootCollection->addChild(new RootCollection( |
|
155 | - $root, |
|
156 | - $userPrincipalBackend, |
|
157 | - 'principals/shares', |
|
158 | - )); |
|
159 | - |
|
160 | - // Mount the upload collection at /public.php/dav/uploads/<share token> |
|
161 | - $rootCollection->addChild(new \OCA\DAV\Upload\RootCollection( |
|
162 | - $userPrincipalBackend, |
|
163 | - 'principals/shares', |
|
164 | - \OCP\Server::get(CleanupService::class), |
|
165 | - \OCP\Server::get(IRootFolder::class), |
|
166 | - \OCP\Server::get(IUserSession::class), |
|
167 | - \OCP\Server::get(\OCP\Share\IManager::class), |
|
168 | - )); |
|
169 | - } else { |
|
170 | - /** @var ObjectTree $tree */ |
|
171 | - $tree->init($root, $view, $this->mountManager); |
|
172 | - } |
|
173 | - |
|
174 | - $server->addPlugin( |
|
175 | - new FilesPlugin( |
|
176 | - $tree, |
|
177 | - $this->config, |
|
178 | - $this->request, |
|
179 | - $this->previewManager, |
|
180 | - $this->userSession, |
|
181 | - \OCP\Server::get(IFilenameValidator::class), |
|
182 | - \OCP\Server::get(IAccountManager::class), |
|
183 | - false, |
|
184 | - !$this->config->getSystemValue('debug', false) |
|
185 | - ) |
|
186 | - ); |
|
187 | - $server->addPlugin(new QuotaPlugin($view, true)); |
|
188 | - $server->addPlugin(new ChecksumUpdatePlugin()); |
|
189 | - |
|
190 | - // Allow view-only plugin for webdav requests |
|
191 | - $server->addPlugin(new ViewOnlyPlugin( |
|
192 | - $userFolder, |
|
193 | - )); |
|
194 | - |
|
195 | - if ($this->userSession->isLoggedIn()) { |
|
196 | - $server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession)); |
|
197 | - $server->addPlugin(new SharesPlugin( |
|
198 | - $tree, |
|
199 | - $this->userSession, |
|
200 | - $userFolder, |
|
201 | - \OCP\Server::get(\OCP\Share\IManager::class) |
|
202 | - )); |
|
203 | - $server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession)); |
|
204 | - $server->addPlugin(new FilesReportPlugin( |
|
205 | - $tree, |
|
206 | - $view, |
|
207 | - \OCP\Server::get(ISystemTagManager::class), |
|
208 | - \OCP\Server::get(ISystemTagObjectMapper::class), |
|
209 | - \OCP\Server::get(ITagManager::class), |
|
210 | - $this->userSession, |
|
211 | - \OCP\Server::get(IGroupManager::class), |
|
212 | - $userFolder, |
|
213 | - \OCP\Server::get(IAppManager::class) |
|
214 | - )); |
|
215 | - // custom properties plugin must be the last one |
|
216 | - $server->addPlugin( |
|
217 | - new \Sabre\DAV\PropertyStorage\Plugin( |
|
218 | - new CustomPropertiesBackend( |
|
219 | - $server, |
|
220 | - $tree, |
|
221 | - $this->databaseConnection, |
|
222 | - $this->userSession->getUser(), |
|
223 | - \OCP\Server::get(DefaultCalendarValidator::class), |
|
224 | - ) |
|
225 | - ) |
|
226 | - ); |
|
227 | - } |
|
228 | - $server->addPlugin(new CopyEtagHeaderPlugin()); |
|
229 | - |
|
230 | - // Load dav plugins from apps |
|
231 | - $event = new SabrePluginEvent($server); |
|
232 | - $this->eventDispatcher->dispatchTyped($event); |
|
233 | - $pluginManager = new PluginManager( |
|
234 | - \OC::$server, |
|
235 | - \OCP\Server::get(IAppManager::class) |
|
236 | - ); |
|
237 | - foreach ($pluginManager->getAppPlugins() as $appPlugin) { |
|
238 | - $server->addPlugin($appPlugin); |
|
239 | - } |
|
240 | - }, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request |
|
241 | - return $server; |
|
242 | - } |
|
47 | + public function __construct( |
|
48 | + private IConfig $config, |
|
49 | + private LoggerInterface $logger, |
|
50 | + private IDBConnection $databaseConnection, |
|
51 | + private IUserSession $userSession, |
|
52 | + private IMountManager $mountManager, |
|
53 | + private ITagManager $tagManager, |
|
54 | + private IRequest $request, |
|
55 | + private IPreview $previewManager, |
|
56 | + private IEventDispatcher $eventDispatcher, |
|
57 | + private IL10N $l10n, |
|
58 | + ) { |
|
59 | + } |
|
60 | + |
|
61 | + /** |
|
62 | + * @param callable $viewCallBack callback that should return the view for the dav endpoint |
|
63 | + */ |
|
64 | + public function createServer( |
|
65 | + bool $isPublicShare, |
|
66 | + string $baseUri, |
|
67 | + string $requestUri, |
|
68 | + Plugin $authPlugin, |
|
69 | + callable $viewCallBack, |
|
70 | + ): Server { |
|
71 | + // Fire up server |
|
72 | + if ($isPublicShare) { |
|
73 | + $rootCollection = new SimpleCollection('root'); |
|
74 | + $tree = new CachingTree($rootCollection); |
|
75 | + } else { |
|
76 | + $rootCollection = null; |
|
77 | + $tree = new ObjectTree(); |
|
78 | + } |
|
79 | + $server = new Server($tree); |
|
80 | + // Set URL explicitly due to reverse-proxy situations |
|
81 | + $server->httpRequest->setUrl($requestUri); |
|
82 | + $server->setBaseUri($baseUri); |
|
83 | + |
|
84 | + // Load plugins |
|
85 | + $server->addPlugin(new MaintenancePlugin($this->config, $this->l10n)); |
|
86 | + $server->addPlugin(new BlockLegacyClientPlugin( |
|
87 | + $this->config, |
|
88 | + \OCP\Server::get(ThemingDefaults::class), |
|
89 | + )); |
|
90 | + $server->addPlugin(new AnonymousOptionsPlugin()); |
|
91 | + $server->addPlugin($authPlugin); |
|
92 | + // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / |
|
93 | + $server->addPlugin(new DummyGetResponsePlugin()); |
|
94 | + $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger)); |
|
95 | + $server->addPlugin(new LockPlugin()); |
|
96 | + |
|
97 | + $server->addPlugin(new RequestIdHeaderPlugin($this->request)); |
|
98 | + |
|
99 | + $server->addPlugin(new ZipFolderPlugin( |
|
100 | + $tree, |
|
101 | + $this->logger, |
|
102 | + $this->eventDispatcher, |
|
103 | + )); |
|
104 | + |
|
105 | + // Some WebDAV clients do require Class 2 WebDAV support (locking), since |
|
106 | + // we do not provide locking we emulate it using a fake locking plugin. |
|
107 | + if ($this->request->isUserAgent([ |
|
108 | + '/WebDAVFS/', |
|
109 | + '/OneNote/', |
|
110 | + '/Microsoft-WebDAV-MiniRedir/', |
|
111 | + ])) { |
|
112 | + $server->addPlugin(new FakeLockerPlugin()); |
|
113 | + } |
|
114 | + |
|
115 | + if (BrowserErrorPagePlugin::isBrowserRequest($this->request)) { |
|
116 | + $server->addPlugin(new BrowserErrorPagePlugin()); |
|
117 | + } |
|
118 | + |
|
119 | + // wait with registering these until auth is handled and the filesystem is setup |
|
120 | + $server->on('beforeMethod:*', function () use ($server, $tree, $viewCallBack, $isPublicShare, $rootCollection): void { |
|
121 | + // ensure the skeleton is copied |
|
122 | + $userFolder = \OC::$server->getUserFolder(); |
|
123 | + |
|
124 | + /** @var View $view */ |
|
125 | + $view = $viewCallBack($server); |
|
126 | + if ($userFolder instanceof Folder && $userFolder->getPath() === $view->getRoot()) { |
|
127 | + $rootInfo = $userFolder; |
|
128 | + } else { |
|
129 | + $rootInfo = $view->getFileInfo(''); |
|
130 | + } |
|
131 | + |
|
132 | + // Create Nextcloud Dir |
|
133 | + if ($rootInfo->getType() === 'dir') { |
|
134 | + $root = new Directory($view, $rootInfo, $tree); |
|
135 | + } else { |
|
136 | + $root = new File($view, $rootInfo); |
|
137 | + } |
|
138 | + |
|
139 | + if ($isPublicShare) { |
|
140 | + $userPrincipalBackend = new Principal( |
|
141 | + \OCP\Server::get(IUserManager::class), |
|
142 | + \OCP\Server::get(IGroupManager::class), |
|
143 | + \OCP\Server::get(IAccountManager::class), |
|
144 | + \OCP\Server::get(\OCP\Share\IManager::class), |
|
145 | + \OCP\Server::get(IUserSession::class), |
|
146 | + \OCP\Server::get(IAppManager::class), |
|
147 | + \OCP\Server::get(ProxyMapper::class), |
|
148 | + \OCP\Server::get(KnownUserService::class), |
|
149 | + \OCP\Server::get(IConfig::class), |
|
150 | + \OC::$server->getL10NFactory(), |
|
151 | + ); |
|
152 | + |
|
153 | + // Mount the share collection at /public.php/dav/shares/<share token> |
|
154 | + $rootCollection->addChild(new RootCollection( |
|
155 | + $root, |
|
156 | + $userPrincipalBackend, |
|
157 | + 'principals/shares', |
|
158 | + )); |
|
159 | + |
|
160 | + // Mount the upload collection at /public.php/dav/uploads/<share token> |
|
161 | + $rootCollection->addChild(new \OCA\DAV\Upload\RootCollection( |
|
162 | + $userPrincipalBackend, |
|
163 | + 'principals/shares', |
|
164 | + \OCP\Server::get(CleanupService::class), |
|
165 | + \OCP\Server::get(IRootFolder::class), |
|
166 | + \OCP\Server::get(IUserSession::class), |
|
167 | + \OCP\Server::get(\OCP\Share\IManager::class), |
|
168 | + )); |
|
169 | + } else { |
|
170 | + /** @var ObjectTree $tree */ |
|
171 | + $tree->init($root, $view, $this->mountManager); |
|
172 | + } |
|
173 | + |
|
174 | + $server->addPlugin( |
|
175 | + new FilesPlugin( |
|
176 | + $tree, |
|
177 | + $this->config, |
|
178 | + $this->request, |
|
179 | + $this->previewManager, |
|
180 | + $this->userSession, |
|
181 | + \OCP\Server::get(IFilenameValidator::class), |
|
182 | + \OCP\Server::get(IAccountManager::class), |
|
183 | + false, |
|
184 | + !$this->config->getSystemValue('debug', false) |
|
185 | + ) |
|
186 | + ); |
|
187 | + $server->addPlugin(new QuotaPlugin($view, true)); |
|
188 | + $server->addPlugin(new ChecksumUpdatePlugin()); |
|
189 | + |
|
190 | + // Allow view-only plugin for webdav requests |
|
191 | + $server->addPlugin(new ViewOnlyPlugin( |
|
192 | + $userFolder, |
|
193 | + )); |
|
194 | + |
|
195 | + if ($this->userSession->isLoggedIn()) { |
|
196 | + $server->addPlugin(new TagsPlugin($tree, $this->tagManager, $this->eventDispatcher, $this->userSession)); |
|
197 | + $server->addPlugin(new SharesPlugin( |
|
198 | + $tree, |
|
199 | + $this->userSession, |
|
200 | + $userFolder, |
|
201 | + \OCP\Server::get(\OCP\Share\IManager::class) |
|
202 | + )); |
|
203 | + $server->addPlugin(new CommentPropertiesPlugin(\OCP\Server::get(ICommentsManager::class), $this->userSession)); |
|
204 | + $server->addPlugin(new FilesReportPlugin( |
|
205 | + $tree, |
|
206 | + $view, |
|
207 | + \OCP\Server::get(ISystemTagManager::class), |
|
208 | + \OCP\Server::get(ISystemTagObjectMapper::class), |
|
209 | + \OCP\Server::get(ITagManager::class), |
|
210 | + $this->userSession, |
|
211 | + \OCP\Server::get(IGroupManager::class), |
|
212 | + $userFolder, |
|
213 | + \OCP\Server::get(IAppManager::class) |
|
214 | + )); |
|
215 | + // custom properties plugin must be the last one |
|
216 | + $server->addPlugin( |
|
217 | + new \Sabre\DAV\PropertyStorage\Plugin( |
|
218 | + new CustomPropertiesBackend( |
|
219 | + $server, |
|
220 | + $tree, |
|
221 | + $this->databaseConnection, |
|
222 | + $this->userSession->getUser(), |
|
223 | + \OCP\Server::get(DefaultCalendarValidator::class), |
|
224 | + ) |
|
225 | + ) |
|
226 | + ); |
|
227 | + } |
|
228 | + $server->addPlugin(new CopyEtagHeaderPlugin()); |
|
229 | + |
|
230 | + // Load dav plugins from apps |
|
231 | + $event = new SabrePluginEvent($server); |
|
232 | + $this->eventDispatcher->dispatchTyped($event); |
|
233 | + $pluginManager = new PluginManager( |
|
234 | + \OC::$server, |
|
235 | + \OCP\Server::get(IAppManager::class) |
|
236 | + ); |
|
237 | + foreach ($pluginManager->getAppPlugins() as $appPlugin) { |
|
238 | + $server->addPlugin($appPlugin); |
|
239 | + } |
|
240 | + }, 30); // priority 30: after auth (10) and acl(20), before lock(50) and handling the request |
|
241 | + return $server; |
|
242 | + } |
|
243 | 243 | } |
@@ -25,469 +25,469 @@ |
||
25 | 25 | |
26 | 26 | abstract class AbstractPrincipalBackend implements BackendInterface { |
27 | 27 | |
28 | - /** @var string */ |
|
29 | - private $dbTableName; |
|
30 | - |
|
31 | - /** @var string */ |
|
32 | - private $dbMetaDataTableName; |
|
33 | - |
|
34 | - /** @var string */ |
|
35 | - private $dbForeignKeyName; |
|
36 | - |
|
37 | - public function __construct( |
|
38 | - private IDBConnection $db, |
|
39 | - private IUserSession $userSession, |
|
40 | - private IGroupManager $groupManager, |
|
41 | - private LoggerInterface $logger, |
|
42 | - private ProxyMapper $proxyMapper, |
|
43 | - private string $principalPrefix, |
|
44 | - string $dbPrefix, |
|
45 | - private string $cuType, |
|
46 | - ) { |
|
47 | - $this->dbTableName = 'calendar_' . $dbPrefix . 's'; |
|
48 | - $this->dbMetaDataTableName = $this->dbTableName . '_md'; |
|
49 | - $this->dbForeignKeyName = $dbPrefix . '_id'; |
|
50 | - } |
|
51 | - |
|
52 | - use PrincipalProxyTrait; |
|
53 | - |
|
54 | - /** |
|
55 | - * Returns a list of principals based on a prefix. |
|
56 | - * |
|
57 | - * This prefix will often contain something like 'principals'. You are only |
|
58 | - * expected to return principals that are in this base path. |
|
59 | - * |
|
60 | - * You are expected to return at least a 'uri' for every user, you can |
|
61 | - * return any additional properties if you wish so. Common properties are: |
|
62 | - * {DAV:}displayname |
|
63 | - * |
|
64 | - * @param string $prefixPath |
|
65 | - * @return string[] |
|
66 | - */ |
|
67 | - public function getPrincipalsByPrefix($prefixPath): array { |
|
68 | - $principals = []; |
|
69 | - |
|
70 | - if ($prefixPath === $this->principalPrefix) { |
|
71 | - $query = $this->db->getQueryBuilder(); |
|
72 | - $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname']) |
|
73 | - ->from($this->dbTableName); |
|
74 | - $stmt = $query->execute(); |
|
75 | - |
|
76 | - $metaDataQuery = $this->db->getQueryBuilder(); |
|
77 | - $metaDataQuery->select([$this->dbForeignKeyName, 'key', 'value']) |
|
78 | - ->from($this->dbMetaDataTableName); |
|
79 | - $metaDataStmt = $metaDataQuery->execute(); |
|
80 | - $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC); |
|
81 | - |
|
82 | - $metaDataById = []; |
|
83 | - foreach ($metaDataRows as $metaDataRow) { |
|
84 | - if (!isset($metaDataById[$metaDataRow[$this->dbForeignKeyName]])) { |
|
85 | - $metaDataById[$metaDataRow[$this->dbForeignKeyName]] = []; |
|
86 | - } |
|
87 | - |
|
88 | - $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] = |
|
89 | - $metaDataRow['value']; |
|
90 | - } |
|
91 | - |
|
92 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
93 | - $id = $row['id']; |
|
94 | - |
|
95 | - if (isset($metaDataById[$id])) { |
|
96 | - $principals[] = $this->rowToPrincipal($row, $metaDataById[$id]); |
|
97 | - } else { |
|
98 | - $principals[] = $this->rowToPrincipal($row); |
|
99 | - } |
|
100 | - } |
|
101 | - |
|
102 | - $stmt->closeCursor(); |
|
103 | - } |
|
104 | - |
|
105 | - return $principals; |
|
106 | - } |
|
107 | - |
|
108 | - /** |
|
109 | - * Returns a specific principal, specified by its path. |
|
110 | - * The returned structure should be the exact same as from |
|
111 | - * getPrincipalsByPrefix. |
|
112 | - * |
|
113 | - * @param string $prefixPath |
|
114 | - * |
|
115 | - * @return array |
|
116 | - */ |
|
117 | - public function getPrincipalByPath($path) { |
|
118 | - if (!str_starts_with($path, $this->principalPrefix)) { |
|
119 | - return null; |
|
120 | - } |
|
121 | - [, $name] = \Sabre\Uri\split($path); |
|
122 | - |
|
123 | - [$backendId, $resourceId] = explode('-', $name, 2); |
|
124 | - |
|
125 | - $query = $this->db->getQueryBuilder(); |
|
126 | - $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname']) |
|
127 | - ->from($this->dbTableName) |
|
128 | - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) |
|
129 | - ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); |
|
130 | - $stmt = $query->execute(); |
|
131 | - $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
132 | - |
|
133 | - if (!$row) { |
|
134 | - return null; |
|
135 | - } |
|
136 | - |
|
137 | - $metaDataQuery = $this->db->getQueryBuilder(); |
|
138 | - $metaDataQuery->select(['key', 'value']) |
|
139 | - ->from($this->dbMetaDataTableName) |
|
140 | - ->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id']))); |
|
141 | - $metaDataStmt = $metaDataQuery->execute(); |
|
142 | - $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC); |
|
143 | - $metadata = []; |
|
144 | - |
|
145 | - foreach ($metaDataRows as $metaDataRow) { |
|
146 | - $metadata[$metaDataRow['key']] = $metaDataRow['value']; |
|
147 | - } |
|
148 | - |
|
149 | - return $this->rowToPrincipal($row, $metadata); |
|
150 | - } |
|
151 | - |
|
152 | - /** |
|
153 | - * @param int $id |
|
154 | - * @return string[]|null |
|
155 | - */ |
|
156 | - public function getPrincipalById($id): ?array { |
|
157 | - $query = $this->db->getQueryBuilder(); |
|
158 | - $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname']) |
|
159 | - ->from($this->dbTableName) |
|
160 | - ->where($query->expr()->eq('id', $query->createNamedParameter($id))); |
|
161 | - $stmt = $query->execute(); |
|
162 | - $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
163 | - |
|
164 | - if (!$row) { |
|
165 | - return null; |
|
166 | - } |
|
167 | - |
|
168 | - $metaDataQuery = $this->db->getQueryBuilder(); |
|
169 | - $metaDataQuery->select(['key', 'value']) |
|
170 | - ->from($this->dbMetaDataTableName) |
|
171 | - ->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id']))); |
|
172 | - $metaDataStmt = $metaDataQuery->execute(); |
|
173 | - $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC); |
|
174 | - $metadata = []; |
|
175 | - |
|
176 | - foreach ($metaDataRows as $metaDataRow) { |
|
177 | - $metadata[$metaDataRow['key']] = $metaDataRow['value']; |
|
178 | - } |
|
179 | - |
|
180 | - return $this->rowToPrincipal($row, $metadata); |
|
181 | - } |
|
182 | - |
|
183 | - /** |
|
184 | - * @param string $path |
|
185 | - * @param PropPatch $propPatch |
|
186 | - * @return int |
|
187 | - */ |
|
188 | - public function updatePrincipal($path, PropPatch $propPatch): int { |
|
189 | - return 0; |
|
190 | - } |
|
191 | - |
|
192 | - /** |
|
193 | - * @param string $prefixPath |
|
194 | - * @param string $test |
|
195 | - * |
|
196 | - * @return array |
|
197 | - */ |
|
198 | - public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { |
|
199 | - $results = []; |
|
200 | - if (\count($searchProperties) === 0) { |
|
201 | - return []; |
|
202 | - } |
|
203 | - if ($prefixPath !== $this->principalPrefix) { |
|
204 | - return []; |
|
205 | - } |
|
206 | - |
|
207 | - $user = $this->userSession->getUser(); |
|
208 | - if (!$user) { |
|
209 | - return []; |
|
210 | - } |
|
211 | - $usersGroups = $this->groupManager->getUserGroupIds($user); |
|
212 | - |
|
213 | - foreach ($searchProperties as $prop => $value) { |
|
214 | - switch ($prop) { |
|
215 | - case '{http://sabredav.org/ns}email-address': |
|
216 | - $query = $this->db->getQueryBuilder(); |
|
217 | - $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
218 | - ->from($this->dbTableName) |
|
219 | - ->where($query->expr()->iLike('email', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
220 | - |
|
221 | - $stmt = $query->execute(); |
|
222 | - $principals = []; |
|
223 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
224 | - if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
225 | - continue; |
|
226 | - } |
|
227 | - $principals[] = $this->rowToPrincipal($row)['uri']; |
|
228 | - } |
|
229 | - $results[] = $principals; |
|
230 | - |
|
231 | - $stmt->closeCursor(); |
|
232 | - break; |
|
233 | - |
|
234 | - case '{DAV:}displayname': |
|
235 | - $query = $this->db->getQueryBuilder(); |
|
236 | - $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
237 | - ->from($this->dbTableName) |
|
238 | - ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
239 | - |
|
240 | - $stmt = $query->execute(); |
|
241 | - $principals = []; |
|
242 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
243 | - if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
244 | - continue; |
|
245 | - } |
|
246 | - $principals[] = $this->rowToPrincipal($row)['uri']; |
|
247 | - } |
|
248 | - $results[] = $principals; |
|
249 | - |
|
250 | - $stmt->closeCursor(); |
|
251 | - break; |
|
252 | - |
|
253 | - case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set': |
|
254 | - // If you add support for more search properties that qualify as a user-address, |
|
255 | - // please also add them to the array below |
|
256 | - $results[] = $this->searchPrincipals($this->principalPrefix, [ |
|
257 | - '{http://sabredav.org/ns}email-address' => $value, |
|
258 | - ], 'anyof'); |
|
259 | - break; |
|
260 | - |
|
261 | - case IRoomMetadata::FEATURES: |
|
262 | - $results[] = $this->searchPrincipalsByRoomFeature($prop, $value); |
|
263 | - break; |
|
264 | - |
|
265 | - case IRoomMetadata::CAPACITY: |
|
266 | - case IResourceMetadata::VEHICLE_SEATING_CAPACITY: |
|
267 | - $results[] = $this->searchPrincipalsByCapacity($prop, $value); |
|
268 | - break; |
|
269 | - |
|
270 | - default: |
|
271 | - $results[] = $this->searchPrincipalsByMetadataKey($prop, $value, $usersGroups); |
|
272 | - break; |
|
273 | - } |
|
274 | - } |
|
275 | - |
|
276 | - // results is an array of arrays, so this is not the first search result |
|
277 | - // but the results of the first searchProperty |
|
278 | - if (count($results) === 1) { |
|
279 | - return $results[0]; |
|
280 | - } |
|
281 | - |
|
282 | - switch ($test) { |
|
283 | - case 'anyof': |
|
284 | - return array_values(array_unique(array_merge(...$results))); |
|
285 | - |
|
286 | - case 'allof': |
|
287 | - default: |
|
288 | - return array_values(array_intersect(...$results)); |
|
289 | - } |
|
290 | - } |
|
291 | - |
|
292 | - /** |
|
293 | - * @param string $key |
|
294 | - * @return IQueryBuilder |
|
295 | - */ |
|
296 | - private function getMetadataQuery(string $key): IQueryBuilder { |
|
297 | - $query = $this->db->getQueryBuilder(); |
|
298 | - $query->select([$this->dbForeignKeyName]) |
|
299 | - ->from($this->dbMetaDataTableName) |
|
300 | - ->where($query->expr()->eq('key', $query->createNamedParameter($key))); |
|
301 | - return $query; |
|
302 | - } |
|
303 | - |
|
304 | - /** |
|
305 | - * Searches principals based on their metadata keys. |
|
306 | - * This allows to search for all principals with a specific key. |
|
307 | - * e.g.: |
|
308 | - * '{http://nextcloud.com/ns}room-building-address' => 'ABC Street 123, ...' |
|
309 | - * |
|
310 | - * @param string $key |
|
311 | - * @param string $value |
|
312 | - * @param string[] $usersGroups |
|
313 | - * @return string[] |
|
314 | - */ |
|
315 | - private function searchPrincipalsByMetadataKey(string $key, string $value, array $usersGroups = []): array { |
|
316 | - $query = $this->getMetadataQuery($key); |
|
317 | - $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
318 | - return $this->getRows($query, $usersGroups); |
|
319 | - } |
|
320 | - |
|
321 | - /** |
|
322 | - * Searches principals based on room features |
|
323 | - * e.g.: |
|
324 | - * '{http://nextcloud.com/ns}room-features' => 'TV,PROJECTOR' |
|
325 | - * |
|
326 | - * @param string $key |
|
327 | - * @param string $value |
|
328 | - * @param string[] $usersGroups |
|
329 | - * @return string[] |
|
330 | - */ |
|
331 | - private function searchPrincipalsByRoomFeature(string $key, string $value, array $usersGroups = []): array { |
|
332 | - $query = $this->getMetadataQuery($key); |
|
333 | - foreach (explode(',', $value) as $v) { |
|
334 | - $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($v) . '%'))); |
|
335 | - } |
|
336 | - return $this->getRows($query, $usersGroups); |
|
337 | - } |
|
338 | - |
|
339 | - /** |
|
340 | - * Searches principals based on room seating capacity or vehicle capacity |
|
341 | - * e.g.: |
|
342 | - * '{http://nextcloud.com/ns}room-seating-capacity' => '100' |
|
343 | - * |
|
344 | - * @param string $key |
|
345 | - * @param string $value |
|
346 | - * @param string[] $usersGroups |
|
347 | - * @return string[] |
|
348 | - */ |
|
349 | - private function searchPrincipalsByCapacity(string $key, string $value, array $usersGroups = []): array { |
|
350 | - $query = $this->getMetadataQuery($key); |
|
351 | - $query->andWhere($query->expr()->gte('value', $query->createNamedParameter($value))); |
|
352 | - return $this->getRows($query, $usersGroups); |
|
353 | - } |
|
354 | - |
|
355 | - /** |
|
356 | - * @param IQueryBuilder $query |
|
357 | - * @param string[] $usersGroups |
|
358 | - * @return string[] |
|
359 | - */ |
|
360 | - private function getRows(IQueryBuilder $query, array $usersGroups): array { |
|
361 | - try { |
|
362 | - $stmt = $query->executeQuery(); |
|
363 | - } catch (Exception $e) { |
|
364 | - $this->logger->error('Could not search resources: ' . $e->getMessage(), ['exception' => $e]); |
|
365 | - } |
|
366 | - |
|
367 | - $rows = []; |
|
368 | - while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
369 | - $principalRow = $this->getPrincipalById($row[$this->dbForeignKeyName]); |
|
370 | - if (!$principalRow) { |
|
371 | - continue; |
|
372 | - } |
|
373 | - |
|
374 | - $rows[] = $principalRow; |
|
375 | - } |
|
376 | - |
|
377 | - $stmt->closeCursor(); |
|
378 | - |
|
379 | - $filteredRows = array_filter($rows, function ($row) use ($usersGroups) { |
|
380 | - return $this->isAllowedToAccessResource($row, $usersGroups); |
|
381 | - }); |
|
382 | - |
|
383 | - return array_map(static function ($row): string { |
|
384 | - return $row['uri']; |
|
385 | - }, $filteredRows); |
|
386 | - } |
|
387 | - |
|
388 | - /** |
|
389 | - * @param string $uri |
|
390 | - * @param string $principalPrefix |
|
391 | - * @return null|string |
|
392 | - * @throws Exception |
|
393 | - */ |
|
394 | - public function findByUri($uri, $principalPrefix): ?string { |
|
395 | - $user = $this->userSession->getUser(); |
|
396 | - if (!$user) { |
|
397 | - return null; |
|
398 | - } |
|
399 | - $usersGroups = $this->groupManager->getUserGroupIds($user); |
|
400 | - |
|
401 | - if (str_starts_with($uri, 'mailto:')) { |
|
402 | - $email = substr($uri, 7); |
|
403 | - $query = $this->db->getQueryBuilder(); |
|
404 | - $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
405 | - ->from($this->dbTableName) |
|
406 | - ->where($query->expr()->eq('email', $query->createNamedParameter($email))); |
|
407 | - |
|
408 | - $stmt = $query->execute(); |
|
409 | - $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
410 | - |
|
411 | - if (!$row) { |
|
412 | - return null; |
|
413 | - } |
|
414 | - if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
415 | - return null; |
|
416 | - } |
|
417 | - |
|
418 | - return $this->rowToPrincipal($row)['uri']; |
|
419 | - } |
|
420 | - |
|
421 | - if (str_starts_with($uri, 'principal:')) { |
|
422 | - $path = substr($uri, 10); |
|
423 | - if (!str_starts_with($path, $this->principalPrefix)) { |
|
424 | - return null; |
|
425 | - } |
|
426 | - |
|
427 | - [, $name] = \Sabre\Uri\split($path); |
|
428 | - [$backendId, $resourceId] = explode('-', $name, 2); |
|
429 | - |
|
430 | - $query = $this->db->getQueryBuilder(); |
|
431 | - $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
432 | - ->from($this->dbTableName) |
|
433 | - ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) |
|
434 | - ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); |
|
435 | - $stmt = $query->execute(); |
|
436 | - $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
437 | - |
|
438 | - if (!$row) { |
|
439 | - return null; |
|
440 | - } |
|
441 | - if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
442 | - return null; |
|
443 | - } |
|
444 | - |
|
445 | - return $this->rowToPrincipal($row)['uri']; |
|
446 | - } |
|
447 | - |
|
448 | - return null; |
|
449 | - } |
|
450 | - |
|
451 | - /** |
|
452 | - * convert database row to principal |
|
453 | - * |
|
454 | - * @param string[] $row |
|
455 | - * @param string[] $metadata |
|
456 | - * @return string[] |
|
457 | - */ |
|
458 | - private function rowToPrincipal(array $row, array $metadata = []): array { |
|
459 | - return array_merge([ |
|
460 | - 'uri' => $this->principalPrefix . '/' . $row['backend_id'] . '-' . $row['resource_id'], |
|
461 | - '{DAV:}displayname' => $row['displayname'], |
|
462 | - '{http://sabredav.org/ns}email-address' => $row['email'], |
|
463 | - '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->cuType, |
|
464 | - ], $metadata); |
|
465 | - } |
|
466 | - |
|
467 | - /** |
|
468 | - * @param array $row |
|
469 | - * @param array $userGroups |
|
470 | - * @return bool |
|
471 | - */ |
|
472 | - private function isAllowedToAccessResource(array $row, array $userGroups): bool { |
|
473 | - if (!isset($row['group_restrictions']) || |
|
474 | - $row['group_restrictions'] === null || |
|
475 | - $row['group_restrictions'] === '') { |
|
476 | - return true; |
|
477 | - } |
|
478 | - |
|
479 | - // group restrictions contains something, but not parsable, deny access and log warning |
|
480 | - $json = json_decode($row['group_restrictions'], null, 512, JSON_THROW_ON_ERROR); |
|
481 | - if (!\is_array($json)) { |
|
482 | - $this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource'); |
|
483 | - return false; |
|
484 | - } |
|
485 | - |
|
486 | - // empty array => no group restrictions |
|
487 | - if (empty($json)) { |
|
488 | - return true; |
|
489 | - } |
|
490 | - |
|
491 | - return !empty(array_intersect($json, $userGroups)); |
|
492 | - } |
|
28 | + /** @var string */ |
|
29 | + private $dbTableName; |
|
30 | + |
|
31 | + /** @var string */ |
|
32 | + private $dbMetaDataTableName; |
|
33 | + |
|
34 | + /** @var string */ |
|
35 | + private $dbForeignKeyName; |
|
36 | + |
|
37 | + public function __construct( |
|
38 | + private IDBConnection $db, |
|
39 | + private IUserSession $userSession, |
|
40 | + private IGroupManager $groupManager, |
|
41 | + private LoggerInterface $logger, |
|
42 | + private ProxyMapper $proxyMapper, |
|
43 | + private string $principalPrefix, |
|
44 | + string $dbPrefix, |
|
45 | + private string $cuType, |
|
46 | + ) { |
|
47 | + $this->dbTableName = 'calendar_' . $dbPrefix . 's'; |
|
48 | + $this->dbMetaDataTableName = $this->dbTableName . '_md'; |
|
49 | + $this->dbForeignKeyName = $dbPrefix . '_id'; |
|
50 | + } |
|
51 | + |
|
52 | + use PrincipalProxyTrait; |
|
53 | + |
|
54 | + /** |
|
55 | + * Returns a list of principals based on a prefix. |
|
56 | + * |
|
57 | + * This prefix will often contain something like 'principals'. You are only |
|
58 | + * expected to return principals that are in this base path. |
|
59 | + * |
|
60 | + * You are expected to return at least a 'uri' for every user, you can |
|
61 | + * return any additional properties if you wish so. Common properties are: |
|
62 | + * {DAV:}displayname |
|
63 | + * |
|
64 | + * @param string $prefixPath |
|
65 | + * @return string[] |
|
66 | + */ |
|
67 | + public function getPrincipalsByPrefix($prefixPath): array { |
|
68 | + $principals = []; |
|
69 | + |
|
70 | + if ($prefixPath === $this->principalPrefix) { |
|
71 | + $query = $this->db->getQueryBuilder(); |
|
72 | + $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname']) |
|
73 | + ->from($this->dbTableName); |
|
74 | + $stmt = $query->execute(); |
|
75 | + |
|
76 | + $metaDataQuery = $this->db->getQueryBuilder(); |
|
77 | + $metaDataQuery->select([$this->dbForeignKeyName, 'key', 'value']) |
|
78 | + ->from($this->dbMetaDataTableName); |
|
79 | + $metaDataStmt = $metaDataQuery->execute(); |
|
80 | + $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC); |
|
81 | + |
|
82 | + $metaDataById = []; |
|
83 | + foreach ($metaDataRows as $metaDataRow) { |
|
84 | + if (!isset($metaDataById[$metaDataRow[$this->dbForeignKeyName]])) { |
|
85 | + $metaDataById[$metaDataRow[$this->dbForeignKeyName]] = []; |
|
86 | + } |
|
87 | + |
|
88 | + $metaDataById[$metaDataRow[$this->dbForeignKeyName]][$metaDataRow['key']] = |
|
89 | + $metaDataRow['value']; |
|
90 | + } |
|
91 | + |
|
92 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
93 | + $id = $row['id']; |
|
94 | + |
|
95 | + if (isset($metaDataById[$id])) { |
|
96 | + $principals[] = $this->rowToPrincipal($row, $metaDataById[$id]); |
|
97 | + } else { |
|
98 | + $principals[] = $this->rowToPrincipal($row); |
|
99 | + } |
|
100 | + } |
|
101 | + |
|
102 | + $stmt->closeCursor(); |
|
103 | + } |
|
104 | + |
|
105 | + return $principals; |
|
106 | + } |
|
107 | + |
|
108 | + /** |
|
109 | + * Returns a specific principal, specified by its path. |
|
110 | + * The returned structure should be the exact same as from |
|
111 | + * getPrincipalsByPrefix. |
|
112 | + * |
|
113 | + * @param string $prefixPath |
|
114 | + * |
|
115 | + * @return array |
|
116 | + */ |
|
117 | + public function getPrincipalByPath($path) { |
|
118 | + if (!str_starts_with($path, $this->principalPrefix)) { |
|
119 | + return null; |
|
120 | + } |
|
121 | + [, $name] = \Sabre\Uri\split($path); |
|
122 | + |
|
123 | + [$backendId, $resourceId] = explode('-', $name, 2); |
|
124 | + |
|
125 | + $query = $this->db->getQueryBuilder(); |
|
126 | + $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname']) |
|
127 | + ->from($this->dbTableName) |
|
128 | + ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) |
|
129 | + ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); |
|
130 | + $stmt = $query->execute(); |
|
131 | + $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
132 | + |
|
133 | + if (!$row) { |
|
134 | + return null; |
|
135 | + } |
|
136 | + |
|
137 | + $metaDataQuery = $this->db->getQueryBuilder(); |
|
138 | + $metaDataQuery->select(['key', 'value']) |
|
139 | + ->from($this->dbMetaDataTableName) |
|
140 | + ->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id']))); |
|
141 | + $metaDataStmt = $metaDataQuery->execute(); |
|
142 | + $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC); |
|
143 | + $metadata = []; |
|
144 | + |
|
145 | + foreach ($metaDataRows as $metaDataRow) { |
|
146 | + $metadata[$metaDataRow['key']] = $metaDataRow['value']; |
|
147 | + } |
|
148 | + |
|
149 | + return $this->rowToPrincipal($row, $metadata); |
|
150 | + } |
|
151 | + |
|
152 | + /** |
|
153 | + * @param int $id |
|
154 | + * @return string[]|null |
|
155 | + */ |
|
156 | + public function getPrincipalById($id): ?array { |
|
157 | + $query = $this->db->getQueryBuilder(); |
|
158 | + $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname']) |
|
159 | + ->from($this->dbTableName) |
|
160 | + ->where($query->expr()->eq('id', $query->createNamedParameter($id))); |
|
161 | + $stmt = $query->execute(); |
|
162 | + $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
163 | + |
|
164 | + if (!$row) { |
|
165 | + return null; |
|
166 | + } |
|
167 | + |
|
168 | + $metaDataQuery = $this->db->getQueryBuilder(); |
|
169 | + $metaDataQuery->select(['key', 'value']) |
|
170 | + ->from($this->dbMetaDataTableName) |
|
171 | + ->where($metaDataQuery->expr()->eq($this->dbForeignKeyName, $metaDataQuery->createNamedParameter($row['id']))); |
|
172 | + $metaDataStmt = $metaDataQuery->execute(); |
|
173 | + $metaDataRows = $metaDataStmt->fetchAll(\PDO::FETCH_ASSOC); |
|
174 | + $metadata = []; |
|
175 | + |
|
176 | + foreach ($metaDataRows as $metaDataRow) { |
|
177 | + $metadata[$metaDataRow['key']] = $metaDataRow['value']; |
|
178 | + } |
|
179 | + |
|
180 | + return $this->rowToPrincipal($row, $metadata); |
|
181 | + } |
|
182 | + |
|
183 | + /** |
|
184 | + * @param string $path |
|
185 | + * @param PropPatch $propPatch |
|
186 | + * @return int |
|
187 | + */ |
|
188 | + public function updatePrincipal($path, PropPatch $propPatch): int { |
|
189 | + return 0; |
|
190 | + } |
|
191 | + |
|
192 | + /** |
|
193 | + * @param string $prefixPath |
|
194 | + * @param string $test |
|
195 | + * |
|
196 | + * @return array |
|
197 | + */ |
|
198 | + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') { |
|
199 | + $results = []; |
|
200 | + if (\count($searchProperties) === 0) { |
|
201 | + return []; |
|
202 | + } |
|
203 | + if ($prefixPath !== $this->principalPrefix) { |
|
204 | + return []; |
|
205 | + } |
|
206 | + |
|
207 | + $user = $this->userSession->getUser(); |
|
208 | + if (!$user) { |
|
209 | + return []; |
|
210 | + } |
|
211 | + $usersGroups = $this->groupManager->getUserGroupIds($user); |
|
212 | + |
|
213 | + foreach ($searchProperties as $prop => $value) { |
|
214 | + switch ($prop) { |
|
215 | + case '{http://sabredav.org/ns}email-address': |
|
216 | + $query = $this->db->getQueryBuilder(); |
|
217 | + $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
218 | + ->from($this->dbTableName) |
|
219 | + ->where($query->expr()->iLike('email', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
220 | + |
|
221 | + $stmt = $query->execute(); |
|
222 | + $principals = []; |
|
223 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
224 | + if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
225 | + continue; |
|
226 | + } |
|
227 | + $principals[] = $this->rowToPrincipal($row)['uri']; |
|
228 | + } |
|
229 | + $results[] = $principals; |
|
230 | + |
|
231 | + $stmt->closeCursor(); |
|
232 | + break; |
|
233 | + |
|
234 | + case '{DAV:}displayname': |
|
235 | + $query = $this->db->getQueryBuilder(); |
|
236 | + $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
237 | + ->from($this->dbTableName) |
|
238 | + ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
239 | + |
|
240 | + $stmt = $query->execute(); |
|
241 | + $principals = []; |
|
242 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
243 | + if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
244 | + continue; |
|
245 | + } |
|
246 | + $principals[] = $this->rowToPrincipal($row)['uri']; |
|
247 | + } |
|
248 | + $results[] = $principals; |
|
249 | + |
|
250 | + $stmt->closeCursor(); |
|
251 | + break; |
|
252 | + |
|
253 | + case '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set': |
|
254 | + // If you add support for more search properties that qualify as a user-address, |
|
255 | + // please also add them to the array below |
|
256 | + $results[] = $this->searchPrincipals($this->principalPrefix, [ |
|
257 | + '{http://sabredav.org/ns}email-address' => $value, |
|
258 | + ], 'anyof'); |
|
259 | + break; |
|
260 | + |
|
261 | + case IRoomMetadata::FEATURES: |
|
262 | + $results[] = $this->searchPrincipalsByRoomFeature($prop, $value); |
|
263 | + break; |
|
264 | + |
|
265 | + case IRoomMetadata::CAPACITY: |
|
266 | + case IResourceMetadata::VEHICLE_SEATING_CAPACITY: |
|
267 | + $results[] = $this->searchPrincipalsByCapacity($prop, $value); |
|
268 | + break; |
|
269 | + |
|
270 | + default: |
|
271 | + $results[] = $this->searchPrincipalsByMetadataKey($prop, $value, $usersGroups); |
|
272 | + break; |
|
273 | + } |
|
274 | + } |
|
275 | + |
|
276 | + // results is an array of arrays, so this is not the first search result |
|
277 | + // but the results of the first searchProperty |
|
278 | + if (count($results) === 1) { |
|
279 | + return $results[0]; |
|
280 | + } |
|
281 | + |
|
282 | + switch ($test) { |
|
283 | + case 'anyof': |
|
284 | + return array_values(array_unique(array_merge(...$results))); |
|
285 | + |
|
286 | + case 'allof': |
|
287 | + default: |
|
288 | + return array_values(array_intersect(...$results)); |
|
289 | + } |
|
290 | + } |
|
291 | + |
|
292 | + /** |
|
293 | + * @param string $key |
|
294 | + * @return IQueryBuilder |
|
295 | + */ |
|
296 | + private function getMetadataQuery(string $key): IQueryBuilder { |
|
297 | + $query = $this->db->getQueryBuilder(); |
|
298 | + $query->select([$this->dbForeignKeyName]) |
|
299 | + ->from($this->dbMetaDataTableName) |
|
300 | + ->where($query->expr()->eq('key', $query->createNamedParameter($key))); |
|
301 | + return $query; |
|
302 | + } |
|
303 | + |
|
304 | + /** |
|
305 | + * Searches principals based on their metadata keys. |
|
306 | + * This allows to search for all principals with a specific key. |
|
307 | + * e.g.: |
|
308 | + * '{http://nextcloud.com/ns}room-building-address' => 'ABC Street 123, ...' |
|
309 | + * |
|
310 | + * @param string $key |
|
311 | + * @param string $value |
|
312 | + * @param string[] $usersGroups |
|
313 | + * @return string[] |
|
314 | + */ |
|
315 | + private function searchPrincipalsByMetadataKey(string $key, string $value, array $usersGroups = []): array { |
|
316 | + $query = $this->getMetadataQuery($key); |
|
317 | + $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
318 | + return $this->getRows($query, $usersGroups); |
|
319 | + } |
|
320 | + |
|
321 | + /** |
|
322 | + * Searches principals based on room features |
|
323 | + * e.g.: |
|
324 | + * '{http://nextcloud.com/ns}room-features' => 'TV,PROJECTOR' |
|
325 | + * |
|
326 | + * @param string $key |
|
327 | + * @param string $value |
|
328 | + * @param string[] $usersGroups |
|
329 | + * @return string[] |
|
330 | + */ |
|
331 | + private function searchPrincipalsByRoomFeature(string $key, string $value, array $usersGroups = []): array { |
|
332 | + $query = $this->getMetadataQuery($key); |
|
333 | + foreach (explode(',', $value) as $v) { |
|
334 | + $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($v) . '%'))); |
|
335 | + } |
|
336 | + return $this->getRows($query, $usersGroups); |
|
337 | + } |
|
338 | + |
|
339 | + /** |
|
340 | + * Searches principals based on room seating capacity or vehicle capacity |
|
341 | + * e.g.: |
|
342 | + * '{http://nextcloud.com/ns}room-seating-capacity' => '100' |
|
343 | + * |
|
344 | + * @param string $key |
|
345 | + * @param string $value |
|
346 | + * @param string[] $usersGroups |
|
347 | + * @return string[] |
|
348 | + */ |
|
349 | + private function searchPrincipalsByCapacity(string $key, string $value, array $usersGroups = []): array { |
|
350 | + $query = $this->getMetadataQuery($key); |
|
351 | + $query->andWhere($query->expr()->gte('value', $query->createNamedParameter($value))); |
|
352 | + return $this->getRows($query, $usersGroups); |
|
353 | + } |
|
354 | + |
|
355 | + /** |
|
356 | + * @param IQueryBuilder $query |
|
357 | + * @param string[] $usersGroups |
|
358 | + * @return string[] |
|
359 | + */ |
|
360 | + private function getRows(IQueryBuilder $query, array $usersGroups): array { |
|
361 | + try { |
|
362 | + $stmt = $query->executeQuery(); |
|
363 | + } catch (Exception $e) { |
|
364 | + $this->logger->error('Could not search resources: ' . $e->getMessage(), ['exception' => $e]); |
|
365 | + } |
|
366 | + |
|
367 | + $rows = []; |
|
368 | + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { |
|
369 | + $principalRow = $this->getPrincipalById($row[$this->dbForeignKeyName]); |
|
370 | + if (!$principalRow) { |
|
371 | + continue; |
|
372 | + } |
|
373 | + |
|
374 | + $rows[] = $principalRow; |
|
375 | + } |
|
376 | + |
|
377 | + $stmt->closeCursor(); |
|
378 | + |
|
379 | + $filteredRows = array_filter($rows, function ($row) use ($usersGroups) { |
|
380 | + return $this->isAllowedToAccessResource($row, $usersGroups); |
|
381 | + }); |
|
382 | + |
|
383 | + return array_map(static function ($row): string { |
|
384 | + return $row['uri']; |
|
385 | + }, $filteredRows); |
|
386 | + } |
|
387 | + |
|
388 | + /** |
|
389 | + * @param string $uri |
|
390 | + * @param string $principalPrefix |
|
391 | + * @return null|string |
|
392 | + * @throws Exception |
|
393 | + */ |
|
394 | + public function findByUri($uri, $principalPrefix): ?string { |
|
395 | + $user = $this->userSession->getUser(); |
|
396 | + if (!$user) { |
|
397 | + return null; |
|
398 | + } |
|
399 | + $usersGroups = $this->groupManager->getUserGroupIds($user); |
|
400 | + |
|
401 | + if (str_starts_with($uri, 'mailto:')) { |
|
402 | + $email = substr($uri, 7); |
|
403 | + $query = $this->db->getQueryBuilder(); |
|
404 | + $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
405 | + ->from($this->dbTableName) |
|
406 | + ->where($query->expr()->eq('email', $query->createNamedParameter($email))); |
|
407 | + |
|
408 | + $stmt = $query->execute(); |
|
409 | + $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
410 | + |
|
411 | + if (!$row) { |
|
412 | + return null; |
|
413 | + } |
|
414 | + if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
415 | + return null; |
|
416 | + } |
|
417 | + |
|
418 | + return $this->rowToPrincipal($row)['uri']; |
|
419 | + } |
|
420 | + |
|
421 | + if (str_starts_with($uri, 'principal:')) { |
|
422 | + $path = substr($uri, 10); |
|
423 | + if (!str_starts_with($path, $this->principalPrefix)) { |
|
424 | + return null; |
|
425 | + } |
|
426 | + |
|
427 | + [, $name] = \Sabre\Uri\split($path); |
|
428 | + [$backendId, $resourceId] = explode('-', $name, 2); |
|
429 | + |
|
430 | + $query = $this->db->getQueryBuilder(); |
|
431 | + $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
|
432 | + ->from($this->dbTableName) |
|
433 | + ->where($query->expr()->eq('backend_id', $query->createNamedParameter($backendId))) |
|
434 | + ->andWhere($query->expr()->eq('resource_id', $query->createNamedParameter($resourceId))); |
|
435 | + $stmt = $query->execute(); |
|
436 | + $row = $stmt->fetch(\PDO::FETCH_ASSOC); |
|
437 | + |
|
438 | + if (!$row) { |
|
439 | + return null; |
|
440 | + } |
|
441 | + if (!$this->isAllowedToAccessResource($row, $usersGroups)) { |
|
442 | + return null; |
|
443 | + } |
|
444 | + |
|
445 | + return $this->rowToPrincipal($row)['uri']; |
|
446 | + } |
|
447 | + |
|
448 | + return null; |
|
449 | + } |
|
450 | + |
|
451 | + /** |
|
452 | + * convert database row to principal |
|
453 | + * |
|
454 | + * @param string[] $row |
|
455 | + * @param string[] $metadata |
|
456 | + * @return string[] |
|
457 | + */ |
|
458 | + private function rowToPrincipal(array $row, array $metadata = []): array { |
|
459 | + return array_merge([ |
|
460 | + 'uri' => $this->principalPrefix . '/' . $row['backend_id'] . '-' . $row['resource_id'], |
|
461 | + '{DAV:}displayname' => $row['displayname'], |
|
462 | + '{http://sabredav.org/ns}email-address' => $row['email'], |
|
463 | + '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->cuType, |
|
464 | + ], $metadata); |
|
465 | + } |
|
466 | + |
|
467 | + /** |
|
468 | + * @param array $row |
|
469 | + * @param array $userGroups |
|
470 | + * @return bool |
|
471 | + */ |
|
472 | + private function isAllowedToAccessResource(array $row, array $userGroups): bool { |
|
473 | + if (!isset($row['group_restrictions']) || |
|
474 | + $row['group_restrictions'] === null || |
|
475 | + $row['group_restrictions'] === '') { |
|
476 | + return true; |
|
477 | + } |
|
478 | + |
|
479 | + // group restrictions contains something, but not parsable, deny access and log warning |
|
480 | + $json = json_decode($row['group_restrictions'], null, 512, JSON_THROW_ON_ERROR); |
|
481 | + if (!\is_array($json)) { |
|
482 | + $this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource'); |
|
483 | + return false; |
|
484 | + } |
|
485 | + |
|
486 | + // empty array => no group restrictions |
|
487 | + if (empty($json)) { |
|
488 | + return true; |
|
489 | + } |
|
490 | + |
|
491 | + return !empty(array_intersect($json, $userGroups)); |
|
492 | + } |
|
493 | 493 | } |
@@ -44,9 +44,9 @@ discard block |
||
44 | 44 | string $dbPrefix, |
45 | 45 | private string $cuType, |
46 | 46 | ) { |
47 | - $this->dbTableName = 'calendar_' . $dbPrefix . 's'; |
|
48 | - $this->dbMetaDataTableName = $this->dbTableName . '_md'; |
|
49 | - $this->dbForeignKeyName = $dbPrefix . '_id'; |
|
47 | + $this->dbTableName = 'calendar_'.$dbPrefix.'s'; |
|
48 | + $this->dbMetaDataTableName = $this->dbTableName.'_md'; |
|
49 | + $this->dbForeignKeyName = $dbPrefix.'_id'; |
|
50 | 50 | } |
51 | 51 | |
52 | 52 | use PrincipalProxyTrait; |
@@ -216,7 +216,7 @@ discard block |
||
216 | 216 | $query = $this->db->getQueryBuilder(); |
217 | 217 | $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
218 | 218 | ->from($this->dbTableName) |
219 | - ->where($query->expr()->iLike('email', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
219 | + ->where($query->expr()->iLike('email', $query->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%'))); |
|
220 | 220 | |
221 | 221 | $stmt = $query->execute(); |
222 | 222 | $principals = []; |
@@ -235,7 +235,7 @@ discard block |
||
235 | 235 | $query = $this->db->getQueryBuilder(); |
236 | 236 | $query->select(['id', 'backend_id', 'resource_id', 'email', 'displayname', 'group_restrictions']) |
237 | 237 | ->from($this->dbTableName) |
238 | - ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
238 | + ->where($query->expr()->iLike('displayname', $query->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%'))); |
|
239 | 239 | |
240 | 240 | $stmt = $query->execute(); |
241 | 241 | $principals = []; |
@@ -314,7 +314,7 @@ discard block |
||
314 | 314 | */ |
315 | 315 | private function searchPrincipalsByMetadataKey(string $key, string $value, array $usersGroups = []): array { |
316 | 316 | $query = $this->getMetadataQuery($key); |
317 | - $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%'))); |
|
317 | + $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%'.$this->db->escapeLikeParameter($value).'%'))); |
|
318 | 318 | return $this->getRows($query, $usersGroups); |
319 | 319 | } |
320 | 320 | |
@@ -331,7 +331,7 @@ discard block |
||
331 | 331 | private function searchPrincipalsByRoomFeature(string $key, string $value, array $usersGroups = []): array { |
332 | 332 | $query = $this->getMetadataQuery($key); |
333 | 333 | foreach (explode(',', $value) as $v) { |
334 | - $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%' . $this->db->escapeLikeParameter($v) . '%'))); |
|
334 | + $query->andWhere($query->expr()->iLike('value', $query->createNamedParameter('%'.$this->db->escapeLikeParameter($v).'%'))); |
|
335 | 335 | } |
336 | 336 | return $this->getRows($query, $usersGroups); |
337 | 337 | } |
@@ -361,7 +361,7 @@ discard block |
||
361 | 361 | try { |
362 | 362 | $stmt = $query->executeQuery(); |
363 | 363 | } catch (Exception $e) { |
364 | - $this->logger->error('Could not search resources: ' . $e->getMessage(), ['exception' => $e]); |
|
364 | + $this->logger->error('Could not search resources: '.$e->getMessage(), ['exception' => $e]); |
|
365 | 365 | } |
366 | 366 | |
367 | 367 | $rows = []; |
@@ -376,11 +376,11 @@ discard block |
||
376 | 376 | |
377 | 377 | $stmt->closeCursor(); |
378 | 378 | |
379 | - $filteredRows = array_filter($rows, function ($row) use ($usersGroups) { |
|
379 | + $filteredRows = array_filter($rows, function($row) use ($usersGroups) { |
|
380 | 380 | return $this->isAllowedToAccessResource($row, $usersGroups); |
381 | 381 | }); |
382 | 382 | |
383 | - return array_map(static function ($row): string { |
|
383 | + return array_map(static function($row): string { |
|
384 | 384 | return $row['uri']; |
385 | 385 | }, $filteredRows); |
386 | 386 | } |
@@ -457,7 +457,7 @@ discard block |
||
457 | 457 | */ |
458 | 458 | private function rowToPrincipal(array $row, array $metadata = []): array { |
459 | 459 | return array_merge([ |
460 | - 'uri' => $this->principalPrefix . '/' . $row['backend_id'] . '-' . $row['resource_id'], |
|
460 | + 'uri' => $this->principalPrefix.'/'.$row['backend_id'].'-'.$row['resource_id'], |
|
461 | 461 | '{DAV:}displayname' => $row['displayname'], |
462 | 462 | '{http://sabredav.org/ns}email-address' => $row['email'], |
463 | 463 | '{urn:ietf:params:xml:ns:caldav}calendar-user-type' => $this->cuType, |
@@ -479,7 +479,7 @@ discard block |
||
479 | 479 | // group restrictions contains something, but not parsable, deny access and log warning |
480 | 480 | $json = json_decode($row['group_restrictions'], null, 512, JSON_THROW_ON_ERROR); |
481 | 481 | if (!\is_array($json)) { |
482 | - $this->logger->info('group_restrictions field could not be parsed for ' . $this->dbTableName . '::' . $row['id'] . ', denying access to resource'); |
|
482 | + $this->logger->info('group_restrictions field could not be parsed for '.$this->dbTableName.'::'.$row['id'].', denying access to resource'); |
|
483 | 483 | return false; |
484 | 484 | } |
485 | 485 |
@@ -30,222 +30,222 @@ |
||
30 | 30 | */ |
31 | 31 | class CalDavSharingBackendTest extends TestCase { |
32 | 32 | |
33 | - private IDBConnection $db; |
|
34 | - private IUserManager $userManager; |
|
35 | - private IGroupManager $groupManager; |
|
36 | - private Principal $principalBackend; |
|
37 | - private ICacheFactory $cacheFactory; |
|
38 | - private LoggerInterface $logger; |
|
39 | - private SharingMapper $sharingMapper; |
|
40 | - private SharingService $sharingService; |
|
41 | - private Backend $sharingBackend; |
|
42 | - |
|
43 | - private $resourceIds = [10001]; |
|
44 | - |
|
45 | - protected function setUp(): void { |
|
46 | - parent::setUp(); |
|
47 | - |
|
48 | - $this->db = Server::get(IDBConnection::class); |
|
49 | - |
|
50 | - $this->userManager = $this->createMock(IUserManager::class); |
|
51 | - $this->groupManager = $this->createMock(IGroupManager::class); |
|
52 | - $this->principalBackend = $this->createMock(Principal::class); |
|
53 | - $this->cacheFactory = $this->createMock(ICacheFactory::class); |
|
54 | - $this->cacheFactory->method('createInMemory') |
|
55 | - ->willReturn(new NullCache()); |
|
56 | - $this->logger = new \Psr\Log\NullLogger(); |
|
57 | - |
|
58 | - $this->sharingMapper = new SharingMapper($this->db); |
|
59 | - $this->sharingService = new Service($this->sharingMapper); |
|
60 | - |
|
61 | - $this->sharingBackend = new \OCA\DAV\CalDAV\Sharing\Backend( |
|
62 | - $this->userManager, |
|
63 | - $this->groupManager, |
|
64 | - $this->principalBackend, |
|
65 | - $this->cacheFactory, |
|
66 | - $this->sharingService, |
|
67 | - $this->logger |
|
68 | - ); |
|
69 | - |
|
70 | - $this->removeFixtures(); |
|
71 | - } |
|
72 | - |
|
73 | - protected function tearDown(): void { |
|
74 | - $this->removeFixtures(); |
|
75 | - } |
|
76 | - |
|
77 | - protected function removeFixtures(): void { |
|
78 | - $qb = $this->db->getQueryBuilder(); |
|
79 | - $qb->delete('dav_shares') |
|
80 | - ->where($qb->expr()->in('resourceid', $qb->createNamedParameter($this->resourceIds, IQueryBuilder::PARAM_INT_ARRAY))); |
|
81 | - $qb->executeStatement(); |
|
82 | - } |
|
83 | - |
|
84 | - public function testShareCalendarWithGroup(): void { |
|
85 | - $calendar = $this->createMock(Calendar::class); |
|
86 | - $calendar->method('getResourceId') |
|
87 | - ->willReturn(10001); |
|
88 | - $calendar->method('getOwner') |
|
89 | - ->willReturn('principals/users/admin'); |
|
90 | - |
|
91 | - $this->principalBackend->method('findByUri') |
|
92 | - ->willReturn('principals/groups/alice_bob'); |
|
93 | - |
|
94 | - $this->groupManager->method('groupExists') |
|
95 | - ->willReturn(true); |
|
96 | - |
|
97 | - $this->sharingBackend->updateShares( |
|
98 | - $calendar, |
|
99 | - [['href' => 'principals/groups/alice_bob']], |
|
100 | - [], |
|
101 | - [] |
|
102 | - ); |
|
103 | - |
|
104 | - $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
105 | - } |
|
106 | - |
|
107 | - public function testUnshareCalendarFromGroup(): void { |
|
108 | - $calendar = $this->createMock(Calendar::class); |
|
109 | - $calendar->method('getResourceId') |
|
110 | - ->willReturn(10001); |
|
111 | - $calendar->method('getOwner') |
|
112 | - ->willReturn('principals/users/admin'); |
|
113 | - |
|
114 | - $this->principalBackend->method('findByUri') |
|
115 | - ->willReturn('principals/groups/alice_bob'); |
|
116 | - |
|
117 | - $this->groupManager->method('groupExists') |
|
118 | - ->willReturn(true); |
|
119 | - |
|
120 | - $this->sharingBackend->updateShares( |
|
121 | - shareable: $calendar, |
|
122 | - add: [['href' => 'principals/groups/alice_bob']], |
|
123 | - remove: [], |
|
124 | - ); |
|
125 | - |
|
126 | - $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
127 | - |
|
128 | - $this->sharingBackend->updateShares( |
|
129 | - shareable: $calendar, |
|
130 | - add: [], |
|
131 | - remove: ['principals/groups/alice_bob'], |
|
132 | - ); |
|
133 | - |
|
134 | - $this->assertCount(0, $this->sharingService->getShares(10001)); |
|
135 | - } |
|
136 | - |
|
137 | - public function testShareCalendarWithGroupAndUnshareAsUser(): void { |
|
138 | - $calendar = $this->createMock(Calendar::class); |
|
139 | - $calendar->method('getResourceId') |
|
140 | - ->willReturn(10001); |
|
141 | - $calendar->method('getOwner') |
|
142 | - ->willReturn('principals/users/admin'); |
|
143 | - |
|
144 | - $this->principalBackend->method('findByUri') |
|
145 | - ->willReturnMap([ |
|
146 | - ['principals/groups/alice_bob', '', 'principals/groups/alice_bob'], |
|
147 | - ['principals/users/bob', '', 'principals/users/bob'], |
|
148 | - ]); |
|
149 | - $this->principalBackend->method('getGroupMembership') |
|
150 | - ->willReturn([ |
|
151 | - 'principals/groups/alice_bob', |
|
152 | - ]); |
|
153 | - $this->principalBackend->method('getCircleMembership') |
|
154 | - ->willReturn([]); |
|
155 | - |
|
156 | - $this->groupManager->method('groupExists') |
|
157 | - ->willReturn(true); |
|
158 | - |
|
159 | - /* |
|
33 | + private IDBConnection $db; |
|
34 | + private IUserManager $userManager; |
|
35 | + private IGroupManager $groupManager; |
|
36 | + private Principal $principalBackend; |
|
37 | + private ICacheFactory $cacheFactory; |
|
38 | + private LoggerInterface $logger; |
|
39 | + private SharingMapper $sharingMapper; |
|
40 | + private SharingService $sharingService; |
|
41 | + private Backend $sharingBackend; |
|
42 | + |
|
43 | + private $resourceIds = [10001]; |
|
44 | + |
|
45 | + protected function setUp(): void { |
|
46 | + parent::setUp(); |
|
47 | + |
|
48 | + $this->db = Server::get(IDBConnection::class); |
|
49 | + |
|
50 | + $this->userManager = $this->createMock(IUserManager::class); |
|
51 | + $this->groupManager = $this->createMock(IGroupManager::class); |
|
52 | + $this->principalBackend = $this->createMock(Principal::class); |
|
53 | + $this->cacheFactory = $this->createMock(ICacheFactory::class); |
|
54 | + $this->cacheFactory->method('createInMemory') |
|
55 | + ->willReturn(new NullCache()); |
|
56 | + $this->logger = new \Psr\Log\NullLogger(); |
|
57 | + |
|
58 | + $this->sharingMapper = new SharingMapper($this->db); |
|
59 | + $this->sharingService = new Service($this->sharingMapper); |
|
60 | + |
|
61 | + $this->sharingBackend = new \OCA\DAV\CalDAV\Sharing\Backend( |
|
62 | + $this->userManager, |
|
63 | + $this->groupManager, |
|
64 | + $this->principalBackend, |
|
65 | + $this->cacheFactory, |
|
66 | + $this->sharingService, |
|
67 | + $this->logger |
|
68 | + ); |
|
69 | + |
|
70 | + $this->removeFixtures(); |
|
71 | + } |
|
72 | + |
|
73 | + protected function tearDown(): void { |
|
74 | + $this->removeFixtures(); |
|
75 | + } |
|
76 | + |
|
77 | + protected function removeFixtures(): void { |
|
78 | + $qb = $this->db->getQueryBuilder(); |
|
79 | + $qb->delete('dav_shares') |
|
80 | + ->where($qb->expr()->in('resourceid', $qb->createNamedParameter($this->resourceIds, IQueryBuilder::PARAM_INT_ARRAY))); |
|
81 | + $qb->executeStatement(); |
|
82 | + } |
|
83 | + |
|
84 | + public function testShareCalendarWithGroup(): void { |
|
85 | + $calendar = $this->createMock(Calendar::class); |
|
86 | + $calendar->method('getResourceId') |
|
87 | + ->willReturn(10001); |
|
88 | + $calendar->method('getOwner') |
|
89 | + ->willReturn('principals/users/admin'); |
|
90 | + |
|
91 | + $this->principalBackend->method('findByUri') |
|
92 | + ->willReturn('principals/groups/alice_bob'); |
|
93 | + |
|
94 | + $this->groupManager->method('groupExists') |
|
95 | + ->willReturn(true); |
|
96 | + |
|
97 | + $this->sharingBackend->updateShares( |
|
98 | + $calendar, |
|
99 | + [['href' => 'principals/groups/alice_bob']], |
|
100 | + [], |
|
101 | + [] |
|
102 | + ); |
|
103 | + |
|
104 | + $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
105 | + } |
|
106 | + |
|
107 | + public function testUnshareCalendarFromGroup(): void { |
|
108 | + $calendar = $this->createMock(Calendar::class); |
|
109 | + $calendar->method('getResourceId') |
|
110 | + ->willReturn(10001); |
|
111 | + $calendar->method('getOwner') |
|
112 | + ->willReturn('principals/users/admin'); |
|
113 | + |
|
114 | + $this->principalBackend->method('findByUri') |
|
115 | + ->willReturn('principals/groups/alice_bob'); |
|
116 | + |
|
117 | + $this->groupManager->method('groupExists') |
|
118 | + ->willReturn(true); |
|
119 | + |
|
120 | + $this->sharingBackend->updateShares( |
|
121 | + shareable: $calendar, |
|
122 | + add: [['href' => 'principals/groups/alice_bob']], |
|
123 | + remove: [], |
|
124 | + ); |
|
125 | + |
|
126 | + $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
127 | + |
|
128 | + $this->sharingBackend->updateShares( |
|
129 | + shareable: $calendar, |
|
130 | + add: [], |
|
131 | + remove: ['principals/groups/alice_bob'], |
|
132 | + ); |
|
133 | + |
|
134 | + $this->assertCount(0, $this->sharingService->getShares(10001)); |
|
135 | + } |
|
136 | + |
|
137 | + public function testShareCalendarWithGroupAndUnshareAsUser(): void { |
|
138 | + $calendar = $this->createMock(Calendar::class); |
|
139 | + $calendar->method('getResourceId') |
|
140 | + ->willReturn(10001); |
|
141 | + $calendar->method('getOwner') |
|
142 | + ->willReturn('principals/users/admin'); |
|
143 | + |
|
144 | + $this->principalBackend->method('findByUri') |
|
145 | + ->willReturnMap([ |
|
146 | + ['principals/groups/alice_bob', '', 'principals/groups/alice_bob'], |
|
147 | + ['principals/users/bob', '', 'principals/users/bob'], |
|
148 | + ]); |
|
149 | + $this->principalBackend->method('getGroupMembership') |
|
150 | + ->willReturn([ |
|
151 | + 'principals/groups/alice_bob', |
|
152 | + ]); |
|
153 | + $this->principalBackend->method('getCircleMembership') |
|
154 | + ->willReturn([]); |
|
155 | + |
|
156 | + $this->groupManager->method('groupExists') |
|
157 | + ->willReturn(true); |
|
158 | + |
|
159 | + /* |
|
160 | 160 | * Owner is sharing the calendar with a group. |
161 | 161 | */ |
162 | - $this->sharingBackend->updateShares( |
|
163 | - shareable: $calendar, |
|
164 | - add: [['href' => 'principals/groups/alice_bob']], |
|
165 | - remove: [], |
|
166 | - ); |
|
162 | + $this->sharingBackend->updateShares( |
|
163 | + shareable: $calendar, |
|
164 | + add: [['href' => 'principals/groups/alice_bob']], |
|
165 | + remove: [], |
|
166 | + ); |
|
167 | 167 | |
168 | - $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
168 | + $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
169 | 169 | |
170 | - /* |
|
170 | + /* |
|
171 | 171 | * Member of the group unshares the calendar. |
172 | 172 | */ |
173 | - $this->sharingBackend->unshare( |
|
174 | - shareable: $calendar, |
|
175 | - principalUri: 'principals/users/bob' |
|
176 | - ); |
|
177 | - |
|
178 | - $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
179 | - $this->assertCount(1, $this->sharingService->getUnshares(10001)); |
|
180 | - } |
|
181 | - |
|
182 | - /** |
|
183 | - * Tests the functionality of sharing a calendar with a user, then with a group (that includes the shared user), |
|
184 | - * and subsequently unsharing it from the individual user. Verifies that the unshare operation correctly removes the specific user share |
|
185 | - * without creating an additional unshare entry. |
|
186 | - */ |
|
187 | - public function testShareCalendarWithUserThenGroupThenUnshareUser(): void { |
|
188 | - $calendar = $this->createMock(Calendar::class); |
|
189 | - $calendar->method('getResourceId') |
|
190 | - ->willReturn(10001); |
|
191 | - $calendar->method('getOwner') |
|
192 | - ->willReturn('principals/users/admin'); |
|
193 | - |
|
194 | - $this->principalBackend->method('findByUri') |
|
195 | - ->willReturnMap([ |
|
196 | - ['principals/groups/alice_bob', '', 'principals/groups/alice_bob'], |
|
197 | - ['principals/users/bob', '', 'principals/users/bob'], |
|
198 | - ]); |
|
199 | - $this->principalBackend->method('getGroupMembership') |
|
200 | - ->willReturn([ |
|
201 | - 'principals/groups/alice_bob', |
|
202 | - ]); |
|
203 | - $this->principalBackend->method('getCircleMembership') |
|
204 | - ->willReturn([]); |
|
205 | - |
|
206 | - $this->userManager->method('userExists') |
|
207 | - ->willReturn(true); |
|
208 | - $this->groupManager->method('groupExists') |
|
209 | - ->willReturn(true); |
|
210 | - |
|
211 | - /* |
|
173 | + $this->sharingBackend->unshare( |
|
174 | + shareable: $calendar, |
|
175 | + principalUri: 'principals/users/bob' |
|
176 | + ); |
|
177 | + |
|
178 | + $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
179 | + $this->assertCount(1, $this->sharingService->getUnshares(10001)); |
|
180 | + } |
|
181 | + |
|
182 | + /** |
|
183 | + * Tests the functionality of sharing a calendar with a user, then with a group (that includes the shared user), |
|
184 | + * and subsequently unsharing it from the individual user. Verifies that the unshare operation correctly removes the specific user share |
|
185 | + * without creating an additional unshare entry. |
|
186 | + */ |
|
187 | + public function testShareCalendarWithUserThenGroupThenUnshareUser(): void { |
|
188 | + $calendar = $this->createMock(Calendar::class); |
|
189 | + $calendar->method('getResourceId') |
|
190 | + ->willReturn(10001); |
|
191 | + $calendar->method('getOwner') |
|
192 | + ->willReturn('principals/users/admin'); |
|
193 | + |
|
194 | + $this->principalBackend->method('findByUri') |
|
195 | + ->willReturnMap([ |
|
196 | + ['principals/groups/alice_bob', '', 'principals/groups/alice_bob'], |
|
197 | + ['principals/users/bob', '', 'principals/users/bob'], |
|
198 | + ]); |
|
199 | + $this->principalBackend->method('getGroupMembership') |
|
200 | + ->willReturn([ |
|
201 | + 'principals/groups/alice_bob', |
|
202 | + ]); |
|
203 | + $this->principalBackend->method('getCircleMembership') |
|
204 | + ->willReturn([]); |
|
205 | + |
|
206 | + $this->userManager->method('userExists') |
|
207 | + ->willReturn(true); |
|
208 | + $this->groupManager->method('groupExists') |
|
209 | + ->willReturn(true); |
|
210 | + |
|
211 | + /* |
|
212 | 212 | * Step 1) The owner shares the calendar with a user. |
213 | 213 | */ |
214 | - $this->sharingBackend->updateShares( |
|
215 | - shareable: $calendar, |
|
216 | - add: [['href' => 'principals/users/bob']], |
|
217 | - remove: [], |
|
218 | - ); |
|
214 | + $this->sharingBackend->updateShares( |
|
215 | + shareable: $calendar, |
|
216 | + add: [['href' => 'principals/users/bob']], |
|
217 | + remove: [], |
|
218 | + ); |
|
219 | 219 | |
220 | - $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
220 | + $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
221 | 221 | |
222 | - /* |
|
222 | + /* |
|
223 | 223 | * Step 2) The owner shares the calendar with a group that includes the |
224 | 224 | * user from step 1 as a member. |
225 | 225 | */ |
226 | - $this->sharingBackend->updateShares( |
|
227 | - shareable: $calendar, |
|
228 | - add: [['href' => 'principals/groups/alice_bob']], |
|
229 | - remove: [], |
|
230 | - ); |
|
226 | + $this->sharingBackend->updateShares( |
|
227 | + shareable: $calendar, |
|
228 | + add: [['href' => 'principals/groups/alice_bob']], |
|
229 | + remove: [], |
|
230 | + ); |
|
231 | 231 | |
232 | - $this->assertCount(2, $this->sharingService->getShares(10001)); |
|
232 | + $this->assertCount(2, $this->sharingService->getShares(10001)); |
|
233 | 233 | |
234 | - /* |
|
234 | + /* |
|
235 | 235 | * Step 3) Unshare the calendar from user as owner. |
236 | 236 | */ |
237 | - $this->sharingBackend->updateShares( |
|
238 | - shareable: $calendar, |
|
239 | - add: [], |
|
240 | - remove: ['principals/users/bob'], |
|
241 | - ); |
|
237 | + $this->sharingBackend->updateShares( |
|
238 | + shareable: $calendar, |
|
239 | + add: [], |
|
240 | + remove: ['principals/users/bob'], |
|
241 | + ); |
|
242 | 242 | |
243 | - /* |
|
243 | + /* |
|
244 | 244 | * The purpose of this test is to ensure that removing a user from a share, as the owner, does not result in an "unshare" row being added. |
245 | 245 | * Instead, the actual user share should be removed. |
246 | 246 | */ |
247 | - $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
248 | - $this->assertCount(0, $this->sharingService->getUnshares(10001)); |
|
249 | - } |
|
247 | + $this->assertCount(1, $this->sharingService->getShares(10001)); |
|
248 | + $this->assertCount(0, $this->sharingService->getUnshares(10001)); |
|
249 | + } |
|
250 | 250 | |
251 | 251 | } |
@@ -24,32 +24,32 @@ discard block |
||
24 | 24 | use Test\Traits\UserTrait; |
25 | 25 | |
26 | 26 | class TestViewDirectory extends View { |
27 | - public function __construct( |
|
28 | - private $updatables, |
|
29 | - private $deletables, |
|
30 | - private $canRename = true, |
|
31 | - ) { |
|
32 | - } |
|
33 | - |
|
34 | - public function isUpdatable($path) { |
|
35 | - return $this->updatables[$path]; |
|
36 | - } |
|
37 | - |
|
38 | - public function isCreatable($path) { |
|
39 | - return $this->updatables[$path]; |
|
40 | - } |
|
41 | - |
|
42 | - public function isDeletable($path) { |
|
43 | - return $this->deletables[$path]; |
|
44 | - } |
|
45 | - |
|
46 | - public function rename($path1, $path2, array $options = []) { |
|
47 | - return $this->canRename; |
|
48 | - } |
|
49 | - |
|
50 | - public function getRelativePath($path): ?string { |
|
51 | - return $path; |
|
52 | - } |
|
27 | + public function __construct( |
|
28 | + private $updatables, |
|
29 | + private $deletables, |
|
30 | + private $canRename = true, |
|
31 | + ) { |
|
32 | + } |
|
33 | + |
|
34 | + public function isUpdatable($path) { |
|
35 | + return $this->updatables[$path]; |
|
36 | + } |
|
37 | + |
|
38 | + public function isCreatable($path) { |
|
39 | + return $this->updatables[$path]; |
|
40 | + } |
|
41 | + |
|
42 | + public function isDeletable($path) { |
|
43 | + return $this->deletables[$path]; |
|
44 | + } |
|
45 | + |
|
46 | + public function rename($path1, $path2, array $options = []) { |
|
47 | + return $this->canRename; |
|
48 | + } |
|
49 | + |
|
50 | + public function getRelativePath($path): ?string { |
|
51 | + return $path; |
|
52 | + } |
|
53 | 53 | } |
54 | 54 | |
55 | 55 | |
@@ -57,431 +57,431 @@ discard block |
||
57 | 57 | * @group DB |
58 | 58 | */ |
59 | 59 | class DirectoryTest extends \Test\TestCase { |
60 | - use UserTrait; |
|
60 | + use UserTrait; |
|
61 | 61 | |
62 | - /** @var View|\PHPUnit\Framework\MockObject\MockObject */ |
|
63 | - private $view; |
|
64 | - /** @var FileInfo|\PHPUnit\Framework\MockObject\MockObject */ |
|
65 | - private $info; |
|
62 | + /** @var View|\PHPUnit\Framework\MockObject\MockObject */ |
|
63 | + private $view; |
|
64 | + /** @var FileInfo|\PHPUnit\Framework\MockObject\MockObject */ |
|
65 | + private $info; |
|
66 | 66 | |
67 | - protected function setUp(): void { |
|
68 | - parent::setUp(); |
|
69 | - |
|
70 | - $this->view = $this->createMock('OC\Files\View'); |
|
71 | - $this->info = $this->createMock('OC\Files\FileInfo'); |
|
72 | - $this->info->method('isReadable') |
|
73 | - ->willReturn(true); |
|
74 | - $this->info->method('getType') |
|
75 | - ->willReturn(Node::TYPE_FOLDER); |
|
76 | - $this->info->method('getName') |
|
77 | - ->willReturn('folder'); |
|
78 | - $this->info->method('getPath') |
|
79 | - ->willReturn('/admin/files/folder'); |
|
80 | - $this->info->method('getPermissions') |
|
81 | - ->willReturn(Constants::PERMISSION_READ); |
|
82 | - } |
|
83 | - |
|
84 | - private function getDir($path = '/') { |
|
85 | - $this->view->expects($this->once()) |
|
86 | - ->method('getRelativePath') |
|
87 | - ->willReturn($path); |
|
88 | - |
|
89 | - $this->info->expects($this->once()) |
|
90 | - ->method('getPath') |
|
91 | - ->willReturn($path); |
|
92 | - |
|
93 | - return new Directory($this->view, $this->info); |
|
94 | - } |
|
95 | - |
|
96 | - |
|
97 | - public function testDeleteRootFolderFails(): void { |
|
98 | - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
99 | - |
|
100 | - $this->info->expects($this->any()) |
|
101 | - ->method('isDeletable') |
|
102 | - ->willReturn(true); |
|
103 | - $this->view->expects($this->never()) |
|
104 | - ->method('rmdir'); |
|
105 | - $dir = $this->getDir(); |
|
106 | - $dir->delete(); |
|
107 | - } |
|
108 | - |
|
109 | - |
|
110 | - public function testDeleteForbidden(): void { |
|
111 | - $this->expectException(Forbidden::class); |
|
112 | - |
|
113 | - // deletion allowed |
|
114 | - $this->info->expects($this->once()) |
|
115 | - ->method('isDeletable') |
|
116 | - ->willReturn(true); |
|
117 | - |
|
118 | - // but fails |
|
119 | - $this->view->expects($this->once()) |
|
120 | - ->method('rmdir') |
|
121 | - ->with('sub') |
|
122 | - ->willThrowException(new ForbiddenException('', true)); |
|
123 | - |
|
124 | - $dir = $this->getDir('sub'); |
|
125 | - $dir->delete(); |
|
126 | - } |
|
127 | - |
|
128 | - |
|
129 | - public function testDeleteFolderWhenAllowed(): void { |
|
130 | - // deletion allowed |
|
131 | - $this->info->expects($this->once()) |
|
132 | - ->method('isDeletable') |
|
133 | - ->willReturn(true); |
|
134 | - |
|
135 | - // but fails |
|
136 | - $this->view->expects($this->once()) |
|
137 | - ->method('rmdir') |
|
138 | - ->with('sub') |
|
139 | - ->willReturn(true); |
|
140 | - |
|
141 | - $dir = $this->getDir('sub'); |
|
142 | - $dir->delete(); |
|
143 | - } |
|
144 | - |
|
145 | - |
|
146 | - public function testDeleteFolderFailsWhenNotAllowed(): void { |
|
147 | - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
148 | - |
|
149 | - $this->info->expects($this->once()) |
|
150 | - ->method('isDeletable') |
|
151 | - ->willReturn(false); |
|
152 | - |
|
153 | - $dir = $this->getDir('sub'); |
|
154 | - $dir->delete(); |
|
155 | - } |
|
156 | - |
|
157 | - |
|
158 | - public function testDeleteFolderThrowsWhenDeletionFailed(): void { |
|
159 | - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
160 | - |
|
161 | - // deletion allowed |
|
162 | - $this->info->expects($this->once()) |
|
163 | - ->method('isDeletable') |
|
164 | - ->willReturn(true); |
|
165 | - |
|
166 | - // but fails |
|
167 | - $this->view->expects($this->once()) |
|
168 | - ->method('rmdir') |
|
169 | - ->with('sub') |
|
170 | - ->willReturn(false); |
|
171 | - |
|
172 | - $dir = $this->getDir('sub'); |
|
173 | - $dir->delete(); |
|
174 | - } |
|
175 | - |
|
176 | - public function testGetChildren(): void { |
|
177 | - $info1 = $this->getMockBuilder(FileInfo::class) |
|
178 | - ->disableOriginalConstructor() |
|
179 | - ->getMock(); |
|
180 | - $info2 = $this->getMockBuilder(FileInfo::class) |
|
181 | - ->disableOriginalConstructor() |
|
182 | - ->getMock(); |
|
183 | - $info1->method('getName') |
|
184 | - ->willReturn('first'); |
|
185 | - $info1->method('getPath') |
|
186 | - ->willReturn('folder/first'); |
|
187 | - $info1->method('getEtag') |
|
188 | - ->willReturn('abc'); |
|
189 | - $info2->method('getName') |
|
190 | - ->willReturn('second'); |
|
191 | - $info2->method('getPath') |
|
192 | - ->willReturn('folder/second'); |
|
193 | - $info2->method('getEtag') |
|
194 | - ->willReturn('def'); |
|
195 | - |
|
196 | - $this->view->expects($this->once()) |
|
197 | - ->method('getDirectoryContent') |
|
198 | - ->willReturn([$info1, $info2]); |
|
199 | - |
|
200 | - $this->view->expects($this->any()) |
|
201 | - ->method('getRelativePath') |
|
202 | - ->willReturnCallback(function ($path) { |
|
203 | - return str_replace('/admin/files/', '', $path); |
|
204 | - }); |
|
205 | - |
|
206 | - $this->view->expects($this->any()) |
|
207 | - ->method('getAbsolutePath') |
|
208 | - ->willReturnCallback(function ($path) { |
|
209 | - return Filesystem::normalizePath('/admin/files' . $path); |
|
210 | - }); |
|
211 | - |
|
212 | - $this->overwriteService(View::class, $this->view); |
|
213 | - |
|
214 | - $dir = new Directory($this->view, $this->info); |
|
215 | - $nodes = $dir->getChildren(); |
|
216 | - |
|
217 | - $this->assertEquals(2, count($nodes)); |
|
218 | - |
|
219 | - // calling a second time just returns the cached values, |
|
220 | - // does not call getDirectoryContents again |
|
221 | - $dir->getChildren(); |
|
222 | - } |
|
223 | - |
|
224 | - |
|
225 | - public function testGetChildrenNoPermission(): void { |
|
226 | - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
227 | - |
|
228 | - $info = $this->createMock(FileInfo::class); |
|
229 | - $info->expects($this->any()) |
|
230 | - ->method('isReadable') |
|
231 | - ->willReturn(false); |
|
232 | - |
|
233 | - $dir = new Directory($this->view, $info); |
|
234 | - $dir->getChildren(); |
|
235 | - } |
|
236 | - |
|
237 | - |
|
238 | - public function testGetChildNoPermission(): void { |
|
239 | - $this->expectException(\Sabre\DAV\Exception\NotFound::class); |
|
240 | - |
|
241 | - $this->info->expects($this->any()) |
|
242 | - ->method('isReadable') |
|
243 | - ->willReturn(false); |
|
244 | - |
|
245 | - $dir = new Directory($this->view, $this->info); |
|
246 | - $dir->getChild('test'); |
|
247 | - } |
|
248 | - |
|
249 | - |
|
250 | - public function testGetChildThrowStorageNotAvailableException(): void { |
|
251 | - $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class); |
|
252 | - |
|
253 | - $this->view->expects($this->once()) |
|
254 | - ->method('getFileInfo') |
|
255 | - ->willThrowException(new StorageNotAvailableException()); |
|
256 | - |
|
257 | - $dir = new Directory($this->view, $this->info); |
|
258 | - $dir->getChild('.'); |
|
259 | - } |
|
260 | - |
|
261 | - |
|
262 | - public function testGetChildThrowInvalidPath(): void { |
|
263 | - $this->expectException(InvalidPath::class); |
|
264 | - |
|
265 | - $this->view->expects($this->once()) |
|
266 | - ->method('verifyPath') |
|
267 | - ->willThrowException(new InvalidPathException()); |
|
268 | - $this->view->expects($this->never()) |
|
269 | - ->method('getFileInfo'); |
|
270 | - |
|
271 | - $dir = new Directory($this->view, $this->info); |
|
272 | - $dir->getChild('.'); |
|
273 | - } |
|
274 | - |
|
275 | - public function testGetQuotaInfoUnlimited(): void { |
|
276 | - self::createUser('user', 'password'); |
|
277 | - self::loginAsUser('user'); |
|
278 | - $mountPoint = $this->createMock(IMountPoint::class); |
|
279 | - $storage = $this->getMockBuilder(Quota::class) |
|
280 | - ->disableOriginalConstructor() |
|
281 | - ->getMock(); |
|
282 | - $mountPoint->method('getStorage') |
|
283 | - ->willReturn($storage); |
|
284 | - |
|
285 | - $storage->expects($this->any()) |
|
286 | - ->method('instanceOfStorage') |
|
287 | - ->willReturnMap([ |
|
288 | - ['\OCA\Files_Sharing\SharedStorage', false], |
|
289 | - ['\OC\Files\Storage\Wrapper\Quota', false], |
|
290 | - [Storage::class, false], |
|
291 | - ]); |
|
292 | - |
|
293 | - $storage->expects($this->once()) |
|
294 | - ->method('getOwner') |
|
295 | - ->willReturn('user'); |
|
296 | - |
|
297 | - $storage->expects($this->never()) |
|
298 | - ->method('getQuota'); |
|
299 | - |
|
300 | - $storage->expects($this->once()) |
|
301 | - ->method('free_space') |
|
302 | - ->willReturn(800); |
|
303 | - |
|
304 | - $this->info->expects($this->any()) |
|
305 | - ->method('getPath') |
|
306 | - ->willReturn('/admin/files/foo'); |
|
307 | - |
|
308 | - $this->info->expects($this->once()) |
|
309 | - ->method('getSize') |
|
310 | - ->willReturn(200); |
|
311 | - |
|
312 | - $this->info->expects($this->once()) |
|
313 | - ->method('getMountPoint') |
|
314 | - ->willReturn($mountPoint); |
|
315 | - |
|
316 | - $this->view->expects($this->any()) |
|
317 | - ->method('getRelativePath') |
|
318 | - ->willReturn('/foo'); |
|
319 | - |
|
320 | - $this->info->expects($this->once()) |
|
321 | - ->method('getInternalPath') |
|
322 | - ->willReturn('/foo'); |
|
323 | - |
|
324 | - $mountPoint->method('getMountPoint') |
|
325 | - ->willReturn('/user/files/mymountpoint'); |
|
326 | - |
|
327 | - $dir = new Directory($this->view, $this->info); |
|
328 | - $this->assertEquals([200, -3], $dir->getQuotaInfo()); //200 used, unlimited |
|
329 | - } |
|
330 | - |
|
331 | - public function testGetQuotaInfoSpecific(): void { |
|
332 | - self::createUser('user', 'password'); |
|
333 | - self::loginAsUser('user'); |
|
334 | - $mountPoint = $this->createMock(IMountPoint::class); |
|
335 | - $storage = $this->getMockBuilder(Quota::class) |
|
336 | - ->disableOriginalConstructor() |
|
337 | - ->getMock(); |
|
338 | - $mountPoint->method('getStorage') |
|
339 | - ->willReturn($storage); |
|
340 | - |
|
341 | - $storage->expects($this->any()) |
|
342 | - ->method('instanceOfStorage') |
|
343 | - ->willReturnMap([ |
|
344 | - ['\OCA\Files_Sharing\SharedStorage', false], |
|
345 | - ['\OC\Files\Storage\Wrapper\Quota', true], |
|
346 | - [Storage::class, false], |
|
347 | - ]); |
|
348 | - |
|
349 | - $storage->expects($this->once()) |
|
350 | - ->method('getOwner') |
|
351 | - ->willReturn('user'); |
|
352 | - |
|
353 | - $storage->expects($this->once()) |
|
354 | - ->method('getQuota') |
|
355 | - ->willReturn(1000); |
|
356 | - |
|
357 | - $storage->expects($this->once()) |
|
358 | - ->method('free_space') |
|
359 | - ->willReturn(800); |
|
360 | - |
|
361 | - $this->info->expects($this->once()) |
|
362 | - ->method('getSize') |
|
363 | - ->willReturn(200); |
|
364 | - |
|
365 | - $this->info->expects($this->once()) |
|
366 | - ->method('getMountPoint') |
|
367 | - ->willReturn($mountPoint); |
|
368 | - |
|
369 | - $this->info->expects($this->once()) |
|
370 | - ->method('getInternalPath') |
|
371 | - ->willReturn('/foo'); |
|
372 | - |
|
373 | - $mountPoint->method('getMountPoint') |
|
374 | - ->willReturn('/user/files/mymountpoint'); |
|
375 | - |
|
376 | - $this->view->expects($this->any()) |
|
377 | - ->method('getRelativePath') |
|
378 | - ->willReturn('/foo'); |
|
379 | - |
|
380 | - $dir = new Directory($this->view, $this->info); |
|
381 | - $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free |
|
382 | - } |
|
383 | - |
|
384 | - /** |
|
385 | - * @dataProvider moveFailedProvider |
|
386 | - */ |
|
387 | - public function testMoveFailed($source, $destination, $updatables, $deletables): void { |
|
388 | - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
389 | - |
|
390 | - $this->moveTest($source, $destination, $updatables, $deletables); |
|
391 | - } |
|
392 | - |
|
393 | - /** |
|
394 | - * @dataProvider moveSuccessProvider |
|
395 | - */ |
|
396 | - public function testMoveSuccess($source, $destination, $updatables, $deletables): void { |
|
397 | - $this->moveTest($source, $destination, $updatables, $deletables); |
|
398 | - $this->addToAssertionCount(1); |
|
399 | - } |
|
400 | - |
|
401 | - /** |
|
402 | - * @dataProvider moveFailedInvalidCharsProvider |
|
403 | - */ |
|
404 | - public function testMoveFailedInvalidChars($source, $destination, $updatables, $deletables): void { |
|
405 | - $this->expectException(InvalidPath::class); |
|
406 | - |
|
407 | - $this->moveTest($source, $destination, $updatables, $deletables); |
|
408 | - } |
|
409 | - |
|
410 | - public function moveFailedInvalidCharsProvider() { |
|
411 | - return [ |
|
412 | - ['a/valid', "a/i\nvalid", ['a' => true, 'a/valid' => true, 'a/c*' => false], []], |
|
413 | - ]; |
|
414 | - } |
|
415 | - |
|
416 | - public function moveFailedProvider() { |
|
417 | - return [ |
|
418 | - ['a/b', 'a/c', ['a' => false, 'a/b' => false, 'a/c' => false], []], |
|
419 | - ['a/b', 'b/b', ['a' => false, 'a/b' => false, 'b' => false, 'b/b' => false], []], |
|
420 | - ['a/b', 'b/b', ['a' => false, 'a/b' => true, 'b' => false, 'b/b' => false], []], |
|
421 | - ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => false, 'b/b' => false], []], |
|
422 | - ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => false]], |
|
423 | - ['a/b', 'a/c', ['a' => false, 'a/b' => true, 'a/c' => false], []], |
|
424 | - ]; |
|
425 | - } |
|
426 | - |
|
427 | - public function moveSuccessProvider() { |
|
428 | - return [ |
|
429 | - ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => true]], |
|
430 | - // older files with special chars can still be renamed to valid names |
|
431 | - ['a/b*', 'b/b', ['a' => true, 'a/b*' => true, 'b' => true, 'b/b' => false], ['a/b*' => true]], |
|
432 | - ]; |
|
433 | - } |
|
434 | - |
|
435 | - /** |
|
436 | - * @param $source |
|
437 | - * @param $destination |
|
438 | - * @param $updatables |
|
439 | - */ |
|
440 | - private function moveTest($source, $destination, $updatables, $deletables): void { |
|
441 | - $view = new TestViewDirectory($updatables, $deletables); |
|
442 | - |
|
443 | - $sourceInfo = new FileInfo($source, null, null, [ |
|
444 | - 'type' => FileInfo::TYPE_FOLDER, |
|
445 | - ], null); |
|
446 | - $targetInfo = new FileInfo(dirname($destination), null, null, [ |
|
447 | - 'type' => FileInfo::TYPE_FOLDER, |
|
448 | - ], null); |
|
449 | - |
|
450 | - $sourceNode = new Directory($view, $sourceInfo); |
|
451 | - $targetNode = $this->getMockBuilder(Directory::class) |
|
452 | - ->setMethods(['childExists']) |
|
453 | - ->setConstructorArgs([$view, $targetInfo]) |
|
454 | - ->getMock(); |
|
455 | - $targetNode->expects($this->any())->method('childExists') |
|
456 | - ->with(basename($destination)) |
|
457 | - ->willReturn(false); |
|
458 | - $this->assertTrue($targetNode->moveInto(basename($destination), $source, $sourceNode)); |
|
459 | - } |
|
460 | - |
|
461 | - |
|
462 | - public function testFailingMove(): void { |
|
463 | - $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
464 | - $this->expectExceptionMessage('Could not copy directory b, target exists'); |
|
465 | - |
|
466 | - $source = 'a/b'; |
|
467 | - $destination = 'c/b'; |
|
468 | - $updatables = ['a' => true, 'a/b' => true, 'b' => true, 'c/b' => false]; |
|
469 | - $deletables = ['a/b' => true]; |
|
470 | - |
|
471 | - $view = new TestViewDirectory($updatables, $deletables); |
|
472 | - |
|
473 | - $sourceInfo = new FileInfo($source, null, null, ['type' => FileInfo::TYPE_FOLDER], null); |
|
474 | - $targetInfo = new FileInfo(dirname($destination), null, null, ['type' => FileInfo::TYPE_FOLDER], null); |
|
475 | - |
|
476 | - $sourceNode = new Directory($view, $sourceInfo); |
|
477 | - $targetNode = $this->getMockBuilder(Directory::class) |
|
478 | - ->onlyMethods(['childExists']) |
|
479 | - ->setConstructorArgs([$view, $targetInfo]) |
|
480 | - ->getMock(); |
|
481 | - $targetNode->expects($this->once())->method('childExists') |
|
482 | - ->with(basename($destination)) |
|
483 | - ->willReturn(true); |
|
484 | - |
|
485 | - $targetNode->moveInto(basename($destination), $source, $sourceNode); |
|
486 | - } |
|
67 | + protected function setUp(): void { |
|
68 | + parent::setUp(); |
|
69 | + |
|
70 | + $this->view = $this->createMock('OC\Files\View'); |
|
71 | + $this->info = $this->createMock('OC\Files\FileInfo'); |
|
72 | + $this->info->method('isReadable') |
|
73 | + ->willReturn(true); |
|
74 | + $this->info->method('getType') |
|
75 | + ->willReturn(Node::TYPE_FOLDER); |
|
76 | + $this->info->method('getName') |
|
77 | + ->willReturn('folder'); |
|
78 | + $this->info->method('getPath') |
|
79 | + ->willReturn('/admin/files/folder'); |
|
80 | + $this->info->method('getPermissions') |
|
81 | + ->willReturn(Constants::PERMISSION_READ); |
|
82 | + } |
|
83 | + |
|
84 | + private function getDir($path = '/') { |
|
85 | + $this->view->expects($this->once()) |
|
86 | + ->method('getRelativePath') |
|
87 | + ->willReturn($path); |
|
88 | + |
|
89 | + $this->info->expects($this->once()) |
|
90 | + ->method('getPath') |
|
91 | + ->willReturn($path); |
|
92 | + |
|
93 | + return new Directory($this->view, $this->info); |
|
94 | + } |
|
95 | + |
|
96 | + |
|
97 | + public function testDeleteRootFolderFails(): void { |
|
98 | + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
99 | + |
|
100 | + $this->info->expects($this->any()) |
|
101 | + ->method('isDeletable') |
|
102 | + ->willReturn(true); |
|
103 | + $this->view->expects($this->never()) |
|
104 | + ->method('rmdir'); |
|
105 | + $dir = $this->getDir(); |
|
106 | + $dir->delete(); |
|
107 | + } |
|
108 | + |
|
109 | + |
|
110 | + public function testDeleteForbidden(): void { |
|
111 | + $this->expectException(Forbidden::class); |
|
112 | + |
|
113 | + // deletion allowed |
|
114 | + $this->info->expects($this->once()) |
|
115 | + ->method('isDeletable') |
|
116 | + ->willReturn(true); |
|
117 | + |
|
118 | + // but fails |
|
119 | + $this->view->expects($this->once()) |
|
120 | + ->method('rmdir') |
|
121 | + ->with('sub') |
|
122 | + ->willThrowException(new ForbiddenException('', true)); |
|
123 | + |
|
124 | + $dir = $this->getDir('sub'); |
|
125 | + $dir->delete(); |
|
126 | + } |
|
127 | + |
|
128 | + |
|
129 | + public function testDeleteFolderWhenAllowed(): void { |
|
130 | + // deletion allowed |
|
131 | + $this->info->expects($this->once()) |
|
132 | + ->method('isDeletable') |
|
133 | + ->willReturn(true); |
|
134 | + |
|
135 | + // but fails |
|
136 | + $this->view->expects($this->once()) |
|
137 | + ->method('rmdir') |
|
138 | + ->with('sub') |
|
139 | + ->willReturn(true); |
|
140 | + |
|
141 | + $dir = $this->getDir('sub'); |
|
142 | + $dir->delete(); |
|
143 | + } |
|
144 | + |
|
145 | + |
|
146 | + public function testDeleteFolderFailsWhenNotAllowed(): void { |
|
147 | + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
148 | + |
|
149 | + $this->info->expects($this->once()) |
|
150 | + ->method('isDeletable') |
|
151 | + ->willReturn(false); |
|
152 | + |
|
153 | + $dir = $this->getDir('sub'); |
|
154 | + $dir->delete(); |
|
155 | + } |
|
156 | + |
|
157 | + |
|
158 | + public function testDeleteFolderThrowsWhenDeletionFailed(): void { |
|
159 | + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
160 | + |
|
161 | + // deletion allowed |
|
162 | + $this->info->expects($this->once()) |
|
163 | + ->method('isDeletable') |
|
164 | + ->willReturn(true); |
|
165 | + |
|
166 | + // but fails |
|
167 | + $this->view->expects($this->once()) |
|
168 | + ->method('rmdir') |
|
169 | + ->with('sub') |
|
170 | + ->willReturn(false); |
|
171 | + |
|
172 | + $dir = $this->getDir('sub'); |
|
173 | + $dir->delete(); |
|
174 | + } |
|
175 | + |
|
176 | + public function testGetChildren(): void { |
|
177 | + $info1 = $this->getMockBuilder(FileInfo::class) |
|
178 | + ->disableOriginalConstructor() |
|
179 | + ->getMock(); |
|
180 | + $info2 = $this->getMockBuilder(FileInfo::class) |
|
181 | + ->disableOriginalConstructor() |
|
182 | + ->getMock(); |
|
183 | + $info1->method('getName') |
|
184 | + ->willReturn('first'); |
|
185 | + $info1->method('getPath') |
|
186 | + ->willReturn('folder/first'); |
|
187 | + $info1->method('getEtag') |
|
188 | + ->willReturn('abc'); |
|
189 | + $info2->method('getName') |
|
190 | + ->willReturn('second'); |
|
191 | + $info2->method('getPath') |
|
192 | + ->willReturn('folder/second'); |
|
193 | + $info2->method('getEtag') |
|
194 | + ->willReturn('def'); |
|
195 | + |
|
196 | + $this->view->expects($this->once()) |
|
197 | + ->method('getDirectoryContent') |
|
198 | + ->willReturn([$info1, $info2]); |
|
199 | + |
|
200 | + $this->view->expects($this->any()) |
|
201 | + ->method('getRelativePath') |
|
202 | + ->willReturnCallback(function ($path) { |
|
203 | + return str_replace('/admin/files/', '', $path); |
|
204 | + }); |
|
205 | + |
|
206 | + $this->view->expects($this->any()) |
|
207 | + ->method('getAbsolutePath') |
|
208 | + ->willReturnCallback(function ($path) { |
|
209 | + return Filesystem::normalizePath('/admin/files' . $path); |
|
210 | + }); |
|
211 | + |
|
212 | + $this->overwriteService(View::class, $this->view); |
|
213 | + |
|
214 | + $dir = new Directory($this->view, $this->info); |
|
215 | + $nodes = $dir->getChildren(); |
|
216 | + |
|
217 | + $this->assertEquals(2, count($nodes)); |
|
218 | + |
|
219 | + // calling a second time just returns the cached values, |
|
220 | + // does not call getDirectoryContents again |
|
221 | + $dir->getChildren(); |
|
222 | + } |
|
223 | + |
|
224 | + |
|
225 | + public function testGetChildrenNoPermission(): void { |
|
226 | + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
227 | + |
|
228 | + $info = $this->createMock(FileInfo::class); |
|
229 | + $info->expects($this->any()) |
|
230 | + ->method('isReadable') |
|
231 | + ->willReturn(false); |
|
232 | + |
|
233 | + $dir = new Directory($this->view, $info); |
|
234 | + $dir->getChildren(); |
|
235 | + } |
|
236 | + |
|
237 | + |
|
238 | + public function testGetChildNoPermission(): void { |
|
239 | + $this->expectException(\Sabre\DAV\Exception\NotFound::class); |
|
240 | + |
|
241 | + $this->info->expects($this->any()) |
|
242 | + ->method('isReadable') |
|
243 | + ->willReturn(false); |
|
244 | + |
|
245 | + $dir = new Directory($this->view, $this->info); |
|
246 | + $dir->getChild('test'); |
|
247 | + } |
|
248 | + |
|
249 | + |
|
250 | + public function testGetChildThrowStorageNotAvailableException(): void { |
|
251 | + $this->expectException(\Sabre\DAV\Exception\ServiceUnavailable::class); |
|
252 | + |
|
253 | + $this->view->expects($this->once()) |
|
254 | + ->method('getFileInfo') |
|
255 | + ->willThrowException(new StorageNotAvailableException()); |
|
256 | + |
|
257 | + $dir = new Directory($this->view, $this->info); |
|
258 | + $dir->getChild('.'); |
|
259 | + } |
|
260 | + |
|
261 | + |
|
262 | + public function testGetChildThrowInvalidPath(): void { |
|
263 | + $this->expectException(InvalidPath::class); |
|
264 | + |
|
265 | + $this->view->expects($this->once()) |
|
266 | + ->method('verifyPath') |
|
267 | + ->willThrowException(new InvalidPathException()); |
|
268 | + $this->view->expects($this->never()) |
|
269 | + ->method('getFileInfo'); |
|
270 | + |
|
271 | + $dir = new Directory($this->view, $this->info); |
|
272 | + $dir->getChild('.'); |
|
273 | + } |
|
274 | + |
|
275 | + public function testGetQuotaInfoUnlimited(): void { |
|
276 | + self::createUser('user', 'password'); |
|
277 | + self::loginAsUser('user'); |
|
278 | + $mountPoint = $this->createMock(IMountPoint::class); |
|
279 | + $storage = $this->getMockBuilder(Quota::class) |
|
280 | + ->disableOriginalConstructor() |
|
281 | + ->getMock(); |
|
282 | + $mountPoint->method('getStorage') |
|
283 | + ->willReturn($storage); |
|
284 | + |
|
285 | + $storage->expects($this->any()) |
|
286 | + ->method('instanceOfStorage') |
|
287 | + ->willReturnMap([ |
|
288 | + ['\OCA\Files_Sharing\SharedStorage', false], |
|
289 | + ['\OC\Files\Storage\Wrapper\Quota', false], |
|
290 | + [Storage::class, false], |
|
291 | + ]); |
|
292 | + |
|
293 | + $storage->expects($this->once()) |
|
294 | + ->method('getOwner') |
|
295 | + ->willReturn('user'); |
|
296 | + |
|
297 | + $storage->expects($this->never()) |
|
298 | + ->method('getQuota'); |
|
299 | + |
|
300 | + $storage->expects($this->once()) |
|
301 | + ->method('free_space') |
|
302 | + ->willReturn(800); |
|
303 | + |
|
304 | + $this->info->expects($this->any()) |
|
305 | + ->method('getPath') |
|
306 | + ->willReturn('/admin/files/foo'); |
|
307 | + |
|
308 | + $this->info->expects($this->once()) |
|
309 | + ->method('getSize') |
|
310 | + ->willReturn(200); |
|
311 | + |
|
312 | + $this->info->expects($this->once()) |
|
313 | + ->method('getMountPoint') |
|
314 | + ->willReturn($mountPoint); |
|
315 | + |
|
316 | + $this->view->expects($this->any()) |
|
317 | + ->method('getRelativePath') |
|
318 | + ->willReturn('/foo'); |
|
319 | + |
|
320 | + $this->info->expects($this->once()) |
|
321 | + ->method('getInternalPath') |
|
322 | + ->willReturn('/foo'); |
|
323 | + |
|
324 | + $mountPoint->method('getMountPoint') |
|
325 | + ->willReturn('/user/files/mymountpoint'); |
|
326 | + |
|
327 | + $dir = new Directory($this->view, $this->info); |
|
328 | + $this->assertEquals([200, -3], $dir->getQuotaInfo()); //200 used, unlimited |
|
329 | + } |
|
330 | + |
|
331 | + public function testGetQuotaInfoSpecific(): void { |
|
332 | + self::createUser('user', 'password'); |
|
333 | + self::loginAsUser('user'); |
|
334 | + $mountPoint = $this->createMock(IMountPoint::class); |
|
335 | + $storage = $this->getMockBuilder(Quota::class) |
|
336 | + ->disableOriginalConstructor() |
|
337 | + ->getMock(); |
|
338 | + $mountPoint->method('getStorage') |
|
339 | + ->willReturn($storage); |
|
340 | + |
|
341 | + $storage->expects($this->any()) |
|
342 | + ->method('instanceOfStorage') |
|
343 | + ->willReturnMap([ |
|
344 | + ['\OCA\Files_Sharing\SharedStorage', false], |
|
345 | + ['\OC\Files\Storage\Wrapper\Quota', true], |
|
346 | + [Storage::class, false], |
|
347 | + ]); |
|
348 | + |
|
349 | + $storage->expects($this->once()) |
|
350 | + ->method('getOwner') |
|
351 | + ->willReturn('user'); |
|
352 | + |
|
353 | + $storage->expects($this->once()) |
|
354 | + ->method('getQuota') |
|
355 | + ->willReturn(1000); |
|
356 | + |
|
357 | + $storage->expects($this->once()) |
|
358 | + ->method('free_space') |
|
359 | + ->willReturn(800); |
|
360 | + |
|
361 | + $this->info->expects($this->once()) |
|
362 | + ->method('getSize') |
|
363 | + ->willReturn(200); |
|
364 | + |
|
365 | + $this->info->expects($this->once()) |
|
366 | + ->method('getMountPoint') |
|
367 | + ->willReturn($mountPoint); |
|
368 | + |
|
369 | + $this->info->expects($this->once()) |
|
370 | + ->method('getInternalPath') |
|
371 | + ->willReturn('/foo'); |
|
372 | + |
|
373 | + $mountPoint->method('getMountPoint') |
|
374 | + ->willReturn('/user/files/mymountpoint'); |
|
375 | + |
|
376 | + $this->view->expects($this->any()) |
|
377 | + ->method('getRelativePath') |
|
378 | + ->willReturn('/foo'); |
|
379 | + |
|
380 | + $dir = new Directory($this->view, $this->info); |
|
381 | + $this->assertEquals([200, 800], $dir->getQuotaInfo()); //200 used, 800 free |
|
382 | + } |
|
383 | + |
|
384 | + /** |
|
385 | + * @dataProvider moveFailedProvider |
|
386 | + */ |
|
387 | + public function testMoveFailed($source, $destination, $updatables, $deletables): void { |
|
388 | + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
389 | + |
|
390 | + $this->moveTest($source, $destination, $updatables, $deletables); |
|
391 | + } |
|
392 | + |
|
393 | + /** |
|
394 | + * @dataProvider moveSuccessProvider |
|
395 | + */ |
|
396 | + public function testMoveSuccess($source, $destination, $updatables, $deletables): void { |
|
397 | + $this->moveTest($source, $destination, $updatables, $deletables); |
|
398 | + $this->addToAssertionCount(1); |
|
399 | + } |
|
400 | + |
|
401 | + /** |
|
402 | + * @dataProvider moveFailedInvalidCharsProvider |
|
403 | + */ |
|
404 | + public function testMoveFailedInvalidChars($source, $destination, $updatables, $deletables): void { |
|
405 | + $this->expectException(InvalidPath::class); |
|
406 | + |
|
407 | + $this->moveTest($source, $destination, $updatables, $deletables); |
|
408 | + } |
|
409 | + |
|
410 | + public function moveFailedInvalidCharsProvider() { |
|
411 | + return [ |
|
412 | + ['a/valid', "a/i\nvalid", ['a' => true, 'a/valid' => true, 'a/c*' => false], []], |
|
413 | + ]; |
|
414 | + } |
|
415 | + |
|
416 | + public function moveFailedProvider() { |
|
417 | + return [ |
|
418 | + ['a/b', 'a/c', ['a' => false, 'a/b' => false, 'a/c' => false], []], |
|
419 | + ['a/b', 'b/b', ['a' => false, 'a/b' => false, 'b' => false, 'b/b' => false], []], |
|
420 | + ['a/b', 'b/b', ['a' => false, 'a/b' => true, 'b' => false, 'b/b' => false], []], |
|
421 | + ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => false, 'b/b' => false], []], |
|
422 | + ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => false]], |
|
423 | + ['a/b', 'a/c', ['a' => false, 'a/b' => true, 'a/c' => false], []], |
|
424 | + ]; |
|
425 | + } |
|
426 | + |
|
427 | + public function moveSuccessProvider() { |
|
428 | + return [ |
|
429 | + ['a/b', 'b/b', ['a' => true, 'a/b' => true, 'b' => true, 'b/b' => false], ['a/b' => true]], |
|
430 | + // older files with special chars can still be renamed to valid names |
|
431 | + ['a/b*', 'b/b', ['a' => true, 'a/b*' => true, 'b' => true, 'b/b' => false], ['a/b*' => true]], |
|
432 | + ]; |
|
433 | + } |
|
434 | + |
|
435 | + /** |
|
436 | + * @param $source |
|
437 | + * @param $destination |
|
438 | + * @param $updatables |
|
439 | + */ |
|
440 | + private function moveTest($source, $destination, $updatables, $deletables): void { |
|
441 | + $view = new TestViewDirectory($updatables, $deletables); |
|
442 | + |
|
443 | + $sourceInfo = new FileInfo($source, null, null, [ |
|
444 | + 'type' => FileInfo::TYPE_FOLDER, |
|
445 | + ], null); |
|
446 | + $targetInfo = new FileInfo(dirname($destination), null, null, [ |
|
447 | + 'type' => FileInfo::TYPE_FOLDER, |
|
448 | + ], null); |
|
449 | + |
|
450 | + $sourceNode = new Directory($view, $sourceInfo); |
|
451 | + $targetNode = $this->getMockBuilder(Directory::class) |
|
452 | + ->setMethods(['childExists']) |
|
453 | + ->setConstructorArgs([$view, $targetInfo]) |
|
454 | + ->getMock(); |
|
455 | + $targetNode->expects($this->any())->method('childExists') |
|
456 | + ->with(basename($destination)) |
|
457 | + ->willReturn(false); |
|
458 | + $this->assertTrue($targetNode->moveInto(basename($destination), $source, $sourceNode)); |
|
459 | + } |
|
460 | + |
|
461 | + |
|
462 | + public function testFailingMove(): void { |
|
463 | + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); |
|
464 | + $this->expectExceptionMessage('Could not copy directory b, target exists'); |
|
465 | + |
|
466 | + $source = 'a/b'; |
|
467 | + $destination = 'c/b'; |
|
468 | + $updatables = ['a' => true, 'a/b' => true, 'b' => true, 'c/b' => false]; |
|
469 | + $deletables = ['a/b' => true]; |
|
470 | + |
|
471 | + $view = new TestViewDirectory($updatables, $deletables); |
|
472 | + |
|
473 | + $sourceInfo = new FileInfo($source, null, null, ['type' => FileInfo::TYPE_FOLDER], null); |
|
474 | + $targetInfo = new FileInfo(dirname($destination), null, null, ['type' => FileInfo::TYPE_FOLDER], null); |
|
475 | + |
|
476 | + $sourceNode = new Directory($view, $sourceInfo); |
|
477 | + $targetNode = $this->getMockBuilder(Directory::class) |
|
478 | + ->onlyMethods(['childExists']) |
|
479 | + ->setConstructorArgs([$view, $targetInfo]) |
|
480 | + ->getMock(); |
|
481 | + $targetNode->expects($this->once())->method('childExists') |
|
482 | + ->with(basename($destination)) |
|
483 | + ->willReturn(true); |
|
484 | + |
|
485 | + $targetNode->moveInto(basename($destination), $source, $sourceNode); |
|
486 | + } |
|
487 | 487 | } |
@@ -33,790 +33,790 @@ |
||
33 | 33 | * represents an LDAP user, gets and holds user-specific information from LDAP |
34 | 34 | */ |
35 | 35 | class User { |
36 | - protected Connection $connection; |
|
37 | - /** |
|
38 | - * @var array<string,1> |
|
39 | - */ |
|
40 | - protected array $refreshedFeatures = []; |
|
41 | - protected string|false|null $avatarImage = null; |
|
42 | - |
|
43 | - protected BirthdateParserService $birthdateParser; |
|
44 | - |
|
45 | - /** |
|
46 | - * DB config keys for user preferences |
|
47 | - * @var string |
|
48 | - */ |
|
49 | - public const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished'; |
|
50 | - |
|
51 | - /** |
|
52 | - * @brief constructor, make sure the subclasses call this one! |
|
53 | - */ |
|
54 | - public function __construct( |
|
55 | - protected string $uid, |
|
56 | - protected string $dn, |
|
57 | - protected Access $access, |
|
58 | - protected IConfig $config, |
|
59 | - protected Image $image, |
|
60 | - protected LoggerInterface $logger, |
|
61 | - protected IAvatarManager $avatarManager, |
|
62 | - protected IUserManager $userManager, |
|
63 | - protected INotificationManager $notificationManager, |
|
64 | - ) { |
|
65 | - if ($uid === '') { |
|
66 | - $logger->error("uid for '$dn' must not be an empty string", ['app' => 'user_ldap']); |
|
67 | - throw new \InvalidArgumentException('uid must not be an empty string!'); |
|
68 | - } |
|
69 | - $this->connection = $this->access->getConnection(); |
|
70 | - $this->birthdateParser = new BirthdateParserService(); |
|
71 | - |
|
72 | - Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); |
|
73 | - } |
|
74 | - |
|
75 | - /** |
|
76 | - * marks a user as deleted |
|
77 | - * |
|
78 | - * @throws PreConditionNotMetException |
|
79 | - */ |
|
80 | - public function markUser(): void { |
|
81 | - $curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0'); |
|
82 | - if ($curValue === '1') { |
|
83 | - // the user is already marked, do not write to DB again |
|
84 | - return; |
|
85 | - } |
|
86 | - $this->config->setUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '1'); |
|
87 | - $this->config->setUserValue($this->getUsername(), 'user_ldap', 'foundDeleted', (string)time()); |
|
88 | - } |
|
89 | - |
|
90 | - /** |
|
91 | - * processes results from LDAP for attributes as returned by getAttributesToRead() |
|
92 | - * @param array $ldapEntry the user entry as retrieved from LDAP |
|
93 | - */ |
|
94 | - public function processAttributes(array $ldapEntry): void { |
|
95 | - //Quota |
|
96 | - $attr = strtolower($this->connection->ldapQuotaAttribute); |
|
97 | - if (isset($ldapEntry[$attr])) { |
|
98 | - $this->updateQuota($ldapEntry[$attr][0]); |
|
99 | - } else { |
|
100 | - if ($this->connection->ldapQuotaDefault !== '') { |
|
101 | - $this->updateQuota(); |
|
102 | - } |
|
103 | - } |
|
104 | - unset($attr); |
|
105 | - |
|
106 | - //displayName |
|
107 | - $displayName = $displayName2 = ''; |
|
108 | - $attr = strtolower($this->connection->ldapUserDisplayName); |
|
109 | - if (isset($ldapEntry[$attr])) { |
|
110 | - $displayName = (string)$ldapEntry[$attr][0]; |
|
111 | - } |
|
112 | - $attr = strtolower($this->connection->ldapUserDisplayName2); |
|
113 | - if (isset($ldapEntry[$attr])) { |
|
114 | - $displayName2 = (string)$ldapEntry[$attr][0]; |
|
115 | - } |
|
116 | - if ($displayName !== '') { |
|
117 | - $this->composeAndStoreDisplayName($displayName, $displayName2); |
|
118 | - $this->access->cacheUserDisplayName( |
|
119 | - $this->getUsername(), |
|
120 | - $displayName, |
|
121 | - $displayName2 |
|
122 | - ); |
|
123 | - } |
|
124 | - unset($attr); |
|
125 | - |
|
126 | ||
127 | - //email must be stored after displayname, because it would cause a user |
|
128 | - //change event that will trigger fetching the display name again |
|
129 | - $attr = strtolower($this->connection->ldapEmailAttribute); |
|
130 | - if (isset($ldapEntry[$attr])) { |
|
131 | - $mailValue = 0; |
|
132 | - for ($x = 0; $x < count($ldapEntry[$attr]); $x++) { |
|
133 | - if (filter_var($ldapEntry[$attr][$x], FILTER_VALIDATE_EMAIL)) { |
|
134 | - $mailValue = $x; |
|
135 | - break; |
|
136 | - } |
|
137 | - } |
|
138 | - $this->updateEmail($ldapEntry[$attr][$mailValue]); |
|
139 | - } |
|
140 | - unset($attr); |
|
141 | - |
|
142 | - // LDAP Username, needed for s2s sharing |
|
143 | - if (isset($ldapEntry['uid'])) { |
|
144 | - $this->storeLDAPUserName($ldapEntry['uid'][0]); |
|
145 | - } elseif (isset($ldapEntry['samaccountname'])) { |
|
146 | - $this->storeLDAPUserName($ldapEntry['samaccountname'][0]); |
|
147 | - } |
|
148 | - |
|
149 | - //homePath |
|
150 | - if (str_starts_with($this->connection->homeFolderNamingRule, 'attr:')) { |
|
151 | - $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:'))); |
|
152 | - if (isset($ldapEntry[$attr])) { |
|
153 | - $this->access->cacheUserHome( |
|
154 | - $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0])); |
|
155 | - } |
|
156 | - } |
|
157 | - |
|
158 | - //memberOf groups |
|
159 | - $cacheKey = 'getMemberOf' . $this->getUsername(); |
|
160 | - $groups = false; |
|
161 | - if (isset($ldapEntry['memberof'])) { |
|
162 | - $groups = $ldapEntry['memberof']; |
|
163 | - } |
|
164 | - $this->connection->writeToCache($cacheKey, $groups); |
|
165 | - |
|
166 | - //external storage var |
|
167 | - $attr = strtolower($this->connection->ldapExtStorageHomeAttribute); |
|
168 | - if (isset($ldapEntry[$attr])) { |
|
169 | - $this->updateExtStorageHome($ldapEntry[$attr][0]); |
|
170 | - } |
|
171 | - unset($attr); |
|
172 | - |
|
173 | - // check for cached profile data |
|
174 | - $username = $this->getUsername(); // buffer variable, to save resource |
|
175 | - $cacheKey = 'getUserProfile-' . $username; |
|
176 | - $profileCached = $this->connection->getFromCache($cacheKey); |
|
177 | - // honoring profile disabled in config.php and check if user profile was refreshed |
|
178 | - if ($this->config->getSystemValueBool('profile.enabled', true) && |
|
179 | - ($profileCached === null) && // no cache or TTL not expired |
|
180 | - !$this->wasRefreshed('profile')) { |
|
181 | - // check current data |
|
182 | - $profileValues = []; |
|
183 | - //User Profile Field - Phone number |
|
184 | - $attr = strtolower($this->connection->ldapAttributePhone); |
|
185 | - if (!empty($attr)) { // attribute configured |
|
186 | - $profileValues[IAccountManager::PROPERTY_PHONE] |
|
187 | - = $ldapEntry[$attr][0] ?? ''; |
|
188 | - } |
|
189 | - //User Profile Field - website |
|
190 | - $attr = strtolower($this->connection->ldapAttributeWebsite); |
|
191 | - if (isset($ldapEntry[$attr])) { |
|
192 | - $cutPosition = strpos($ldapEntry[$attr][0], ' '); |
|
193 | - if ($cutPosition) { |
|
194 | - // drop appended label |
|
195 | - $profileValues[IAccountManager::PROPERTY_WEBSITE] |
|
196 | - = substr($ldapEntry[$attr][0], 0, $cutPosition); |
|
197 | - } else { |
|
198 | - $profileValues[IAccountManager::PROPERTY_WEBSITE] |
|
199 | - = $ldapEntry[$attr][0]; |
|
200 | - } |
|
201 | - } elseif (!empty($attr)) { // configured, but not defined |
|
202 | - $profileValues[IAccountManager::PROPERTY_WEBSITE] = ''; |
|
203 | - } |
|
204 | - //User Profile Field - Address |
|
205 | - $attr = strtolower($this->connection->ldapAttributeAddress); |
|
206 | - if (isset($ldapEntry[$attr])) { |
|
207 | - if (str_contains($ldapEntry[$attr][0], '$')) { |
|
208 | - // basic format conversion from postalAddress syntax to commata delimited |
|
209 | - $profileValues[IAccountManager::PROPERTY_ADDRESS] |
|
210 | - = str_replace('$', ', ', $ldapEntry[$attr][0]); |
|
211 | - } else { |
|
212 | - $profileValues[IAccountManager::PROPERTY_ADDRESS] |
|
213 | - = $ldapEntry[$attr][0]; |
|
214 | - } |
|
215 | - } elseif (!empty($attr)) { // configured, but not defined |
|
216 | - $profileValues[IAccountManager::PROPERTY_ADDRESS] = ''; |
|
217 | - } |
|
218 | - //User Profile Field - Twitter |
|
219 | - $attr = strtolower($this->connection->ldapAttributeTwitter); |
|
220 | - if (!empty($attr)) { |
|
221 | - $profileValues[IAccountManager::PROPERTY_TWITTER] |
|
222 | - = $ldapEntry[$attr][0] ?? ''; |
|
223 | - } |
|
224 | - //User Profile Field - fediverse |
|
225 | - $attr = strtolower($this->connection->ldapAttributeFediverse); |
|
226 | - if (!empty($attr)) { |
|
227 | - $profileValues[IAccountManager::PROPERTY_FEDIVERSE] |
|
228 | - = $ldapEntry[$attr][0] ?? ''; |
|
229 | - } |
|
230 | - //User Profile Field - organisation |
|
231 | - $attr = strtolower($this->connection->ldapAttributeOrganisation); |
|
232 | - if (!empty($attr)) { |
|
233 | - $profileValues[IAccountManager::PROPERTY_ORGANISATION] |
|
234 | - = $ldapEntry[$attr][0] ?? ''; |
|
235 | - } |
|
236 | - //User Profile Field - role |
|
237 | - $attr = strtolower($this->connection->ldapAttributeRole); |
|
238 | - if (!empty($attr)) { |
|
239 | - $profileValues[IAccountManager::PROPERTY_ROLE] |
|
240 | - = $ldapEntry[$attr][0] ?? ''; |
|
241 | - } |
|
242 | - //User Profile Field - headline |
|
243 | - $attr = strtolower($this->connection->ldapAttributeHeadline); |
|
244 | - if (!empty($attr)) { |
|
245 | - $profileValues[IAccountManager::PROPERTY_HEADLINE] |
|
246 | - = $ldapEntry[$attr][0] ?? ''; |
|
247 | - } |
|
248 | - //User Profile Field - biography |
|
249 | - $attr = strtolower($this->connection->ldapAttributeBiography); |
|
250 | - if (isset($ldapEntry[$attr])) { |
|
251 | - if (str_contains($ldapEntry[$attr][0], '\r')) { |
|
252 | - // convert line endings |
|
253 | - $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] |
|
254 | - = str_replace(["\r\n","\r"], "\n", $ldapEntry[$attr][0]); |
|
255 | - } else { |
|
256 | - $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] |
|
257 | - = $ldapEntry[$attr][0]; |
|
258 | - } |
|
259 | - } elseif (!empty($attr)) { // configured, but not defined |
|
260 | - $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] = ''; |
|
261 | - } |
|
262 | - //User Profile Field - birthday |
|
263 | - $attr = strtolower($this->connection->ldapAttributeBirthDate); |
|
264 | - if (!empty($attr) && !empty($ldapEntry[$attr][0])) { |
|
265 | - $value = $ldapEntry[$attr][0]; |
|
266 | - try { |
|
267 | - $birthdate = $this->birthdateParser->parseBirthdate($value); |
|
268 | - $profileValues[IAccountManager::PROPERTY_BIRTHDATE] |
|
269 | - = $birthdate->format('Y-m-d'); |
|
270 | - } catch (InvalidArgumentException $e) { |
|
271 | - // Invalid date -> just skip the property |
|
272 | - $this->logger->info("Failed to parse user's birthdate from LDAP: $value", [ |
|
273 | - 'exception' => $e, |
|
274 | - 'userId' => $username, |
|
275 | - ]); |
|
276 | - } |
|
277 | - } |
|
278 | - //User Profile Field - pronouns |
|
279 | - $attr = strtolower($this->connection->ldapAttributePronouns); |
|
280 | - if (!empty($attr)) { |
|
281 | - $profileValues[IAccountManager::PROPERTY_PRONOUNS] |
|
282 | - = $ldapEntry[$attr][0] ?? ''; |
|
283 | - } |
|
284 | - // check for changed data and cache just for TTL checking |
|
285 | - $checksum = hash('sha256', json_encode($profileValues)); |
|
286 | - $this->connection->writeToCache($cacheKey, $checksum // write array to cache. is waste of cache space |
|
287 | - , null); // use ldapCacheTTL from configuration |
|
288 | - // Update user profile |
|
289 | - if ($this->config->getUserValue($username, 'user_ldap', 'lastProfileChecksum', null) !== $checksum) { |
|
290 | - $this->config->setUserValue($username, 'user_ldap', 'lastProfileChecksum', $checksum); |
|
291 | - $this->updateProfile($profileValues); |
|
292 | - $this->logger->info("updated profile uid=$username", ['app' => 'user_ldap']); |
|
293 | - } else { |
|
294 | - $this->logger->debug('profile data from LDAP unchanged', ['app' => 'user_ldap', 'uid' => $username]); |
|
295 | - } |
|
296 | - unset($attr); |
|
297 | - } elseif ($profileCached !== null) { // message delayed, to declutter log |
|
298 | - $this->logger->debug('skipping profile check, while cached data exist', ['app' => 'user_ldap', 'uid' => $username]); |
|
299 | - } |
|
300 | - |
|
301 | - //Avatar |
|
302 | - /** @var Connection $connection */ |
|
303 | - $connection = $this->access->getConnection(); |
|
304 | - $attributes = $connection->resolveRule('avatar'); |
|
305 | - foreach ($attributes as $attribute) { |
|
306 | - if (isset($ldapEntry[$attribute])) { |
|
307 | - $this->avatarImage = $ldapEntry[$attribute][0]; |
|
308 | - $this->updateAvatar(); |
|
309 | - break; |
|
310 | - } |
|
311 | - } |
|
312 | - } |
|
313 | - |
|
314 | - /** |
|
315 | - * @brief returns the LDAP DN of the user |
|
316 | - * @return string |
|
317 | - */ |
|
318 | - public function getDN() { |
|
319 | - return $this->dn; |
|
320 | - } |
|
321 | - |
|
322 | - /** |
|
323 | - * @brief returns the Nextcloud internal username of the user |
|
324 | - * @return string |
|
325 | - */ |
|
326 | - public function getUsername() { |
|
327 | - return $this->uid; |
|
328 | - } |
|
329 | - |
|
330 | - /** |
|
331 | - * returns the home directory of the user if specified by LDAP settings |
|
332 | - * @throws \Exception |
|
333 | - */ |
|
334 | - public function getHomePath(?string $valueFromLDAP = null): string|false { |
|
335 | - $path = (string)$valueFromLDAP; |
|
336 | - $attr = null; |
|
337 | - |
|
338 | - if (is_null($valueFromLDAP) |
|
339 | - && str_starts_with($this->access->connection->homeFolderNamingRule, 'attr:') |
|
340 | - && $this->access->connection->homeFolderNamingRule !== 'attr:') { |
|
341 | - $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); |
|
342 | - $dn = $this->access->username2dn($this->getUsername()); |
|
343 | - if ($dn === false) { |
|
344 | - return false; |
|
345 | - } |
|
346 | - $homedir = $this->access->readAttribute($dn, $attr); |
|
347 | - if ($homedir !== false && isset($homedir[0])) { |
|
348 | - $path = $homedir[0]; |
|
349 | - } |
|
350 | - } |
|
351 | - |
|
352 | - if ($path !== '') { |
|
353 | - //if attribute's value is an absolute path take this, otherwise append it to data dir |
|
354 | - //check for / at the beginning or pattern c:\ resp. c:/ |
|
355 | - if ($path[0] !== '/' |
|
356 | - && !(strlen($path) > 3 && ctype_alpha($path[0]) |
|
357 | - && $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/')) |
|
358 | - ) { |
|
359 | - $path = $this->config->getSystemValue('datadirectory', |
|
360 | - \OC::$SERVERROOT . '/data') . '/' . $path; |
|
361 | - } |
|
362 | - //we need it to store it in the DB as well in case a user gets |
|
363 | - //deleted so we can clean up afterwards |
|
364 | - $this->config->setUserValue( |
|
365 | - $this->getUsername(), 'user_ldap', 'homePath', $path |
|
366 | - ); |
|
367 | - return $path; |
|
368 | - } |
|
369 | - |
|
370 | - if (!is_null($attr) |
|
371 | - && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', 'true') |
|
372 | - ) { |
|
373 | - // a naming rule attribute is defined, but it doesn't exist for that LDAP user |
|
374 | - throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername()); |
|
375 | - } |
|
376 | - |
|
377 | - //false will apply default behaviour as defined and done by OC_User |
|
378 | - $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', ''); |
|
379 | - return false; |
|
380 | - } |
|
381 | - |
|
382 | - public function getMemberOfGroups(): array|false { |
|
383 | - $cacheKey = 'getMemberOf' . $this->getUsername(); |
|
384 | - $memberOfGroups = $this->connection->getFromCache($cacheKey); |
|
385 | - if (!is_null($memberOfGroups)) { |
|
386 | - return $memberOfGroups; |
|
387 | - } |
|
388 | - $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf'); |
|
389 | - $this->connection->writeToCache($cacheKey, $groupDNs); |
|
390 | - return $groupDNs; |
|
391 | - } |
|
392 | - |
|
393 | - /** |
|
394 | - * @brief reads the image from LDAP that shall be used as Avatar |
|
395 | - * @return string|false data (provided by LDAP) |
|
396 | - */ |
|
397 | - public function getAvatarImage(): string|false { |
|
398 | - if (!is_null($this->avatarImage)) { |
|
399 | - return $this->avatarImage; |
|
400 | - } |
|
401 | - |
|
402 | - $this->avatarImage = false; |
|
403 | - /** @var Connection $connection */ |
|
404 | - $connection = $this->access->getConnection(); |
|
405 | - $attributes = $connection->resolveRule('avatar'); |
|
406 | - foreach ($attributes as $attribute) { |
|
407 | - $result = $this->access->readAttribute($this->dn, $attribute); |
|
408 | - if ($result !== false && isset($result[0])) { |
|
409 | - $this->avatarImage = $result[0]; |
|
410 | - break; |
|
411 | - } |
|
412 | - } |
|
413 | - |
|
414 | - return $this->avatarImage; |
|
415 | - } |
|
416 | - |
|
417 | - /** |
|
418 | - * @brief marks the user as having logged in at least once |
|
419 | - */ |
|
420 | - public function markLogin(): void { |
|
421 | - $this->config->setUserValue( |
|
422 | - $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, '1'); |
|
423 | - } |
|
424 | - |
|
425 | - /** |
|
426 | - * Stores a key-value pair in relation to this user |
|
427 | - */ |
|
428 | - private function store(string $key, string $value): void { |
|
429 | - $this->config->setUserValue($this->uid, 'user_ldap', $key, $value); |
|
430 | - } |
|
431 | - |
|
432 | - /** |
|
433 | - * Composes the display name and stores it in the database. The final |
|
434 | - * display name is returned. |
|
435 | - * |
|
436 | - * @return string the effective display name |
|
437 | - */ |
|
438 | - public function composeAndStoreDisplayName(string $displayName, string $displayName2 = ''): string { |
|
439 | - if ($displayName2 !== '') { |
|
440 | - $displayName .= ' (' . $displayName2 . ')'; |
|
441 | - } |
|
442 | - $oldName = $this->config->getUserValue($this->uid, 'user_ldap', 'displayName', null); |
|
443 | - if ($oldName !== $displayName) { |
|
444 | - $this->store('displayName', $displayName); |
|
445 | - $user = $this->userManager->get($this->getUsername()); |
|
446 | - if (!empty($oldName) && $user instanceof \OC\User\User) { |
|
447 | - // if it was empty, it would be a new record, not a change emitting the trigger could |
|
448 | - // potentially cause a UniqueConstraintViolationException, depending on some factors. |
|
449 | - $user->triggerChange('displayName', $displayName, $oldName); |
|
450 | - } |
|
451 | - } |
|
452 | - return $displayName; |
|
453 | - } |
|
454 | - |
|
455 | - /** |
|
456 | - * Stores the LDAP Username in the Database |
|
457 | - */ |
|
458 | - public function storeLDAPUserName(string $userName): void { |
|
459 | - $this->store('uid', $userName); |
|
460 | - } |
|
461 | - |
|
462 | - /** |
|
463 | - * @brief checks whether an update method specified by feature was run |
|
464 | - * already. If not, it will marked like this, because it is expected that |
|
465 | - * the method will be run, when false is returned. |
|
466 | - * @param string $feature email | quota | avatar | profile (can be extended) |
|
467 | - */ |
|
468 | - private function wasRefreshed(string $feature): bool { |
|
469 | - if (isset($this->refreshedFeatures[$feature])) { |
|
470 | - return true; |
|
471 | - } |
|
472 | - $this->refreshedFeatures[$feature] = 1; |
|
473 | - return false; |
|
474 | - } |
|
475 | - |
|
476 | - /** |
|
477 | - * fetches the email from LDAP and stores it as Nextcloud user value |
|
478 | - * @param ?string $valueFromLDAP if known, to save an LDAP read request |
|
479 | - */ |
|
480 | - public function updateEmail(?string $valueFromLDAP = null): void { |
|
481 | - if ($this->wasRefreshed('email')) { |
|
482 | - return; |
|
483 | - } |
|
484 | - $email = (string)$valueFromLDAP; |
|
485 | - if (is_null($valueFromLDAP)) { |
|
486 | - $emailAttribute = $this->connection->ldapEmailAttribute; |
|
487 | - if ($emailAttribute !== '') { |
|
488 | - $aEmail = $this->access->readAttribute($this->dn, $emailAttribute); |
|
489 | - if (is_array($aEmail) && (count($aEmail) > 0)) { |
|
490 | - $email = (string)$aEmail[0]; |
|
491 | - } |
|
492 | - } |
|
493 | - } |
|
494 | - if ($email !== '') { |
|
495 | - $user = $this->userManager->get($this->uid); |
|
496 | - if (!is_null($user)) { |
|
497 | - $currentEmail = (string)$user->getSystemEMailAddress(); |
|
498 | - if ($currentEmail !== $email) { |
|
499 | - $user->setSystemEMailAddress($email); |
|
500 | - } |
|
501 | - } |
|
502 | - } |
|
503 | - } |
|
504 | - |
|
505 | - /** |
|
506 | - * Overall process goes as follow: |
|
507 | - * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function |
|
508 | - * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota |
|
509 | - * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default') |
|
510 | - * 4. check if the target user exists and set the quota for the user. |
|
511 | - * |
|
512 | - * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP |
|
513 | - * parameter can be passed with the value of the attribute. This value will be considered as the |
|
514 | - * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to |
|
515 | - * fetch all the user's attributes in one call and use the fetched values in this function. |
|
516 | - * The expected value for that parameter is a string describing the quota for the user. Valid |
|
517 | - * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in |
|
518 | - * bytes), '1234 MB' (quota in MB - check the \OCP\Util::computerFileSize method for more info) |
|
519 | - * |
|
520 | - * fetches the quota from LDAP and stores it as Nextcloud user value |
|
521 | - * @param ?string $valueFromLDAP the quota attribute's value can be passed, |
|
522 | - * to save the readAttribute request |
|
523 | - */ |
|
524 | - public function updateQuota(?string $valueFromLDAP = null): void { |
|
525 | - if ($this->wasRefreshed('quota')) { |
|
526 | - return; |
|
527 | - } |
|
528 | - |
|
529 | - $quotaAttribute = $this->connection->ldapQuotaAttribute; |
|
530 | - $defaultQuota = $this->connection->ldapQuotaDefault; |
|
531 | - if ($quotaAttribute === '' && $defaultQuota === '') { |
|
532 | - return; |
|
533 | - } |
|
534 | - |
|
535 | - $quota = false; |
|
536 | - if (is_null($valueFromLDAP) && $quotaAttribute !== '') { |
|
537 | - $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); |
|
538 | - if ($aQuota !== false && isset($aQuota[0]) && $this->verifyQuotaValue($aQuota[0])) { |
|
539 | - $quota = $aQuota[0]; |
|
540 | - } elseif (is_array($aQuota) && isset($aQuota[0])) { |
|
541 | - $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ['app' => 'user_ldap']); |
|
542 | - } |
|
543 | - } elseif (!is_null($valueFromLDAP) && $this->verifyQuotaValue($valueFromLDAP)) { |
|
544 | - $quota = $valueFromLDAP; |
|
545 | - } else { |
|
546 | - $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . ($valueFromLDAP ?? '') . ']', ['app' => 'user_ldap']); |
|
547 | - } |
|
548 | - |
|
549 | - if ($quota === false && $this->verifyQuotaValue($defaultQuota)) { |
|
550 | - // quota not found using the LDAP attribute (or not parseable). Try the default quota |
|
551 | - $quota = $defaultQuota; |
|
552 | - } elseif ($quota === false) { |
|
553 | - $this->logger->debug('no suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ['app' => 'user_ldap']); |
|
554 | - return; |
|
555 | - } |
|
556 | - |
|
557 | - $targetUser = $this->userManager->get($this->uid); |
|
558 | - if ($targetUser instanceof IUser) { |
|
559 | - $targetUser->setQuota($quota); |
|
560 | - } else { |
|
561 | - $this->logger->info('trying to set a quota for user ' . $this->uid . ' but the user is missing', ['app' => 'user_ldap']); |
|
562 | - } |
|
563 | - } |
|
564 | - |
|
565 | - private function verifyQuotaValue(string $quotaValue): bool { |
|
566 | - return $quotaValue === 'none' || $quotaValue === 'default' || Util::computerFileSize($quotaValue) !== false; |
|
567 | - } |
|
568 | - |
|
569 | - /** |
|
570 | - * takes values from LDAP and stores it as Nextcloud user profile value |
|
571 | - * |
|
572 | - * @param array $profileValues associative array of property keys and values from LDAP |
|
573 | - */ |
|
574 | - private function updateProfile(array $profileValues): void { |
|
575 | - // check if given array is empty |
|
576 | - if (empty($profileValues)) { |
|
577 | - return; // okay, nothing to do |
|
578 | - } |
|
579 | - // fetch/prepare user |
|
580 | - $user = $this->userManager->get($this->uid); |
|
581 | - if (is_null($user)) { |
|
582 | - $this->logger->error('could not get user for uid=' . $this->uid . '', ['app' => 'user_ldap']); |
|
583 | - return; |
|
584 | - } |
|
585 | - // prepare AccountManager and Account |
|
586 | - $accountManager = Server::get(IAccountManager::class); |
|
587 | - $account = $accountManager->getAccount($user); // get Account |
|
588 | - $defaultScopes = array_merge(AccountManager::DEFAULT_SCOPES, |
|
589 | - $this->config->getSystemValue('account_manager.default_property_scope', [])); |
|
590 | - // loop through the properties and handle them |
|
591 | - foreach ($profileValues as $property => $valueFromLDAP) { |
|
592 | - // check and update profile properties |
|
593 | - $value = (is_array($valueFromLDAP) ? $valueFromLDAP[0] : $valueFromLDAP); // take ONLY the first value, if multiple values specified |
|
594 | - try { |
|
595 | - $accountProperty = $account->getProperty($property); |
|
596 | - $currentValue = $accountProperty->getValue(); |
|
597 | - $scope = ($accountProperty->getScope() ?: $defaultScopes[$property]); |
|
598 | - } catch (PropertyDoesNotExistException $e) { // thrown at getProperty |
|
599 | - $this->logger->error('property does not exist: ' . $property |
|
600 | - . ' for uid=' . $this->uid . '', ['app' => 'user_ldap', 'exception' => $e]); |
|
601 | - $currentValue = ''; |
|
602 | - $scope = $defaultScopes[$property]; |
|
603 | - } |
|
604 | - $verified = IAccountManager::VERIFIED; // trust the LDAP admin knew what they put there |
|
605 | - if ($currentValue !== $value) { |
|
606 | - $account->setProperty($property, $value, $scope, $verified); |
|
607 | - $this->logger->debug('update user profile: ' . $property . '=' . $value |
|
608 | - . ' for uid=' . $this->uid . '', ['app' => 'user_ldap']); |
|
609 | - } |
|
610 | - } |
|
611 | - try { |
|
612 | - $accountManager->updateAccount($account); // may throw InvalidArgumentException |
|
613 | - } catch (\InvalidArgumentException $e) { |
|
614 | - $this->logger->error('invalid data from LDAP: for uid=' . $this->uid . '', ['app' => 'user_ldap', 'func' => 'updateProfile' |
|
615 | - , 'exception' => $e]); |
|
616 | - } |
|
617 | - } |
|
618 | - |
|
619 | - /** |
|
620 | - * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar |
|
621 | - * @return bool true when the avatar was set successfully or is up to date |
|
622 | - */ |
|
623 | - public function updateAvatar(bool $force = false): bool { |
|
624 | - if (!$force && $this->wasRefreshed('avatar')) { |
|
625 | - return false; |
|
626 | - } |
|
627 | - $avatarImage = $this->getAvatarImage(); |
|
628 | - if ($avatarImage === false) { |
|
629 | - //not set, nothing left to do; |
|
630 | - return false; |
|
631 | - } |
|
632 | - |
|
633 | - if (!$this->image->loadFromBase64(base64_encode($avatarImage))) { |
|
634 | - return false; |
|
635 | - } |
|
636 | - |
|
637 | - // use the checksum before modifications |
|
638 | - $checksum = md5($this->image->data()); |
|
639 | - |
|
640 | - if ($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '') && $this->avatarExists()) { |
|
641 | - return true; |
|
642 | - } |
|
643 | - |
|
644 | - $isSet = $this->setNextcloudAvatar(); |
|
645 | - |
|
646 | - if ($isSet) { |
|
647 | - // save checksum only after successful setting |
|
648 | - $this->config->setUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', $checksum); |
|
649 | - } |
|
650 | - |
|
651 | - return $isSet; |
|
652 | - } |
|
653 | - |
|
654 | - private function avatarExists(): bool { |
|
655 | - try { |
|
656 | - $currentAvatar = $this->avatarManager->getAvatar($this->uid); |
|
657 | - return $currentAvatar->exists() && $currentAvatar->isCustomAvatar(); |
|
658 | - } catch (\Exception $e) { |
|
659 | - return false; |
|
660 | - } |
|
661 | - } |
|
662 | - |
|
663 | - /** |
|
664 | - * @brief sets an image as Nextcloud avatar |
|
665 | - */ |
|
666 | - private function setNextcloudAvatar(): bool { |
|
667 | - if (!$this->image->valid()) { |
|
668 | - $this->logger->error('avatar image data from LDAP invalid for ' . $this->dn, ['app' => 'user_ldap']); |
|
669 | - return false; |
|
670 | - } |
|
671 | - |
|
672 | - |
|
673 | - //make sure it is a square and not bigger than 512x512 |
|
674 | - $size = min([$this->image->width(), $this->image->height(), 512]); |
|
675 | - if (!$this->image->centerCrop($size)) { |
|
676 | - $this->logger->error('croping image for avatar failed for ' . $this->dn, ['app' => 'user_ldap']); |
|
677 | - return false; |
|
678 | - } |
|
679 | - |
|
680 | - try { |
|
681 | - $avatar = $this->avatarManager->getAvatar($this->uid); |
|
682 | - $avatar->set($this->image); |
|
683 | - return true; |
|
684 | - } catch (\Exception $e) { |
|
685 | - $this->logger->info('Could not set avatar for ' . $this->dn, ['exception' => $e]); |
|
686 | - } |
|
687 | - return false; |
|
688 | - } |
|
689 | - |
|
690 | - /** |
|
691 | - * @throws AttributeNotSet |
|
692 | - * @throws \OC\ServerNotAvailableException |
|
693 | - * @throws PreConditionNotMetException |
|
694 | - */ |
|
695 | - public function getExtStorageHome():string { |
|
696 | - $value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', ''); |
|
697 | - if ($value !== '') { |
|
698 | - return $value; |
|
699 | - } |
|
700 | - |
|
701 | - $value = $this->updateExtStorageHome(); |
|
702 | - if ($value !== '') { |
|
703 | - return $value; |
|
704 | - } |
|
705 | - |
|
706 | - throw new AttributeNotSet(sprintf( |
|
707 | - 'external home storage attribute yield no value for %s', $this->getUsername() |
|
708 | - )); |
|
709 | - } |
|
710 | - |
|
711 | - /** |
|
712 | - * @throws PreConditionNotMetException |
|
713 | - * @throws \OC\ServerNotAvailableException |
|
714 | - */ |
|
715 | - public function updateExtStorageHome(?string $valueFromLDAP = null):string { |
|
716 | - if ($valueFromLDAP === null) { |
|
717 | - $extHomeValues = $this->access->readAttribute($this->getDN(), $this->connection->ldapExtStorageHomeAttribute); |
|
718 | - } else { |
|
719 | - $extHomeValues = [$valueFromLDAP]; |
|
720 | - } |
|
721 | - if ($extHomeValues !== false && isset($extHomeValues[0])) { |
|
722 | - $extHome = $extHomeValues[0]; |
|
723 | - $this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome); |
|
724 | - return $extHome; |
|
725 | - } else { |
|
726 | - $this->config->deleteUserValue($this->getUsername(), 'user_ldap', 'extStorageHome'); |
|
727 | - return ''; |
|
728 | - } |
|
729 | - } |
|
730 | - |
|
731 | - /** |
|
732 | - * called by a post_login hook to handle password expiry |
|
733 | - */ |
|
734 | - public function handlePasswordExpiry(array $params): void { |
|
735 | - $ppolicyDN = $this->connection->ldapDefaultPPolicyDN; |
|
736 | - if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) { |
|
737 | - //password expiry handling disabled |
|
738 | - return; |
|
739 | - } |
|
740 | - $uid = $params['uid']; |
|
741 | - if (isset($uid) && $uid === $this->getUsername()) { |
|
742 | - //retrieve relevant user attributes |
|
743 | - $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); |
|
744 | - |
|
745 | - if (array_key_exists('pwdpolicysubentry', $result[0])) { |
|
746 | - $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; |
|
747 | - if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) { |
|
748 | - $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN |
|
749 | - } |
|
750 | - } |
|
751 | - |
|
752 | - $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : []; |
|
753 | - $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : []; |
|
754 | - $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : []; |
|
755 | - |
|
756 | - //retrieve relevant password policy attributes |
|
757 | - $cacheKey = 'ppolicyAttributes' . $ppolicyDN; |
|
758 | - $result = $this->connection->getFromCache($cacheKey); |
|
759 | - if (is_null($result)) { |
|
760 | - $result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']); |
|
761 | - $this->connection->writeToCache($cacheKey, $result); |
|
762 | - } |
|
763 | - |
|
764 | - $pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : []; |
|
765 | - $pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : []; |
|
766 | - $pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : []; |
|
767 | - |
|
768 | - //handle grace login |
|
769 | - if (!empty($pwdGraceUseTime)) { //was this a grace login? |
|
770 | - if (!empty($pwdGraceAuthNLimit) |
|
771 | - && count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available? |
|
772 | - $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
773 | - header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( |
|
774 | - 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); |
|
775 | - } else { //no more grace login available |
|
776 | - header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( |
|
777 | - 'user_ldap.renewPassword.showLoginFormInvalidPassword', ['user' => $uid])); |
|
778 | - } |
|
779 | - exit(); |
|
780 | - } |
|
781 | - //handle pwdReset attribute |
|
782 | - if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change their password |
|
783 | - $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
784 | - header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( |
|
785 | - 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); |
|
786 | - exit(); |
|
787 | - } |
|
788 | - //handle password expiry warning |
|
789 | - if (!empty($pwdChangedTime)) { |
|
790 | - if (!empty($pwdMaxAge) |
|
791 | - && !empty($pwdExpireWarning)) { |
|
792 | - $pwdMaxAgeInt = (int)$pwdMaxAge[0]; |
|
793 | - $pwdExpireWarningInt = (int)$pwdExpireWarning[0]; |
|
794 | - if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0) { |
|
795 | - $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]); |
|
796 | - $pwdChangedTimeDt->add(new \DateInterval('PT' . $pwdMaxAgeInt . 'S')); |
|
797 | - $currentDateTime = new \DateTime(); |
|
798 | - $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp(); |
|
799 | - if ($secondsToExpiry <= $pwdExpireWarningInt) { |
|
800 | - //remove last password expiry warning if any |
|
801 | - $notification = $this->notificationManager->createNotification(); |
|
802 | - $notification->setApp('user_ldap') |
|
803 | - ->setUser($uid) |
|
804 | - ->setObject('pwd_exp_warn', $uid) |
|
805 | - ; |
|
806 | - $this->notificationManager->markProcessed($notification); |
|
807 | - //create new password expiry warning |
|
808 | - $notification = $this->notificationManager->createNotification(); |
|
809 | - $notification->setApp('user_ldap') |
|
810 | - ->setUser($uid) |
|
811 | - ->setDateTime($currentDateTime) |
|
812 | - ->setObject('pwd_exp_warn', $uid) |
|
813 | - ->setSubject('pwd_exp_warn_days', [(int)ceil($secondsToExpiry / 60 / 60 / 24)]) |
|
814 | - ; |
|
815 | - $this->notificationManager->notify($notification); |
|
816 | - } |
|
817 | - } |
|
818 | - } |
|
819 | - } |
|
820 | - } |
|
821 | - } |
|
36 | + protected Connection $connection; |
|
37 | + /** |
|
38 | + * @var array<string,1> |
|
39 | + */ |
|
40 | + protected array $refreshedFeatures = []; |
|
41 | + protected string|false|null $avatarImage = null; |
|
42 | + |
|
43 | + protected BirthdateParserService $birthdateParser; |
|
44 | + |
|
45 | + /** |
|
46 | + * DB config keys for user preferences |
|
47 | + * @var string |
|
48 | + */ |
|
49 | + public const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished'; |
|
50 | + |
|
51 | + /** |
|
52 | + * @brief constructor, make sure the subclasses call this one! |
|
53 | + */ |
|
54 | + public function __construct( |
|
55 | + protected string $uid, |
|
56 | + protected string $dn, |
|
57 | + protected Access $access, |
|
58 | + protected IConfig $config, |
|
59 | + protected Image $image, |
|
60 | + protected LoggerInterface $logger, |
|
61 | + protected IAvatarManager $avatarManager, |
|
62 | + protected IUserManager $userManager, |
|
63 | + protected INotificationManager $notificationManager, |
|
64 | + ) { |
|
65 | + if ($uid === '') { |
|
66 | + $logger->error("uid for '$dn' must not be an empty string", ['app' => 'user_ldap']); |
|
67 | + throw new \InvalidArgumentException('uid must not be an empty string!'); |
|
68 | + } |
|
69 | + $this->connection = $this->access->getConnection(); |
|
70 | + $this->birthdateParser = new BirthdateParserService(); |
|
71 | + |
|
72 | + Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); |
|
73 | + } |
|
74 | + |
|
75 | + /** |
|
76 | + * marks a user as deleted |
|
77 | + * |
|
78 | + * @throws PreConditionNotMetException |
|
79 | + */ |
|
80 | + public function markUser(): void { |
|
81 | + $curValue = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '0'); |
|
82 | + if ($curValue === '1') { |
|
83 | + // the user is already marked, do not write to DB again |
|
84 | + return; |
|
85 | + } |
|
86 | + $this->config->setUserValue($this->getUsername(), 'user_ldap', 'isDeleted', '1'); |
|
87 | + $this->config->setUserValue($this->getUsername(), 'user_ldap', 'foundDeleted', (string)time()); |
|
88 | + } |
|
89 | + |
|
90 | + /** |
|
91 | + * processes results from LDAP for attributes as returned by getAttributesToRead() |
|
92 | + * @param array $ldapEntry the user entry as retrieved from LDAP |
|
93 | + */ |
|
94 | + public function processAttributes(array $ldapEntry): void { |
|
95 | + //Quota |
|
96 | + $attr = strtolower($this->connection->ldapQuotaAttribute); |
|
97 | + if (isset($ldapEntry[$attr])) { |
|
98 | + $this->updateQuota($ldapEntry[$attr][0]); |
|
99 | + } else { |
|
100 | + if ($this->connection->ldapQuotaDefault !== '') { |
|
101 | + $this->updateQuota(); |
|
102 | + } |
|
103 | + } |
|
104 | + unset($attr); |
|
105 | + |
|
106 | + //displayName |
|
107 | + $displayName = $displayName2 = ''; |
|
108 | + $attr = strtolower($this->connection->ldapUserDisplayName); |
|
109 | + if (isset($ldapEntry[$attr])) { |
|
110 | + $displayName = (string)$ldapEntry[$attr][0]; |
|
111 | + } |
|
112 | + $attr = strtolower($this->connection->ldapUserDisplayName2); |
|
113 | + if (isset($ldapEntry[$attr])) { |
|
114 | + $displayName2 = (string)$ldapEntry[$attr][0]; |
|
115 | + } |
|
116 | + if ($displayName !== '') { |
|
117 | + $this->composeAndStoreDisplayName($displayName, $displayName2); |
|
118 | + $this->access->cacheUserDisplayName( |
|
119 | + $this->getUsername(), |
|
120 | + $displayName, |
|
121 | + $displayName2 |
|
122 | + ); |
|
123 | + } |
|
124 | + unset($attr); |
|
125 | + |
|
126 | ||
127 | + //email must be stored after displayname, because it would cause a user |
|
128 | + //change event that will trigger fetching the display name again |
|
129 | + $attr = strtolower($this->connection->ldapEmailAttribute); |
|
130 | + if (isset($ldapEntry[$attr])) { |
|
131 | + $mailValue = 0; |
|
132 | + for ($x = 0; $x < count($ldapEntry[$attr]); $x++) { |
|
133 | + if (filter_var($ldapEntry[$attr][$x], FILTER_VALIDATE_EMAIL)) { |
|
134 | + $mailValue = $x; |
|
135 | + break; |
|
136 | + } |
|
137 | + } |
|
138 | + $this->updateEmail($ldapEntry[$attr][$mailValue]); |
|
139 | + } |
|
140 | + unset($attr); |
|
141 | + |
|
142 | + // LDAP Username, needed for s2s sharing |
|
143 | + if (isset($ldapEntry['uid'])) { |
|
144 | + $this->storeLDAPUserName($ldapEntry['uid'][0]); |
|
145 | + } elseif (isset($ldapEntry['samaccountname'])) { |
|
146 | + $this->storeLDAPUserName($ldapEntry['samaccountname'][0]); |
|
147 | + } |
|
148 | + |
|
149 | + //homePath |
|
150 | + if (str_starts_with($this->connection->homeFolderNamingRule, 'attr:')) { |
|
151 | + $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:'))); |
|
152 | + if (isset($ldapEntry[$attr])) { |
|
153 | + $this->access->cacheUserHome( |
|
154 | + $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0])); |
|
155 | + } |
|
156 | + } |
|
157 | + |
|
158 | + //memberOf groups |
|
159 | + $cacheKey = 'getMemberOf' . $this->getUsername(); |
|
160 | + $groups = false; |
|
161 | + if (isset($ldapEntry['memberof'])) { |
|
162 | + $groups = $ldapEntry['memberof']; |
|
163 | + } |
|
164 | + $this->connection->writeToCache($cacheKey, $groups); |
|
165 | + |
|
166 | + //external storage var |
|
167 | + $attr = strtolower($this->connection->ldapExtStorageHomeAttribute); |
|
168 | + if (isset($ldapEntry[$attr])) { |
|
169 | + $this->updateExtStorageHome($ldapEntry[$attr][0]); |
|
170 | + } |
|
171 | + unset($attr); |
|
172 | + |
|
173 | + // check for cached profile data |
|
174 | + $username = $this->getUsername(); // buffer variable, to save resource |
|
175 | + $cacheKey = 'getUserProfile-' . $username; |
|
176 | + $profileCached = $this->connection->getFromCache($cacheKey); |
|
177 | + // honoring profile disabled in config.php and check if user profile was refreshed |
|
178 | + if ($this->config->getSystemValueBool('profile.enabled', true) && |
|
179 | + ($profileCached === null) && // no cache or TTL not expired |
|
180 | + !$this->wasRefreshed('profile')) { |
|
181 | + // check current data |
|
182 | + $profileValues = []; |
|
183 | + //User Profile Field - Phone number |
|
184 | + $attr = strtolower($this->connection->ldapAttributePhone); |
|
185 | + if (!empty($attr)) { // attribute configured |
|
186 | + $profileValues[IAccountManager::PROPERTY_PHONE] |
|
187 | + = $ldapEntry[$attr][0] ?? ''; |
|
188 | + } |
|
189 | + //User Profile Field - website |
|
190 | + $attr = strtolower($this->connection->ldapAttributeWebsite); |
|
191 | + if (isset($ldapEntry[$attr])) { |
|
192 | + $cutPosition = strpos($ldapEntry[$attr][0], ' '); |
|
193 | + if ($cutPosition) { |
|
194 | + // drop appended label |
|
195 | + $profileValues[IAccountManager::PROPERTY_WEBSITE] |
|
196 | + = substr($ldapEntry[$attr][0], 0, $cutPosition); |
|
197 | + } else { |
|
198 | + $profileValues[IAccountManager::PROPERTY_WEBSITE] |
|
199 | + = $ldapEntry[$attr][0]; |
|
200 | + } |
|
201 | + } elseif (!empty($attr)) { // configured, but not defined |
|
202 | + $profileValues[IAccountManager::PROPERTY_WEBSITE] = ''; |
|
203 | + } |
|
204 | + //User Profile Field - Address |
|
205 | + $attr = strtolower($this->connection->ldapAttributeAddress); |
|
206 | + if (isset($ldapEntry[$attr])) { |
|
207 | + if (str_contains($ldapEntry[$attr][0], '$')) { |
|
208 | + // basic format conversion from postalAddress syntax to commata delimited |
|
209 | + $profileValues[IAccountManager::PROPERTY_ADDRESS] |
|
210 | + = str_replace('$', ', ', $ldapEntry[$attr][0]); |
|
211 | + } else { |
|
212 | + $profileValues[IAccountManager::PROPERTY_ADDRESS] |
|
213 | + = $ldapEntry[$attr][0]; |
|
214 | + } |
|
215 | + } elseif (!empty($attr)) { // configured, but not defined |
|
216 | + $profileValues[IAccountManager::PROPERTY_ADDRESS] = ''; |
|
217 | + } |
|
218 | + //User Profile Field - Twitter |
|
219 | + $attr = strtolower($this->connection->ldapAttributeTwitter); |
|
220 | + if (!empty($attr)) { |
|
221 | + $profileValues[IAccountManager::PROPERTY_TWITTER] |
|
222 | + = $ldapEntry[$attr][0] ?? ''; |
|
223 | + } |
|
224 | + //User Profile Field - fediverse |
|
225 | + $attr = strtolower($this->connection->ldapAttributeFediverse); |
|
226 | + if (!empty($attr)) { |
|
227 | + $profileValues[IAccountManager::PROPERTY_FEDIVERSE] |
|
228 | + = $ldapEntry[$attr][0] ?? ''; |
|
229 | + } |
|
230 | + //User Profile Field - organisation |
|
231 | + $attr = strtolower($this->connection->ldapAttributeOrganisation); |
|
232 | + if (!empty($attr)) { |
|
233 | + $profileValues[IAccountManager::PROPERTY_ORGANISATION] |
|
234 | + = $ldapEntry[$attr][0] ?? ''; |
|
235 | + } |
|
236 | + //User Profile Field - role |
|
237 | + $attr = strtolower($this->connection->ldapAttributeRole); |
|
238 | + if (!empty($attr)) { |
|
239 | + $profileValues[IAccountManager::PROPERTY_ROLE] |
|
240 | + = $ldapEntry[$attr][0] ?? ''; |
|
241 | + } |
|
242 | + //User Profile Field - headline |
|
243 | + $attr = strtolower($this->connection->ldapAttributeHeadline); |
|
244 | + if (!empty($attr)) { |
|
245 | + $profileValues[IAccountManager::PROPERTY_HEADLINE] |
|
246 | + = $ldapEntry[$attr][0] ?? ''; |
|
247 | + } |
|
248 | + //User Profile Field - biography |
|
249 | + $attr = strtolower($this->connection->ldapAttributeBiography); |
|
250 | + if (isset($ldapEntry[$attr])) { |
|
251 | + if (str_contains($ldapEntry[$attr][0], '\r')) { |
|
252 | + // convert line endings |
|
253 | + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] |
|
254 | + = str_replace(["\r\n","\r"], "\n", $ldapEntry[$attr][0]); |
|
255 | + } else { |
|
256 | + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] |
|
257 | + = $ldapEntry[$attr][0]; |
|
258 | + } |
|
259 | + } elseif (!empty($attr)) { // configured, but not defined |
|
260 | + $profileValues[IAccountManager::PROPERTY_BIOGRAPHY] = ''; |
|
261 | + } |
|
262 | + //User Profile Field - birthday |
|
263 | + $attr = strtolower($this->connection->ldapAttributeBirthDate); |
|
264 | + if (!empty($attr) && !empty($ldapEntry[$attr][0])) { |
|
265 | + $value = $ldapEntry[$attr][0]; |
|
266 | + try { |
|
267 | + $birthdate = $this->birthdateParser->parseBirthdate($value); |
|
268 | + $profileValues[IAccountManager::PROPERTY_BIRTHDATE] |
|
269 | + = $birthdate->format('Y-m-d'); |
|
270 | + } catch (InvalidArgumentException $e) { |
|
271 | + // Invalid date -> just skip the property |
|
272 | + $this->logger->info("Failed to parse user's birthdate from LDAP: $value", [ |
|
273 | + 'exception' => $e, |
|
274 | + 'userId' => $username, |
|
275 | + ]); |
|
276 | + } |
|
277 | + } |
|
278 | + //User Profile Field - pronouns |
|
279 | + $attr = strtolower($this->connection->ldapAttributePronouns); |
|
280 | + if (!empty($attr)) { |
|
281 | + $profileValues[IAccountManager::PROPERTY_PRONOUNS] |
|
282 | + = $ldapEntry[$attr][0] ?? ''; |
|
283 | + } |
|
284 | + // check for changed data and cache just for TTL checking |
|
285 | + $checksum = hash('sha256', json_encode($profileValues)); |
|
286 | + $this->connection->writeToCache($cacheKey, $checksum // write array to cache. is waste of cache space |
|
287 | + , null); // use ldapCacheTTL from configuration |
|
288 | + // Update user profile |
|
289 | + if ($this->config->getUserValue($username, 'user_ldap', 'lastProfileChecksum', null) !== $checksum) { |
|
290 | + $this->config->setUserValue($username, 'user_ldap', 'lastProfileChecksum', $checksum); |
|
291 | + $this->updateProfile($profileValues); |
|
292 | + $this->logger->info("updated profile uid=$username", ['app' => 'user_ldap']); |
|
293 | + } else { |
|
294 | + $this->logger->debug('profile data from LDAP unchanged', ['app' => 'user_ldap', 'uid' => $username]); |
|
295 | + } |
|
296 | + unset($attr); |
|
297 | + } elseif ($profileCached !== null) { // message delayed, to declutter log |
|
298 | + $this->logger->debug('skipping profile check, while cached data exist', ['app' => 'user_ldap', 'uid' => $username]); |
|
299 | + } |
|
300 | + |
|
301 | + //Avatar |
|
302 | + /** @var Connection $connection */ |
|
303 | + $connection = $this->access->getConnection(); |
|
304 | + $attributes = $connection->resolveRule('avatar'); |
|
305 | + foreach ($attributes as $attribute) { |
|
306 | + if (isset($ldapEntry[$attribute])) { |
|
307 | + $this->avatarImage = $ldapEntry[$attribute][0]; |
|
308 | + $this->updateAvatar(); |
|
309 | + break; |
|
310 | + } |
|
311 | + } |
|
312 | + } |
|
313 | + |
|
314 | + /** |
|
315 | + * @brief returns the LDAP DN of the user |
|
316 | + * @return string |
|
317 | + */ |
|
318 | + public function getDN() { |
|
319 | + return $this->dn; |
|
320 | + } |
|
321 | + |
|
322 | + /** |
|
323 | + * @brief returns the Nextcloud internal username of the user |
|
324 | + * @return string |
|
325 | + */ |
|
326 | + public function getUsername() { |
|
327 | + return $this->uid; |
|
328 | + } |
|
329 | + |
|
330 | + /** |
|
331 | + * returns the home directory of the user if specified by LDAP settings |
|
332 | + * @throws \Exception |
|
333 | + */ |
|
334 | + public function getHomePath(?string $valueFromLDAP = null): string|false { |
|
335 | + $path = (string)$valueFromLDAP; |
|
336 | + $attr = null; |
|
337 | + |
|
338 | + if (is_null($valueFromLDAP) |
|
339 | + && str_starts_with($this->access->connection->homeFolderNamingRule, 'attr:') |
|
340 | + && $this->access->connection->homeFolderNamingRule !== 'attr:') { |
|
341 | + $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); |
|
342 | + $dn = $this->access->username2dn($this->getUsername()); |
|
343 | + if ($dn === false) { |
|
344 | + return false; |
|
345 | + } |
|
346 | + $homedir = $this->access->readAttribute($dn, $attr); |
|
347 | + if ($homedir !== false && isset($homedir[0])) { |
|
348 | + $path = $homedir[0]; |
|
349 | + } |
|
350 | + } |
|
351 | + |
|
352 | + if ($path !== '') { |
|
353 | + //if attribute's value is an absolute path take this, otherwise append it to data dir |
|
354 | + //check for / at the beginning or pattern c:\ resp. c:/ |
|
355 | + if ($path[0] !== '/' |
|
356 | + && !(strlen($path) > 3 && ctype_alpha($path[0]) |
|
357 | + && $path[1] === ':' && ($path[2] === '\\' || $path[2] === '/')) |
|
358 | + ) { |
|
359 | + $path = $this->config->getSystemValue('datadirectory', |
|
360 | + \OC::$SERVERROOT . '/data') . '/' . $path; |
|
361 | + } |
|
362 | + //we need it to store it in the DB as well in case a user gets |
|
363 | + //deleted so we can clean up afterwards |
|
364 | + $this->config->setUserValue( |
|
365 | + $this->getUsername(), 'user_ldap', 'homePath', $path |
|
366 | + ); |
|
367 | + return $path; |
|
368 | + } |
|
369 | + |
|
370 | + if (!is_null($attr) |
|
371 | + && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', 'true') |
|
372 | + ) { |
|
373 | + // a naming rule attribute is defined, but it doesn't exist for that LDAP user |
|
374 | + throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername()); |
|
375 | + } |
|
376 | + |
|
377 | + //false will apply default behaviour as defined and done by OC_User |
|
378 | + $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', ''); |
|
379 | + return false; |
|
380 | + } |
|
381 | + |
|
382 | + public function getMemberOfGroups(): array|false { |
|
383 | + $cacheKey = 'getMemberOf' . $this->getUsername(); |
|
384 | + $memberOfGroups = $this->connection->getFromCache($cacheKey); |
|
385 | + if (!is_null($memberOfGroups)) { |
|
386 | + return $memberOfGroups; |
|
387 | + } |
|
388 | + $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf'); |
|
389 | + $this->connection->writeToCache($cacheKey, $groupDNs); |
|
390 | + return $groupDNs; |
|
391 | + } |
|
392 | + |
|
393 | + /** |
|
394 | + * @brief reads the image from LDAP that shall be used as Avatar |
|
395 | + * @return string|false data (provided by LDAP) |
|
396 | + */ |
|
397 | + public function getAvatarImage(): string|false { |
|
398 | + if (!is_null($this->avatarImage)) { |
|
399 | + return $this->avatarImage; |
|
400 | + } |
|
401 | + |
|
402 | + $this->avatarImage = false; |
|
403 | + /** @var Connection $connection */ |
|
404 | + $connection = $this->access->getConnection(); |
|
405 | + $attributes = $connection->resolveRule('avatar'); |
|
406 | + foreach ($attributes as $attribute) { |
|
407 | + $result = $this->access->readAttribute($this->dn, $attribute); |
|
408 | + if ($result !== false && isset($result[0])) { |
|
409 | + $this->avatarImage = $result[0]; |
|
410 | + break; |
|
411 | + } |
|
412 | + } |
|
413 | + |
|
414 | + return $this->avatarImage; |
|
415 | + } |
|
416 | + |
|
417 | + /** |
|
418 | + * @brief marks the user as having logged in at least once |
|
419 | + */ |
|
420 | + public function markLogin(): void { |
|
421 | + $this->config->setUserValue( |
|
422 | + $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, '1'); |
|
423 | + } |
|
424 | + |
|
425 | + /** |
|
426 | + * Stores a key-value pair in relation to this user |
|
427 | + */ |
|
428 | + private function store(string $key, string $value): void { |
|
429 | + $this->config->setUserValue($this->uid, 'user_ldap', $key, $value); |
|
430 | + } |
|
431 | + |
|
432 | + /** |
|
433 | + * Composes the display name and stores it in the database. The final |
|
434 | + * display name is returned. |
|
435 | + * |
|
436 | + * @return string the effective display name |
|
437 | + */ |
|
438 | + public function composeAndStoreDisplayName(string $displayName, string $displayName2 = ''): string { |
|
439 | + if ($displayName2 !== '') { |
|
440 | + $displayName .= ' (' . $displayName2 . ')'; |
|
441 | + } |
|
442 | + $oldName = $this->config->getUserValue($this->uid, 'user_ldap', 'displayName', null); |
|
443 | + if ($oldName !== $displayName) { |
|
444 | + $this->store('displayName', $displayName); |
|
445 | + $user = $this->userManager->get($this->getUsername()); |
|
446 | + if (!empty($oldName) && $user instanceof \OC\User\User) { |
|
447 | + // if it was empty, it would be a new record, not a change emitting the trigger could |
|
448 | + // potentially cause a UniqueConstraintViolationException, depending on some factors. |
|
449 | + $user->triggerChange('displayName', $displayName, $oldName); |
|
450 | + } |
|
451 | + } |
|
452 | + return $displayName; |
|
453 | + } |
|
454 | + |
|
455 | + /** |
|
456 | + * Stores the LDAP Username in the Database |
|
457 | + */ |
|
458 | + public function storeLDAPUserName(string $userName): void { |
|
459 | + $this->store('uid', $userName); |
|
460 | + } |
|
461 | + |
|
462 | + /** |
|
463 | + * @brief checks whether an update method specified by feature was run |
|
464 | + * already. If not, it will marked like this, because it is expected that |
|
465 | + * the method will be run, when false is returned. |
|
466 | + * @param string $feature email | quota | avatar | profile (can be extended) |
|
467 | + */ |
|
468 | + private function wasRefreshed(string $feature): bool { |
|
469 | + if (isset($this->refreshedFeatures[$feature])) { |
|
470 | + return true; |
|
471 | + } |
|
472 | + $this->refreshedFeatures[$feature] = 1; |
|
473 | + return false; |
|
474 | + } |
|
475 | + |
|
476 | + /** |
|
477 | + * fetches the email from LDAP and stores it as Nextcloud user value |
|
478 | + * @param ?string $valueFromLDAP if known, to save an LDAP read request |
|
479 | + */ |
|
480 | + public function updateEmail(?string $valueFromLDAP = null): void { |
|
481 | + if ($this->wasRefreshed('email')) { |
|
482 | + return; |
|
483 | + } |
|
484 | + $email = (string)$valueFromLDAP; |
|
485 | + if (is_null($valueFromLDAP)) { |
|
486 | + $emailAttribute = $this->connection->ldapEmailAttribute; |
|
487 | + if ($emailAttribute !== '') { |
|
488 | + $aEmail = $this->access->readAttribute($this->dn, $emailAttribute); |
|
489 | + if (is_array($aEmail) && (count($aEmail) > 0)) { |
|
490 | + $email = (string)$aEmail[0]; |
|
491 | + } |
|
492 | + } |
|
493 | + } |
|
494 | + if ($email !== '') { |
|
495 | + $user = $this->userManager->get($this->uid); |
|
496 | + if (!is_null($user)) { |
|
497 | + $currentEmail = (string)$user->getSystemEMailAddress(); |
|
498 | + if ($currentEmail !== $email) { |
|
499 | + $user->setSystemEMailAddress($email); |
|
500 | + } |
|
501 | + } |
|
502 | + } |
|
503 | + } |
|
504 | + |
|
505 | + /** |
|
506 | + * Overall process goes as follow: |
|
507 | + * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function |
|
508 | + * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota |
|
509 | + * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default') |
|
510 | + * 4. check if the target user exists and set the quota for the user. |
|
511 | + * |
|
512 | + * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP |
|
513 | + * parameter can be passed with the value of the attribute. This value will be considered as the |
|
514 | + * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to |
|
515 | + * fetch all the user's attributes in one call and use the fetched values in this function. |
|
516 | + * The expected value for that parameter is a string describing the quota for the user. Valid |
|
517 | + * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in |
|
518 | + * bytes), '1234 MB' (quota in MB - check the \OCP\Util::computerFileSize method for more info) |
|
519 | + * |
|
520 | + * fetches the quota from LDAP and stores it as Nextcloud user value |
|
521 | + * @param ?string $valueFromLDAP the quota attribute's value can be passed, |
|
522 | + * to save the readAttribute request |
|
523 | + */ |
|
524 | + public function updateQuota(?string $valueFromLDAP = null): void { |
|
525 | + if ($this->wasRefreshed('quota')) { |
|
526 | + return; |
|
527 | + } |
|
528 | + |
|
529 | + $quotaAttribute = $this->connection->ldapQuotaAttribute; |
|
530 | + $defaultQuota = $this->connection->ldapQuotaDefault; |
|
531 | + if ($quotaAttribute === '' && $defaultQuota === '') { |
|
532 | + return; |
|
533 | + } |
|
534 | + |
|
535 | + $quota = false; |
|
536 | + if (is_null($valueFromLDAP) && $quotaAttribute !== '') { |
|
537 | + $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); |
|
538 | + if ($aQuota !== false && isset($aQuota[0]) && $this->verifyQuotaValue($aQuota[0])) { |
|
539 | + $quota = $aQuota[0]; |
|
540 | + } elseif (is_array($aQuota) && isset($aQuota[0])) { |
|
541 | + $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ['app' => 'user_ldap']); |
|
542 | + } |
|
543 | + } elseif (!is_null($valueFromLDAP) && $this->verifyQuotaValue($valueFromLDAP)) { |
|
544 | + $quota = $valueFromLDAP; |
|
545 | + } else { |
|
546 | + $this->logger->debug('no suitable LDAP quota found for user ' . $this->uid . ': [' . ($valueFromLDAP ?? '') . ']', ['app' => 'user_ldap']); |
|
547 | + } |
|
548 | + |
|
549 | + if ($quota === false && $this->verifyQuotaValue($defaultQuota)) { |
|
550 | + // quota not found using the LDAP attribute (or not parseable). Try the default quota |
|
551 | + $quota = $defaultQuota; |
|
552 | + } elseif ($quota === false) { |
|
553 | + $this->logger->debug('no suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ['app' => 'user_ldap']); |
|
554 | + return; |
|
555 | + } |
|
556 | + |
|
557 | + $targetUser = $this->userManager->get($this->uid); |
|
558 | + if ($targetUser instanceof IUser) { |
|
559 | + $targetUser->setQuota($quota); |
|
560 | + } else { |
|
561 | + $this->logger->info('trying to set a quota for user ' . $this->uid . ' but the user is missing', ['app' => 'user_ldap']); |
|
562 | + } |
|
563 | + } |
|
564 | + |
|
565 | + private function verifyQuotaValue(string $quotaValue): bool { |
|
566 | + return $quotaValue === 'none' || $quotaValue === 'default' || Util::computerFileSize($quotaValue) !== false; |
|
567 | + } |
|
568 | + |
|
569 | + /** |
|
570 | + * takes values from LDAP and stores it as Nextcloud user profile value |
|
571 | + * |
|
572 | + * @param array $profileValues associative array of property keys and values from LDAP |
|
573 | + */ |
|
574 | + private function updateProfile(array $profileValues): void { |
|
575 | + // check if given array is empty |
|
576 | + if (empty($profileValues)) { |
|
577 | + return; // okay, nothing to do |
|
578 | + } |
|
579 | + // fetch/prepare user |
|
580 | + $user = $this->userManager->get($this->uid); |
|
581 | + if (is_null($user)) { |
|
582 | + $this->logger->error('could not get user for uid=' . $this->uid . '', ['app' => 'user_ldap']); |
|
583 | + return; |
|
584 | + } |
|
585 | + // prepare AccountManager and Account |
|
586 | + $accountManager = Server::get(IAccountManager::class); |
|
587 | + $account = $accountManager->getAccount($user); // get Account |
|
588 | + $defaultScopes = array_merge(AccountManager::DEFAULT_SCOPES, |
|
589 | + $this->config->getSystemValue('account_manager.default_property_scope', [])); |
|
590 | + // loop through the properties and handle them |
|
591 | + foreach ($profileValues as $property => $valueFromLDAP) { |
|
592 | + // check and update profile properties |
|
593 | + $value = (is_array($valueFromLDAP) ? $valueFromLDAP[0] : $valueFromLDAP); // take ONLY the first value, if multiple values specified |
|
594 | + try { |
|
595 | + $accountProperty = $account->getProperty($property); |
|
596 | + $currentValue = $accountProperty->getValue(); |
|
597 | + $scope = ($accountProperty->getScope() ?: $defaultScopes[$property]); |
|
598 | + } catch (PropertyDoesNotExistException $e) { // thrown at getProperty |
|
599 | + $this->logger->error('property does not exist: ' . $property |
|
600 | + . ' for uid=' . $this->uid . '', ['app' => 'user_ldap', 'exception' => $e]); |
|
601 | + $currentValue = ''; |
|
602 | + $scope = $defaultScopes[$property]; |
|
603 | + } |
|
604 | + $verified = IAccountManager::VERIFIED; // trust the LDAP admin knew what they put there |
|
605 | + if ($currentValue !== $value) { |
|
606 | + $account->setProperty($property, $value, $scope, $verified); |
|
607 | + $this->logger->debug('update user profile: ' . $property . '=' . $value |
|
608 | + . ' for uid=' . $this->uid . '', ['app' => 'user_ldap']); |
|
609 | + } |
|
610 | + } |
|
611 | + try { |
|
612 | + $accountManager->updateAccount($account); // may throw InvalidArgumentException |
|
613 | + } catch (\InvalidArgumentException $e) { |
|
614 | + $this->logger->error('invalid data from LDAP: for uid=' . $this->uid . '', ['app' => 'user_ldap', 'func' => 'updateProfile' |
|
615 | + , 'exception' => $e]); |
|
616 | + } |
|
617 | + } |
|
618 | + |
|
619 | + /** |
|
620 | + * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar |
|
621 | + * @return bool true when the avatar was set successfully or is up to date |
|
622 | + */ |
|
623 | + public function updateAvatar(bool $force = false): bool { |
|
624 | + if (!$force && $this->wasRefreshed('avatar')) { |
|
625 | + return false; |
|
626 | + } |
|
627 | + $avatarImage = $this->getAvatarImage(); |
|
628 | + if ($avatarImage === false) { |
|
629 | + //not set, nothing left to do; |
|
630 | + return false; |
|
631 | + } |
|
632 | + |
|
633 | + if (!$this->image->loadFromBase64(base64_encode($avatarImage))) { |
|
634 | + return false; |
|
635 | + } |
|
636 | + |
|
637 | + // use the checksum before modifications |
|
638 | + $checksum = md5($this->image->data()); |
|
639 | + |
|
640 | + if ($checksum === $this->config->getUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', '') && $this->avatarExists()) { |
|
641 | + return true; |
|
642 | + } |
|
643 | + |
|
644 | + $isSet = $this->setNextcloudAvatar(); |
|
645 | + |
|
646 | + if ($isSet) { |
|
647 | + // save checksum only after successful setting |
|
648 | + $this->config->setUserValue($this->uid, 'user_ldap', 'lastAvatarChecksum', $checksum); |
|
649 | + } |
|
650 | + |
|
651 | + return $isSet; |
|
652 | + } |
|
653 | + |
|
654 | + private function avatarExists(): bool { |
|
655 | + try { |
|
656 | + $currentAvatar = $this->avatarManager->getAvatar($this->uid); |
|
657 | + return $currentAvatar->exists() && $currentAvatar->isCustomAvatar(); |
|
658 | + } catch (\Exception $e) { |
|
659 | + return false; |
|
660 | + } |
|
661 | + } |
|
662 | + |
|
663 | + /** |
|
664 | + * @brief sets an image as Nextcloud avatar |
|
665 | + */ |
|
666 | + private function setNextcloudAvatar(): bool { |
|
667 | + if (!$this->image->valid()) { |
|
668 | + $this->logger->error('avatar image data from LDAP invalid for ' . $this->dn, ['app' => 'user_ldap']); |
|
669 | + return false; |
|
670 | + } |
|
671 | + |
|
672 | + |
|
673 | + //make sure it is a square and not bigger than 512x512 |
|
674 | + $size = min([$this->image->width(), $this->image->height(), 512]); |
|
675 | + if (!$this->image->centerCrop($size)) { |
|
676 | + $this->logger->error('croping image for avatar failed for ' . $this->dn, ['app' => 'user_ldap']); |
|
677 | + return false; |
|
678 | + } |
|
679 | + |
|
680 | + try { |
|
681 | + $avatar = $this->avatarManager->getAvatar($this->uid); |
|
682 | + $avatar->set($this->image); |
|
683 | + return true; |
|
684 | + } catch (\Exception $e) { |
|
685 | + $this->logger->info('Could not set avatar for ' . $this->dn, ['exception' => $e]); |
|
686 | + } |
|
687 | + return false; |
|
688 | + } |
|
689 | + |
|
690 | + /** |
|
691 | + * @throws AttributeNotSet |
|
692 | + * @throws \OC\ServerNotAvailableException |
|
693 | + * @throws PreConditionNotMetException |
|
694 | + */ |
|
695 | + public function getExtStorageHome():string { |
|
696 | + $value = $this->config->getUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', ''); |
|
697 | + if ($value !== '') { |
|
698 | + return $value; |
|
699 | + } |
|
700 | + |
|
701 | + $value = $this->updateExtStorageHome(); |
|
702 | + if ($value !== '') { |
|
703 | + return $value; |
|
704 | + } |
|
705 | + |
|
706 | + throw new AttributeNotSet(sprintf( |
|
707 | + 'external home storage attribute yield no value for %s', $this->getUsername() |
|
708 | + )); |
|
709 | + } |
|
710 | + |
|
711 | + /** |
|
712 | + * @throws PreConditionNotMetException |
|
713 | + * @throws \OC\ServerNotAvailableException |
|
714 | + */ |
|
715 | + public function updateExtStorageHome(?string $valueFromLDAP = null):string { |
|
716 | + if ($valueFromLDAP === null) { |
|
717 | + $extHomeValues = $this->access->readAttribute($this->getDN(), $this->connection->ldapExtStorageHomeAttribute); |
|
718 | + } else { |
|
719 | + $extHomeValues = [$valueFromLDAP]; |
|
720 | + } |
|
721 | + if ($extHomeValues !== false && isset($extHomeValues[0])) { |
|
722 | + $extHome = $extHomeValues[0]; |
|
723 | + $this->config->setUserValue($this->getUsername(), 'user_ldap', 'extStorageHome', $extHome); |
|
724 | + return $extHome; |
|
725 | + } else { |
|
726 | + $this->config->deleteUserValue($this->getUsername(), 'user_ldap', 'extStorageHome'); |
|
727 | + return ''; |
|
728 | + } |
|
729 | + } |
|
730 | + |
|
731 | + /** |
|
732 | + * called by a post_login hook to handle password expiry |
|
733 | + */ |
|
734 | + public function handlePasswordExpiry(array $params): void { |
|
735 | + $ppolicyDN = $this->connection->ldapDefaultPPolicyDN; |
|
736 | + if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) { |
|
737 | + //password expiry handling disabled |
|
738 | + return; |
|
739 | + } |
|
740 | + $uid = $params['uid']; |
|
741 | + if (isset($uid) && $uid === $this->getUsername()) { |
|
742 | + //retrieve relevant user attributes |
|
743 | + $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); |
|
744 | + |
|
745 | + if (array_key_exists('pwdpolicysubentry', $result[0])) { |
|
746 | + $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; |
|
747 | + if ($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)) { |
|
748 | + $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN |
|
749 | + } |
|
750 | + } |
|
751 | + |
|
752 | + $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : []; |
|
753 | + $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : []; |
|
754 | + $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : []; |
|
755 | + |
|
756 | + //retrieve relevant password policy attributes |
|
757 | + $cacheKey = 'ppolicyAttributes' . $ppolicyDN; |
|
758 | + $result = $this->connection->getFromCache($cacheKey); |
|
759 | + if (is_null($result)) { |
|
760 | + $result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']); |
|
761 | + $this->connection->writeToCache($cacheKey, $result); |
|
762 | + } |
|
763 | + |
|
764 | + $pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : []; |
|
765 | + $pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : []; |
|
766 | + $pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : []; |
|
767 | + |
|
768 | + //handle grace login |
|
769 | + if (!empty($pwdGraceUseTime)) { //was this a grace login? |
|
770 | + if (!empty($pwdGraceAuthNLimit) |
|
771 | + && count($pwdGraceUseTime) < (int)$pwdGraceAuthNLimit[0]) { //at least one more grace login available? |
|
772 | + $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
773 | + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( |
|
774 | + 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); |
|
775 | + } else { //no more grace login available |
|
776 | + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( |
|
777 | + 'user_ldap.renewPassword.showLoginFormInvalidPassword', ['user' => $uid])); |
|
778 | + } |
|
779 | + exit(); |
|
780 | + } |
|
781 | + //handle pwdReset attribute |
|
782 | + if (!empty($pwdReset) && $pwdReset[0] === 'TRUE') { //user must change their password |
|
783 | + $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
784 | + header('Location: ' . Server::get(IURLGenerator::class)->linkToRouteAbsolute( |
|
785 | + 'user_ldap.renewPassword.showRenewPasswordForm', ['user' => $uid])); |
|
786 | + exit(); |
|
787 | + } |
|
788 | + //handle password expiry warning |
|
789 | + if (!empty($pwdChangedTime)) { |
|
790 | + if (!empty($pwdMaxAge) |
|
791 | + && !empty($pwdExpireWarning)) { |
|
792 | + $pwdMaxAgeInt = (int)$pwdMaxAge[0]; |
|
793 | + $pwdExpireWarningInt = (int)$pwdExpireWarning[0]; |
|
794 | + if ($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0) { |
|
795 | + $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]); |
|
796 | + $pwdChangedTimeDt->add(new \DateInterval('PT' . $pwdMaxAgeInt . 'S')); |
|
797 | + $currentDateTime = new \DateTime(); |
|
798 | + $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp(); |
|
799 | + if ($secondsToExpiry <= $pwdExpireWarningInt) { |
|
800 | + //remove last password expiry warning if any |
|
801 | + $notification = $this->notificationManager->createNotification(); |
|
802 | + $notification->setApp('user_ldap') |
|
803 | + ->setUser($uid) |
|
804 | + ->setObject('pwd_exp_warn', $uid) |
|
805 | + ; |
|
806 | + $this->notificationManager->markProcessed($notification); |
|
807 | + //create new password expiry warning |
|
808 | + $notification = $this->notificationManager->createNotification(); |
|
809 | + $notification->setApp('user_ldap') |
|
810 | + ->setUser($uid) |
|
811 | + ->setDateTime($currentDateTime) |
|
812 | + ->setObject('pwd_exp_warn', $uid) |
|
813 | + ->setSubject('pwd_exp_warn_days', [(int)ceil($secondsToExpiry / 60 / 60 / 24)]) |
|
814 | + ; |
|
815 | + $this->notificationManager->notify($notification); |
|
816 | + } |
|
817 | + } |
|
818 | + } |
|
819 | + } |
|
820 | + } |
|
821 | + } |
|
822 | 822 | } |