Completed
Pull Request — master (#9293)
by Blizzz
18:49
created
apps/files_external/lib/Service/LegacyStoragesService.php 1 patch
Indentation   +171 added lines, -171 removed lines patch added patch discarded remove patch
@@ -31,179 +31,179 @@
 block discarded – undo
31 31
  * Read mount config from legacy mount.json
32 32
  */
33 33
 abstract class LegacyStoragesService {
34
-	/** @var BackendService */
35
-	protected $backendService;
34
+    /** @var BackendService */
35
+    protected $backendService;
36 36
 
37
-	/**
38
-	 * Read legacy config data
39
-	 *
40
-	 * @return array list of mount configs
41
-	 */
42
-	abstract protected function readLegacyConfig();
37
+    /**
38
+     * Read legacy config data
39
+     *
40
+     * @return array list of mount configs
41
+     */
42
+    abstract protected function readLegacyConfig();
43 43
 
44
-	/**
45
-	 * Copy legacy storage options into the given storage config object.
46
-	 *
47
-	 * @param StorageConfig $storageConfig storage config to populate
48
-	 * @param string $mountType mount type
49
-	 * @param string $applicable applicable user or group
50
-	 * @param array $storageOptions legacy storage options
51
-	 *
52
-	 * @return StorageConfig populated storage config
53
-	 */
54
-	protected function populateStorageConfigWithLegacyOptions(
55
-		&$storageConfig,
56
-		$mountType,
57
-		$applicable,
58
-		$storageOptions
59
-	) {
60
-		$backend = $this->backendService->getBackend($storageOptions['backend']);
61
-		if (!$backend) {
62
-			throw new \UnexpectedValueException('Invalid backend ' . $storageOptions['backend']);
63
-		}
64
-		$storageConfig->setBackend($backend);
65
-		if (isset($storageOptions['authMechanism']) && $storageOptions['authMechanism'] !== 'builtin::builtin') {
66
-			$authMechanism = $this->backendService->getAuthMechanism($storageOptions['authMechanism']);
67
-		} else {
68
-			$authMechanism = $backend->getLegacyAuthMechanism($storageOptions);
69
-			$storageOptions['authMechanism'] = 'null'; // to make error handling easier
70
-		}
71
-		if (!$authMechanism) {
72
-			throw new \UnexpectedValueException('Invalid authentication mechanism ' . $storageOptions['authMechanism']);
73
-		}
74
-		$storageConfig->setAuthMechanism($authMechanism);
75
-		$storageConfig->setBackendOptions($storageOptions['options']);
76
-		if (isset($storageOptions['mountOptions'])) {
77
-			$storageConfig->setMountOptions($storageOptions['mountOptions']);
78
-		}
79
-		if (!isset($storageOptions['priority'])) {
80
-			$storageOptions['priority'] = $backend->getPriority();
81
-		}
82
-		$storageConfig->setPriority($storageOptions['priority']);
83
-		if ($mountType === \OC_Mount_Config::MOUNT_TYPE_USER) {
84
-			$applicableUsers = $storageConfig->getApplicableUsers();
85
-			if ($applicable !== 'all') {
86
-				$applicableUsers[] = $applicable;
87
-				$storageConfig->setApplicableUsers($applicableUsers);
88
-			}
89
-		} else if ($mountType === \OC_Mount_Config::MOUNT_TYPE_GROUP) {
90
-			$applicableGroups = $storageConfig->getApplicableGroups();
91
-			$applicableGroups[] = $applicable;
92
-			$storageConfig->setApplicableGroups($applicableGroups);
93
-		}
94
-		return $storageConfig;
95
-	}
44
+    /**
45
+     * Copy legacy storage options into the given storage config object.
46
+     *
47
+     * @param StorageConfig $storageConfig storage config to populate
48
+     * @param string $mountType mount type
49
+     * @param string $applicable applicable user or group
50
+     * @param array $storageOptions legacy storage options
51
+     *
52
+     * @return StorageConfig populated storage config
53
+     */
54
+    protected function populateStorageConfigWithLegacyOptions(
55
+        &$storageConfig,
56
+        $mountType,
57
+        $applicable,
58
+        $storageOptions
59
+    ) {
60
+        $backend = $this->backendService->getBackend($storageOptions['backend']);
61
+        if (!$backend) {
62
+            throw new \UnexpectedValueException('Invalid backend ' . $storageOptions['backend']);
63
+        }
64
+        $storageConfig->setBackend($backend);
65
+        if (isset($storageOptions['authMechanism']) && $storageOptions['authMechanism'] !== 'builtin::builtin') {
66
+            $authMechanism = $this->backendService->getAuthMechanism($storageOptions['authMechanism']);
67
+        } else {
68
+            $authMechanism = $backend->getLegacyAuthMechanism($storageOptions);
69
+            $storageOptions['authMechanism'] = 'null'; // to make error handling easier
70
+        }
71
+        if (!$authMechanism) {
72
+            throw new \UnexpectedValueException('Invalid authentication mechanism ' . $storageOptions['authMechanism']);
73
+        }
74
+        $storageConfig->setAuthMechanism($authMechanism);
75
+        $storageConfig->setBackendOptions($storageOptions['options']);
76
+        if (isset($storageOptions['mountOptions'])) {
77
+            $storageConfig->setMountOptions($storageOptions['mountOptions']);
78
+        }
79
+        if (!isset($storageOptions['priority'])) {
80
+            $storageOptions['priority'] = $backend->getPriority();
81
+        }
82
+        $storageConfig->setPriority($storageOptions['priority']);
83
+        if ($mountType === \OC_Mount_Config::MOUNT_TYPE_USER) {
84
+            $applicableUsers = $storageConfig->getApplicableUsers();
85
+            if ($applicable !== 'all') {
86
+                $applicableUsers[] = $applicable;
87
+                $storageConfig->setApplicableUsers($applicableUsers);
88
+            }
89
+        } else if ($mountType === \OC_Mount_Config::MOUNT_TYPE_GROUP) {
90
+            $applicableGroups = $storageConfig->getApplicableGroups();
91
+            $applicableGroups[] = $applicable;
92
+            $storageConfig->setApplicableGroups($applicableGroups);
93
+        }
94
+        return $storageConfig;
95
+    }
96 96
 
97
-	/**
98
-	 * Read the external storages config
99
-	 *
100
-	 * @return StorageConfig[] map of storage id to storage config
101
-	 */
102
-	public function getAllStorages() {
103
-		$mountPoints = $this->readLegacyConfig();
104
-		/**
105
-		 * Here is the how the horribly messy mount point array looks like
106
-		 * from the mount.json file:
107
-		 *
108
-		 * $storageOptions = $mountPoints[$mountType][$applicable][$mountPath]
109
-		 *
110
-		 * - $mountType is either "user" or "group"
111
-		 * - $applicable is the name of a user or group (or the current user for personal mounts)
112
-		 * - $mountPath is the mount point path (where the storage must be mounted)
113
-		 * - $storageOptions is a map of storage options:
114
-		 *     - "priority": storage priority
115
-		 *     - "backend": backend identifier
116
-		 *     - "class": LEGACY backend class name
117
-		 *     - "options": backend-specific options
118
-		 *     - "authMechanism": authentication mechanism identifier
119
-		 *     - "mountOptions": mount-specific options (ex: disable previews, scanner, etc)
120
-		 */
121
-		// group by storage id
122
-		/** @var StorageConfig[] $storages */
123
-		$storages = [];
124
-		// for storages without id (legacy), group by config hash for
125
-		// later processing
126
-		$storagesWithConfigHash = [];
127
-		foreach ($mountPoints as $mountType => $applicables) {
128
-			foreach ($applicables as $applicable => $mountPaths) {
129
-				foreach ($mountPaths as $rootMountPath => $storageOptions) {
130
-					$currentStorage = null;
131
-					/**
132
-					 * Flag whether the config that was read already has an id.
133
-					 * If not, it will use a config hash instead and generate
134
-					 * a proper id later
135
-					 *
136
-					 * @var boolean
137
-					 */
138
-					$hasId = false;
139
-					// the root mount point is in the format "/$user/files/the/mount/point"
140
-					// we remove the "/$user/files" prefix
141
-					$parts = explode('/', ltrim($rootMountPath, '/'), 3);
142
-					if (count($parts) < 3) {
143
-						// something went wrong, skip
144
-						\OC::$server->getLogger()->error('Could not parse mount point "' . $rootMountPath . '"', ['app' => 'files_external']);
145
-						continue;
146
-					}
147
-					$relativeMountPath = rtrim($parts[2], '/');
148
-					// note: we cannot do this after the loop because the decrypted config
149
-					// options might be needed for the config hash
150
-					$storageOptions['options'] = \OC_Mount_Config::decryptPasswords($storageOptions['options']);
151
-					if (!isset($storageOptions['backend'])) {
152
-						$storageOptions['backend'] = $storageOptions['class']; // legacy compat
153
-					}
154
-					if (!isset($storageOptions['authMechanism'])) {
155
-						$storageOptions['authMechanism'] = null; // ensure config hash works
156
-					}
157
-					if (isset($storageOptions['id'])) {
158
-						$configId = (int)$storageOptions['id'];
159
-						if (isset($storages[$configId])) {
160
-							$currentStorage = $storages[$configId];
161
-						}
162
-						$hasId = true;
163
-					} else {
164
-						// missing id in legacy config, need to generate
165
-						// but at this point we don't know the max-id, so use
166
-						// first group it by config hash
167
-						$storageOptions['mountpoint'] = $rootMountPath;
168
-						$configId = \OC_Mount_Config::makeConfigHash($storageOptions);
169
-						if (isset($storagesWithConfigHash[$configId])) {
170
-							$currentStorage = $storagesWithConfigHash[$configId];
171
-						}
172
-					}
173
-					if (is_null($currentStorage)) {
174
-						// create new
175
-						$currentStorage = new StorageConfig($configId);
176
-						$currentStorage->setMountPoint($relativeMountPath);
177
-					}
178
-					try {
179
-						$this->populateStorageConfigWithLegacyOptions(
180
-							$currentStorage,
181
-							$mountType,
182
-							$applicable,
183
-							$storageOptions
184
-						);
185
-						if ($hasId) {
186
-							$storages[$configId] = $currentStorage;
187
-						} else {
188
-							$storagesWithConfigHash[$configId] = $currentStorage;
189
-						}
190
-					} catch (\UnexpectedValueException $e) {
191
-						// don't die if a storage backend doesn't exist
192
-						\OC::$server->getLogger()->logException($e, [
193
-							'message' => 'Could not load storage.',
194
-							'level' => ILogger::ERROR,
195
-							'app' => 'files_external',
196
-						]);
197
-					}
198
-				}
199
-			}
200
-		}
97
+    /**
98
+     * Read the external storages config
99
+     *
100
+     * @return StorageConfig[] map of storage id to storage config
101
+     */
102
+    public function getAllStorages() {
103
+        $mountPoints = $this->readLegacyConfig();
104
+        /**
105
+         * Here is the how the horribly messy mount point array looks like
106
+         * from the mount.json file:
107
+         *
108
+         * $storageOptions = $mountPoints[$mountType][$applicable][$mountPath]
109
+         *
110
+         * - $mountType is either "user" or "group"
111
+         * - $applicable is the name of a user or group (or the current user for personal mounts)
112
+         * - $mountPath is the mount point path (where the storage must be mounted)
113
+         * - $storageOptions is a map of storage options:
114
+         *     - "priority": storage priority
115
+         *     - "backend": backend identifier
116
+         *     - "class": LEGACY backend class name
117
+         *     - "options": backend-specific options
118
+         *     - "authMechanism": authentication mechanism identifier
119
+         *     - "mountOptions": mount-specific options (ex: disable previews, scanner, etc)
120
+         */
121
+        // group by storage id
122
+        /** @var StorageConfig[] $storages */
123
+        $storages = [];
124
+        // for storages without id (legacy), group by config hash for
125
+        // later processing
126
+        $storagesWithConfigHash = [];
127
+        foreach ($mountPoints as $mountType => $applicables) {
128
+            foreach ($applicables as $applicable => $mountPaths) {
129
+                foreach ($mountPaths as $rootMountPath => $storageOptions) {
130
+                    $currentStorage = null;
131
+                    /**
132
+                     * Flag whether the config that was read already has an id.
133
+                     * If not, it will use a config hash instead and generate
134
+                     * a proper id later
135
+                     *
136
+                     * @var boolean
137
+                     */
138
+                    $hasId = false;
139
+                    // the root mount point is in the format "/$user/files/the/mount/point"
140
+                    // we remove the "/$user/files" prefix
141
+                    $parts = explode('/', ltrim($rootMountPath, '/'), 3);
142
+                    if (count($parts) < 3) {
143
+                        // something went wrong, skip
144
+                        \OC::$server->getLogger()->error('Could not parse mount point "' . $rootMountPath . '"', ['app' => 'files_external']);
145
+                        continue;
146
+                    }
147
+                    $relativeMountPath = rtrim($parts[2], '/');
148
+                    // note: we cannot do this after the loop because the decrypted config
149
+                    // options might be needed for the config hash
150
+                    $storageOptions['options'] = \OC_Mount_Config::decryptPasswords($storageOptions['options']);
151
+                    if (!isset($storageOptions['backend'])) {
152
+                        $storageOptions['backend'] = $storageOptions['class']; // legacy compat
153
+                    }
154
+                    if (!isset($storageOptions['authMechanism'])) {
155
+                        $storageOptions['authMechanism'] = null; // ensure config hash works
156
+                    }
157
+                    if (isset($storageOptions['id'])) {
158
+                        $configId = (int)$storageOptions['id'];
159
+                        if (isset($storages[$configId])) {
160
+                            $currentStorage = $storages[$configId];
161
+                        }
162
+                        $hasId = true;
163
+                    } else {
164
+                        // missing id in legacy config, need to generate
165
+                        // but at this point we don't know the max-id, so use
166
+                        // first group it by config hash
167
+                        $storageOptions['mountpoint'] = $rootMountPath;
168
+                        $configId = \OC_Mount_Config::makeConfigHash($storageOptions);
169
+                        if (isset($storagesWithConfigHash[$configId])) {
170
+                            $currentStorage = $storagesWithConfigHash[$configId];
171
+                        }
172
+                    }
173
+                    if (is_null($currentStorage)) {
174
+                        // create new
175
+                        $currentStorage = new StorageConfig($configId);
176
+                        $currentStorage->setMountPoint($relativeMountPath);
177
+                    }
178
+                    try {
179
+                        $this->populateStorageConfigWithLegacyOptions(
180
+                            $currentStorage,
181
+                            $mountType,
182
+                            $applicable,
183
+                            $storageOptions
184
+                        );
185
+                        if ($hasId) {
186
+                            $storages[$configId] = $currentStorage;
187
+                        } else {
188
+                            $storagesWithConfigHash[$configId] = $currentStorage;
189
+                        }
190
+                    } catch (\UnexpectedValueException $e) {
191
+                        // don't die if a storage backend doesn't exist
192
+                        \OC::$server->getLogger()->logException($e, [
193
+                            'message' => 'Could not load storage.',
194
+                            'level' => ILogger::ERROR,
195
+                            'app' => 'files_external',
196
+                        ]);
197
+                    }
198
+                }
199
+            }
200
+        }
201 201
 
202
-		// convert parameter values
203
-		foreach ($storages as $storage) {
204
-			$storage->getBackend()->validateStorageDefinition($storage);
205
-			$storage->getAuthMechanism()->validateStorageDefinition($storage);
206
-		}
207
-		return $storages;
208
-	}
202
+        // convert parameter values
203
+        foreach ($storages as $storage) {
204
+            $storage->getBackend()->validateStorageDefinition($storage);
205
+            $storage->getAuthMechanism()->validateStorageDefinition($storage);
206
+        }
207
+        return $storages;
208
+    }
209 209
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Service/StoragesService.php 1 patch
Indentation   +489 added lines, -489 removed lines patch added patch discarded remove patch
@@ -44,493 +44,493 @@
 block discarded – undo
44 44
  */
45 45
 abstract class StoragesService {
46 46
 
47
-	/** @var BackendService */
48
-	protected $backendService;
49
-
50
-	/**
51
-	 * @var DBConfigService
52
-	 */
53
-	protected $dbConfig;
54
-
55
-	/**
56
-	 * @var IUserMountCache
57
-	 */
58
-	protected $userMountCache;
59
-
60
-	/**
61
-	 * @param BackendService $backendService
62
-	 * @param DBConfigService $dbConfigService
63
-	 * @param IUserMountCache $userMountCache
64
-	 */
65
-	public function __construct(BackendService $backendService, DBConfigService $dbConfigService, IUserMountCache $userMountCache) {
66
-		$this->backendService = $backendService;
67
-		$this->dbConfig = $dbConfigService;
68
-		$this->userMountCache = $userMountCache;
69
-	}
70
-
71
-	protected function readDBConfig() {
72
-		return $this->dbConfig->getAdminMounts();
73
-	}
74
-
75
-	protected function getStorageConfigFromDBMount(array $mount) {
76
-		$applicableUsers = array_filter($mount['applicable'], function ($applicable) {
77
-			return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_USER;
78
-		});
79
-		$applicableUsers = array_map(function ($applicable) {
80
-			return $applicable['value'];
81
-		}, $applicableUsers);
82
-
83
-		$applicableGroups = array_filter($mount['applicable'], function ($applicable) {
84
-			return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_GROUP;
85
-		});
86
-		$applicableGroups = array_map(function ($applicable) {
87
-			return $applicable['value'];
88
-		}, $applicableGroups);
89
-
90
-		try {
91
-			$config = $this->createStorage(
92
-				$mount['mount_point'],
93
-				$mount['storage_backend'],
94
-				$mount['auth_backend'],
95
-				$mount['config'],
96
-				$mount['options'],
97
-				array_values($applicableUsers),
98
-				array_values($applicableGroups),
99
-				$mount['priority']
100
-			);
101
-			$config->setType($mount['type']);
102
-			$config->setId((int)$mount['mount_id']);
103
-			return $config;
104
-		} catch (\UnexpectedValueException $e) {
105
-			// don't die if a storage backend doesn't exist
106
-			\OC::$server->getLogger()->logException($e, [
107
-				'message' => 'Could not load storage.',
108
-				'level' => ILogger::ERROR,
109
-				'app' => 'files_external',
110
-			]);
111
-			return null;
112
-		} catch (\InvalidArgumentException $e) {
113
-			\OC::$server->getLogger()->logException($e, [
114
-				'message' => 'Could not load storage.',
115
-				'level' => ILogger::ERROR,
116
-				'app' => 'files_external',
117
-			]);
118
-			return null;
119
-		}
120
-	}
121
-
122
-	/**
123
-	 * Read the external storages config
124
-	 *
125
-	 * @return array map of storage id to storage config
126
-	 */
127
-	protected function readConfig() {
128
-		$mounts = $this->readDBConfig();
129
-		$configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
130
-		$configs = array_filter($configs, function ($config) {
131
-			return $config instanceof StorageConfig;
132
-		});
133
-
134
-		$keys = array_map(function (StorageConfig $config) {
135
-			return $config->getId();
136
-		}, $configs);
137
-
138
-		return array_combine($keys, $configs);
139
-	}
140
-
141
-	/**
142
-	 * Get a storage with status
143
-	 *
144
-	 * @param int $id storage id
145
-	 *
146
-	 * @return StorageConfig
147
-	 * @throws NotFoundException if the storage with the given id was not found
148
-	 */
149
-	public function getStorage($id) {
150
-		$mount = $this->dbConfig->getMountById($id);
151
-
152
-		if (!is_array($mount)) {
153
-			throw new NotFoundException('Storage with ID "' . $id . '" not found');
154
-		}
155
-
156
-		$config = $this->getStorageConfigFromDBMount($mount);
157
-		if ($this->isApplicable($config)) {
158
-			return $config;
159
-		} else {
160
-			throw new NotFoundException('Storage with ID "' . $id . '" not found');
161
-		}
162
-	}
163
-
164
-	/**
165
-	 * Check whether this storage service should provide access to a storage
166
-	 *
167
-	 * @param StorageConfig $config
168
-	 * @return bool
169
-	 */
170
-	abstract protected function isApplicable(StorageConfig $config);
171
-
172
-	/**
173
-	 * Gets all storages, valid or not
174
-	 *
175
-	 * @return StorageConfig[] array of storage configs
176
-	 */
177
-	public function getAllStorages() {
178
-		return $this->readConfig();
179
-	}
180
-
181
-	/**
182
-	 * Gets all valid storages
183
-	 *
184
-	 * @return StorageConfig[]
185
-	 */
186
-	public function getStorages() {
187
-		return array_filter($this->getAllStorages(), [$this, 'validateStorage']);
188
-	}
189
-
190
-	/**
191
-	 * Validate storage
192
-	 * FIXME: De-duplicate with StoragesController::validate()
193
-	 *
194
-	 * @param StorageConfig $storage
195
-	 * @return bool
196
-	 */
197
-	protected function validateStorage(StorageConfig $storage) {
198
-		/** @var Backend */
199
-		$backend = $storage->getBackend();
200
-		/** @var AuthMechanism */
201
-		$authMechanism = $storage->getAuthMechanism();
202
-
203
-		if (!$backend->isVisibleFor($this->getVisibilityType())) {
204
-			// not permitted to use backend
205
-			return false;
206
-		}
207
-		if (!$authMechanism->isVisibleFor($this->getVisibilityType())) {
208
-			// not permitted to use auth mechanism
209
-			return false;
210
-		}
211
-
212
-		return true;
213
-	}
214
-
215
-	/**
216
-	 * Get the visibility type for this controller, used in validation
217
-	 *
218
-	 * @return string BackendService::VISIBILITY_* constants
219
-	 */
220
-	abstract public function getVisibilityType();
221
-
222
-	/**
223
-	 * @return integer
224
-	 */
225
-	protected function getType() {
226
-		return DBConfigService::MOUNT_TYPE_ADMIN;
227
-	}
228
-
229
-	/**
230
-	 * Add new storage to the configuration
231
-	 *
232
-	 * @param StorageConfig $newStorage storage attributes
233
-	 *
234
-	 * @return StorageConfig storage config, with added id
235
-	 */
236
-	public function addStorage(StorageConfig $newStorage) {
237
-		$allStorages = $this->readConfig();
238
-
239
-		$configId = $this->dbConfig->addMount(
240
-			$newStorage->getMountPoint(),
241
-			$newStorage->getBackend()->getIdentifier(),
242
-			$newStorage->getAuthMechanism()->getIdentifier(),
243
-			$newStorage->getPriority(),
244
-			$this->getType()
245
-		);
246
-
247
-		$newStorage->setId($configId);
248
-
249
-		foreach ($newStorage->getApplicableUsers() as $user) {
250
-			$this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_USER, $user);
251
-		}
252
-		foreach ($newStorage->getApplicableGroups() as $group) {
253
-			$this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
254
-		}
255
-		foreach ($newStorage->getBackendOptions() as $key => $value) {
256
-			$this->dbConfig->setConfig($configId, $key, $value);
257
-		}
258
-		foreach ($newStorage->getMountOptions() as $key => $value) {
259
-			$this->dbConfig->setOption($configId, $key, $value);
260
-		}
261
-
262
-		if (count($newStorage->getApplicableUsers()) === 0 && count($newStorage->getApplicableGroups()) === 0) {
263
-			$this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
264
-		}
265
-
266
-		// add new storage
267
-		$allStorages[$configId] = $newStorage;
268
-
269
-		$this->triggerHooks($newStorage, Filesystem::signal_create_mount);
270
-
271
-		$newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS);
272
-		return $newStorage;
273
-	}
274
-
275
-	/**
276
-	 * Create a storage from its parameters
277
-	 *
278
-	 * @param string $mountPoint storage mount point
279
-	 * @param string $backendIdentifier backend identifier
280
-	 * @param string $authMechanismIdentifier authentication mechanism identifier
281
-	 * @param array $backendOptions backend-specific options
282
-	 * @param array|null $mountOptions mount-specific options
283
-	 * @param array|null $applicableUsers users for which to mount the storage
284
-	 * @param array|null $applicableGroups groups for which to mount the storage
285
-	 * @param int|null $priority priority
286
-	 *
287
-	 * @return StorageConfig
288
-	 */
289
-	public function createStorage(
290
-		$mountPoint,
291
-		$backendIdentifier,
292
-		$authMechanismIdentifier,
293
-		$backendOptions,
294
-		$mountOptions = null,
295
-		$applicableUsers = null,
296
-		$applicableGroups = null,
297
-		$priority = null
298
-	) {
299
-		$backend = $this->backendService->getBackend($backendIdentifier);
300
-		if (!$backend) {
301
-			$backend = new InvalidBackend($backendIdentifier);
302
-		}
303
-		$authMechanism = $this->backendService->getAuthMechanism($authMechanismIdentifier);
304
-		if (!$authMechanism) {
305
-			$authMechanism = new InvalidAuth($authMechanismIdentifier);
306
-		}
307
-		$newStorage = new StorageConfig();
308
-		$newStorage->setMountPoint($mountPoint);
309
-		$newStorage->setBackend($backend);
310
-		$newStorage->setAuthMechanism($authMechanism);
311
-		$newStorage->setBackendOptions($backendOptions);
312
-		if (isset($mountOptions)) {
313
-			$newStorage->setMountOptions($mountOptions);
314
-		}
315
-		if (isset($applicableUsers)) {
316
-			$newStorage->setApplicableUsers($applicableUsers);
317
-		}
318
-		if (isset($applicableGroups)) {
319
-			$newStorage->setApplicableGroups($applicableGroups);
320
-		}
321
-		if (isset($priority)) {
322
-			$newStorage->setPriority($priority);
323
-		}
324
-
325
-		return $newStorage;
326
-	}
327
-
328
-	/**
329
-	 * Triggers the given hook signal for all the applicables given
330
-	 *
331
-	 * @param string $signal signal
332
-	 * @param string $mountPoint hook mount pount param
333
-	 * @param string $mountType hook mount type param
334
-	 * @param array $applicableArray array of applicable users/groups for which to trigger the hook
335
-	 */
336
-	protected function triggerApplicableHooks($signal, $mountPoint, $mountType, $applicableArray) {
337
-		foreach ($applicableArray as $applicable) {
338
-			\OCP\Util::emitHook(
339
-				Filesystem::CLASSNAME,
340
-				$signal,
341
-				[
342
-					Filesystem::signal_param_path => $mountPoint,
343
-					Filesystem::signal_param_mount_type => $mountType,
344
-					Filesystem::signal_param_users => $applicable,
345
-				]
346
-			);
347
-		}
348
-	}
349
-
350
-	/**
351
-	 * Triggers $signal for all applicable users of the given
352
-	 * storage
353
-	 *
354
-	 * @param StorageConfig $storage storage data
355
-	 * @param string $signal signal to trigger
356
-	 */
357
-	abstract protected function triggerHooks(StorageConfig $storage, $signal);
358
-
359
-	/**
360
-	 * Triggers signal_create_mount or signal_delete_mount to
361
-	 * accommodate for additions/deletions in applicableUsers
362
-	 * and applicableGroups fields.
363
-	 *
364
-	 * @param StorageConfig $oldStorage old storage data
365
-	 * @param StorageConfig $newStorage new storage data
366
-	 */
367
-	abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage);
368
-
369
-	/**
370
-	 * Update storage to the configuration
371
-	 *
372
-	 * @param StorageConfig $updatedStorage storage attributes
373
-	 *
374
-	 * @return StorageConfig storage config
375
-	 * @throws NotFoundException if the given storage does not exist in the config
376
-	 */
377
-	public function updateStorage(StorageConfig $updatedStorage) {
378
-		$id = $updatedStorage->getId();
379
-
380
-		$existingMount = $this->dbConfig->getMountById($id);
381
-
382
-		if (!is_array($existingMount)) {
383
-			throw new NotFoundException('Storage with ID "' . $id . '" not found while updating storage');
384
-		}
385
-
386
-		$oldStorage = $this->getStorageConfigFromDBMount($existingMount);
387
-
388
-		if ($oldStorage->getBackend() instanceof InvalidBackend) {
389
-			throw new NotFoundException('Storage with id "' . $id . '" cannot be edited due to missing backend');
390
-		}
391
-
392
-		$removedUsers = array_diff($oldStorage->getApplicableUsers(), $updatedStorage->getApplicableUsers());
393
-		$removedGroups = array_diff($oldStorage->getApplicableGroups(), $updatedStorage->getApplicableGroups());
394
-		$addedUsers = array_diff($updatedStorage->getApplicableUsers(), $oldStorage->getApplicableUsers());
395
-		$addedGroups = array_diff($updatedStorage->getApplicableGroups(), $oldStorage->getApplicableGroups());
396
-
397
-		$oldUserCount = count($oldStorage->getApplicableUsers());
398
-		$oldGroupCount = count($oldStorage->getApplicableGroups());
399
-		$newUserCount = count($updatedStorage->getApplicableUsers());
400
-		$newGroupCount = count($updatedStorage->getApplicableGroups());
401
-		$wasGlobal = ($oldUserCount + $oldGroupCount) === 0;
402
-		$isGlobal = ($newUserCount + $newGroupCount) === 0;
403
-
404
-		foreach ($removedUsers as $user) {
405
-			$this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
406
-		}
407
-		foreach ($removedGroups as $group) {
408
-			$this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
409
-		}
410
-		foreach ($addedUsers as $user) {
411
-			$this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
412
-		}
413
-		foreach ($addedGroups as $group) {
414
-			$this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
415
-		}
416
-
417
-		if ($wasGlobal && !$isGlobal) {
418
-			$this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
419
-		} else if (!$wasGlobal && $isGlobal) {
420
-			$this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
421
-		}
422
-
423
-		$changedConfig = array_diff_assoc($updatedStorage->getBackendOptions(), $oldStorage->getBackendOptions());
424
-		$changedOptions = array_diff_assoc($updatedStorage->getMountOptions(), $oldStorage->getMountOptions());
425
-
426
-		foreach ($changedConfig as $key => $value) {
427
-			$this->dbConfig->setConfig($id, $key, $value);
428
-		}
429
-		foreach ($changedOptions as $key => $value) {
430
-			$this->dbConfig->setOption($id, $key, $value);
431
-		}
432
-
433
-		if ($updatedStorage->getMountPoint() !== $oldStorage->getMountPoint()) {
434
-			$this->dbConfig->setMountPoint($id, $updatedStorage->getMountPoint());
435
-		}
436
-
437
-		if ($updatedStorage->getAuthMechanism()->getIdentifier() !== $oldStorage->getAuthMechanism()->getIdentifier()) {
438
-			$this->dbConfig->setAuthBackend($id, $updatedStorage->getAuthMechanism()->getIdentifier());
439
-		}
440
-
441
-		$this->triggerChangeHooks($oldStorage, $updatedStorage);
442
-
443
-		if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly
444
-			$this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage));
445
-		} else {
446
-			$storageId = $this->getStorageId($updatedStorage);
447
-			foreach ($removedUsers as $userId) {
448
-				$this->userMountCache->removeUserStorageMount($storageId, $userId);
449
-			}
450
-		}
451
-
452
-		return $this->getStorage($id);
453
-	}
454
-
455
-	/**
456
-	 * Delete the storage with the given id.
457
-	 *
458
-	 * @param int $id storage id
459
-	 *
460
-	 * @throws NotFoundException if no storage was found with the given id
461
-	 */
462
-	public function removeStorage($id) {
463
-		$existingMount = $this->dbConfig->getMountById($id);
464
-
465
-		if (!is_array($existingMount)) {
466
-			throw new NotFoundException('Storage with ID "' . $id . '" not found');
467
-		}
468
-
469
-		$this->dbConfig->removeMount($id);
470
-
471
-		$deletedStorage = $this->getStorageConfigFromDBMount($existingMount);
472
-		$this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount);
473
-
474
-		// delete oc_storages entries and oc_filecache
475
-		try {
476
-			$rustyStorageId = $this->getRustyStorageIdFromConfig($deletedStorage);
477
-			\OC\Files\Cache\Storage::remove($rustyStorageId);
478
-		} catch (\Exception $e) {
479
-			// can happen either for invalid configs where the storage could not
480
-			// be instantiated or whenever $user vars where used, in which case
481
-			// the storage id could not be computed
482
-			\OC::$server->getLogger()->logException($e, [
483
-				'level' => ILogger::ERROR,
484
-				'app' => 'files_external',
485
-			]);
486
-		}
487
-	}
488
-
489
-	/**
490
-	 * Returns the rusty storage id from oc_storages from the given storage config.
491
-	 *
492
-	 * @param StorageConfig $storageConfig
493
-	 * @return string rusty storage id
494
-	 */
495
-	private function getRustyStorageIdFromConfig(StorageConfig $storageConfig) {
496
-		// if any of the storage options contains $user, it is not possible
497
-		// to compute the possible storage id as we don't know which users
498
-		// mounted it already (and we certainly don't want to iterate over ALL users)
499
-		foreach ($storageConfig->getBackendOptions() as $value) {
500
-			if (strpos($value, '$user') !== false) {
501
-				throw new \Exception('Cannot compute storage id for deletion due to $user vars in the configuration');
502
-			}
503
-		}
504
-
505
-		// note: similar to ConfigAdapter->prepateStorageConfig()
506
-		$storageConfig->getAuthMechanism()->manipulateStorageConfig($storageConfig);
507
-		$storageConfig->getBackend()->manipulateStorageConfig($storageConfig);
508
-
509
-		$class = $storageConfig->getBackend()->getStorageClass();
510
-		$storageImpl = new $class($storageConfig->getBackendOptions());
511
-
512
-		return $storageImpl->getId();
513
-	}
514
-
515
-	/**
516
-	 * Construct the storage implementation
517
-	 *
518
-	 * @param StorageConfig $storageConfig
519
-	 * @return int
520
-	 */
521
-	private function getStorageId(StorageConfig $storageConfig) {
522
-		try {
523
-			$class = $storageConfig->getBackend()->getStorageClass();
524
-			/** @var \OC\Files\Storage\Storage $storage */
525
-			$storage = new $class($storageConfig->getBackendOptions());
526
-
527
-			// auth mechanism should fire first
528
-			$storage = $storageConfig->getBackend()->wrapStorage($storage);
529
-			$storage = $storageConfig->getAuthMechanism()->wrapStorage($storage);
530
-
531
-			return $storage->getStorageCache()->getNumericId();
532
-		} catch (\Exception $e) {
533
-			return -1;
534
-		}
535
-	}
47
+    /** @var BackendService */
48
+    protected $backendService;
49
+
50
+    /**
51
+     * @var DBConfigService
52
+     */
53
+    protected $dbConfig;
54
+
55
+    /**
56
+     * @var IUserMountCache
57
+     */
58
+    protected $userMountCache;
59
+
60
+    /**
61
+     * @param BackendService $backendService
62
+     * @param DBConfigService $dbConfigService
63
+     * @param IUserMountCache $userMountCache
64
+     */
65
+    public function __construct(BackendService $backendService, DBConfigService $dbConfigService, IUserMountCache $userMountCache) {
66
+        $this->backendService = $backendService;
67
+        $this->dbConfig = $dbConfigService;
68
+        $this->userMountCache = $userMountCache;
69
+    }
70
+
71
+    protected function readDBConfig() {
72
+        return $this->dbConfig->getAdminMounts();
73
+    }
74
+
75
+    protected function getStorageConfigFromDBMount(array $mount) {
76
+        $applicableUsers = array_filter($mount['applicable'], function ($applicable) {
77
+            return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_USER;
78
+        });
79
+        $applicableUsers = array_map(function ($applicable) {
80
+            return $applicable['value'];
81
+        }, $applicableUsers);
82
+
83
+        $applicableGroups = array_filter($mount['applicable'], function ($applicable) {
84
+            return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_GROUP;
85
+        });
86
+        $applicableGroups = array_map(function ($applicable) {
87
+            return $applicable['value'];
88
+        }, $applicableGroups);
89
+
90
+        try {
91
+            $config = $this->createStorage(
92
+                $mount['mount_point'],
93
+                $mount['storage_backend'],
94
+                $mount['auth_backend'],
95
+                $mount['config'],
96
+                $mount['options'],
97
+                array_values($applicableUsers),
98
+                array_values($applicableGroups),
99
+                $mount['priority']
100
+            );
101
+            $config->setType($mount['type']);
102
+            $config->setId((int)$mount['mount_id']);
103
+            return $config;
104
+        } catch (\UnexpectedValueException $e) {
105
+            // don't die if a storage backend doesn't exist
106
+            \OC::$server->getLogger()->logException($e, [
107
+                'message' => 'Could not load storage.',
108
+                'level' => ILogger::ERROR,
109
+                'app' => 'files_external',
110
+            ]);
111
+            return null;
112
+        } catch (\InvalidArgumentException $e) {
113
+            \OC::$server->getLogger()->logException($e, [
114
+                'message' => 'Could not load storage.',
115
+                'level' => ILogger::ERROR,
116
+                'app' => 'files_external',
117
+            ]);
118
+            return null;
119
+        }
120
+    }
121
+
122
+    /**
123
+     * Read the external storages config
124
+     *
125
+     * @return array map of storage id to storage config
126
+     */
127
+    protected function readConfig() {
128
+        $mounts = $this->readDBConfig();
129
+        $configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
130
+        $configs = array_filter($configs, function ($config) {
131
+            return $config instanceof StorageConfig;
132
+        });
133
+
134
+        $keys = array_map(function (StorageConfig $config) {
135
+            return $config->getId();
136
+        }, $configs);
137
+
138
+        return array_combine($keys, $configs);
139
+    }
140
+
141
+    /**
142
+     * Get a storage with status
143
+     *
144
+     * @param int $id storage id
145
+     *
146
+     * @return StorageConfig
147
+     * @throws NotFoundException if the storage with the given id was not found
148
+     */
149
+    public function getStorage($id) {
150
+        $mount = $this->dbConfig->getMountById($id);
151
+
152
+        if (!is_array($mount)) {
153
+            throw new NotFoundException('Storage with ID "' . $id . '" not found');
154
+        }
155
+
156
+        $config = $this->getStorageConfigFromDBMount($mount);
157
+        if ($this->isApplicable($config)) {
158
+            return $config;
159
+        } else {
160
+            throw new NotFoundException('Storage with ID "' . $id . '" not found');
161
+        }
162
+    }
163
+
164
+    /**
165
+     * Check whether this storage service should provide access to a storage
166
+     *
167
+     * @param StorageConfig $config
168
+     * @return bool
169
+     */
170
+    abstract protected function isApplicable(StorageConfig $config);
171
+
172
+    /**
173
+     * Gets all storages, valid or not
174
+     *
175
+     * @return StorageConfig[] array of storage configs
176
+     */
177
+    public function getAllStorages() {
178
+        return $this->readConfig();
179
+    }
180
+
181
+    /**
182
+     * Gets all valid storages
183
+     *
184
+     * @return StorageConfig[]
185
+     */
186
+    public function getStorages() {
187
+        return array_filter($this->getAllStorages(), [$this, 'validateStorage']);
188
+    }
189
+
190
+    /**
191
+     * Validate storage
192
+     * FIXME: De-duplicate with StoragesController::validate()
193
+     *
194
+     * @param StorageConfig $storage
195
+     * @return bool
196
+     */
197
+    protected function validateStorage(StorageConfig $storage) {
198
+        /** @var Backend */
199
+        $backend = $storage->getBackend();
200
+        /** @var AuthMechanism */
201
+        $authMechanism = $storage->getAuthMechanism();
202
+
203
+        if (!$backend->isVisibleFor($this->getVisibilityType())) {
204
+            // not permitted to use backend
205
+            return false;
206
+        }
207
+        if (!$authMechanism->isVisibleFor($this->getVisibilityType())) {
208
+            // not permitted to use auth mechanism
209
+            return false;
210
+        }
211
+
212
+        return true;
213
+    }
214
+
215
+    /**
216
+     * Get the visibility type for this controller, used in validation
217
+     *
218
+     * @return string BackendService::VISIBILITY_* constants
219
+     */
220
+    abstract public function getVisibilityType();
221
+
222
+    /**
223
+     * @return integer
224
+     */
225
+    protected function getType() {
226
+        return DBConfigService::MOUNT_TYPE_ADMIN;
227
+    }
228
+
229
+    /**
230
+     * Add new storage to the configuration
231
+     *
232
+     * @param StorageConfig $newStorage storage attributes
233
+     *
234
+     * @return StorageConfig storage config, with added id
235
+     */
236
+    public function addStorage(StorageConfig $newStorage) {
237
+        $allStorages = $this->readConfig();
238
+
239
+        $configId = $this->dbConfig->addMount(
240
+            $newStorage->getMountPoint(),
241
+            $newStorage->getBackend()->getIdentifier(),
242
+            $newStorage->getAuthMechanism()->getIdentifier(),
243
+            $newStorage->getPriority(),
244
+            $this->getType()
245
+        );
246
+
247
+        $newStorage->setId($configId);
248
+
249
+        foreach ($newStorage->getApplicableUsers() as $user) {
250
+            $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_USER, $user);
251
+        }
252
+        foreach ($newStorage->getApplicableGroups() as $group) {
253
+            $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
254
+        }
255
+        foreach ($newStorage->getBackendOptions() as $key => $value) {
256
+            $this->dbConfig->setConfig($configId, $key, $value);
257
+        }
258
+        foreach ($newStorage->getMountOptions() as $key => $value) {
259
+            $this->dbConfig->setOption($configId, $key, $value);
260
+        }
261
+
262
+        if (count($newStorage->getApplicableUsers()) === 0 && count($newStorage->getApplicableGroups()) === 0) {
263
+            $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
264
+        }
265
+
266
+        // add new storage
267
+        $allStorages[$configId] = $newStorage;
268
+
269
+        $this->triggerHooks($newStorage, Filesystem::signal_create_mount);
270
+
271
+        $newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS);
272
+        return $newStorage;
273
+    }
274
+
275
+    /**
276
+     * Create a storage from its parameters
277
+     *
278
+     * @param string $mountPoint storage mount point
279
+     * @param string $backendIdentifier backend identifier
280
+     * @param string $authMechanismIdentifier authentication mechanism identifier
281
+     * @param array $backendOptions backend-specific options
282
+     * @param array|null $mountOptions mount-specific options
283
+     * @param array|null $applicableUsers users for which to mount the storage
284
+     * @param array|null $applicableGroups groups for which to mount the storage
285
+     * @param int|null $priority priority
286
+     *
287
+     * @return StorageConfig
288
+     */
289
+    public function createStorage(
290
+        $mountPoint,
291
+        $backendIdentifier,
292
+        $authMechanismIdentifier,
293
+        $backendOptions,
294
+        $mountOptions = null,
295
+        $applicableUsers = null,
296
+        $applicableGroups = null,
297
+        $priority = null
298
+    ) {
299
+        $backend = $this->backendService->getBackend($backendIdentifier);
300
+        if (!$backend) {
301
+            $backend = new InvalidBackend($backendIdentifier);
302
+        }
303
+        $authMechanism = $this->backendService->getAuthMechanism($authMechanismIdentifier);
304
+        if (!$authMechanism) {
305
+            $authMechanism = new InvalidAuth($authMechanismIdentifier);
306
+        }
307
+        $newStorage = new StorageConfig();
308
+        $newStorage->setMountPoint($mountPoint);
309
+        $newStorage->setBackend($backend);
310
+        $newStorage->setAuthMechanism($authMechanism);
311
+        $newStorage->setBackendOptions($backendOptions);
312
+        if (isset($mountOptions)) {
313
+            $newStorage->setMountOptions($mountOptions);
314
+        }
315
+        if (isset($applicableUsers)) {
316
+            $newStorage->setApplicableUsers($applicableUsers);
317
+        }
318
+        if (isset($applicableGroups)) {
319
+            $newStorage->setApplicableGroups($applicableGroups);
320
+        }
321
+        if (isset($priority)) {
322
+            $newStorage->setPriority($priority);
323
+        }
324
+
325
+        return $newStorage;
326
+    }
327
+
328
+    /**
329
+     * Triggers the given hook signal for all the applicables given
330
+     *
331
+     * @param string $signal signal
332
+     * @param string $mountPoint hook mount pount param
333
+     * @param string $mountType hook mount type param
334
+     * @param array $applicableArray array of applicable users/groups for which to trigger the hook
335
+     */
336
+    protected function triggerApplicableHooks($signal, $mountPoint, $mountType, $applicableArray) {
337
+        foreach ($applicableArray as $applicable) {
338
+            \OCP\Util::emitHook(
339
+                Filesystem::CLASSNAME,
340
+                $signal,
341
+                [
342
+                    Filesystem::signal_param_path => $mountPoint,
343
+                    Filesystem::signal_param_mount_type => $mountType,
344
+                    Filesystem::signal_param_users => $applicable,
345
+                ]
346
+            );
347
+        }
348
+    }
349
+
350
+    /**
351
+     * Triggers $signal for all applicable users of the given
352
+     * storage
353
+     *
354
+     * @param StorageConfig $storage storage data
355
+     * @param string $signal signal to trigger
356
+     */
357
+    abstract protected function triggerHooks(StorageConfig $storage, $signal);
358
+
359
+    /**
360
+     * Triggers signal_create_mount or signal_delete_mount to
361
+     * accommodate for additions/deletions in applicableUsers
362
+     * and applicableGroups fields.
363
+     *
364
+     * @param StorageConfig $oldStorage old storage data
365
+     * @param StorageConfig $newStorage new storage data
366
+     */
367
+    abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage);
368
+
369
+    /**
370
+     * Update storage to the configuration
371
+     *
372
+     * @param StorageConfig $updatedStorage storage attributes
373
+     *
374
+     * @return StorageConfig storage config
375
+     * @throws NotFoundException if the given storage does not exist in the config
376
+     */
377
+    public function updateStorage(StorageConfig $updatedStorage) {
378
+        $id = $updatedStorage->getId();
379
+
380
+        $existingMount = $this->dbConfig->getMountById($id);
381
+
382
+        if (!is_array($existingMount)) {
383
+            throw new NotFoundException('Storage with ID "' . $id . '" not found while updating storage');
384
+        }
385
+
386
+        $oldStorage = $this->getStorageConfigFromDBMount($existingMount);
387
+
388
+        if ($oldStorage->getBackend() instanceof InvalidBackend) {
389
+            throw new NotFoundException('Storage with id "' . $id . '" cannot be edited due to missing backend');
390
+        }
391
+
392
+        $removedUsers = array_diff($oldStorage->getApplicableUsers(), $updatedStorage->getApplicableUsers());
393
+        $removedGroups = array_diff($oldStorage->getApplicableGroups(), $updatedStorage->getApplicableGroups());
394
+        $addedUsers = array_diff($updatedStorage->getApplicableUsers(), $oldStorage->getApplicableUsers());
395
+        $addedGroups = array_diff($updatedStorage->getApplicableGroups(), $oldStorage->getApplicableGroups());
396
+
397
+        $oldUserCount = count($oldStorage->getApplicableUsers());
398
+        $oldGroupCount = count($oldStorage->getApplicableGroups());
399
+        $newUserCount = count($updatedStorage->getApplicableUsers());
400
+        $newGroupCount = count($updatedStorage->getApplicableGroups());
401
+        $wasGlobal = ($oldUserCount + $oldGroupCount) === 0;
402
+        $isGlobal = ($newUserCount + $newGroupCount) === 0;
403
+
404
+        foreach ($removedUsers as $user) {
405
+            $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
406
+        }
407
+        foreach ($removedGroups as $group) {
408
+            $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
409
+        }
410
+        foreach ($addedUsers as $user) {
411
+            $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
412
+        }
413
+        foreach ($addedGroups as $group) {
414
+            $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
415
+        }
416
+
417
+        if ($wasGlobal && !$isGlobal) {
418
+            $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
419
+        } else if (!$wasGlobal && $isGlobal) {
420
+            $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
421
+        }
422
+
423
+        $changedConfig = array_diff_assoc($updatedStorage->getBackendOptions(), $oldStorage->getBackendOptions());
424
+        $changedOptions = array_diff_assoc($updatedStorage->getMountOptions(), $oldStorage->getMountOptions());
425
+
426
+        foreach ($changedConfig as $key => $value) {
427
+            $this->dbConfig->setConfig($id, $key, $value);
428
+        }
429
+        foreach ($changedOptions as $key => $value) {
430
+            $this->dbConfig->setOption($id, $key, $value);
431
+        }
432
+
433
+        if ($updatedStorage->getMountPoint() !== $oldStorage->getMountPoint()) {
434
+            $this->dbConfig->setMountPoint($id, $updatedStorage->getMountPoint());
435
+        }
436
+
437
+        if ($updatedStorage->getAuthMechanism()->getIdentifier() !== $oldStorage->getAuthMechanism()->getIdentifier()) {
438
+            $this->dbConfig->setAuthBackend($id, $updatedStorage->getAuthMechanism()->getIdentifier());
439
+        }
440
+
441
+        $this->triggerChangeHooks($oldStorage, $updatedStorage);
442
+
443
+        if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly
444
+            $this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage));
445
+        } else {
446
+            $storageId = $this->getStorageId($updatedStorage);
447
+            foreach ($removedUsers as $userId) {
448
+                $this->userMountCache->removeUserStorageMount($storageId, $userId);
449
+            }
450
+        }
451
+
452
+        return $this->getStorage($id);
453
+    }
454
+
455
+    /**
456
+     * Delete the storage with the given id.
457
+     *
458
+     * @param int $id storage id
459
+     *
460
+     * @throws NotFoundException if no storage was found with the given id
461
+     */
462
+    public function removeStorage($id) {
463
+        $existingMount = $this->dbConfig->getMountById($id);
464
+
465
+        if (!is_array($existingMount)) {
466
+            throw new NotFoundException('Storage with ID "' . $id . '" not found');
467
+        }
468
+
469
+        $this->dbConfig->removeMount($id);
470
+
471
+        $deletedStorage = $this->getStorageConfigFromDBMount($existingMount);
472
+        $this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount);
473
+
474
+        // delete oc_storages entries and oc_filecache
475
+        try {
476
+            $rustyStorageId = $this->getRustyStorageIdFromConfig($deletedStorage);
477
+            \OC\Files\Cache\Storage::remove($rustyStorageId);
478
+        } catch (\Exception $e) {
479
+            // can happen either for invalid configs where the storage could not
480
+            // be instantiated or whenever $user vars where used, in which case
481
+            // the storage id could not be computed
482
+            \OC::$server->getLogger()->logException($e, [
483
+                'level' => ILogger::ERROR,
484
+                'app' => 'files_external',
485
+            ]);
486
+        }
487
+    }
488
+
489
+    /**
490
+     * Returns the rusty storage id from oc_storages from the given storage config.
491
+     *
492
+     * @param StorageConfig $storageConfig
493
+     * @return string rusty storage id
494
+     */
495
+    private function getRustyStorageIdFromConfig(StorageConfig $storageConfig) {
496
+        // if any of the storage options contains $user, it is not possible
497
+        // to compute the possible storage id as we don't know which users
498
+        // mounted it already (and we certainly don't want to iterate over ALL users)
499
+        foreach ($storageConfig->getBackendOptions() as $value) {
500
+            if (strpos($value, '$user') !== false) {
501
+                throw new \Exception('Cannot compute storage id for deletion due to $user vars in the configuration');
502
+            }
503
+        }
504
+
505
+        // note: similar to ConfigAdapter->prepateStorageConfig()
506
+        $storageConfig->getAuthMechanism()->manipulateStorageConfig($storageConfig);
507
+        $storageConfig->getBackend()->manipulateStorageConfig($storageConfig);
508
+
509
+        $class = $storageConfig->getBackend()->getStorageClass();
510
+        $storageImpl = new $class($storageConfig->getBackendOptions());
511
+
512
+        return $storageImpl->getId();
513
+    }
514
+
515
+    /**
516
+     * Construct the storage implementation
517
+     *
518
+     * @param StorageConfig $storageConfig
519
+     * @return int
520
+     */
521
+    private function getStorageId(StorageConfig $storageConfig) {
522
+        try {
523
+            $class = $storageConfig->getBackend()->getStorageClass();
524
+            /** @var \OC\Files\Storage\Storage $storage */
525
+            $storage = new $class($storageConfig->getBackendOptions());
526
+
527
+            // auth mechanism should fire first
528
+            $storage = $storageConfig->getBackend()->wrapStorage($storage);
529
+            $storage = $storageConfig->getAuthMechanism()->wrapStorage($storage);
530
+
531
+            return $storage->getStorageCache()->getNumericId();
532
+        } catch (\Exception $e) {
533
+            return -1;
534
+        }
535
+    }
536 536
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Lib/Storage/SMB.php 1 patch
Indentation   +480 added lines, -480 removed lines patch added patch discarded remove patch
@@ -56,484 +56,484 @@
 block discarded – undo
56 56
 use OCP\Util;
57 57
 
58 58
 class SMB extends Common implements INotifyStorage {
59
-	/**
60
-	 * @var \Icewind\SMB\Server
61
-	 */
62
-	protected $server;
63
-
64
-	/**
65
-	 * @var \Icewind\SMB\Share
66
-	 */
67
-	protected $share;
68
-
69
-	/**
70
-	 * @var string
71
-	 */
72
-	protected $root;
73
-
74
-	/**
75
-	 * @var \Icewind\SMB\FileInfo[]
76
-	 */
77
-	protected $statCache;
78
-
79
-	public function __construct($params) {
80
-		if (isset($params['host']) && isset($params['user']) && isset($params['password']) && isset($params['share'])) {
81
-			if (Server::NativeAvailable()) {
82
-				$this->server = new NativeServer($params['host'], $params['user'], $params['password']);
83
-			} else {
84
-				$this->server = new Server($params['host'], $params['user'], $params['password']);
85
-			}
86
-			$this->share = $this->server->getShare(trim($params['share'], '/'));
87
-
88
-			$this->root = $params['root'] ?? '/';
89
-			$this->root = '/' . ltrim($this->root, '/');
90
-			$this->root = rtrim($this->root, '/') . '/';
91
-		} else {
92
-			throw new \Exception('Invalid configuration');
93
-		}
94
-		$this->statCache = new CappedMemoryCache();
95
-		parent::__construct($params);
96
-	}
97
-
98
-	/**
99
-	 * @return string
100
-	 */
101
-	public function getId() {
102
-		// FIXME: double slash to keep compatible with the old storage ids,
103
-		// failure to do so will lead to creation of a new storage id and
104
-		// loss of shares from the storage
105
-		return 'smb::' . $this->server->getUser() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
106
-	}
107
-
108
-	/**
109
-	 * @param string $path
110
-	 * @return string
111
-	 */
112
-	protected function buildPath($path) {
113
-		return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
114
-	}
115
-
116
-	protected function relativePath($fullPath) {
117
-		if ($fullPath === $this->root) {
118
-			return '';
119
-		} else if (substr($fullPath, 0, strlen($this->root)) === $this->root) {
120
-			return substr($fullPath, strlen($this->root));
121
-		} else {
122
-			return null;
123
-		}
124
-	}
125
-
126
-	/**
127
-	 * @param string $path
128
-	 * @return \Icewind\SMB\IFileInfo
129
-	 * @throws StorageNotAvailableException
130
-	 */
131
-	protected function getFileInfo($path) {
132
-		try {
133
-			$path = $this->buildPath($path);
134
-			if (!isset($this->statCache[$path])) {
135
-				$this->statCache[$path] = $this->share->stat($path);
136
-			}
137
-			return $this->statCache[$path];
138
-		} catch (ConnectException $e) {
139
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
140
-		}
141
-	}
142
-
143
-	/**
144
-	 * @param string $path
145
-	 * @return \Icewind\SMB\IFileInfo[]
146
-	 * @throws StorageNotAvailableException
147
-	 */
148
-	protected function getFolderContents($path) {
149
-		try {
150
-			$path = $this->buildPath($path);
151
-			$files = $this->share->dir($path);
152
-			foreach ($files as $file) {
153
-				$this->statCache[$path . '/' . $file->getName()] = $file;
154
-			}
155
-			return array_filter($files, function (IFileInfo $file) {
156
-				try {
157
-					return !$file->isHidden();
158
-				} catch (ForbiddenException $e) {
159
-					return false;
160
-				} catch (NotFoundException $e) {
161
-					return false;
162
-				}
163
-			});
164
-		} catch (ConnectException $e) {
165
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
166
-		}
167
-	}
168
-
169
-	/**
170
-	 * @param \Icewind\SMB\IFileInfo $info
171
-	 * @return array
172
-	 */
173
-	protected function formatInfo($info) {
174
-		$result = [
175
-			'size' => $info->getSize(),
176
-			'mtime' => $info->getMTime(),
177
-		];
178
-		if ($info->isDirectory()) {
179
-			$result['type'] = 'dir';
180
-		} else {
181
-			$result['type'] = 'file';
182
-		}
183
-		return $result;
184
-	}
185
-
186
-	/**
187
-	 * Rename the files. If the source or the target is the root, the rename won't happen.
188
-	 *
189
-	 * @param string $source the old name of the path
190
-	 * @param string $target the new name of the path
191
-	 * @return bool true if the rename is successful, false otherwise
192
-	 */
193
-	public function rename($source, $target) {
194
-		if ($this->isRootDir($source) || $this->isRootDir($target)) {
195
-			return false;
196
-		}
197
-
198
-		$absoluteSource = $this->buildPath($source);
199
-		$absoluteTarget = $this->buildPath($target);
200
-		try {
201
-			$result = $this->share->rename($absoluteSource, $absoluteTarget);
202
-		} catch (AlreadyExistsException $e) {
203
-			$this->remove($target);
204
-			$result = $this->share->rename($absoluteSource, $absoluteTarget);
205
-		} catch (\Exception $e) {
206
-			\OC::$server->getLogger()->logException($e, ['level' => ILogger::WARN]);
207
-			return false;
208
-		}
209
-		unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
210
-		return $result;
211
-	}
212
-
213
-	public function stat($path) {
214
-		try {
215
-			$result = $this->formatInfo($this->getFileInfo($path));
216
-		} catch (ForbiddenException $e) {
217
-			return false;
218
-		} catch (NotFoundException $e) {
219
-			return false;
220
-		}
221
-		if ($this->remoteIsShare() && $this->isRootDir($path)) {
222
-			$result['mtime'] = $this->shareMTime();
223
-		}
224
-		return $result;
225
-	}
226
-
227
-	/**
228
-	 * get the best guess for the modification time of the share
229
-	 *
230
-	 * @return int
231
-	 */
232
-	private function shareMTime() {
233
-		$highestMTime = 0;
234
-		$files = $this->share->dir($this->root);
235
-		foreach ($files as $fileInfo) {
236
-			try {
237
-				if ($fileInfo->getMTime() > $highestMTime) {
238
-					$highestMTime = $fileInfo->getMTime();
239
-				}
240
-			} catch (NotFoundException $e) {
241
-				// Ignore this, can happen on unavailable DFS shares
242
-			}
243
-		}
244
-		return $highestMTime;
245
-	}
246
-
247
-	/**
248
-	 * Check if the path is our root dir (not the smb one)
249
-	 *
250
-	 * @param string $path the path
251
-	 * @return bool
252
-	 */
253
-	private function isRootDir($path) {
254
-		return $path === '' || $path === '/' || $path === '.';
255
-	}
256
-
257
-	/**
258
-	 * Check if our root points to a smb share
259
-	 *
260
-	 * @return bool true if our root points to a share false otherwise
261
-	 */
262
-	private function remoteIsShare() {
263
-		return $this->share->getName() && (!$this->root || $this->root === '/');
264
-	}
265
-
266
-	/**
267
-	 * @param string $path
268
-	 * @return bool
269
-	 */
270
-	public function unlink($path) {
271
-		if ($this->isRootDir($path)) {
272
-			return false;
273
-		}
274
-
275
-		try {
276
-			if ($this->is_dir($path)) {
277
-				return $this->rmdir($path);
278
-			} else {
279
-				$path = $this->buildPath($path);
280
-				unset($this->statCache[$path]);
281
-				$this->share->del($path);
282
-				return true;
283
-			}
284
-		} catch (NotFoundException $e) {
285
-			return false;
286
-		} catch (ForbiddenException $e) {
287
-			return false;
288
-		} catch (ConnectException $e) {
289
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
290
-		}
291
-	}
292
-
293
-	/**
294
-	 * check if a file or folder has been updated since $time
295
-	 *
296
-	 * @param string $path
297
-	 * @param int $time
298
-	 * @return bool
299
-	 */
300
-	public function hasUpdated($path, $time) {
301
-		if (!$path and $this->root === '/') {
302
-			// mtime doesn't work for shares, but giving the nature of the backend,
303
-			// doing a full update is still just fast enough
304
-			return true;
305
-		} else {
306
-			$actualTime = $this->filemtime($path);
307
-			return $actualTime > $time;
308
-		}
309
-	}
310
-
311
-	/**
312
-	 * @param string $path
313
-	 * @param string $mode
314
-	 * @return resource|false
315
-	 */
316
-	public function fopen($path, $mode) {
317
-		$fullPath = $this->buildPath($path);
318
-		try {
319
-			switch ($mode) {
320
-				case 'r':
321
-				case 'rb':
322
-					if (!$this->file_exists($path)) {
323
-						return false;
324
-					}
325
-					return $this->share->read($fullPath);
326
-				case 'w':
327
-				case 'wb':
328
-					$source = $this->share->write($fullPath);
329
-					return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
330
-						unset($this->statCache[$fullPath]);
331
-					});
332
-				case 'a':
333
-				case 'ab':
334
-				case 'r+':
335
-				case 'w+':
336
-				case 'wb+':
337
-				case 'a+':
338
-				case 'x':
339
-				case 'x+':
340
-				case 'c':
341
-				case 'c+':
342
-					//emulate these
343
-					if (strrpos($path, '.') !== false) {
344
-						$ext = substr($path, strrpos($path, '.'));
345
-					} else {
346
-						$ext = '';
347
-					}
348
-					if ($this->file_exists($path)) {
349
-						if (!$this->isUpdatable($path)) {
350
-							return false;
351
-						}
352
-						$tmpFile = $this->getCachedFile($path);
353
-					} else {
354
-						if (!$this->isCreatable(dirname($path))) {
355
-							return false;
356
-						}
357
-						$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
358
-					}
359
-					$source = fopen($tmpFile, $mode);
360
-					$share = $this->share;
361
-					return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
362
-						unset($this->statCache[$fullPath]);
363
-						$share->put($tmpFile, $fullPath);
364
-						unlink($tmpFile);
365
-					});
366
-			}
367
-			return false;
368
-		} catch (NotFoundException $e) {
369
-			return false;
370
-		} catch (ForbiddenException $e) {
371
-			return false;
372
-		} catch (ConnectException $e) {
373
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
374
-		}
375
-	}
376
-
377
-	public function rmdir($path) {
378
-		if ($this->isRootDir($path)) {
379
-			return false;
380
-		}
381
-
382
-		try {
383
-			$this->statCache = array();
384
-			$content = $this->share->dir($this->buildPath($path));
385
-			foreach ($content as $file) {
386
-				if ($file->isDirectory()) {
387
-					$this->rmdir($path . '/' . $file->getName());
388
-				} else {
389
-					$this->share->del($file->getPath());
390
-				}
391
-			}
392
-			$this->share->rmdir($this->buildPath($path));
393
-			return true;
394
-		} catch (NotFoundException $e) {
395
-			return false;
396
-		} catch (ForbiddenException $e) {
397
-			return false;
398
-		} catch (ConnectException $e) {
399
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
400
-		}
401
-	}
402
-
403
-	public function touch($path, $time = null) {
404
-		try {
405
-			if (!$this->file_exists($path)) {
406
-				$fh = $this->share->write($this->buildPath($path));
407
-				fclose($fh);
408
-				return true;
409
-			}
410
-			return false;
411
-		} catch (ConnectException $e) {
412
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
413
-		}
414
-	}
415
-
416
-	public function opendir($path) {
417
-		try {
418
-			$files = $this->getFolderContents($path);
419
-		} catch (NotFoundException $e) {
420
-			return false;
421
-		} catch (ForbiddenException $e) {
422
-			return false;
423
-		}
424
-		$names = array_map(function ($info) {
425
-			/** @var \Icewind\SMB\IFileInfo $info */
426
-			return $info->getName();
427
-		}, $files);
428
-		return IteratorDirectory::wrap($names);
429
-	}
430
-
431
-	public function filetype($path) {
432
-		try {
433
-			return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
434
-		} catch (NotFoundException $e) {
435
-			return false;
436
-		} catch (ForbiddenException $e) {
437
-			return false;
438
-		}
439
-	}
440
-
441
-	public function mkdir($path) {
442
-		$path = $this->buildPath($path);
443
-		try {
444
-			$this->share->mkdir($path);
445
-			return true;
446
-		} catch (ConnectException $e) {
447
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
448
-		} catch (Exception $e) {
449
-			return false;
450
-		}
451
-	}
452
-
453
-	public function file_exists($path) {
454
-		try {
455
-			$this->getFileInfo($path);
456
-			return true;
457
-		} catch (NotFoundException $e) {
458
-			return false;
459
-		} catch (ForbiddenException $e) {
460
-			return false;
461
-		} catch (ConnectException $e) {
462
-			throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
463
-		}
464
-	}
465
-
466
-	public function isReadable($path) {
467
-		try {
468
-			$info = $this->getFileInfo($path);
469
-			return !$info->isHidden();
470
-		} catch (NotFoundException $e) {
471
-			return false;
472
-		} catch (ForbiddenException $e) {
473
-			return false;
474
-		}
475
-	}
476
-
477
-	public function isUpdatable($path) {
478
-		try {
479
-			$info = $this->getFileInfo($path);
480
-			// following windows behaviour for read-only folders: they can be written into
481
-			// (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
482
-			return !$info->isHidden() && (!$info->isReadOnly() || $this->is_dir($path));
483
-		} catch (NotFoundException $e) {
484
-			return false;
485
-		} catch (ForbiddenException $e) {
486
-			return false;
487
-		}
488
-	}
489
-
490
-	public function isDeletable($path) {
491
-		try {
492
-			$info = $this->getFileInfo($path);
493
-			return !$info->isHidden() && !$info->isReadOnly();
494
-		} catch (NotFoundException $e) {
495
-			return false;
496
-		} catch (ForbiddenException $e) {
497
-			return false;
498
-		}
499
-	}
500
-
501
-	/**
502
-	 * check if smbclient is installed
503
-	 */
504
-	public static function checkDependencies() {
505
-		return (
506
-			(bool)\OC_Helper::findBinaryPath('smbclient')
507
-			|| Server::NativeAvailable()
508
-		) ? true : ['smbclient'];
509
-	}
510
-
511
-	/**
512
-	 * Test a storage for availability
513
-	 *
514
-	 * @return bool
515
-	 */
516
-	public function test() {
517
-		try {
518
-			return parent::test();
519
-		} catch (Exception $e) {
520
-			return false;
521
-		}
522
-	}
523
-
524
-	public function listen($path, callable $callback) {
525
-		$this->notify($path)->listen(function (IChange $change) use ($callback) {
526
-			if ($change instanceof IRenameChange) {
527
-				return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
528
-			} else {
529
-				return $callback($change->getType(), $change->getPath());
530
-			}
531
-		});
532
-	}
533
-
534
-	public function notify($path) {
535
-		$path = '/' . ltrim($path, '/');
536
-		$shareNotifyHandler = $this->share->notify($this->buildPath($path));
537
-		return new SMBNotifyHandler($shareNotifyHandler, $this->root);
538
-	}
59
+    /**
60
+     * @var \Icewind\SMB\Server
61
+     */
62
+    protected $server;
63
+
64
+    /**
65
+     * @var \Icewind\SMB\Share
66
+     */
67
+    protected $share;
68
+
69
+    /**
70
+     * @var string
71
+     */
72
+    protected $root;
73
+
74
+    /**
75
+     * @var \Icewind\SMB\FileInfo[]
76
+     */
77
+    protected $statCache;
78
+
79
+    public function __construct($params) {
80
+        if (isset($params['host']) && isset($params['user']) && isset($params['password']) && isset($params['share'])) {
81
+            if (Server::NativeAvailable()) {
82
+                $this->server = new NativeServer($params['host'], $params['user'], $params['password']);
83
+            } else {
84
+                $this->server = new Server($params['host'], $params['user'], $params['password']);
85
+            }
86
+            $this->share = $this->server->getShare(trim($params['share'], '/'));
87
+
88
+            $this->root = $params['root'] ?? '/';
89
+            $this->root = '/' . ltrim($this->root, '/');
90
+            $this->root = rtrim($this->root, '/') . '/';
91
+        } else {
92
+            throw new \Exception('Invalid configuration');
93
+        }
94
+        $this->statCache = new CappedMemoryCache();
95
+        parent::__construct($params);
96
+    }
97
+
98
+    /**
99
+     * @return string
100
+     */
101
+    public function getId() {
102
+        // FIXME: double slash to keep compatible with the old storage ids,
103
+        // failure to do so will lead to creation of a new storage id and
104
+        // loss of shares from the storage
105
+        return 'smb::' . $this->server->getUser() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
106
+    }
107
+
108
+    /**
109
+     * @param string $path
110
+     * @return string
111
+     */
112
+    protected function buildPath($path) {
113
+        return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
114
+    }
115
+
116
+    protected function relativePath($fullPath) {
117
+        if ($fullPath === $this->root) {
118
+            return '';
119
+        } else if (substr($fullPath, 0, strlen($this->root)) === $this->root) {
120
+            return substr($fullPath, strlen($this->root));
121
+        } else {
122
+            return null;
123
+        }
124
+    }
125
+
126
+    /**
127
+     * @param string $path
128
+     * @return \Icewind\SMB\IFileInfo
129
+     * @throws StorageNotAvailableException
130
+     */
131
+    protected function getFileInfo($path) {
132
+        try {
133
+            $path = $this->buildPath($path);
134
+            if (!isset($this->statCache[$path])) {
135
+                $this->statCache[$path] = $this->share->stat($path);
136
+            }
137
+            return $this->statCache[$path];
138
+        } catch (ConnectException $e) {
139
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
140
+        }
141
+    }
142
+
143
+    /**
144
+     * @param string $path
145
+     * @return \Icewind\SMB\IFileInfo[]
146
+     * @throws StorageNotAvailableException
147
+     */
148
+    protected function getFolderContents($path) {
149
+        try {
150
+            $path = $this->buildPath($path);
151
+            $files = $this->share->dir($path);
152
+            foreach ($files as $file) {
153
+                $this->statCache[$path . '/' . $file->getName()] = $file;
154
+            }
155
+            return array_filter($files, function (IFileInfo $file) {
156
+                try {
157
+                    return !$file->isHidden();
158
+                } catch (ForbiddenException $e) {
159
+                    return false;
160
+                } catch (NotFoundException $e) {
161
+                    return false;
162
+                }
163
+            });
164
+        } catch (ConnectException $e) {
165
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
166
+        }
167
+    }
168
+
169
+    /**
170
+     * @param \Icewind\SMB\IFileInfo $info
171
+     * @return array
172
+     */
173
+    protected function formatInfo($info) {
174
+        $result = [
175
+            'size' => $info->getSize(),
176
+            'mtime' => $info->getMTime(),
177
+        ];
178
+        if ($info->isDirectory()) {
179
+            $result['type'] = 'dir';
180
+        } else {
181
+            $result['type'] = 'file';
182
+        }
183
+        return $result;
184
+    }
185
+
186
+    /**
187
+     * Rename the files. If the source or the target is the root, the rename won't happen.
188
+     *
189
+     * @param string $source the old name of the path
190
+     * @param string $target the new name of the path
191
+     * @return bool true if the rename is successful, false otherwise
192
+     */
193
+    public function rename($source, $target) {
194
+        if ($this->isRootDir($source) || $this->isRootDir($target)) {
195
+            return false;
196
+        }
197
+
198
+        $absoluteSource = $this->buildPath($source);
199
+        $absoluteTarget = $this->buildPath($target);
200
+        try {
201
+            $result = $this->share->rename($absoluteSource, $absoluteTarget);
202
+        } catch (AlreadyExistsException $e) {
203
+            $this->remove($target);
204
+            $result = $this->share->rename($absoluteSource, $absoluteTarget);
205
+        } catch (\Exception $e) {
206
+            \OC::$server->getLogger()->logException($e, ['level' => ILogger::WARN]);
207
+            return false;
208
+        }
209
+        unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
210
+        return $result;
211
+    }
212
+
213
+    public function stat($path) {
214
+        try {
215
+            $result = $this->formatInfo($this->getFileInfo($path));
216
+        } catch (ForbiddenException $e) {
217
+            return false;
218
+        } catch (NotFoundException $e) {
219
+            return false;
220
+        }
221
+        if ($this->remoteIsShare() && $this->isRootDir($path)) {
222
+            $result['mtime'] = $this->shareMTime();
223
+        }
224
+        return $result;
225
+    }
226
+
227
+    /**
228
+     * get the best guess for the modification time of the share
229
+     *
230
+     * @return int
231
+     */
232
+    private function shareMTime() {
233
+        $highestMTime = 0;
234
+        $files = $this->share->dir($this->root);
235
+        foreach ($files as $fileInfo) {
236
+            try {
237
+                if ($fileInfo->getMTime() > $highestMTime) {
238
+                    $highestMTime = $fileInfo->getMTime();
239
+                }
240
+            } catch (NotFoundException $e) {
241
+                // Ignore this, can happen on unavailable DFS shares
242
+            }
243
+        }
244
+        return $highestMTime;
245
+    }
246
+
247
+    /**
248
+     * Check if the path is our root dir (not the smb one)
249
+     *
250
+     * @param string $path the path
251
+     * @return bool
252
+     */
253
+    private function isRootDir($path) {
254
+        return $path === '' || $path === '/' || $path === '.';
255
+    }
256
+
257
+    /**
258
+     * Check if our root points to a smb share
259
+     *
260
+     * @return bool true if our root points to a share false otherwise
261
+     */
262
+    private function remoteIsShare() {
263
+        return $this->share->getName() && (!$this->root || $this->root === '/');
264
+    }
265
+
266
+    /**
267
+     * @param string $path
268
+     * @return bool
269
+     */
270
+    public function unlink($path) {
271
+        if ($this->isRootDir($path)) {
272
+            return false;
273
+        }
274
+
275
+        try {
276
+            if ($this->is_dir($path)) {
277
+                return $this->rmdir($path);
278
+            } else {
279
+                $path = $this->buildPath($path);
280
+                unset($this->statCache[$path]);
281
+                $this->share->del($path);
282
+                return true;
283
+            }
284
+        } catch (NotFoundException $e) {
285
+            return false;
286
+        } catch (ForbiddenException $e) {
287
+            return false;
288
+        } catch (ConnectException $e) {
289
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
290
+        }
291
+    }
292
+
293
+    /**
294
+     * check if a file or folder has been updated since $time
295
+     *
296
+     * @param string $path
297
+     * @param int $time
298
+     * @return bool
299
+     */
300
+    public function hasUpdated($path, $time) {
301
+        if (!$path and $this->root === '/') {
302
+            // mtime doesn't work for shares, but giving the nature of the backend,
303
+            // doing a full update is still just fast enough
304
+            return true;
305
+        } else {
306
+            $actualTime = $this->filemtime($path);
307
+            return $actualTime > $time;
308
+        }
309
+    }
310
+
311
+    /**
312
+     * @param string $path
313
+     * @param string $mode
314
+     * @return resource|false
315
+     */
316
+    public function fopen($path, $mode) {
317
+        $fullPath = $this->buildPath($path);
318
+        try {
319
+            switch ($mode) {
320
+                case 'r':
321
+                case 'rb':
322
+                    if (!$this->file_exists($path)) {
323
+                        return false;
324
+                    }
325
+                    return $this->share->read($fullPath);
326
+                case 'w':
327
+                case 'wb':
328
+                    $source = $this->share->write($fullPath);
329
+                    return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
330
+                        unset($this->statCache[$fullPath]);
331
+                    });
332
+                case 'a':
333
+                case 'ab':
334
+                case 'r+':
335
+                case 'w+':
336
+                case 'wb+':
337
+                case 'a+':
338
+                case 'x':
339
+                case 'x+':
340
+                case 'c':
341
+                case 'c+':
342
+                    //emulate these
343
+                    if (strrpos($path, '.') !== false) {
344
+                        $ext = substr($path, strrpos($path, '.'));
345
+                    } else {
346
+                        $ext = '';
347
+                    }
348
+                    if ($this->file_exists($path)) {
349
+                        if (!$this->isUpdatable($path)) {
350
+                            return false;
351
+                        }
352
+                        $tmpFile = $this->getCachedFile($path);
353
+                    } else {
354
+                        if (!$this->isCreatable(dirname($path))) {
355
+                            return false;
356
+                        }
357
+                        $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
358
+                    }
359
+                    $source = fopen($tmpFile, $mode);
360
+                    $share = $this->share;
361
+                    return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
362
+                        unset($this->statCache[$fullPath]);
363
+                        $share->put($tmpFile, $fullPath);
364
+                        unlink($tmpFile);
365
+                    });
366
+            }
367
+            return false;
368
+        } catch (NotFoundException $e) {
369
+            return false;
370
+        } catch (ForbiddenException $e) {
371
+            return false;
372
+        } catch (ConnectException $e) {
373
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
374
+        }
375
+    }
376
+
377
+    public function rmdir($path) {
378
+        if ($this->isRootDir($path)) {
379
+            return false;
380
+        }
381
+
382
+        try {
383
+            $this->statCache = array();
384
+            $content = $this->share->dir($this->buildPath($path));
385
+            foreach ($content as $file) {
386
+                if ($file->isDirectory()) {
387
+                    $this->rmdir($path . '/' . $file->getName());
388
+                } else {
389
+                    $this->share->del($file->getPath());
390
+                }
391
+            }
392
+            $this->share->rmdir($this->buildPath($path));
393
+            return true;
394
+        } catch (NotFoundException $e) {
395
+            return false;
396
+        } catch (ForbiddenException $e) {
397
+            return false;
398
+        } catch (ConnectException $e) {
399
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
400
+        }
401
+    }
402
+
403
+    public function touch($path, $time = null) {
404
+        try {
405
+            if (!$this->file_exists($path)) {
406
+                $fh = $this->share->write($this->buildPath($path));
407
+                fclose($fh);
408
+                return true;
409
+            }
410
+            return false;
411
+        } catch (ConnectException $e) {
412
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
413
+        }
414
+    }
415
+
416
+    public function opendir($path) {
417
+        try {
418
+            $files = $this->getFolderContents($path);
419
+        } catch (NotFoundException $e) {
420
+            return false;
421
+        } catch (ForbiddenException $e) {
422
+            return false;
423
+        }
424
+        $names = array_map(function ($info) {
425
+            /** @var \Icewind\SMB\IFileInfo $info */
426
+            return $info->getName();
427
+        }, $files);
428
+        return IteratorDirectory::wrap($names);
429
+    }
430
+
431
+    public function filetype($path) {
432
+        try {
433
+            return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
434
+        } catch (NotFoundException $e) {
435
+            return false;
436
+        } catch (ForbiddenException $e) {
437
+            return false;
438
+        }
439
+    }
440
+
441
+    public function mkdir($path) {
442
+        $path = $this->buildPath($path);
443
+        try {
444
+            $this->share->mkdir($path);
445
+            return true;
446
+        } catch (ConnectException $e) {
447
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
448
+        } catch (Exception $e) {
449
+            return false;
450
+        }
451
+    }
452
+
453
+    public function file_exists($path) {
454
+        try {
455
+            $this->getFileInfo($path);
456
+            return true;
457
+        } catch (NotFoundException $e) {
458
+            return false;
459
+        } catch (ForbiddenException $e) {
460
+            return false;
461
+        } catch (ConnectException $e) {
462
+            throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
463
+        }
464
+    }
465
+
466
+    public function isReadable($path) {
467
+        try {
468
+            $info = $this->getFileInfo($path);
469
+            return !$info->isHidden();
470
+        } catch (NotFoundException $e) {
471
+            return false;
472
+        } catch (ForbiddenException $e) {
473
+            return false;
474
+        }
475
+    }
476
+
477
+    public function isUpdatable($path) {
478
+        try {
479
+            $info = $this->getFileInfo($path);
480
+            // following windows behaviour for read-only folders: they can be written into
481
+            // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
482
+            return !$info->isHidden() && (!$info->isReadOnly() || $this->is_dir($path));
483
+        } catch (NotFoundException $e) {
484
+            return false;
485
+        } catch (ForbiddenException $e) {
486
+            return false;
487
+        }
488
+    }
489
+
490
+    public function isDeletable($path) {
491
+        try {
492
+            $info = $this->getFileInfo($path);
493
+            return !$info->isHidden() && !$info->isReadOnly();
494
+        } catch (NotFoundException $e) {
495
+            return false;
496
+        } catch (ForbiddenException $e) {
497
+            return false;
498
+        }
499
+    }
500
+
501
+    /**
502
+     * check if smbclient is installed
503
+     */
504
+    public static function checkDependencies() {
505
+        return (
506
+            (bool)\OC_Helper::findBinaryPath('smbclient')
507
+            || Server::NativeAvailable()
508
+        ) ? true : ['smbclient'];
509
+    }
510
+
511
+    /**
512
+     * Test a storage for availability
513
+     *
514
+     * @return bool
515
+     */
516
+    public function test() {
517
+        try {
518
+            return parent::test();
519
+        } catch (Exception $e) {
520
+            return false;
521
+        }
522
+    }
523
+
524
+    public function listen($path, callable $callback) {
525
+        $this->notify($path)->listen(function (IChange $change) use ($callback) {
526
+            if ($change instanceof IRenameChange) {
527
+                return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
528
+            } else {
529
+                return $callback($change->getType(), $change->getPath());
530
+            }
531
+        });
532
+    }
533
+
534
+    public function notify($path) {
535
+        $path = '/' . ltrim($path, '/');
536
+        $shareNotifyHandler = $this->share->notify($this->buildPath($path));
537
+        return new SMBNotifyHandler($shareNotifyHandler, $this->root);
538
+    }
539 539
 }
Please login to merge, or discard this patch.
apps/files_external/lib/Lib/Storage/Swift.php 1 patch
Indentation   +569 added lines, -569 removed lines patch added patch discarded remove patch
@@ -50,577 +50,577 @@
 block discarded – undo
50 50
 use OpenStack\ObjectStore\v1\Models\StorageObject;
51 51
 
52 52
 class Swift extends \OC\Files\Storage\Common {
53
-	/** @var SwiftFactory */
54
-	private $connectionFactory;
55
-	/**
56
-	 * @var \OpenStack\ObjectStore\v1\Models\Container
57
-	 */
58
-	private $container;
59
-	/**
60
-	 * @var string
61
-	 */
62
-	private $bucket;
63
-	/**
64
-	 * Connection parameters
65
-	 *
66
-	 * @var array
67
-	 */
68
-	private $params;
69
-
70
-	/** @var string */
71
-	private $id;
72
-
73
-	/** @var \OC\Files\ObjectStore\Swift */
74
-	private $objectStore;
75
-
76
-	/**
77
-	 * Key value cache mapping path to data object. Maps path to
78
-	 * \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing
79
-	 * paths and path to false for not existing paths.
80
-	 *
81
-	 * @var \OCP\ICache
82
-	 */
83
-	private $objectCache;
84
-
85
-	/**
86
-	 * @param string $path
87
-	 * @return mixed|string
88
-	 */
89
-	private function normalizePath(string $path) {
90
-		$path = trim($path, '/');
91
-
92
-		if (!$path) {
93
-			$path = '.';
94
-		}
95
-
96
-		$path = str_replace('#', '%23', $path);
97
-
98
-		return $path;
99
-	}
100
-
101
-	const SUBCONTAINER_FILE = '.subcontainers';
102
-
103
-	/**
104
-	 * translate directory path to container name
105
-	 *
106
-	 * @param string $path
107
-	 * @return string
108
-	 */
109
-
110
-	/**
111
-	 * Fetches an object from the API.
112
-	 * If the object is cached already or a
113
-	 * failed "doesn't exist" response was cached,
114
-	 * that one will be returned.
115
-	 *
116
-	 * @param string $path
117
-	 * @return StorageObject|bool object
118
-	 * or false if the object did not exist
119
-	 * @throws \OCP\Files\StorageAuthException
120
-	 * @throws \OCP\Files\StorageNotAvailableException
121
-	 */
122
-	private function fetchObject(string $path) {
123
-		if ($this->objectCache->hasKey($path)) {
124
-			// might be "false" if object did not exist from last check
125
-			return $this->objectCache->get($path);
126
-		}
127
-		try {
128
-			$object = $this->getContainer()->getObject($path);
129
-			$object->retrieve();
130
-			$this->objectCache->set($path, $object);
131
-			return $object;
132
-		} catch (BadResponseError $e) {
133
-			// Expected response is "404 Not Found", so only log if it isn't
134
-			if ($e->getResponse()->getStatusCode() !== 404) {
135
-				\OC::$server->getLogger()->logException($e, [
136
-					'level' => ILogger::ERROR,
137
-					'app' => 'files_external',
138
-				]);
139
-			}
140
-			$this->objectCache->set($path, false);
141
-			return false;
142
-		}
143
-	}
144
-
145
-	/**
146
-	 * Returns whether the given path exists.
147
-	 *
148
-	 * @param string $path
149
-	 *
150
-	 * @return bool true if the object exist, false otherwise
151
-	 * @throws \OCP\Files\StorageAuthException
152
-	 * @throws \OCP\Files\StorageNotAvailableException
153
-	 */
154
-	private function doesObjectExist($path) {
155
-		return $this->fetchObject($path) !== false;
156
-	}
157
-
158
-	public function __construct($params) {
159
-		if ((empty($params['key']) and empty($params['password']))
160
-			or (empty($params['user']) && empty($params['userid'])) or empty($params['bucket'])
161
-			or empty($params['region'])
162
-		) {
163
-			throw new StorageBadConfigException("API Key or password, Username, Bucket and Region have to be configured.");
164
-		}
165
-
166
-		$user = $params['user'];
167
-		$this->id = 'swift::' . $user . md5($params['bucket']);
168
-
169
-		$bucketUrl = new Uri($params['bucket']);
170
-		if ($bucketUrl->getHost()) {
171
-			$params['bucket'] = basename($bucketUrl->getPath());
172
-			$params['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath()));
173
-		}
174
-
175
-		if (empty($params['url'])) {
176
-			$params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/';
177
-		}
178
-
179
-		if (empty($params['service_name'])) {
180
-			$params['service_name'] = 'cloudFiles';
181
-		}
182
-
183
-		$params['autocreate'] = true;
184
-
185
-		if (isset($params['domain'])) {
186
-			$params['user'] = [
187
-				'name' => $params['user'],
188
-				'password' => $params['password'],
189
-				'domain' => [
190
-					'name' => $params['domain'],
191
-				]
192
-			];
193
-		}
194
-
195
-		$this->params = $params;
196
-		// FIXME: private class...
197
-		$this->objectCache = new \OC\Cache\CappedMemoryCache();
198
-		$this->connectionFactory = new SwiftFactory(
199
-			\OC::$server->getMemCacheFactory()->createDistributed('swift/'),
200
-			$this->params,
201
-			\OC::$server->getLogger()
202
-		);
203
-		$this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory);
204
-		$this->bucket = $params['bucket'];
205
-	}
206
-
207
-	public function mkdir($path) {
208
-		$path = $this->normalizePath($path);
209
-
210
-		if ($this->is_dir($path)) {
211
-			return false;
212
-		}
213
-
214
-		if ($path !== '.') {
215
-			$path .= '/';
216
-		}
217
-
218
-		try {
219
-			$this->getContainer()->createObject([
220
-				'name' => $path,
221
-				'content' => '',
222
-				'headers' => ['content-type' => 'httpd/unix-directory']
223
-			]);
224
-			// invalidate so that the next access gets the real object
225
-			// with all properties
226
-			$this->objectCache->remove($path);
227
-		} catch (BadResponseError $e) {
228
-			\OC::$server->getLogger()->logException($e, [
229
-				'level' => ILogger::ERROR,
230
-				'app' => 'files_external',
231
-			]);
232
-			return false;
233
-		}
234
-
235
-		return true;
236
-	}
237
-
238
-	public function file_exists($path) {
239
-		$path = $this->normalizePath($path);
240
-
241
-		if ($path !== '.' && $this->is_dir($path)) {
242
-			$path .= '/';
243
-		}
244
-
245
-		return $this->doesObjectExist($path);
246
-	}
247
-
248
-	public function rmdir($path) {
249
-		$path = $this->normalizePath($path);
250
-
251
-		if (!$this->is_dir($path) || !$this->isDeletable($path)) {
252
-			return false;
253
-		}
254
-
255
-		$dh = $this->opendir($path);
256
-		while ($file = readdir($dh)) {
257
-			if (\OC\Files\Filesystem::isIgnoredDir($file)) {
258
-				continue;
259
-			}
260
-
261
-			if ($this->is_dir($path . '/' . $file)) {
262
-				$this->rmdir($path . '/' . $file);
263
-			} else {
264
-				$this->unlink($path . '/' . $file);
265
-			}
266
-		}
267
-
268
-		try {
269
-			$this->objectStore->deleteObject($path . '/');
270
-			$this->objectCache->remove($path . '/');
271
-		} catch (BadResponseError $e) {
272
-			\OC::$server->getLogger()->logException($e, [
273
-				'level' => ILogger::ERROR,
274
-				'app' => 'files_external',
275
-			]);
276
-			return false;
277
-		}
278
-
279
-		return true;
280
-	}
281
-
282
-	public function opendir($path) {
283
-		$path = $this->normalizePath($path);
284
-
285
-		if ($path === '.') {
286
-			$path = '';
287
-		} else {
288
-			$path .= '/';
289
-		}
53
+    /** @var SwiftFactory */
54
+    private $connectionFactory;
55
+    /**
56
+     * @var \OpenStack\ObjectStore\v1\Models\Container
57
+     */
58
+    private $container;
59
+    /**
60
+     * @var string
61
+     */
62
+    private $bucket;
63
+    /**
64
+     * Connection parameters
65
+     *
66
+     * @var array
67
+     */
68
+    private $params;
69
+
70
+    /** @var string */
71
+    private $id;
72
+
73
+    /** @var \OC\Files\ObjectStore\Swift */
74
+    private $objectStore;
75
+
76
+    /**
77
+     * Key value cache mapping path to data object. Maps path to
78
+     * \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing
79
+     * paths and path to false for not existing paths.
80
+     *
81
+     * @var \OCP\ICache
82
+     */
83
+    private $objectCache;
84
+
85
+    /**
86
+     * @param string $path
87
+     * @return mixed|string
88
+     */
89
+    private function normalizePath(string $path) {
90
+        $path = trim($path, '/');
91
+
92
+        if (!$path) {
93
+            $path = '.';
94
+        }
95
+
96
+        $path = str_replace('#', '%23', $path);
97
+
98
+        return $path;
99
+    }
100
+
101
+    const SUBCONTAINER_FILE = '.subcontainers';
102
+
103
+    /**
104
+     * translate directory path to container name
105
+     *
106
+     * @param string $path
107
+     * @return string
108
+     */
109
+
110
+    /**
111
+     * Fetches an object from the API.
112
+     * If the object is cached already or a
113
+     * failed "doesn't exist" response was cached,
114
+     * that one will be returned.
115
+     *
116
+     * @param string $path
117
+     * @return StorageObject|bool object
118
+     * or false if the object did not exist
119
+     * @throws \OCP\Files\StorageAuthException
120
+     * @throws \OCP\Files\StorageNotAvailableException
121
+     */
122
+    private function fetchObject(string $path) {
123
+        if ($this->objectCache->hasKey($path)) {
124
+            // might be "false" if object did not exist from last check
125
+            return $this->objectCache->get($path);
126
+        }
127
+        try {
128
+            $object = $this->getContainer()->getObject($path);
129
+            $object->retrieve();
130
+            $this->objectCache->set($path, $object);
131
+            return $object;
132
+        } catch (BadResponseError $e) {
133
+            // Expected response is "404 Not Found", so only log if it isn't
134
+            if ($e->getResponse()->getStatusCode() !== 404) {
135
+                \OC::$server->getLogger()->logException($e, [
136
+                    'level' => ILogger::ERROR,
137
+                    'app' => 'files_external',
138
+                ]);
139
+            }
140
+            $this->objectCache->set($path, false);
141
+            return false;
142
+        }
143
+    }
144
+
145
+    /**
146
+     * Returns whether the given path exists.
147
+     *
148
+     * @param string $path
149
+     *
150
+     * @return bool true if the object exist, false otherwise
151
+     * @throws \OCP\Files\StorageAuthException
152
+     * @throws \OCP\Files\StorageNotAvailableException
153
+     */
154
+    private function doesObjectExist($path) {
155
+        return $this->fetchObject($path) !== false;
156
+    }
157
+
158
+    public function __construct($params) {
159
+        if ((empty($params['key']) and empty($params['password']))
160
+            or (empty($params['user']) && empty($params['userid'])) or empty($params['bucket'])
161
+            or empty($params['region'])
162
+        ) {
163
+            throw new StorageBadConfigException("API Key or password, Username, Bucket and Region have to be configured.");
164
+        }
165
+
166
+        $user = $params['user'];
167
+        $this->id = 'swift::' . $user . md5($params['bucket']);
168
+
169
+        $bucketUrl = new Uri($params['bucket']);
170
+        if ($bucketUrl->getHost()) {
171
+            $params['bucket'] = basename($bucketUrl->getPath());
172
+            $params['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath()));
173
+        }
174
+
175
+        if (empty($params['url'])) {
176
+            $params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/';
177
+        }
178
+
179
+        if (empty($params['service_name'])) {
180
+            $params['service_name'] = 'cloudFiles';
181
+        }
182
+
183
+        $params['autocreate'] = true;
184
+
185
+        if (isset($params['domain'])) {
186
+            $params['user'] = [
187
+                'name' => $params['user'],
188
+                'password' => $params['password'],
189
+                'domain' => [
190
+                    'name' => $params['domain'],
191
+                ]
192
+            ];
193
+        }
194
+
195
+        $this->params = $params;
196
+        // FIXME: private class...
197
+        $this->objectCache = new \OC\Cache\CappedMemoryCache();
198
+        $this->connectionFactory = new SwiftFactory(
199
+            \OC::$server->getMemCacheFactory()->createDistributed('swift/'),
200
+            $this->params,
201
+            \OC::$server->getLogger()
202
+        );
203
+        $this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory);
204
+        $this->bucket = $params['bucket'];
205
+    }
206
+
207
+    public function mkdir($path) {
208
+        $path = $this->normalizePath($path);
209
+
210
+        if ($this->is_dir($path)) {
211
+            return false;
212
+        }
213
+
214
+        if ($path !== '.') {
215
+            $path .= '/';
216
+        }
217
+
218
+        try {
219
+            $this->getContainer()->createObject([
220
+                'name' => $path,
221
+                'content' => '',
222
+                'headers' => ['content-type' => 'httpd/unix-directory']
223
+            ]);
224
+            // invalidate so that the next access gets the real object
225
+            // with all properties
226
+            $this->objectCache->remove($path);
227
+        } catch (BadResponseError $e) {
228
+            \OC::$server->getLogger()->logException($e, [
229
+                'level' => ILogger::ERROR,
230
+                'app' => 'files_external',
231
+            ]);
232
+            return false;
233
+        }
234
+
235
+        return true;
236
+    }
237
+
238
+    public function file_exists($path) {
239
+        $path = $this->normalizePath($path);
240
+
241
+        if ($path !== '.' && $this->is_dir($path)) {
242
+            $path .= '/';
243
+        }
244
+
245
+        return $this->doesObjectExist($path);
246
+    }
247
+
248
+    public function rmdir($path) {
249
+        $path = $this->normalizePath($path);
250
+
251
+        if (!$this->is_dir($path) || !$this->isDeletable($path)) {
252
+            return false;
253
+        }
254
+
255
+        $dh = $this->opendir($path);
256
+        while ($file = readdir($dh)) {
257
+            if (\OC\Files\Filesystem::isIgnoredDir($file)) {
258
+                continue;
259
+            }
260
+
261
+            if ($this->is_dir($path . '/' . $file)) {
262
+                $this->rmdir($path . '/' . $file);
263
+            } else {
264
+                $this->unlink($path . '/' . $file);
265
+            }
266
+        }
267
+
268
+        try {
269
+            $this->objectStore->deleteObject($path . '/');
270
+            $this->objectCache->remove($path . '/');
271
+        } catch (BadResponseError $e) {
272
+            \OC::$server->getLogger()->logException($e, [
273
+                'level' => ILogger::ERROR,
274
+                'app' => 'files_external',
275
+            ]);
276
+            return false;
277
+        }
278
+
279
+        return true;
280
+    }
281
+
282
+    public function opendir($path) {
283
+        $path = $this->normalizePath($path);
284
+
285
+        if ($path === '.') {
286
+            $path = '';
287
+        } else {
288
+            $path .= '/';
289
+        }
290 290
 
291 291
 //		$path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of #
292 292
 
293
-		try {
294
-			$files = [];
295
-			$objects = $this->getContainer()->listObjects([
296
-				'prefix' => $path,
297
-				'delimiter' => '/'
298
-			]);
299
-
300
-			/** @var StorageObject $object */
301
-			foreach ($objects as $object) {
302
-				$file = basename($object->name);
303
-				if ($file !== basename($path) && $file !== '.') {
304
-					$files[] = $file;
305
-				}
306
-			}
307
-
308
-			return IteratorDirectory::wrap($files);
309
-		} catch (\Exception $e) {
310
-			\OC::$server->getLogger()->logException($e, [
311
-				'level' => ILogger::ERROR,
312
-				'app' => 'files_external',
313
-			]);
314
-			return false;
315
-		}
316
-
317
-	}
318
-
319
-	public function stat($path) {
320
-		$path = $this->normalizePath($path);
321
-
322
-		if ($path === '.') {
323
-			$path = '';
324
-		} else if ($this->is_dir($path)) {
325
-			$path .= '/';
326
-		}
327
-
328
-		try {
329
-			$object = $this->fetchObject($path);
330
-			if (!$object) {
331
-				return false;
332
-			}
333
-		} catch (BadResponseError $e) {
334
-			\OC::$server->getLogger()->logException($e, [
335
-				'level' => ILogger::ERROR,
336
-				'app' => 'files_external',
337
-			]);
338
-			return false;
339
-		}
340
-
341
-		$dateTime = $object->lastModified ? \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified) : false;
342
-		$mtime = $dateTime ? $dateTime->getTimestamp() : null;
343
-		$objectMetadata = $object->getMetadata();
344
-		if (isset($objectMetadata['timestamp'])) {
345
-			$mtime = $objectMetadata['timestamp'];
346
-		}
347
-
348
-		if (!empty($mtime)) {
349
-			$mtime = floor($mtime);
350
-		}
351
-
352
-		$stat = array();
353
-		$stat['size'] = (int)$object->contentLength;
354
-		$stat['mtime'] = $mtime;
355
-		$stat['atime'] = time();
356
-		return $stat;
357
-	}
358
-
359
-	public function filetype($path) {
360
-		$path = $this->normalizePath($path);
361
-
362
-		if ($path !== '.' && $this->doesObjectExist($path)) {
363
-			return 'file';
364
-		}
365
-
366
-		if ($path !== '.') {
367
-			$path .= '/';
368
-		}
369
-
370
-		if ($this->doesObjectExist($path)) {
371
-			return 'dir';
372
-		}
373
-	}
374
-
375
-	public function unlink($path) {
376
-		$path = $this->normalizePath($path);
377
-
378
-		if ($this->is_dir($path)) {
379
-			return $this->rmdir($path);
380
-		}
381
-
382
-		try {
383
-			$this->objectStore->deleteObject($path);
384
-			$this->objectCache->remove($path);
385
-			$this->objectCache->remove($path . '/');
386
-		} catch (BadResponseError $e) {
387
-			if ($e->getResponse()->getStatusCode() !== 404) {
388
-				\OC::$server->getLogger()->logException($e, [
389
-					'level' => ILogger::ERROR,
390
-					'app' => 'files_external',
391
-				]);
392
-				throw $e;
393
-			}
394
-		}
395
-
396
-		return true;
397
-	}
398
-
399
-	public function fopen($path, $mode) {
400
-		$path = $this->normalizePath($path);
401
-
402
-		switch ($mode) {
403
-			case 'a':
404
-			case 'ab':
405
-			case 'a+':
406
-				return false;
407
-			case 'r':
408
-			case 'rb':
409
-				try {
410
-					return $this->objectStore->readObject($path);
411
-				} catch (BadResponseError $e) {
412
-					\OC::$server->getLogger()->logException($e, [
413
-						'level' => ILogger::ERROR,
414
-						'app' => 'files_external',
415
-					]);
416
-					return false;
417
-				}
418
-			case 'w':
419
-			case 'wb':
420
-			case 'r+':
421
-			case 'w+':
422
-			case 'wb+':
423
-			case 'x':
424
-			case 'x+':
425
-			case 'c':
426
-			case 'c+':
427
-				if (strrpos($path, '.') !== false) {
428
-					$ext = substr($path, strrpos($path, '.'));
429
-				} else {
430
-					$ext = '';
431
-				}
432
-				$tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
433
-				// Fetch existing file if required
434
-				if ($mode[0] !== 'w' && $this->file_exists($path)) {
435
-					if ($mode[0] === 'x') {
436
-						// File cannot already exist
437
-						return false;
438
-					}
439
-					$source = $this->fopen($path, 'r');
440
-					file_put_contents($tmpFile, $source);
441
-				}
442
-				$handle = fopen($tmpFile, $mode);
443
-				return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
444
-					$this->writeBack($tmpFile, $path);
445
-				});
446
-		}
447
-	}
448
-
449
-	public function touch($path, $mtime = null) {
450
-		$path = $this->normalizePath($path);
451
-		if (is_null($mtime)) {
452
-			$mtime = time();
453
-		}
454
-		$metadata = ['timestamp' => $mtime];
455
-		if ($this->file_exists($path)) {
456
-			if ($this->is_dir($path) && $path !== '.') {
457
-				$path .= '/';
458
-			}
459
-
460
-			$object = $this->fetchObject($path);
461
-			if ($object->mergeMetadata($metadata)) {
462
-				// invalidate target object to force repopulation on fetch
463
-				$this->objectCache->remove($path);
464
-			}
465
-			return true;
466
-		} else {
467
-			$mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
468
-			$this->getContainer()->createObject([
469
-				'name' => $path,
470
-				'content' => '',
471
-				'headers' => ['content-type' => 'httpd/unix-directory']
472
-			]);
473
-			// invalidate target object to force repopulation on fetch
474
-			$this->objectCache->remove($path);
475
-			return true;
476
-		}
477
-	}
478
-
479
-	public function copy($path1, $path2) {
480
-		$path1 = $this->normalizePath($path1);
481
-		$path2 = $this->normalizePath($path2);
482
-
483
-		$fileType = $this->filetype($path1);
484
-		if ($fileType) {
485
-			// make way
486
-			$this->unlink($path2);
487
-		}
488
-
489
-		if ($fileType === 'file') {
490
-			try {
491
-				$source = $this->fetchObject($path1);
492
-				$source->copy([
493
-					'destination' => $this->bucket . '/' . $path2
494
-				]);
495
-				// invalidate target object to force repopulation on fetch
496
-				$this->objectCache->remove($path2);
497
-				$this->objectCache->remove($path2 . '/');
498
-			} catch (BadResponseError $e) {
499
-				\OC::$server->getLogger()->logException($e, [
500
-					'level' => ILogger::ERROR,
501
-					'app' => 'files_external',
502
-				]);
503
-				return false;
504
-			}
505
-
506
-		} else if ($fileType === 'dir') {
507
-			try {
508
-				$source = $this->fetchObject($path1 . '/');
509
-				$source->copy([
510
-					'destination' => $this->bucket . '/' . $path2 . '/'
511
-				]);
512
-				// invalidate target object to force repopulation on fetch
513
-				$this->objectCache->remove($path2);
514
-				$this->objectCache->remove($path2 . '/');
515
-			} catch (BadResponseError $e) {
516
-				\OC::$server->getLogger()->logException($e, [
517
-					'level' => ILogger::ERROR,
518
-					'app' => 'files_external',
519
-				]);
520
-				return false;
521
-			}
522
-
523
-			$dh = $this->opendir($path1);
524
-			while ($file = readdir($dh)) {
525
-				if (\OC\Files\Filesystem::isIgnoredDir($file)) {
526
-					continue;
527
-				}
528
-
529
-				$source = $path1 . '/' . $file;
530
-				$target = $path2 . '/' . $file;
531
-				$this->copy($source, $target);
532
-			}
533
-
534
-		} else {
535
-			//file does not exist
536
-			return false;
537
-		}
538
-
539
-		return true;
540
-	}
541
-
542
-	public function rename($path1, $path2) {
543
-		$path1 = $this->normalizePath($path1);
544
-		$path2 = $this->normalizePath($path2);
545
-
546
-		$fileType = $this->filetype($path1);
547
-
548
-		if ($fileType === 'dir' || $fileType === 'file') {
549
-			// copy
550
-			if ($this->copy($path1, $path2) === false) {
551
-				return false;
552
-			}
553
-
554
-			// cleanup
555
-			if ($this->unlink($path1) === false) {
556
-				throw new \Exception('failed to remove original');
557
-				$this->unlink($path2);
558
-				return false;
559
-			}
560
-
561
-			return true;
562
-		}
563
-
564
-		return false;
565
-	}
566
-
567
-	public function getId() {
568
-		return $this->id;
569
-	}
570
-
571
-	/**
572
-	 * Returns the initialized object store container.
573
-	 *
574
-	 * @return \OpenStack\ObjectStore\v1\Models\Container
575
-	 * @throws \OCP\Files\StorageAuthException
576
-	 * @throws \OCP\Files\StorageNotAvailableException
577
-	 */
578
-	public function getContainer() {
579
-		if (is_null($this->container)) {
580
-			$this->container = $this->connectionFactory->getContainer();
581
-
582
-			if (!$this->file_exists('.')) {
583
-				$this->mkdir('.');
584
-			}
585
-		}
586
-		return $this->container;
587
-	}
588
-
589
-	public function writeBack($tmpFile, $path) {
590
-		$fileData = fopen($tmpFile, 'r');
591
-		$this->objectStore->writeObject($path, $fileData);
592
-		// invalidate target object to force repopulation on fetch
593
-		$this->objectCache->remove($path);
594
-		unlink($tmpFile);
595
-	}
596
-
597
-	public function hasUpdated($path, $time) {
598
-		if ($this->is_file($path)) {
599
-			return parent::hasUpdated($path, $time);
600
-		}
601
-		$path = $this->normalizePath($path);
602
-		$dh = $this->opendir($path);
603
-		$content = array();
604
-		while (($file = readdir($dh)) !== false) {
605
-			$content[] = $file;
606
-		}
607
-		if ($path === '.') {
608
-			$path = '';
609
-		}
610
-		$cachedContent = $this->getCache()->getFolderContents($path);
611
-		$cachedNames = array_map(function ($content) {
612
-			return $content['name'];
613
-		}, $cachedContent);
614
-		sort($cachedNames);
615
-		sort($content);
616
-		return $cachedNames !== $content;
617
-	}
618
-
619
-	/**
620
-	 * check if curl is installed
621
-	 */
622
-	public static function checkDependencies() {
623
-		return true;
624
-	}
293
+        try {
294
+            $files = [];
295
+            $objects = $this->getContainer()->listObjects([
296
+                'prefix' => $path,
297
+                'delimiter' => '/'
298
+            ]);
299
+
300
+            /** @var StorageObject $object */
301
+            foreach ($objects as $object) {
302
+                $file = basename($object->name);
303
+                if ($file !== basename($path) && $file !== '.') {
304
+                    $files[] = $file;
305
+                }
306
+            }
307
+
308
+            return IteratorDirectory::wrap($files);
309
+        } catch (\Exception $e) {
310
+            \OC::$server->getLogger()->logException($e, [
311
+                'level' => ILogger::ERROR,
312
+                'app' => 'files_external',
313
+            ]);
314
+            return false;
315
+        }
316
+
317
+    }
318
+
319
+    public function stat($path) {
320
+        $path = $this->normalizePath($path);
321
+
322
+        if ($path === '.') {
323
+            $path = '';
324
+        } else if ($this->is_dir($path)) {
325
+            $path .= '/';
326
+        }
327
+
328
+        try {
329
+            $object = $this->fetchObject($path);
330
+            if (!$object) {
331
+                return false;
332
+            }
333
+        } catch (BadResponseError $e) {
334
+            \OC::$server->getLogger()->logException($e, [
335
+                'level' => ILogger::ERROR,
336
+                'app' => 'files_external',
337
+            ]);
338
+            return false;
339
+        }
340
+
341
+        $dateTime = $object->lastModified ? \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified) : false;
342
+        $mtime = $dateTime ? $dateTime->getTimestamp() : null;
343
+        $objectMetadata = $object->getMetadata();
344
+        if (isset($objectMetadata['timestamp'])) {
345
+            $mtime = $objectMetadata['timestamp'];
346
+        }
347
+
348
+        if (!empty($mtime)) {
349
+            $mtime = floor($mtime);
350
+        }
351
+
352
+        $stat = array();
353
+        $stat['size'] = (int)$object->contentLength;
354
+        $stat['mtime'] = $mtime;
355
+        $stat['atime'] = time();
356
+        return $stat;
357
+    }
358
+
359
+    public function filetype($path) {
360
+        $path = $this->normalizePath($path);
361
+
362
+        if ($path !== '.' && $this->doesObjectExist($path)) {
363
+            return 'file';
364
+        }
365
+
366
+        if ($path !== '.') {
367
+            $path .= '/';
368
+        }
369
+
370
+        if ($this->doesObjectExist($path)) {
371
+            return 'dir';
372
+        }
373
+    }
374
+
375
+    public function unlink($path) {
376
+        $path = $this->normalizePath($path);
377
+
378
+        if ($this->is_dir($path)) {
379
+            return $this->rmdir($path);
380
+        }
381
+
382
+        try {
383
+            $this->objectStore->deleteObject($path);
384
+            $this->objectCache->remove($path);
385
+            $this->objectCache->remove($path . '/');
386
+        } catch (BadResponseError $e) {
387
+            if ($e->getResponse()->getStatusCode() !== 404) {
388
+                \OC::$server->getLogger()->logException($e, [
389
+                    'level' => ILogger::ERROR,
390
+                    'app' => 'files_external',
391
+                ]);
392
+                throw $e;
393
+            }
394
+        }
395
+
396
+        return true;
397
+    }
398
+
399
+    public function fopen($path, $mode) {
400
+        $path = $this->normalizePath($path);
401
+
402
+        switch ($mode) {
403
+            case 'a':
404
+            case 'ab':
405
+            case 'a+':
406
+                return false;
407
+            case 'r':
408
+            case 'rb':
409
+                try {
410
+                    return $this->objectStore->readObject($path);
411
+                } catch (BadResponseError $e) {
412
+                    \OC::$server->getLogger()->logException($e, [
413
+                        'level' => ILogger::ERROR,
414
+                        'app' => 'files_external',
415
+                    ]);
416
+                    return false;
417
+                }
418
+            case 'w':
419
+            case 'wb':
420
+            case 'r+':
421
+            case 'w+':
422
+            case 'wb+':
423
+            case 'x':
424
+            case 'x+':
425
+            case 'c':
426
+            case 'c+':
427
+                if (strrpos($path, '.') !== false) {
428
+                    $ext = substr($path, strrpos($path, '.'));
429
+                } else {
430
+                    $ext = '';
431
+                }
432
+                $tmpFile = \OC::$server->getTempManager()->getTemporaryFile($ext);
433
+                // Fetch existing file if required
434
+                if ($mode[0] !== 'w' && $this->file_exists($path)) {
435
+                    if ($mode[0] === 'x') {
436
+                        // File cannot already exist
437
+                        return false;
438
+                    }
439
+                    $source = $this->fopen($path, 'r');
440
+                    file_put_contents($tmpFile, $source);
441
+                }
442
+                $handle = fopen($tmpFile, $mode);
443
+                return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
444
+                    $this->writeBack($tmpFile, $path);
445
+                });
446
+        }
447
+    }
448
+
449
+    public function touch($path, $mtime = null) {
450
+        $path = $this->normalizePath($path);
451
+        if (is_null($mtime)) {
452
+            $mtime = time();
453
+        }
454
+        $metadata = ['timestamp' => $mtime];
455
+        if ($this->file_exists($path)) {
456
+            if ($this->is_dir($path) && $path !== '.') {
457
+                $path .= '/';
458
+            }
459
+
460
+            $object = $this->fetchObject($path);
461
+            if ($object->mergeMetadata($metadata)) {
462
+                // invalidate target object to force repopulation on fetch
463
+                $this->objectCache->remove($path);
464
+            }
465
+            return true;
466
+        } else {
467
+            $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
468
+            $this->getContainer()->createObject([
469
+                'name' => $path,
470
+                'content' => '',
471
+                'headers' => ['content-type' => 'httpd/unix-directory']
472
+            ]);
473
+            // invalidate target object to force repopulation on fetch
474
+            $this->objectCache->remove($path);
475
+            return true;
476
+        }
477
+    }
478
+
479
+    public function copy($path1, $path2) {
480
+        $path1 = $this->normalizePath($path1);
481
+        $path2 = $this->normalizePath($path2);
482
+
483
+        $fileType = $this->filetype($path1);
484
+        if ($fileType) {
485
+            // make way
486
+            $this->unlink($path2);
487
+        }
488
+
489
+        if ($fileType === 'file') {
490
+            try {
491
+                $source = $this->fetchObject($path1);
492
+                $source->copy([
493
+                    'destination' => $this->bucket . '/' . $path2
494
+                ]);
495
+                // invalidate target object to force repopulation on fetch
496
+                $this->objectCache->remove($path2);
497
+                $this->objectCache->remove($path2 . '/');
498
+            } catch (BadResponseError $e) {
499
+                \OC::$server->getLogger()->logException($e, [
500
+                    'level' => ILogger::ERROR,
501
+                    'app' => 'files_external',
502
+                ]);
503
+                return false;
504
+            }
505
+
506
+        } else if ($fileType === 'dir') {
507
+            try {
508
+                $source = $this->fetchObject($path1 . '/');
509
+                $source->copy([
510
+                    'destination' => $this->bucket . '/' . $path2 . '/'
511
+                ]);
512
+                // invalidate target object to force repopulation on fetch
513
+                $this->objectCache->remove($path2);
514
+                $this->objectCache->remove($path2 . '/');
515
+            } catch (BadResponseError $e) {
516
+                \OC::$server->getLogger()->logException($e, [
517
+                    'level' => ILogger::ERROR,
518
+                    'app' => 'files_external',
519
+                ]);
520
+                return false;
521
+            }
522
+
523
+            $dh = $this->opendir($path1);
524
+            while ($file = readdir($dh)) {
525
+                if (\OC\Files\Filesystem::isIgnoredDir($file)) {
526
+                    continue;
527
+                }
528
+
529
+                $source = $path1 . '/' . $file;
530
+                $target = $path2 . '/' . $file;
531
+                $this->copy($source, $target);
532
+            }
533
+
534
+        } else {
535
+            //file does not exist
536
+            return false;
537
+        }
538
+
539
+        return true;
540
+    }
541
+
542
+    public function rename($path1, $path2) {
543
+        $path1 = $this->normalizePath($path1);
544
+        $path2 = $this->normalizePath($path2);
545
+
546
+        $fileType = $this->filetype($path1);
547
+
548
+        if ($fileType === 'dir' || $fileType === 'file') {
549
+            // copy
550
+            if ($this->copy($path1, $path2) === false) {
551
+                return false;
552
+            }
553
+
554
+            // cleanup
555
+            if ($this->unlink($path1) === false) {
556
+                throw new \Exception('failed to remove original');
557
+                $this->unlink($path2);
558
+                return false;
559
+            }
560
+
561
+            return true;
562
+        }
563
+
564
+        return false;
565
+    }
566
+
567
+    public function getId() {
568
+        return $this->id;
569
+    }
570
+
571
+    /**
572
+     * Returns the initialized object store container.
573
+     *
574
+     * @return \OpenStack\ObjectStore\v1\Models\Container
575
+     * @throws \OCP\Files\StorageAuthException
576
+     * @throws \OCP\Files\StorageNotAvailableException
577
+     */
578
+    public function getContainer() {
579
+        if (is_null($this->container)) {
580
+            $this->container = $this->connectionFactory->getContainer();
581
+
582
+            if (!$this->file_exists('.')) {
583
+                $this->mkdir('.');
584
+            }
585
+        }
586
+        return $this->container;
587
+    }
588
+
589
+    public function writeBack($tmpFile, $path) {
590
+        $fileData = fopen($tmpFile, 'r');
591
+        $this->objectStore->writeObject($path, $fileData);
592
+        // invalidate target object to force repopulation on fetch
593
+        $this->objectCache->remove($path);
594
+        unlink($tmpFile);
595
+    }
596
+
597
+    public function hasUpdated($path, $time) {
598
+        if ($this->is_file($path)) {
599
+            return parent::hasUpdated($path, $time);
600
+        }
601
+        $path = $this->normalizePath($path);
602
+        $dh = $this->opendir($path);
603
+        $content = array();
604
+        while (($file = readdir($dh)) !== false) {
605
+            $content[] = $file;
606
+        }
607
+        if ($path === '.') {
608
+            $path = '';
609
+        }
610
+        $cachedContent = $this->getCache()->getFolderContents($path);
611
+        $cachedNames = array_map(function ($content) {
612
+            return $content['name'];
613
+        }, $cachedContent);
614
+        sort($cachedNames);
615
+        sort($content);
616
+        return $cachedNames !== $content;
617
+    }
618
+
619
+    /**
620
+     * check if curl is installed
621
+     */
622
+    public static function checkDependencies() {
623
+        return true;
624
+    }
625 625
 
626 626
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Jobs/UpdateGroups.php 1 patch
Indentation   +164 added lines, -164 removed lines patch added patch discarded remove patch
@@ -46,183 +46,183 @@
 block discarded – undo
46 46
 use OCP\ILogger;
47 47
 
48 48
 class UpdateGroups extends \OC\BackgroundJob\TimedJob {
49
-	static private $groupsFromDB;
50
-
51
-	static private $groupBE;
52
-
53
-	public function __construct(){
54
-		$this->interval = self::getRefreshInterval();
55
-	}
56
-
57
-	/**
58
-	 * @param mixed $argument
59
-	 */
60
-	public function run($argument){
61
-		self::updateGroups();
62
-	}
63
-
64
-	static public function updateGroups() {
65
-		\OCP\Util::writeLog('user_ldap', 'Run background job "updateGroups"', ILogger::DEBUG);
66
-
67
-		$knownGroups = array_keys(self::getKnownGroups());
68
-		$actualGroups = self::getGroupBE()->getGroups();
69
-
70
-		if(empty($actualGroups) && empty($knownGroups)) {
71
-			\OCP\Util::writeLog('user_ldap',
72
-				'bgJ "updateGroups" – groups do not seem to be configured properly, aborting.',
73
-				ILogger::INFO);
74
-			return;
75
-		}
76
-
77
-		self::handleKnownGroups(array_intersect($actualGroups, $knownGroups));
78
-		self::handleCreatedGroups(array_diff($actualGroups, $knownGroups));
79
-		self::handleRemovedGroups(array_diff($knownGroups, $actualGroups));
80
-
81
-		\OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – Finished.', ILogger::DEBUG);
82
-	}
83
-
84
-	/**
85
-	 * @return int
86
-	 */
87
-	static private function getRefreshInterval() {
88
-		//defaults to every hour
89
-		return \OC::$server->getConfig()->getAppValue('user_ldap', 'bgjRefreshInterval', 3600);
90
-	}
91
-
92
-	/**
93
-	 * @param string[] $groups
94
-	 */
95
-	static private function handleKnownGroups($groups) {
96
-		\OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – Dealing with known Groups.', ILogger::DEBUG);
97
-		$query = \OC_DB::prepare('
49
+    static private $groupsFromDB;
50
+
51
+    static private $groupBE;
52
+
53
+    public function __construct(){
54
+        $this->interval = self::getRefreshInterval();
55
+    }
56
+
57
+    /**
58
+     * @param mixed $argument
59
+     */
60
+    public function run($argument){
61
+        self::updateGroups();
62
+    }
63
+
64
+    static public function updateGroups() {
65
+        \OCP\Util::writeLog('user_ldap', 'Run background job "updateGroups"', ILogger::DEBUG);
66
+
67
+        $knownGroups = array_keys(self::getKnownGroups());
68
+        $actualGroups = self::getGroupBE()->getGroups();
69
+
70
+        if(empty($actualGroups) && empty($knownGroups)) {
71
+            \OCP\Util::writeLog('user_ldap',
72
+                'bgJ "updateGroups" – groups do not seem to be configured properly, aborting.',
73
+                ILogger::INFO);
74
+            return;
75
+        }
76
+
77
+        self::handleKnownGroups(array_intersect($actualGroups, $knownGroups));
78
+        self::handleCreatedGroups(array_diff($actualGroups, $knownGroups));
79
+        self::handleRemovedGroups(array_diff($knownGroups, $actualGroups));
80
+
81
+        \OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – Finished.', ILogger::DEBUG);
82
+    }
83
+
84
+    /**
85
+     * @return int
86
+     */
87
+    static private function getRefreshInterval() {
88
+        //defaults to every hour
89
+        return \OC::$server->getConfig()->getAppValue('user_ldap', 'bgjRefreshInterval', 3600);
90
+    }
91
+
92
+    /**
93
+     * @param string[] $groups
94
+     */
95
+    static private function handleKnownGroups($groups) {
96
+        \OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – Dealing with known Groups.', ILogger::DEBUG);
97
+        $query = \OC_DB::prepare('
98 98
 			UPDATE `*PREFIX*ldap_group_members`
99 99
 			SET `owncloudusers` = ?
100 100
 			WHERE `owncloudname` = ?
101 101
 		');
102
-		foreach($groups as $group) {
103
-			//we assume, that self::$groupsFromDB has been retrieved already
104
-			$knownUsers = unserialize(self::$groupsFromDB[$group]['owncloudusers']);
105
-			$actualUsers = self::getGroupBE()->usersInGroup($group);
106
-			$hasChanged = false;
107
-			foreach(array_diff($knownUsers, $actualUsers) as $removedUser) {
108
-				\OCP\Util::emitHook('OC_User', 'post_removeFromGroup', array('uid' => $removedUser, 'gid' => $group));
109
-				\OCP\Util::writeLog('user_ldap',
110
-				'bgJ "updateGroups" – "'.$removedUser.'" removed from "'.$group.'".',
111
-					ILogger::INFO);
112
-				$hasChanged = true;
113
-			}
114
-			foreach(array_diff($actualUsers, $knownUsers) as $addedUser) {
115
-				\OCP\Util::emitHook('OC_User', 'post_addToGroup', array('uid' => $addedUser, 'gid' => $group));
116
-				\OCP\Util::writeLog('user_ldap',
117
-				'bgJ "updateGroups" – "'.$addedUser.'" added to "'.$group.'".',
118
-					ILogger::INFO);
119
-				$hasChanged = true;
120
-			}
121
-			if($hasChanged) {
122
-				$query->execute(array(serialize($actualUsers), $group));
123
-			}
124
-		}
125
-		\OCP\Util::writeLog('user_ldap',
126
-			'bgJ "updateGroups" – FINISHED dealing with known Groups.',
127
-			ILogger::DEBUG);
128
-	}
129
-
130
-	/**
131
-	 * @param string[] $createdGroups
132
-	 */
133
-	static private function handleCreatedGroups($createdGroups) {
134
-		\OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – dealing with created Groups.', ILogger::DEBUG);
135
-		$query = \OC_DB::prepare('
102
+        foreach($groups as $group) {
103
+            //we assume, that self::$groupsFromDB has been retrieved already
104
+            $knownUsers = unserialize(self::$groupsFromDB[$group]['owncloudusers']);
105
+            $actualUsers = self::getGroupBE()->usersInGroup($group);
106
+            $hasChanged = false;
107
+            foreach(array_diff($knownUsers, $actualUsers) as $removedUser) {
108
+                \OCP\Util::emitHook('OC_User', 'post_removeFromGroup', array('uid' => $removedUser, 'gid' => $group));
109
+                \OCP\Util::writeLog('user_ldap',
110
+                'bgJ "updateGroups" – "'.$removedUser.'" removed from "'.$group.'".',
111
+                    ILogger::INFO);
112
+                $hasChanged = true;
113
+            }
114
+            foreach(array_diff($actualUsers, $knownUsers) as $addedUser) {
115
+                \OCP\Util::emitHook('OC_User', 'post_addToGroup', array('uid' => $addedUser, 'gid' => $group));
116
+                \OCP\Util::writeLog('user_ldap',
117
+                'bgJ "updateGroups" – "'.$addedUser.'" added to "'.$group.'".',
118
+                    ILogger::INFO);
119
+                $hasChanged = true;
120
+            }
121
+            if($hasChanged) {
122
+                $query->execute(array(serialize($actualUsers), $group));
123
+            }
124
+        }
125
+        \OCP\Util::writeLog('user_ldap',
126
+            'bgJ "updateGroups" – FINISHED dealing with known Groups.',
127
+            ILogger::DEBUG);
128
+    }
129
+
130
+    /**
131
+     * @param string[] $createdGroups
132
+     */
133
+    static private function handleCreatedGroups($createdGroups) {
134
+        \OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – dealing with created Groups.', ILogger::DEBUG);
135
+        $query = \OC_DB::prepare('
136 136
 			INSERT
137 137
 			INTO `*PREFIX*ldap_group_members` (`owncloudname`, `owncloudusers`)
138 138
 			VALUES (?, ?)
139 139
 		');
140
-		foreach($createdGroups as $createdGroup) {
141
-			\OCP\Util::writeLog('user_ldap',
142
-				'bgJ "updateGroups" – new group "'.$createdGroup.'" found.',
143
-				ILogger::INFO);
144
-			$users = serialize(self::getGroupBE()->usersInGroup($createdGroup));
145
-			$query->execute(array($createdGroup, $users));
146
-		}
147
-		\OCP\Util::writeLog('user_ldap',
148
-			'bgJ "updateGroups" – FINISHED dealing with created Groups.',
149
-			ILogger::DEBUG);
150
-	}
151
-
152
-	/**
153
-	 * @param string[] $removedGroups
154
-	 */
155
-	static private function handleRemovedGroups($removedGroups) {
156
-		\OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – dealing with removed groups.', ILogger::DEBUG);
157
-		$query = \OC_DB::prepare('
140
+        foreach($createdGroups as $createdGroup) {
141
+            \OCP\Util::writeLog('user_ldap',
142
+                'bgJ "updateGroups" – new group "'.$createdGroup.'" found.',
143
+                ILogger::INFO);
144
+            $users = serialize(self::getGroupBE()->usersInGroup($createdGroup));
145
+            $query->execute(array($createdGroup, $users));
146
+        }
147
+        \OCP\Util::writeLog('user_ldap',
148
+            'bgJ "updateGroups" – FINISHED dealing with created Groups.',
149
+            ILogger::DEBUG);
150
+    }
151
+
152
+    /**
153
+     * @param string[] $removedGroups
154
+     */
155
+    static private function handleRemovedGroups($removedGroups) {
156
+        \OCP\Util::writeLog('user_ldap', 'bgJ "updateGroups" – dealing with removed groups.', ILogger::DEBUG);
157
+        $query = \OC_DB::prepare('
158 158
 			DELETE
159 159
 			FROM `*PREFIX*ldap_group_members`
160 160
 			WHERE `owncloudname` = ?
161 161
 		');
162
-		foreach($removedGroups as $removedGroup) {
163
-			\OCP\Util::writeLog('user_ldap',
164
-				'bgJ "updateGroups" – group "'.$removedGroup.'" was removed.',
165
-				ILogger::INFO);
166
-			$query->execute(array($removedGroup));
167
-		}
168
-		\OCP\Util::writeLog('user_ldap',
169
-			'bgJ "updateGroups" – FINISHED dealing with removed groups.',
170
-			ILogger::DEBUG);
171
-	}
172
-
173
-	/**
174
-	 * @return \OCA\User_LDAP\Group_LDAP|\OCA\User_LDAP\Group_Proxy
175
-	 */
176
-	static private function getGroupBE() {
177
-		if(!is_null(self::$groupBE)) {
178
-			return self::$groupBE;
179
-		}
180
-		$helper = new Helper(\OC::$server->getConfig());
181
-		$configPrefixes = $helper->getServerConfigurationPrefixes(true);
182
-		$ldapWrapper = new LDAP();
183
-		if(count($configPrefixes) === 1) {
184
-			//avoid the proxy when there is only one LDAP server configured
185
-			$dbc = \OC::$server->getDatabaseConnection();
186
-			$userManager = new Manager(
187
-				\OC::$server->getConfig(),
188
-				new FilesystemHelper(),
189
-				new LogWrapper(),
190
-				\OC::$server->getAvatarManager(),
191
-				new \OCP\Image(),
192
-				$dbc,
193
-				\OC::$server->getUserManager(),
194
-				\OC::$server->getNotificationManager());
195
-			$connector = new Connection($ldapWrapper, $configPrefixes[0]);
196
-			$ldapAccess = new Access($connector, $ldapWrapper, $userManager, $helper, \OC::$server->getConfig(), \OC::$server->getUserManager());
197
-			$groupMapper = new GroupMapping($dbc);
198
-			$userMapper  = new UserMapping($dbc);
199
-			$ldapAccess->setGroupMapper($groupMapper);
200
-			$ldapAccess->setUserMapper($userMapper);
201
-			self::$groupBE = new \OCA\User_LDAP\Group_LDAP($ldapAccess, \OC::$server->query('LDAPGroupPluginManager'));
202
-		} else {
203
-			self::$groupBE = new \OCA\User_LDAP\Group_Proxy($configPrefixes, $ldapWrapper, \OC::$server->query('LDAPGroupPluginManager'));
204
-		}
205
-
206
-		return self::$groupBE;
207
-	}
208
-
209
-	/**
210
-	 * @return array
211
-	 */
212
-	static private function getKnownGroups() {
213
-		if(is_array(self::$groupsFromDB)) {
214
-			return self::$groupsFromDB;
215
-		}
216
-		$query = \OC_DB::prepare('
162
+        foreach($removedGroups as $removedGroup) {
163
+            \OCP\Util::writeLog('user_ldap',
164
+                'bgJ "updateGroups" – group "'.$removedGroup.'" was removed.',
165
+                ILogger::INFO);
166
+            $query->execute(array($removedGroup));
167
+        }
168
+        \OCP\Util::writeLog('user_ldap',
169
+            'bgJ "updateGroups" – FINISHED dealing with removed groups.',
170
+            ILogger::DEBUG);
171
+    }
172
+
173
+    /**
174
+     * @return \OCA\User_LDAP\Group_LDAP|\OCA\User_LDAP\Group_Proxy
175
+     */
176
+    static private function getGroupBE() {
177
+        if(!is_null(self::$groupBE)) {
178
+            return self::$groupBE;
179
+        }
180
+        $helper = new Helper(\OC::$server->getConfig());
181
+        $configPrefixes = $helper->getServerConfigurationPrefixes(true);
182
+        $ldapWrapper = new LDAP();
183
+        if(count($configPrefixes) === 1) {
184
+            //avoid the proxy when there is only one LDAP server configured
185
+            $dbc = \OC::$server->getDatabaseConnection();
186
+            $userManager = new Manager(
187
+                \OC::$server->getConfig(),
188
+                new FilesystemHelper(),
189
+                new LogWrapper(),
190
+                \OC::$server->getAvatarManager(),
191
+                new \OCP\Image(),
192
+                $dbc,
193
+                \OC::$server->getUserManager(),
194
+                \OC::$server->getNotificationManager());
195
+            $connector = new Connection($ldapWrapper, $configPrefixes[0]);
196
+            $ldapAccess = new Access($connector, $ldapWrapper, $userManager, $helper, \OC::$server->getConfig(), \OC::$server->getUserManager());
197
+            $groupMapper = new GroupMapping($dbc);
198
+            $userMapper  = new UserMapping($dbc);
199
+            $ldapAccess->setGroupMapper($groupMapper);
200
+            $ldapAccess->setUserMapper($userMapper);
201
+            self::$groupBE = new \OCA\User_LDAP\Group_LDAP($ldapAccess, \OC::$server->query('LDAPGroupPluginManager'));
202
+        } else {
203
+            self::$groupBE = new \OCA\User_LDAP\Group_Proxy($configPrefixes, $ldapWrapper, \OC::$server->query('LDAPGroupPluginManager'));
204
+        }
205
+
206
+        return self::$groupBE;
207
+    }
208
+
209
+    /**
210
+     * @return array
211
+     */
212
+    static private function getKnownGroups() {
213
+        if(is_array(self::$groupsFromDB)) {
214
+            return self::$groupsFromDB;
215
+        }
216
+        $query = \OC_DB::prepare('
217 217
 			SELECT `owncloudname`, `owncloudusers`
218 218
 			FROM `*PREFIX*ldap_group_members`
219 219
 		');
220
-		$result = $query->execute()->fetchAll();
221
-		self::$groupsFromDB = array();
222
-		foreach($result as $dataset) {
223
-			self::$groupsFromDB[$dataset['owncloudname']] = $dataset;
224
-		}
225
-
226
-		return self::$groupsFromDB;
227
-	}
220
+        $result = $query->execute()->fetchAll();
221
+        self::$groupsFromDB = array();
222
+        foreach($result as $dataset) {
223
+            self::$groupsFromDB[$dataset['owncloudname']] = $dataset;
224
+        }
225
+
226
+        return self::$groupsFromDB;
227
+    }
228 228
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Connection.php 1 patch
Indentation   +615 added lines, -615 removed lines patch added patch discarded remove patch
@@ -59,620 +59,620 @@
 block discarded – undo
59 59
  * @property string ldapExpertUUIDGroupAttr
60 60
  */
61 61
 class Connection extends LDAPUtility {
62
-	private $ldapConnectionRes = null;
63
-	private $configPrefix;
64
-	private $configID;
65
-	private $configured = false;
66
-	private $hasPagedResultSupport = true;
67
-	//whether connection should be kept on __destruct
68
-	private $dontDestruct = false;
69
-
70
-	/**
71
-	 * @var bool runtime flag that indicates whether supported primary groups are available
72
-	 */
73
-	public $hasPrimaryGroups = true;
74
-
75
-	/**
76
-	 * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
77
-	 */
78
-	public $hasGidNumber = true;
79
-
80
-	//cache handler
81
-	protected $cache;
82
-
83
-	/** @var Configuration settings handler **/
84
-	protected $configuration;
85
-
86
-	protected $doNotValidate = false;
87
-
88
-	protected $ignoreValidation = false;
89
-
90
-	protected $bindResult = [];
91
-
92
-	/**
93
-	 * Constructor
94
-	 * @param ILDAPWrapper $ldap
95
-	 * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
96
-	 * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
97
-	 */
98
-	public function __construct(ILDAPWrapper $ldap, $configPrefix = '', $configID = 'user_ldap') {
99
-		parent::__construct($ldap);
100
-		$this->configPrefix = $configPrefix;
101
-		$this->configID = $configID;
102
-		$this->configuration = new Configuration($configPrefix,
103
-												 !is_null($configID));
104
-		$memcache = \OC::$server->getMemCacheFactory();
105
-		if($memcache->isAvailable()) {
106
-			$this->cache = $memcache->createDistributed();
107
-		}
108
-		$helper = new Helper(\OC::$server->getConfig());
109
-		$this->doNotValidate = !in_array($this->configPrefix,
110
-			$helper->getServerConfigurationPrefixes());
111
-		$this->hasPagedResultSupport =
112
-			(int)$this->configuration->ldapPagingSize !== 0
113
-			|| $this->ldap->hasPagedResultSupport();
114
-	}
115
-
116
-	public function __destruct() {
117
-		if(!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
118
-			@$this->ldap->unbind($this->ldapConnectionRes);
119
-			$this->bindResult = [];
120
-		}
121
-	}
122
-
123
-	/**
124
-	 * defines behaviour when the instance is cloned
125
-	 */
126
-	public function __clone() {
127
-		$this->configuration = new Configuration($this->configPrefix,
128
-												 !is_null($this->configID));
129
-		$this->ldapConnectionRes = null;
130
-		$this->dontDestruct = true;
131
-	}
132
-
133
-	/**
134
-	 * @param string $name
135
-	 * @return bool|mixed
136
-	 */
137
-	public function __get($name) {
138
-		if(!$this->configured) {
139
-			$this->readConfiguration();
140
-		}
141
-
142
-		if($name === 'hasPagedResultSupport') {
143
-			return $this->hasPagedResultSupport;
144
-		}
145
-
146
-		return $this->configuration->$name;
147
-	}
148
-
149
-	/**
150
-	 * @param string $name
151
-	 * @param mixed $value
152
-	 */
153
-	public function __set($name, $value) {
154
-		$this->doNotValidate = false;
155
-		$before = $this->configuration->$name;
156
-		$this->configuration->$name = $value;
157
-		$after = $this->configuration->$name;
158
-		if($before !== $after) {
159
-			if ($this->configID !== '' && $this->configID !== null) {
160
-				$this->configuration->saveConfiguration();
161
-			}
162
-			$this->validateConfiguration();
163
-		}
164
-	}
165
-
166
-	/**
167
-	 * sets whether the result of the configuration validation shall
168
-	 * be ignored when establishing the connection. Used by the Wizard
169
-	 * in early configuration state.
170
-	 * @param bool $state
171
-	 */
172
-	public function setIgnoreValidation($state) {
173
-		$this->ignoreValidation = (bool)$state;
174
-	}
175
-
176
-	/**
177
-	 * initializes the LDAP backend
178
-	 * @param bool $force read the config settings no matter what
179
-	 */
180
-	public function init($force = false) {
181
-		$this->readConfiguration($force);
182
-		$this->establishConnection();
183
-	}
184
-
185
-	/**
186
-	 * Returns the LDAP handler
187
-	 */
188
-	public function getConnectionResource() {
189
-		if(!$this->ldapConnectionRes) {
190
-			$this->init();
191
-		} else if(!$this->ldap->isResource($this->ldapConnectionRes)) {
192
-			$this->ldapConnectionRes = null;
193
-			$this->establishConnection();
194
-		}
195
-		if(is_null($this->ldapConnectionRes)) {
196
-			\OCP\Util::writeLog('user_ldap', 'No LDAP Connection to server ' . $this->configuration->ldapHost, ILogger::ERROR);
197
-			throw new ServerNotAvailableException('Connection to LDAP server could not be established');
198
-		}
199
-		return $this->ldapConnectionRes;
200
-	}
201
-
202
-	/**
203
-	 * resets the connection resource
204
-	 */
205
-	public function resetConnectionResource() {
206
-		if(!is_null($this->ldapConnectionRes)) {
207
-			@$this->ldap->unbind($this->ldapConnectionRes);
208
-			$this->ldapConnectionRes = null;
209
-			$this->bindResult = [];
210
-		}
211
-	}
212
-
213
-	/**
214
-	 * @param string|null $key
215
-	 * @return string
216
-	 */
217
-	private function getCacheKey($key) {
218
-		$prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
219
-		if(is_null($key)) {
220
-			return $prefix;
221
-		}
222
-		return $prefix.hash('sha256', $key);
223
-	}
224
-
225
-	/**
226
-	 * @param string $key
227
-	 * @return mixed|null
228
-	 */
229
-	public function getFromCache($key) {
230
-		if(!$this->configured) {
231
-			$this->readConfiguration();
232
-		}
233
-		if(is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
234
-			return null;
235
-		}
236
-		$key = $this->getCacheKey($key);
237
-
238
-		return json_decode(base64_decode($this->cache->get($key)), true);
239
-	}
240
-
241
-	/**
242
-	 * @param string $key
243
-	 * @param mixed $value
244
-	 *
245
-	 * @return string
246
-	 */
247
-	public function writeToCache($key, $value) {
248
-		if(!$this->configured) {
249
-			$this->readConfiguration();
250
-		}
251
-		if(is_null($this->cache)
252
-			|| !$this->configuration->ldapCacheTTL
253
-			|| !$this->configuration->ldapConfigurationActive) {
254
-			return null;
255
-		}
256
-		$key   = $this->getCacheKey($key);
257
-		$value = base64_encode(json_encode($value));
258
-		$this->cache->set($key, $value, $this->configuration->ldapCacheTTL);
259
-	}
260
-
261
-	public function clearCache() {
262
-		if(!is_null($this->cache)) {
263
-			$this->cache->clear($this->getCacheKey(null));
264
-		}
265
-	}
266
-
267
-	/**
268
-	 * Caches the general LDAP configuration.
269
-	 * @param bool $force optional. true, if the re-read should be forced. defaults
270
-	 * to false.
271
-	 * @return null
272
-	 */
273
-	private function readConfiguration($force = false) {
274
-		if((!$this->configured || $force) && !is_null($this->configID)) {
275
-			$this->configuration->readConfiguration();
276
-			$this->configured = $this->validateConfiguration();
277
-		}
278
-	}
279
-
280
-	/**
281
-	 * set LDAP configuration with values delivered by an array, not read from configuration
282
-	 * @param array $config array that holds the config parameters in an associated array
283
-	 * @param array &$setParameters optional; array where the set fields will be given to
284
-	 * @return boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
285
-	 */
286
-	public function setConfiguration($config, &$setParameters = null) {
287
-		if(is_null($setParameters)) {
288
-			$setParameters = array();
289
-		}
290
-		$this->doNotValidate = false;
291
-		$this->configuration->setConfiguration($config, $setParameters);
292
-		if(count($setParameters) > 0) {
293
-			$this->configured = $this->validateConfiguration();
294
-		}
295
-
296
-
297
-		return $this->configured;
298
-	}
299
-
300
-	/**
301
-	 * saves the current Configuration in the database and empties the
302
-	 * cache
303
-	 * @return null
304
-	 */
305
-	public function saveConfiguration() {
306
-		$this->configuration->saveConfiguration();
307
-		$this->clearCache();
308
-	}
309
-
310
-	/**
311
-	 * get the current LDAP configuration
312
-	 * @return array
313
-	 */
314
-	public function getConfiguration() {
315
-		$this->readConfiguration();
316
-		$config = $this->configuration->getConfiguration();
317
-		$cta = $this->configuration->getConfigTranslationArray();
318
-		$result = array();
319
-		foreach($cta as $dbkey => $configkey) {
320
-			switch($configkey) {
321
-				case 'homeFolderNamingRule':
322
-					if(strpos($config[$configkey], 'attr:') === 0) {
323
-						$result[$dbkey] = substr($config[$configkey], 5);
324
-					} else {
325
-						$result[$dbkey] = '';
326
-					}
327
-					break;
328
-				case 'ldapBase':
329
-				case 'ldapBaseUsers':
330
-				case 'ldapBaseGroups':
331
-				case 'ldapAttributesForUserSearch':
332
-				case 'ldapAttributesForGroupSearch':
333
-					if(is_array($config[$configkey])) {
334
-						$result[$dbkey] = implode("\n", $config[$configkey]);
335
-						break;
336
-					} //else follows default
337
-				default:
338
-					$result[$dbkey] = $config[$configkey];
339
-			}
340
-		}
341
-		return $result;
342
-	}
343
-
344
-	private function doSoftValidation() {
345
-		//if User or Group Base are not set, take over Base DN setting
346
-		foreach(array('ldapBaseUsers', 'ldapBaseGroups') as $keyBase) {
347
-			$val = $this->configuration->$keyBase;
348
-			if(empty($val)) {
349
-				$this->configuration->$keyBase = $this->configuration->ldapBase;
350
-			}
351
-		}
352
-
353
-		foreach(array('ldapExpertUUIDUserAttr'  => 'ldapUuidUserAttribute',
354
-					  'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute')
355
-				as $expertSetting => $effectiveSetting) {
356
-			$uuidOverride = $this->configuration->$expertSetting;
357
-			if(!empty($uuidOverride)) {
358
-				$this->configuration->$effectiveSetting = $uuidOverride;
359
-			} else {
360
-				$uuidAttributes = Access::UUID_ATTRIBUTES;
361
-				array_unshift($uuidAttributes, 'auto');
362
-				if(!in_array($this->configuration->$effectiveSetting,
363
-							$uuidAttributes)
364
-					&& (!is_null($this->configID))) {
365
-					$this->configuration->$effectiveSetting = 'auto';
366
-					$this->configuration->saveConfiguration();
367
-					\OCP\Util::writeLog('user_ldap',
368
-										'Illegal value for the '.
369
-										$effectiveSetting.', '.'reset to '.
370
-										'autodetect.', ILogger::INFO);
371
-				}
372
-
373
-			}
374
-		}
375
-
376
-		$backupPort = (int)$this->configuration->ldapBackupPort;
377
-		if ($backupPort <= 0) {
378
-			$this->configuration->backupPort = $this->configuration->ldapPort;
379
-		}
380
-
381
-		//make sure empty search attributes are saved as simple, empty array
382
-		$saKeys = array('ldapAttributesForUserSearch',
383
-						'ldapAttributesForGroupSearch');
384
-		foreach($saKeys as $key) {
385
-			$val = $this->configuration->$key;
386
-			if(is_array($val) && count($val) === 1 && empty($val[0])) {
387
-				$this->configuration->$key = array();
388
-			}
389
-		}
390
-
391
-		if((stripos($this->configuration->ldapHost, 'ldaps://') === 0)
392
-			&& $this->configuration->ldapTLS) {
393
-			$this->configuration->ldapTLS = false;
394
-			\OCP\Util::writeLog(
395
-				'user_ldap',
396
-				'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
397
-				ILogger::INFO
398
-			);
399
-		}
400
-	}
401
-
402
-	/**
403
-	 * @return bool
404
-	 */
405
-	private function doCriticalValidation() {
406
-		$configurationOK = true;
407
-		$errorStr = 'Configuration Error (prefix '.
408
-			(string)$this->configPrefix .'): ';
409
-
410
-		//options that shall not be empty
411
-		$options = array('ldapHost', 'ldapPort', 'ldapUserDisplayName',
412
-						 'ldapGroupDisplayName', 'ldapLoginFilter');
413
-		foreach($options as $key) {
414
-			$val = $this->configuration->$key;
415
-			if(empty($val)) {
416
-				switch($key) {
417
-					case 'ldapHost':
418
-						$subj = 'LDAP Host';
419
-						break;
420
-					case 'ldapPort':
421
-						$subj = 'LDAP Port';
422
-						break;
423
-					case 'ldapUserDisplayName':
424
-						$subj = 'LDAP User Display Name';
425
-						break;
426
-					case 'ldapGroupDisplayName':
427
-						$subj = 'LDAP Group Display Name';
428
-						break;
429
-					case 'ldapLoginFilter':
430
-						$subj = 'LDAP Login Filter';
431
-						break;
432
-					default:
433
-						$subj = $key;
434
-						break;
435
-				}
436
-				$configurationOK = false;
437
-				\OCP\Util::writeLog(
438
-					'user_ldap',
439
-					$errorStr.'No '.$subj.' given!',
440
-					ILogger::WARN
441
-				);
442
-			}
443
-		}
444
-
445
-		//combinations
446
-		$agent = $this->configuration->ldapAgentName;
447
-		$pwd = $this->configuration->ldapAgentPassword;
448
-		if (
449
-			($agent === ''  && $pwd !== '')
450
-			|| ($agent !== '' && $pwd === '')
451
-		) {
452
-			\OCP\Util::writeLog(
453
-				'user_ldap',
454
-				$errorStr.'either no password is given for the user ' .
455
-					'agent or a password is given, but not an LDAP agent.',
456
-				ILogger::WARN);
457
-			$configurationOK = false;
458
-		}
459
-
460
-		$base = $this->configuration->ldapBase;
461
-		$baseUsers = $this->configuration->ldapBaseUsers;
462
-		$baseGroups = $this->configuration->ldapBaseGroups;
463
-
464
-		if(empty($base) && empty($baseUsers) && empty($baseGroups)) {
465
-			\OCP\Util::writeLog(
466
-				'user_ldap',
467
-				$errorStr.'Not a single Base DN given.',
468
-				ILogger::WARN
469
-			);
470
-			$configurationOK = false;
471
-		}
472
-
473
-		if(mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
474
-		   === false) {
475
-			\OCP\Util::writeLog(
476
-				'user_ldap',
477
-				$errorStr.'login filter does not contain %uid place holder.',
478
-				ILogger::WARN
479
-			);
480
-			$configurationOK = false;
481
-		}
482
-
483
-		return $configurationOK;
484
-	}
485
-
486
-	/**
487
-	 * Validates the user specified configuration
488
-	 * @return bool true if configuration seems OK, false otherwise
489
-	 */
490
-	private function validateConfiguration() {
491
-
492
-		if($this->doNotValidate) {
493
-			//don't do a validation if it is a new configuration with pure
494
-			//default values. Will be allowed on changes via __set or
495
-			//setConfiguration
496
-			return false;
497
-		}
498
-
499
-		// first step: "soft" checks: settings that are not really
500
-		// necessary, but advisable. If left empty, give an info message
501
-		$this->doSoftValidation();
502
-
503
-		//second step: critical checks. If left empty or filled wrong, mark as
504
-		//not configured and give a warning.
505
-		return $this->doCriticalValidation();
506
-	}
507
-
508
-
509
-	/**
510
-	 * Connects and Binds to LDAP
511
-	 */
512
-	private function establishConnection() {
513
-		if(!$this->configuration->ldapConfigurationActive) {
514
-			return null;
515
-		}
516
-		static $phpLDAPinstalled = true;
517
-		if(!$phpLDAPinstalled) {
518
-			return false;
519
-		}
520
-		if(!$this->ignoreValidation && !$this->configured) {
521
-			\OCP\Util::writeLog(
522
-				'user_ldap',
523
-				'Configuration is invalid, cannot connect',
524
-				ILogger::WARN
525
-			);
526
-			return false;
527
-		}
528
-		if(!$this->ldapConnectionRes) {
529
-			if(!$this->ldap->areLDAPFunctionsAvailable()) {
530
-				$phpLDAPinstalled = false;
531
-				\OCP\Util::writeLog(
532
-					'user_ldap',
533
-					'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
534
-					ILogger::ERROR
535
-				);
536
-
537
-				return false;
538
-			}
539
-			if($this->configuration->turnOffCertCheck) {
540
-				if(putenv('LDAPTLS_REQCERT=never')) {
541
-					\OCP\Util::writeLog('user_ldap',
542
-						'Turned off SSL certificate validation successfully.',
543
-						ILogger::DEBUG);
544
-				} else {
545
-					\OCP\Util::writeLog(
546
-						'user_ldap',
547
-						'Could not turn off SSL certificate validation.',
548
-						ILogger::WARN
549
-					);
550
-				}
551
-			}
552
-
553
-			$isOverrideMainServer = ($this->configuration->ldapOverrideMainServer
554
-				|| $this->getFromCache('overrideMainServer'));
555
-			$isBackupHost = (trim($this->configuration->ldapBackupHost) !== "");
556
-			$bindStatus = false;
557
-			$error = -1;
558
-			try {
559
-				if (!$isOverrideMainServer) {
560
-					$this->doConnect($this->configuration->ldapHost,
561
-						$this->configuration->ldapPort);
562
-					$bindStatus = $this->bind();
563
-					$error = $this->ldap->isResource($this->ldapConnectionRes) ?
564
-						$this->ldap->errno($this->ldapConnectionRes) : -1;
565
-				}
566
-				if($bindStatus === true) {
567
-					return $bindStatus;
568
-				}
569
-			} catch (ServerNotAvailableException $e) {
570
-				if(!$isBackupHost) {
571
-					throw $e;
572
-				}
573
-			}
574
-
575
-			//if LDAP server is not reachable, try the Backup (Replica!) Server
576
-			if($isBackupHost && ($error !== 0 || $isOverrideMainServer)) {
577
-				$this->doConnect($this->configuration->ldapBackupHost,
578
-								 $this->configuration->ldapBackupPort);
579
-				$this->bindResult = [];
580
-				$bindStatus = $this->bind();
581
-				$error = $this->ldap->isResource($this->ldapConnectionRes) ?
582
-					$this->ldap->errno($this->ldapConnectionRes) : -1;
583
-				if($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) {
584
-					//when bind to backup server succeeded and failed to main server,
585
-					//skip contacting him until next cache refresh
586
-					$this->writeToCache('overrideMainServer', true);
587
-				}
588
-			}
589
-
590
-			return $bindStatus;
591
-		}
592
-		return null;
593
-	}
594
-
595
-	/**
596
-	 * @param string $host
597
-	 * @param string $port
598
-	 * @return bool
599
-	 * @throws \OC\ServerNotAvailableException
600
-	 */
601
-	private function doConnect($host, $port) {
602
-		if ($host === '') {
603
-			return false;
604
-		}
605
-
606
-		$this->ldapConnectionRes = $this->ldap->connect($host, $port);
607
-
608
-		if(!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
609
-			throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
610
-		}
611
-
612
-		if(!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
613
-			throw new ServerNotAvailableException('Could not disable LDAP referrals.');
614
-		}
615
-
616
-		if($this->configuration->ldapTLS) {
617
-			if(!$this->ldap->startTls($this->ldapConnectionRes)) {
618
-				throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
619
-			}
620
-		}
621
-
622
-		return true;
623
-	}
624
-
625
-	/**
626
-	 * Binds to LDAP
627
-	 */
628
-	public function bind() {
629
-		if(!$this->configuration->ldapConfigurationActive) {
630
-			return false;
631
-		}
632
-		$cr = $this->ldapConnectionRes;
633
-		if(!$this->ldap->isResource($cr)) {
634
-			$cr = $this->getConnectionResource();
635
-		}
636
-
637
-		if(
638
-			count($this->bindResult) !== 0
639
-			&& $this->bindResult['dn'] === $this->configuration->ldapAgentName
640
-			&& \OC::$server->getHasher()->verify(
641
-				$this->configPrefix . $this->configuration->ldapAgentPassword,
642
-				$this->bindResult['hash']
643
-			)
644
-		) {
645
-			// don't attempt to bind again with the same data as before
646
-			// bind might have been invoked via getConnectionResource(),
647
-			// but we need results specifically for e.g. user login
648
-			return $this->bindResult['result'];
649
-		}
650
-
651
-		$ldapLogin = @$this->ldap->bind($cr,
652
-										$this->configuration->ldapAgentName,
653
-										$this->configuration->ldapAgentPassword);
654
-
655
-		$this->bindResult = [
656
-			'dn' => $this->configuration->ldapAgentName,
657
-			'hash' => \OC::$server->getHasher()->hash($this->configPrefix . $this->configuration->ldapAgentPassword),
658
-			'result' => $ldapLogin,
659
-		];
660
-
661
-		if(!$ldapLogin) {
662
-			$errno = $this->ldap->errno($cr);
663
-
664
-			\OCP\Util::writeLog('user_ldap',
665
-				'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
666
-				ILogger::WARN);
667
-
668
-			// Set to failure mode, if LDAP error code is not LDAP_SUCCESS or LDAP_INVALID_CREDENTIALS
669
-			if($errno !== 0x00 && $errno !== 0x31) {
670
-				$this->ldapConnectionRes = null;
671
-			}
672
-
673
-			return false;
674
-		}
675
-		return true;
676
-	}
62
+    private $ldapConnectionRes = null;
63
+    private $configPrefix;
64
+    private $configID;
65
+    private $configured = false;
66
+    private $hasPagedResultSupport = true;
67
+    //whether connection should be kept on __destruct
68
+    private $dontDestruct = false;
69
+
70
+    /**
71
+     * @var bool runtime flag that indicates whether supported primary groups are available
72
+     */
73
+    public $hasPrimaryGroups = true;
74
+
75
+    /**
76
+     * @var bool runtime flag that indicates whether supported POSIX gidNumber are available
77
+     */
78
+    public $hasGidNumber = true;
79
+
80
+    //cache handler
81
+    protected $cache;
82
+
83
+    /** @var Configuration settings handler **/
84
+    protected $configuration;
85
+
86
+    protected $doNotValidate = false;
87
+
88
+    protected $ignoreValidation = false;
89
+
90
+    protected $bindResult = [];
91
+
92
+    /**
93
+     * Constructor
94
+     * @param ILDAPWrapper $ldap
95
+     * @param string $configPrefix a string with the prefix for the configkey column (appconfig table)
96
+     * @param string|null $configID a string with the value for the appid column (appconfig table) or null for on-the-fly connections
97
+     */
98
+    public function __construct(ILDAPWrapper $ldap, $configPrefix = '', $configID = 'user_ldap') {
99
+        parent::__construct($ldap);
100
+        $this->configPrefix = $configPrefix;
101
+        $this->configID = $configID;
102
+        $this->configuration = new Configuration($configPrefix,
103
+                                                    !is_null($configID));
104
+        $memcache = \OC::$server->getMemCacheFactory();
105
+        if($memcache->isAvailable()) {
106
+            $this->cache = $memcache->createDistributed();
107
+        }
108
+        $helper = new Helper(\OC::$server->getConfig());
109
+        $this->doNotValidate = !in_array($this->configPrefix,
110
+            $helper->getServerConfigurationPrefixes());
111
+        $this->hasPagedResultSupport =
112
+            (int)$this->configuration->ldapPagingSize !== 0
113
+            || $this->ldap->hasPagedResultSupport();
114
+    }
115
+
116
+    public function __destruct() {
117
+        if(!$this->dontDestruct && $this->ldap->isResource($this->ldapConnectionRes)) {
118
+            @$this->ldap->unbind($this->ldapConnectionRes);
119
+            $this->bindResult = [];
120
+        }
121
+    }
122
+
123
+    /**
124
+     * defines behaviour when the instance is cloned
125
+     */
126
+    public function __clone() {
127
+        $this->configuration = new Configuration($this->configPrefix,
128
+                                                    !is_null($this->configID));
129
+        $this->ldapConnectionRes = null;
130
+        $this->dontDestruct = true;
131
+    }
132
+
133
+    /**
134
+     * @param string $name
135
+     * @return bool|mixed
136
+     */
137
+    public function __get($name) {
138
+        if(!$this->configured) {
139
+            $this->readConfiguration();
140
+        }
141
+
142
+        if($name === 'hasPagedResultSupport') {
143
+            return $this->hasPagedResultSupport;
144
+        }
145
+
146
+        return $this->configuration->$name;
147
+    }
148
+
149
+    /**
150
+     * @param string $name
151
+     * @param mixed $value
152
+     */
153
+    public function __set($name, $value) {
154
+        $this->doNotValidate = false;
155
+        $before = $this->configuration->$name;
156
+        $this->configuration->$name = $value;
157
+        $after = $this->configuration->$name;
158
+        if($before !== $after) {
159
+            if ($this->configID !== '' && $this->configID !== null) {
160
+                $this->configuration->saveConfiguration();
161
+            }
162
+            $this->validateConfiguration();
163
+        }
164
+    }
165
+
166
+    /**
167
+     * sets whether the result of the configuration validation shall
168
+     * be ignored when establishing the connection. Used by the Wizard
169
+     * in early configuration state.
170
+     * @param bool $state
171
+     */
172
+    public function setIgnoreValidation($state) {
173
+        $this->ignoreValidation = (bool)$state;
174
+    }
175
+
176
+    /**
177
+     * initializes the LDAP backend
178
+     * @param bool $force read the config settings no matter what
179
+     */
180
+    public function init($force = false) {
181
+        $this->readConfiguration($force);
182
+        $this->establishConnection();
183
+    }
184
+
185
+    /**
186
+     * Returns the LDAP handler
187
+     */
188
+    public function getConnectionResource() {
189
+        if(!$this->ldapConnectionRes) {
190
+            $this->init();
191
+        } else if(!$this->ldap->isResource($this->ldapConnectionRes)) {
192
+            $this->ldapConnectionRes = null;
193
+            $this->establishConnection();
194
+        }
195
+        if(is_null($this->ldapConnectionRes)) {
196
+            \OCP\Util::writeLog('user_ldap', 'No LDAP Connection to server ' . $this->configuration->ldapHost, ILogger::ERROR);
197
+            throw new ServerNotAvailableException('Connection to LDAP server could not be established');
198
+        }
199
+        return $this->ldapConnectionRes;
200
+    }
201
+
202
+    /**
203
+     * resets the connection resource
204
+     */
205
+    public function resetConnectionResource() {
206
+        if(!is_null($this->ldapConnectionRes)) {
207
+            @$this->ldap->unbind($this->ldapConnectionRes);
208
+            $this->ldapConnectionRes = null;
209
+            $this->bindResult = [];
210
+        }
211
+    }
212
+
213
+    /**
214
+     * @param string|null $key
215
+     * @return string
216
+     */
217
+    private function getCacheKey($key) {
218
+        $prefix = 'LDAP-'.$this->configID.'-'.$this->configPrefix.'-';
219
+        if(is_null($key)) {
220
+            return $prefix;
221
+        }
222
+        return $prefix.hash('sha256', $key);
223
+    }
224
+
225
+    /**
226
+     * @param string $key
227
+     * @return mixed|null
228
+     */
229
+    public function getFromCache($key) {
230
+        if(!$this->configured) {
231
+            $this->readConfiguration();
232
+        }
233
+        if(is_null($this->cache) || !$this->configuration->ldapCacheTTL) {
234
+            return null;
235
+        }
236
+        $key = $this->getCacheKey($key);
237
+
238
+        return json_decode(base64_decode($this->cache->get($key)), true);
239
+    }
240
+
241
+    /**
242
+     * @param string $key
243
+     * @param mixed $value
244
+     *
245
+     * @return string
246
+     */
247
+    public function writeToCache($key, $value) {
248
+        if(!$this->configured) {
249
+            $this->readConfiguration();
250
+        }
251
+        if(is_null($this->cache)
252
+            || !$this->configuration->ldapCacheTTL
253
+            || !$this->configuration->ldapConfigurationActive) {
254
+            return null;
255
+        }
256
+        $key   = $this->getCacheKey($key);
257
+        $value = base64_encode(json_encode($value));
258
+        $this->cache->set($key, $value, $this->configuration->ldapCacheTTL);
259
+    }
260
+
261
+    public function clearCache() {
262
+        if(!is_null($this->cache)) {
263
+            $this->cache->clear($this->getCacheKey(null));
264
+        }
265
+    }
266
+
267
+    /**
268
+     * Caches the general LDAP configuration.
269
+     * @param bool $force optional. true, if the re-read should be forced. defaults
270
+     * to false.
271
+     * @return null
272
+     */
273
+    private function readConfiguration($force = false) {
274
+        if((!$this->configured || $force) && !is_null($this->configID)) {
275
+            $this->configuration->readConfiguration();
276
+            $this->configured = $this->validateConfiguration();
277
+        }
278
+    }
279
+
280
+    /**
281
+     * set LDAP configuration with values delivered by an array, not read from configuration
282
+     * @param array $config array that holds the config parameters in an associated array
283
+     * @param array &$setParameters optional; array where the set fields will be given to
284
+     * @return boolean true if config validates, false otherwise. Check with $setParameters for detailed success on single parameters
285
+     */
286
+    public function setConfiguration($config, &$setParameters = null) {
287
+        if(is_null($setParameters)) {
288
+            $setParameters = array();
289
+        }
290
+        $this->doNotValidate = false;
291
+        $this->configuration->setConfiguration($config, $setParameters);
292
+        if(count($setParameters) > 0) {
293
+            $this->configured = $this->validateConfiguration();
294
+        }
295
+
296
+
297
+        return $this->configured;
298
+    }
299
+
300
+    /**
301
+     * saves the current Configuration in the database and empties the
302
+     * cache
303
+     * @return null
304
+     */
305
+    public function saveConfiguration() {
306
+        $this->configuration->saveConfiguration();
307
+        $this->clearCache();
308
+    }
309
+
310
+    /**
311
+     * get the current LDAP configuration
312
+     * @return array
313
+     */
314
+    public function getConfiguration() {
315
+        $this->readConfiguration();
316
+        $config = $this->configuration->getConfiguration();
317
+        $cta = $this->configuration->getConfigTranslationArray();
318
+        $result = array();
319
+        foreach($cta as $dbkey => $configkey) {
320
+            switch($configkey) {
321
+                case 'homeFolderNamingRule':
322
+                    if(strpos($config[$configkey], 'attr:') === 0) {
323
+                        $result[$dbkey] = substr($config[$configkey], 5);
324
+                    } else {
325
+                        $result[$dbkey] = '';
326
+                    }
327
+                    break;
328
+                case 'ldapBase':
329
+                case 'ldapBaseUsers':
330
+                case 'ldapBaseGroups':
331
+                case 'ldapAttributesForUserSearch':
332
+                case 'ldapAttributesForGroupSearch':
333
+                    if(is_array($config[$configkey])) {
334
+                        $result[$dbkey] = implode("\n", $config[$configkey]);
335
+                        break;
336
+                    } //else follows default
337
+                default:
338
+                    $result[$dbkey] = $config[$configkey];
339
+            }
340
+        }
341
+        return $result;
342
+    }
343
+
344
+    private function doSoftValidation() {
345
+        //if User or Group Base are not set, take over Base DN setting
346
+        foreach(array('ldapBaseUsers', 'ldapBaseGroups') as $keyBase) {
347
+            $val = $this->configuration->$keyBase;
348
+            if(empty($val)) {
349
+                $this->configuration->$keyBase = $this->configuration->ldapBase;
350
+            }
351
+        }
352
+
353
+        foreach(array('ldapExpertUUIDUserAttr'  => 'ldapUuidUserAttribute',
354
+                        'ldapExpertUUIDGroupAttr' => 'ldapUuidGroupAttribute')
355
+                as $expertSetting => $effectiveSetting) {
356
+            $uuidOverride = $this->configuration->$expertSetting;
357
+            if(!empty($uuidOverride)) {
358
+                $this->configuration->$effectiveSetting = $uuidOverride;
359
+            } else {
360
+                $uuidAttributes = Access::UUID_ATTRIBUTES;
361
+                array_unshift($uuidAttributes, 'auto');
362
+                if(!in_array($this->configuration->$effectiveSetting,
363
+                            $uuidAttributes)
364
+                    && (!is_null($this->configID))) {
365
+                    $this->configuration->$effectiveSetting = 'auto';
366
+                    $this->configuration->saveConfiguration();
367
+                    \OCP\Util::writeLog('user_ldap',
368
+                                        'Illegal value for the '.
369
+                                        $effectiveSetting.', '.'reset to '.
370
+                                        'autodetect.', ILogger::INFO);
371
+                }
372
+
373
+            }
374
+        }
375
+
376
+        $backupPort = (int)$this->configuration->ldapBackupPort;
377
+        if ($backupPort <= 0) {
378
+            $this->configuration->backupPort = $this->configuration->ldapPort;
379
+        }
380
+
381
+        //make sure empty search attributes are saved as simple, empty array
382
+        $saKeys = array('ldapAttributesForUserSearch',
383
+                        'ldapAttributesForGroupSearch');
384
+        foreach($saKeys as $key) {
385
+            $val = $this->configuration->$key;
386
+            if(is_array($val) && count($val) === 1 && empty($val[0])) {
387
+                $this->configuration->$key = array();
388
+            }
389
+        }
390
+
391
+        if((stripos($this->configuration->ldapHost, 'ldaps://') === 0)
392
+            && $this->configuration->ldapTLS) {
393
+            $this->configuration->ldapTLS = false;
394
+            \OCP\Util::writeLog(
395
+                'user_ldap',
396
+                'LDAPS (already using secure connection) and TLS do not work together. Switched off TLS.',
397
+                ILogger::INFO
398
+            );
399
+        }
400
+    }
401
+
402
+    /**
403
+     * @return bool
404
+     */
405
+    private function doCriticalValidation() {
406
+        $configurationOK = true;
407
+        $errorStr = 'Configuration Error (prefix '.
408
+            (string)$this->configPrefix .'): ';
409
+
410
+        //options that shall not be empty
411
+        $options = array('ldapHost', 'ldapPort', 'ldapUserDisplayName',
412
+                            'ldapGroupDisplayName', 'ldapLoginFilter');
413
+        foreach($options as $key) {
414
+            $val = $this->configuration->$key;
415
+            if(empty($val)) {
416
+                switch($key) {
417
+                    case 'ldapHost':
418
+                        $subj = 'LDAP Host';
419
+                        break;
420
+                    case 'ldapPort':
421
+                        $subj = 'LDAP Port';
422
+                        break;
423
+                    case 'ldapUserDisplayName':
424
+                        $subj = 'LDAP User Display Name';
425
+                        break;
426
+                    case 'ldapGroupDisplayName':
427
+                        $subj = 'LDAP Group Display Name';
428
+                        break;
429
+                    case 'ldapLoginFilter':
430
+                        $subj = 'LDAP Login Filter';
431
+                        break;
432
+                    default:
433
+                        $subj = $key;
434
+                        break;
435
+                }
436
+                $configurationOK = false;
437
+                \OCP\Util::writeLog(
438
+                    'user_ldap',
439
+                    $errorStr.'No '.$subj.' given!',
440
+                    ILogger::WARN
441
+                );
442
+            }
443
+        }
444
+
445
+        //combinations
446
+        $agent = $this->configuration->ldapAgentName;
447
+        $pwd = $this->configuration->ldapAgentPassword;
448
+        if (
449
+            ($agent === ''  && $pwd !== '')
450
+            || ($agent !== '' && $pwd === '')
451
+        ) {
452
+            \OCP\Util::writeLog(
453
+                'user_ldap',
454
+                $errorStr.'either no password is given for the user ' .
455
+                    'agent or a password is given, but not an LDAP agent.',
456
+                ILogger::WARN);
457
+            $configurationOK = false;
458
+        }
459
+
460
+        $base = $this->configuration->ldapBase;
461
+        $baseUsers = $this->configuration->ldapBaseUsers;
462
+        $baseGroups = $this->configuration->ldapBaseGroups;
463
+
464
+        if(empty($base) && empty($baseUsers) && empty($baseGroups)) {
465
+            \OCP\Util::writeLog(
466
+                'user_ldap',
467
+                $errorStr.'Not a single Base DN given.',
468
+                ILogger::WARN
469
+            );
470
+            $configurationOK = false;
471
+        }
472
+
473
+        if(mb_strpos($this->configuration->ldapLoginFilter, '%uid', 0, 'UTF-8')
474
+            === false) {
475
+            \OCP\Util::writeLog(
476
+                'user_ldap',
477
+                $errorStr.'login filter does not contain %uid place holder.',
478
+                ILogger::WARN
479
+            );
480
+            $configurationOK = false;
481
+        }
482
+
483
+        return $configurationOK;
484
+    }
485
+
486
+    /**
487
+     * Validates the user specified configuration
488
+     * @return bool true if configuration seems OK, false otherwise
489
+     */
490
+    private function validateConfiguration() {
491
+
492
+        if($this->doNotValidate) {
493
+            //don't do a validation if it is a new configuration with pure
494
+            //default values. Will be allowed on changes via __set or
495
+            //setConfiguration
496
+            return false;
497
+        }
498
+
499
+        // first step: "soft" checks: settings that are not really
500
+        // necessary, but advisable. If left empty, give an info message
501
+        $this->doSoftValidation();
502
+
503
+        //second step: critical checks. If left empty or filled wrong, mark as
504
+        //not configured and give a warning.
505
+        return $this->doCriticalValidation();
506
+    }
507
+
508
+
509
+    /**
510
+     * Connects and Binds to LDAP
511
+     */
512
+    private function establishConnection() {
513
+        if(!$this->configuration->ldapConfigurationActive) {
514
+            return null;
515
+        }
516
+        static $phpLDAPinstalled = true;
517
+        if(!$phpLDAPinstalled) {
518
+            return false;
519
+        }
520
+        if(!$this->ignoreValidation && !$this->configured) {
521
+            \OCP\Util::writeLog(
522
+                'user_ldap',
523
+                'Configuration is invalid, cannot connect',
524
+                ILogger::WARN
525
+            );
526
+            return false;
527
+        }
528
+        if(!$this->ldapConnectionRes) {
529
+            if(!$this->ldap->areLDAPFunctionsAvailable()) {
530
+                $phpLDAPinstalled = false;
531
+                \OCP\Util::writeLog(
532
+                    'user_ldap',
533
+                    'function ldap_connect is not available. Make sure that the PHP ldap module is installed.',
534
+                    ILogger::ERROR
535
+                );
536
+
537
+                return false;
538
+            }
539
+            if($this->configuration->turnOffCertCheck) {
540
+                if(putenv('LDAPTLS_REQCERT=never')) {
541
+                    \OCP\Util::writeLog('user_ldap',
542
+                        'Turned off SSL certificate validation successfully.',
543
+                        ILogger::DEBUG);
544
+                } else {
545
+                    \OCP\Util::writeLog(
546
+                        'user_ldap',
547
+                        'Could not turn off SSL certificate validation.',
548
+                        ILogger::WARN
549
+                    );
550
+                }
551
+            }
552
+
553
+            $isOverrideMainServer = ($this->configuration->ldapOverrideMainServer
554
+                || $this->getFromCache('overrideMainServer'));
555
+            $isBackupHost = (trim($this->configuration->ldapBackupHost) !== "");
556
+            $bindStatus = false;
557
+            $error = -1;
558
+            try {
559
+                if (!$isOverrideMainServer) {
560
+                    $this->doConnect($this->configuration->ldapHost,
561
+                        $this->configuration->ldapPort);
562
+                    $bindStatus = $this->bind();
563
+                    $error = $this->ldap->isResource($this->ldapConnectionRes) ?
564
+                        $this->ldap->errno($this->ldapConnectionRes) : -1;
565
+                }
566
+                if($bindStatus === true) {
567
+                    return $bindStatus;
568
+                }
569
+            } catch (ServerNotAvailableException $e) {
570
+                if(!$isBackupHost) {
571
+                    throw $e;
572
+                }
573
+            }
574
+
575
+            //if LDAP server is not reachable, try the Backup (Replica!) Server
576
+            if($isBackupHost && ($error !== 0 || $isOverrideMainServer)) {
577
+                $this->doConnect($this->configuration->ldapBackupHost,
578
+                                    $this->configuration->ldapBackupPort);
579
+                $this->bindResult = [];
580
+                $bindStatus = $this->bind();
581
+                $error = $this->ldap->isResource($this->ldapConnectionRes) ?
582
+                    $this->ldap->errno($this->ldapConnectionRes) : -1;
583
+                if($bindStatus && $error === 0 && !$this->getFromCache('overrideMainServer')) {
584
+                    //when bind to backup server succeeded and failed to main server,
585
+                    //skip contacting him until next cache refresh
586
+                    $this->writeToCache('overrideMainServer', true);
587
+                }
588
+            }
589
+
590
+            return $bindStatus;
591
+        }
592
+        return null;
593
+    }
594
+
595
+    /**
596
+     * @param string $host
597
+     * @param string $port
598
+     * @return bool
599
+     * @throws \OC\ServerNotAvailableException
600
+     */
601
+    private function doConnect($host, $port) {
602
+        if ($host === '') {
603
+            return false;
604
+        }
605
+
606
+        $this->ldapConnectionRes = $this->ldap->connect($host, $port);
607
+
608
+        if(!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_PROTOCOL_VERSION, 3)) {
609
+            throw new ServerNotAvailableException('Could not set required LDAP Protocol version.');
610
+        }
611
+
612
+        if(!$this->ldap->setOption($this->ldapConnectionRes, LDAP_OPT_REFERRALS, 0)) {
613
+            throw new ServerNotAvailableException('Could not disable LDAP referrals.');
614
+        }
615
+
616
+        if($this->configuration->ldapTLS) {
617
+            if(!$this->ldap->startTls($this->ldapConnectionRes)) {
618
+                throw new ServerNotAvailableException('Start TLS failed, when connecting to LDAP host ' . $host . '.');
619
+            }
620
+        }
621
+
622
+        return true;
623
+    }
624
+
625
+    /**
626
+     * Binds to LDAP
627
+     */
628
+    public function bind() {
629
+        if(!$this->configuration->ldapConfigurationActive) {
630
+            return false;
631
+        }
632
+        $cr = $this->ldapConnectionRes;
633
+        if(!$this->ldap->isResource($cr)) {
634
+            $cr = $this->getConnectionResource();
635
+        }
636
+
637
+        if(
638
+            count($this->bindResult) !== 0
639
+            && $this->bindResult['dn'] === $this->configuration->ldapAgentName
640
+            && \OC::$server->getHasher()->verify(
641
+                $this->configPrefix . $this->configuration->ldapAgentPassword,
642
+                $this->bindResult['hash']
643
+            )
644
+        ) {
645
+            // don't attempt to bind again with the same data as before
646
+            // bind might have been invoked via getConnectionResource(),
647
+            // but we need results specifically for e.g. user login
648
+            return $this->bindResult['result'];
649
+        }
650
+
651
+        $ldapLogin = @$this->ldap->bind($cr,
652
+                                        $this->configuration->ldapAgentName,
653
+                                        $this->configuration->ldapAgentPassword);
654
+
655
+        $this->bindResult = [
656
+            'dn' => $this->configuration->ldapAgentName,
657
+            'hash' => \OC::$server->getHasher()->hash($this->configPrefix . $this->configuration->ldapAgentPassword),
658
+            'result' => $ldapLogin,
659
+        ];
660
+
661
+        if(!$ldapLogin) {
662
+            $errno = $this->ldap->errno($cr);
663
+
664
+            \OCP\Util::writeLog('user_ldap',
665
+                'Bind failed: ' . $errno . ': ' . $this->ldap->error($cr),
666
+                ILogger::WARN);
667
+
668
+            // Set to failure mode, if LDAP error code is not LDAP_SUCCESS or LDAP_INVALID_CREDENTIALS
669
+            if($errno !== 0x00 && $errno !== 0x31) {
670
+                $this->ldapConnectionRes = null;
671
+            }
672
+
673
+            return false;
674
+        }
675
+        return true;
676
+    }
677 677
 
678 678
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Wizard.php 1 patch
Indentation   +1314 added lines, -1314 removed lines patch added patch discarded remove patch
@@ -42,1320 +42,1320 @@
 block discarded – undo
42 42
 use OCP\ILogger;
43 43
 
44 44
 class Wizard extends LDAPUtility {
45
-	/** @var \OCP\IL10N */
46
-	static protected $l;
47
-	protected $access;
48
-	protected $cr;
49
-	protected $configuration;
50
-	protected $result;
51
-	protected $resultCache = array();
52
-
53
-	const LRESULT_PROCESSED_OK = 2;
54
-	const LRESULT_PROCESSED_INVALID = 3;
55
-	const LRESULT_PROCESSED_SKIP = 4;
56
-
57
-	const LFILTER_LOGIN      = 2;
58
-	const LFILTER_USER_LIST  = 3;
59
-	const LFILTER_GROUP_LIST = 4;
60
-
61
-	const LFILTER_MODE_ASSISTED = 2;
62
-	const LFILTER_MODE_RAW = 1;
63
-
64
-	const LDAP_NW_TIMEOUT = 4;
65
-
66
-	/**
67
-	 * Constructor
68
-	 * @param Configuration $configuration an instance of Configuration
69
-	 * @param ILDAPWrapper $ldap an instance of ILDAPWrapper
70
-	 * @param Access $access
71
-	 */
72
-	public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) {
73
-		parent::__construct($ldap);
74
-		$this->configuration = $configuration;
75
-		if(is_null(Wizard::$l)) {
76
-			Wizard::$l = \OC::$server->getL10N('user_ldap');
77
-		}
78
-		$this->access = $access;
79
-		$this->result = new WizardResult();
80
-	}
81
-
82
-	public function  __destruct() {
83
-		if($this->result->hasChanges()) {
84
-			$this->configuration->saveConfiguration();
85
-		}
86
-	}
87
-
88
-	/**
89
-	 * counts entries in the LDAP directory
90
-	 *
91
-	 * @param string $filter the LDAP search filter
92
-	 * @param string $type a string being either 'users' or 'groups';
93
-	 * @return int
94
-	 * @throws \Exception
95
-	 */
96
-	public function countEntries(string $filter, string $type): int {
97
-		$reqs = ['ldapHost', 'ldapPort', 'ldapBase'];
98
-		if($type === 'users') {
99
-			$reqs[] = 'ldapUserFilter';
100
-		}
101
-		if(!$this->checkRequirements($reqs)) {
102
-			throw new \Exception('Requirements not met', 400);
103
-		}
104
-
105
-		$attr = ['dn']; // default
106
-		$limit = 1001;
107
-		if($type === 'groups') {
108
-			$result =  $this->access->countGroups($filter, $attr, $limit);
109
-		} else if($type === 'users') {
110
-			$result = $this->access->countUsers($filter, $attr, $limit);
111
-		} else if ($type === 'objects') {
112
-			$result = $this->access->countObjects($limit);
113
-		} else {
114
-			throw new \Exception('Internal error: Invalid object type', 500);
115
-		}
116
-
117
-		return (int)$result;
118
-	}
119
-
120
-	/**
121
-	 * formats the return value of a count operation to the string to be
122
-	 * inserted.
123
-	 *
124
-	 * @param int $count
125
-	 * @return string
126
-	 */
127
-	private function formatCountResult(int $count): string {
128
-		if($count > 1000) {
129
-			return '> 1000';
130
-		}
131
-		return (string)$count;
132
-	}
133
-
134
-	public function countGroups() {
135
-		$filter = $this->configuration->ldapGroupFilter;
136
-
137
-		if(empty($filter)) {
138
-			$output = self::$l->n('%s group found', '%s groups found', 0, array(0));
139
-			$this->result->addChange('ldap_group_count', $output);
140
-			return $this->result;
141
-		}
142
-
143
-		try {
144
-			$groupsTotal = $this->countEntries($filter, 'groups');
145
-		} catch (\Exception $e) {
146
-			//400 can be ignored, 500 is forwarded
147
-			if($e->getCode() === 500) {
148
-				throw $e;
149
-			}
150
-			return false;
151
-		}
152
-		$output = self::$l->n(
153
-			'%s group found',
154
-			'%s groups found',
155
-			$groupsTotal,
156
-			[$this->formatCountResult($groupsTotal)]
157
-		);
158
-		$this->result->addChange('ldap_group_count', $output);
159
-		return $this->result;
160
-	}
161
-
162
-	/**
163
-	 * @return WizardResult
164
-	 * @throws \Exception
165
-	 */
166
-	public function countUsers() {
167
-		$filter = $this->access->getFilterForUserCount();
168
-
169
-		$usersTotal = $this->countEntries($filter, 'users');
170
-		$output = self::$l->n(
171
-			'%s user found',
172
-			'%s users found',
173
-			$usersTotal,
174
-			[$this->formatCountResult($usersTotal)]
175
-		);
176
-		$this->result->addChange('ldap_user_count', $output);
177
-		return $this->result;
178
-	}
179
-
180
-	/**
181
-	 * counts any objects in the currently set base dn
182
-	 *
183
-	 * @return WizardResult
184
-	 * @throws \Exception
185
-	 */
186
-	public function countInBaseDN() {
187
-		// we don't need to provide a filter in this case
188
-		$total = $this->countEntries('', 'objects');
189
-		if($total === false) {
190
-			throw new \Exception('invalid results received');
191
-		}
192
-		$this->result->addChange('ldap_test_base', $total);
193
-		return $this->result;
194
-	}
195
-
196
-	/**
197
-	 * counts users with a specified attribute
198
-	 * @param string $attr
199
-	 * @param bool $existsCheck
200
-	 * @return int|bool
201
-	 */
202
-	public function countUsersWithAttribute($attr, $existsCheck = false) {
203
-		if(!$this->checkRequirements(array('ldapHost',
204
-										   'ldapPort',
205
-										   'ldapBase',
206
-										   'ldapUserFilter',
207
-										   ))) {
208
-			return  false;
209
-		}
210
-
211
-		$filter = $this->access->combineFilterWithAnd(array(
212
-			$this->configuration->ldapUserFilter,
213
-			$attr . '=*'
214
-		));
215
-
216
-		$limit = ($existsCheck === false) ? null : 1;
217
-
218
-		return $this->access->countUsers($filter, array('dn'), $limit);
219
-	}
220
-
221
-	/**
222
-	 * detects the display name attribute. If a setting is already present that
223
-	 * returns at least one hit, the detection will be canceled.
224
-	 * @return WizardResult|bool
225
-	 * @throws \Exception
226
-	 */
227
-	public function detectUserDisplayNameAttribute() {
228
-		if(!$this->checkRequirements(array('ldapHost',
229
-										'ldapPort',
230
-										'ldapBase',
231
-										'ldapUserFilter',
232
-										))) {
233
-			return  false;
234
-		}
235
-
236
-		$attr = $this->configuration->ldapUserDisplayName;
237
-		if ($attr !== '' && $attr !== 'displayName') {
238
-			// most likely not the default value with upper case N,
239
-			// verify it still produces a result
240
-			$count = (int)$this->countUsersWithAttribute($attr, true);
241
-			if($count > 0) {
242
-				//no change, but we sent it back to make sure the user interface
243
-				//is still correct, even if the ajax call was cancelled meanwhile
244
-				$this->result->addChange('ldap_display_name', $attr);
245
-				return $this->result;
246
-			}
247
-		}
248
-
249
-		// first attribute that has at least one result wins
250
-		$displayNameAttrs = array('displayname', 'cn');
251
-		foreach ($displayNameAttrs as $attr) {
252
-			$count = (int)$this->countUsersWithAttribute($attr, true);
253
-
254
-			if($count > 0) {
255
-				$this->applyFind('ldap_display_name', $attr);
256
-				return $this->result;
257
-			}
258
-		}
259
-
260
-		throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.'));
261
-	}
262
-
263
-	/**
264
-	 * detects the most often used email attribute for users applying to the
265
-	 * user list filter. If a setting is already present that returns at least
266
-	 * one hit, the detection will be canceled.
267
-	 * @return WizardResult|bool
268
-	 */
269
-	public function detectEmailAttribute() {
270
-		if(!$this->checkRequirements(array('ldapHost',
271
-										   'ldapPort',
272
-										   'ldapBase',
273
-										   'ldapUserFilter',
274
-										   ))) {
275
-			return  false;
276
-		}
277
-
278
-		$attr = $this->configuration->ldapEmailAttribute;
279
-		if ($attr !== '') {
280
-			$count = (int)$this->countUsersWithAttribute($attr, true);
281
-			if($count > 0) {
282
-				return false;
283
-			}
284
-			$writeLog = true;
285
-		} else {
286
-			$writeLog = false;
287
-		}
288
-
289
-		$emailAttributes = array('mail', 'mailPrimaryAddress');
290
-		$winner = '';
291
-		$maxUsers = 0;
292
-		foreach($emailAttributes as $attr) {
293
-			$count = $this->countUsersWithAttribute($attr);
294
-			if($count > $maxUsers) {
295
-				$maxUsers = $count;
296
-				$winner = $attr;
297
-			}
298
-		}
299
-
300
-		if($winner !== '') {
301
-			$this->applyFind('ldap_email_attr', $winner);
302
-			if($writeLog) {
303
-				\OCP\Util::writeLog('user_ldap', 'The mail attribute has ' .
304
-					'automatically been reset, because the original value ' .
305
-					'did not return any results.', ILogger::INFO);
306
-			}
307
-		}
308
-
309
-		return $this->result;
310
-	}
311
-
312
-	/**
313
-	 * @return WizardResult
314
-	 * @throws \Exception
315
-	 */
316
-	public function determineAttributes() {
317
-		if(!$this->checkRequirements(array('ldapHost',
318
-										   'ldapPort',
319
-										   'ldapBase',
320
-										   'ldapUserFilter',
321
-										   ))) {
322
-			return  false;
323
-		}
324
-
325
-		$attributes = $this->getUserAttributes();
326
-
327
-		natcasesort($attributes);
328
-		$attributes = array_values($attributes);
329
-
330
-		$this->result->addOptions('ldap_loginfilter_attributes', $attributes);
331
-
332
-		$selected = $this->configuration->ldapLoginFilterAttributes;
333
-		if(is_array($selected) && !empty($selected)) {
334
-			$this->result->addChange('ldap_loginfilter_attributes', $selected);
335
-		}
336
-
337
-		return $this->result;
338
-	}
339
-
340
-	/**
341
-	 * detects the available LDAP attributes
342
-	 * @return array|false The instance's WizardResult instance
343
-	 * @throws \Exception
344
-	 */
345
-	private function getUserAttributes() {
346
-		if(!$this->checkRequirements(array('ldapHost',
347
-										   'ldapPort',
348
-										   'ldapBase',
349
-										   'ldapUserFilter',
350
-										   ))) {
351
-			return  false;
352
-		}
353
-		$cr = $this->getConnection();
354
-		if(!$cr) {
355
-			throw new \Exception('Could not connect to LDAP');
356
-		}
357
-
358
-		$base = $this->configuration->ldapBase[0];
359
-		$filter = $this->configuration->ldapUserFilter;
360
-		$rr = $this->ldap->search($cr, $base, $filter, array(), 1, 1);
361
-		if(!$this->ldap->isResource($rr)) {
362
-			return false;
363
-		}
364
-		$er = $this->ldap->firstEntry($cr, $rr);
365
-		$attributes = $this->ldap->getAttributes($cr, $er);
366
-		$pureAttributes = array();
367
-		for($i = 0; $i < $attributes['count']; $i++) {
368
-			$pureAttributes[] = $attributes[$i];
369
-		}
370
-
371
-		return $pureAttributes;
372
-	}
373
-
374
-	/**
375
-	 * detects the available LDAP groups
376
-	 * @return WizardResult|false the instance's WizardResult instance
377
-	 */
378
-	public function determineGroupsForGroups() {
379
-		return $this->determineGroups('ldap_groupfilter_groups',
380
-									  'ldapGroupFilterGroups',
381
-									  false);
382
-	}
383
-
384
-	/**
385
-	 * detects the available LDAP groups
386
-	 * @return WizardResult|false the instance's WizardResult instance
387
-	 */
388
-	public function determineGroupsForUsers() {
389
-		return $this->determineGroups('ldap_userfilter_groups',
390
-									  'ldapUserFilterGroups');
391
-	}
392
-
393
-	/**
394
-	 * detects the available LDAP groups
395
-	 * @param string $dbKey
396
-	 * @param string $confKey
397
-	 * @param bool $testMemberOf
398
-	 * @return WizardResult|false the instance's WizardResult instance
399
-	 * @throws \Exception
400
-	 */
401
-	private function determineGroups($dbKey, $confKey, $testMemberOf = true) {
402
-		if(!$this->checkRequirements(array('ldapHost',
403
-										   'ldapPort',
404
-										   'ldapBase',
405
-										   ))) {
406
-			return  false;
407
-		}
408
-		$cr = $this->getConnection();
409
-		if(!$cr) {
410
-			throw new \Exception('Could not connect to LDAP');
411
-		}
412
-
413
-		$this->fetchGroups($dbKey, $confKey);
414
-
415
-		if($testMemberOf) {
416
-			$this->configuration->hasMemberOfFilterSupport = $this->testMemberOf();
417
-			$this->result->markChange();
418
-			if(!$this->configuration->hasMemberOfFilterSupport) {
419
-				throw new \Exception('memberOf is not supported by the server');
420
-			}
421
-		}
422
-
423
-		return $this->result;
424
-	}
425
-
426
-	/**
427
-	 * fetches all groups from LDAP and adds them to the result object
428
-	 *
429
-	 * @param string $dbKey
430
-	 * @param string $confKey
431
-	 * @return array $groupEntries
432
-	 * @throws \Exception
433
-	 */
434
-	public function fetchGroups($dbKey, $confKey) {
435
-		$obclasses = array('posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames');
436
-
437
-		$filterParts = array();
438
-		foreach($obclasses as $obclass) {
439
-			$filterParts[] = 'objectclass='.$obclass;
440
-		}
441
-		//we filter for everything
442
-		//- that looks like a group and
443
-		//- has the group display name set
444
-		$filter = $this->access->combineFilterWithOr($filterParts);
445
-		$filter = $this->access->combineFilterWithAnd(array($filter, 'cn=*'));
446
-
447
-		$groupNames = array();
448
-		$groupEntries = array();
449
-		$limit = 400;
450
-		$offset = 0;
451
-		do {
452
-			// we need to request dn additionally here, otherwise memberOf
453
-			// detection will fail later
454
-			$result = $this->access->searchGroups($filter, array('cn', 'dn'), $limit, $offset);
455
-			foreach($result as $item) {
456
-				if(!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) {
457
-					// just in case - no issue known
458
-					continue;
459
-				}
460
-				$groupNames[] = $item['cn'][0];
461
-				$groupEntries[] = $item;
462
-			}
463
-			$offset += $limit;
464
-		} while ($this->access->hasMoreResults());
465
-
466
-		if(count($groupNames) > 0) {
467
-			natsort($groupNames);
468
-			$this->result->addOptions($dbKey, array_values($groupNames));
469
-		} else {
470
-			throw new \Exception(self::$l->t('Could not find the desired feature'));
471
-		}
472
-
473
-		$setFeatures = $this->configuration->$confKey;
474
-		if(is_array($setFeatures) && !empty($setFeatures)) {
475
-			//something is already configured? pre-select it.
476
-			$this->result->addChange($dbKey, $setFeatures);
477
-		}
478
-		return $groupEntries;
479
-	}
480
-
481
-	public function determineGroupMemberAssoc() {
482
-		if(!$this->checkRequirements(array('ldapHost',
483
-										   'ldapPort',
484
-										   'ldapGroupFilter',
485
-										   ))) {
486
-			return  false;
487
-		}
488
-		$attribute = $this->detectGroupMemberAssoc();
489
-		if($attribute === false) {
490
-			return false;
491
-		}
492
-		$this->configuration->setConfiguration(array('ldapGroupMemberAssocAttr' => $attribute));
493
-		$this->result->addChange('ldap_group_member_assoc_attribute', $attribute);
494
-
495
-		return $this->result;
496
-	}
497
-
498
-	/**
499
-	 * Detects the available object classes
500
-	 * @return WizardResult|false the instance's WizardResult instance
501
-	 * @throws \Exception
502
-	 */
503
-	public function determineGroupObjectClasses() {
504
-		if(!$this->checkRequirements(array('ldapHost',
505
-										   'ldapPort',
506
-										   'ldapBase',
507
-										   ))) {
508
-			return  false;
509
-		}
510
-		$cr = $this->getConnection();
511
-		if(!$cr) {
512
-			throw new \Exception('Could not connect to LDAP');
513
-		}
514
-
515
-		$obclasses = array('groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*');
516
-		$this->determineFeature($obclasses,
517
-								'objectclass',
518
-								'ldap_groupfilter_objectclass',
519
-								'ldapGroupFilterObjectclass',
520
-								false);
521
-
522
-		return $this->result;
523
-	}
524
-
525
-	/**
526
-	 * detects the available object classes
527
-	 * @return WizardResult
528
-	 * @throws \Exception
529
-	 */
530
-	public function determineUserObjectClasses() {
531
-		if(!$this->checkRequirements(array('ldapHost',
532
-										   'ldapPort',
533
-										   'ldapBase',
534
-										   ))) {
535
-			return  false;
536
-		}
537
-		$cr = $this->getConnection();
538
-		if(!$cr) {
539
-			throw new \Exception('Could not connect to LDAP');
540
-		}
541
-
542
-		$obclasses = array('inetOrgPerson', 'person', 'organizationalPerson',
543
-						   'user', 'posixAccount', '*');
544
-		$filter = $this->configuration->ldapUserFilter;
545
-		//if filter is empty, it is probably the first time the wizard is called
546
-		//then, apply suggestions.
547
-		$this->determineFeature($obclasses,
548
-								'objectclass',
549
-								'ldap_userfilter_objectclass',
550
-								'ldapUserFilterObjectclass',
551
-								empty($filter));
552
-
553
-		return $this->result;
554
-	}
555
-
556
-	/**
557
-	 * @return WizardResult|false
558
-	 * @throws \Exception
559
-	 */
560
-	public function getGroupFilter() {
561
-		if(!$this->checkRequirements(array('ldapHost',
562
-										   'ldapPort',
563
-										   'ldapBase',
564
-										   ))) {
565
-			return false;
566
-		}
567
-		//make sure the use display name is set
568
-		$displayName = $this->configuration->ldapGroupDisplayName;
569
-		if ($displayName === '') {
570
-			$d = $this->configuration->getDefaults();
571
-			$this->applyFind('ldap_group_display_name',
572
-							 $d['ldap_group_display_name']);
573
-		}
574
-		$filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
575
-
576
-		$this->applyFind('ldap_group_filter', $filter);
577
-		return $this->result;
578
-	}
579
-
580
-	/**
581
-	 * @return WizardResult|false
582
-	 * @throws \Exception
583
-	 */
584
-	public function getUserListFilter() {
585
-		if(!$this->checkRequirements(array('ldapHost',
586
-										   'ldapPort',
587
-										   'ldapBase',
588
-										   ))) {
589
-			return false;
590
-		}
591
-		//make sure the use display name is set
592
-		$displayName = $this->configuration->ldapUserDisplayName;
593
-		if ($displayName === '') {
594
-			$d = $this->configuration->getDefaults();
595
-			$this->applyFind('ldap_display_name', $d['ldap_display_name']);
596
-		}
597
-		$filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
598
-		if(!$filter) {
599
-			throw new \Exception('Cannot create filter');
600
-		}
601
-
602
-		$this->applyFind('ldap_userlist_filter', $filter);
603
-		return $this->result;
604
-	}
605
-
606
-	/**
607
-	 * @return bool|WizardResult
608
-	 * @throws \Exception
609
-	 */
610
-	public function getUserLoginFilter() {
611
-		if(!$this->checkRequirements(array('ldapHost',
612
-										   'ldapPort',
613
-										   'ldapBase',
614
-										   'ldapUserFilter',
615
-										   ))) {
616
-			return false;
617
-		}
618
-
619
-		$filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
620
-		if(!$filter) {
621
-			throw new \Exception('Cannot create filter');
622
-		}
623
-
624
-		$this->applyFind('ldap_login_filter', $filter);
625
-		return $this->result;
626
-	}
627
-
628
-	/**
629
-	 * @return bool|WizardResult
630
-	 * @param string $loginName
631
-	 * @throws \Exception
632
-	 */
633
-	public function testLoginName($loginName) {
634
-		if(!$this->checkRequirements(array('ldapHost',
635
-			'ldapPort',
636
-			'ldapBase',
637
-			'ldapLoginFilter',
638
-		))) {
639
-			return false;
640
-		}
641
-
642
-		$cr = $this->access->connection->getConnectionResource();
643
-		if(!$this->ldap->isResource($cr)) {
644
-			throw new \Exception('connection error');
645
-		}
646
-
647
-		if(mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8')
648
-			=== false) {
649
-			throw new \Exception('missing placeholder');
650
-		}
651
-
652
-		$users = $this->access->countUsersByLoginName($loginName);
653
-		if($this->ldap->errno($cr) !== 0) {
654
-			throw new \Exception($this->ldap->error($cr));
655
-		}
656
-		$filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter);
657
-		$this->result->addChange('ldap_test_loginname', $users);
658
-		$this->result->addChange('ldap_test_effective_filter', $filter);
659
-		return $this->result;
660
-	}
661
-
662
-	/**
663
-	 * Tries to determine the port, requires given Host, User DN and Password
664
-	 * @return WizardResult|false WizardResult on success, false otherwise
665
-	 * @throws \Exception
666
-	 */
667
-	public function guessPortAndTLS() {
668
-		if(!$this->checkRequirements(array('ldapHost',
669
-										   ))) {
670
-			return false;
671
-		}
672
-		$this->checkHost();
673
-		$portSettings = $this->getPortSettingsToTry();
674
-
675
-		if(!is_array($portSettings)) {
676
-			throw new \Exception(print_r($portSettings, true));
677
-		}
678
-
679
-		//proceed from the best configuration and return on first success
680
-		foreach($portSettings as $setting) {
681
-			$p = $setting['port'];
682
-			$t = $setting['tls'];
683
-			\OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, ILogger::DEBUG);
684
-			//connectAndBind may throw Exception, it needs to be catched by the
685
-			//callee of this method
686
-
687
-			try {
688
-				$settingsFound = $this->connectAndBind($p, $t);
689
-			} catch (\Exception $e) {
690
-				// any reply other than -1 (= cannot connect) is already okay,
691
-				// because then we found the server
692
-				// unavailable startTLS returns -11
693
-				if($e->getCode() > 0) {
694
-					$settingsFound = true;
695
-				} else {
696
-					throw $e;
697
-				}
698
-			}
699
-
700
-			if ($settingsFound === true) {
701
-				$config = array(
702
-					'ldapPort' => $p,
703
-					'ldapTLS' => (int)$t
704
-				);
705
-				$this->configuration->setConfiguration($config);
706
-				\OCP\Util::writeLog('user_ldap', 'Wiz: detected Port ' . $p, ILogger::DEBUG);
707
-				$this->result->addChange('ldap_port', $p);
708
-				return $this->result;
709
-			}
710
-		}
711
-
712
-		//custom port, undetected (we do not brute force)
713
-		return false;
714
-	}
715
-
716
-	/**
717
-	 * tries to determine a base dn from User DN or LDAP Host
718
-	 * @return WizardResult|false WizardResult on success, false otherwise
719
-	 */
720
-	public function guessBaseDN() {
721
-		if(!$this->checkRequirements(array('ldapHost',
722
-										   'ldapPort',
723
-										   ))) {
724
-			return false;
725
-		}
726
-
727
-		//check whether a DN is given in the agent name (99.9% of all cases)
728
-		$base = null;
729
-		$i = stripos($this->configuration->ldapAgentName, 'dc=');
730
-		if($i !== false) {
731
-			$base = substr($this->configuration->ldapAgentName, $i);
732
-			if($this->testBaseDN($base)) {
733
-				$this->applyFind('ldap_base', $base);
734
-				return $this->result;
735
-			}
736
-		}
737
-
738
-		//this did not help :(
739
-		//Let's see whether we can parse the Host URL and convert the domain to
740
-		//a base DN
741
-		$helper = new Helper(\OC::$server->getConfig());
742
-		$domain = $helper->getDomainFromURL($this->configuration->ldapHost);
743
-		if(!$domain) {
744
-			return false;
745
-		}
746
-
747
-		$dparts = explode('.', $domain);
748
-		while(count($dparts) > 0) {
749
-			$base2 = 'dc=' . implode(',dc=', $dparts);
750
-			if ($base !== $base2 && $this->testBaseDN($base2)) {
751
-				$this->applyFind('ldap_base', $base2);
752
-				return $this->result;
753
-			}
754
-			array_shift($dparts);
755
-		}
756
-
757
-		return false;
758
-	}
759
-
760
-	/**
761
-	 * sets the found value for the configuration key in the WizardResult
762
-	 * as well as in the Configuration instance
763
-	 * @param string $key the configuration key
764
-	 * @param string $value the (detected) value
765
-	 *
766
-	 */
767
-	private function applyFind($key, $value) {
768
-		$this->result->addChange($key, $value);
769
-		$this->configuration->setConfiguration(array($key => $value));
770
-	}
771
-
772
-	/**
773
-	 * Checks, whether a port was entered in the Host configuration
774
-	 * field. In this case the port will be stripped off, but also stored as
775
-	 * setting.
776
-	 */
777
-	private function checkHost() {
778
-		$host = $this->configuration->ldapHost;
779
-		$hostInfo = parse_url($host);
780
-
781
-		//removes Port from Host
782
-		if(is_array($hostInfo) && isset($hostInfo['port'])) {
783
-			$port = $hostInfo['port'];
784
-			$host = str_replace(':'.$port, '', $host);
785
-			$this->applyFind('ldap_host', $host);
786
-			$this->applyFind('ldap_port', $port);
787
-		}
788
-	}
789
-
790
-	/**
791
-	 * tries to detect the group member association attribute which is
792
-	 * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber'
793
-	 * @return string|false, string with the attribute name, false on error
794
-	 * @throws \Exception
795
-	 */
796
-	private function detectGroupMemberAssoc() {
797
-		$possibleAttrs = array('uniqueMember', 'memberUid', 'member', 'gidNumber');
798
-		$filter = $this->configuration->ldapGroupFilter;
799
-		if(empty($filter)) {
800
-			return false;
801
-		}
802
-		$cr = $this->getConnection();
803
-		if(!$cr) {
804
-			throw new \Exception('Could not connect to LDAP');
805
-		}
806
-		$base = $this->configuration->ldapBase[0];
807
-		$rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000);
808
-		if(!$this->ldap->isResource($rr)) {
809
-			return false;
810
-		}
811
-		$er = $this->ldap->firstEntry($cr, $rr);
812
-		while(is_resource($er)) {
813
-			$this->ldap->getDN($cr, $er);
814
-			$attrs = $this->ldap->getAttributes($cr, $er);
815
-			$result = array();
816
-			$possibleAttrsCount = count($possibleAttrs);
817
-			for($i = 0; $i < $possibleAttrsCount; $i++) {
818
-				if(isset($attrs[$possibleAttrs[$i]])) {
819
-					$result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
820
-				}
821
-			}
822
-			if(!empty($result)) {
823
-				natsort($result);
824
-				return key($result);
825
-			}
826
-
827
-			$er = $this->ldap->nextEntry($cr, $er);
828
-		}
829
-
830
-		return false;
831
-	}
832
-
833
-	/**
834
-	 * Checks whether for a given BaseDN results will be returned
835
-	 * @param string $base the BaseDN to test
836
-	 * @return bool true on success, false otherwise
837
-	 * @throws \Exception
838
-	 */
839
-	private function testBaseDN($base) {
840
-		$cr = $this->getConnection();
841
-		if(!$cr) {
842
-			throw new \Exception('Could not connect to LDAP');
843
-		}
844
-
845
-		//base is there, let's validate it. If we search for anything, we should
846
-		//get a result set > 0 on a proper base
847
-		$rr = $this->ldap->search($cr, $base, 'objectClass=*', array('dn'), 0, 1);
848
-		if(!$this->ldap->isResource($rr)) {
849
-			$errorNo  = $this->ldap->errno($cr);
850
-			$errorMsg = $this->ldap->error($cr);
851
-			\OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base.
852
-							' Error '.$errorNo.': '.$errorMsg, ILogger::INFO);
853
-			return false;
854
-		}
855
-		$entries = $this->ldap->countEntries($cr, $rr);
856
-		return ($entries !== false) && ($entries > 0);
857
-	}
858
-
859
-	/**
860
-	 * Checks whether the server supports memberOf in LDAP Filter.
861
-	 * Note: at least in OpenLDAP, availability of memberOf is dependent on
862
-	 * a configured objectClass. I.e. not necessarily for all available groups
863
-	 * memberOf does work.
864
-	 *
865
-	 * @return bool true if it does, false otherwise
866
-	 * @throws \Exception
867
-	 */
868
-	private function testMemberOf() {
869
-		$cr = $this->getConnection();
870
-		if(!$cr) {
871
-			throw new \Exception('Could not connect to LDAP');
872
-		}
873
-		$result = $this->access->countUsers('memberOf=*', array('memberOf'), 1);
874
-		if(is_int($result) &&  $result > 0) {
875
-			return true;
876
-		}
877
-		return false;
878
-	}
879
-
880
-	/**
881
-	 * creates an LDAP Filter from given configuration
882
-	 * @param integer $filterType int, for which use case the filter shall be created
883
-	 * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
884
-	 * self::LFILTER_GROUP_LIST
885
-	 * @return string|false string with the filter on success, false otherwise
886
-	 * @throws \Exception
887
-	 */
888
-	private function composeLdapFilter($filterType) {
889
-		$filter = '';
890
-		$parts = 0;
891
-		switch ($filterType) {
892
-			case self::LFILTER_USER_LIST:
893
-				$objcs = $this->configuration->ldapUserFilterObjectclass;
894
-				//glue objectclasses
895
-				if(is_array($objcs) && count($objcs) > 0) {
896
-					$filter .= '(|';
897
-					foreach($objcs as $objc) {
898
-						$filter .= '(objectclass=' . $objc . ')';
899
-					}
900
-					$filter .= ')';
901
-					$parts++;
902
-				}
903
-				//glue group memberships
904
-				if($this->configuration->hasMemberOfFilterSupport) {
905
-					$cns = $this->configuration->ldapUserFilterGroups;
906
-					if(is_array($cns) && count($cns) > 0) {
907
-						$filter .= '(|';
908
-						$cr = $this->getConnection();
909
-						if(!$cr) {
910
-							throw new \Exception('Could not connect to LDAP');
911
-						}
912
-						$base = $this->configuration->ldapBase[0];
913
-						foreach($cns as $cn) {
914
-							$rr = $this->ldap->search($cr, $base, 'cn=' . $cn, array('dn', 'primaryGroupToken'));
915
-							if(!$this->ldap->isResource($rr)) {
916
-								continue;
917
-							}
918
-							$er = $this->ldap->firstEntry($cr, $rr);
919
-							$attrs = $this->ldap->getAttributes($cr, $er);
920
-							$dn = $this->ldap->getDN($cr, $er);
921
-							if ($dn === false || $dn === '') {
922
-								continue;
923
-							}
924
-							$filterPart = '(memberof=' . $dn . ')';
925
-							if(isset($attrs['primaryGroupToken'])) {
926
-								$pgt = $attrs['primaryGroupToken'][0];
927
-								$primaryFilterPart = '(primaryGroupID=' . $pgt .')';
928
-								$filterPart = '(|' . $filterPart . $primaryFilterPart . ')';
929
-							}
930
-							$filter .= $filterPart;
931
-						}
932
-						$filter .= ')';
933
-					}
934
-					$parts++;
935
-				}
936
-				//wrap parts in AND condition
937
-				if($parts > 1) {
938
-					$filter = '(&' . $filter . ')';
939
-				}
940
-				if ($filter === '') {
941
-					$filter = '(objectclass=*)';
942
-				}
943
-				break;
944
-
945
-			case self::LFILTER_GROUP_LIST:
946
-				$objcs = $this->configuration->ldapGroupFilterObjectclass;
947
-				//glue objectclasses
948
-				if(is_array($objcs) && count($objcs) > 0) {
949
-					$filter .= '(|';
950
-					foreach($objcs as $objc) {
951
-						$filter .= '(objectclass=' . $objc . ')';
952
-					}
953
-					$filter .= ')';
954
-					$parts++;
955
-				}
956
-				//glue group memberships
957
-				$cns = $this->configuration->ldapGroupFilterGroups;
958
-				if(is_array($cns) && count($cns) > 0) {
959
-					$filter .= '(|';
960
-					foreach($cns as $cn) {
961
-						$filter .= '(cn=' . $cn . ')';
962
-					}
963
-					$filter .= ')';
964
-				}
965
-				$parts++;
966
-				//wrap parts in AND condition
967
-				if($parts > 1) {
968
-					$filter = '(&' . $filter . ')';
969
-				}
970
-				break;
971
-
972
-			case self::LFILTER_LOGIN:
973
-				$ulf = $this->configuration->ldapUserFilter;
974
-				$loginpart = '=%uid';
975
-				$filterUsername = '';
976
-				$userAttributes = $this->getUserAttributes();
977
-				$userAttributes = array_change_key_case(array_flip($userAttributes));
978
-				$parts = 0;
979
-
980
-				if($this->configuration->ldapLoginFilterUsername === '1') {
981
-					$attr = '';
982
-					if(isset($userAttributes['uid'])) {
983
-						$attr = 'uid';
984
-					} else if(isset($userAttributes['samaccountname'])) {
985
-						$attr = 'samaccountname';
986
-					} else if(isset($userAttributes['cn'])) {
987
-						//fallback
988
-						$attr = 'cn';
989
-					}
990
-					if ($attr !== '') {
991
-						$filterUsername = '(' . $attr . $loginpart . ')';
992
-						$parts++;
993
-					}
994
-				}
995
-
996
-				$filterEmail = '';
997
-				if($this->configuration->ldapLoginFilterEmail === '1') {
998
-					$filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
999
-					$parts++;
1000
-				}
1001
-
1002
-				$filterAttributes = '';
1003
-				$attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
1004
-				if(is_array($attrsToFilter) && count($attrsToFilter) > 0) {
1005
-					$filterAttributes = '(|';
1006
-					foreach($attrsToFilter as $attribute) {
1007
-						$filterAttributes .= '(' . $attribute . $loginpart . ')';
1008
-					}
1009
-					$filterAttributes .= ')';
1010
-					$parts++;
1011
-				}
1012
-
1013
-				$filterLogin = '';
1014
-				if($parts > 1) {
1015
-					$filterLogin = '(|';
1016
-				}
1017
-				$filterLogin .= $filterUsername;
1018
-				$filterLogin .= $filterEmail;
1019
-				$filterLogin .= $filterAttributes;
1020
-				if($parts > 1) {
1021
-					$filterLogin .= ')';
1022
-				}
1023
-
1024
-				$filter = '(&'.$ulf.$filterLogin.')';
1025
-				break;
1026
-		}
1027
-
1028
-		\OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, ILogger::DEBUG);
1029
-
1030
-		return $filter;
1031
-	}
1032
-
1033
-	/**
1034
-	 * Connects and Binds to an LDAP Server
1035
-	 *
1036
-	 * @param int $port the port to connect with
1037
-	 * @param bool $tls whether startTLS is to be used
1038
-	 * @return bool
1039
-	 * @throws \Exception
1040
-	 */
1041
-	private function connectAndBind($port, $tls) {
1042
-		//connect, does not really trigger any server communication
1043
-		$host = $this->configuration->ldapHost;
1044
-		$hostInfo = parse_url($host);
1045
-		if(!$hostInfo) {
1046
-			throw new \Exception(self::$l->t('Invalid Host'));
1047
-		}
1048
-		\OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', ILogger::DEBUG);
1049
-		$cr = $this->ldap->connect($host, $port);
1050
-		if(!is_resource($cr)) {
1051
-			throw new \Exception(self::$l->t('Invalid Host'));
1052
-		}
1053
-
1054
-		//set LDAP options
1055
-		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1056
-		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1057
-		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1058
-
1059
-		try {
1060
-			if($tls) {
1061
-				$isTlsWorking = @$this->ldap->startTls($cr);
1062
-				if(!$isTlsWorking) {
1063
-					return false;
1064
-				}
1065
-			}
1066
-
1067
-			\OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', ILogger::DEBUG);
1068
-			//interesting part: do the bind!
1069
-			$login = $this->ldap->bind($cr,
1070
-				$this->configuration->ldapAgentName,
1071
-				$this->configuration->ldapAgentPassword
1072
-			);
1073
-			$errNo = $this->ldap->errno($cr);
1074
-			$error = ldap_error($cr);
1075
-			$this->ldap->unbind($cr);
1076
-		} catch(ServerNotAvailableException $e) {
1077
-			return false;
1078
-		}
1079
-
1080
-		if($login === true) {
1081
-			$this->ldap->unbind($cr);
1082
-			\OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls, ILogger::DEBUG);
1083
-			return true;
1084
-		}
1085
-
1086
-		if($errNo === -1) {
1087
-			//host, port or TLS wrong
1088
-			return false;
1089
-		}
1090
-		throw new \Exception($error, $errNo);
1091
-	}
1092
-
1093
-	/**
1094
-	 * checks whether a valid combination of agent and password has been
1095
-	 * provided (either two values or nothing for anonymous connect)
1096
-	 * @return bool, true if everything is fine, false otherwise
1097
-	 */
1098
-	private function checkAgentRequirements() {
1099
-		$agent = $this->configuration->ldapAgentName;
1100
-		$pwd = $this->configuration->ldapAgentPassword;
1101
-
1102
-		return
1103
-			($agent !== '' && $pwd !== '')
1104
-			||  ($agent === '' && $pwd === '')
1105
-		;
1106
-	}
1107
-
1108
-	/**
1109
-	 * @param array $reqs
1110
-	 * @return bool
1111
-	 */
1112
-	private function checkRequirements($reqs) {
1113
-		$this->checkAgentRequirements();
1114
-		foreach($reqs as $option) {
1115
-			$value = $this->configuration->$option;
1116
-			if(empty($value)) {
1117
-				return false;
1118
-			}
1119
-		}
1120
-		return true;
1121
-	}
1122
-
1123
-	/**
1124
-	 * does a cumulativeSearch on LDAP to get different values of a
1125
-	 * specified attribute
1126
-	 * @param string[] $filters array, the filters that shall be used in the search
1127
-	 * @param string $attr the attribute of which a list of values shall be returned
1128
-	 * @param int $dnReadLimit the amount of how many DNs should be analyzed.
1129
-	 * The lower, the faster
1130
-	 * @param string $maxF string. if not null, this variable will have the filter that
1131
-	 * yields most result entries
1132
-	 * @return array|false an array with the values on success, false otherwise
1133
-	 */
1134
-	public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) {
1135
-		$dnRead = array();
1136
-		$foundItems = array();
1137
-		$maxEntries = 0;
1138
-		if(!is_array($this->configuration->ldapBase)
1139
-		   || !isset($this->configuration->ldapBase[0])) {
1140
-			return false;
1141
-		}
1142
-		$base = $this->configuration->ldapBase[0];
1143
-		$cr = $this->getConnection();
1144
-		if(!$this->ldap->isResource($cr)) {
1145
-			return false;
1146
-		}
1147
-		$lastFilter = null;
1148
-		if(isset($filters[count($filters)-1])) {
1149
-			$lastFilter = $filters[count($filters)-1];
1150
-		}
1151
-		foreach($filters as $filter) {
1152
-			if($lastFilter === $filter && count($foundItems) > 0) {
1153
-				//skip when the filter is a wildcard and results were found
1154
-				continue;
1155
-			}
1156
-			// 20k limit for performance and reason
1157
-			$rr = $this->ldap->search($cr, $base, $filter, array($attr), 0, 20000);
1158
-			if(!$this->ldap->isResource($rr)) {
1159
-				continue;
1160
-			}
1161
-			$entries = $this->ldap->countEntries($cr, $rr);
1162
-			$getEntryFunc = 'firstEntry';
1163
-			if(($entries !== false) && ($entries > 0)) {
1164
-				if(!is_null($maxF) && $entries > $maxEntries) {
1165
-					$maxEntries = $entries;
1166
-					$maxF = $filter;
1167
-				}
1168
-				$dnReadCount = 0;
1169
-				do {
1170
-					$entry = $this->ldap->$getEntryFunc($cr, $rr);
1171
-					$getEntryFunc = 'nextEntry';
1172
-					if(!$this->ldap->isResource($entry)) {
1173
-						continue 2;
1174
-					}
1175
-					$rr = $entry; //will be expected by nextEntry next round
1176
-					$attributes = $this->ldap->getAttributes($cr, $entry);
1177
-					$dn = $this->ldap->getDN($cr, $entry);
1178
-					if($dn === false || in_array($dn, $dnRead)) {
1179
-						continue;
1180
-					}
1181
-					$newItems = array();
1182
-					$state = $this->getAttributeValuesFromEntry($attributes,
1183
-																$attr,
1184
-																$newItems);
1185
-					$dnReadCount++;
1186
-					$foundItems = array_merge($foundItems, $newItems);
1187
-					$this->resultCache[$dn][$attr] = $newItems;
1188
-					$dnRead[] = $dn;
1189
-				} while(($state === self::LRESULT_PROCESSED_SKIP
1190
-						|| $this->ldap->isResource($entry))
1191
-						&& ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
1192
-			}
1193
-		}
1194
-
1195
-		return array_unique($foundItems);
1196
-	}
1197
-
1198
-	/**
1199
-	 * determines if and which $attr are available on the LDAP server
1200
-	 * @param string[] $objectclasses the objectclasses to use as search filter
1201
-	 * @param string $attr the attribute to look for
1202
-	 * @param string $dbkey the dbkey of the setting the feature is connected to
1203
-	 * @param string $confkey the confkey counterpart for the $dbkey as used in the
1204
-	 * Configuration class
1205
-	 * @param bool $po whether the objectClass with most result entries
1206
-	 * shall be pre-selected via the result
1207
-	 * @return array|false list of found items.
1208
-	 * @throws \Exception
1209
-	 */
1210
-	private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
1211
-		$cr = $this->getConnection();
1212
-		if(!$cr) {
1213
-			throw new \Exception('Could not connect to LDAP');
1214
-		}
1215
-		$p = 'objectclass=';
1216
-		foreach($objectclasses as $key => $value) {
1217
-			$objectclasses[$key] = $p.$value;
1218
-		}
1219
-		$maxEntryObjC = '';
1220
-
1221
-		//how deep to dig?
1222
-		//When looking for objectclasses, testing few entries is sufficient,
1223
-		$dig = 3;
1224
-
1225
-		$availableFeatures =
1226
-			$this->cumulativeSearchOnAttribute($objectclasses, $attr,
1227
-											   $dig, $maxEntryObjC);
1228
-		if(is_array($availableFeatures)
1229
-		   && count($availableFeatures) > 0) {
1230
-			natcasesort($availableFeatures);
1231
-			//natcasesort keeps indices, but we must get rid of them for proper
1232
-			//sorting in the web UI. Therefore: array_values
1233
-			$this->result->addOptions($dbkey, array_values($availableFeatures));
1234
-		} else {
1235
-			throw new \Exception(self::$l->t('Could not find the desired feature'));
1236
-		}
1237
-
1238
-		$setFeatures = $this->configuration->$confkey;
1239
-		if(is_array($setFeatures) && !empty($setFeatures)) {
1240
-			//something is already configured? pre-select it.
1241
-			$this->result->addChange($dbkey, $setFeatures);
1242
-		} else if ($po && $maxEntryObjC !== '') {
1243
-			//pre-select objectclass with most result entries
1244
-			$maxEntryObjC = str_replace($p, '', $maxEntryObjC);
1245
-			$this->applyFind($dbkey, $maxEntryObjC);
1246
-			$this->result->addChange($dbkey, $maxEntryObjC);
1247
-		}
1248
-
1249
-		return $availableFeatures;
1250
-	}
1251
-
1252
-	/**
1253
-	 * appends a list of values fr
1254
-	 * @param resource $result the return value from ldap_get_attributes
1255
-	 * @param string $attribute the attribute values to look for
1256
-	 * @param array &$known new values will be appended here
1257
-	 * @return int, state on of the class constants LRESULT_PROCESSED_OK,
1258
-	 * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
1259
-	 */
1260
-	private function getAttributeValuesFromEntry($result, $attribute, &$known) {
1261
-		if(!is_array($result)
1262
-		   || !isset($result['count'])
1263
-		   || !$result['count'] > 0) {
1264
-			return self::LRESULT_PROCESSED_INVALID;
1265
-		}
1266
-
1267
-		// strtolower on all keys for proper comparison
1268
-		$result = \OCP\Util::mb_array_change_key_case($result);
1269
-		$attribute = strtolower($attribute);
1270
-		if(isset($result[$attribute])) {
1271
-			foreach($result[$attribute] as $key => $val) {
1272
-				if($key === 'count') {
1273
-					continue;
1274
-				}
1275
-				if(!in_array($val, $known)) {
1276
-					$known[] = $val;
1277
-				}
1278
-			}
1279
-			return self::LRESULT_PROCESSED_OK;
1280
-		} else {
1281
-			return self::LRESULT_PROCESSED_SKIP;
1282
-		}
1283
-	}
1284
-
1285
-	/**
1286
-	 * @return bool|mixed
1287
-	 */
1288
-	private function getConnection() {
1289
-		if(!is_null($this->cr)) {
1290
-			return $this->cr;
1291
-		}
1292
-
1293
-		$cr = $this->ldap->connect(
1294
-			$this->configuration->ldapHost,
1295
-			$this->configuration->ldapPort
1296
-		);
1297
-
1298
-		$this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1299
-		$this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1300
-		$this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1301
-		if($this->configuration->ldapTLS === 1) {
1302
-			$this->ldap->startTls($cr);
1303
-		}
1304
-
1305
-		$lo = @$this->ldap->bind($cr,
1306
-								 $this->configuration->ldapAgentName,
1307
-								 $this->configuration->ldapAgentPassword);
1308
-		if($lo === true) {
1309
-			$this->$cr = $cr;
1310
-			return $cr;
1311
-		}
1312
-
1313
-		return false;
1314
-	}
1315
-
1316
-	/**
1317
-	 * @return array
1318
-	 */
1319
-	private function getDefaultLdapPortSettings() {
1320
-		static $settings = array(
1321
-								array('port' => 7636, 'tls' => false),
1322
-								array('port' =>  636, 'tls' => false),
1323
-								array('port' => 7389, 'tls' => true),
1324
-								array('port' =>  389, 'tls' => true),
1325
-								array('port' => 7389, 'tls' => false),
1326
-								array('port' =>  389, 'tls' => false),
1327
-						  );
1328
-		return $settings;
1329
-	}
1330
-
1331
-	/**
1332
-	 * @return array
1333
-	 */
1334
-	private function getPortSettingsToTry() {
1335
-		//389 ← LDAP / Unencrypted or StartTLS
1336
-		//636 ← LDAPS / SSL
1337
-		//7xxx ← UCS. need to be checked first, because both ports may be open
1338
-		$host = $this->configuration->ldapHost;
1339
-		$port = (int)$this->configuration->ldapPort;
1340
-		$portSettings = array();
1341
-
1342
-		//In case the port is already provided, we will check this first
1343
-		if($port > 0) {
1344
-			$hostInfo = parse_url($host);
1345
-			if(!(is_array($hostInfo)
1346
-				&& isset($hostInfo['scheme'])
1347
-				&& stripos($hostInfo['scheme'], 'ldaps') !== false)) {
1348
-				$portSettings[] = array('port' => $port, 'tls' => true);
1349
-			}
1350
-			$portSettings[] =array('port' => $port, 'tls' => false);
1351
-		}
1352
-
1353
-		//default ports
1354
-		$portSettings = array_merge($portSettings,
1355
-		                            $this->getDefaultLdapPortSettings());
1356
-
1357
-		return $portSettings;
1358
-	}
45
+    /** @var \OCP\IL10N */
46
+    static protected $l;
47
+    protected $access;
48
+    protected $cr;
49
+    protected $configuration;
50
+    protected $result;
51
+    protected $resultCache = array();
52
+
53
+    const LRESULT_PROCESSED_OK = 2;
54
+    const LRESULT_PROCESSED_INVALID = 3;
55
+    const LRESULT_PROCESSED_SKIP = 4;
56
+
57
+    const LFILTER_LOGIN      = 2;
58
+    const LFILTER_USER_LIST  = 3;
59
+    const LFILTER_GROUP_LIST = 4;
60
+
61
+    const LFILTER_MODE_ASSISTED = 2;
62
+    const LFILTER_MODE_RAW = 1;
63
+
64
+    const LDAP_NW_TIMEOUT = 4;
65
+
66
+    /**
67
+     * Constructor
68
+     * @param Configuration $configuration an instance of Configuration
69
+     * @param ILDAPWrapper $ldap an instance of ILDAPWrapper
70
+     * @param Access $access
71
+     */
72
+    public function __construct(Configuration $configuration, ILDAPWrapper $ldap, Access $access) {
73
+        parent::__construct($ldap);
74
+        $this->configuration = $configuration;
75
+        if(is_null(Wizard::$l)) {
76
+            Wizard::$l = \OC::$server->getL10N('user_ldap');
77
+        }
78
+        $this->access = $access;
79
+        $this->result = new WizardResult();
80
+    }
81
+
82
+    public function  __destruct() {
83
+        if($this->result->hasChanges()) {
84
+            $this->configuration->saveConfiguration();
85
+        }
86
+    }
87
+
88
+    /**
89
+     * counts entries in the LDAP directory
90
+     *
91
+     * @param string $filter the LDAP search filter
92
+     * @param string $type a string being either 'users' or 'groups';
93
+     * @return int
94
+     * @throws \Exception
95
+     */
96
+    public function countEntries(string $filter, string $type): int {
97
+        $reqs = ['ldapHost', 'ldapPort', 'ldapBase'];
98
+        if($type === 'users') {
99
+            $reqs[] = 'ldapUserFilter';
100
+        }
101
+        if(!$this->checkRequirements($reqs)) {
102
+            throw new \Exception('Requirements not met', 400);
103
+        }
104
+
105
+        $attr = ['dn']; // default
106
+        $limit = 1001;
107
+        if($type === 'groups') {
108
+            $result =  $this->access->countGroups($filter, $attr, $limit);
109
+        } else if($type === 'users') {
110
+            $result = $this->access->countUsers($filter, $attr, $limit);
111
+        } else if ($type === 'objects') {
112
+            $result = $this->access->countObjects($limit);
113
+        } else {
114
+            throw new \Exception('Internal error: Invalid object type', 500);
115
+        }
116
+
117
+        return (int)$result;
118
+    }
119
+
120
+    /**
121
+     * formats the return value of a count operation to the string to be
122
+     * inserted.
123
+     *
124
+     * @param int $count
125
+     * @return string
126
+     */
127
+    private function formatCountResult(int $count): string {
128
+        if($count > 1000) {
129
+            return '> 1000';
130
+        }
131
+        return (string)$count;
132
+    }
133
+
134
+    public function countGroups() {
135
+        $filter = $this->configuration->ldapGroupFilter;
136
+
137
+        if(empty($filter)) {
138
+            $output = self::$l->n('%s group found', '%s groups found', 0, array(0));
139
+            $this->result->addChange('ldap_group_count', $output);
140
+            return $this->result;
141
+        }
142
+
143
+        try {
144
+            $groupsTotal = $this->countEntries($filter, 'groups');
145
+        } catch (\Exception $e) {
146
+            //400 can be ignored, 500 is forwarded
147
+            if($e->getCode() === 500) {
148
+                throw $e;
149
+            }
150
+            return false;
151
+        }
152
+        $output = self::$l->n(
153
+            '%s group found',
154
+            '%s groups found',
155
+            $groupsTotal,
156
+            [$this->formatCountResult($groupsTotal)]
157
+        );
158
+        $this->result->addChange('ldap_group_count', $output);
159
+        return $this->result;
160
+    }
161
+
162
+    /**
163
+     * @return WizardResult
164
+     * @throws \Exception
165
+     */
166
+    public function countUsers() {
167
+        $filter = $this->access->getFilterForUserCount();
168
+
169
+        $usersTotal = $this->countEntries($filter, 'users');
170
+        $output = self::$l->n(
171
+            '%s user found',
172
+            '%s users found',
173
+            $usersTotal,
174
+            [$this->formatCountResult($usersTotal)]
175
+        );
176
+        $this->result->addChange('ldap_user_count', $output);
177
+        return $this->result;
178
+    }
179
+
180
+    /**
181
+     * counts any objects in the currently set base dn
182
+     *
183
+     * @return WizardResult
184
+     * @throws \Exception
185
+     */
186
+    public function countInBaseDN() {
187
+        // we don't need to provide a filter in this case
188
+        $total = $this->countEntries('', 'objects');
189
+        if($total === false) {
190
+            throw new \Exception('invalid results received');
191
+        }
192
+        $this->result->addChange('ldap_test_base', $total);
193
+        return $this->result;
194
+    }
195
+
196
+    /**
197
+     * counts users with a specified attribute
198
+     * @param string $attr
199
+     * @param bool $existsCheck
200
+     * @return int|bool
201
+     */
202
+    public function countUsersWithAttribute($attr, $existsCheck = false) {
203
+        if(!$this->checkRequirements(array('ldapHost',
204
+                                            'ldapPort',
205
+                                            'ldapBase',
206
+                                            'ldapUserFilter',
207
+                                            ))) {
208
+            return  false;
209
+        }
210
+
211
+        $filter = $this->access->combineFilterWithAnd(array(
212
+            $this->configuration->ldapUserFilter,
213
+            $attr . '=*'
214
+        ));
215
+
216
+        $limit = ($existsCheck === false) ? null : 1;
217
+
218
+        return $this->access->countUsers($filter, array('dn'), $limit);
219
+    }
220
+
221
+    /**
222
+     * detects the display name attribute. If a setting is already present that
223
+     * returns at least one hit, the detection will be canceled.
224
+     * @return WizardResult|bool
225
+     * @throws \Exception
226
+     */
227
+    public function detectUserDisplayNameAttribute() {
228
+        if(!$this->checkRequirements(array('ldapHost',
229
+                                        'ldapPort',
230
+                                        'ldapBase',
231
+                                        'ldapUserFilter',
232
+                                        ))) {
233
+            return  false;
234
+        }
235
+
236
+        $attr = $this->configuration->ldapUserDisplayName;
237
+        if ($attr !== '' && $attr !== 'displayName') {
238
+            // most likely not the default value with upper case N,
239
+            // verify it still produces a result
240
+            $count = (int)$this->countUsersWithAttribute($attr, true);
241
+            if($count > 0) {
242
+                //no change, but we sent it back to make sure the user interface
243
+                //is still correct, even if the ajax call was cancelled meanwhile
244
+                $this->result->addChange('ldap_display_name', $attr);
245
+                return $this->result;
246
+            }
247
+        }
248
+
249
+        // first attribute that has at least one result wins
250
+        $displayNameAttrs = array('displayname', 'cn');
251
+        foreach ($displayNameAttrs as $attr) {
252
+            $count = (int)$this->countUsersWithAttribute($attr, true);
253
+
254
+            if($count > 0) {
255
+                $this->applyFind('ldap_display_name', $attr);
256
+                return $this->result;
257
+            }
258
+        }
259
+
260
+        throw new \Exception(self::$l->t('Could not detect user display name attribute. Please specify it yourself in advanced LDAP settings.'));
261
+    }
262
+
263
+    /**
264
+     * detects the most often used email attribute for users applying to the
265
+     * user list filter. If a setting is already present that returns at least
266
+     * one hit, the detection will be canceled.
267
+     * @return WizardResult|bool
268
+     */
269
+    public function detectEmailAttribute() {
270
+        if(!$this->checkRequirements(array('ldapHost',
271
+                                            'ldapPort',
272
+                                            'ldapBase',
273
+                                            'ldapUserFilter',
274
+                                            ))) {
275
+            return  false;
276
+        }
277
+
278
+        $attr = $this->configuration->ldapEmailAttribute;
279
+        if ($attr !== '') {
280
+            $count = (int)$this->countUsersWithAttribute($attr, true);
281
+            if($count > 0) {
282
+                return false;
283
+            }
284
+            $writeLog = true;
285
+        } else {
286
+            $writeLog = false;
287
+        }
288
+
289
+        $emailAttributes = array('mail', 'mailPrimaryAddress');
290
+        $winner = '';
291
+        $maxUsers = 0;
292
+        foreach($emailAttributes as $attr) {
293
+            $count = $this->countUsersWithAttribute($attr);
294
+            if($count > $maxUsers) {
295
+                $maxUsers = $count;
296
+                $winner = $attr;
297
+            }
298
+        }
299
+
300
+        if($winner !== '') {
301
+            $this->applyFind('ldap_email_attr', $winner);
302
+            if($writeLog) {
303
+                \OCP\Util::writeLog('user_ldap', 'The mail attribute has ' .
304
+                    'automatically been reset, because the original value ' .
305
+                    'did not return any results.', ILogger::INFO);
306
+            }
307
+        }
308
+
309
+        return $this->result;
310
+    }
311
+
312
+    /**
313
+     * @return WizardResult
314
+     * @throws \Exception
315
+     */
316
+    public function determineAttributes() {
317
+        if(!$this->checkRequirements(array('ldapHost',
318
+                                            'ldapPort',
319
+                                            'ldapBase',
320
+                                            'ldapUserFilter',
321
+                                            ))) {
322
+            return  false;
323
+        }
324
+
325
+        $attributes = $this->getUserAttributes();
326
+
327
+        natcasesort($attributes);
328
+        $attributes = array_values($attributes);
329
+
330
+        $this->result->addOptions('ldap_loginfilter_attributes', $attributes);
331
+
332
+        $selected = $this->configuration->ldapLoginFilterAttributes;
333
+        if(is_array($selected) && !empty($selected)) {
334
+            $this->result->addChange('ldap_loginfilter_attributes', $selected);
335
+        }
336
+
337
+        return $this->result;
338
+    }
339
+
340
+    /**
341
+     * detects the available LDAP attributes
342
+     * @return array|false The instance's WizardResult instance
343
+     * @throws \Exception
344
+     */
345
+    private function getUserAttributes() {
346
+        if(!$this->checkRequirements(array('ldapHost',
347
+                                            'ldapPort',
348
+                                            'ldapBase',
349
+                                            'ldapUserFilter',
350
+                                            ))) {
351
+            return  false;
352
+        }
353
+        $cr = $this->getConnection();
354
+        if(!$cr) {
355
+            throw new \Exception('Could not connect to LDAP');
356
+        }
357
+
358
+        $base = $this->configuration->ldapBase[0];
359
+        $filter = $this->configuration->ldapUserFilter;
360
+        $rr = $this->ldap->search($cr, $base, $filter, array(), 1, 1);
361
+        if(!$this->ldap->isResource($rr)) {
362
+            return false;
363
+        }
364
+        $er = $this->ldap->firstEntry($cr, $rr);
365
+        $attributes = $this->ldap->getAttributes($cr, $er);
366
+        $pureAttributes = array();
367
+        for($i = 0; $i < $attributes['count']; $i++) {
368
+            $pureAttributes[] = $attributes[$i];
369
+        }
370
+
371
+        return $pureAttributes;
372
+    }
373
+
374
+    /**
375
+     * detects the available LDAP groups
376
+     * @return WizardResult|false the instance's WizardResult instance
377
+     */
378
+    public function determineGroupsForGroups() {
379
+        return $this->determineGroups('ldap_groupfilter_groups',
380
+                                        'ldapGroupFilterGroups',
381
+                                        false);
382
+    }
383
+
384
+    /**
385
+     * detects the available LDAP groups
386
+     * @return WizardResult|false the instance's WizardResult instance
387
+     */
388
+    public function determineGroupsForUsers() {
389
+        return $this->determineGroups('ldap_userfilter_groups',
390
+                                        'ldapUserFilterGroups');
391
+    }
392
+
393
+    /**
394
+     * detects the available LDAP groups
395
+     * @param string $dbKey
396
+     * @param string $confKey
397
+     * @param bool $testMemberOf
398
+     * @return WizardResult|false the instance's WizardResult instance
399
+     * @throws \Exception
400
+     */
401
+    private function determineGroups($dbKey, $confKey, $testMemberOf = true) {
402
+        if(!$this->checkRequirements(array('ldapHost',
403
+                                            'ldapPort',
404
+                                            'ldapBase',
405
+                                            ))) {
406
+            return  false;
407
+        }
408
+        $cr = $this->getConnection();
409
+        if(!$cr) {
410
+            throw new \Exception('Could not connect to LDAP');
411
+        }
412
+
413
+        $this->fetchGroups($dbKey, $confKey);
414
+
415
+        if($testMemberOf) {
416
+            $this->configuration->hasMemberOfFilterSupport = $this->testMemberOf();
417
+            $this->result->markChange();
418
+            if(!$this->configuration->hasMemberOfFilterSupport) {
419
+                throw new \Exception('memberOf is not supported by the server');
420
+            }
421
+        }
422
+
423
+        return $this->result;
424
+    }
425
+
426
+    /**
427
+     * fetches all groups from LDAP and adds them to the result object
428
+     *
429
+     * @param string $dbKey
430
+     * @param string $confKey
431
+     * @return array $groupEntries
432
+     * @throws \Exception
433
+     */
434
+    public function fetchGroups($dbKey, $confKey) {
435
+        $obclasses = array('posixGroup', 'group', 'zimbraDistributionList', 'groupOfNames', 'groupOfUniqueNames');
436
+
437
+        $filterParts = array();
438
+        foreach($obclasses as $obclass) {
439
+            $filterParts[] = 'objectclass='.$obclass;
440
+        }
441
+        //we filter for everything
442
+        //- that looks like a group and
443
+        //- has the group display name set
444
+        $filter = $this->access->combineFilterWithOr($filterParts);
445
+        $filter = $this->access->combineFilterWithAnd(array($filter, 'cn=*'));
446
+
447
+        $groupNames = array();
448
+        $groupEntries = array();
449
+        $limit = 400;
450
+        $offset = 0;
451
+        do {
452
+            // we need to request dn additionally here, otherwise memberOf
453
+            // detection will fail later
454
+            $result = $this->access->searchGroups($filter, array('cn', 'dn'), $limit, $offset);
455
+            foreach($result as $item) {
456
+                if(!isset($item['cn']) && !is_array($item['cn']) && !isset($item['cn'][0])) {
457
+                    // just in case - no issue known
458
+                    continue;
459
+                }
460
+                $groupNames[] = $item['cn'][0];
461
+                $groupEntries[] = $item;
462
+            }
463
+            $offset += $limit;
464
+        } while ($this->access->hasMoreResults());
465
+
466
+        if(count($groupNames) > 0) {
467
+            natsort($groupNames);
468
+            $this->result->addOptions($dbKey, array_values($groupNames));
469
+        } else {
470
+            throw new \Exception(self::$l->t('Could not find the desired feature'));
471
+        }
472
+
473
+        $setFeatures = $this->configuration->$confKey;
474
+        if(is_array($setFeatures) && !empty($setFeatures)) {
475
+            //something is already configured? pre-select it.
476
+            $this->result->addChange($dbKey, $setFeatures);
477
+        }
478
+        return $groupEntries;
479
+    }
480
+
481
+    public function determineGroupMemberAssoc() {
482
+        if(!$this->checkRequirements(array('ldapHost',
483
+                                            'ldapPort',
484
+                                            'ldapGroupFilter',
485
+                                            ))) {
486
+            return  false;
487
+        }
488
+        $attribute = $this->detectGroupMemberAssoc();
489
+        if($attribute === false) {
490
+            return false;
491
+        }
492
+        $this->configuration->setConfiguration(array('ldapGroupMemberAssocAttr' => $attribute));
493
+        $this->result->addChange('ldap_group_member_assoc_attribute', $attribute);
494
+
495
+        return $this->result;
496
+    }
497
+
498
+    /**
499
+     * Detects the available object classes
500
+     * @return WizardResult|false the instance's WizardResult instance
501
+     * @throws \Exception
502
+     */
503
+    public function determineGroupObjectClasses() {
504
+        if(!$this->checkRequirements(array('ldapHost',
505
+                                            'ldapPort',
506
+                                            'ldapBase',
507
+                                            ))) {
508
+            return  false;
509
+        }
510
+        $cr = $this->getConnection();
511
+        if(!$cr) {
512
+            throw new \Exception('Could not connect to LDAP');
513
+        }
514
+
515
+        $obclasses = array('groupOfNames', 'groupOfUniqueNames', 'group', 'posixGroup', '*');
516
+        $this->determineFeature($obclasses,
517
+                                'objectclass',
518
+                                'ldap_groupfilter_objectclass',
519
+                                'ldapGroupFilterObjectclass',
520
+                                false);
521
+
522
+        return $this->result;
523
+    }
524
+
525
+    /**
526
+     * detects the available object classes
527
+     * @return WizardResult
528
+     * @throws \Exception
529
+     */
530
+    public function determineUserObjectClasses() {
531
+        if(!$this->checkRequirements(array('ldapHost',
532
+                                            'ldapPort',
533
+                                            'ldapBase',
534
+                                            ))) {
535
+            return  false;
536
+        }
537
+        $cr = $this->getConnection();
538
+        if(!$cr) {
539
+            throw new \Exception('Could not connect to LDAP');
540
+        }
541
+
542
+        $obclasses = array('inetOrgPerson', 'person', 'organizationalPerson',
543
+                            'user', 'posixAccount', '*');
544
+        $filter = $this->configuration->ldapUserFilter;
545
+        //if filter is empty, it is probably the first time the wizard is called
546
+        //then, apply suggestions.
547
+        $this->determineFeature($obclasses,
548
+                                'objectclass',
549
+                                'ldap_userfilter_objectclass',
550
+                                'ldapUserFilterObjectclass',
551
+                                empty($filter));
552
+
553
+        return $this->result;
554
+    }
555
+
556
+    /**
557
+     * @return WizardResult|false
558
+     * @throws \Exception
559
+     */
560
+    public function getGroupFilter() {
561
+        if(!$this->checkRequirements(array('ldapHost',
562
+                                            'ldapPort',
563
+                                            'ldapBase',
564
+                                            ))) {
565
+            return false;
566
+        }
567
+        //make sure the use display name is set
568
+        $displayName = $this->configuration->ldapGroupDisplayName;
569
+        if ($displayName === '') {
570
+            $d = $this->configuration->getDefaults();
571
+            $this->applyFind('ldap_group_display_name',
572
+                                $d['ldap_group_display_name']);
573
+        }
574
+        $filter = $this->composeLdapFilter(self::LFILTER_GROUP_LIST);
575
+
576
+        $this->applyFind('ldap_group_filter', $filter);
577
+        return $this->result;
578
+    }
579
+
580
+    /**
581
+     * @return WizardResult|false
582
+     * @throws \Exception
583
+     */
584
+    public function getUserListFilter() {
585
+        if(!$this->checkRequirements(array('ldapHost',
586
+                                            'ldapPort',
587
+                                            'ldapBase',
588
+                                            ))) {
589
+            return false;
590
+        }
591
+        //make sure the use display name is set
592
+        $displayName = $this->configuration->ldapUserDisplayName;
593
+        if ($displayName === '') {
594
+            $d = $this->configuration->getDefaults();
595
+            $this->applyFind('ldap_display_name', $d['ldap_display_name']);
596
+        }
597
+        $filter = $this->composeLdapFilter(self::LFILTER_USER_LIST);
598
+        if(!$filter) {
599
+            throw new \Exception('Cannot create filter');
600
+        }
601
+
602
+        $this->applyFind('ldap_userlist_filter', $filter);
603
+        return $this->result;
604
+    }
605
+
606
+    /**
607
+     * @return bool|WizardResult
608
+     * @throws \Exception
609
+     */
610
+    public function getUserLoginFilter() {
611
+        if(!$this->checkRequirements(array('ldapHost',
612
+                                            'ldapPort',
613
+                                            'ldapBase',
614
+                                            'ldapUserFilter',
615
+                                            ))) {
616
+            return false;
617
+        }
618
+
619
+        $filter = $this->composeLdapFilter(self::LFILTER_LOGIN);
620
+        if(!$filter) {
621
+            throw new \Exception('Cannot create filter');
622
+        }
623
+
624
+        $this->applyFind('ldap_login_filter', $filter);
625
+        return $this->result;
626
+    }
627
+
628
+    /**
629
+     * @return bool|WizardResult
630
+     * @param string $loginName
631
+     * @throws \Exception
632
+     */
633
+    public function testLoginName($loginName) {
634
+        if(!$this->checkRequirements(array('ldapHost',
635
+            'ldapPort',
636
+            'ldapBase',
637
+            'ldapLoginFilter',
638
+        ))) {
639
+            return false;
640
+        }
641
+
642
+        $cr = $this->access->connection->getConnectionResource();
643
+        if(!$this->ldap->isResource($cr)) {
644
+            throw new \Exception('connection error');
645
+        }
646
+
647
+        if(mb_strpos($this->access->connection->ldapLoginFilter, '%uid', 0, 'UTF-8')
648
+            === false) {
649
+            throw new \Exception('missing placeholder');
650
+        }
651
+
652
+        $users = $this->access->countUsersByLoginName($loginName);
653
+        if($this->ldap->errno($cr) !== 0) {
654
+            throw new \Exception($this->ldap->error($cr));
655
+        }
656
+        $filter = str_replace('%uid', $loginName, $this->access->connection->ldapLoginFilter);
657
+        $this->result->addChange('ldap_test_loginname', $users);
658
+        $this->result->addChange('ldap_test_effective_filter', $filter);
659
+        return $this->result;
660
+    }
661
+
662
+    /**
663
+     * Tries to determine the port, requires given Host, User DN and Password
664
+     * @return WizardResult|false WizardResult on success, false otherwise
665
+     * @throws \Exception
666
+     */
667
+    public function guessPortAndTLS() {
668
+        if(!$this->checkRequirements(array('ldapHost',
669
+                                            ))) {
670
+            return false;
671
+        }
672
+        $this->checkHost();
673
+        $portSettings = $this->getPortSettingsToTry();
674
+
675
+        if(!is_array($portSettings)) {
676
+            throw new \Exception(print_r($portSettings, true));
677
+        }
678
+
679
+        //proceed from the best configuration and return on first success
680
+        foreach($portSettings as $setting) {
681
+            $p = $setting['port'];
682
+            $t = $setting['tls'];
683
+            \OCP\Util::writeLog('user_ldap', 'Wiz: trying port '. $p . ', TLS '. $t, ILogger::DEBUG);
684
+            //connectAndBind may throw Exception, it needs to be catched by the
685
+            //callee of this method
686
+
687
+            try {
688
+                $settingsFound = $this->connectAndBind($p, $t);
689
+            } catch (\Exception $e) {
690
+                // any reply other than -1 (= cannot connect) is already okay,
691
+                // because then we found the server
692
+                // unavailable startTLS returns -11
693
+                if($e->getCode() > 0) {
694
+                    $settingsFound = true;
695
+                } else {
696
+                    throw $e;
697
+                }
698
+            }
699
+
700
+            if ($settingsFound === true) {
701
+                $config = array(
702
+                    'ldapPort' => $p,
703
+                    'ldapTLS' => (int)$t
704
+                );
705
+                $this->configuration->setConfiguration($config);
706
+                \OCP\Util::writeLog('user_ldap', 'Wiz: detected Port ' . $p, ILogger::DEBUG);
707
+                $this->result->addChange('ldap_port', $p);
708
+                return $this->result;
709
+            }
710
+        }
711
+
712
+        //custom port, undetected (we do not brute force)
713
+        return false;
714
+    }
715
+
716
+    /**
717
+     * tries to determine a base dn from User DN or LDAP Host
718
+     * @return WizardResult|false WizardResult on success, false otherwise
719
+     */
720
+    public function guessBaseDN() {
721
+        if(!$this->checkRequirements(array('ldapHost',
722
+                                            'ldapPort',
723
+                                            ))) {
724
+            return false;
725
+        }
726
+
727
+        //check whether a DN is given in the agent name (99.9% of all cases)
728
+        $base = null;
729
+        $i = stripos($this->configuration->ldapAgentName, 'dc=');
730
+        if($i !== false) {
731
+            $base = substr($this->configuration->ldapAgentName, $i);
732
+            if($this->testBaseDN($base)) {
733
+                $this->applyFind('ldap_base', $base);
734
+                return $this->result;
735
+            }
736
+        }
737
+
738
+        //this did not help :(
739
+        //Let's see whether we can parse the Host URL and convert the domain to
740
+        //a base DN
741
+        $helper = new Helper(\OC::$server->getConfig());
742
+        $domain = $helper->getDomainFromURL($this->configuration->ldapHost);
743
+        if(!$domain) {
744
+            return false;
745
+        }
746
+
747
+        $dparts = explode('.', $domain);
748
+        while(count($dparts) > 0) {
749
+            $base2 = 'dc=' . implode(',dc=', $dparts);
750
+            if ($base !== $base2 && $this->testBaseDN($base2)) {
751
+                $this->applyFind('ldap_base', $base2);
752
+                return $this->result;
753
+            }
754
+            array_shift($dparts);
755
+        }
756
+
757
+        return false;
758
+    }
759
+
760
+    /**
761
+     * sets the found value for the configuration key in the WizardResult
762
+     * as well as in the Configuration instance
763
+     * @param string $key the configuration key
764
+     * @param string $value the (detected) value
765
+     *
766
+     */
767
+    private function applyFind($key, $value) {
768
+        $this->result->addChange($key, $value);
769
+        $this->configuration->setConfiguration(array($key => $value));
770
+    }
771
+
772
+    /**
773
+     * Checks, whether a port was entered in the Host configuration
774
+     * field. In this case the port will be stripped off, but also stored as
775
+     * setting.
776
+     */
777
+    private function checkHost() {
778
+        $host = $this->configuration->ldapHost;
779
+        $hostInfo = parse_url($host);
780
+
781
+        //removes Port from Host
782
+        if(is_array($hostInfo) && isset($hostInfo['port'])) {
783
+            $port = $hostInfo['port'];
784
+            $host = str_replace(':'.$port, '', $host);
785
+            $this->applyFind('ldap_host', $host);
786
+            $this->applyFind('ldap_port', $port);
787
+        }
788
+    }
789
+
790
+    /**
791
+     * tries to detect the group member association attribute which is
792
+     * one of 'uniqueMember', 'memberUid', 'member', 'gidNumber'
793
+     * @return string|false, string with the attribute name, false on error
794
+     * @throws \Exception
795
+     */
796
+    private function detectGroupMemberAssoc() {
797
+        $possibleAttrs = array('uniqueMember', 'memberUid', 'member', 'gidNumber');
798
+        $filter = $this->configuration->ldapGroupFilter;
799
+        if(empty($filter)) {
800
+            return false;
801
+        }
802
+        $cr = $this->getConnection();
803
+        if(!$cr) {
804
+            throw new \Exception('Could not connect to LDAP');
805
+        }
806
+        $base = $this->configuration->ldapBase[0];
807
+        $rr = $this->ldap->search($cr, $base, $filter, $possibleAttrs, 0, 1000);
808
+        if(!$this->ldap->isResource($rr)) {
809
+            return false;
810
+        }
811
+        $er = $this->ldap->firstEntry($cr, $rr);
812
+        while(is_resource($er)) {
813
+            $this->ldap->getDN($cr, $er);
814
+            $attrs = $this->ldap->getAttributes($cr, $er);
815
+            $result = array();
816
+            $possibleAttrsCount = count($possibleAttrs);
817
+            for($i = 0; $i < $possibleAttrsCount; $i++) {
818
+                if(isset($attrs[$possibleAttrs[$i]])) {
819
+                    $result[$possibleAttrs[$i]] = $attrs[$possibleAttrs[$i]]['count'];
820
+                }
821
+            }
822
+            if(!empty($result)) {
823
+                natsort($result);
824
+                return key($result);
825
+            }
826
+
827
+            $er = $this->ldap->nextEntry($cr, $er);
828
+        }
829
+
830
+        return false;
831
+    }
832
+
833
+    /**
834
+     * Checks whether for a given BaseDN results will be returned
835
+     * @param string $base the BaseDN to test
836
+     * @return bool true on success, false otherwise
837
+     * @throws \Exception
838
+     */
839
+    private function testBaseDN($base) {
840
+        $cr = $this->getConnection();
841
+        if(!$cr) {
842
+            throw new \Exception('Could not connect to LDAP');
843
+        }
844
+
845
+        //base is there, let's validate it. If we search for anything, we should
846
+        //get a result set > 0 on a proper base
847
+        $rr = $this->ldap->search($cr, $base, 'objectClass=*', array('dn'), 0, 1);
848
+        if(!$this->ldap->isResource($rr)) {
849
+            $errorNo  = $this->ldap->errno($cr);
850
+            $errorMsg = $this->ldap->error($cr);
851
+            \OCP\Util::writeLog('user_ldap', 'Wiz: Could not search base '.$base.
852
+                            ' Error '.$errorNo.': '.$errorMsg, ILogger::INFO);
853
+            return false;
854
+        }
855
+        $entries = $this->ldap->countEntries($cr, $rr);
856
+        return ($entries !== false) && ($entries > 0);
857
+    }
858
+
859
+    /**
860
+     * Checks whether the server supports memberOf in LDAP Filter.
861
+     * Note: at least in OpenLDAP, availability of memberOf is dependent on
862
+     * a configured objectClass. I.e. not necessarily for all available groups
863
+     * memberOf does work.
864
+     *
865
+     * @return bool true if it does, false otherwise
866
+     * @throws \Exception
867
+     */
868
+    private function testMemberOf() {
869
+        $cr = $this->getConnection();
870
+        if(!$cr) {
871
+            throw new \Exception('Could not connect to LDAP');
872
+        }
873
+        $result = $this->access->countUsers('memberOf=*', array('memberOf'), 1);
874
+        if(is_int($result) &&  $result > 0) {
875
+            return true;
876
+        }
877
+        return false;
878
+    }
879
+
880
+    /**
881
+     * creates an LDAP Filter from given configuration
882
+     * @param integer $filterType int, for which use case the filter shall be created
883
+     * can be any of self::LFILTER_USER_LIST, self::LFILTER_LOGIN or
884
+     * self::LFILTER_GROUP_LIST
885
+     * @return string|false string with the filter on success, false otherwise
886
+     * @throws \Exception
887
+     */
888
+    private function composeLdapFilter($filterType) {
889
+        $filter = '';
890
+        $parts = 0;
891
+        switch ($filterType) {
892
+            case self::LFILTER_USER_LIST:
893
+                $objcs = $this->configuration->ldapUserFilterObjectclass;
894
+                //glue objectclasses
895
+                if(is_array($objcs) && count($objcs) > 0) {
896
+                    $filter .= '(|';
897
+                    foreach($objcs as $objc) {
898
+                        $filter .= '(objectclass=' . $objc . ')';
899
+                    }
900
+                    $filter .= ')';
901
+                    $parts++;
902
+                }
903
+                //glue group memberships
904
+                if($this->configuration->hasMemberOfFilterSupport) {
905
+                    $cns = $this->configuration->ldapUserFilterGroups;
906
+                    if(is_array($cns) && count($cns) > 0) {
907
+                        $filter .= '(|';
908
+                        $cr = $this->getConnection();
909
+                        if(!$cr) {
910
+                            throw new \Exception('Could not connect to LDAP');
911
+                        }
912
+                        $base = $this->configuration->ldapBase[0];
913
+                        foreach($cns as $cn) {
914
+                            $rr = $this->ldap->search($cr, $base, 'cn=' . $cn, array('dn', 'primaryGroupToken'));
915
+                            if(!$this->ldap->isResource($rr)) {
916
+                                continue;
917
+                            }
918
+                            $er = $this->ldap->firstEntry($cr, $rr);
919
+                            $attrs = $this->ldap->getAttributes($cr, $er);
920
+                            $dn = $this->ldap->getDN($cr, $er);
921
+                            if ($dn === false || $dn === '') {
922
+                                continue;
923
+                            }
924
+                            $filterPart = '(memberof=' . $dn . ')';
925
+                            if(isset($attrs['primaryGroupToken'])) {
926
+                                $pgt = $attrs['primaryGroupToken'][0];
927
+                                $primaryFilterPart = '(primaryGroupID=' . $pgt .')';
928
+                                $filterPart = '(|' . $filterPart . $primaryFilterPart . ')';
929
+                            }
930
+                            $filter .= $filterPart;
931
+                        }
932
+                        $filter .= ')';
933
+                    }
934
+                    $parts++;
935
+                }
936
+                //wrap parts in AND condition
937
+                if($parts > 1) {
938
+                    $filter = '(&' . $filter . ')';
939
+                }
940
+                if ($filter === '') {
941
+                    $filter = '(objectclass=*)';
942
+                }
943
+                break;
944
+
945
+            case self::LFILTER_GROUP_LIST:
946
+                $objcs = $this->configuration->ldapGroupFilterObjectclass;
947
+                //glue objectclasses
948
+                if(is_array($objcs) && count($objcs) > 0) {
949
+                    $filter .= '(|';
950
+                    foreach($objcs as $objc) {
951
+                        $filter .= '(objectclass=' . $objc . ')';
952
+                    }
953
+                    $filter .= ')';
954
+                    $parts++;
955
+                }
956
+                //glue group memberships
957
+                $cns = $this->configuration->ldapGroupFilterGroups;
958
+                if(is_array($cns) && count($cns) > 0) {
959
+                    $filter .= '(|';
960
+                    foreach($cns as $cn) {
961
+                        $filter .= '(cn=' . $cn . ')';
962
+                    }
963
+                    $filter .= ')';
964
+                }
965
+                $parts++;
966
+                //wrap parts in AND condition
967
+                if($parts > 1) {
968
+                    $filter = '(&' . $filter . ')';
969
+                }
970
+                break;
971
+
972
+            case self::LFILTER_LOGIN:
973
+                $ulf = $this->configuration->ldapUserFilter;
974
+                $loginpart = '=%uid';
975
+                $filterUsername = '';
976
+                $userAttributes = $this->getUserAttributes();
977
+                $userAttributes = array_change_key_case(array_flip($userAttributes));
978
+                $parts = 0;
979
+
980
+                if($this->configuration->ldapLoginFilterUsername === '1') {
981
+                    $attr = '';
982
+                    if(isset($userAttributes['uid'])) {
983
+                        $attr = 'uid';
984
+                    } else if(isset($userAttributes['samaccountname'])) {
985
+                        $attr = 'samaccountname';
986
+                    } else if(isset($userAttributes['cn'])) {
987
+                        //fallback
988
+                        $attr = 'cn';
989
+                    }
990
+                    if ($attr !== '') {
991
+                        $filterUsername = '(' . $attr . $loginpart . ')';
992
+                        $parts++;
993
+                    }
994
+                }
995
+
996
+                $filterEmail = '';
997
+                if($this->configuration->ldapLoginFilterEmail === '1') {
998
+                    $filterEmail = '(|(mailPrimaryAddress=%uid)(mail=%uid))';
999
+                    $parts++;
1000
+                }
1001
+
1002
+                $filterAttributes = '';
1003
+                $attrsToFilter = $this->configuration->ldapLoginFilterAttributes;
1004
+                if(is_array($attrsToFilter) && count($attrsToFilter) > 0) {
1005
+                    $filterAttributes = '(|';
1006
+                    foreach($attrsToFilter as $attribute) {
1007
+                        $filterAttributes .= '(' . $attribute . $loginpart . ')';
1008
+                    }
1009
+                    $filterAttributes .= ')';
1010
+                    $parts++;
1011
+                }
1012
+
1013
+                $filterLogin = '';
1014
+                if($parts > 1) {
1015
+                    $filterLogin = '(|';
1016
+                }
1017
+                $filterLogin .= $filterUsername;
1018
+                $filterLogin .= $filterEmail;
1019
+                $filterLogin .= $filterAttributes;
1020
+                if($parts > 1) {
1021
+                    $filterLogin .= ')';
1022
+                }
1023
+
1024
+                $filter = '(&'.$ulf.$filterLogin.')';
1025
+                break;
1026
+        }
1027
+
1028
+        \OCP\Util::writeLog('user_ldap', 'Wiz: Final filter '.$filter, ILogger::DEBUG);
1029
+
1030
+        return $filter;
1031
+    }
1032
+
1033
+    /**
1034
+     * Connects and Binds to an LDAP Server
1035
+     *
1036
+     * @param int $port the port to connect with
1037
+     * @param bool $tls whether startTLS is to be used
1038
+     * @return bool
1039
+     * @throws \Exception
1040
+     */
1041
+    private function connectAndBind($port, $tls) {
1042
+        //connect, does not really trigger any server communication
1043
+        $host = $this->configuration->ldapHost;
1044
+        $hostInfo = parse_url($host);
1045
+        if(!$hostInfo) {
1046
+            throw new \Exception(self::$l->t('Invalid Host'));
1047
+        }
1048
+        \OCP\Util::writeLog('user_ldap', 'Wiz: Attempting to connect ', ILogger::DEBUG);
1049
+        $cr = $this->ldap->connect($host, $port);
1050
+        if(!is_resource($cr)) {
1051
+            throw new \Exception(self::$l->t('Invalid Host'));
1052
+        }
1053
+
1054
+        //set LDAP options
1055
+        $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1056
+        $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1057
+        $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1058
+
1059
+        try {
1060
+            if($tls) {
1061
+                $isTlsWorking = @$this->ldap->startTls($cr);
1062
+                if(!$isTlsWorking) {
1063
+                    return false;
1064
+                }
1065
+            }
1066
+
1067
+            \OCP\Util::writeLog('user_ldap', 'Wiz: Attemping to Bind ', ILogger::DEBUG);
1068
+            //interesting part: do the bind!
1069
+            $login = $this->ldap->bind($cr,
1070
+                $this->configuration->ldapAgentName,
1071
+                $this->configuration->ldapAgentPassword
1072
+            );
1073
+            $errNo = $this->ldap->errno($cr);
1074
+            $error = ldap_error($cr);
1075
+            $this->ldap->unbind($cr);
1076
+        } catch(ServerNotAvailableException $e) {
1077
+            return false;
1078
+        }
1079
+
1080
+        if($login === true) {
1081
+            $this->ldap->unbind($cr);
1082
+            \OCP\Util::writeLog('user_ldap', 'Wiz: Bind successful to Port '. $port . ' TLS ' . (int)$tls, ILogger::DEBUG);
1083
+            return true;
1084
+        }
1085
+
1086
+        if($errNo === -1) {
1087
+            //host, port or TLS wrong
1088
+            return false;
1089
+        }
1090
+        throw new \Exception($error, $errNo);
1091
+    }
1092
+
1093
+    /**
1094
+     * checks whether a valid combination of agent and password has been
1095
+     * provided (either two values or nothing for anonymous connect)
1096
+     * @return bool, true if everything is fine, false otherwise
1097
+     */
1098
+    private function checkAgentRequirements() {
1099
+        $agent = $this->configuration->ldapAgentName;
1100
+        $pwd = $this->configuration->ldapAgentPassword;
1101
+
1102
+        return
1103
+            ($agent !== '' && $pwd !== '')
1104
+            ||  ($agent === '' && $pwd === '')
1105
+        ;
1106
+    }
1107
+
1108
+    /**
1109
+     * @param array $reqs
1110
+     * @return bool
1111
+     */
1112
+    private function checkRequirements($reqs) {
1113
+        $this->checkAgentRequirements();
1114
+        foreach($reqs as $option) {
1115
+            $value = $this->configuration->$option;
1116
+            if(empty($value)) {
1117
+                return false;
1118
+            }
1119
+        }
1120
+        return true;
1121
+    }
1122
+
1123
+    /**
1124
+     * does a cumulativeSearch on LDAP to get different values of a
1125
+     * specified attribute
1126
+     * @param string[] $filters array, the filters that shall be used in the search
1127
+     * @param string $attr the attribute of which a list of values shall be returned
1128
+     * @param int $dnReadLimit the amount of how many DNs should be analyzed.
1129
+     * The lower, the faster
1130
+     * @param string $maxF string. if not null, this variable will have the filter that
1131
+     * yields most result entries
1132
+     * @return array|false an array with the values on success, false otherwise
1133
+     */
1134
+    public function cumulativeSearchOnAttribute($filters, $attr, $dnReadLimit = 3, &$maxF = null) {
1135
+        $dnRead = array();
1136
+        $foundItems = array();
1137
+        $maxEntries = 0;
1138
+        if(!is_array($this->configuration->ldapBase)
1139
+           || !isset($this->configuration->ldapBase[0])) {
1140
+            return false;
1141
+        }
1142
+        $base = $this->configuration->ldapBase[0];
1143
+        $cr = $this->getConnection();
1144
+        if(!$this->ldap->isResource($cr)) {
1145
+            return false;
1146
+        }
1147
+        $lastFilter = null;
1148
+        if(isset($filters[count($filters)-1])) {
1149
+            $lastFilter = $filters[count($filters)-1];
1150
+        }
1151
+        foreach($filters as $filter) {
1152
+            if($lastFilter === $filter && count($foundItems) > 0) {
1153
+                //skip when the filter is a wildcard and results were found
1154
+                continue;
1155
+            }
1156
+            // 20k limit for performance and reason
1157
+            $rr = $this->ldap->search($cr, $base, $filter, array($attr), 0, 20000);
1158
+            if(!$this->ldap->isResource($rr)) {
1159
+                continue;
1160
+            }
1161
+            $entries = $this->ldap->countEntries($cr, $rr);
1162
+            $getEntryFunc = 'firstEntry';
1163
+            if(($entries !== false) && ($entries > 0)) {
1164
+                if(!is_null($maxF) && $entries > $maxEntries) {
1165
+                    $maxEntries = $entries;
1166
+                    $maxF = $filter;
1167
+                }
1168
+                $dnReadCount = 0;
1169
+                do {
1170
+                    $entry = $this->ldap->$getEntryFunc($cr, $rr);
1171
+                    $getEntryFunc = 'nextEntry';
1172
+                    if(!$this->ldap->isResource($entry)) {
1173
+                        continue 2;
1174
+                    }
1175
+                    $rr = $entry; //will be expected by nextEntry next round
1176
+                    $attributes = $this->ldap->getAttributes($cr, $entry);
1177
+                    $dn = $this->ldap->getDN($cr, $entry);
1178
+                    if($dn === false || in_array($dn, $dnRead)) {
1179
+                        continue;
1180
+                    }
1181
+                    $newItems = array();
1182
+                    $state = $this->getAttributeValuesFromEntry($attributes,
1183
+                                                                $attr,
1184
+                                                                $newItems);
1185
+                    $dnReadCount++;
1186
+                    $foundItems = array_merge($foundItems, $newItems);
1187
+                    $this->resultCache[$dn][$attr] = $newItems;
1188
+                    $dnRead[] = $dn;
1189
+                } while(($state === self::LRESULT_PROCESSED_SKIP
1190
+                        || $this->ldap->isResource($entry))
1191
+                        && ($dnReadLimit === 0 || $dnReadCount < $dnReadLimit));
1192
+            }
1193
+        }
1194
+
1195
+        return array_unique($foundItems);
1196
+    }
1197
+
1198
+    /**
1199
+     * determines if and which $attr are available on the LDAP server
1200
+     * @param string[] $objectclasses the objectclasses to use as search filter
1201
+     * @param string $attr the attribute to look for
1202
+     * @param string $dbkey the dbkey of the setting the feature is connected to
1203
+     * @param string $confkey the confkey counterpart for the $dbkey as used in the
1204
+     * Configuration class
1205
+     * @param bool $po whether the objectClass with most result entries
1206
+     * shall be pre-selected via the result
1207
+     * @return array|false list of found items.
1208
+     * @throws \Exception
1209
+     */
1210
+    private function determineFeature($objectclasses, $attr, $dbkey, $confkey, $po = false) {
1211
+        $cr = $this->getConnection();
1212
+        if(!$cr) {
1213
+            throw new \Exception('Could not connect to LDAP');
1214
+        }
1215
+        $p = 'objectclass=';
1216
+        foreach($objectclasses as $key => $value) {
1217
+            $objectclasses[$key] = $p.$value;
1218
+        }
1219
+        $maxEntryObjC = '';
1220
+
1221
+        //how deep to dig?
1222
+        //When looking for objectclasses, testing few entries is sufficient,
1223
+        $dig = 3;
1224
+
1225
+        $availableFeatures =
1226
+            $this->cumulativeSearchOnAttribute($objectclasses, $attr,
1227
+                                                $dig, $maxEntryObjC);
1228
+        if(is_array($availableFeatures)
1229
+           && count($availableFeatures) > 0) {
1230
+            natcasesort($availableFeatures);
1231
+            //natcasesort keeps indices, but we must get rid of them for proper
1232
+            //sorting in the web UI. Therefore: array_values
1233
+            $this->result->addOptions($dbkey, array_values($availableFeatures));
1234
+        } else {
1235
+            throw new \Exception(self::$l->t('Could not find the desired feature'));
1236
+        }
1237
+
1238
+        $setFeatures = $this->configuration->$confkey;
1239
+        if(is_array($setFeatures) && !empty($setFeatures)) {
1240
+            //something is already configured? pre-select it.
1241
+            $this->result->addChange($dbkey, $setFeatures);
1242
+        } else if ($po && $maxEntryObjC !== '') {
1243
+            //pre-select objectclass with most result entries
1244
+            $maxEntryObjC = str_replace($p, '', $maxEntryObjC);
1245
+            $this->applyFind($dbkey, $maxEntryObjC);
1246
+            $this->result->addChange($dbkey, $maxEntryObjC);
1247
+        }
1248
+
1249
+        return $availableFeatures;
1250
+    }
1251
+
1252
+    /**
1253
+     * appends a list of values fr
1254
+     * @param resource $result the return value from ldap_get_attributes
1255
+     * @param string $attribute the attribute values to look for
1256
+     * @param array &$known new values will be appended here
1257
+     * @return int, state on of the class constants LRESULT_PROCESSED_OK,
1258
+     * LRESULT_PROCESSED_INVALID or LRESULT_PROCESSED_SKIP
1259
+     */
1260
+    private function getAttributeValuesFromEntry($result, $attribute, &$known) {
1261
+        if(!is_array($result)
1262
+           || !isset($result['count'])
1263
+           || !$result['count'] > 0) {
1264
+            return self::LRESULT_PROCESSED_INVALID;
1265
+        }
1266
+
1267
+        // strtolower on all keys for proper comparison
1268
+        $result = \OCP\Util::mb_array_change_key_case($result);
1269
+        $attribute = strtolower($attribute);
1270
+        if(isset($result[$attribute])) {
1271
+            foreach($result[$attribute] as $key => $val) {
1272
+                if($key === 'count') {
1273
+                    continue;
1274
+                }
1275
+                if(!in_array($val, $known)) {
1276
+                    $known[] = $val;
1277
+                }
1278
+            }
1279
+            return self::LRESULT_PROCESSED_OK;
1280
+        } else {
1281
+            return self::LRESULT_PROCESSED_SKIP;
1282
+        }
1283
+    }
1284
+
1285
+    /**
1286
+     * @return bool|mixed
1287
+     */
1288
+    private function getConnection() {
1289
+        if(!is_null($this->cr)) {
1290
+            return $this->cr;
1291
+        }
1292
+
1293
+        $cr = $this->ldap->connect(
1294
+            $this->configuration->ldapHost,
1295
+            $this->configuration->ldapPort
1296
+        );
1297
+
1298
+        $this->ldap->setOption($cr, LDAP_OPT_PROTOCOL_VERSION, 3);
1299
+        $this->ldap->setOption($cr, LDAP_OPT_REFERRALS, 0);
1300
+        $this->ldap->setOption($cr, LDAP_OPT_NETWORK_TIMEOUT, self::LDAP_NW_TIMEOUT);
1301
+        if($this->configuration->ldapTLS === 1) {
1302
+            $this->ldap->startTls($cr);
1303
+        }
1304
+
1305
+        $lo = @$this->ldap->bind($cr,
1306
+                                    $this->configuration->ldapAgentName,
1307
+                                    $this->configuration->ldapAgentPassword);
1308
+        if($lo === true) {
1309
+            $this->$cr = $cr;
1310
+            return $cr;
1311
+        }
1312
+
1313
+        return false;
1314
+    }
1315
+
1316
+    /**
1317
+     * @return array
1318
+     */
1319
+    private function getDefaultLdapPortSettings() {
1320
+        static $settings = array(
1321
+                                array('port' => 7636, 'tls' => false),
1322
+                                array('port' =>  636, 'tls' => false),
1323
+                                array('port' => 7389, 'tls' => true),
1324
+                                array('port' =>  389, 'tls' => true),
1325
+                                array('port' => 7389, 'tls' => false),
1326
+                                array('port' =>  389, 'tls' => false),
1327
+                            );
1328
+        return $settings;
1329
+    }
1330
+
1331
+    /**
1332
+     * @return array
1333
+     */
1334
+    private function getPortSettingsToTry() {
1335
+        //389 ← LDAP / Unencrypted or StartTLS
1336
+        //636 ← LDAPS / SSL
1337
+        //7xxx ← UCS. need to be checked first, because both ports may be open
1338
+        $host = $this->configuration->ldapHost;
1339
+        $port = (int)$this->configuration->ldapPort;
1340
+        $portSettings = array();
1341
+
1342
+        //In case the port is already provided, we will check this first
1343
+        if($port > 0) {
1344
+            $hostInfo = parse_url($host);
1345
+            if(!(is_array($hostInfo)
1346
+                && isset($hostInfo['scheme'])
1347
+                && stripos($hostInfo['scheme'], 'ldaps') !== false)) {
1348
+                $portSettings[] = array('port' => $port, 'tls' => true);
1349
+            }
1350
+            $portSettings[] =array('port' => $port, 'tls' => false);
1351
+        }
1352
+
1353
+        //default ports
1354
+        $portSettings = array_merge($portSettings,
1355
+                                    $this->getDefaultLdapPortSettings());
1356
+
1357
+        return $portSettings;
1358
+    }
1359 1359
 
1360 1360
 
1361 1361
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/User_LDAP.php 1 patch
Indentation   +568 added lines, -568 removed lines patch added patch discarded remove patch
@@ -52,576 +52,576 @@
 block discarded – undo
52 52
 use OCP\Util;
53 53
 
54 54
 class User_LDAP extends BackendUtility implements \OCP\IUserBackend, \OCP\UserInterface, IUserLDAP {
55
-	/** @var \OCP\IConfig */
56
-	protected $ocConfig;
57
-
58
-	/** @var INotificationManager */
59
-	protected $notificationManager;
60
-
61
-	/** @var string */
62
-	protected $currentUserInDeletionProcess;
63
-
64
-	/** @var UserPluginManager */
65
-	protected $userPluginManager;
66
-
67
-	/**
68
-	 * @param Access $access
69
-	 * @param \OCP\IConfig $ocConfig
70
-	 * @param \OCP\Notification\IManager $notificationManager
71
-	 * @param IUserSession $userSession
72
-	 */
73
-	public function __construct(Access $access, IConfig $ocConfig, INotificationManager $notificationManager, IUserSession $userSession, UserPluginManager $userPluginManager) {
74
-		parent::__construct($access);
75
-		$this->ocConfig = $ocConfig;
76
-		$this->notificationManager = $notificationManager;
77
-		$this->userPluginManager = $userPluginManager;
78
-		$this->registerHooks($userSession);
79
-	}
80
-
81
-	protected function registerHooks(IUserSession $userSession) {
82
-		$userSession->listen('\OC\User', 'preDelete', [$this, 'preDeleteUser']);
83
-		$userSession->listen('\OC\User', 'postDelete', [$this, 'postDeleteUser']);
84
-	}
85
-
86
-	public function preDeleteUser(IUser $user) {
87
-		$this->currentUserInDeletionProcess = $user->getUID();
88
-	}
89
-
90
-	public function postDeleteUser() {
91
-		$this->currentUserInDeletionProcess = null;
92
-	}
93
-
94
-	/**
95
-	 * checks whether the user is allowed to change his avatar in Nextcloud
96
-	 * @param string $uid the Nextcloud user name
97
-	 * @return boolean either the user can or cannot
98
-	 */
99
-	public function canChangeAvatar($uid) {
100
-		if ($this->userPluginManager->implementsActions(Backend::PROVIDE_AVATAR)) {
101
-			return $this->userPluginManager->canChangeAvatar($uid);
102
-		}
103
-
104
-		$user = $this->access->userManager->get($uid);
105
-		if(!$user instanceof User) {
106
-			return false;
107
-		}
108
-		if($user->getAvatarImage() === false) {
109
-			return true;
110
-		}
111
-
112
-		return false;
113
-	}
114
-
115
-	/**
116
-	 * returns the username for the given login name, if available
117
-	 *
118
-	 * @param string $loginName
119
-	 * @return string|false
120
-	 */
121
-	public function loginName2UserName($loginName) {
122
-		$cacheKey = 'loginName2UserName-'.$loginName;
123
-		$username = $this->access->connection->getFromCache($cacheKey);
124
-		if(!is_null($username)) {
125
-			return $username;
126
-		}
127
-
128
-		try {
129
-			$ldapRecord = $this->getLDAPUserByLoginName($loginName);
130
-			$user = $this->access->userManager->get($ldapRecord['dn'][0]);
131
-			if($user instanceof OfflineUser) {
132
-				// this path is not really possible, however get() is documented
133
-				// to return User or OfflineUser so we are very defensive here.
134
-				$this->access->connection->writeToCache($cacheKey, false);
135
-				return false;
136
-			}
137
-			$username = $user->getUsername();
138
-			$this->access->connection->writeToCache($cacheKey, $username);
139
-			return $username;
140
-		} catch (NotOnLDAP $e) {
141
-			$this->access->connection->writeToCache($cacheKey, false);
142
-			return false;
143
-		}
144
-	}
55
+    /** @var \OCP\IConfig */
56
+    protected $ocConfig;
57
+
58
+    /** @var INotificationManager */
59
+    protected $notificationManager;
60
+
61
+    /** @var string */
62
+    protected $currentUserInDeletionProcess;
63
+
64
+    /** @var UserPluginManager */
65
+    protected $userPluginManager;
66
+
67
+    /**
68
+     * @param Access $access
69
+     * @param \OCP\IConfig $ocConfig
70
+     * @param \OCP\Notification\IManager $notificationManager
71
+     * @param IUserSession $userSession
72
+     */
73
+    public function __construct(Access $access, IConfig $ocConfig, INotificationManager $notificationManager, IUserSession $userSession, UserPluginManager $userPluginManager) {
74
+        parent::__construct($access);
75
+        $this->ocConfig = $ocConfig;
76
+        $this->notificationManager = $notificationManager;
77
+        $this->userPluginManager = $userPluginManager;
78
+        $this->registerHooks($userSession);
79
+    }
80
+
81
+    protected function registerHooks(IUserSession $userSession) {
82
+        $userSession->listen('\OC\User', 'preDelete', [$this, 'preDeleteUser']);
83
+        $userSession->listen('\OC\User', 'postDelete', [$this, 'postDeleteUser']);
84
+    }
85
+
86
+    public function preDeleteUser(IUser $user) {
87
+        $this->currentUserInDeletionProcess = $user->getUID();
88
+    }
89
+
90
+    public function postDeleteUser() {
91
+        $this->currentUserInDeletionProcess = null;
92
+    }
93
+
94
+    /**
95
+     * checks whether the user is allowed to change his avatar in Nextcloud
96
+     * @param string $uid the Nextcloud user name
97
+     * @return boolean either the user can or cannot
98
+     */
99
+    public function canChangeAvatar($uid) {
100
+        if ($this->userPluginManager->implementsActions(Backend::PROVIDE_AVATAR)) {
101
+            return $this->userPluginManager->canChangeAvatar($uid);
102
+        }
103
+
104
+        $user = $this->access->userManager->get($uid);
105
+        if(!$user instanceof User) {
106
+            return false;
107
+        }
108
+        if($user->getAvatarImage() === false) {
109
+            return true;
110
+        }
111
+
112
+        return false;
113
+    }
114
+
115
+    /**
116
+     * returns the username for the given login name, if available
117
+     *
118
+     * @param string $loginName
119
+     * @return string|false
120
+     */
121
+    public function loginName2UserName($loginName) {
122
+        $cacheKey = 'loginName2UserName-'.$loginName;
123
+        $username = $this->access->connection->getFromCache($cacheKey);
124
+        if(!is_null($username)) {
125
+            return $username;
126
+        }
127
+
128
+        try {
129
+            $ldapRecord = $this->getLDAPUserByLoginName($loginName);
130
+            $user = $this->access->userManager->get($ldapRecord['dn'][0]);
131
+            if($user instanceof OfflineUser) {
132
+                // this path is not really possible, however get() is documented
133
+                // to return User or OfflineUser so we are very defensive here.
134
+                $this->access->connection->writeToCache($cacheKey, false);
135
+                return false;
136
+            }
137
+            $username = $user->getUsername();
138
+            $this->access->connection->writeToCache($cacheKey, $username);
139
+            return $username;
140
+        } catch (NotOnLDAP $e) {
141
+            $this->access->connection->writeToCache($cacheKey, false);
142
+            return false;
143
+        }
144
+    }
145 145
 	
146
-	/**
147
-	 * returns the username for the given LDAP DN, if available
148
-	 *
149
-	 * @param string $dn
150
-	 * @return string|false with the username
151
-	 */
152
-	public function dn2UserName($dn) {
153
-		return $this->access->dn2username($dn);
154
-	}
155
-
156
-	/**
157
-	 * returns an LDAP record based on a given login name
158
-	 *
159
-	 * @param string $loginName
160
-	 * @return array
161
-	 * @throws NotOnLDAP
162
-	 */
163
-	public function getLDAPUserByLoginName($loginName) {
164
-		//find out dn of the user name
165
-		$attrs = $this->access->userManager->getAttributes();
166
-		$users = $this->access->fetchUsersByLoginName($loginName, $attrs);
167
-		if(count($users) < 1) {
168
-			throw new NotOnLDAP('No user available for the given login name on ' .
169
-				$this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
170
-		}
171
-		return $users[0];
172
-	}
173
-
174
-	/**
175
-	 * Check if the password is correct without logging in the user
176
-	 *
177
-	 * @param string $uid The username
178
-	 * @param string $password The password
179
-	 * @return false|string
180
-	 */
181
-	public function checkPassword($uid, $password) {
182
-		try {
183
-			$ldapRecord = $this->getLDAPUserByLoginName($uid);
184
-		} catch(NotOnLDAP $e) {
185
-			if($this->ocConfig->getSystemValue('loglevel', ILogger::WARN) === ILogger::DEBUG) {
186
-				\OC::$server->getLogger()->logException($e, ['app' => 'user_ldap']);
187
-			}
188
-			return false;
189
-		}
190
-		$dn = $ldapRecord['dn'][0];
191
-		$user = $this->access->userManager->get($dn);
192
-
193
-		if(!$user instanceof User) {
194
-			Util::writeLog('user_ldap',
195
-				'LDAP Login: Could not get user object for DN ' . $dn .
196
-				'. Maybe the LDAP entry has no set display name attribute?',
197
-				ILogger::WARN);
198
-			return false;
199
-		}
200
-		if($user->getUsername() !== false) {
201
-			//are the credentials OK?
202
-			if(!$this->access->areCredentialsValid($dn, $password)) {
203
-				return false;
204
-			}
205
-
206
-			$this->access->cacheUserExists($user->getUsername());
207
-			$user->processAttributes($ldapRecord);
208
-			$user->markLogin();
209
-
210
-			return $user->getUsername();
211
-		}
212
-
213
-		return false;
214
-	}
215
-
216
-	/**
217
-	 * Set password
218
-	 * @param string $uid The username
219
-	 * @param string $password The new password
220
-	 * @return bool
221
-	 */
222
-	public function setPassword($uid, $password) {
223
-		if ($this->userPluginManager->implementsActions(Backend::SET_PASSWORD)) {
224
-			return $this->userPluginManager->setPassword($uid, $password);
225
-		}
226
-
227
-		$user = $this->access->userManager->get($uid);
228
-
229
-		if(!$user instanceof User) {
230
-			throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
231
-				'. Maybe the LDAP entry has no set display name attribute?');
232
-		}
233
-		if($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
234
-			$ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN;
235
-			$turnOnPasswordChange = $this->access->connection->turnOnPasswordChange;
236
-			if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) {
237
-				//remove last password expiry warning if any
238
-				$notification = $this->notificationManager->createNotification();
239
-				$notification->setApp('user_ldap')
240
-					->setUser($uid)
241
-					->setObject('pwd_exp_warn', $uid)
242
-				;
243
-				$this->notificationManager->markProcessed($notification);
244
-			}
245
-			return true;
246
-		}
247
-
248
-		return false;
249
-	}
250
-
251
-	/**
252
-	 * Get a list of all users
253
-	 *
254
-	 * @param string $search
255
-	 * @param integer $limit
256
-	 * @param integer $offset
257
-	 * @return string[] an array of all uids
258
-	 */
259
-	public function getUsers($search = '', $limit = 10, $offset = 0) {
260
-		$search = $this->access->escapeFilterPart($search, true);
261
-		$cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
262
-
263
-		//check if users are cached, if so return
264
-		$ldap_users = $this->access->connection->getFromCache($cachekey);
265
-		if(!is_null($ldap_users)) {
266
-			return $ldap_users;
267
-		}
268
-
269
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
270
-		// error. With a limit of 0, we get 0 results. So we pass null.
271
-		if($limit <= 0) {
272
-			$limit = null;
273
-		}
274
-		$filter = $this->access->combineFilterWithAnd(array(
275
-			$this->access->connection->ldapUserFilter,
276
-			$this->access->connection->ldapUserDisplayName . '=*',
277
-			$this->access->getFilterPartForUserSearch($search)
278
-		));
279
-
280
-		Util::writeLog('user_ldap',
281
-			'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
282
-			ILogger::DEBUG);
283
-		//do the search and translate results to Nextcloud names
284
-		$ldap_users = $this->access->fetchListOfUsers(
285
-			$filter,
286
-			$this->access->userManager->getAttributes(true),
287
-			$limit, $offset);
288
-		$ldap_users = $this->access->nextcloudUserNames($ldap_users);
289
-		Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', ILogger::DEBUG);
290
-
291
-		$this->access->connection->writeToCache($cachekey, $ldap_users);
292
-		return $ldap_users;
293
-	}
294
-
295
-	/**
296
-	 * checks whether a user is still available on LDAP
297
-	 *
298
-	 * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
299
-	 * name or an instance of that user
300
-	 * @return bool
301
-	 * @throws \Exception
302
-	 * @throws \OC\ServerNotAvailableException
303
-	 */
304
-	public function userExistsOnLDAP($user) {
305
-		if(is_string($user)) {
306
-			$user = $this->access->userManager->get($user);
307
-		}
308
-		if(is_null($user)) {
309
-			return false;
310
-		}
311
-
312
-		$dn = $user->getDN();
313
-		//check if user really still exists by reading its entry
314
-		if(!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
315
-			$lcr = $this->access->connection->getConnectionResource();
316
-			if(is_null($lcr)) {
317
-				throw new \Exception('No LDAP Connection to server ' . $this->access->connection->ldapHost);
318
-			}
319
-
320
-			try {
321
-				$uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
322
-				if (!$uuid) {
323
-					return false;
324
-				}
325
-				$newDn = $this->access->getUserDnByUuid($uuid);
326
-				//check if renamed user is still valid by reapplying the ldap filter
327
-				if (!is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
328
-					return false;
329
-				}
330
-				$this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
331
-				return true;
332
-			} catch (ServerNotAvailableException $e) {
333
-				throw $e;
334
-			} catch (\Exception $e) {
335
-				return false;
336
-			}
337
-		}
338
-
339
-		if($user instanceof OfflineUser) {
340
-			$user->unmark();
341
-		}
342
-
343
-		return true;
344
-	}
345
-
346
-	/**
347
-	 * check if a user exists
348
-	 * @param string $uid the username
349
-	 * @return boolean
350
-	 * @throws \Exception when connection could not be established
351
-	 */
352
-	public function userExists($uid) {
353
-		$userExists = $this->access->connection->getFromCache('userExists'.$uid);
354
-		if(!is_null($userExists)) {
355
-			return (bool)$userExists;
356
-		}
357
-		//getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
358
-		$user = $this->access->userManager->get($uid);
359
-
360
-		if(is_null($user)) {
361
-			Util::writeLog('user_ldap', 'No DN found for '.$uid.' on '.
362
-				$this->access->connection->ldapHost, ILogger::DEBUG);
363
-			$this->access->connection->writeToCache('userExists'.$uid, false);
364
-			return false;
365
-		} else if($user instanceof OfflineUser) {
366
-			//express check for users marked as deleted. Returning true is
367
-			//necessary for cleanup
368
-			return true;
369
-		}
370
-
371
-		$result = $this->userExistsOnLDAP($user);
372
-		$this->access->connection->writeToCache('userExists'.$uid, $result);
373
-		if($result === true) {
374
-			$user->update();
375
-		}
376
-		return $result;
377
-	}
378
-
379
-	/**
380
-	* returns whether a user was deleted in LDAP
381
-	*
382
-	* @param string $uid The username of the user to delete
383
-	* @return bool
384
-	*/
385
-	public function deleteUser($uid) {
386
-		if ($this->userPluginManager->canDeleteUser()) {
387
-			return $this->userPluginManager->deleteUser($uid);
388
-		}
389
-
390
-		$marked = $this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
391
-		if((int)$marked === 0) {
392
-			\OC::$server->getLogger()->notice(
393
-				'User '.$uid . ' is not marked as deleted, not cleaning up.',
394
-				array('app' => 'user_ldap'));
395
-			return false;
396
-		}
397
-		\OC::$server->getLogger()->info('Cleaning up after user ' . $uid,
398
-			array('app' => 'user_ldap'));
399
-
400
-		$this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core
401
-		$this->access->userManager->invalidate($uid);
402
-		return true;
403
-	}
404
-
405
-	/**
406
-	 * get the user's home directory
407
-	 *
408
-	 * @param string $uid the username
409
-	 * @return bool|string
410
-	 * @throws NoUserException
411
-	 * @throws \Exception
412
-	 */
413
-	public function getHome($uid) {
414
-		// user Exists check required as it is not done in user proxy!
415
-		if(!$this->userExists($uid)) {
416
-			return false;
417
-		}
418
-
419
-		if ($this->userPluginManager->implementsActions(Backend::GET_HOME)) {
420
-			return $this->userPluginManager->getHome($uid);
421
-		}
422
-
423
-		$cacheKey = 'getHome'.$uid;
424
-		$path = $this->access->connection->getFromCache($cacheKey);
425
-		if(!is_null($path)) {
426
-			return $path;
427
-		}
428
-
429
-		// early return path if it is a deleted user
430
-		$user = $this->access->userManager->get($uid);
431
-		if($user instanceof OfflineUser) {
432
-			if($this->currentUserInDeletionProcess !== null
433
-				&& $this->currentUserInDeletionProcess === $user->getOCName()
434
-			) {
435
-				return $user->getHomePath();
436
-			} else {
437
-				throw new NoUserException($uid . ' is not a valid user anymore');
438
-			}
439
-		} else if ($user === null) {
440
-			throw new NoUserException($uid . ' is not a valid user anymore');
441
-		}
442
-
443
-		$path = $user->getHomePath();
444
-		$this->access->cacheUserHome($uid, $path);
445
-
446
-		return $path;
447
-	}
448
-
449
-	/**
450
-	 * get display name of the user
451
-	 * @param string $uid user ID of the user
452
-	 * @return string|false display name
453
-	 */
454
-	public function getDisplayName($uid) {
455
-		if ($this->userPluginManager->implementsActions(Backend::GET_DISPLAYNAME)) {
456
-			return $this->userPluginManager->getDisplayName($uid);
457
-		}
458
-
459
-		if(!$this->userExists($uid)) {
460
-			return false;
461
-		}
462
-
463
-		$cacheKey = 'getDisplayName'.$uid;
464
-		if(!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
465
-			return $displayName;
466
-		}
467
-
468
-		//Check whether the display name is configured to have a 2nd feature
469
-		$additionalAttribute = $this->access->connection->ldapUserDisplayName2;
470
-		$displayName2 = '';
471
-		if ($additionalAttribute !== '') {
472
-			$displayName2 = $this->access->readAttribute(
473
-				$this->access->username2dn($uid),
474
-				$additionalAttribute);
475
-		}
476
-
477
-		$displayName = $this->access->readAttribute(
478
-			$this->access->username2dn($uid),
479
-			$this->access->connection->ldapUserDisplayName);
480
-
481
-		if($displayName && (count($displayName) > 0)) {
482
-			$displayName = $displayName[0];
483
-
484
-			if (is_array($displayName2)){
485
-				$displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
486
-			}
487
-
488
-			$user = $this->access->userManager->get($uid);
489
-			if ($user instanceof User) {
490
-				$displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
491
-				$this->access->connection->writeToCache($cacheKey, $displayName);
492
-			}
493
-			if ($user instanceof OfflineUser) {
494
-				/** @var OfflineUser $user*/
495
-				$displayName = $user->getDisplayName();
496
-			}
497
-			return $displayName;
498
-		}
499
-
500
-		return null;
501
-	}
502
-
503
-	/**
504
-	 * set display name of the user
505
-	 * @param string $uid user ID of the user
506
-	 * @param string $displayName new display name of the user
507
-	 * @return string|false display name
508
-	 */
509
-	public function setDisplayName($uid, $displayName) {
510
-		if ($this->userPluginManager->implementsActions(Backend::SET_DISPLAYNAME)) {
511
-			return $this->userPluginManager->setDisplayName($uid, $displayName);
512
-		}
513
-		return false;
514
-	}
515
-
516
-	/**
517
-	 * Get a list of all display names
518
-	 *
519
-	 * @param string $search
520
-	 * @param string|null $limit
521
-	 * @param string|null $offset
522
-	 * @return array an array of all displayNames (value) and the corresponding uids (key)
523
-	 */
524
-	public function getDisplayNames($search = '', $limit = null, $offset = null) {
525
-		$cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
526
-		if(!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
527
-			return $displayNames;
528
-		}
529
-
530
-		$displayNames = array();
531
-		$users = $this->getUsers($search, $limit, $offset);
532
-		foreach ($users as $user) {
533
-			$displayNames[$user] = $this->getDisplayName($user);
534
-		}
535
-		$this->access->connection->writeToCache($cacheKey, $displayNames);
536
-		return $displayNames;
537
-	}
538
-
539
-	/**
540
-	* Check if backend implements actions
541
-	* @param int $actions bitwise-or'ed actions
542
-	* @return boolean
543
-	*
544
-	* Returns the supported actions as int to be
545
-	* compared with \OC\User\Backend::CREATE_USER etc.
546
-	*/
547
-	public function implementsActions($actions) {
548
-		return (bool)((Backend::CHECK_PASSWORD
549
-			| Backend::GET_HOME
550
-			| Backend::GET_DISPLAYNAME
551
-			| Backend::PROVIDE_AVATAR
552
-			| Backend::COUNT_USERS
553
-			| (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0)
554
-			| $this->userPluginManager->getImplementedActions())
555
-			& $actions);
556
-	}
557
-
558
-	/**
559
-	 * @return bool
560
-	 */
561
-	public function hasUserListings() {
562
-		return true;
563
-	}
564
-
565
-	/**
566
-	 * counts the users in LDAP
567
-	 *
568
-	 * @return int|bool
569
-	 */
570
-	public function countUsers() {
571
-		if ($this->userPluginManager->implementsActions(Backend::COUNT_USERS)) {
572
-			return $this->userPluginManager->countUsers();
573
-		}
574
-
575
-		$filter = $this->access->getFilterForUserCount();
576
-		$cacheKey = 'countUsers-'.$filter;
577
-		if(!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
578
-			return $entries;
579
-		}
580
-		$entries = $this->access->countUsers($filter);
581
-		$this->access->connection->writeToCache($cacheKey, $entries);
582
-		return $entries;
583
-	}
584
-
585
-	/**
586
-	 * Backend name to be shown in user management
587
-	 * @return string the name of the backend to be shown
588
-	 */
589
-	public function getBackendName(){
590
-		return 'LDAP';
591
-	}
146
+    /**
147
+     * returns the username for the given LDAP DN, if available
148
+     *
149
+     * @param string $dn
150
+     * @return string|false with the username
151
+     */
152
+    public function dn2UserName($dn) {
153
+        return $this->access->dn2username($dn);
154
+    }
155
+
156
+    /**
157
+     * returns an LDAP record based on a given login name
158
+     *
159
+     * @param string $loginName
160
+     * @return array
161
+     * @throws NotOnLDAP
162
+     */
163
+    public function getLDAPUserByLoginName($loginName) {
164
+        //find out dn of the user name
165
+        $attrs = $this->access->userManager->getAttributes();
166
+        $users = $this->access->fetchUsersByLoginName($loginName, $attrs);
167
+        if(count($users) < 1) {
168
+            throw new NotOnLDAP('No user available for the given login name on ' .
169
+                $this->access->connection->ldapHost . ':' . $this->access->connection->ldapPort);
170
+        }
171
+        return $users[0];
172
+    }
173
+
174
+    /**
175
+     * Check if the password is correct without logging in the user
176
+     *
177
+     * @param string $uid The username
178
+     * @param string $password The password
179
+     * @return false|string
180
+     */
181
+    public function checkPassword($uid, $password) {
182
+        try {
183
+            $ldapRecord = $this->getLDAPUserByLoginName($uid);
184
+        } catch(NotOnLDAP $e) {
185
+            if($this->ocConfig->getSystemValue('loglevel', ILogger::WARN) === ILogger::DEBUG) {
186
+                \OC::$server->getLogger()->logException($e, ['app' => 'user_ldap']);
187
+            }
188
+            return false;
189
+        }
190
+        $dn = $ldapRecord['dn'][0];
191
+        $user = $this->access->userManager->get($dn);
192
+
193
+        if(!$user instanceof User) {
194
+            Util::writeLog('user_ldap',
195
+                'LDAP Login: Could not get user object for DN ' . $dn .
196
+                '. Maybe the LDAP entry has no set display name attribute?',
197
+                ILogger::WARN);
198
+            return false;
199
+        }
200
+        if($user->getUsername() !== false) {
201
+            //are the credentials OK?
202
+            if(!$this->access->areCredentialsValid($dn, $password)) {
203
+                return false;
204
+            }
205
+
206
+            $this->access->cacheUserExists($user->getUsername());
207
+            $user->processAttributes($ldapRecord);
208
+            $user->markLogin();
209
+
210
+            return $user->getUsername();
211
+        }
212
+
213
+        return false;
214
+    }
215
+
216
+    /**
217
+     * Set password
218
+     * @param string $uid The username
219
+     * @param string $password The new password
220
+     * @return bool
221
+     */
222
+    public function setPassword($uid, $password) {
223
+        if ($this->userPluginManager->implementsActions(Backend::SET_PASSWORD)) {
224
+            return $this->userPluginManager->setPassword($uid, $password);
225
+        }
226
+
227
+        $user = $this->access->userManager->get($uid);
228
+
229
+        if(!$user instanceof User) {
230
+            throw new \Exception('LDAP setPassword: Could not get user object for uid ' . $uid .
231
+                '. Maybe the LDAP entry has no set display name attribute?');
232
+        }
233
+        if($user->getUsername() !== false && $this->access->setPassword($user->getDN(), $password)) {
234
+            $ldapDefaultPPolicyDN = $this->access->connection->ldapDefaultPPolicyDN;
235
+            $turnOnPasswordChange = $this->access->connection->turnOnPasswordChange;
236
+            if (!empty($ldapDefaultPPolicyDN) && ((int)$turnOnPasswordChange === 1)) {
237
+                //remove last password expiry warning if any
238
+                $notification = $this->notificationManager->createNotification();
239
+                $notification->setApp('user_ldap')
240
+                    ->setUser($uid)
241
+                    ->setObject('pwd_exp_warn', $uid)
242
+                ;
243
+                $this->notificationManager->markProcessed($notification);
244
+            }
245
+            return true;
246
+        }
247
+
248
+        return false;
249
+    }
250
+
251
+    /**
252
+     * Get a list of all users
253
+     *
254
+     * @param string $search
255
+     * @param integer $limit
256
+     * @param integer $offset
257
+     * @return string[] an array of all uids
258
+     */
259
+    public function getUsers($search = '', $limit = 10, $offset = 0) {
260
+        $search = $this->access->escapeFilterPart($search, true);
261
+        $cachekey = 'getUsers-'.$search.'-'.$limit.'-'.$offset;
262
+
263
+        //check if users are cached, if so return
264
+        $ldap_users = $this->access->connection->getFromCache($cachekey);
265
+        if(!is_null($ldap_users)) {
266
+            return $ldap_users;
267
+        }
268
+
269
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
270
+        // error. With a limit of 0, we get 0 results. So we pass null.
271
+        if($limit <= 0) {
272
+            $limit = null;
273
+        }
274
+        $filter = $this->access->combineFilterWithAnd(array(
275
+            $this->access->connection->ldapUserFilter,
276
+            $this->access->connection->ldapUserDisplayName . '=*',
277
+            $this->access->getFilterPartForUserSearch($search)
278
+        ));
279
+
280
+        Util::writeLog('user_ldap',
281
+            'getUsers: Options: search '.$search.' limit '.$limit.' offset '.$offset.' Filter: '.$filter,
282
+            ILogger::DEBUG);
283
+        //do the search and translate results to Nextcloud names
284
+        $ldap_users = $this->access->fetchListOfUsers(
285
+            $filter,
286
+            $this->access->userManager->getAttributes(true),
287
+            $limit, $offset);
288
+        $ldap_users = $this->access->nextcloudUserNames($ldap_users);
289
+        Util::writeLog('user_ldap', 'getUsers: '.count($ldap_users). ' Users found', ILogger::DEBUG);
290
+
291
+        $this->access->connection->writeToCache($cachekey, $ldap_users);
292
+        return $ldap_users;
293
+    }
294
+
295
+    /**
296
+     * checks whether a user is still available on LDAP
297
+     *
298
+     * @param string|\OCA\User_LDAP\User\User $user either the Nextcloud user
299
+     * name or an instance of that user
300
+     * @return bool
301
+     * @throws \Exception
302
+     * @throws \OC\ServerNotAvailableException
303
+     */
304
+    public function userExistsOnLDAP($user) {
305
+        if(is_string($user)) {
306
+            $user = $this->access->userManager->get($user);
307
+        }
308
+        if(is_null($user)) {
309
+            return false;
310
+        }
311
+
312
+        $dn = $user->getDN();
313
+        //check if user really still exists by reading its entry
314
+        if(!is_array($this->access->readAttribute($dn, '', $this->access->connection->ldapUserFilter))) {
315
+            $lcr = $this->access->connection->getConnectionResource();
316
+            if(is_null($lcr)) {
317
+                throw new \Exception('No LDAP Connection to server ' . $this->access->connection->ldapHost);
318
+            }
319
+
320
+            try {
321
+                $uuid = $this->access->getUserMapper()->getUUIDByDN($dn);
322
+                if (!$uuid) {
323
+                    return false;
324
+                }
325
+                $newDn = $this->access->getUserDnByUuid($uuid);
326
+                //check if renamed user is still valid by reapplying the ldap filter
327
+                if (!is_array($this->access->readAttribute($newDn, '', $this->access->connection->ldapUserFilter))) {
328
+                    return false;
329
+                }
330
+                $this->access->getUserMapper()->setDNbyUUID($newDn, $uuid);
331
+                return true;
332
+            } catch (ServerNotAvailableException $e) {
333
+                throw $e;
334
+            } catch (\Exception $e) {
335
+                return false;
336
+            }
337
+        }
338
+
339
+        if($user instanceof OfflineUser) {
340
+            $user->unmark();
341
+        }
342
+
343
+        return true;
344
+    }
345
+
346
+    /**
347
+     * check if a user exists
348
+     * @param string $uid the username
349
+     * @return boolean
350
+     * @throws \Exception when connection could not be established
351
+     */
352
+    public function userExists($uid) {
353
+        $userExists = $this->access->connection->getFromCache('userExists'.$uid);
354
+        if(!is_null($userExists)) {
355
+            return (bool)$userExists;
356
+        }
357
+        //getting dn, if false the user does not exist. If dn, he may be mapped only, requires more checking.
358
+        $user = $this->access->userManager->get($uid);
359
+
360
+        if(is_null($user)) {
361
+            Util::writeLog('user_ldap', 'No DN found for '.$uid.' on '.
362
+                $this->access->connection->ldapHost, ILogger::DEBUG);
363
+            $this->access->connection->writeToCache('userExists'.$uid, false);
364
+            return false;
365
+        } else if($user instanceof OfflineUser) {
366
+            //express check for users marked as deleted. Returning true is
367
+            //necessary for cleanup
368
+            return true;
369
+        }
370
+
371
+        $result = $this->userExistsOnLDAP($user);
372
+        $this->access->connection->writeToCache('userExists'.$uid, $result);
373
+        if($result === true) {
374
+            $user->update();
375
+        }
376
+        return $result;
377
+    }
378
+
379
+    /**
380
+     * returns whether a user was deleted in LDAP
381
+     *
382
+     * @param string $uid The username of the user to delete
383
+     * @return bool
384
+     */
385
+    public function deleteUser($uid) {
386
+        if ($this->userPluginManager->canDeleteUser()) {
387
+            return $this->userPluginManager->deleteUser($uid);
388
+        }
389
+
390
+        $marked = $this->ocConfig->getUserValue($uid, 'user_ldap', 'isDeleted', 0);
391
+        if((int)$marked === 0) {
392
+            \OC::$server->getLogger()->notice(
393
+                'User '.$uid . ' is not marked as deleted, not cleaning up.',
394
+                array('app' => 'user_ldap'));
395
+            return false;
396
+        }
397
+        \OC::$server->getLogger()->info('Cleaning up after user ' . $uid,
398
+            array('app' => 'user_ldap'));
399
+
400
+        $this->access->getUserMapper()->unmap($uid); // we don't emit unassign signals here, since it is implicit to delete signals fired from core
401
+        $this->access->userManager->invalidate($uid);
402
+        return true;
403
+    }
404
+
405
+    /**
406
+     * get the user's home directory
407
+     *
408
+     * @param string $uid the username
409
+     * @return bool|string
410
+     * @throws NoUserException
411
+     * @throws \Exception
412
+     */
413
+    public function getHome($uid) {
414
+        // user Exists check required as it is not done in user proxy!
415
+        if(!$this->userExists($uid)) {
416
+            return false;
417
+        }
418
+
419
+        if ($this->userPluginManager->implementsActions(Backend::GET_HOME)) {
420
+            return $this->userPluginManager->getHome($uid);
421
+        }
422
+
423
+        $cacheKey = 'getHome'.$uid;
424
+        $path = $this->access->connection->getFromCache($cacheKey);
425
+        if(!is_null($path)) {
426
+            return $path;
427
+        }
428
+
429
+        // early return path if it is a deleted user
430
+        $user = $this->access->userManager->get($uid);
431
+        if($user instanceof OfflineUser) {
432
+            if($this->currentUserInDeletionProcess !== null
433
+                && $this->currentUserInDeletionProcess === $user->getOCName()
434
+            ) {
435
+                return $user->getHomePath();
436
+            } else {
437
+                throw new NoUserException($uid . ' is not a valid user anymore');
438
+            }
439
+        } else if ($user === null) {
440
+            throw new NoUserException($uid . ' is not a valid user anymore');
441
+        }
442
+
443
+        $path = $user->getHomePath();
444
+        $this->access->cacheUserHome($uid, $path);
445
+
446
+        return $path;
447
+    }
448
+
449
+    /**
450
+     * get display name of the user
451
+     * @param string $uid user ID of the user
452
+     * @return string|false display name
453
+     */
454
+    public function getDisplayName($uid) {
455
+        if ($this->userPluginManager->implementsActions(Backend::GET_DISPLAYNAME)) {
456
+            return $this->userPluginManager->getDisplayName($uid);
457
+        }
458
+
459
+        if(!$this->userExists($uid)) {
460
+            return false;
461
+        }
462
+
463
+        $cacheKey = 'getDisplayName'.$uid;
464
+        if(!is_null($displayName = $this->access->connection->getFromCache($cacheKey))) {
465
+            return $displayName;
466
+        }
467
+
468
+        //Check whether the display name is configured to have a 2nd feature
469
+        $additionalAttribute = $this->access->connection->ldapUserDisplayName2;
470
+        $displayName2 = '';
471
+        if ($additionalAttribute !== '') {
472
+            $displayName2 = $this->access->readAttribute(
473
+                $this->access->username2dn($uid),
474
+                $additionalAttribute);
475
+        }
476
+
477
+        $displayName = $this->access->readAttribute(
478
+            $this->access->username2dn($uid),
479
+            $this->access->connection->ldapUserDisplayName);
480
+
481
+        if($displayName && (count($displayName) > 0)) {
482
+            $displayName = $displayName[0];
483
+
484
+            if (is_array($displayName2)){
485
+                $displayName2 = count($displayName2) > 0 ? $displayName2[0] : '';
486
+            }
487
+
488
+            $user = $this->access->userManager->get($uid);
489
+            if ($user instanceof User) {
490
+                $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2);
491
+                $this->access->connection->writeToCache($cacheKey, $displayName);
492
+            }
493
+            if ($user instanceof OfflineUser) {
494
+                /** @var OfflineUser $user*/
495
+                $displayName = $user->getDisplayName();
496
+            }
497
+            return $displayName;
498
+        }
499
+
500
+        return null;
501
+    }
502
+
503
+    /**
504
+     * set display name of the user
505
+     * @param string $uid user ID of the user
506
+     * @param string $displayName new display name of the user
507
+     * @return string|false display name
508
+     */
509
+    public function setDisplayName($uid, $displayName) {
510
+        if ($this->userPluginManager->implementsActions(Backend::SET_DISPLAYNAME)) {
511
+            return $this->userPluginManager->setDisplayName($uid, $displayName);
512
+        }
513
+        return false;
514
+    }
515
+
516
+    /**
517
+     * Get a list of all display names
518
+     *
519
+     * @param string $search
520
+     * @param string|null $limit
521
+     * @param string|null $offset
522
+     * @return array an array of all displayNames (value) and the corresponding uids (key)
523
+     */
524
+    public function getDisplayNames($search = '', $limit = null, $offset = null) {
525
+        $cacheKey = 'getDisplayNames-'.$search.'-'.$limit.'-'.$offset;
526
+        if(!is_null($displayNames = $this->access->connection->getFromCache($cacheKey))) {
527
+            return $displayNames;
528
+        }
529
+
530
+        $displayNames = array();
531
+        $users = $this->getUsers($search, $limit, $offset);
532
+        foreach ($users as $user) {
533
+            $displayNames[$user] = $this->getDisplayName($user);
534
+        }
535
+        $this->access->connection->writeToCache($cacheKey, $displayNames);
536
+        return $displayNames;
537
+    }
538
+
539
+    /**
540
+     * Check if backend implements actions
541
+     * @param int $actions bitwise-or'ed actions
542
+     * @return boolean
543
+     *
544
+     * Returns the supported actions as int to be
545
+     * compared with \OC\User\Backend::CREATE_USER etc.
546
+     */
547
+    public function implementsActions($actions) {
548
+        return (bool)((Backend::CHECK_PASSWORD
549
+            | Backend::GET_HOME
550
+            | Backend::GET_DISPLAYNAME
551
+            | Backend::PROVIDE_AVATAR
552
+            | Backend::COUNT_USERS
553
+            | (((int)$this->access->connection->turnOnPasswordChange === 1)? Backend::SET_PASSWORD :0)
554
+            | $this->userPluginManager->getImplementedActions())
555
+            & $actions);
556
+    }
557
+
558
+    /**
559
+     * @return bool
560
+     */
561
+    public function hasUserListings() {
562
+        return true;
563
+    }
564
+
565
+    /**
566
+     * counts the users in LDAP
567
+     *
568
+     * @return int|bool
569
+     */
570
+    public function countUsers() {
571
+        if ($this->userPluginManager->implementsActions(Backend::COUNT_USERS)) {
572
+            return $this->userPluginManager->countUsers();
573
+        }
574
+
575
+        $filter = $this->access->getFilterForUserCount();
576
+        $cacheKey = 'countUsers-'.$filter;
577
+        if(!is_null($entries = $this->access->connection->getFromCache($cacheKey))) {
578
+            return $entries;
579
+        }
580
+        $entries = $this->access->countUsers($filter);
581
+        $this->access->connection->writeToCache($cacheKey, $entries);
582
+        return $entries;
583
+    }
584
+
585
+    /**
586
+     * Backend name to be shown in user management
587
+     * @return string the name of the backend to be shown
588
+     */
589
+    public function getBackendName(){
590
+        return 'LDAP';
591
+    }
592 592
 	
593
-	/**
594
-	 * Return access for LDAP interaction.
595
-	 * @param string $uid
596
-	 * @return Access instance of Access for LDAP interaction
597
-	 */
598
-	public function getLDAPAccess($uid) {
599
-		return $this->access;
600
-	}
593
+    /**
594
+     * Return access for LDAP interaction.
595
+     * @param string $uid
596
+     * @return Access instance of Access for LDAP interaction
597
+     */
598
+    public function getLDAPAccess($uid) {
599
+        return $this->access;
600
+    }
601 601
 	
602
-	/**
603
-	 * Return LDAP connection resource from a cloned connection.
604
-	 * The cloned connection needs to be closed manually.
605
-	 * of the current access.
606
-	 * @param string $uid
607
-	 * @return resource of the LDAP connection
608
-	 */
609
-	public function getNewLDAPConnection($uid) {
610
-		$connection = clone $this->access->getConnection();
611
-		return $connection->getConnectionResource();
612
-	}
613
-
614
-	/**
615
-	 * create new user
616
-	 * @param string $username username of the new user
617
-	 * @param string $password password of the new user
618
-	 * @return bool was the user created?
619
-	 */
620
-	public function createUser($username, $password) {
621
-		if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) {
622
-			return $this->userPluginManager->createUser($username, $password);
623
-		}
624
-		return false;
625
-	}
602
+    /**
603
+     * Return LDAP connection resource from a cloned connection.
604
+     * The cloned connection needs to be closed manually.
605
+     * of the current access.
606
+     * @param string $uid
607
+     * @return resource of the LDAP connection
608
+     */
609
+    public function getNewLDAPConnection($uid) {
610
+        $connection = clone $this->access->getConnection();
611
+        return $connection->getConnectionResource();
612
+    }
613
+
614
+    /**
615
+     * create new user
616
+     * @param string $username username of the new user
617
+     * @param string $password password of the new user
618
+     * @return bool was the user created?
619
+     */
620
+    public function createUser($username, $password) {
621
+        if ($this->userPluginManager->implementsActions(Backend::CREATE_USER)) {
622
+            return $this->userPluginManager->createUser($username, $password);
623
+        }
624
+        return false;
625
+    }
626 626
 
627 627
 }
Please login to merge, or discard this patch.
apps/user_ldap/lib/Group_LDAP.php 1 patch
Indentation   +1140 added lines, -1140 removed lines patch added patch discarded remove patch
@@ -46,1145 +46,1145 @@
 block discarded – undo
46 46
 use OCP\ILogger;
47 47
 
48 48
 class Group_LDAP extends BackendUtility implements \OCP\GroupInterface, IGroupLDAP {
49
-	protected $enabled = false;
50
-
51
-	/**
52
-	 * @var string[] $cachedGroupMembers array of users with gid as key
53
-	 */
54
-	protected $cachedGroupMembers;
55
-
56
-	/**
57
-	 * @var string[] $cachedGroupsByMember array of groups with uid as key
58
-	 */
59
-	protected $cachedGroupsByMember;
60
-
61
-	/** @var GroupPluginManager */
62
-	protected $groupPluginManager;
63
-
64
-	public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
65
-		parent::__construct($access);
66
-		$filter = $this->access->connection->ldapGroupFilter;
67
-		$gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
68
-		if(!empty($filter) && !empty($gassoc)) {
69
-			$this->enabled = true;
70
-		}
71
-
72
-		$this->cachedGroupMembers = new CappedMemoryCache();
73
-		$this->cachedGroupsByMember = new CappedMemoryCache();
74
-		$this->groupPluginManager = $groupPluginManager;
75
-	}
76
-
77
-	/**
78
-	 * is user in group?
79
-	 * @param string $uid uid of the user
80
-	 * @param string $gid gid of the group
81
-	 * @return bool
82
-	 *
83
-	 * Checks whether the user is member of a group or not.
84
-	 */
85
-	public function inGroup($uid, $gid) {
86
-		if(!$this->enabled) {
87
-			return false;
88
-		}
89
-		$cacheKey = 'inGroup'.$uid.':'.$gid;
90
-		$inGroup = $this->access->connection->getFromCache($cacheKey);
91
-		if(!is_null($inGroup)) {
92
-			return (bool)$inGroup;
93
-		}
94
-
95
-		$userDN = $this->access->username2dn($uid);
96
-
97
-		if(isset($this->cachedGroupMembers[$gid])) {
98
-			$isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
99
-			return $isInGroup;
100
-		}
101
-
102
-		$cacheKeyMembers = 'inGroup-members:'.$gid;
103
-		$members = $this->access->connection->getFromCache($cacheKeyMembers);
104
-		if(!is_null($members)) {
105
-			$this->cachedGroupMembers[$gid] = $members;
106
-			$isInGroup = in_array($userDN, $members);
107
-			$this->access->connection->writeToCache($cacheKey, $isInGroup);
108
-			return $isInGroup;
109
-		}
110
-
111
-		$groupDN = $this->access->groupname2dn($gid);
112
-		// just in case
113
-		if(!$groupDN || !$userDN) {
114
-			$this->access->connection->writeToCache($cacheKey, false);
115
-			return false;
116
-		}
117
-
118
-		//check primary group first
119
-		if($gid === $this->getUserPrimaryGroup($userDN)) {
120
-			$this->access->connection->writeToCache($cacheKey, true);
121
-			return true;
122
-		}
123
-
124
-		//usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
125
-		$members = $this->_groupMembers($groupDN);
126
-		$members = array_keys($members); // uids are returned as keys
127
-		if(!is_array($members) || count($members) === 0) {
128
-			$this->access->connection->writeToCache($cacheKey, false);
129
-			return false;
130
-		}
131
-
132
-		//extra work if we don't get back user DNs
133
-		if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
134
-			$dns = array();
135
-			$filterParts = array();
136
-			$bytes = 0;
137
-			foreach($members as $mid) {
138
-				$filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
139
-				$filterParts[] = $filter;
140
-				$bytes += strlen($filter);
141
-				if($bytes >= 9000000) {
142
-					// AD has a default input buffer of 10 MB, we do not want
143
-					// to take even the chance to exceed it
144
-					$filter = $this->access->combineFilterWithOr($filterParts);
145
-					$bytes = 0;
146
-					$filterParts = array();
147
-					$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
148
-					$dns = array_merge($dns, $users);
149
-				}
150
-			}
151
-			if(count($filterParts) > 0) {
152
-				$filter = $this->access->combineFilterWithOr($filterParts);
153
-				$users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
154
-				$dns = array_merge($dns, $users);
155
-			}
156
-			$members = $dns;
157
-		}
158
-
159
-		$isInGroup = in_array($userDN, $members);
160
-		$this->access->connection->writeToCache($cacheKey, $isInGroup);
161
-		$this->access->connection->writeToCache($cacheKeyMembers, $members);
162
-		$this->cachedGroupMembers[$gid] = $members;
163
-
164
-		return $isInGroup;
165
-	}
166
-
167
-	/**
168
-	 * @param string $dnGroup
169
-	 * @return array
170
-	 *
171
-	 * For a group that has user membership defined by an LDAP search url attribute returns the users
172
-	 * that match the search url otherwise returns an empty array.
173
-	 */
174
-	public function getDynamicGroupMembers($dnGroup) {
175
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
176
-
177
-		if (empty($dynamicGroupMemberURL)) {
178
-			return array();
179
-		}
180
-
181
-		$dynamicMembers = array();
182
-		$memberURLs = $this->access->readAttribute(
183
-			$dnGroup,
184
-			$dynamicGroupMemberURL,
185
-			$this->access->connection->ldapGroupFilter
186
-		);
187
-		if ($memberURLs !== false) {
188
-			// this group has the 'memberURL' attribute so this is a dynamic group
189
-			// example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
190
-			// example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
191
-			$pos = strpos($memberURLs[0], '(');
192
-			if ($pos !== false) {
193
-				$memberUrlFilter = substr($memberURLs[0], $pos);
194
-				$foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
195
-				$dynamicMembers = array();
196
-				foreach($foundMembers as $value) {
197
-					$dynamicMembers[$value['dn'][0]] = 1;
198
-				}
199
-			} else {
200
-				\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
201
-					'of group ' . $dnGroup, ILogger::DEBUG);
202
-			}
203
-		}
204
-		return $dynamicMembers;
205
-	}
206
-
207
-	/**
208
-	 * @param string $dnGroup
209
-	 * @param array|null &$seen
210
-	 * @return array|mixed|null
211
-	 * @throws \OC\ServerNotAvailableException
212
-	 */
213
-	private function _groupMembers($dnGroup, &$seen = null) {
214
-		if ($seen === null) {
215
-			$seen = array();
216
-		}
217
-		$allMembers = array();
218
-		if (array_key_exists($dnGroup, $seen)) {
219
-			// avoid loops
220
-			return array();
221
-		}
222
-		// used extensively in cron job, caching makes sense for nested groups
223
-		$cacheKey = '_groupMembers'.$dnGroup;
224
-		$groupMembers = $this->access->connection->getFromCache($cacheKey);
225
-		if($groupMembers !== null) {
226
-			return $groupMembers;
227
-		}
228
-		$seen[$dnGroup] = 1;
229
-		$members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
230
-												$this->access->connection->ldapGroupFilter);
231
-		if (is_array($members)) {
232
-			foreach ($members as $member) {
233
-				$allMembers[$member] = 1;
234
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
235
-				if (!empty($nestedGroups)) {
236
-					$subMembers = $this->_groupMembers($member, $seen);
237
-					if ($subMembers) {
238
-						$allMembers += $subMembers;
239
-					}
240
-				}
241
-			}
242
-		}
243
-
244
-		$allMembers += $this->getDynamicGroupMembers($dnGroup);
245
-
246
-		$this->access->connection->writeToCache($cacheKey, $allMembers);
247
-		return $allMembers;
248
-	}
249
-
250
-	/**
251
-	 * @param string $DN
252
-	 * @param array|null &$seen
253
-	 * @return array
254
-	 */
255
-	private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
256
-		if ($seen === null) {
257
-			$seen = array();
258
-		}
259
-		if (array_key_exists($DN, $seen)) {
260
-			// avoid loops
261
-			return array();
262
-		}
263
-		$seen[$DN] = 1;
264
-		$groups = $this->access->readAttribute($DN, 'memberOf');
265
-		if (!is_array($groups)) {
266
-			return array();
267
-		}
268
-		$groups = $this->access->groupsMatchFilter($groups);
269
-		$allGroups =  $groups;
270
-		$nestedGroups = $this->access->connection->ldapNestedGroups;
271
-		if ((int)$nestedGroups === 1) {
272
-			foreach ($groups as $group) {
273
-				$subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
274
-				$allGroups = array_merge($allGroups, $subGroups);
275
-			}
276
-		}
277
-		return $allGroups;
278
-	}
279
-
280
-	/**
281
-	 * translates a gidNumber into an ownCloud internal name
282
-	 * @param string $gid as given by gidNumber on POSIX LDAP
283
-	 * @param string $dn a DN that belongs to the same domain as the group
284
-	 * @return string|bool
285
-	 */
286
-	public function gidNumber2Name($gid, $dn) {
287
-		$cacheKey = 'gidNumberToName' . $gid;
288
-		$groupName = $this->access->connection->getFromCache($cacheKey);
289
-		if(!is_null($groupName) && isset($groupName)) {
290
-			return $groupName;
291
-		}
292
-
293
-		//we need to get the DN from LDAP
294
-		$filter = $this->access->combineFilterWithAnd([
295
-			$this->access->connection->ldapGroupFilter,
296
-			'objectClass=posixGroup',
297
-			$this->access->connection->ldapGidNumber . '=' . $gid
298
-		]);
299
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
300
-		if(empty($result)) {
301
-			return false;
302
-		}
303
-		$dn = $result[0]['dn'][0];
304
-
305
-		//and now the group name
306
-		//NOTE once we have separate ownCloud group IDs and group names we can
307
-		//directly read the display name attribute instead of the DN
308
-		$name = $this->access->dn2groupname($dn);
309
-
310
-		$this->access->connection->writeToCache($cacheKey, $name);
311
-
312
-		return $name;
313
-	}
314
-
315
-	/**
316
-	 * returns the entry's gidNumber
317
-	 * @param string $dn
318
-	 * @param string $attribute
319
-	 * @return string|bool
320
-	 */
321
-	private function getEntryGidNumber($dn, $attribute) {
322
-		$value = $this->access->readAttribute($dn, $attribute);
323
-		if(is_array($value) && !empty($value)) {
324
-			return $value[0];
325
-		}
326
-		return false;
327
-	}
328
-
329
-	/**
330
-	 * returns the group's primary ID
331
-	 * @param string $dn
332
-	 * @return string|bool
333
-	 */
334
-	public function getGroupGidNumber($dn) {
335
-		return $this->getEntryGidNumber($dn, 'gidNumber');
336
-	}
337
-
338
-	/**
339
-	 * returns the user's gidNumber
340
-	 * @param string $dn
341
-	 * @return string|bool
342
-	 */
343
-	public function getUserGidNumber($dn) {
344
-		$gidNumber = false;
345
-		if($this->access->connection->hasGidNumber) {
346
-			$gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
347
-			if($gidNumber === false) {
348
-				$this->access->connection->hasGidNumber = false;
349
-			}
350
-		}
351
-		return $gidNumber;
352
-	}
353
-
354
-	/**
355
-	 * returns a filter for a "users has specific gid" search or count operation
356
-	 *
357
-	 * @param string $groupDN
358
-	 * @param string $search
359
-	 * @return string
360
-	 * @throws \Exception
361
-	 */
362
-	private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
363
-		$groupID = $this->getGroupGidNumber($groupDN);
364
-		if($groupID === false) {
365
-			throw new \Exception('Not a valid group');
366
-		}
367
-
368
-		$filterParts = [];
369
-		$filterParts[] = $this->access->getFilterForUserCount();
370
-		if ($search !== '') {
371
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
372
-		}
373
-		$filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
374
-
375
-		return $this->access->combineFilterWithAnd($filterParts);
376
-	}
377
-
378
-	/**
379
-	 * returns a list of users that have the given group as gid number
380
-	 *
381
-	 * @param string $groupDN
382
-	 * @param string $search
383
-	 * @param int $limit
384
-	 * @param int $offset
385
-	 * @return string[]
386
-	 */
387
-	public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
388
-		try {
389
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
390
-			$users = $this->access->fetchListOfUsers(
391
-				$filter,
392
-				[$this->access->connection->ldapUserDisplayName, 'dn'],
393
-				$limit,
394
-				$offset
395
-			);
396
-			return $this->access->nextcloudUserNames($users);
397
-		} catch (\Exception $e) {
398
-			return [];
399
-		}
400
-	}
401
-
402
-	/**
403
-	 * returns the number of users that have the given group as gid number
404
-	 *
405
-	 * @param string $groupDN
406
-	 * @param string $search
407
-	 * @param int $limit
408
-	 * @param int $offset
409
-	 * @return int
410
-	 */
411
-	public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
412
-		try {
413
-			$filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
414
-			$users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
415
-			return (int)$users;
416
-		} catch (\Exception $e) {
417
-			return 0;
418
-		}
419
-	}
420
-
421
-	/**
422
-	 * gets the gidNumber of a user
423
-	 * @param string $dn
424
-	 * @return string
425
-	 */
426
-	public function getUserGroupByGid($dn) {
427
-		$groupID = $this->getUserGidNumber($dn);
428
-		if($groupID !== false) {
429
-			$groupName = $this->gidNumber2Name($groupID, $dn);
430
-			if($groupName !== false) {
431
-				return $groupName;
432
-			}
433
-		}
434
-
435
-		return false;
436
-	}
437
-
438
-	/**
439
-	 * translates a primary group ID into an Nextcloud internal name
440
-	 * @param string $gid as given by primaryGroupID on AD
441
-	 * @param string $dn a DN that belongs to the same domain as the group
442
-	 * @return string|bool
443
-	 */
444
-	public function primaryGroupID2Name($gid, $dn) {
445
-		$cacheKey = 'primaryGroupIDtoName';
446
-		$groupNames = $this->access->connection->getFromCache($cacheKey);
447
-		if(!is_null($groupNames) && isset($groupNames[$gid])) {
448
-			return $groupNames[$gid];
449
-		}
450
-
451
-		$domainObjectSid = $this->access->getSID($dn);
452
-		if($domainObjectSid === false) {
453
-			return false;
454
-		}
455
-
456
-		//we need to get the DN from LDAP
457
-		$filter = $this->access->combineFilterWithAnd(array(
458
-			$this->access->connection->ldapGroupFilter,
459
-			'objectsid=' . $domainObjectSid . '-' . $gid
460
-		));
461
-		$result = $this->access->searchGroups($filter, array('dn'), 1);
462
-		if(empty($result)) {
463
-			return false;
464
-		}
465
-		$dn = $result[0]['dn'][0];
466
-
467
-		//and now the group name
468
-		//NOTE once we have separate Nextcloud group IDs and group names we can
469
-		//directly read the display name attribute instead of the DN
470
-		$name = $this->access->dn2groupname($dn);
471
-
472
-		$this->access->connection->writeToCache($cacheKey, $name);
473
-
474
-		return $name;
475
-	}
476
-
477
-	/**
478
-	 * returns the entry's primary group ID
479
-	 * @param string $dn
480
-	 * @param string $attribute
481
-	 * @return string|bool
482
-	 */
483
-	private function getEntryGroupID($dn, $attribute) {
484
-		$value = $this->access->readAttribute($dn, $attribute);
485
-		if(is_array($value) && !empty($value)) {
486
-			return $value[0];
487
-		}
488
-		return false;
489
-	}
490
-
491
-	/**
492
-	 * returns the group's primary ID
493
-	 * @param string $dn
494
-	 * @return string|bool
495
-	 */
496
-	public function getGroupPrimaryGroupID($dn) {
497
-		return $this->getEntryGroupID($dn, 'primaryGroupToken');
498
-	}
499
-
500
-	/**
501
-	 * returns the user's primary group ID
502
-	 * @param string $dn
503
-	 * @return string|bool
504
-	 */
505
-	public function getUserPrimaryGroupIDs($dn) {
506
-		$primaryGroupID = false;
507
-		if($this->access->connection->hasPrimaryGroups) {
508
-			$primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
509
-			if($primaryGroupID === false) {
510
-				$this->access->connection->hasPrimaryGroups = false;
511
-			}
512
-		}
513
-		return $primaryGroupID;
514
-	}
515
-
516
-	/**
517
-	 * returns a filter for a "users in primary group" search or count operation
518
-	 *
519
-	 * @param string $groupDN
520
-	 * @param string $search
521
-	 * @return string
522
-	 * @throws \Exception
523
-	 */
524
-	private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
525
-		$groupID = $this->getGroupPrimaryGroupID($groupDN);
526
-		if($groupID === false) {
527
-			throw new \Exception('Not a valid group');
528
-		}
529
-
530
-		$filterParts = [];
531
-		$filterParts[] = $this->access->getFilterForUserCount();
532
-		if ($search !== '') {
533
-			$filterParts[] = $this->access->getFilterPartForUserSearch($search);
534
-		}
535
-		$filterParts[] = 'primaryGroupID=' . $groupID;
536
-
537
-		return $this->access->combineFilterWithAnd($filterParts);
538
-	}
539
-
540
-	/**
541
-	 * returns a list of users that have the given group as primary group
542
-	 *
543
-	 * @param string $groupDN
544
-	 * @param string $search
545
-	 * @param int $limit
546
-	 * @param int $offset
547
-	 * @return string[]
548
-	 */
549
-	public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
550
-		try {
551
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
552
-			$users = $this->access->fetchListOfUsers(
553
-				$filter,
554
-				array($this->access->connection->ldapUserDisplayName, 'dn'),
555
-				$limit,
556
-				$offset
557
-			);
558
-			return $this->access->nextcloudUserNames($users);
559
-		} catch (\Exception $e) {
560
-			return array();
561
-		}
562
-	}
563
-
564
-	/**
565
-	 * returns the number of users that have the given group as primary group
566
-	 *
567
-	 * @param string $groupDN
568
-	 * @param string $search
569
-	 * @param int $limit
570
-	 * @param int $offset
571
-	 * @return int
572
-	 */
573
-	public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
574
-		try {
575
-			$filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576
-			$users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
577
-			return (int)$users;
578
-		} catch (\Exception $e) {
579
-			return 0;
580
-		}
581
-	}
582
-
583
-	/**
584
-	 * gets the primary group of a user
585
-	 * @param string $dn
586
-	 * @return string
587
-	 */
588
-	public function getUserPrimaryGroup($dn) {
589
-		$groupID = $this->getUserPrimaryGroupIDs($dn);
590
-		if($groupID !== false) {
591
-			$groupName = $this->primaryGroupID2Name($groupID, $dn);
592
-			if($groupName !== false) {
593
-				return $groupName;
594
-			}
595
-		}
596
-
597
-		return false;
598
-	}
599
-
600
-	/**
601
-	 * Get all groups a user belongs to
602
-	 * @param string $uid Name of the user
603
-	 * @return array with group names
604
-	 *
605
-	 * This function fetches all groups a user belongs to. It does not check
606
-	 * if the user exists at all.
607
-	 *
608
-	 * This function includes groups based on dynamic group membership.
609
-	 */
610
-	public function getUserGroups($uid) {
611
-		if(!$this->enabled) {
612
-			return array();
613
-		}
614
-		$cacheKey = 'getUserGroups'.$uid;
615
-		$userGroups = $this->access->connection->getFromCache($cacheKey);
616
-		if(!is_null($userGroups)) {
617
-			return $userGroups;
618
-		}
619
-		$userDN = $this->access->username2dn($uid);
620
-		if(!$userDN) {
621
-			$this->access->connection->writeToCache($cacheKey, array());
622
-			return array();
623
-		}
624
-
625
-		$groups = [];
626
-		$primaryGroup = $this->getUserPrimaryGroup($userDN);
627
-		$gidGroupName = $this->getUserGroupByGid($userDN);
628
-
629
-		$dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
630
-
631
-		if (!empty($dynamicGroupMemberURL)) {
632
-			// look through dynamic groups to add them to the result array if needed
633
-			$groupsToMatch = $this->access->fetchListOfGroups(
634
-				$this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
635
-			foreach($groupsToMatch as $dynamicGroup) {
636
-				if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
637
-					continue;
638
-				}
639
-				$pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
640
-				if ($pos !== false) {
641
-					$memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
642
-					// apply filter via ldap search to see if this user is in this
643
-					// dynamic group
644
-					$userMatch = $this->access->readAttribute(
645
-						$userDN,
646
-						$this->access->connection->ldapUserDisplayName,
647
-						$memberUrlFilter
648
-					);
649
-					if ($userMatch !== false) {
650
-						// match found so this user is in this group
651
-						$groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
652
-						if(is_string($groupName)) {
653
-							// be sure to never return false if the dn could not be
654
-							// resolved to a name, for whatever reason.
655
-							$groups[] = $groupName;
656
-						}
657
-					}
658
-				} else {
659
-					\OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
660
-						'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
661
-				}
662
-			}
663
-		}
664
-
665
-		// if possible, read out membership via memberOf. It's far faster than
666
-		// performing a search, which still is a fallback later.
667
-		// memberof doesn't support memberuid, so skip it here.
668
-		if((int)$this->access->connection->hasMemberOfFilterSupport === 1
669
-			&& (int)$this->access->connection->useMemberOfToDetectMembership === 1
670
-		    && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
671
-		    ) {
672
-			$groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
673
-			if (is_array($groupDNs)) {
674
-				foreach ($groupDNs as $dn) {
675
-					$groupName = $this->access->dn2groupname($dn);
676
-					if(is_string($groupName)) {
677
-						// be sure to never return false if the dn could not be
678
-						// resolved to a name, for whatever reason.
679
-						$groups[] = $groupName;
680
-					}
681
-				}
682
-			}
683
-
684
-			if($primaryGroup !== false) {
685
-				$groups[] = $primaryGroup;
686
-			}
687
-			if($gidGroupName !== false) {
688
-				$groups[] = $gidGroupName;
689
-			}
690
-			$this->access->connection->writeToCache($cacheKey, $groups);
691
-			return $groups;
692
-		}
693
-
694
-		//uniqueMember takes DN, memberuid the uid, so we need to distinguish
695
-		if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
696
-			|| (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
697
-		) {
698
-			$uid = $userDN;
699
-		} else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
700
-			$result = $this->access->readAttribute($userDN, 'uid');
701
-			if ($result === false) {
702
-				\OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
703
-					$this->access->connection->ldapHost, ILogger::DEBUG);
704
-			}
705
-			$uid = $result[0];
706
-		} else {
707
-			// just in case
708
-			$uid = $userDN;
709
-		}
710
-
711
-		if(isset($this->cachedGroupsByMember[$uid])) {
712
-			$groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
713
-		} else {
714
-			$groupsByMember = array_values($this->getGroupsByMember($uid));
715
-			$groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
716
-			$this->cachedGroupsByMember[$uid] = $groupsByMember;
717
-			$groups = array_merge($groups, $groupsByMember);
718
-		}
719
-
720
-		if($primaryGroup !== false) {
721
-			$groups[] = $primaryGroup;
722
-		}
723
-		if($gidGroupName !== false) {
724
-			$groups[] = $gidGroupName;
725
-		}
726
-
727
-		$groups = array_unique($groups, SORT_LOCALE_STRING);
728
-		$this->access->connection->writeToCache($cacheKey, $groups);
729
-
730
-		return $groups;
731
-	}
732
-
733
-	/**
734
-	 * @param string $dn
735
-	 * @param array|null &$seen
736
-	 * @return array
737
-	 */
738
-	private function getGroupsByMember($dn, &$seen = null) {
739
-		if ($seen === null) {
740
-			$seen = array();
741
-		}
742
-		$allGroups = array();
743
-		if (array_key_exists($dn, $seen)) {
744
-			// avoid loops
745
-			return array();
746
-		}
747
-		$seen[$dn] = true;
748
-		$filter = $this->access->combineFilterWithAnd(array(
749
-			$this->access->connection->ldapGroupFilter,
750
-			$this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
751
-		));
752
-		$groups = $this->access->fetchListOfGroups($filter,
753
-			array($this->access->connection->ldapGroupDisplayName, 'dn'));
754
-		if (is_array($groups)) {
755
-			foreach ($groups as $groupobj) {
756
-				$groupDN = $groupobj['dn'][0];
757
-				$allGroups[$groupDN] = $groupobj;
758
-				$nestedGroups = $this->access->connection->ldapNestedGroups;
759
-				if (!empty($nestedGroups)) {
760
-					$supergroups = $this->getGroupsByMember($groupDN, $seen);
761
-					if (is_array($supergroups) && (count($supergroups)>0)) {
762
-						$allGroups = array_merge($allGroups, $supergroups);
763
-					}
764
-				}
765
-			}
766
-		}
767
-		return $allGroups;
768
-	}
769
-
770
-	/**
771
-	 * get a list of all users in a group
772
-	 *
773
-	 * @param string $gid
774
-	 * @param string $search
775
-	 * @param int $limit
776
-	 * @param int $offset
777
-	 * @return array with user ids
778
-	 */
779
-	public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
780
-		if(!$this->enabled) {
781
-			return array();
782
-		}
783
-		if(!$this->groupExists($gid)) {
784
-			return array();
785
-		}
786
-		$search = $this->access->escapeFilterPart($search, true);
787
-		$cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
788
-		// check for cache of the exact query
789
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
790
-		if(!is_null($groupUsers)) {
791
-			return $groupUsers;
792
-		}
793
-
794
-		// check for cache of the query without limit and offset
795
-		$groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
796
-		if(!is_null($groupUsers)) {
797
-			$groupUsers = array_slice($groupUsers, $offset, $limit);
798
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
799
-			return $groupUsers;
800
-		}
801
-
802
-		if($limit === -1) {
803
-			$limit = null;
804
-		}
805
-		$groupDN = $this->access->groupname2dn($gid);
806
-		if(!$groupDN) {
807
-			// group couldn't be found, return empty resultset
808
-			$this->access->connection->writeToCache($cacheKey, array());
809
-			return array();
810
-		}
811
-
812
-		$primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
813
-		$posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
814
-		$members = array_keys($this->_groupMembers($groupDN));
815
-		if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
816
-			//in case users could not be retrieved, return empty result set
817
-			$this->access->connection->writeToCache($cacheKey, []);
818
-			return [];
819
-		}
820
-
821
-		$groupUsers = array();
822
-		$isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
823
-		$attrs = $this->access->userManager->getAttributes(true);
824
-		foreach($members as $member) {
825
-			if($isMemberUid) {
826
-				//we got uids, need to get their DNs to 'translate' them to user names
827
-				$filter = $this->access->combineFilterWithAnd(array(
828
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
829
-					$this->access->getFilterPartForUserSearch($search)
830
-				));
831
-				$ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
832
-				if(count($ldap_users) < 1) {
833
-					continue;
834
-				}
835
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
836
-			} else {
837
-				//we got DNs, check if we need to filter by search or we can give back all of them
838
-				if ($search !== '') {
839
-					if(!$this->access->readAttribute($member,
840
-						$this->access->connection->ldapUserDisplayName,
841
-						$this->access->getFilterPartForUserSearch($search))) {
842
-						continue;
843
-					}
844
-				}
845
-				// dn2username will also check if the users belong to the allowed base
846
-				if($ocname = $this->access->dn2username($member)) {
847
-					$groupUsers[] = $ocname;
848
-				}
849
-			}
850
-		}
851
-
852
-		$groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
853
-		natsort($groupUsers);
854
-		$this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
855
-		$groupUsers = array_slice($groupUsers, $offset, $limit);
856
-
857
-		$this->access->connection->writeToCache($cacheKey, $groupUsers);
858
-
859
-		return $groupUsers;
860
-	}
861
-
862
-	/**
863
-	 * returns the number of users in a group, who match the search term
864
-	 * @param string $gid the internal group name
865
-	 * @param string $search optional, a search string
866
-	 * @return int|bool
867
-	 */
868
-	public function countUsersInGroup($gid, $search = '') {
869
-		if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
870
-			return $this->groupPluginManager->countUsersInGroup($gid, $search);
871
-		}
872
-
873
-		$cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
874
-		if(!$this->enabled || !$this->groupExists($gid)) {
875
-			return false;
876
-		}
877
-		$groupUsers = $this->access->connection->getFromCache($cacheKey);
878
-		if(!is_null($groupUsers)) {
879
-			return $groupUsers;
880
-		}
881
-
882
-		$groupDN = $this->access->groupname2dn($gid);
883
-		if(!$groupDN) {
884
-			// group couldn't be found, return empty result set
885
-			$this->access->connection->writeToCache($cacheKey, false);
886
-			return false;
887
-		}
888
-
889
-		$members = array_keys($this->_groupMembers($groupDN));
890
-		$primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
891
-		if(!$members && $primaryUserCount === 0) {
892
-			//in case users could not be retrieved, return empty result set
893
-			$this->access->connection->writeToCache($cacheKey, false);
894
-			return false;
895
-		}
896
-
897
-		if ($search === '') {
898
-			$groupUsers = count($members) + $primaryUserCount;
899
-			$this->access->connection->writeToCache($cacheKey, $groupUsers);
900
-			return $groupUsers;
901
-		}
902
-		$search = $this->access->escapeFilterPart($search, true);
903
-		$isMemberUid =
904
-			(strtolower($this->access->connection->ldapGroupMemberAssocAttr)
905
-			=== 'memberuid');
906
-
907
-		//we need to apply the search filter
908
-		//alternatives that need to be checked:
909
-		//a) get all users by search filter and array_intersect them
910
-		//b) a, but only when less than 1k 10k ?k users like it is
911
-		//c) put all DNs|uids in a LDAP filter, combine with the search string
912
-		//   and let it count.
913
-		//For now this is not important, because the only use of this method
914
-		//does not supply a search string
915
-		$groupUsers = array();
916
-		foreach($members as $member) {
917
-			if($isMemberUid) {
918
-				//we got uids, need to get their DNs to 'translate' them to user names
919
-				$filter = $this->access->combineFilterWithAnd(array(
920
-					str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
921
-					$this->access->getFilterPartForUserSearch($search)
922
-				));
923
-				$ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
924
-				if(count($ldap_users) < 1) {
925
-					continue;
926
-				}
927
-				$groupUsers[] = $this->access->dn2username($ldap_users[0]);
928
-			} else {
929
-				//we need to apply the search filter now
930
-				if(!$this->access->readAttribute($member,
931
-					$this->access->connection->ldapUserDisplayName,
932
-					$this->access->getFilterPartForUserSearch($search))) {
933
-					continue;
934
-				}
935
-				// dn2username will also check if the users belong to the allowed base
936
-				if($ocname = $this->access->dn2username($member)) {
937
-					$groupUsers[] = $ocname;
938
-				}
939
-			}
940
-		}
941
-
942
-		//and get users that have the group as primary
943
-		$primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
944
-
945
-		return count($groupUsers) + $primaryUsers;
946
-	}
947
-
948
-	/**
949
-	 * get a list of all groups
950
-	 *
951
-	 * @param string $search
952
-	 * @param $limit
953
-	 * @param int $offset
954
-	 * @return array with group names
955
-	 *
956
-	 * Returns a list with all groups (used by getGroups)
957
-	 */
958
-	protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
959
-		if(!$this->enabled) {
960
-			return array();
961
-		}
962
-		$cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
963
-
964
-		//Check cache before driving unnecessary searches
965
-		\OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
966
-		$ldap_groups = $this->access->connection->getFromCache($cacheKey);
967
-		if(!is_null($ldap_groups)) {
968
-			return $ldap_groups;
969
-		}
970
-
971
-		// if we'd pass -1 to LDAP search, we'd end up in a Protocol
972
-		// error. With a limit of 0, we get 0 results. So we pass null.
973
-		if($limit <= 0) {
974
-			$limit = null;
975
-		}
976
-		$filter = $this->access->combineFilterWithAnd(array(
977
-			$this->access->connection->ldapGroupFilter,
978
-			$this->access->getFilterPartForGroupSearch($search)
979
-		));
980
-		\OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
981
-		$ldap_groups = $this->access->fetchListOfGroups($filter,
982
-				array($this->access->connection->ldapGroupDisplayName, 'dn'),
983
-				$limit,
984
-				$offset);
985
-		$ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
986
-
987
-		$this->access->connection->writeToCache($cacheKey, $ldap_groups);
988
-		return $ldap_groups;
989
-	}
990
-
991
-	/**
992
-	 * get a list of all groups using a paged search
993
-	 *
994
-	 * @param string $search
995
-	 * @param int $limit
996
-	 * @param int $offset
997
-	 * @return array with group names
998
-	 *
999
-	 * Returns a list with all groups
1000
-	 * Uses a paged search if available to override a
1001
-	 * server side search limit.
1002
-	 * (active directory has a limit of 1000 by default)
1003
-	 */
1004
-	public function getGroups($search = '', $limit = -1, $offset = 0) {
1005
-		if(!$this->enabled) {
1006
-			return array();
1007
-		}
1008
-		$search = $this->access->escapeFilterPart($search, true);
1009
-		$pagingSize = (int)$this->access->connection->ldapPagingSize;
1010
-		if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
1011
-			return $this->getGroupsChunk($search, $limit, $offset);
1012
-		}
1013
-		$maxGroups = 100000; // limit max results (just for safety reasons)
1014
-		if ($limit > -1) {
1015
-		   $overallLimit = min($limit + $offset, $maxGroups);
1016
-		} else {
1017
-		   $overallLimit = $maxGroups;
1018
-		}
1019
-		$chunkOffset = $offset;
1020
-		$allGroups = array();
1021
-		while ($chunkOffset < $overallLimit) {
1022
-			$chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1023
-			$ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1024
-			$nread = count($ldapGroups);
1025
-			\OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
1026
-			if ($nread) {
1027
-				$allGroups = array_merge($allGroups, $ldapGroups);
1028
-				$chunkOffset += $nread;
1029
-			}
1030
-			if ($nread < $chunkLimit) {
1031
-				break;
1032
-			}
1033
-		}
1034
-		return $allGroups;
1035
-	}
1036
-
1037
-	/**
1038
-	 * @param string $group
1039
-	 * @return bool
1040
-	 */
1041
-	public function groupMatchesFilter($group) {
1042
-		return (strripos($group, $this->groupSearch) !== false);
1043
-	}
1044
-
1045
-	/**
1046
-	 * check if a group exists
1047
-	 * @param string $gid
1048
-	 * @return bool
1049
-	 */
1050
-	public function groupExists($gid) {
1051
-		$groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1052
-		if(!is_null($groupExists)) {
1053
-			return (bool)$groupExists;
1054
-		}
1055
-
1056
-		//getting dn, if false the group does not exist. If dn, it may be mapped
1057
-		//only, requires more checking.
1058
-		$dn = $this->access->groupname2dn($gid);
1059
-		if(!$dn) {
1060
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1061
-			return false;
1062
-		}
1063
-
1064
-		//if group really still exists, we will be able to read its objectclass
1065
-		if(!is_array($this->access->readAttribute($dn, ''))) {
1066
-			$this->access->connection->writeToCache('groupExists'.$gid, false);
1067
-			return false;
1068
-		}
1069
-
1070
-		$this->access->connection->writeToCache('groupExists'.$gid, true);
1071
-		return true;
1072
-	}
1073
-
1074
-	/**
1075
-	* Check if backend implements actions
1076
-	* @param int $actions bitwise-or'ed actions
1077
-	* @return boolean
1078
-	*
1079
-	* Returns the supported actions as int to be
1080
-	* compared with GroupInterface::CREATE_GROUP etc.
1081
-	*/
1082
-	public function implementsActions($actions) {
1083
-		return (bool)((GroupInterface::COUNT_USERS |
1084
-				$this->groupPluginManager->getImplementedActions()) & $actions);
1085
-	}
1086
-
1087
-	/**
1088
-	 * Return access for LDAP interaction.
1089
-	 * @return Access instance of Access for LDAP interaction
1090
-	 */
1091
-	public function getLDAPAccess($gid) {
1092
-		return $this->access;
1093
-	}
1094
-
1095
-	/**
1096
-	 * create a group
1097
-	 * @param string $gid
1098
-	 * @return bool
1099
-	 * @throws \Exception
1100
-	 */
1101
-	public function createGroup($gid) {
1102
-		if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1103
-			if ($dn = $this->groupPluginManager->createGroup($gid)) {
1104
-				//updates group mapping
1105
-				$this->access->dn2ocname($dn, $gid, false);
1106
-				$this->access->connection->writeToCache("groupExists".$gid, true);
1107
-			}
1108
-			return $dn != null;
1109
-		}
1110
-		throw new \Exception('Could not create group in LDAP backend.');
1111
-	}
1112
-
1113
-	/**
1114
-	 * delete a group
1115
-	 * @param string $gid gid of the group to delete
1116
-	 * @return bool
1117
-	 * @throws \Exception
1118
-	 */
1119
-	public function deleteGroup($gid) {
1120
-		if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1121
-			if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1122
-				#delete group in nextcloud internal db
1123
-				$this->access->getGroupMapper()->unmap($gid);
1124
-				$this->access->connection->writeToCache("groupExists".$gid, false);
1125
-			}
1126
-			return $ret;
1127
-		}
1128
-		throw new \Exception('Could not delete group in LDAP backend.');
1129
-	}
1130
-
1131
-	/**
1132
-	 * Add a user to a group
1133
-	 * @param string $uid Name of the user to add to group
1134
-	 * @param string $gid Name of the group in which add the user
1135
-	 * @return bool
1136
-	 * @throws \Exception
1137
-	 */
1138
-	public function addToGroup($uid, $gid) {
1139
-		if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1140
-			if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1141
-				$this->access->connection->clearCache();
1142
-			}
1143
-			return $ret;
1144
-		}
1145
-		throw new \Exception('Could not add user to group in LDAP backend.');
1146
-	}
1147
-
1148
-	/**
1149
-	 * Removes a user from a group
1150
-	 * @param string $uid Name of the user to remove from group
1151
-	 * @param string $gid Name of the group from which remove the user
1152
-	 * @return bool
1153
-	 * @throws \Exception
1154
-	 */
1155
-	public function removeFromGroup($uid, $gid) {
1156
-		if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1157
-			if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1158
-				$this->access->connection->clearCache();
1159
-			}
1160
-			return $ret;
1161
-		}
1162
-		throw new \Exception('Could not remove user from group in LDAP backend.');
1163
-	}
1164
-
1165
-	/**
1166
-	 * Gets group details
1167
-	 * @param string $gid Name of the group
1168
-	 * @return array | false
1169
-	 * @throws \Exception
1170
-	 */
1171
-	public function getGroupDetails($gid) {
1172
-		if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1173
-			return $this->groupPluginManager->getGroupDetails($gid);
1174
-		}
1175
-		throw new \Exception('Could not get group details in LDAP backend.');
1176
-	}
1177
-
1178
-	/**
1179
-	 * Return LDAP connection resource from a cloned connection.
1180
-	 * The cloned connection needs to be closed manually.
1181
-	 * of the current access.
1182
-	 * @param string $gid
1183
-	 * @return resource of the LDAP connection
1184
-	 */
1185
-	public function getNewLDAPConnection($gid) {
1186
-		$connection = clone $this->access->getConnection();
1187
-		return $connection->getConnectionResource();
1188
-	}
49
+    protected $enabled = false;
50
+
51
+    /**
52
+     * @var string[] $cachedGroupMembers array of users with gid as key
53
+     */
54
+    protected $cachedGroupMembers;
55
+
56
+    /**
57
+     * @var string[] $cachedGroupsByMember array of groups with uid as key
58
+     */
59
+    protected $cachedGroupsByMember;
60
+
61
+    /** @var GroupPluginManager */
62
+    protected $groupPluginManager;
63
+
64
+    public function __construct(Access $access, GroupPluginManager $groupPluginManager) {
65
+        parent::__construct($access);
66
+        $filter = $this->access->connection->ldapGroupFilter;
67
+        $gassoc = $this->access->connection->ldapGroupMemberAssocAttr;
68
+        if(!empty($filter) && !empty($gassoc)) {
69
+            $this->enabled = true;
70
+        }
71
+
72
+        $this->cachedGroupMembers = new CappedMemoryCache();
73
+        $this->cachedGroupsByMember = new CappedMemoryCache();
74
+        $this->groupPluginManager = $groupPluginManager;
75
+    }
76
+
77
+    /**
78
+     * is user in group?
79
+     * @param string $uid uid of the user
80
+     * @param string $gid gid of the group
81
+     * @return bool
82
+     *
83
+     * Checks whether the user is member of a group or not.
84
+     */
85
+    public function inGroup($uid, $gid) {
86
+        if(!$this->enabled) {
87
+            return false;
88
+        }
89
+        $cacheKey = 'inGroup'.$uid.':'.$gid;
90
+        $inGroup = $this->access->connection->getFromCache($cacheKey);
91
+        if(!is_null($inGroup)) {
92
+            return (bool)$inGroup;
93
+        }
94
+
95
+        $userDN = $this->access->username2dn($uid);
96
+
97
+        if(isset($this->cachedGroupMembers[$gid])) {
98
+            $isInGroup = in_array($userDN, $this->cachedGroupMembers[$gid]);
99
+            return $isInGroup;
100
+        }
101
+
102
+        $cacheKeyMembers = 'inGroup-members:'.$gid;
103
+        $members = $this->access->connection->getFromCache($cacheKeyMembers);
104
+        if(!is_null($members)) {
105
+            $this->cachedGroupMembers[$gid] = $members;
106
+            $isInGroup = in_array($userDN, $members);
107
+            $this->access->connection->writeToCache($cacheKey, $isInGroup);
108
+            return $isInGroup;
109
+        }
110
+
111
+        $groupDN = $this->access->groupname2dn($gid);
112
+        // just in case
113
+        if(!$groupDN || !$userDN) {
114
+            $this->access->connection->writeToCache($cacheKey, false);
115
+            return false;
116
+        }
117
+
118
+        //check primary group first
119
+        if($gid === $this->getUserPrimaryGroup($userDN)) {
120
+            $this->access->connection->writeToCache($cacheKey, true);
121
+            return true;
122
+        }
123
+
124
+        //usually, LDAP attributes are said to be case insensitive. But there are exceptions of course.
125
+        $members = $this->_groupMembers($groupDN);
126
+        $members = array_keys($members); // uids are returned as keys
127
+        if(!is_array($members) || count($members) === 0) {
128
+            $this->access->connection->writeToCache($cacheKey, false);
129
+            return false;
130
+        }
131
+
132
+        //extra work if we don't get back user DNs
133
+        if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
134
+            $dns = array();
135
+            $filterParts = array();
136
+            $bytes = 0;
137
+            foreach($members as $mid) {
138
+                $filter = str_replace('%uid', $mid, $this->access->connection->ldapLoginFilter);
139
+                $filterParts[] = $filter;
140
+                $bytes += strlen($filter);
141
+                if($bytes >= 9000000) {
142
+                    // AD has a default input buffer of 10 MB, we do not want
143
+                    // to take even the chance to exceed it
144
+                    $filter = $this->access->combineFilterWithOr($filterParts);
145
+                    $bytes = 0;
146
+                    $filterParts = array();
147
+                    $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
148
+                    $dns = array_merge($dns, $users);
149
+                }
150
+            }
151
+            if(count($filterParts) > 0) {
152
+                $filter = $this->access->combineFilterWithOr($filterParts);
153
+                $users = $this->access->fetchListOfUsers($filter, 'dn', count($filterParts));
154
+                $dns = array_merge($dns, $users);
155
+            }
156
+            $members = $dns;
157
+        }
158
+
159
+        $isInGroup = in_array($userDN, $members);
160
+        $this->access->connection->writeToCache($cacheKey, $isInGroup);
161
+        $this->access->connection->writeToCache($cacheKeyMembers, $members);
162
+        $this->cachedGroupMembers[$gid] = $members;
163
+
164
+        return $isInGroup;
165
+    }
166
+
167
+    /**
168
+     * @param string $dnGroup
169
+     * @return array
170
+     *
171
+     * For a group that has user membership defined by an LDAP search url attribute returns the users
172
+     * that match the search url otherwise returns an empty array.
173
+     */
174
+    public function getDynamicGroupMembers($dnGroup) {
175
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
176
+
177
+        if (empty($dynamicGroupMemberURL)) {
178
+            return array();
179
+        }
180
+
181
+        $dynamicMembers = array();
182
+        $memberURLs = $this->access->readAttribute(
183
+            $dnGroup,
184
+            $dynamicGroupMemberURL,
185
+            $this->access->connection->ldapGroupFilter
186
+        );
187
+        if ($memberURLs !== false) {
188
+            // this group has the 'memberURL' attribute so this is a dynamic group
189
+            // example 1: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(o=HeadOffice)
190
+            // example 2: ldap:///cn=users,cn=accounts,dc=dcsubbase,dc=dcbase??one?(&(o=HeadOffice)(uidNumber>=500))
191
+            $pos = strpos($memberURLs[0], '(');
192
+            if ($pos !== false) {
193
+                $memberUrlFilter = substr($memberURLs[0], $pos);
194
+                $foundMembers = $this->access->searchUsers($memberUrlFilter,'dn');
195
+                $dynamicMembers = array();
196
+                foreach($foundMembers as $value) {
197
+                    $dynamicMembers[$value['dn'][0]] = 1;
198
+                }
199
+            } else {
200
+                \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
201
+                    'of group ' . $dnGroup, ILogger::DEBUG);
202
+            }
203
+        }
204
+        return $dynamicMembers;
205
+    }
206
+
207
+    /**
208
+     * @param string $dnGroup
209
+     * @param array|null &$seen
210
+     * @return array|mixed|null
211
+     * @throws \OC\ServerNotAvailableException
212
+     */
213
+    private function _groupMembers($dnGroup, &$seen = null) {
214
+        if ($seen === null) {
215
+            $seen = array();
216
+        }
217
+        $allMembers = array();
218
+        if (array_key_exists($dnGroup, $seen)) {
219
+            // avoid loops
220
+            return array();
221
+        }
222
+        // used extensively in cron job, caching makes sense for nested groups
223
+        $cacheKey = '_groupMembers'.$dnGroup;
224
+        $groupMembers = $this->access->connection->getFromCache($cacheKey);
225
+        if($groupMembers !== null) {
226
+            return $groupMembers;
227
+        }
228
+        $seen[$dnGroup] = 1;
229
+        $members = $this->access->readAttribute($dnGroup, $this->access->connection->ldapGroupMemberAssocAttr,
230
+                                                $this->access->connection->ldapGroupFilter);
231
+        if (is_array($members)) {
232
+            foreach ($members as $member) {
233
+                $allMembers[$member] = 1;
234
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
235
+                if (!empty($nestedGroups)) {
236
+                    $subMembers = $this->_groupMembers($member, $seen);
237
+                    if ($subMembers) {
238
+                        $allMembers += $subMembers;
239
+                    }
240
+                }
241
+            }
242
+        }
243
+
244
+        $allMembers += $this->getDynamicGroupMembers($dnGroup);
245
+
246
+        $this->access->connection->writeToCache($cacheKey, $allMembers);
247
+        return $allMembers;
248
+    }
249
+
250
+    /**
251
+     * @param string $DN
252
+     * @param array|null &$seen
253
+     * @return array
254
+     */
255
+    private function _getGroupDNsFromMemberOf($DN, &$seen = null) {
256
+        if ($seen === null) {
257
+            $seen = array();
258
+        }
259
+        if (array_key_exists($DN, $seen)) {
260
+            // avoid loops
261
+            return array();
262
+        }
263
+        $seen[$DN] = 1;
264
+        $groups = $this->access->readAttribute($DN, 'memberOf');
265
+        if (!is_array($groups)) {
266
+            return array();
267
+        }
268
+        $groups = $this->access->groupsMatchFilter($groups);
269
+        $allGroups =  $groups;
270
+        $nestedGroups = $this->access->connection->ldapNestedGroups;
271
+        if ((int)$nestedGroups === 1) {
272
+            foreach ($groups as $group) {
273
+                $subGroups = $this->_getGroupDNsFromMemberOf($group, $seen);
274
+                $allGroups = array_merge($allGroups, $subGroups);
275
+            }
276
+        }
277
+        return $allGroups;
278
+    }
279
+
280
+    /**
281
+     * translates a gidNumber into an ownCloud internal name
282
+     * @param string $gid as given by gidNumber on POSIX LDAP
283
+     * @param string $dn a DN that belongs to the same domain as the group
284
+     * @return string|bool
285
+     */
286
+    public function gidNumber2Name($gid, $dn) {
287
+        $cacheKey = 'gidNumberToName' . $gid;
288
+        $groupName = $this->access->connection->getFromCache($cacheKey);
289
+        if(!is_null($groupName) && isset($groupName)) {
290
+            return $groupName;
291
+        }
292
+
293
+        //we need to get the DN from LDAP
294
+        $filter = $this->access->combineFilterWithAnd([
295
+            $this->access->connection->ldapGroupFilter,
296
+            'objectClass=posixGroup',
297
+            $this->access->connection->ldapGidNumber . '=' . $gid
298
+        ]);
299
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
300
+        if(empty($result)) {
301
+            return false;
302
+        }
303
+        $dn = $result[0]['dn'][0];
304
+
305
+        //and now the group name
306
+        //NOTE once we have separate ownCloud group IDs and group names we can
307
+        //directly read the display name attribute instead of the DN
308
+        $name = $this->access->dn2groupname($dn);
309
+
310
+        $this->access->connection->writeToCache($cacheKey, $name);
311
+
312
+        return $name;
313
+    }
314
+
315
+    /**
316
+     * returns the entry's gidNumber
317
+     * @param string $dn
318
+     * @param string $attribute
319
+     * @return string|bool
320
+     */
321
+    private function getEntryGidNumber($dn, $attribute) {
322
+        $value = $this->access->readAttribute($dn, $attribute);
323
+        if(is_array($value) && !empty($value)) {
324
+            return $value[0];
325
+        }
326
+        return false;
327
+    }
328
+
329
+    /**
330
+     * returns the group's primary ID
331
+     * @param string $dn
332
+     * @return string|bool
333
+     */
334
+    public function getGroupGidNumber($dn) {
335
+        return $this->getEntryGidNumber($dn, 'gidNumber');
336
+    }
337
+
338
+    /**
339
+     * returns the user's gidNumber
340
+     * @param string $dn
341
+     * @return string|bool
342
+     */
343
+    public function getUserGidNumber($dn) {
344
+        $gidNumber = false;
345
+        if($this->access->connection->hasGidNumber) {
346
+            $gidNumber = $this->getEntryGidNumber($dn, $this->access->connection->ldapGidNumber);
347
+            if($gidNumber === false) {
348
+                $this->access->connection->hasGidNumber = false;
349
+            }
350
+        }
351
+        return $gidNumber;
352
+    }
353
+
354
+    /**
355
+     * returns a filter for a "users has specific gid" search or count operation
356
+     *
357
+     * @param string $groupDN
358
+     * @param string $search
359
+     * @return string
360
+     * @throws \Exception
361
+     */
362
+    private function prepareFilterForUsersHasGidNumber($groupDN, $search = '') {
363
+        $groupID = $this->getGroupGidNumber($groupDN);
364
+        if($groupID === false) {
365
+            throw new \Exception('Not a valid group');
366
+        }
367
+
368
+        $filterParts = [];
369
+        $filterParts[] = $this->access->getFilterForUserCount();
370
+        if ($search !== '') {
371
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
372
+        }
373
+        $filterParts[] = $this->access->connection->ldapGidNumber .'=' . $groupID;
374
+
375
+        return $this->access->combineFilterWithAnd($filterParts);
376
+    }
377
+
378
+    /**
379
+     * returns a list of users that have the given group as gid number
380
+     *
381
+     * @param string $groupDN
382
+     * @param string $search
383
+     * @param int $limit
384
+     * @param int $offset
385
+     * @return string[]
386
+     */
387
+    public function getUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
388
+        try {
389
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
390
+            $users = $this->access->fetchListOfUsers(
391
+                $filter,
392
+                [$this->access->connection->ldapUserDisplayName, 'dn'],
393
+                $limit,
394
+                $offset
395
+            );
396
+            return $this->access->nextcloudUserNames($users);
397
+        } catch (\Exception $e) {
398
+            return [];
399
+        }
400
+    }
401
+
402
+    /**
403
+     * returns the number of users that have the given group as gid number
404
+     *
405
+     * @param string $groupDN
406
+     * @param string $search
407
+     * @param int $limit
408
+     * @param int $offset
409
+     * @return int
410
+     */
411
+    public function countUsersInGidNumber($groupDN, $search = '', $limit = -1, $offset = 0) {
412
+        try {
413
+            $filter = $this->prepareFilterForUsersHasGidNumber($groupDN, $search);
414
+            $users = $this->access->countUsers($filter, ['dn'], $limit, $offset);
415
+            return (int)$users;
416
+        } catch (\Exception $e) {
417
+            return 0;
418
+        }
419
+    }
420
+
421
+    /**
422
+     * gets the gidNumber of a user
423
+     * @param string $dn
424
+     * @return string
425
+     */
426
+    public function getUserGroupByGid($dn) {
427
+        $groupID = $this->getUserGidNumber($dn);
428
+        if($groupID !== false) {
429
+            $groupName = $this->gidNumber2Name($groupID, $dn);
430
+            if($groupName !== false) {
431
+                return $groupName;
432
+            }
433
+        }
434
+
435
+        return false;
436
+    }
437
+
438
+    /**
439
+     * translates a primary group ID into an Nextcloud internal name
440
+     * @param string $gid as given by primaryGroupID on AD
441
+     * @param string $dn a DN that belongs to the same domain as the group
442
+     * @return string|bool
443
+     */
444
+    public function primaryGroupID2Name($gid, $dn) {
445
+        $cacheKey = 'primaryGroupIDtoName';
446
+        $groupNames = $this->access->connection->getFromCache($cacheKey);
447
+        if(!is_null($groupNames) && isset($groupNames[$gid])) {
448
+            return $groupNames[$gid];
449
+        }
450
+
451
+        $domainObjectSid = $this->access->getSID($dn);
452
+        if($domainObjectSid === false) {
453
+            return false;
454
+        }
455
+
456
+        //we need to get the DN from LDAP
457
+        $filter = $this->access->combineFilterWithAnd(array(
458
+            $this->access->connection->ldapGroupFilter,
459
+            'objectsid=' . $domainObjectSid . '-' . $gid
460
+        ));
461
+        $result = $this->access->searchGroups($filter, array('dn'), 1);
462
+        if(empty($result)) {
463
+            return false;
464
+        }
465
+        $dn = $result[0]['dn'][0];
466
+
467
+        //and now the group name
468
+        //NOTE once we have separate Nextcloud group IDs and group names we can
469
+        //directly read the display name attribute instead of the DN
470
+        $name = $this->access->dn2groupname($dn);
471
+
472
+        $this->access->connection->writeToCache($cacheKey, $name);
473
+
474
+        return $name;
475
+    }
476
+
477
+    /**
478
+     * returns the entry's primary group ID
479
+     * @param string $dn
480
+     * @param string $attribute
481
+     * @return string|bool
482
+     */
483
+    private function getEntryGroupID($dn, $attribute) {
484
+        $value = $this->access->readAttribute($dn, $attribute);
485
+        if(is_array($value) && !empty($value)) {
486
+            return $value[0];
487
+        }
488
+        return false;
489
+    }
490
+
491
+    /**
492
+     * returns the group's primary ID
493
+     * @param string $dn
494
+     * @return string|bool
495
+     */
496
+    public function getGroupPrimaryGroupID($dn) {
497
+        return $this->getEntryGroupID($dn, 'primaryGroupToken');
498
+    }
499
+
500
+    /**
501
+     * returns the user's primary group ID
502
+     * @param string $dn
503
+     * @return string|bool
504
+     */
505
+    public function getUserPrimaryGroupIDs($dn) {
506
+        $primaryGroupID = false;
507
+        if($this->access->connection->hasPrimaryGroups) {
508
+            $primaryGroupID = $this->getEntryGroupID($dn, 'primaryGroupID');
509
+            if($primaryGroupID === false) {
510
+                $this->access->connection->hasPrimaryGroups = false;
511
+            }
512
+        }
513
+        return $primaryGroupID;
514
+    }
515
+
516
+    /**
517
+     * returns a filter for a "users in primary group" search or count operation
518
+     *
519
+     * @param string $groupDN
520
+     * @param string $search
521
+     * @return string
522
+     * @throws \Exception
523
+     */
524
+    private function prepareFilterForUsersInPrimaryGroup($groupDN, $search = '') {
525
+        $groupID = $this->getGroupPrimaryGroupID($groupDN);
526
+        if($groupID === false) {
527
+            throw new \Exception('Not a valid group');
528
+        }
529
+
530
+        $filterParts = [];
531
+        $filterParts[] = $this->access->getFilterForUserCount();
532
+        if ($search !== '') {
533
+            $filterParts[] = $this->access->getFilterPartForUserSearch($search);
534
+        }
535
+        $filterParts[] = 'primaryGroupID=' . $groupID;
536
+
537
+        return $this->access->combineFilterWithAnd($filterParts);
538
+    }
539
+
540
+    /**
541
+     * returns a list of users that have the given group as primary group
542
+     *
543
+     * @param string $groupDN
544
+     * @param string $search
545
+     * @param int $limit
546
+     * @param int $offset
547
+     * @return string[]
548
+     */
549
+    public function getUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
550
+        try {
551
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
552
+            $users = $this->access->fetchListOfUsers(
553
+                $filter,
554
+                array($this->access->connection->ldapUserDisplayName, 'dn'),
555
+                $limit,
556
+                $offset
557
+            );
558
+            return $this->access->nextcloudUserNames($users);
559
+        } catch (\Exception $e) {
560
+            return array();
561
+        }
562
+    }
563
+
564
+    /**
565
+     * returns the number of users that have the given group as primary group
566
+     *
567
+     * @param string $groupDN
568
+     * @param string $search
569
+     * @param int $limit
570
+     * @param int $offset
571
+     * @return int
572
+     */
573
+    public function countUsersInPrimaryGroup($groupDN, $search = '', $limit = -1, $offset = 0) {
574
+        try {
575
+            $filter = $this->prepareFilterForUsersInPrimaryGroup($groupDN, $search);
576
+            $users = $this->access->countUsers($filter, array('dn'), $limit, $offset);
577
+            return (int)$users;
578
+        } catch (\Exception $e) {
579
+            return 0;
580
+        }
581
+    }
582
+
583
+    /**
584
+     * gets the primary group of a user
585
+     * @param string $dn
586
+     * @return string
587
+     */
588
+    public function getUserPrimaryGroup($dn) {
589
+        $groupID = $this->getUserPrimaryGroupIDs($dn);
590
+        if($groupID !== false) {
591
+            $groupName = $this->primaryGroupID2Name($groupID, $dn);
592
+            if($groupName !== false) {
593
+                return $groupName;
594
+            }
595
+        }
596
+
597
+        return false;
598
+    }
599
+
600
+    /**
601
+     * Get all groups a user belongs to
602
+     * @param string $uid Name of the user
603
+     * @return array with group names
604
+     *
605
+     * This function fetches all groups a user belongs to. It does not check
606
+     * if the user exists at all.
607
+     *
608
+     * This function includes groups based on dynamic group membership.
609
+     */
610
+    public function getUserGroups($uid) {
611
+        if(!$this->enabled) {
612
+            return array();
613
+        }
614
+        $cacheKey = 'getUserGroups'.$uid;
615
+        $userGroups = $this->access->connection->getFromCache($cacheKey);
616
+        if(!is_null($userGroups)) {
617
+            return $userGroups;
618
+        }
619
+        $userDN = $this->access->username2dn($uid);
620
+        if(!$userDN) {
621
+            $this->access->connection->writeToCache($cacheKey, array());
622
+            return array();
623
+        }
624
+
625
+        $groups = [];
626
+        $primaryGroup = $this->getUserPrimaryGroup($userDN);
627
+        $gidGroupName = $this->getUserGroupByGid($userDN);
628
+
629
+        $dynamicGroupMemberURL = strtolower($this->access->connection->ldapDynamicGroupMemberURL);
630
+
631
+        if (!empty($dynamicGroupMemberURL)) {
632
+            // look through dynamic groups to add them to the result array if needed
633
+            $groupsToMatch = $this->access->fetchListOfGroups(
634
+                $this->access->connection->ldapGroupFilter,array('dn',$dynamicGroupMemberURL));
635
+            foreach($groupsToMatch as $dynamicGroup) {
636
+                if (!array_key_exists($dynamicGroupMemberURL, $dynamicGroup)) {
637
+                    continue;
638
+                }
639
+                $pos = strpos($dynamicGroup[$dynamicGroupMemberURL][0], '(');
640
+                if ($pos !== false) {
641
+                    $memberUrlFilter = substr($dynamicGroup[$dynamicGroupMemberURL][0],$pos);
642
+                    // apply filter via ldap search to see if this user is in this
643
+                    // dynamic group
644
+                    $userMatch = $this->access->readAttribute(
645
+                        $userDN,
646
+                        $this->access->connection->ldapUserDisplayName,
647
+                        $memberUrlFilter
648
+                    );
649
+                    if ($userMatch !== false) {
650
+                        // match found so this user is in this group
651
+                        $groupName = $this->access->dn2groupname($dynamicGroup['dn'][0]);
652
+                        if(is_string($groupName)) {
653
+                            // be sure to never return false if the dn could not be
654
+                            // resolved to a name, for whatever reason.
655
+                            $groups[] = $groupName;
656
+                        }
657
+                    }
658
+                } else {
659
+                    \OCP\Util::writeLog('user_ldap', 'No search filter found on member url '.
660
+                        'of group ' . print_r($dynamicGroup, true), ILogger::DEBUG);
661
+                }
662
+            }
663
+        }
664
+
665
+        // if possible, read out membership via memberOf. It's far faster than
666
+        // performing a search, which still is a fallback later.
667
+        // memberof doesn't support memberuid, so skip it here.
668
+        if((int)$this->access->connection->hasMemberOfFilterSupport === 1
669
+            && (int)$this->access->connection->useMemberOfToDetectMembership === 1
670
+            && strtolower($this->access->connection->ldapGroupMemberAssocAttr) !== 'memberuid'
671
+            ) {
672
+            $groupDNs = $this->_getGroupDNsFromMemberOf($userDN);
673
+            if (is_array($groupDNs)) {
674
+                foreach ($groupDNs as $dn) {
675
+                    $groupName = $this->access->dn2groupname($dn);
676
+                    if(is_string($groupName)) {
677
+                        // be sure to never return false if the dn could not be
678
+                        // resolved to a name, for whatever reason.
679
+                        $groups[] = $groupName;
680
+                    }
681
+                }
682
+            }
683
+
684
+            if($primaryGroup !== false) {
685
+                $groups[] = $primaryGroup;
686
+            }
687
+            if($gidGroupName !== false) {
688
+                $groups[] = $gidGroupName;
689
+            }
690
+            $this->access->connection->writeToCache($cacheKey, $groups);
691
+            return $groups;
692
+        }
693
+
694
+        //uniqueMember takes DN, memberuid the uid, so we need to distinguish
695
+        if((strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'uniquemember')
696
+            || (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'member')
697
+        ) {
698
+            $uid = $userDN;
699
+        } else if(strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid') {
700
+            $result = $this->access->readAttribute($userDN, 'uid');
701
+            if ($result === false) {
702
+                \OCP\Util::writeLog('user_ldap', 'No uid attribute found for DN ' . $userDN . ' on '.
703
+                    $this->access->connection->ldapHost, ILogger::DEBUG);
704
+            }
705
+            $uid = $result[0];
706
+        } else {
707
+            // just in case
708
+            $uid = $userDN;
709
+        }
710
+
711
+        if(isset($this->cachedGroupsByMember[$uid])) {
712
+            $groups = array_merge($groups, $this->cachedGroupsByMember[$uid]);
713
+        } else {
714
+            $groupsByMember = array_values($this->getGroupsByMember($uid));
715
+            $groupsByMember = $this->access->nextcloudGroupNames($groupsByMember);
716
+            $this->cachedGroupsByMember[$uid] = $groupsByMember;
717
+            $groups = array_merge($groups, $groupsByMember);
718
+        }
719
+
720
+        if($primaryGroup !== false) {
721
+            $groups[] = $primaryGroup;
722
+        }
723
+        if($gidGroupName !== false) {
724
+            $groups[] = $gidGroupName;
725
+        }
726
+
727
+        $groups = array_unique($groups, SORT_LOCALE_STRING);
728
+        $this->access->connection->writeToCache($cacheKey, $groups);
729
+
730
+        return $groups;
731
+    }
732
+
733
+    /**
734
+     * @param string $dn
735
+     * @param array|null &$seen
736
+     * @return array
737
+     */
738
+    private function getGroupsByMember($dn, &$seen = null) {
739
+        if ($seen === null) {
740
+            $seen = array();
741
+        }
742
+        $allGroups = array();
743
+        if (array_key_exists($dn, $seen)) {
744
+            // avoid loops
745
+            return array();
746
+        }
747
+        $seen[$dn] = true;
748
+        $filter = $this->access->combineFilterWithAnd(array(
749
+            $this->access->connection->ldapGroupFilter,
750
+            $this->access->connection->ldapGroupMemberAssocAttr.'='.$dn
751
+        ));
752
+        $groups = $this->access->fetchListOfGroups($filter,
753
+            array($this->access->connection->ldapGroupDisplayName, 'dn'));
754
+        if (is_array($groups)) {
755
+            foreach ($groups as $groupobj) {
756
+                $groupDN = $groupobj['dn'][0];
757
+                $allGroups[$groupDN] = $groupobj;
758
+                $nestedGroups = $this->access->connection->ldapNestedGroups;
759
+                if (!empty($nestedGroups)) {
760
+                    $supergroups = $this->getGroupsByMember($groupDN, $seen);
761
+                    if (is_array($supergroups) && (count($supergroups)>0)) {
762
+                        $allGroups = array_merge($allGroups, $supergroups);
763
+                    }
764
+                }
765
+            }
766
+        }
767
+        return $allGroups;
768
+    }
769
+
770
+    /**
771
+     * get a list of all users in a group
772
+     *
773
+     * @param string $gid
774
+     * @param string $search
775
+     * @param int $limit
776
+     * @param int $offset
777
+     * @return array with user ids
778
+     */
779
+    public function usersInGroup($gid, $search = '', $limit = -1, $offset = 0) {
780
+        if(!$this->enabled) {
781
+            return array();
782
+        }
783
+        if(!$this->groupExists($gid)) {
784
+            return array();
785
+        }
786
+        $search = $this->access->escapeFilterPart($search, true);
787
+        $cacheKey = 'usersInGroup-'.$gid.'-'.$search.'-'.$limit.'-'.$offset;
788
+        // check for cache of the exact query
789
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
790
+        if(!is_null($groupUsers)) {
791
+            return $groupUsers;
792
+        }
793
+
794
+        // check for cache of the query without limit and offset
795
+        $groupUsers = $this->access->connection->getFromCache('usersInGroup-'.$gid.'-'.$search);
796
+        if(!is_null($groupUsers)) {
797
+            $groupUsers = array_slice($groupUsers, $offset, $limit);
798
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
799
+            return $groupUsers;
800
+        }
801
+
802
+        if($limit === -1) {
803
+            $limit = null;
804
+        }
805
+        $groupDN = $this->access->groupname2dn($gid);
806
+        if(!$groupDN) {
807
+            // group couldn't be found, return empty resultset
808
+            $this->access->connection->writeToCache($cacheKey, array());
809
+            return array();
810
+        }
811
+
812
+        $primaryUsers = $this->getUsersInPrimaryGroup($groupDN, $search, $limit, $offset);
813
+        $posixGroupUsers = $this->getUsersInGidNumber($groupDN, $search, $limit, $offset);
814
+        $members = array_keys($this->_groupMembers($groupDN));
815
+        if(!$members && empty($posixGroupUsers) && empty($primaryUsers)) {
816
+            //in case users could not be retrieved, return empty result set
817
+            $this->access->connection->writeToCache($cacheKey, []);
818
+            return [];
819
+        }
820
+
821
+        $groupUsers = array();
822
+        $isMemberUid = (strtolower($this->access->connection->ldapGroupMemberAssocAttr) === 'memberuid');
823
+        $attrs = $this->access->userManager->getAttributes(true);
824
+        foreach($members as $member) {
825
+            if($isMemberUid) {
826
+                //we got uids, need to get their DNs to 'translate' them to user names
827
+                $filter = $this->access->combineFilterWithAnd(array(
828
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
829
+                    $this->access->getFilterPartForUserSearch($search)
830
+                ));
831
+                $ldap_users = $this->access->fetchListOfUsers($filter, $attrs, 1);
832
+                if(count($ldap_users) < 1) {
833
+                    continue;
834
+                }
835
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]['dn'][0]);
836
+            } else {
837
+                //we got DNs, check if we need to filter by search or we can give back all of them
838
+                if ($search !== '') {
839
+                    if(!$this->access->readAttribute($member,
840
+                        $this->access->connection->ldapUserDisplayName,
841
+                        $this->access->getFilterPartForUserSearch($search))) {
842
+                        continue;
843
+                    }
844
+                }
845
+                // dn2username will also check if the users belong to the allowed base
846
+                if($ocname = $this->access->dn2username($member)) {
847
+                    $groupUsers[] = $ocname;
848
+                }
849
+            }
850
+        }
851
+
852
+        $groupUsers = array_unique(array_merge($groupUsers, $primaryUsers, $posixGroupUsers));
853
+        natsort($groupUsers);
854
+        $this->access->connection->writeToCache('usersInGroup-'.$gid.'-'.$search, $groupUsers);
855
+        $groupUsers = array_slice($groupUsers, $offset, $limit);
856
+
857
+        $this->access->connection->writeToCache($cacheKey, $groupUsers);
858
+
859
+        return $groupUsers;
860
+    }
861
+
862
+    /**
863
+     * returns the number of users in a group, who match the search term
864
+     * @param string $gid the internal group name
865
+     * @param string $search optional, a search string
866
+     * @return int|bool
867
+     */
868
+    public function countUsersInGroup($gid, $search = '') {
869
+        if ($this->groupPluginManager->implementsActions(GroupInterface::COUNT_USERS)) {
870
+            return $this->groupPluginManager->countUsersInGroup($gid, $search);
871
+        }
872
+
873
+        $cacheKey = 'countUsersInGroup-'.$gid.'-'.$search;
874
+        if(!$this->enabled || !$this->groupExists($gid)) {
875
+            return false;
876
+        }
877
+        $groupUsers = $this->access->connection->getFromCache($cacheKey);
878
+        if(!is_null($groupUsers)) {
879
+            return $groupUsers;
880
+        }
881
+
882
+        $groupDN = $this->access->groupname2dn($gid);
883
+        if(!$groupDN) {
884
+            // group couldn't be found, return empty result set
885
+            $this->access->connection->writeToCache($cacheKey, false);
886
+            return false;
887
+        }
888
+
889
+        $members = array_keys($this->_groupMembers($groupDN));
890
+        $primaryUserCount = $this->countUsersInPrimaryGroup($groupDN, '');
891
+        if(!$members && $primaryUserCount === 0) {
892
+            //in case users could not be retrieved, return empty result set
893
+            $this->access->connection->writeToCache($cacheKey, false);
894
+            return false;
895
+        }
896
+
897
+        if ($search === '') {
898
+            $groupUsers = count($members) + $primaryUserCount;
899
+            $this->access->connection->writeToCache($cacheKey, $groupUsers);
900
+            return $groupUsers;
901
+        }
902
+        $search = $this->access->escapeFilterPart($search, true);
903
+        $isMemberUid =
904
+            (strtolower($this->access->connection->ldapGroupMemberAssocAttr)
905
+            === 'memberuid');
906
+
907
+        //we need to apply the search filter
908
+        //alternatives that need to be checked:
909
+        //a) get all users by search filter and array_intersect them
910
+        //b) a, but only when less than 1k 10k ?k users like it is
911
+        //c) put all DNs|uids in a LDAP filter, combine with the search string
912
+        //   and let it count.
913
+        //For now this is not important, because the only use of this method
914
+        //does not supply a search string
915
+        $groupUsers = array();
916
+        foreach($members as $member) {
917
+            if($isMemberUid) {
918
+                //we got uids, need to get their DNs to 'translate' them to user names
919
+                $filter = $this->access->combineFilterWithAnd(array(
920
+                    str_replace('%uid', $member, $this->access->connection->ldapLoginFilter),
921
+                    $this->access->getFilterPartForUserSearch($search)
922
+                ));
923
+                $ldap_users = $this->access->fetchListOfUsers($filter, 'dn', 1);
924
+                if(count($ldap_users) < 1) {
925
+                    continue;
926
+                }
927
+                $groupUsers[] = $this->access->dn2username($ldap_users[0]);
928
+            } else {
929
+                //we need to apply the search filter now
930
+                if(!$this->access->readAttribute($member,
931
+                    $this->access->connection->ldapUserDisplayName,
932
+                    $this->access->getFilterPartForUserSearch($search))) {
933
+                    continue;
934
+                }
935
+                // dn2username will also check if the users belong to the allowed base
936
+                if($ocname = $this->access->dn2username($member)) {
937
+                    $groupUsers[] = $ocname;
938
+                }
939
+            }
940
+        }
941
+
942
+        //and get users that have the group as primary
943
+        $primaryUsers = $this->countUsersInPrimaryGroup($groupDN, $search);
944
+
945
+        return count($groupUsers) + $primaryUsers;
946
+    }
947
+
948
+    /**
949
+     * get a list of all groups
950
+     *
951
+     * @param string $search
952
+     * @param $limit
953
+     * @param int $offset
954
+     * @return array with group names
955
+     *
956
+     * Returns a list with all groups (used by getGroups)
957
+     */
958
+    protected function getGroupsChunk($search = '', $limit = -1, $offset = 0) {
959
+        if(!$this->enabled) {
960
+            return array();
961
+        }
962
+        $cacheKey = 'getGroups-'.$search.'-'.$limit.'-'.$offset;
963
+
964
+        //Check cache before driving unnecessary searches
965
+        \OCP\Util::writeLog('user_ldap', 'getGroups '.$cacheKey, ILogger::DEBUG);
966
+        $ldap_groups = $this->access->connection->getFromCache($cacheKey);
967
+        if(!is_null($ldap_groups)) {
968
+            return $ldap_groups;
969
+        }
970
+
971
+        // if we'd pass -1 to LDAP search, we'd end up in a Protocol
972
+        // error. With a limit of 0, we get 0 results. So we pass null.
973
+        if($limit <= 0) {
974
+            $limit = null;
975
+        }
976
+        $filter = $this->access->combineFilterWithAnd(array(
977
+            $this->access->connection->ldapGroupFilter,
978
+            $this->access->getFilterPartForGroupSearch($search)
979
+        ));
980
+        \OCP\Util::writeLog('user_ldap', 'getGroups Filter '.$filter, ILogger::DEBUG);
981
+        $ldap_groups = $this->access->fetchListOfGroups($filter,
982
+                array($this->access->connection->ldapGroupDisplayName, 'dn'),
983
+                $limit,
984
+                $offset);
985
+        $ldap_groups = $this->access->nextcloudGroupNames($ldap_groups);
986
+
987
+        $this->access->connection->writeToCache($cacheKey, $ldap_groups);
988
+        return $ldap_groups;
989
+    }
990
+
991
+    /**
992
+     * get a list of all groups using a paged search
993
+     *
994
+     * @param string $search
995
+     * @param int $limit
996
+     * @param int $offset
997
+     * @return array with group names
998
+     *
999
+     * Returns a list with all groups
1000
+     * Uses a paged search if available to override a
1001
+     * server side search limit.
1002
+     * (active directory has a limit of 1000 by default)
1003
+     */
1004
+    public function getGroups($search = '', $limit = -1, $offset = 0) {
1005
+        if(!$this->enabled) {
1006
+            return array();
1007
+        }
1008
+        $search = $this->access->escapeFilterPart($search, true);
1009
+        $pagingSize = (int)$this->access->connection->ldapPagingSize;
1010
+        if (!$this->access->connection->hasPagedResultSupport || $pagingSize <= 0) {
1011
+            return $this->getGroupsChunk($search, $limit, $offset);
1012
+        }
1013
+        $maxGroups = 100000; // limit max results (just for safety reasons)
1014
+        if ($limit > -1) {
1015
+            $overallLimit = min($limit + $offset, $maxGroups);
1016
+        } else {
1017
+            $overallLimit = $maxGroups;
1018
+        }
1019
+        $chunkOffset = $offset;
1020
+        $allGroups = array();
1021
+        while ($chunkOffset < $overallLimit) {
1022
+            $chunkLimit = min($pagingSize, $overallLimit - $chunkOffset);
1023
+            $ldapGroups = $this->getGroupsChunk($search, $chunkLimit, $chunkOffset);
1024
+            $nread = count($ldapGroups);
1025
+            \OCP\Util::writeLog('user_ldap', 'getGroups('.$search.'): read '.$nread.' at offset '.$chunkOffset.' (limit: '.$chunkLimit.')', ILogger::DEBUG);
1026
+            if ($nread) {
1027
+                $allGroups = array_merge($allGroups, $ldapGroups);
1028
+                $chunkOffset += $nread;
1029
+            }
1030
+            if ($nread < $chunkLimit) {
1031
+                break;
1032
+            }
1033
+        }
1034
+        return $allGroups;
1035
+    }
1036
+
1037
+    /**
1038
+     * @param string $group
1039
+     * @return bool
1040
+     */
1041
+    public function groupMatchesFilter($group) {
1042
+        return (strripos($group, $this->groupSearch) !== false);
1043
+    }
1044
+
1045
+    /**
1046
+     * check if a group exists
1047
+     * @param string $gid
1048
+     * @return bool
1049
+     */
1050
+    public function groupExists($gid) {
1051
+        $groupExists = $this->access->connection->getFromCache('groupExists'.$gid);
1052
+        if(!is_null($groupExists)) {
1053
+            return (bool)$groupExists;
1054
+        }
1055
+
1056
+        //getting dn, if false the group does not exist. If dn, it may be mapped
1057
+        //only, requires more checking.
1058
+        $dn = $this->access->groupname2dn($gid);
1059
+        if(!$dn) {
1060
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1061
+            return false;
1062
+        }
1063
+
1064
+        //if group really still exists, we will be able to read its objectclass
1065
+        if(!is_array($this->access->readAttribute($dn, ''))) {
1066
+            $this->access->connection->writeToCache('groupExists'.$gid, false);
1067
+            return false;
1068
+        }
1069
+
1070
+        $this->access->connection->writeToCache('groupExists'.$gid, true);
1071
+        return true;
1072
+    }
1073
+
1074
+    /**
1075
+     * Check if backend implements actions
1076
+     * @param int $actions bitwise-or'ed actions
1077
+     * @return boolean
1078
+     *
1079
+     * Returns the supported actions as int to be
1080
+     * compared with GroupInterface::CREATE_GROUP etc.
1081
+     */
1082
+    public function implementsActions($actions) {
1083
+        return (bool)((GroupInterface::COUNT_USERS |
1084
+                $this->groupPluginManager->getImplementedActions()) & $actions);
1085
+    }
1086
+
1087
+    /**
1088
+     * Return access for LDAP interaction.
1089
+     * @return Access instance of Access for LDAP interaction
1090
+     */
1091
+    public function getLDAPAccess($gid) {
1092
+        return $this->access;
1093
+    }
1094
+
1095
+    /**
1096
+     * create a group
1097
+     * @param string $gid
1098
+     * @return bool
1099
+     * @throws \Exception
1100
+     */
1101
+    public function createGroup($gid) {
1102
+        if ($this->groupPluginManager->implementsActions(GroupInterface::CREATE_GROUP)) {
1103
+            if ($dn = $this->groupPluginManager->createGroup($gid)) {
1104
+                //updates group mapping
1105
+                $this->access->dn2ocname($dn, $gid, false);
1106
+                $this->access->connection->writeToCache("groupExists".$gid, true);
1107
+            }
1108
+            return $dn != null;
1109
+        }
1110
+        throw new \Exception('Could not create group in LDAP backend.');
1111
+    }
1112
+
1113
+    /**
1114
+     * delete a group
1115
+     * @param string $gid gid of the group to delete
1116
+     * @return bool
1117
+     * @throws \Exception
1118
+     */
1119
+    public function deleteGroup($gid) {
1120
+        if ($this->groupPluginManager->implementsActions(GroupInterface::DELETE_GROUP)) {
1121
+            if ($ret = $this->groupPluginManager->deleteGroup($gid)) {
1122
+                #delete group in nextcloud internal db
1123
+                $this->access->getGroupMapper()->unmap($gid);
1124
+                $this->access->connection->writeToCache("groupExists".$gid, false);
1125
+            }
1126
+            return $ret;
1127
+        }
1128
+        throw new \Exception('Could not delete group in LDAP backend.');
1129
+    }
1130
+
1131
+    /**
1132
+     * Add a user to a group
1133
+     * @param string $uid Name of the user to add to group
1134
+     * @param string $gid Name of the group in which add the user
1135
+     * @return bool
1136
+     * @throws \Exception
1137
+     */
1138
+    public function addToGroup($uid, $gid) {
1139
+        if ($this->groupPluginManager->implementsActions(GroupInterface::ADD_TO_GROUP)) {
1140
+            if ($ret = $this->groupPluginManager->addToGroup($uid, $gid)) {
1141
+                $this->access->connection->clearCache();
1142
+            }
1143
+            return $ret;
1144
+        }
1145
+        throw new \Exception('Could not add user to group in LDAP backend.');
1146
+    }
1147
+
1148
+    /**
1149
+     * Removes a user from a group
1150
+     * @param string $uid Name of the user to remove from group
1151
+     * @param string $gid Name of the group from which remove the user
1152
+     * @return bool
1153
+     * @throws \Exception
1154
+     */
1155
+    public function removeFromGroup($uid, $gid) {
1156
+        if ($this->groupPluginManager->implementsActions(GroupInterface::REMOVE_FROM_GROUP)) {
1157
+            if ($ret = $this->groupPluginManager->removeFromGroup($uid, $gid)) {
1158
+                $this->access->connection->clearCache();
1159
+            }
1160
+            return $ret;
1161
+        }
1162
+        throw new \Exception('Could not remove user from group in LDAP backend.');
1163
+    }
1164
+
1165
+    /**
1166
+     * Gets group details
1167
+     * @param string $gid Name of the group
1168
+     * @return array | false
1169
+     * @throws \Exception
1170
+     */
1171
+    public function getGroupDetails($gid) {
1172
+        if ($this->groupPluginManager->implementsActions(GroupInterface::GROUP_DETAILS)) {
1173
+            return $this->groupPluginManager->getGroupDetails($gid);
1174
+        }
1175
+        throw new \Exception('Could not get group details in LDAP backend.');
1176
+    }
1177
+
1178
+    /**
1179
+     * Return LDAP connection resource from a cloned connection.
1180
+     * The cloned connection needs to be closed manually.
1181
+     * of the current access.
1182
+     * @param string $gid
1183
+     * @return resource of the LDAP connection
1184
+     */
1185
+    public function getNewLDAPConnection($gid) {
1186
+        $connection = clone $this->access->getConnection();
1187
+        return $connection->getConnectionResource();
1188
+    }
1189 1189
 
1190 1190
 }
Please login to merge, or discard this patch.