@@ -46,1145 +46,1145 @@ |
||
| 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 | } |
@@ -61,1672 +61,1672 @@ discard block |
||
| 61 | 61 | * @package OCA\User_LDAP |
| 62 | 62 | */ |
| 63 | 63 | class Access extends LDAPUtility implements IUserTools { |
| 64 | - const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid']; |
|
| 65 | - |
|
| 66 | - /** @var \OCA\User_LDAP\Connection */ |
|
| 67 | - public $connection; |
|
| 68 | - /** @var Manager */ |
|
| 69 | - public $userManager; |
|
| 70 | - //never ever check this var directly, always use getPagedSearchResultState |
|
| 71 | - protected $pagedSearchedSuccessful; |
|
| 72 | - |
|
| 73 | - /** |
|
| 74 | - * @var string[] $cookies an array of returned Paged Result cookies |
|
| 75 | - */ |
|
| 76 | - protected $cookies = array(); |
|
| 77 | - |
|
| 78 | - /** |
|
| 79 | - * @var string $lastCookie the last cookie returned from a Paged Results |
|
| 80 | - * operation, defaults to an empty string |
|
| 81 | - */ |
|
| 82 | - protected $lastCookie = ''; |
|
| 83 | - |
|
| 84 | - /** |
|
| 85 | - * @var AbstractMapping $userMapper |
|
| 86 | - */ |
|
| 87 | - protected $userMapper; |
|
| 88 | - |
|
| 89 | - /** |
|
| 90 | - * @var AbstractMapping $userMapper |
|
| 91 | - */ |
|
| 92 | - protected $groupMapper; |
|
| 93 | - |
|
| 94 | - /** |
|
| 95 | - * @var \OCA\User_LDAP\Helper |
|
| 96 | - */ |
|
| 97 | - private $helper; |
|
| 98 | - /** @var IConfig */ |
|
| 99 | - private $config; |
|
| 100 | - /** @var IUserManager */ |
|
| 101 | - private $ncUserManager; |
|
| 102 | - |
|
| 103 | - public function __construct( |
|
| 104 | - Connection $connection, |
|
| 105 | - ILDAPWrapper $ldap, |
|
| 106 | - Manager $userManager, |
|
| 107 | - Helper $helper, |
|
| 108 | - IConfig $config, |
|
| 109 | - IUserManager $ncUserManager |
|
| 110 | - ) { |
|
| 111 | - parent::__construct($ldap); |
|
| 112 | - $this->connection = $connection; |
|
| 113 | - $this->userManager = $userManager; |
|
| 114 | - $this->userManager->setLdapAccess($this); |
|
| 115 | - $this->helper = $helper; |
|
| 116 | - $this->config = $config; |
|
| 117 | - $this->ncUserManager = $ncUserManager; |
|
| 118 | - } |
|
| 119 | - |
|
| 120 | - /** |
|
| 121 | - * sets the User Mapper |
|
| 122 | - * @param AbstractMapping $mapper |
|
| 123 | - */ |
|
| 124 | - public function setUserMapper(AbstractMapping $mapper) { |
|
| 125 | - $this->userMapper = $mapper; |
|
| 126 | - } |
|
| 127 | - |
|
| 128 | - /** |
|
| 129 | - * returns the User Mapper |
|
| 130 | - * @throws \Exception |
|
| 131 | - * @return AbstractMapping |
|
| 132 | - */ |
|
| 133 | - public function getUserMapper() { |
|
| 134 | - if(is_null($this->userMapper)) { |
|
| 135 | - throw new \Exception('UserMapper was not assigned to this Access instance.'); |
|
| 136 | - } |
|
| 137 | - return $this->userMapper; |
|
| 138 | - } |
|
| 139 | - |
|
| 140 | - /** |
|
| 141 | - * sets the Group Mapper |
|
| 142 | - * @param AbstractMapping $mapper |
|
| 143 | - */ |
|
| 144 | - public function setGroupMapper(AbstractMapping $mapper) { |
|
| 145 | - $this->groupMapper = $mapper; |
|
| 146 | - } |
|
| 147 | - |
|
| 148 | - /** |
|
| 149 | - * returns the Group Mapper |
|
| 150 | - * @throws \Exception |
|
| 151 | - * @return AbstractMapping |
|
| 152 | - */ |
|
| 153 | - public function getGroupMapper() { |
|
| 154 | - if(is_null($this->groupMapper)) { |
|
| 155 | - throw new \Exception('GroupMapper was not assigned to this Access instance.'); |
|
| 156 | - } |
|
| 157 | - return $this->groupMapper; |
|
| 158 | - } |
|
| 159 | - |
|
| 160 | - /** |
|
| 161 | - * @return bool |
|
| 162 | - */ |
|
| 163 | - private function checkConnection() { |
|
| 164 | - return ($this->connection instanceof Connection); |
|
| 165 | - } |
|
| 166 | - |
|
| 167 | - /** |
|
| 168 | - * returns the Connection instance |
|
| 169 | - * @return \OCA\User_LDAP\Connection |
|
| 170 | - */ |
|
| 171 | - public function getConnection() { |
|
| 172 | - return $this->connection; |
|
| 173 | - } |
|
| 174 | - |
|
| 175 | - /** |
|
| 176 | - * reads a given attribute for an LDAP record identified by a DN |
|
| 177 | - * |
|
| 178 | - * @param string $dn the record in question |
|
| 179 | - * @param string $attr the attribute that shall be retrieved |
|
| 180 | - * if empty, just check the record's existence |
|
| 181 | - * @param string $filter |
|
| 182 | - * @return array|false an array of values on success or an empty |
|
| 183 | - * array if $attr is empty, false otherwise |
|
| 184 | - * @throws ServerNotAvailableException |
|
| 185 | - */ |
|
| 186 | - public function readAttribute($dn, $attr, $filter = 'objectClass=*') { |
|
| 187 | - if(!$this->checkConnection()) { |
|
| 188 | - \OCP\Util::writeLog('user_ldap', |
|
| 189 | - 'No LDAP Connector assigned, access impossible for readAttribute.', |
|
| 190 | - ILogger::WARN); |
|
| 191 | - return false; |
|
| 192 | - } |
|
| 193 | - $cr = $this->connection->getConnectionResource(); |
|
| 194 | - if(!$this->ldap->isResource($cr)) { |
|
| 195 | - //LDAP not available |
|
| 196 | - \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG); |
|
| 197 | - return false; |
|
| 198 | - } |
|
| 199 | - //Cancel possibly running Paged Results operation, otherwise we run in |
|
| 200 | - //LDAP protocol errors |
|
| 201 | - $this->abandonPagedSearch(); |
|
| 202 | - // openLDAP requires that we init a new Paged Search. Not needed by AD, |
|
| 203 | - // but does not hurt either. |
|
| 204 | - $pagingSize = (int)$this->connection->ldapPagingSize; |
|
| 205 | - // 0 won't result in replies, small numbers may leave out groups |
|
| 206 | - // (cf. #12306), 500 is default for paging and should work everywhere. |
|
| 207 | - $maxResults = $pagingSize > 20 ? $pagingSize : 500; |
|
| 208 | - $attr = mb_strtolower($attr, 'UTF-8'); |
|
| 209 | - // the actual read attribute later may contain parameters on a ranged |
|
| 210 | - // request, e.g. member;range=99-199. Depends on server reply. |
|
| 211 | - $attrToRead = $attr; |
|
| 212 | - |
|
| 213 | - $values = []; |
|
| 214 | - $isRangeRequest = false; |
|
| 215 | - do { |
|
| 216 | - $result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults); |
|
| 217 | - if(is_bool($result)) { |
|
| 218 | - // when an exists request was run and it was successful, an empty |
|
| 219 | - // array must be returned |
|
| 220 | - return $result ? [] : false; |
|
| 221 | - } |
|
| 222 | - |
|
| 223 | - if (!$isRangeRequest) { |
|
| 224 | - $values = $this->extractAttributeValuesFromResult($result, $attr); |
|
| 225 | - if (!empty($values)) { |
|
| 226 | - return $values; |
|
| 227 | - } |
|
| 228 | - } |
|
| 229 | - |
|
| 230 | - $isRangeRequest = false; |
|
| 231 | - $result = $this->extractRangeData($result, $attr); |
|
| 232 | - if (!empty($result)) { |
|
| 233 | - $normalizedResult = $this->extractAttributeValuesFromResult( |
|
| 234 | - [ $attr => $result['values'] ], |
|
| 235 | - $attr |
|
| 236 | - ); |
|
| 237 | - $values = array_merge($values, $normalizedResult); |
|
| 238 | - |
|
| 239 | - if($result['rangeHigh'] === '*') { |
|
| 240 | - // when server replies with * as high range value, there are |
|
| 241 | - // no more results left |
|
| 242 | - return $values; |
|
| 243 | - } else { |
|
| 244 | - $low = $result['rangeHigh'] + 1; |
|
| 245 | - $attrToRead = $result['attributeName'] . ';range=' . $low . '-*'; |
|
| 246 | - $isRangeRequest = true; |
|
| 247 | - } |
|
| 248 | - } |
|
| 249 | - } while($isRangeRequest); |
|
| 250 | - |
|
| 251 | - \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG); |
|
| 252 | - return false; |
|
| 253 | - } |
|
| 254 | - |
|
| 255 | - /** |
|
| 256 | - * Runs an read operation against LDAP |
|
| 257 | - * |
|
| 258 | - * @param resource $cr the LDAP connection |
|
| 259 | - * @param string $dn |
|
| 260 | - * @param string $attribute |
|
| 261 | - * @param string $filter |
|
| 262 | - * @param int $maxResults |
|
| 263 | - * @return array|bool false if there was any error, true if an exists check |
|
| 264 | - * was performed and the requested DN found, array with the |
|
| 265 | - * returned data on a successful usual operation |
|
| 266 | - * @throws ServerNotAvailableException |
|
| 267 | - */ |
|
| 268 | - public function executeRead($cr, $dn, $attribute, $filter, $maxResults) { |
|
| 269 | - $this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0); |
|
| 270 | - $dn = $this->helper->DNasBaseParameter($dn); |
|
| 271 | - $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute)); |
|
| 272 | - if (!$this->ldap->isResource($rr)) { |
|
| 273 | - if ($attribute !== '') { |
|
| 274 | - //do not throw this message on userExists check, irritates |
|
| 275 | - \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG); |
|
| 276 | - } |
|
| 277 | - //in case an error occurs , e.g. object does not exist |
|
| 278 | - return false; |
|
| 279 | - } |
|
| 280 | - if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) { |
|
| 281 | - \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG); |
|
| 282 | - return true; |
|
| 283 | - } |
|
| 284 | - $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr); |
|
| 285 | - if (!$this->ldap->isResource($er)) { |
|
| 286 | - //did not match the filter, return false |
|
| 287 | - return false; |
|
| 288 | - } |
|
| 289 | - //LDAP attributes are not case sensitive |
|
| 290 | - $result = \OCP\Util::mb_array_change_key_case( |
|
| 291 | - $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8'); |
|
| 292 | - |
|
| 293 | - return $result; |
|
| 294 | - } |
|
| 295 | - |
|
| 296 | - /** |
|
| 297 | - * Normalizes a result grom getAttributes(), i.e. handles DNs and binary |
|
| 298 | - * data if present. |
|
| 299 | - * |
|
| 300 | - * @param array $result from ILDAPWrapper::getAttributes() |
|
| 301 | - * @param string $attribute the attribute name that was read |
|
| 302 | - * @return string[] |
|
| 303 | - */ |
|
| 304 | - public function extractAttributeValuesFromResult($result, $attribute) { |
|
| 305 | - $values = []; |
|
| 306 | - if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) { |
|
| 307 | - $lowercaseAttribute = strtolower($attribute); |
|
| 308 | - for($i=0;$i<$result[$attribute]['count'];$i++) { |
|
| 309 | - if($this->resemblesDN($attribute)) { |
|
| 310 | - $values[] = $this->helper->sanitizeDN($result[$attribute][$i]); |
|
| 311 | - } elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') { |
|
| 312 | - $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]); |
|
| 313 | - } else { |
|
| 314 | - $values[] = $result[$attribute][$i]; |
|
| 315 | - } |
|
| 316 | - } |
|
| 317 | - } |
|
| 318 | - return $values; |
|
| 319 | - } |
|
| 320 | - |
|
| 321 | - /** |
|
| 322 | - * Attempts to find ranged data in a getAttribute results and extracts the |
|
| 323 | - * returned values as well as information on the range and full attribute |
|
| 324 | - * name for further processing. |
|
| 325 | - * |
|
| 326 | - * @param array $result from ILDAPWrapper::getAttributes() |
|
| 327 | - * @param string $attribute the attribute name that was read. Without ";range=…" |
|
| 328 | - * @return array If a range was detected with keys 'values', 'attributeName', |
|
| 329 | - * 'attributeFull' and 'rangeHigh', otherwise empty. |
|
| 330 | - */ |
|
| 331 | - public function extractRangeData($result, $attribute) { |
|
| 332 | - $keys = array_keys($result); |
|
| 333 | - foreach($keys as $key) { |
|
| 334 | - if($key !== $attribute && strpos($key, $attribute) === 0) { |
|
| 335 | - $queryData = explode(';', $key); |
|
| 336 | - if(strpos($queryData[1], 'range=') === 0) { |
|
| 337 | - $high = substr($queryData[1], 1 + strpos($queryData[1], '-')); |
|
| 338 | - $data = [ |
|
| 339 | - 'values' => $result[$key], |
|
| 340 | - 'attributeName' => $queryData[0], |
|
| 341 | - 'attributeFull' => $key, |
|
| 342 | - 'rangeHigh' => $high, |
|
| 343 | - ]; |
|
| 344 | - return $data; |
|
| 345 | - } |
|
| 346 | - } |
|
| 347 | - } |
|
| 348 | - return []; |
|
| 349 | - } |
|
| 64 | + const UUID_ATTRIBUTES = ['entryuuid', 'nsuniqueid', 'objectguid', 'guid', 'ipauniqueid']; |
|
| 65 | + |
|
| 66 | + /** @var \OCA\User_LDAP\Connection */ |
|
| 67 | + public $connection; |
|
| 68 | + /** @var Manager */ |
|
| 69 | + public $userManager; |
|
| 70 | + //never ever check this var directly, always use getPagedSearchResultState |
|
| 71 | + protected $pagedSearchedSuccessful; |
|
| 72 | + |
|
| 73 | + /** |
|
| 74 | + * @var string[] $cookies an array of returned Paged Result cookies |
|
| 75 | + */ |
|
| 76 | + protected $cookies = array(); |
|
| 77 | + |
|
| 78 | + /** |
|
| 79 | + * @var string $lastCookie the last cookie returned from a Paged Results |
|
| 80 | + * operation, defaults to an empty string |
|
| 81 | + */ |
|
| 82 | + protected $lastCookie = ''; |
|
| 83 | + |
|
| 84 | + /** |
|
| 85 | + * @var AbstractMapping $userMapper |
|
| 86 | + */ |
|
| 87 | + protected $userMapper; |
|
| 88 | + |
|
| 89 | + /** |
|
| 90 | + * @var AbstractMapping $userMapper |
|
| 91 | + */ |
|
| 92 | + protected $groupMapper; |
|
| 93 | + |
|
| 94 | + /** |
|
| 95 | + * @var \OCA\User_LDAP\Helper |
|
| 96 | + */ |
|
| 97 | + private $helper; |
|
| 98 | + /** @var IConfig */ |
|
| 99 | + private $config; |
|
| 100 | + /** @var IUserManager */ |
|
| 101 | + private $ncUserManager; |
|
| 102 | + |
|
| 103 | + public function __construct( |
|
| 104 | + Connection $connection, |
|
| 105 | + ILDAPWrapper $ldap, |
|
| 106 | + Manager $userManager, |
|
| 107 | + Helper $helper, |
|
| 108 | + IConfig $config, |
|
| 109 | + IUserManager $ncUserManager |
|
| 110 | + ) { |
|
| 111 | + parent::__construct($ldap); |
|
| 112 | + $this->connection = $connection; |
|
| 113 | + $this->userManager = $userManager; |
|
| 114 | + $this->userManager->setLdapAccess($this); |
|
| 115 | + $this->helper = $helper; |
|
| 116 | + $this->config = $config; |
|
| 117 | + $this->ncUserManager = $ncUserManager; |
|
| 118 | + } |
|
| 119 | + |
|
| 120 | + /** |
|
| 121 | + * sets the User Mapper |
|
| 122 | + * @param AbstractMapping $mapper |
|
| 123 | + */ |
|
| 124 | + public function setUserMapper(AbstractMapping $mapper) { |
|
| 125 | + $this->userMapper = $mapper; |
|
| 126 | + } |
|
| 127 | + |
|
| 128 | + /** |
|
| 129 | + * returns the User Mapper |
|
| 130 | + * @throws \Exception |
|
| 131 | + * @return AbstractMapping |
|
| 132 | + */ |
|
| 133 | + public function getUserMapper() { |
|
| 134 | + if(is_null($this->userMapper)) { |
|
| 135 | + throw new \Exception('UserMapper was not assigned to this Access instance.'); |
|
| 136 | + } |
|
| 137 | + return $this->userMapper; |
|
| 138 | + } |
|
| 139 | + |
|
| 140 | + /** |
|
| 141 | + * sets the Group Mapper |
|
| 142 | + * @param AbstractMapping $mapper |
|
| 143 | + */ |
|
| 144 | + public function setGroupMapper(AbstractMapping $mapper) { |
|
| 145 | + $this->groupMapper = $mapper; |
|
| 146 | + } |
|
| 147 | + |
|
| 148 | + /** |
|
| 149 | + * returns the Group Mapper |
|
| 150 | + * @throws \Exception |
|
| 151 | + * @return AbstractMapping |
|
| 152 | + */ |
|
| 153 | + public function getGroupMapper() { |
|
| 154 | + if(is_null($this->groupMapper)) { |
|
| 155 | + throw new \Exception('GroupMapper was not assigned to this Access instance.'); |
|
| 156 | + } |
|
| 157 | + return $this->groupMapper; |
|
| 158 | + } |
|
| 159 | + |
|
| 160 | + /** |
|
| 161 | + * @return bool |
|
| 162 | + */ |
|
| 163 | + private function checkConnection() { |
|
| 164 | + return ($this->connection instanceof Connection); |
|
| 165 | + } |
|
| 166 | + |
|
| 167 | + /** |
|
| 168 | + * returns the Connection instance |
|
| 169 | + * @return \OCA\User_LDAP\Connection |
|
| 170 | + */ |
|
| 171 | + public function getConnection() { |
|
| 172 | + return $this->connection; |
|
| 173 | + } |
|
| 174 | + |
|
| 175 | + /** |
|
| 176 | + * reads a given attribute for an LDAP record identified by a DN |
|
| 177 | + * |
|
| 178 | + * @param string $dn the record in question |
|
| 179 | + * @param string $attr the attribute that shall be retrieved |
|
| 180 | + * if empty, just check the record's existence |
|
| 181 | + * @param string $filter |
|
| 182 | + * @return array|false an array of values on success or an empty |
|
| 183 | + * array if $attr is empty, false otherwise |
|
| 184 | + * @throws ServerNotAvailableException |
|
| 185 | + */ |
|
| 186 | + public function readAttribute($dn, $attr, $filter = 'objectClass=*') { |
|
| 187 | + if(!$this->checkConnection()) { |
|
| 188 | + \OCP\Util::writeLog('user_ldap', |
|
| 189 | + 'No LDAP Connector assigned, access impossible for readAttribute.', |
|
| 190 | + ILogger::WARN); |
|
| 191 | + return false; |
|
| 192 | + } |
|
| 193 | + $cr = $this->connection->getConnectionResource(); |
|
| 194 | + if(!$this->ldap->isResource($cr)) { |
|
| 195 | + //LDAP not available |
|
| 196 | + \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG); |
|
| 197 | + return false; |
|
| 198 | + } |
|
| 199 | + //Cancel possibly running Paged Results operation, otherwise we run in |
|
| 200 | + //LDAP protocol errors |
|
| 201 | + $this->abandonPagedSearch(); |
|
| 202 | + // openLDAP requires that we init a new Paged Search. Not needed by AD, |
|
| 203 | + // but does not hurt either. |
|
| 204 | + $pagingSize = (int)$this->connection->ldapPagingSize; |
|
| 205 | + // 0 won't result in replies, small numbers may leave out groups |
|
| 206 | + // (cf. #12306), 500 is default for paging and should work everywhere. |
|
| 207 | + $maxResults = $pagingSize > 20 ? $pagingSize : 500; |
|
| 208 | + $attr = mb_strtolower($attr, 'UTF-8'); |
|
| 209 | + // the actual read attribute later may contain parameters on a ranged |
|
| 210 | + // request, e.g. member;range=99-199. Depends on server reply. |
|
| 211 | + $attrToRead = $attr; |
|
| 212 | + |
|
| 213 | + $values = []; |
|
| 214 | + $isRangeRequest = false; |
|
| 215 | + do { |
|
| 216 | + $result = $this->executeRead($cr, $dn, $attrToRead, $filter, $maxResults); |
|
| 217 | + if(is_bool($result)) { |
|
| 218 | + // when an exists request was run and it was successful, an empty |
|
| 219 | + // array must be returned |
|
| 220 | + return $result ? [] : false; |
|
| 221 | + } |
|
| 222 | + |
|
| 223 | + if (!$isRangeRequest) { |
|
| 224 | + $values = $this->extractAttributeValuesFromResult($result, $attr); |
|
| 225 | + if (!empty($values)) { |
|
| 226 | + return $values; |
|
| 227 | + } |
|
| 228 | + } |
|
| 229 | + |
|
| 230 | + $isRangeRequest = false; |
|
| 231 | + $result = $this->extractRangeData($result, $attr); |
|
| 232 | + if (!empty($result)) { |
|
| 233 | + $normalizedResult = $this->extractAttributeValuesFromResult( |
|
| 234 | + [ $attr => $result['values'] ], |
|
| 235 | + $attr |
|
| 236 | + ); |
|
| 237 | + $values = array_merge($values, $normalizedResult); |
|
| 238 | + |
|
| 239 | + if($result['rangeHigh'] === '*') { |
|
| 240 | + // when server replies with * as high range value, there are |
|
| 241 | + // no more results left |
|
| 242 | + return $values; |
|
| 243 | + } else { |
|
| 244 | + $low = $result['rangeHigh'] + 1; |
|
| 245 | + $attrToRead = $result['attributeName'] . ';range=' . $low . '-*'; |
|
| 246 | + $isRangeRequest = true; |
|
| 247 | + } |
|
| 248 | + } |
|
| 249 | + } while($isRangeRequest); |
|
| 250 | + |
|
| 251 | + \OCP\Util::writeLog('user_ldap', 'Requested attribute '.$attr.' not found for '.$dn, ILogger::DEBUG); |
|
| 252 | + return false; |
|
| 253 | + } |
|
| 254 | + |
|
| 255 | + /** |
|
| 256 | + * Runs an read operation against LDAP |
|
| 257 | + * |
|
| 258 | + * @param resource $cr the LDAP connection |
|
| 259 | + * @param string $dn |
|
| 260 | + * @param string $attribute |
|
| 261 | + * @param string $filter |
|
| 262 | + * @param int $maxResults |
|
| 263 | + * @return array|bool false if there was any error, true if an exists check |
|
| 264 | + * was performed and the requested DN found, array with the |
|
| 265 | + * returned data on a successful usual operation |
|
| 266 | + * @throws ServerNotAvailableException |
|
| 267 | + */ |
|
| 268 | + public function executeRead($cr, $dn, $attribute, $filter, $maxResults) { |
|
| 269 | + $this->initPagedSearch($filter, array($dn), array($attribute), $maxResults, 0); |
|
| 270 | + $dn = $this->helper->DNasBaseParameter($dn); |
|
| 271 | + $rr = @$this->invokeLDAPMethod('read', $cr, $dn, $filter, array($attribute)); |
|
| 272 | + if (!$this->ldap->isResource($rr)) { |
|
| 273 | + if ($attribute !== '') { |
|
| 274 | + //do not throw this message on userExists check, irritates |
|
| 275 | + \OCP\Util::writeLog('user_ldap', 'readAttribute failed for DN ' . $dn, ILogger::DEBUG); |
|
| 276 | + } |
|
| 277 | + //in case an error occurs , e.g. object does not exist |
|
| 278 | + return false; |
|
| 279 | + } |
|
| 280 | + if ($attribute === '' && ($filter === 'objectclass=*' || $this->invokeLDAPMethod('countEntries', $cr, $rr) === 1)) { |
|
| 281 | + \OCP\Util::writeLog('user_ldap', 'readAttribute: ' . $dn . ' found', ILogger::DEBUG); |
|
| 282 | + return true; |
|
| 283 | + } |
|
| 284 | + $er = $this->invokeLDAPMethod('firstEntry', $cr, $rr); |
|
| 285 | + if (!$this->ldap->isResource($er)) { |
|
| 286 | + //did not match the filter, return false |
|
| 287 | + return false; |
|
| 288 | + } |
|
| 289 | + //LDAP attributes are not case sensitive |
|
| 290 | + $result = \OCP\Util::mb_array_change_key_case( |
|
| 291 | + $this->invokeLDAPMethod('getAttributes', $cr, $er), MB_CASE_LOWER, 'UTF-8'); |
|
| 292 | + |
|
| 293 | + return $result; |
|
| 294 | + } |
|
| 295 | + |
|
| 296 | + /** |
|
| 297 | + * Normalizes a result grom getAttributes(), i.e. handles DNs and binary |
|
| 298 | + * data if present. |
|
| 299 | + * |
|
| 300 | + * @param array $result from ILDAPWrapper::getAttributes() |
|
| 301 | + * @param string $attribute the attribute name that was read |
|
| 302 | + * @return string[] |
|
| 303 | + */ |
|
| 304 | + public function extractAttributeValuesFromResult($result, $attribute) { |
|
| 305 | + $values = []; |
|
| 306 | + if(isset($result[$attribute]) && $result[$attribute]['count'] > 0) { |
|
| 307 | + $lowercaseAttribute = strtolower($attribute); |
|
| 308 | + for($i=0;$i<$result[$attribute]['count'];$i++) { |
|
| 309 | + if($this->resemblesDN($attribute)) { |
|
| 310 | + $values[] = $this->helper->sanitizeDN($result[$attribute][$i]); |
|
| 311 | + } elseif($lowercaseAttribute === 'objectguid' || $lowercaseAttribute === 'guid') { |
|
| 312 | + $values[] = $this->convertObjectGUID2Str($result[$attribute][$i]); |
|
| 313 | + } else { |
|
| 314 | + $values[] = $result[$attribute][$i]; |
|
| 315 | + } |
|
| 316 | + } |
|
| 317 | + } |
|
| 318 | + return $values; |
|
| 319 | + } |
|
| 320 | + |
|
| 321 | + /** |
|
| 322 | + * Attempts to find ranged data in a getAttribute results and extracts the |
|
| 323 | + * returned values as well as information on the range and full attribute |
|
| 324 | + * name for further processing. |
|
| 325 | + * |
|
| 326 | + * @param array $result from ILDAPWrapper::getAttributes() |
|
| 327 | + * @param string $attribute the attribute name that was read. Without ";range=…" |
|
| 328 | + * @return array If a range was detected with keys 'values', 'attributeName', |
|
| 329 | + * 'attributeFull' and 'rangeHigh', otherwise empty. |
|
| 330 | + */ |
|
| 331 | + public function extractRangeData($result, $attribute) { |
|
| 332 | + $keys = array_keys($result); |
|
| 333 | + foreach($keys as $key) { |
|
| 334 | + if($key !== $attribute && strpos($key, $attribute) === 0) { |
|
| 335 | + $queryData = explode(';', $key); |
|
| 336 | + if(strpos($queryData[1], 'range=') === 0) { |
|
| 337 | + $high = substr($queryData[1], 1 + strpos($queryData[1], '-')); |
|
| 338 | + $data = [ |
|
| 339 | + 'values' => $result[$key], |
|
| 340 | + 'attributeName' => $queryData[0], |
|
| 341 | + 'attributeFull' => $key, |
|
| 342 | + 'rangeHigh' => $high, |
|
| 343 | + ]; |
|
| 344 | + return $data; |
|
| 345 | + } |
|
| 346 | + } |
|
| 347 | + } |
|
| 348 | + return []; |
|
| 349 | + } |
|
| 350 | 350 | |
| 351 | - /** |
|
| 352 | - * Set password for an LDAP user identified by a DN |
|
| 353 | - * |
|
| 354 | - * @param string $userDN the user in question |
|
| 355 | - * @param string $password the new password |
|
| 356 | - * @return bool |
|
| 357 | - * @throws HintException |
|
| 358 | - * @throws \Exception |
|
| 359 | - */ |
|
| 360 | - public function setPassword($userDN, $password) { |
|
| 361 | - if((int)$this->connection->turnOnPasswordChange !== 1) { |
|
| 362 | - throw new \Exception('LDAP password changes are disabled.'); |
|
| 363 | - } |
|
| 364 | - $cr = $this->connection->getConnectionResource(); |
|
| 365 | - if(!$this->ldap->isResource($cr)) { |
|
| 366 | - //LDAP not available |
|
| 367 | - \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG); |
|
| 368 | - return false; |
|
| 369 | - } |
|
| 370 | - try { |
|
| 371 | - return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password); |
|
| 372 | - } catch(ConstraintViolationException $e) { |
|
| 373 | - throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode()); |
|
| 374 | - } |
|
| 375 | - } |
|
| 376 | - |
|
| 377 | - /** |
|
| 378 | - * checks whether the given attributes value is probably a DN |
|
| 379 | - * @param string $attr the attribute in question |
|
| 380 | - * @return boolean if so true, otherwise false |
|
| 381 | - */ |
|
| 382 | - private function resemblesDN($attr) { |
|
| 383 | - $resemblingAttributes = array( |
|
| 384 | - 'dn', |
|
| 385 | - 'uniquemember', |
|
| 386 | - 'member', |
|
| 387 | - // memberOf is an "operational" attribute, without a definition in any RFC |
|
| 388 | - 'memberof' |
|
| 389 | - ); |
|
| 390 | - return in_array($attr, $resemblingAttributes); |
|
| 391 | - } |
|
| 392 | - |
|
| 393 | - /** |
|
| 394 | - * checks whether the given string is probably a DN |
|
| 395 | - * @param string $string |
|
| 396 | - * @return boolean |
|
| 397 | - */ |
|
| 398 | - public function stringResemblesDN($string) { |
|
| 399 | - $r = $this->ldap->explodeDN($string, 0); |
|
| 400 | - // if exploding a DN succeeds and does not end up in |
|
| 401 | - // an empty array except for $r[count] being 0. |
|
| 402 | - return (is_array($r) && count($r) > 1); |
|
| 403 | - } |
|
| 404 | - |
|
| 405 | - /** |
|
| 406 | - * returns a DN-string that is cleaned from not domain parts, e.g. |
|
| 407 | - * cn=foo,cn=bar,dc=foobar,dc=server,dc=org |
|
| 408 | - * becomes dc=foobar,dc=server,dc=org |
|
| 409 | - * @param string $dn |
|
| 410 | - * @return string |
|
| 411 | - */ |
|
| 412 | - public function getDomainDNFromDN($dn) { |
|
| 413 | - $allParts = $this->ldap->explodeDN($dn, 0); |
|
| 414 | - if($allParts === false) { |
|
| 415 | - //not a valid DN |
|
| 416 | - return ''; |
|
| 417 | - } |
|
| 418 | - $domainParts = array(); |
|
| 419 | - $dcFound = false; |
|
| 420 | - foreach($allParts as $part) { |
|
| 421 | - if(!$dcFound && strpos($part, 'dc=') === 0) { |
|
| 422 | - $dcFound = true; |
|
| 423 | - } |
|
| 424 | - if($dcFound) { |
|
| 425 | - $domainParts[] = $part; |
|
| 426 | - } |
|
| 427 | - } |
|
| 428 | - return implode(',', $domainParts); |
|
| 429 | - } |
|
| 430 | - |
|
| 431 | - /** |
|
| 432 | - * returns the LDAP DN for the given internal Nextcloud name of the group |
|
| 433 | - * @param string $name the Nextcloud name in question |
|
| 434 | - * @return string|false LDAP DN on success, otherwise false |
|
| 435 | - */ |
|
| 436 | - public function groupname2dn($name) { |
|
| 437 | - return $this->groupMapper->getDNByName($name); |
|
| 438 | - } |
|
| 439 | - |
|
| 440 | - /** |
|
| 441 | - * returns the LDAP DN for the given internal Nextcloud name of the user |
|
| 442 | - * @param string $name the Nextcloud name in question |
|
| 443 | - * @return string|false with the LDAP DN on success, otherwise false |
|
| 444 | - */ |
|
| 445 | - public function username2dn($name) { |
|
| 446 | - $fdn = $this->userMapper->getDNByName($name); |
|
| 447 | - |
|
| 448 | - //Check whether the DN belongs to the Base, to avoid issues on multi- |
|
| 449 | - //server setups |
|
| 450 | - if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) { |
|
| 451 | - return $fdn; |
|
| 452 | - } |
|
| 453 | - |
|
| 454 | - return false; |
|
| 455 | - } |
|
| 456 | - |
|
| 457 | - /** |
|
| 458 | - * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure |
|
| 459 | - * @param string $fdn the dn of the group object |
|
| 460 | - * @param string $ldapName optional, the display name of the object |
|
| 461 | - * @return string|false with the name to use in Nextcloud, false on DN outside of search DN |
|
| 462 | - */ |
|
| 463 | - public function dn2groupname($fdn, $ldapName = null) { |
|
| 464 | - //To avoid bypassing the base DN settings under certain circumstances |
|
| 465 | - //with the group support, check whether the provided DN matches one of |
|
| 466 | - //the given Bases |
|
| 467 | - if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) { |
|
| 468 | - return false; |
|
| 469 | - } |
|
| 470 | - |
|
| 471 | - return $this->dn2ocname($fdn, $ldapName, false); |
|
| 472 | - } |
|
| 473 | - |
|
| 474 | - /** |
|
| 475 | - * accepts an array of group DNs and tests whether they match the user |
|
| 476 | - * filter by doing read operations against the group entries. Returns an |
|
| 477 | - * array of DNs that match the filter. |
|
| 478 | - * |
|
| 479 | - * @param string[] $groupDNs |
|
| 480 | - * @return string[] |
|
| 481 | - * @throws ServerNotAvailableException |
|
| 482 | - */ |
|
| 483 | - public function groupsMatchFilter($groupDNs) { |
|
| 484 | - $validGroupDNs = []; |
|
| 485 | - foreach($groupDNs as $dn) { |
|
| 486 | - $cacheKey = 'groupsMatchFilter-'.$dn; |
|
| 487 | - $groupMatchFilter = $this->connection->getFromCache($cacheKey); |
|
| 488 | - if(!is_null($groupMatchFilter)) { |
|
| 489 | - if($groupMatchFilter) { |
|
| 490 | - $validGroupDNs[] = $dn; |
|
| 491 | - } |
|
| 492 | - continue; |
|
| 493 | - } |
|
| 494 | - |
|
| 495 | - // Check the base DN first. If this is not met already, we don't |
|
| 496 | - // need to ask the server at all. |
|
| 497 | - if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) { |
|
| 498 | - $this->connection->writeToCache($cacheKey, false); |
|
| 499 | - continue; |
|
| 500 | - } |
|
| 501 | - |
|
| 502 | - $result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter); |
|
| 503 | - if(is_array($result)) { |
|
| 504 | - $this->connection->writeToCache($cacheKey, true); |
|
| 505 | - $validGroupDNs[] = $dn; |
|
| 506 | - } else { |
|
| 507 | - $this->connection->writeToCache($cacheKey, false); |
|
| 508 | - } |
|
| 509 | - |
|
| 510 | - } |
|
| 511 | - return $validGroupDNs; |
|
| 512 | - } |
|
| 513 | - |
|
| 514 | - /** |
|
| 515 | - * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure |
|
| 516 | - * @param string $dn the dn of the user object |
|
| 517 | - * @param string $ldapName optional, the display name of the object |
|
| 518 | - * @return string|false with with the name to use in Nextcloud |
|
| 519 | - */ |
|
| 520 | - public function dn2username($fdn, $ldapName = null) { |
|
| 521 | - //To avoid bypassing the base DN settings under certain circumstances |
|
| 522 | - //with the group support, check whether the provided DN matches one of |
|
| 523 | - //the given Bases |
|
| 524 | - if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) { |
|
| 525 | - return false; |
|
| 526 | - } |
|
| 527 | - |
|
| 528 | - return $this->dn2ocname($fdn, $ldapName, true); |
|
| 529 | - } |
|
| 530 | - |
|
| 531 | - /** |
|
| 532 | - * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN |
|
| 533 | - * |
|
| 534 | - * @param string $fdn the dn of the user object |
|
| 535 | - * @param string|null $ldapName optional, the display name of the object |
|
| 536 | - * @param bool $isUser optional, whether it is a user object (otherwise group assumed) |
|
| 537 | - * @param bool|null $newlyMapped |
|
| 538 | - * @param array|null $record |
|
| 539 | - * @return false|string with with the name to use in Nextcloud |
|
| 540 | - * @throws \Exception |
|
| 541 | - */ |
|
| 542 | - public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) { |
|
| 543 | - $newlyMapped = false; |
|
| 544 | - if($isUser) { |
|
| 545 | - $mapper = $this->getUserMapper(); |
|
| 546 | - $nameAttribute = $this->connection->ldapUserDisplayName; |
|
| 547 | - $filter = $this->connection->ldapUserFilter; |
|
| 548 | - } else { |
|
| 549 | - $mapper = $this->getGroupMapper(); |
|
| 550 | - $nameAttribute = $this->connection->ldapGroupDisplayName; |
|
| 551 | - $filter = $this->connection->ldapGroupFilter; |
|
| 552 | - } |
|
| 553 | - |
|
| 554 | - //let's try to retrieve the Nextcloud name from the mappings table |
|
| 555 | - $ncName = $mapper->getNameByDN($fdn); |
|
| 556 | - if(is_string($ncName)) { |
|
| 557 | - return $ncName; |
|
| 558 | - } |
|
| 559 | - |
|
| 560 | - //second try: get the UUID and check if it is known. Then, update the DN and return the name. |
|
| 561 | - $uuid = $this->getUUID($fdn, $isUser, $record); |
|
| 562 | - if(is_string($uuid)) { |
|
| 563 | - $ncName = $mapper->getNameByUUID($uuid); |
|
| 564 | - if(is_string($ncName)) { |
|
| 565 | - $mapper->setDNbyUUID($fdn, $uuid); |
|
| 566 | - return $ncName; |
|
| 567 | - } |
|
| 568 | - } else { |
|
| 569 | - //If the UUID can't be detected something is foul. |
|
| 570 | - \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO); |
|
| 571 | - return false; |
|
| 572 | - } |
|
| 573 | - |
|
| 574 | - if(is_null($ldapName)) { |
|
| 575 | - $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter); |
|
| 576 | - if(!isset($ldapName[0]) && empty($ldapName[0])) { |
|
| 577 | - \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO); |
|
| 578 | - return false; |
|
| 579 | - } |
|
| 580 | - $ldapName = $ldapName[0]; |
|
| 581 | - } |
|
| 582 | - |
|
| 583 | - if($isUser) { |
|
| 584 | - $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr; |
|
| 585 | - if ($usernameAttribute !== '') { |
|
| 586 | - $username = $this->readAttribute($fdn, $usernameAttribute); |
|
| 587 | - $username = $username[0]; |
|
| 588 | - } else { |
|
| 589 | - $username = $uuid; |
|
| 590 | - } |
|
| 591 | - try { |
|
| 592 | - $intName = $this->sanitizeUsername($username); |
|
| 593 | - } catch (\InvalidArgumentException $e) { |
|
| 594 | - \OC::$server->getLogger()->logException($e, [ |
|
| 595 | - 'app' => 'user_ldap', |
|
| 596 | - 'level' => ILogger::WARN, |
|
| 597 | - ]); |
|
| 598 | - // we don't attempt to set a username here. We can go for |
|
| 599 | - // for an alternative 4 digit random number as we would append |
|
| 600 | - // otherwise, however it's likely not enough space in bigger |
|
| 601 | - // setups, and most importantly: this is not intended. |
|
| 602 | - return false; |
|
| 603 | - } |
|
| 604 | - } else { |
|
| 605 | - $intName = $ldapName; |
|
| 606 | - } |
|
| 607 | - |
|
| 608 | - //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups |
|
| 609 | - //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check |
|
| 610 | - //NOTE: mind, disabling cache affects only this instance! Using it |
|
| 611 | - // outside of core user management will still cache the user as non-existing. |
|
| 612 | - $originalTTL = $this->connection->ldapCacheTTL; |
|
| 613 | - $this->connection->setConfiguration(array('ldapCacheTTL' => 0)); |
|
| 614 | - if(($isUser && $intName !== '' && !$this->ncUserManager->userExists($intName)) |
|
| 615 | - || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) { |
|
| 616 | - if($mapper->map($fdn, $intName, $uuid)) { |
|
| 617 | - $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL)); |
|
| 618 | - if($this->ncUserManager instanceof PublicEmitter) { |
|
| 619 | - $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]); |
|
| 620 | - } |
|
| 621 | - $newlyMapped = true; |
|
| 622 | - return $intName; |
|
| 623 | - } |
|
| 624 | - } |
|
| 625 | - $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL)); |
|
| 626 | - |
|
| 627 | - $altName = $this->createAltInternalOwnCloudName($intName, $isUser); |
|
| 628 | - if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) { |
|
| 629 | - if($this->ncUserManager instanceof PublicEmitter) { |
|
| 630 | - $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]); |
|
| 631 | - } |
|
| 632 | - $newlyMapped = true; |
|
| 633 | - return $altName; |
|
| 634 | - } |
|
| 635 | - |
|
| 636 | - //if everything else did not help.. |
|
| 637 | - \OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO); |
|
| 638 | - return false; |
|
| 639 | - } |
|
| 640 | - |
|
| 641 | - /** |
|
| 642 | - * gives back the user names as they are used ownClod internally |
|
| 643 | - * @param array $ldapUsers as returned by fetchList() |
|
| 644 | - * @return array an array with the user names to use in Nextcloud |
|
| 645 | - * |
|
| 646 | - * gives back the user names as they are used ownClod internally |
|
| 647 | - */ |
|
| 648 | - public function nextcloudUserNames($ldapUsers) { |
|
| 649 | - return $this->ldap2NextcloudNames($ldapUsers, true); |
|
| 650 | - } |
|
| 651 | - |
|
| 652 | - /** |
|
| 653 | - * gives back the group names as they are used ownClod internally |
|
| 654 | - * @param array $ldapGroups as returned by fetchList() |
|
| 655 | - * @return array an array with the group names to use in Nextcloud |
|
| 656 | - * |
|
| 657 | - * gives back the group names as they are used ownClod internally |
|
| 658 | - */ |
|
| 659 | - public function nextcloudGroupNames($ldapGroups) { |
|
| 660 | - return $this->ldap2NextcloudNames($ldapGroups, false); |
|
| 661 | - } |
|
| 662 | - |
|
| 663 | - /** |
|
| 664 | - * @param array $ldapObjects as returned by fetchList() |
|
| 665 | - * @param bool $isUsers |
|
| 666 | - * @return array |
|
| 667 | - */ |
|
| 668 | - private function ldap2NextcloudNames($ldapObjects, $isUsers) { |
|
| 669 | - if($isUsers) { |
|
| 670 | - $nameAttribute = $this->connection->ldapUserDisplayName; |
|
| 671 | - $sndAttribute = $this->connection->ldapUserDisplayName2; |
|
| 672 | - } else { |
|
| 673 | - $nameAttribute = $this->connection->ldapGroupDisplayName; |
|
| 674 | - } |
|
| 675 | - $nextcloudNames = array(); |
|
| 676 | - |
|
| 677 | - foreach($ldapObjects as $ldapObject) { |
|
| 678 | - $nameByLDAP = null; |
|
| 679 | - if( isset($ldapObject[$nameAttribute]) |
|
| 680 | - && is_array($ldapObject[$nameAttribute]) |
|
| 681 | - && isset($ldapObject[$nameAttribute][0]) |
|
| 682 | - ) { |
|
| 683 | - // might be set, but not necessarily. if so, we use it. |
|
| 684 | - $nameByLDAP = $ldapObject[$nameAttribute][0]; |
|
| 685 | - } |
|
| 686 | - |
|
| 687 | - $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers); |
|
| 688 | - if($ncName) { |
|
| 689 | - $nextcloudNames[] = $ncName; |
|
| 690 | - if($isUsers) { |
|
| 691 | - //cache the user names so it does not need to be retrieved |
|
| 692 | - //again later (e.g. sharing dialogue). |
|
| 693 | - if(is_null($nameByLDAP)) { |
|
| 694 | - continue; |
|
| 695 | - } |
|
| 696 | - $sndName = isset($ldapObject[$sndAttribute][0]) |
|
| 697 | - ? $ldapObject[$sndAttribute][0] : ''; |
|
| 698 | - $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName); |
|
| 699 | - } |
|
| 700 | - } |
|
| 701 | - } |
|
| 702 | - return $nextcloudNames; |
|
| 703 | - } |
|
| 704 | - |
|
| 705 | - /** |
|
| 706 | - * caches the user display name |
|
| 707 | - * @param string $ocName the internal Nextcloud username |
|
| 708 | - * @param string|false $home the home directory path |
|
| 709 | - */ |
|
| 710 | - public function cacheUserHome($ocName, $home) { |
|
| 711 | - $cacheKey = 'getHome'.$ocName; |
|
| 712 | - $this->connection->writeToCache($cacheKey, $home); |
|
| 713 | - } |
|
| 714 | - |
|
| 715 | - /** |
|
| 716 | - * caches a user as existing |
|
| 717 | - * @param string $ocName the internal Nextcloud username |
|
| 718 | - */ |
|
| 719 | - public function cacheUserExists($ocName) { |
|
| 720 | - $this->connection->writeToCache('userExists'.$ocName, true); |
|
| 721 | - } |
|
| 722 | - |
|
| 723 | - /** |
|
| 724 | - * caches the user display name |
|
| 725 | - * @param string $ocName the internal Nextcloud username |
|
| 726 | - * @param string $displayName the display name |
|
| 727 | - * @param string $displayName2 the second display name |
|
| 728 | - */ |
|
| 729 | - public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') { |
|
| 730 | - $user = $this->userManager->get($ocName); |
|
| 731 | - if($user === null) { |
|
| 732 | - return; |
|
| 733 | - } |
|
| 734 | - $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2); |
|
| 735 | - $cacheKeyTrunk = 'getDisplayName'; |
|
| 736 | - $this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName); |
|
| 737 | - } |
|
| 738 | - |
|
| 739 | - /** |
|
| 740 | - * creates a unique name for internal Nextcloud use for users. Don't call it directly. |
|
| 741 | - * @param string $name the display name of the object |
|
| 742 | - * @return string|false with with the name to use in Nextcloud or false if unsuccessful |
|
| 743 | - * |
|
| 744 | - * Instead of using this method directly, call |
|
| 745 | - * createAltInternalOwnCloudName($name, true) |
|
| 746 | - */ |
|
| 747 | - private function _createAltInternalOwnCloudNameForUsers($name) { |
|
| 748 | - $attempts = 0; |
|
| 749 | - //while loop is just a precaution. If a name is not generated within |
|
| 750 | - //20 attempts, something else is very wrong. Avoids infinite loop. |
|
| 751 | - while($attempts < 20){ |
|
| 752 | - $altName = $name . '_' . rand(1000,9999); |
|
| 753 | - if(!$this->ncUserManager->userExists($altName)) { |
|
| 754 | - return $altName; |
|
| 755 | - } |
|
| 756 | - $attempts++; |
|
| 757 | - } |
|
| 758 | - return false; |
|
| 759 | - } |
|
| 760 | - |
|
| 761 | - /** |
|
| 762 | - * creates a unique name for internal Nextcloud use for groups. Don't call it directly. |
|
| 763 | - * @param string $name the display name of the object |
|
| 764 | - * @return string|false with with the name to use in Nextcloud or false if unsuccessful. |
|
| 765 | - * |
|
| 766 | - * Instead of using this method directly, call |
|
| 767 | - * createAltInternalOwnCloudName($name, false) |
|
| 768 | - * |
|
| 769 | - * Group names are also used as display names, so we do a sequential |
|
| 770 | - * numbering, e.g. Developers_42 when there are 41 other groups called |
|
| 771 | - * "Developers" |
|
| 772 | - */ |
|
| 773 | - private function _createAltInternalOwnCloudNameForGroups($name) { |
|
| 774 | - $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%'); |
|
| 775 | - if(!$usedNames || count($usedNames) === 0) { |
|
| 776 | - $lastNo = 1; //will become name_2 |
|
| 777 | - } else { |
|
| 778 | - natsort($usedNames); |
|
| 779 | - $lastName = array_pop($usedNames); |
|
| 780 | - $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1); |
|
| 781 | - } |
|
| 782 | - $altName = $name.'_'. (string)($lastNo+1); |
|
| 783 | - unset($usedNames); |
|
| 784 | - |
|
| 785 | - $attempts = 1; |
|
| 786 | - while($attempts < 21){ |
|
| 787 | - // Check to be really sure it is unique |
|
| 788 | - // while loop is just a precaution. If a name is not generated within |
|
| 789 | - // 20 attempts, something else is very wrong. Avoids infinite loop. |
|
| 790 | - if(!\OC::$server->getGroupManager()->groupExists($altName)) { |
|
| 791 | - return $altName; |
|
| 792 | - } |
|
| 793 | - $altName = $name . '_' . ($lastNo + $attempts); |
|
| 794 | - $attempts++; |
|
| 795 | - } |
|
| 796 | - return false; |
|
| 797 | - } |
|
| 798 | - |
|
| 799 | - /** |
|
| 800 | - * creates a unique name for internal Nextcloud use. |
|
| 801 | - * @param string $name the display name of the object |
|
| 802 | - * @param boolean $isUser whether name should be created for a user (true) or a group (false) |
|
| 803 | - * @return string|false with with the name to use in Nextcloud or false if unsuccessful |
|
| 804 | - */ |
|
| 805 | - private function createAltInternalOwnCloudName($name, $isUser) { |
|
| 806 | - $originalTTL = $this->connection->ldapCacheTTL; |
|
| 807 | - $this->connection->setConfiguration(array('ldapCacheTTL' => 0)); |
|
| 808 | - if($isUser) { |
|
| 809 | - $altName = $this->_createAltInternalOwnCloudNameForUsers($name); |
|
| 810 | - } else { |
|
| 811 | - $altName = $this->_createAltInternalOwnCloudNameForGroups($name); |
|
| 812 | - } |
|
| 813 | - $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL)); |
|
| 814 | - |
|
| 815 | - return $altName; |
|
| 816 | - } |
|
| 817 | - |
|
| 818 | - /** |
|
| 819 | - * fetches a list of users according to a provided loginName and utilizing |
|
| 820 | - * the login filter. |
|
| 821 | - * |
|
| 822 | - * @param string $loginName |
|
| 823 | - * @param array $attributes optional, list of attributes to read |
|
| 824 | - * @return array |
|
| 825 | - */ |
|
| 826 | - public function fetchUsersByLoginName($loginName, $attributes = array('dn')) { |
|
| 827 | - $loginName = $this->escapeFilterPart($loginName); |
|
| 828 | - $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter); |
|
| 829 | - return $this->fetchListOfUsers($filter, $attributes); |
|
| 830 | - } |
|
| 831 | - |
|
| 832 | - /** |
|
| 833 | - * counts the number of users according to a provided loginName and |
|
| 834 | - * utilizing the login filter. |
|
| 835 | - * |
|
| 836 | - * @param string $loginName |
|
| 837 | - * @return int |
|
| 838 | - */ |
|
| 839 | - public function countUsersByLoginName($loginName) { |
|
| 840 | - $loginName = $this->escapeFilterPart($loginName); |
|
| 841 | - $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter); |
|
| 842 | - return $this->countUsers($filter); |
|
| 843 | - } |
|
| 844 | - |
|
| 845 | - /** |
|
| 846 | - * @param string $filter |
|
| 847 | - * @param string|string[] $attr |
|
| 848 | - * @param int $limit |
|
| 849 | - * @param int $offset |
|
| 850 | - * @param bool $forceApplyAttributes |
|
| 851 | - * @return array |
|
| 852 | - */ |
|
| 853 | - public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) { |
|
| 854 | - $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset); |
|
| 855 | - $recordsToUpdate = $ldapRecords; |
|
| 856 | - if(!$forceApplyAttributes) { |
|
| 857 | - $isBackgroundJobModeAjax = $this->config |
|
| 858 | - ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax'; |
|
| 859 | - $recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) { |
|
| 860 | - $newlyMapped = false; |
|
| 861 | - $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record); |
|
| 862 | - if(is_string($uid)) { |
|
| 863 | - $this->cacheUserExists($uid); |
|
| 864 | - } |
|
| 865 | - return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax); |
|
| 866 | - }); |
|
| 867 | - } |
|
| 868 | - $this->batchApplyUserAttributes($recordsToUpdate); |
|
| 869 | - return $this->fetchList($ldapRecords, count($attr) > 1); |
|
| 870 | - } |
|
| 871 | - |
|
| 872 | - /** |
|
| 873 | - * provided with an array of LDAP user records the method will fetch the |
|
| 874 | - * user object and requests it to process the freshly fetched attributes and |
|
| 875 | - * and their values |
|
| 876 | - * @param array $ldapRecords |
|
| 877 | - */ |
|
| 878 | - public function batchApplyUserAttributes(array $ldapRecords){ |
|
| 879 | - $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName); |
|
| 880 | - foreach($ldapRecords as $userRecord) { |
|
| 881 | - if(!isset($userRecord[$displayNameAttribute])) { |
|
| 882 | - // displayName is obligatory |
|
| 883 | - continue; |
|
| 884 | - } |
|
| 885 | - $ocName = $this->dn2ocname($userRecord['dn'][0], null, true); |
|
| 886 | - if($ocName === false) { |
|
| 887 | - continue; |
|
| 888 | - } |
|
| 889 | - $user = $this->userManager->get($ocName); |
|
| 890 | - if($user instanceof OfflineUser) { |
|
| 891 | - $user->unmark(); |
|
| 892 | - $user = $this->userManager->get($ocName); |
|
| 893 | - } |
|
| 894 | - if ($user !== null) { |
|
| 895 | - $user->processAttributes($userRecord); |
|
| 896 | - } else { |
|
| 897 | - \OC::$server->getLogger()->debug( |
|
| 898 | - "The ldap user manager returned null for $ocName", |
|
| 899 | - ['app'=>'user_ldap'] |
|
| 900 | - ); |
|
| 901 | - } |
|
| 902 | - } |
|
| 903 | - } |
|
| 904 | - |
|
| 905 | - /** |
|
| 906 | - * @param string $filter |
|
| 907 | - * @param string|string[] $attr |
|
| 908 | - * @param int $limit |
|
| 909 | - * @param int $offset |
|
| 910 | - * @return array |
|
| 911 | - */ |
|
| 912 | - public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) { |
|
| 913 | - return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), count($attr) > 1); |
|
| 914 | - } |
|
| 915 | - |
|
| 916 | - /** |
|
| 917 | - * @param array $list |
|
| 918 | - * @param bool $manyAttributes |
|
| 919 | - * @return array |
|
| 920 | - */ |
|
| 921 | - private function fetchList($list, $manyAttributes) { |
|
| 922 | - if(is_array($list)) { |
|
| 923 | - if($manyAttributes) { |
|
| 924 | - return $list; |
|
| 925 | - } else { |
|
| 926 | - $list = array_reduce($list, function($carry, $item) { |
|
| 927 | - $attribute = array_keys($item)[0]; |
|
| 928 | - $carry[] = $item[$attribute][0]; |
|
| 929 | - return $carry; |
|
| 930 | - }, array()); |
|
| 931 | - return array_unique($list, SORT_LOCALE_STRING); |
|
| 932 | - } |
|
| 933 | - } |
|
| 934 | - |
|
| 935 | - //error cause actually, maybe throw an exception in future. |
|
| 936 | - return array(); |
|
| 937 | - } |
|
| 938 | - |
|
| 939 | - /** |
|
| 940 | - * executes an LDAP search, optimized for Users |
|
| 941 | - * @param string $filter the LDAP filter for the search |
|
| 942 | - * @param string|string[] $attr optional, when a certain attribute shall be filtered out |
|
| 943 | - * @param integer $limit |
|
| 944 | - * @param integer $offset |
|
| 945 | - * @return array with the search result |
|
| 946 | - * |
|
| 947 | - * Executes an LDAP search |
|
| 948 | - */ |
|
| 949 | - public function searchUsers($filter, $attr = null, $limit = null, $offset = null) { |
|
| 950 | - return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset); |
|
| 951 | - } |
|
| 952 | - |
|
| 953 | - /** |
|
| 954 | - * @param string $filter |
|
| 955 | - * @param string|string[] $attr |
|
| 956 | - * @param int $limit |
|
| 957 | - * @param int $offset |
|
| 958 | - * @return false|int |
|
| 959 | - */ |
|
| 960 | - public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) { |
|
| 961 | - return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset); |
|
| 962 | - } |
|
| 963 | - |
|
| 964 | - /** |
|
| 965 | - * executes an LDAP search, optimized for Groups |
|
| 966 | - * @param string $filter the LDAP filter for the search |
|
| 967 | - * @param string|string[] $attr optional, when a certain attribute shall be filtered out |
|
| 968 | - * @param integer $limit |
|
| 969 | - * @param integer $offset |
|
| 970 | - * @return array with the search result |
|
| 971 | - * |
|
| 972 | - * Executes an LDAP search |
|
| 973 | - */ |
|
| 974 | - public function searchGroups($filter, $attr = null, $limit = null, $offset = null) { |
|
| 975 | - return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset); |
|
| 976 | - } |
|
| 977 | - |
|
| 978 | - /** |
|
| 979 | - * returns the number of available groups |
|
| 980 | - * @param string $filter the LDAP search filter |
|
| 981 | - * @param string[] $attr optional |
|
| 982 | - * @param int|null $limit |
|
| 983 | - * @param int|null $offset |
|
| 984 | - * @return int|bool |
|
| 985 | - */ |
|
| 986 | - public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) { |
|
| 987 | - return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset); |
|
| 988 | - } |
|
| 989 | - |
|
| 990 | - /** |
|
| 991 | - * returns the number of available objects on the base DN |
|
| 992 | - * |
|
| 993 | - * @param int|null $limit |
|
| 994 | - * @param int|null $offset |
|
| 995 | - * @return int|bool |
|
| 996 | - */ |
|
| 997 | - public function countObjects($limit = null, $offset = null) { |
|
| 998 | - return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset); |
|
| 999 | - } |
|
| 1000 | - |
|
| 1001 | - /** |
|
| 1002 | - * Returns the LDAP handler |
|
| 1003 | - * @throws \OC\ServerNotAvailableException |
|
| 1004 | - */ |
|
| 1005 | - |
|
| 1006 | - /** |
|
| 1007 | - * @return mixed |
|
| 1008 | - * @throws \OC\ServerNotAvailableException |
|
| 1009 | - */ |
|
| 1010 | - private function invokeLDAPMethod() { |
|
| 1011 | - $arguments = func_get_args(); |
|
| 1012 | - $command = array_shift($arguments); |
|
| 1013 | - $cr = array_shift($arguments); |
|
| 1014 | - if (!method_exists($this->ldap, $command)) { |
|
| 1015 | - return null; |
|
| 1016 | - } |
|
| 1017 | - array_unshift($arguments, $cr); |
|
| 1018 | - // php no longer supports call-time pass-by-reference |
|
| 1019 | - // thus cannot support controlPagedResultResponse as the third argument |
|
| 1020 | - // is a reference |
|
| 1021 | - $doMethod = function () use ($command, &$arguments) { |
|
| 1022 | - if ($command == 'controlPagedResultResponse') { |
|
| 1023 | - throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.'); |
|
| 1024 | - } else { |
|
| 1025 | - return call_user_func_array(array($this->ldap, $command), $arguments); |
|
| 1026 | - } |
|
| 1027 | - }; |
|
| 1028 | - try { |
|
| 1029 | - $ret = $doMethod(); |
|
| 1030 | - } catch (ServerNotAvailableException $e) { |
|
| 1031 | - /* Server connection lost, attempt to reestablish it |
|
| 351 | + /** |
|
| 352 | + * Set password for an LDAP user identified by a DN |
|
| 353 | + * |
|
| 354 | + * @param string $userDN the user in question |
|
| 355 | + * @param string $password the new password |
|
| 356 | + * @return bool |
|
| 357 | + * @throws HintException |
|
| 358 | + * @throws \Exception |
|
| 359 | + */ |
|
| 360 | + public function setPassword($userDN, $password) { |
|
| 361 | + if((int)$this->connection->turnOnPasswordChange !== 1) { |
|
| 362 | + throw new \Exception('LDAP password changes are disabled.'); |
|
| 363 | + } |
|
| 364 | + $cr = $this->connection->getConnectionResource(); |
|
| 365 | + if(!$this->ldap->isResource($cr)) { |
|
| 366 | + //LDAP not available |
|
| 367 | + \OCP\Util::writeLog('user_ldap', 'LDAP resource not available.', ILogger::DEBUG); |
|
| 368 | + return false; |
|
| 369 | + } |
|
| 370 | + try { |
|
| 371 | + return @$this->invokeLDAPMethod('modReplace', $cr, $userDN, $password); |
|
| 372 | + } catch(ConstraintViolationException $e) { |
|
| 373 | + throw new HintException('Password change rejected.', \OC::$server->getL10N('user_ldap')->t('Password change rejected. Hint: ').$e->getMessage(), $e->getCode()); |
|
| 374 | + } |
|
| 375 | + } |
|
| 376 | + |
|
| 377 | + /** |
|
| 378 | + * checks whether the given attributes value is probably a DN |
|
| 379 | + * @param string $attr the attribute in question |
|
| 380 | + * @return boolean if so true, otherwise false |
|
| 381 | + */ |
|
| 382 | + private function resemblesDN($attr) { |
|
| 383 | + $resemblingAttributes = array( |
|
| 384 | + 'dn', |
|
| 385 | + 'uniquemember', |
|
| 386 | + 'member', |
|
| 387 | + // memberOf is an "operational" attribute, without a definition in any RFC |
|
| 388 | + 'memberof' |
|
| 389 | + ); |
|
| 390 | + return in_array($attr, $resemblingAttributes); |
|
| 391 | + } |
|
| 392 | + |
|
| 393 | + /** |
|
| 394 | + * checks whether the given string is probably a DN |
|
| 395 | + * @param string $string |
|
| 396 | + * @return boolean |
|
| 397 | + */ |
|
| 398 | + public function stringResemblesDN($string) { |
|
| 399 | + $r = $this->ldap->explodeDN($string, 0); |
|
| 400 | + // if exploding a DN succeeds and does not end up in |
|
| 401 | + // an empty array except for $r[count] being 0. |
|
| 402 | + return (is_array($r) && count($r) > 1); |
|
| 403 | + } |
|
| 404 | + |
|
| 405 | + /** |
|
| 406 | + * returns a DN-string that is cleaned from not domain parts, e.g. |
|
| 407 | + * cn=foo,cn=bar,dc=foobar,dc=server,dc=org |
|
| 408 | + * becomes dc=foobar,dc=server,dc=org |
|
| 409 | + * @param string $dn |
|
| 410 | + * @return string |
|
| 411 | + */ |
|
| 412 | + public function getDomainDNFromDN($dn) { |
|
| 413 | + $allParts = $this->ldap->explodeDN($dn, 0); |
|
| 414 | + if($allParts === false) { |
|
| 415 | + //not a valid DN |
|
| 416 | + return ''; |
|
| 417 | + } |
|
| 418 | + $domainParts = array(); |
|
| 419 | + $dcFound = false; |
|
| 420 | + foreach($allParts as $part) { |
|
| 421 | + if(!$dcFound && strpos($part, 'dc=') === 0) { |
|
| 422 | + $dcFound = true; |
|
| 423 | + } |
|
| 424 | + if($dcFound) { |
|
| 425 | + $domainParts[] = $part; |
|
| 426 | + } |
|
| 427 | + } |
|
| 428 | + return implode(',', $domainParts); |
|
| 429 | + } |
|
| 430 | + |
|
| 431 | + /** |
|
| 432 | + * returns the LDAP DN for the given internal Nextcloud name of the group |
|
| 433 | + * @param string $name the Nextcloud name in question |
|
| 434 | + * @return string|false LDAP DN on success, otherwise false |
|
| 435 | + */ |
|
| 436 | + public function groupname2dn($name) { |
|
| 437 | + return $this->groupMapper->getDNByName($name); |
|
| 438 | + } |
|
| 439 | + |
|
| 440 | + /** |
|
| 441 | + * returns the LDAP DN for the given internal Nextcloud name of the user |
|
| 442 | + * @param string $name the Nextcloud name in question |
|
| 443 | + * @return string|false with the LDAP DN on success, otherwise false |
|
| 444 | + */ |
|
| 445 | + public function username2dn($name) { |
|
| 446 | + $fdn = $this->userMapper->getDNByName($name); |
|
| 447 | + |
|
| 448 | + //Check whether the DN belongs to the Base, to avoid issues on multi- |
|
| 449 | + //server setups |
|
| 450 | + if(is_string($fdn) && $this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) { |
|
| 451 | + return $fdn; |
|
| 452 | + } |
|
| 453 | + |
|
| 454 | + return false; |
|
| 455 | + } |
|
| 456 | + |
|
| 457 | + /** |
|
| 458 | + * returns the internal Nextcloud name for the given LDAP DN of the group, false on DN outside of search DN or failure |
|
| 459 | + * @param string $fdn the dn of the group object |
|
| 460 | + * @param string $ldapName optional, the display name of the object |
|
| 461 | + * @return string|false with the name to use in Nextcloud, false on DN outside of search DN |
|
| 462 | + */ |
|
| 463 | + public function dn2groupname($fdn, $ldapName = null) { |
|
| 464 | + //To avoid bypassing the base DN settings under certain circumstances |
|
| 465 | + //with the group support, check whether the provided DN matches one of |
|
| 466 | + //the given Bases |
|
| 467 | + if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseGroups)) { |
|
| 468 | + return false; |
|
| 469 | + } |
|
| 470 | + |
|
| 471 | + return $this->dn2ocname($fdn, $ldapName, false); |
|
| 472 | + } |
|
| 473 | + |
|
| 474 | + /** |
|
| 475 | + * accepts an array of group DNs and tests whether they match the user |
|
| 476 | + * filter by doing read operations against the group entries. Returns an |
|
| 477 | + * array of DNs that match the filter. |
|
| 478 | + * |
|
| 479 | + * @param string[] $groupDNs |
|
| 480 | + * @return string[] |
|
| 481 | + * @throws ServerNotAvailableException |
|
| 482 | + */ |
|
| 483 | + public function groupsMatchFilter($groupDNs) { |
|
| 484 | + $validGroupDNs = []; |
|
| 485 | + foreach($groupDNs as $dn) { |
|
| 486 | + $cacheKey = 'groupsMatchFilter-'.$dn; |
|
| 487 | + $groupMatchFilter = $this->connection->getFromCache($cacheKey); |
|
| 488 | + if(!is_null($groupMatchFilter)) { |
|
| 489 | + if($groupMatchFilter) { |
|
| 490 | + $validGroupDNs[] = $dn; |
|
| 491 | + } |
|
| 492 | + continue; |
|
| 493 | + } |
|
| 494 | + |
|
| 495 | + // Check the base DN first. If this is not met already, we don't |
|
| 496 | + // need to ask the server at all. |
|
| 497 | + if(!$this->isDNPartOfBase($dn, $this->connection->ldapBaseGroups)) { |
|
| 498 | + $this->connection->writeToCache($cacheKey, false); |
|
| 499 | + continue; |
|
| 500 | + } |
|
| 501 | + |
|
| 502 | + $result = $this->readAttribute($dn, '', $this->connection->ldapGroupFilter); |
|
| 503 | + if(is_array($result)) { |
|
| 504 | + $this->connection->writeToCache($cacheKey, true); |
|
| 505 | + $validGroupDNs[] = $dn; |
|
| 506 | + } else { |
|
| 507 | + $this->connection->writeToCache($cacheKey, false); |
|
| 508 | + } |
|
| 509 | + |
|
| 510 | + } |
|
| 511 | + return $validGroupDNs; |
|
| 512 | + } |
|
| 513 | + |
|
| 514 | + /** |
|
| 515 | + * returns the internal Nextcloud name for the given LDAP DN of the user, false on DN outside of search DN or failure |
|
| 516 | + * @param string $dn the dn of the user object |
|
| 517 | + * @param string $ldapName optional, the display name of the object |
|
| 518 | + * @return string|false with with the name to use in Nextcloud |
|
| 519 | + */ |
|
| 520 | + public function dn2username($fdn, $ldapName = null) { |
|
| 521 | + //To avoid bypassing the base DN settings under certain circumstances |
|
| 522 | + //with the group support, check whether the provided DN matches one of |
|
| 523 | + //the given Bases |
|
| 524 | + if(!$this->isDNPartOfBase($fdn, $this->connection->ldapBaseUsers)) { |
|
| 525 | + return false; |
|
| 526 | + } |
|
| 527 | + |
|
| 528 | + return $this->dn2ocname($fdn, $ldapName, true); |
|
| 529 | + } |
|
| 530 | + |
|
| 531 | + /** |
|
| 532 | + * returns an internal Nextcloud name for the given LDAP DN, false on DN outside of search DN |
|
| 533 | + * |
|
| 534 | + * @param string $fdn the dn of the user object |
|
| 535 | + * @param string|null $ldapName optional, the display name of the object |
|
| 536 | + * @param bool $isUser optional, whether it is a user object (otherwise group assumed) |
|
| 537 | + * @param bool|null $newlyMapped |
|
| 538 | + * @param array|null $record |
|
| 539 | + * @return false|string with with the name to use in Nextcloud |
|
| 540 | + * @throws \Exception |
|
| 541 | + */ |
|
| 542 | + public function dn2ocname($fdn, $ldapName = null, $isUser = true, &$newlyMapped = null, array $record = null) { |
|
| 543 | + $newlyMapped = false; |
|
| 544 | + if($isUser) { |
|
| 545 | + $mapper = $this->getUserMapper(); |
|
| 546 | + $nameAttribute = $this->connection->ldapUserDisplayName; |
|
| 547 | + $filter = $this->connection->ldapUserFilter; |
|
| 548 | + } else { |
|
| 549 | + $mapper = $this->getGroupMapper(); |
|
| 550 | + $nameAttribute = $this->connection->ldapGroupDisplayName; |
|
| 551 | + $filter = $this->connection->ldapGroupFilter; |
|
| 552 | + } |
|
| 553 | + |
|
| 554 | + //let's try to retrieve the Nextcloud name from the mappings table |
|
| 555 | + $ncName = $mapper->getNameByDN($fdn); |
|
| 556 | + if(is_string($ncName)) { |
|
| 557 | + return $ncName; |
|
| 558 | + } |
|
| 559 | + |
|
| 560 | + //second try: get the UUID and check if it is known. Then, update the DN and return the name. |
|
| 561 | + $uuid = $this->getUUID($fdn, $isUser, $record); |
|
| 562 | + if(is_string($uuid)) { |
|
| 563 | + $ncName = $mapper->getNameByUUID($uuid); |
|
| 564 | + if(is_string($ncName)) { |
|
| 565 | + $mapper->setDNbyUUID($fdn, $uuid); |
|
| 566 | + return $ncName; |
|
| 567 | + } |
|
| 568 | + } else { |
|
| 569 | + //If the UUID can't be detected something is foul. |
|
| 570 | + \OCP\Util::writeLog('user_ldap', 'Cannot determine UUID for '.$fdn.'. Skipping.', ILogger::INFO); |
|
| 571 | + return false; |
|
| 572 | + } |
|
| 573 | + |
|
| 574 | + if(is_null($ldapName)) { |
|
| 575 | + $ldapName = $this->readAttribute($fdn, $nameAttribute, $filter); |
|
| 576 | + if(!isset($ldapName[0]) && empty($ldapName[0])) { |
|
| 577 | + \OCP\Util::writeLog('user_ldap', 'No or empty name for '.$fdn.' with filter '.$filter.'.', ILogger::INFO); |
|
| 578 | + return false; |
|
| 579 | + } |
|
| 580 | + $ldapName = $ldapName[0]; |
|
| 581 | + } |
|
| 582 | + |
|
| 583 | + if($isUser) { |
|
| 584 | + $usernameAttribute = (string)$this->connection->ldapExpertUsernameAttr; |
|
| 585 | + if ($usernameAttribute !== '') { |
|
| 586 | + $username = $this->readAttribute($fdn, $usernameAttribute); |
|
| 587 | + $username = $username[0]; |
|
| 588 | + } else { |
|
| 589 | + $username = $uuid; |
|
| 590 | + } |
|
| 591 | + try { |
|
| 592 | + $intName = $this->sanitizeUsername($username); |
|
| 593 | + } catch (\InvalidArgumentException $e) { |
|
| 594 | + \OC::$server->getLogger()->logException($e, [ |
|
| 595 | + 'app' => 'user_ldap', |
|
| 596 | + 'level' => ILogger::WARN, |
|
| 597 | + ]); |
|
| 598 | + // we don't attempt to set a username here. We can go for |
|
| 599 | + // for an alternative 4 digit random number as we would append |
|
| 600 | + // otherwise, however it's likely not enough space in bigger |
|
| 601 | + // setups, and most importantly: this is not intended. |
|
| 602 | + return false; |
|
| 603 | + } |
|
| 604 | + } else { |
|
| 605 | + $intName = $ldapName; |
|
| 606 | + } |
|
| 607 | + |
|
| 608 | + //a new user/group! Add it only if it doesn't conflict with other backend's users or existing groups |
|
| 609 | + //disabling Cache is required to avoid that the new user is cached as not-existing in fooExists check |
|
| 610 | + //NOTE: mind, disabling cache affects only this instance! Using it |
|
| 611 | + // outside of core user management will still cache the user as non-existing. |
|
| 612 | + $originalTTL = $this->connection->ldapCacheTTL; |
|
| 613 | + $this->connection->setConfiguration(array('ldapCacheTTL' => 0)); |
|
| 614 | + if(($isUser && $intName !== '' && !$this->ncUserManager->userExists($intName)) |
|
| 615 | + || (!$isUser && !\OC::$server->getGroupManager()->groupExists($intName))) { |
|
| 616 | + if($mapper->map($fdn, $intName, $uuid)) { |
|
| 617 | + $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL)); |
|
| 618 | + if($this->ncUserManager instanceof PublicEmitter) { |
|
| 619 | + $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]); |
|
| 620 | + } |
|
| 621 | + $newlyMapped = true; |
|
| 622 | + return $intName; |
|
| 623 | + } |
|
| 624 | + } |
|
| 625 | + $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL)); |
|
| 626 | + |
|
| 627 | + $altName = $this->createAltInternalOwnCloudName($intName, $isUser); |
|
| 628 | + if(is_string($altName) && $mapper->map($fdn, $altName, $uuid)) { |
|
| 629 | + if($this->ncUserManager instanceof PublicEmitter) { |
|
| 630 | + $this->ncUserManager->emit('\OC\User', 'assignedUserId', [$intName]); |
|
| 631 | + } |
|
| 632 | + $newlyMapped = true; |
|
| 633 | + return $altName; |
|
| 634 | + } |
|
| 635 | + |
|
| 636 | + //if everything else did not help.. |
|
| 637 | + \OCP\Util::writeLog('user_ldap', 'Could not create unique name for '.$fdn.'.', ILogger::INFO); |
|
| 638 | + return false; |
|
| 639 | + } |
|
| 640 | + |
|
| 641 | + /** |
|
| 642 | + * gives back the user names as they are used ownClod internally |
|
| 643 | + * @param array $ldapUsers as returned by fetchList() |
|
| 644 | + * @return array an array with the user names to use in Nextcloud |
|
| 645 | + * |
|
| 646 | + * gives back the user names as they are used ownClod internally |
|
| 647 | + */ |
|
| 648 | + public function nextcloudUserNames($ldapUsers) { |
|
| 649 | + return $this->ldap2NextcloudNames($ldapUsers, true); |
|
| 650 | + } |
|
| 651 | + |
|
| 652 | + /** |
|
| 653 | + * gives back the group names as they are used ownClod internally |
|
| 654 | + * @param array $ldapGroups as returned by fetchList() |
|
| 655 | + * @return array an array with the group names to use in Nextcloud |
|
| 656 | + * |
|
| 657 | + * gives back the group names as they are used ownClod internally |
|
| 658 | + */ |
|
| 659 | + public function nextcloudGroupNames($ldapGroups) { |
|
| 660 | + return $this->ldap2NextcloudNames($ldapGroups, false); |
|
| 661 | + } |
|
| 662 | + |
|
| 663 | + /** |
|
| 664 | + * @param array $ldapObjects as returned by fetchList() |
|
| 665 | + * @param bool $isUsers |
|
| 666 | + * @return array |
|
| 667 | + */ |
|
| 668 | + private function ldap2NextcloudNames($ldapObjects, $isUsers) { |
|
| 669 | + if($isUsers) { |
|
| 670 | + $nameAttribute = $this->connection->ldapUserDisplayName; |
|
| 671 | + $sndAttribute = $this->connection->ldapUserDisplayName2; |
|
| 672 | + } else { |
|
| 673 | + $nameAttribute = $this->connection->ldapGroupDisplayName; |
|
| 674 | + } |
|
| 675 | + $nextcloudNames = array(); |
|
| 676 | + |
|
| 677 | + foreach($ldapObjects as $ldapObject) { |
|
| 678 | + $nameByLDAP = null; |
|
| 679 | + if( isset($ldapObject[$nameAttribute]) |
|
| 680 | + && is_array($ldapObject[$nameAttribute]) |
|
| 681 | + && isset($ldapObject[$nameAttribute][0]) |
|
| 682 | + ) { |
|
| 683 | + // might be set, but not necessarily. if so, we use it. |
|
| 684 | + $nameByLDAP = $ldapObject[$nameAttribute][0]; |
|
| 685 | + } |
|
| 686 | + |
|
| 687 | + $ncName = $this->dn2ocname($ldapObject['dn'][0], $nameByLDAP, $isUsers); |
|
| 688 | + if($ncName) { |
|
| 689 | + $nextcloudNames[] = $ncName; |
|
| 690 | + if($isUsers) { |
|
| 691 | + //cache the user names so it does not need to be retrieved |
|
| 692 | + //again later (e.g. sharing dialogue). |
|
| 693 | + if(is_null($nameByLDAP)) { |
|
| 694 | + continue; |
|
| 695 | + } |
|
| 696 | + $sndName = isset($ldapObject[$sndAttribute][0]) |
|
| 697 | + ? $ldapObject[$sndAttribute][0] : ''; |
|
| 698 | + $this->cacheUserDisplayName($ncName, $nameByLDAP, $sndName); |
|
| 699 | + } |
|
| 700 | + } |
|
| 701 | + } |
|
| 702 | + return $nextcloudNames; |
|
| 703 | + } |
|
| 704 | + |
|
| 705 | + /** |
|
| 706 | + * caches the user display name |
|
| 707 | + * @param string $ocName the internal Nextcloud username |
|
| 708 | + * @param string|false $home the home directory path |
|
| 709 | + */ |
|
| 710 | + public function cacheUserHome($ocName, $home) { |
|
| 711 | + $cacheKey = 'getHome'.$ocName; |
|
| 712 | + $this->connection->writeToCache($cacheKey, $home); |
|
| 713 | + } |
|
| 714 | + |
|
| 715 | + /** |
|
| 716 | + * caches a user as existing |
|
| 717 | + * @param string $ocName the internal Nextcloud username |
|
| 718 | + */ |
|
| 719 | + public function cacheUserExists($ocName) { |
|
| 720 | + $this->connection->writeToCache('userExists'.$ocName, true); |
|
| 721 | + } |
|
| 722 | + |
|
| 723 | + /** |
|
| 724 | + * caches the user display name |
|
| 725 | + * @param string $ocName the internal Nextcloud username |
|
| 726 | + * @param string $displayName the display name |
|
| 727 | + * @param string $displayName2 the second display name |
|
| 728 | + */ |
|
| 729 | + public function cacheUserDisplayName($ocName, $displayName, $displayName2 = '') { |
|
| 730 | + $user = $this->userManager->get($ocName); |
|
| 731 | + if($user === null) { |
|
| 732 | + return; |
|
| 733 | + } |
|
| 734 | + $displayName = $user->composeAndStoreDisplayName($displayName, $displayName2); |
|
| 735 | + $cacheKeyTrunk = 'getDisplayName'; |
|
| 736 | + $this->connection->writeToCache($cacheKeyTrunk.$ocName, $displayName); |
|
| 737 | + } |
|
| 738 | + |
|
| 739 | + /** |
|
| 740 | + * creates a unique name for internal Nextcloud use for users. Don't call it directly. |
|
| 741 | + * @param string $name the display name of the object |
|
| 742 | + * @return string|false with with the name to use in Nextcloud or false if unsuccessful |
|
| 743 | + * |
|
| 744 | + * Instead of using this method directly, call |
|
| 745 | + * createAltInternalOwnCloudName($name, true) |
|
| 746 | + */ |
|
| 747 | + private function _createAltInternalOwnCloudNameForUsers($name) { |
|
| 748 | + $attempts = 0; |
|
| 749 | + //while loop is just a precaution. If a name is not generated within |
|
| 750 | + //20 attempts, something else is very wrong. Avoids infinite loop. |
|
| 751 | + while($attempts < 20){ |
|
| 752 | + $altName = $name . '_' . rand(1000,9999); |
|
| 753 | + if(!$this->ncUserManager->userExists($altName)) { |
|
| 754 | + return $altName; |
|
| 755 | + } |
|
| 756 | + $attempts++; |
|
| 757 | + } |
|
| 758 | + return false; |
|
| 759 | + } |
|
| 760 | + |
|
| 761 | + /** |
|
| 762 | + * creates a unique name for internal Nextcloud use for groups. Don't call it directly. |
|
| 763 | + * @param string $name the display name of the object |
|
| 764 | + * @return string|false with with the name to use in Nextcloud or false if unsuccessful. |
|
| 765 | + * |
|
| 766 | + * Instead of using this method directly, call |
|
| 767 | + * createAltInternalOwnCloudName($name, false) |
|
| 768 | + * |
|
| 769 | + * Group names are also used as display names, so we do a sequential |
|
| 770 | + * numbering, e.g. Developers_42 when there are 41 other groups called |
|
| 771 | + * "Developers" |
|
| 772 | + */ |
|
| 773 | + private function _createAltInternalOwnCloudNameForGroups($name) { |
|
| 774 | + $usedNames = $this->groupMapper->getNamesBySearch($name, "", '_%'); |
|
| 775 | + if(!$usedNames || count($usedNames) === 0) { |
|
| 776 | + $lastNo = 1; //will become name_2 |
|
| 777 | + } else { |
|
| 778 | + natsort($usedNames); |
|
| 779 | + $lastName = array_pop($usedNames); |
|
| 780 | + $lastNo = (int)substr($lastName, strrpos($lastName, '_') + 1); |
|
| 781 | + } |
|
| 782 | + $altName = $name.'_'. (string)($lastNo+1); |
|
| 783 | + unset($usedNames); |
|
| 784 | + |
|
| 785 | + $attempts = 1; |
|
| 786 | + while($attempts < 21){ |
|
| 787 | + // Check to be really sure it is unique |
|
| 788 | + // while loop is just a precaution. If a name is not generated within |
|
| 789 | + // 20 attempts, something else is very wrong. Avoids infinite loop. |
|
| 790 | + if(!\OC::$server->getGroupManager()->groupExists($altName)) { |
|
| 791 | + return $altName; |
|
| 792 | + } |
|
| 793 | + $altName = $name . '_' . ($lastNo + $attempts); |
|
| 794 | + $attempts++; |
|
| 795 | + } |
|
| 796 | + return false; |
|
| 797 | + } |
|
| 798 | + |
|
| 799 | + /** |
|
| 800 | + * creates a unique name for internal Nextcloud use. |
|
| 801 | + * @param string $name the display name of the object |
|
| 802 | + * @param boolean $isUser whether name should be created for a user (true) or a group (false) |
|
| 803 | + * @return string|false with with the name to use in Nextcloud or false if unsuccessful |
|
| 804 | + */ |
|
| 805 | + private function createAltInternalOwnCloudName($name, $isUser) { |
|
| 806 | + $originalTTL = $this->connection->ldapCacheTTL; |
|
| 807 | + $this->connection->setConfiguration(array('ldapCacheTTL' => 0)); |
|
| 808 | + if($isUser) { |
|
| 809 | + $altName = $this->_createAltInternalOwnCloudNameForUsers($name); |
|
| 810 | + } else { |
|
| 811 | + $altName = $this->_createAltInternalOwnCloudNameForGroups($name); |
|
| 812 | + } |
|
| 813 | + $this->connection->setConfiguration(array('ldapCacheTTL' => $originalTTL)); |
|
| 814 | + |
|
| 815 | + return $altName; |
|
| 816 | + } |
|
| 817 | + |
|
| 818 | + /** |
|
| 819 | + * fetches a list of users according to a provided loginName and utilizing |
|
| 820 | + * the login filter. |
|
| 821 | + * |
|
| 822 | + * @param string $loginName |
|
| 823 | + * @param array $attributes optional, list of attributes to read |
|
| 824 | + * @return array |
|
| 825 | + */ |
|
| 826 | + public function fetchUsersByLoginName($loginName, $attributes = array('dn')) { |
|
| 827 | + $loginName = $this->escapeFilterPart($loginName); |
|
| 828 | + $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter); |
|
| 829 | + return $this->fetchListOfUsers($filter, $attributes); |
|
| 830 | + } |
|
| 831 | + |
|
| 832 | + /** |
|
| 833 | + * counts the number of users according to a provided loginName and |
|
| 834 | + * utilizing the login filter. |
|
| 835 | + * |
|
| 836 | + * @param string $loginName |
|
| 837 | + * @return int |
|
| 838 | + */ |
|
| 839 | + public function countUsersByLoginName($loginName) { |
|
| 840 | + $loginName = $this->escapeFilterPart($loginName); |
|
| 841 | + $filter = str_replace('%uid', $loginName, $this->connection->ldapLoginFilter); |
|
| 842 | + return $this->countUsers($filter); |
|
| 843 | + } |
|
| 844 | + |
|
| 845 | + /** |
|
| 846 | + * @param string $filter |
|
| 847 | + * @param string|string[] $attr |
|
| 848 | + * @param int $limit |
|
| 849 | + * @param int $offset |
|
| 850 | + * @param bool $forceApplyAttributes |
|
| 851 | + * @return array |
|
| 852 | + */ |
|
| 853 | + public function fetchListOfUsers($filter, $attr, $limit = null, $offset = null, $forceApplyAttributes = false) { |
|
| 854 | + $ldapRecords = $this->searchUsers($filter, $attr, $limit, $offset); |
|
| 855 | + $recordsToUpdate = $ldapRecords; |
|
| 856 | + if(!$forceApplyAttributes) { |
|
| 857 | + $isBackgroundJobModeAjax = $this->config |
|
| 858 | + ->getAppValue('core', 'backgroundjobs_mode', 'ajax') === 'ajax'; |
|
| 859 | + $recordsToUpdate = array_filter($ldapRecords, function($record) use ($isBackgroundJobModeAjax) { |
|
| 860 | + $newlyMapped = false; |
|
| 861 | + $uid = $this->dn2ocname($record['dn'][0], null, true, $newlyMapped, $record); |
|
| 862 | + if(is_string($uid)) { |
|
| 863 | + $this->cacheUserExists($uid); |
|
| 864 | + } |
|
| 865 | + return ($uid !== false) && ($newlyMapped || $isBackgroundJobModeAjax); |
|
| 866 | + }); |
|
| 867 | + } |
|
| 868 | + $this->batchApplyUserAttributes($recordsToUpdate); |
|
| 869 | + return $this->fetchList($ldapRecords, count($attr) > 1); |
|
| 870 | + } |
|
| 871 | + |
|
| 872 | + /** |
|
| 873 | + * provided with an array of LDAP user records the method will fetch the |
|
| 874 | + * user object and requests it to process the freshly fetched attributes and |
|
| 875 | + * and their values |
|
| 876 | + * @param array $ldapRecords |
|
| 877 | + */ |
|
| 878 | + public function batchApplyUserAttributes(array $ldapRecords){ |
|
| 879 | + $displayNameAttribute = strtolower($this->connection->ldapUserDisplayName); |
|
| 880 | + foreach($ldapRecords as $userRecord) { |
|
| 881 | + if(!isset($userRecord[$displayNameAttribute])) { |
|
| 882 | + // displayName is obligatory |
|
| 883 | + continue; |
|
| 884 | + } |
|
| 885 | + $ocName = $this->dn2ocname($userRecord['dn'][0], null, true); |
|
| 886 | + if($ocName === false) { |
|
| 887 | + continue; |
|
| 888 | + } |
|
| 889 | + $user = $this->userManager->get($ocName); |
|
| 890 | + if($user instanceof OfflineUser) { |
|
| 891 | + $user->unmark(); |
|
| 892 | + $user = $this->userManager->get($ocName); |
|
| 893 | + } |
|
| 894 | + if ($user !== null) { |
|
| 895 | + $user->processAttributes($userRecord); |
|
| 896 | + } else { |
|
| 897 | + \OC::$server->getLogger()->debug( |
|
| 898 | + "The ldap user manager returned null for $ocName", |
|
| 899 | + ['app'=>'user_ldap'] |
|
| 900 | + ); |
|
| 901 | + } |
|
| 902 | + } |
|
| 903 | + } |
|
| 904 | + |
|
| 905 | + /** |
|
| 906 | + * @param string $filter |
|
| 907 | + * @param string|string[] $attr |
|
| 908 | + * @param int $limit |
|
| 909 | + * @param int $offset |
|
| 910 | + * @return array |
|
| 911 | + */ |
|
| 912 | + public function fetchListOfGroups($filter, $attr, $limit = null, $offset = null) { |
|
| 913 | + return $this->fetchList($this->searchGroups($filter, $attr, $limit, $offset), count($attr) > 1); |
|
| 914 | + } |
|
| 915 | + |
|
| 916 | + /** |
|
| 917 | + * @param array $list |
|
| 918 | + * @param bool $manyAttributes |
|
| 919 | + * @return array |
|
| 920 | + */ |
|
| 921 | + private function fetchList($list, $manyAttributes) { |
|
| 922 | + if(is_array($list)) { |
|
| 923 | + if($manyAttributes) { |
|
| 924 | + return $list; |
|
| 925 | + } else { |
|
| 926 | + $list = array_reduce($list, function($carry, $item) { |
|
| 927 | + $attribute = array_keys($item)[0]; |
|
| 928 | + $carry[] = $item[$attribute][0]; |
|
| 929 | + return $carry; |
|
| 930 | + }, array()); |
|
| 931 | + return array_unique($list, SORT_LOCALE_STRING); |
|
| 932 | + } |
|
| 933 | + } |
|
| 934 | + |
|
| 935 | + //error cause actually, maybe throw an exception in future. |
|
| 936 | + return array(); |
|
| 937 | + } |
|
| 938 | + |
|
| 939 | + /** |
|
| 940 | + * executes an LDAP search, optimized for Users |
|
| 941 | + * @param string $filter the LDAP filter for the search |
|
| 942 | + * @param string|string[] $attr optional, when a certain attribute shall be filtered out |
|
| 943 | + * @param integer $limit |
|
| 944 | + * @param integer $offset |
|
| 945 | + * @return array with the search result |
|
| 946 | + * |
|
| 947 | + * Executes an LDAP search |
|
| 948 | + */ |
|
| 949 | + public function searchUsers($filter, $attr = null, $limit = null, $offset = null) { |
|
| 950 | + return $this->search($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset); |
|
| 951 | + } |
|
| 952 | + |
|
| 953 | + /** |
|
| 954 | + * @param string $filter |
|
| 955 | + * @param string|string[] $attr |
|
| 956 | + * @param int $limit |
|
| 957 | + * @param int $offset |
|
| 958 | + * @return false|int |
|
| 959 | + */ |
|
| 960 | + public function countUsers($filter, $attr = array('dn'), $limit = null, $offset = null) { |
|
| 961 | + return $this->count($filter, $this->connection->ldapBaseUsers, $attr, $limit, $offset); |
|
| 962 | + } |
|
| 963 | + |
|
| 964 | + /** |
|
| 965 | + * executes an LDAP search, optimized for Groups |
|
| 966 | + * @param string $filter the LDAP filter for the search |
|
| 967 | + * @param string|string[] $attr optional, when a certain attribute shall be filtered out |
|
| 968 | + * @param integer $limit |
|
| 969 | + * @param integer $offset |
|
| 970 | + * @return array with the search result |
|
| 971 | + * |
|
| 972 | + * Executes an LDAP search |
|
| 973 | + */ |
|
| 974 | + public function searchGroups($filter, $attr = null, $limit = null, $offset = null) { |
|
| 975 | + return $this->search($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset); |
|
| 976 | + } |
|
| 977 | + |
|
| 978 | + /** |
|
| 979 | + * returns the number of available groups |
|
| 980 | + * @param string $filter the LDAP search filter |
|
| 981 | + * @param string[] $attr optional |
|
| 982 | + * @param int|null $limit |
|
| 983 | + * @param int|null $offset |
|
| 984 | + * @return int|bool |
|
| 985 | + */ |
|
| 986 | + public function countGroups($filter, $attr = array('dn'), $limit = null, $offset = null) { |
|
| 987 | + return $this->count($filter, $this->connection->ldapBaseGroups, $attr, $limit, $offset); |
|
| 988 | + } |
|
| 989 | + |
|
| 990 | + /** |
|
| 991 | + * returns the number of available objects on the base DN |
|
| 992 | + * |
|
| 993 | + * @param int|null $limit |
|
| 994 | + * @param int|null $offset |
|
| 995 | + * @return int|bool |
|
| 996 | + */ |
|
| 997 | + public function countObjects($limit = null, $offset = null) { |
|
| 998 | + return $this->count('objectclass=*', $this->connection->ldapBase, array('dn'), $limit, $offset); |
|
| 999 | + } |
|
| 1000 | + |
|
| 1001 | + /** |
|
| 1002 | + * Returns the LDAP handler |
|
| 1003 | + * @throws \OC\ServerNotAvailableException |
|
| 1004 | + */ |
|
| 1005 | + |
|
| 1006 | + /** |
|
| 1007 | + * @return mixed |
|
| 1008 | + * @throws \OC\ServerNotAvailableException |
|
| 1009 | + */ |
|
| 1010 | + private function invokeLDAPMethod() { |
|
| 1011 | + $arguments = func_get_args(); |
|
| 1012 | + $command = array_shift($arguments); |
|
| 1013 | + $cr = array_shift($arguments); |
|
| 1014 | + if (!method_exists($this->ldap, $command)) { |
|
| 1015 | + return null; |
|
| 1016 | + } |
|
| 1017 | + array_unshift($arguments, $cr); |
|
| 1018 | + // php no longer supports call-time pass-by-reference |
|
| 1019 | + // thus cannot support controlPagedResultResponse as the third argument |
|
| 1020 | + // is a reference |
|
| 1021 | + $doMethod = function () use ($command, &$arguments) { |
|
| 1022 | + if ($command == 'controlPagedResultResponse') { |
|
| 1023 | + throw new \InvalidArgumentException('Invoker does not support controlPagedResultResponse, call LDAP Wrapper directly instead.'); |
|
| 1024 | + } else { |
|
| 1025 | + return call_user_func_array(array($this->ldap, $command), $arguments); |
|
| 1026 | + } |
|
| 1027 | + }; |
|
| 1028 | + try { |
|
| 1029 | + $ret = $doMethod(); |
|
| 1030 | + } catch (ServerNotAvailableException $e) { |
|
| 1031 | + /* Server connection lost, attempt to reestablish it |
|
| 1032 | 1032 | * Maybe implement exponential backoff? |
| 1033 | 1033 | * This was enough to get solr indexer working which has large delays between LDAP fetches. |
| 1034 | 1034 | */ |
| 1035 | - \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG); |
|
| 1036 | - $this->connection->resetConnectionResource(); |
|
| 1037 | - $cr = $this->connection->getConnectionResource(); |
|
| 1038 | - |
|
| 1039 | - if(!$this->ldap->isResource($cr)) { |
|
| 1040 | - // Seems like we didn't find any resource. |
|
| 1041 | - \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG); |
|
| 1042 | - throw $e; |
|
| 1043 | - } |
|
| 1044 | - |
|
| 1045 | - $arguments[0] = array_pad([], count($arguments[0]), $cr); |
|
| 1046 | - $ret = $doMethod(); |
|
| 1047 | - } |
|
| 1048 | - return $ret; |
|
| 1049 | - } |
|
| 1050 | - |
|
| 1051 | - /** |
|
| 1052 | - * retrieved. Results will according to the order in the array. |
|
| 1053 | - * |
|
| 1054 | - * @param $filter |
|
| 1055 | - * @param $base |
|
| 1056 | - * @param string[]|string|null $attr |
|
| 1057 | - * @param int $limit optional, maximum results to be counted |
|
| 1058 | - * @param int $offset optional, a starting point |
|
| 1059 | - * @return array|false array with the search result as first value and pagedSearchOK as |
|
| 1060 | - * second | false if not successful |
|
| 1061 | - * @throws ServerNotAvailableException |
|
| 1062 | - */ |
|
| 1063 | - private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) { |
|
| 1064 | - if(!is_null($attr) && !is_array($attr)) { |
|
| 1065 | - $attr = array(mb_strtolower($attr, 'UTF-8')); |
|
| 1066 | - } |
|
| 1067 | - |
|
| 1068 | - // See if we have a resource, in case not cancel with message |
|
| 1069 | - $cr = $this->connection->getConnectionResource(); |
|
| 1070 | - if(!$this->ldap->isResource($cr)) { |
|
| 1071 | - // Seems like we didn't find any resource. |
|
| 1072 | - // Return an empty array just like before. |
|
| 1073 | - \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG); |
|
| 1074 | - return false; |
|
| 1075 | - } |
|
| 1076 | - |
|
| 1077 | - //check whether paged search should be attempted |
|
| 1078 | - $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset); |
|
| 1079 | - |
|
| 1080 | - $linkResources = array_pad(array(), count($base), $cr); |
|
| 1081 | - $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr); |
|
| 1082 | - // cannot use $cr anymore, might have changed in the previous call! |
|
| 1083 | - $error = $this->ldap->errno($this->connection->getConnectionResource()); |
|
| 1084 | - if(!is_array($sr) || $error !== 0) { |
|
| 1085 | - \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), ILogger::ERROR); |
|
| 1086 | - return false; |
|
| 1087 | - } |
|
| 1088 | - |
|
| 1089 | - return array($sr, $pagedSearchOK); |
|
| 1090 | - } |
|
| 1091 | - |
|
| 1092 | - /** |
|
| 1093 | - * processes an LDAP paged search operation |
|
| 1094 | - * @param array $sr the array containing the LDAP search resources |
|
| 1095 | - * @param string $filter the LDAP filter for the search |
|
| 1096 | - * @param array $base an array containing the LDAP subtree(s) that shall be searched |
|
| 1097 | - * @param int $iFoundItems number of results in the single search operation |
|
| 1098 | - * @param int $limit maximum results to be counted |
|
| 1099 | - * @param int $offset a starting point |
|
| 1100 | - * @param bool $pagedSearchOK whether a paged search has been executed |
|
| 1101 | - * @param bool $skipHandling required for paged search when cookies to |
|
| 1102 | - * prior results need to be gained |
|
| 1103 | - * @return bool cookie validity, true if we have more pages, false otherwise. |
|
| 1104 | - */ |
|
| 1105 | - private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) { |
|
| 1106 | - $cookie = null; |
|
| 1107 | - if($pagedSearchOK) { |
|
| 1108 | - $cr = $this->connection->getConnectionResource(); |
|
| 1109 | - foreach($sr as $key => $res) { |
|
| 1110 | - if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) { |
|
| 1111 | - $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie); |
|
| 1112 | - } |
|
| 1113 | - } |
|
| 1114 | - |
|
| 1115 | - //browsing through prior pages to get the cookie for the new one |
|
| 1116 | - if($skipHandling) { |
|
| 1117 | - return false; |
|
| 1118 | - } |
|
| 1119 | - // if count is bigger, then the server does not support |
|
| 1120 | - // paged search. Instead, he did a normal search. We set a |
|
| 1121 | - // flag here, so the callee knows how to deal with it. |
|
| 1122 | - if($iFoundItems <= $limit) { |
|
| 1123 | - $this->pagedSearchedSuccessful = true; |
|
| 1124 | - } |
|
| 1125 | - } else { |
|
| 1126 | - if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) { |
|
| 1127 | - \OC::$server->getLogger()->debug( |
|
| 1128 | - 'Paged search was not available', |
|
| 1129 | - [ 'app' => 'user_ldap' ] |
|
| 1130 | - ); |
|
| 1131 | - } |
|
| 1132 | - } |
|
| 1133 | - /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1035 | + \OCP\Util::writeLog('user_ldap', "Connection lost on $command, attempting to reestablish.", ILogger::DEBUG); |
|
| 1036 | + $this->connection->resetConnectionResource(); |
|
| 1037 | + $cr = $this->connection->getConnectionResource(); |
|
| 1038 | + |
|
| 1039 | + if(!$this->ldap->isResource($cr)) { |
|
| 1040 | + // Seems like we didn't find any resource. |
|
| 1041 | + \OCP\Util::writeLog('user_ldap', "Could not $command, because resource is missing.", ILogger::DEBUG); |
|
| 1042 | + throw $e; |
|
| 1043 | + } |
|
| 1044 | + |
|
| 1045 | + $arguments[0] = array_pad([], count($arguments[0]), $cr); |
|
| 1046 | + $ret = $doMethod(); |
|
| 1047 | + } |
|
| 1048 | + return $ret; |
|
| 1049 | + } |
|
| 1050 | + |
|
| 1051 | + /** |
|
| 1052 | + * retrieved. Results will according to the order in the array. |
|
| 1053 | + * |
|
| 1054 | + * @param $filter |
|
| 1055 | + * @param $base |
|
| 1056 | + * @param string[]|string|null $attr |
|
| 1057 | + * @param int $limit optional, maximum results to be counted |
|
| 1058 | + * @param int $offset optional, a starting point |
|
| 1059 | + * @return array|false array with the search result as first value and pagedSearchOK as |
|
| 1060 | + * second | false if not successful |
|
| 1061 | + * @throws ServerNotAvailableException |
|
| 1062 | + */ |
|
| 1063 | + private function executeSearch($filter, $base, &$attr = null, $limit = null, $offset = null) { |
|
| 1064 | + if(!is_null($attr) && !is_array($attr)) { |
|
| 1065 | + $attr = array(mb_strtolower($attr, 'UTF-8')); |
|
| 1066 | + } |
|
| 1067 | + |
|
| 1068 | + // See if we have a resource, in case not cancel with message |
|
| 1069 | + $cr = $this->connection->getConnectionResource(); |
|
| 1070 | + if(!$this->ldap->isResource($cr)) { |
|
| 1071 | + // Seems like we didn't find any resource. |
|
| 1072 | + // Return an empty array just like before. |
|
| 1073 | + \OCP\Util::writeLog('user_ldap', 'Could not search, because resource is missing.', ILogger::DEBUG); |
|
| 1074 | + return false; |
|
| 1075 | + } |
|
| 1076 | + |
|
| 1077 | + //check whether paged search should be attempted |
|
| 1078 | + $pagedSearchOK = $this->initPagedSearch($filter, $base, $attr, (int)$limit, $offset); |
|
| 1079 | + |
|
| 1080 | + $linkResources = array_pad(array(), count($base), $cr); |
|
| 1081 | + $sr = $this->invokeLDAPMethod('search', $linkResources, $base, $filter, $attr); |
|
| 1082 | + // cannot use $cr anymore, might have changed in the previous call! |
|
| 1083 | + $error = $this->ldap->errno($this->connection->getConnectionResource()); |
|
| 1084 | + if(!is_array($sr) || $error !== 0) { |
|
| 1085 | + \OCP\Util::writeLog('user_ldap', 'Attempt for Paging? '.print_r($pagedSearchOK, true), ILogger::ERROR); |
|
| 1086 | + return false; |
|
| 1087 | + } |
|
| 1088 | + |
|
| 1089 | + return array($sr, $pagedSearchOK); |
|
| 1090 | + } |
|
| 1091 | + |
|
| 1092 | + /** |
|
| 1093 | + * processes an LDAP paged search operation |
|
| 1094 | + * @param array $sr the array containing the LDAP search resources |
|
| 1095 | + * @param string $filter the LDAP filter for the search |
|
| 1096 | + * @param array $base an array containing the LDAP subtree(s) that shall be searched |
|
| 1097 | + * @param int $iFoundItems number of results in the single search operation |
|
| 1098 | + * @param int $limit maximum results to be counted |
|
| 1099 | + * @param int $offset a starting point |
|
| 1100 | + * @param bool $pagedSearchOK whether a paged search has been executed |
|
| 1101 | + * @param bool $skipHandling required for paged search when cookies to |
|
| 1102 | + * prior results need to be gained |
|
| 1103 | + * @return bool cookie validity, true if we have more pages, false otherwise. |
|
| 1104 | + */ |
|
| 1105 | + private function processPagedSearchStatus($sr, $filter, $base, $iFoundItems, $limit, $offset, $pagedSearchOK, $skipHandling) { |
|
| 1106 | + $cookie = null; |
|
| 1107 | + if($pagedSearchOK) { |
|
| 1108 | + $cr = $this->connection->getConnectionResource(); |
|
| 1109 | + foreach($sr as $key => $res) { |
|
| 1110 | + if($this->ldap->controlPagedResultResponse($cr, $res, $cookie)) { |
|
| 1111 | + $this->setPagedResultCookie($base[$key], $filter, $limit, $offset, $cookie); |
|
| 1112 | + } |
|
| 1113 | + } |
|
| 1114 | + |
|
| 1115 | + //browsing through prior pages to get the cookie for the new one |
|
| 1116 | + if($skipHandling) { |
|
| 1117 | + return false; |
|
| 1118 | + } |
|
| 1119 | + // if count is bigger, then the server does not support |
|
| 1120 | + // paged search. Instead, he did a normal search. We set a |
|
| 1121 | + // flag here, so the callee knows how to deal with it. |
|
| 1122 | + if($iFoundItems <= $limit) { |
|
| 1123 | + $this->pagedSearchedSuccessful = true; |
|
| 1124 | + } |
|
| 1125 | + } else { |
|
| 1126 | + if(!is_null($limit) && (int)$this->connection->ldapPagingSize !== 0) { |
|
| 1127 | + \OC::$server->getLogger()->debug( |
|
| 1128 | + 'Paged search was not available', |
|
| 1129 | + [ 'app' => 'user_ldap' ] |
|
| 1130 | + ); |
|
| 1131 | + } |
|
| 1132 | + } |
|
| 1133 | + /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1134 | 1134 | * Return cookie status. If we don't have more pages, with RHDS |
| 1135 | 1135 | * cookie is null, with openldap cookie is an empty string and |
| 1136 | 1136 | * to 386ds '0' is a valid cookie. Even if $iFoundItems == 0 |
| 1137 | 1137 | */ |
| 1138 | - return !empty($cookie) || $cookie === '0'; |
|
| 1139 | - } |
|
| 1140 | - |
|
| 1141 | - /** |
|
| 1142 | - * executes an LDAP search, but counts the results only |
|
| 1143 | - * |
|
| 1144 | - * @param string $filter the LDAP filter for the search |
|
| 1145 | - * @param array $base an array containing the LDAP subtree(s) that shall be searched |
|
| 1146 | - * @param string|string[] $attr optional, array, one or more attributes that shall be |
|
| 1147 | - * retrieved. Results will according to the order in the array. |
|
| 1148 | - * @param int $limit optional, maximum results to be counted |
|
| 1149 | - * @param int $offset optional, a starting point |
|
| 1150 | - * @param bool $skipHandling indicates whether the pages search operation is |
|
| 1151 | - * completed |
|
| 1152 | - * @return int|false Integer or false if the search could not be initialized |
|
| 1153 | - * @throws ServerNotAvailableException |
|
| 1154 | - */ |
|
| 1155 | - private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) { |
|
| 1156 | - \OCP\Util::writeLog('user_ldap', 'Count filter: '.print_r($filter, true), ILogger::DEBUG); |
|
| 1157 | - |
|
| 1158 | - $limitPerPage = (int)$this->connection->ldapPagingSize; |
|
| 1159 | - if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) { |
|
| 1160 | - $limitPerPage = $limit; |
|
| 1161 | - } |
|
| 1162 | - |
|
| 1163 | - $counter = 0; |
|
| 1164 | - $count = null; |
|
| 1165 | - $this->connection->getConnectionResource(); |
|
| 1166 | - |
|
| 1167 | - do { |
|
| 1168 | - $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset); |
|
| 1169 | - if($search === false) { |
|
| 1170 | - return $counter > 0 ? $counter : false; |
|
| 1171 | - } |
|
| 1172 | - list($sr, $pagedSearchOK) = $search; |
|
| 1173 | - |
|
| 1174 | - /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1138 | + return !empty($cookie) || $cookie === '0'; |
|
| 1139 | + } |
|
| 1140 | + |
|
| 1141 | + /** |
|
| 1142 | + * executes an LDAP search, but counts the results only |
|
| 1143 | + * |
|
| 1144 | + * @param string $filter the LDAP filter for the search |
|
| 1145 | + * @param array $base an array containing the LDAP subtree(s) that shall be searched |
|
| 1146 | + * @param string|string[] $attr optional, array, one or more attributes that shall be |
|
| 1147 | + * retrieved. Results will according to the order in the array. |
|
| 1148 | + * @param int $limit optional, maximum results to be counted |
|
| 1149 | + * @param int $offset optional, a starting point |
|
| 1150 | + * @param bool $skipHandling indicates whether the pages search operation is |
|
| 1151 | + * completed |
|
| 1152 | + * @return int|false Integer or false if the search could not be initialized |
|
| 1153 | + * @throws ServerNotAvailableException |
|
| 1154 | + */ |
|
| 1155 | + private function count($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) { |
|
| 1156 | + \OCP\Util::writeLog('user_ldap', 'Count filter: '.print_r($filter, true), ILogger::DEBUG); |
|
| 1157 | + |
|
| 1158 | + $limitPerPage = (int)$this->connection->ldapPagingSize; |
|
| 1159 | + if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) { |
|
| 1160 | + $limitPerPage = $limit; |
|
| 1161 | + } |
|
| 1162 | + |
|
| 1163 | + $counter = 0; |
|
| 1164 | + $count = null; |
|
| 1165 | + $this->connection->getConnectionResource(); |
|
| 1166 | + |
|
| 1167 | + do { |
|
| 1168 | + $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset); |
|
| 1169 | + if($search === false) { |
|
| 1170 | + return $counter > 0 ? $counter : false; |
|
| 1171 | + } |
|
| 1172 | + list($sr, $pagedSearchOK) = $search; |
|
| 1173 | + |
|
| 1174 | + /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1175 | 1175 | * countEntriesInSearchResults() method signature changed |
| 1176 | 1176 | * by removing $limit and &$hasHitLimit parameters |
| 1177 | 1177 | */ |
| 1178 | - $count = $this->countEntriesInSearchResults($sr); |
|
| 1179 | - $counter += $count; |
|
| 1178 | + $count = $this->countEntriesInSearchResults($sr); |
|
| 1179 | + $counter += $count; |
|
| 1180 | 1180 | |
| 1181 | - $hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage, |
|
| 1182 | - $offset, $pagedSearchOK, $skipHandling); |
|
| 1183 | - $offset += $limitPerPage; |
|
| 1184 | - /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1181 | + $hasMorePages = $this->processPagedSearchStatus($sr, $filter, $base, $count, $limitPerPage, |
|
| 1182 | + $offset, $pagedSearchOK, $skipHandling); |
|
| 1183 | + $offset += $limitPerPage; |
|
| 1184 | + /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1185 | 1185 | * Continue now depends on $hasMorePages value |
| 1186 | 1186 | */ |
| 1187 | - $continue = $pagedSearchOK && $hasMorePages; |
|
| 1188 | - } while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter)); |
|
| 1189 | - |
|
| 1190 | - return $counter; |
|
| 1191 | - } |
|
| 1192 | - |
|
| 1193 | - /** |
|
| 1194 | - * @param array $searchResults |
|
| 1195 | - * @return int |
|
| 1196 | - */ |
|
| 1197 | - private function countEntriesInSearchResults($searchResults) { |
|
| 1198 | - $counter = 0; |
|
| 1199 | - |
|
| 1200 | - foreach($searchResults as $res) { |
|
| 1201 | - $count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res); |
|
| 1202 | - $counter += $count; |
|
| 1203 | - } |
|
| 1204 | - |
|
| 1205 | - return $counter; |
|
| 1206 | - } |
|
| 1207 | - |
|
| 1208 | - /** |
|
| 1209 | - * Executes an LDAP search |
|
| 1210 | - * |
|
| 1211 | - * @param string $filter the LDAP filter for the search |
|
| 1212 | - * @param array $base an array containing the LDAP subtree(s) that shall be searched |
|
| 1213 | - * @param string|string[] $attr optional, array, one or more attributes that shall be |
|
| 1214 | - * @param int $limit |
|
| 1215 | - * @param int $offset |
|
| 1216 | - * @param bool $skipHandling |
|
| 1217 | - * @return array with the search result |
|
| 1218 | - * @throws ServerNotAvailableException |
|
| 1219 | - */ |
|
| 1220 | - public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) { |
|
| 1221 | - $limitPerPage = (int)$this->connection->ldapPagingSize; |
|
| 1222 | - if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) { |
|
| 1223 | - $limitPerPage = $limit; |
|
| 1224 | - } |
|
| 1225 | - |
|
| 1226 | - /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1187 | + $continue = $pagedSearchOK && $hasMorePages; |
|
| 1188 | + } while($continue && (is_null($limit) || $limit <= 0 || $limit > $counter)); |
|
| 1189 | + |
|
| 1190 | + return $counter; |
|
| 1191 | + } |
|
| 1192 | + |
|
| 1193 | + /** |
|
| 1194 | + * @param array $searchResults |
|
| 1195 | + * @return int |
|
| 1196 | + */ |
|
| 1197 | + private function countEntriesInSearchResults($searchResults) { |
|
| 1198 | + $counter = 0; |
|
| 1199 | + |
|
| 1200 | + foreach($searchResults as $res) { |
|
| 1201 | + $count = (int)$this->invokeLDAPMethod('countEntries', $this->connection->getConnectionResource(), $res); |
|
| 1202 | + $counter += $count; |
|
| 1203 | + } |
|
| 1204 | + |
|
| 1205 | + return $counter; |
|
| 1206 | + } |
|
| 1207 | + |
|
| 1208 | + /** |
|
| 1209 | + * Executes an LDAP search |
|
| 1210 | + * |
|
| 1211 | + * @param string $filter the LDAP filter for the search |
|
| 1212 | + * @param array $base an array containing the LDAP subtree(s) that shall be searched |
|
| 1213 | + * @param string|string[] $attr optional, array, one or more attributes that shall be |
|
| 1214 | + * @param int $limit |
|
| 1215 | + * @param int $offset |
|
| 1216 | + * @param bool $skipHandling |
|
| 1217 | + * @return array with the search result |
|
| 1218 | + * @throws ServerNotAvailableException |
|
| 1219 | + */ |
|
| 1220 | + public function search($filter, $base, $attr = null, $limit = null, $offset = null, $skipHandling = false) { |
|
| 1221 | + $limitPerPage = (int)$this->connection->ldapPagingSize; |
|
| 1222 | + if(!is_null($limit) && $limit < $limitPerPage && $limit > 0) { |
|
| 1223 | + $limitPerPage = $limit; |
|
| 1224 | + } |
|
| 1225 | + |
|
| 1226 | + /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1227 | 1227 | * As we can have pages with zero results and/or pages with less |
| 1228 | 1228 | * than $limit results but with a still valid server 'cookie', |
| 1229 | 1229 | * loops through until we get $continue equals true and |
| 1230 | 1230 | * $findings['count'] < $limit |
| 1231 | 1231 | */ |
| 1232 | - $findings = []; |
|
| 1233 | - $savedoffset = $offset; |
|
| 1234 | - do { |
|
| 1235 | - $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset); |
|
| 1236 | - if($search === false) { |
|
| 1237 | - return []; |
|
| 1238 | - } |
|
| 1239 | - list($sr, $pagedSearchOK) = $search; |
|
| 1240 | - $cr = $this->connection->getConnectionResource(); |
|
| 1241 | - |
|
| 1242 | - if($skipHandling) { |
|
| 1243 | - //i.e. result do not need to be fetched, we just need the cookie |
|
| 1244 | - //thus pass 1 or any other value as $iFoundItems because it is not |
|
| 1245 | - //used |
|
| 1246 | - $this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage, |
|
| 1247 | - $offset, $pagedSearchOK, |
|
| 1248 | - $skipHandling); |
|
| 1249 | - return array(); |
|
| 1250 | - } |
|
| 1251 | - |
|
| 1252 | - $iFoundItems = 0; |
|
| 1253 | - foreach($sr as $res) { |
|
| 1254 | - $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res)); |
|
| 1255 | - $iFoundItems = max($iFoundItems, $findings['count']); |
|
| 1256 | - unset($findings['count']); |
|
| 1257 | - } |
|
| 1258 | - |
|
| 1259 | - $continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems, |
|
| 1260 | - $limitPerPage, $offset, $pagedSearchOK, |
|
| 1261 | - $skipHandling); |
|
| 1262 | - $offset += $limitPerPage; |
|
| 1263 | - } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit)); |
|
| 1264 | - // reseting offset |
|
| 1265 | - $offset = $savedoffset; |
|
| 1266 | - |
|
| 1267 | - // if we're here, probably no connection resource is returned. |
|
| 1268 | - // to make Nextcloud behave nicely, we simply give back an empty array. |
|
| 1269 | - if(is_null($findings)) { |
|
| 1270 | - return array(); |
|
| 1271 | - } |
|
| 1272 | - |
|
| 1273 | - if(!is_null($attr)) { |
|
| 1274 | - $selection = []; |
|
| 1275 | - $i = 0; |
|
| 1276 | - foreach($findings as $item) { |
|
| 1277 | - if(!is_array($item)) { |
|
| 1278 | - continue; |
|
| 1279 | - } |
|
| 1280 | - $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8'); |
|
| 1281 | - foreach($attr as $key) { |
|
| 1282 | - if(isset($item[$key])) { |
|
| 1283 | - if(is_array($item[$key]) && isset($item[$key]['count'])) { |
|
| 1284 | - unset($item[$key]['count']); |
|
| 1285 | - } |
|
| 1286 | - if($key !== 'dn') { |
|
| 1287 | - if($this->resemblesDN($key)) { |
|
| 1288 | - $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]); |
|
| 1289 | - } else if($key === 'objectguid' || $key === 'guid') { |
|
| 1290 | - $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])]; |
|
| 1291 | - } else { |
|
| 1292 | - $selection[$i][$key] = $item[$key]; |
|
| 1293 | - } |
|
| 1294 | - } else { |
|
| 1295 | - $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])]; |
|
| 1296 | - } |
|
| 1297 | - } |
|
| 1298 | - |
|
| 1299 | - } |
|
| 1300 | - $i++; |
|
| 1301 | - } |
|
| 1302 | - $findings = $selection; |
|
| 1303 | - } |
|
| 1304 | - //we slice the findings, when |
|
| 1305 | - //a) paged search unsuccessful, though attempted |
|
| 1306 | - //b) no paged search, but limit set |
|
| 1307 | - if((!$this->getPagedSearchResultState() |
|
| 1308 | - && $pagedSearchOK) |
|
| 1309 | - || ( |
|
| 1310 | - !$pagedSearchOK |
|
| 1311 | - && !is_null($limit) |
|
| 1312 | - ) |
|
| 1313 | - ) { |
|
| 1314 | - $findings = array_slice($findings, (int)$offset, $limit); |
|
| 1315 | - } |
|
| 1316 | - return $findings; |
|
| 1317 | - } |
|
| 1318 | - |
|
| 1319 | - /** |
|
| 1320 | - * @param string $name |
|
| 1321 | - * @return string |
|
| 1322 | - * @throws \InvalidArgumentException |
|
| 1323 | - */ |
|
| 1324 | - public function sanitizeUsername($name) { |
|
| 1325 | - $name = trim($name); |
|
| 1326 | - |
|
| 1327 | - if($this->connection->ldapIgnoreNamingRules) { |
|
| 1328 | - return $name; |
|
| 1329 | - } |
|
| 1330 | - |
|
| 1331 | - // Transliteration to ASCII |
|
| 1332 | - $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name); |
|
| 1333 | - if($transliterated !== false) { |
|
| 1334 | - // depending on system config iconv can work or not |
|
| 1335 | - $name = $transliterated; |
|
| 1336 | - } |
|
| 1337 | - |
|
| 1338 | - // Replacements |
|
| 1339 | - $name = str_replace(' ', '_', $name); |
|
| 1340 | - |
|
| 1341 | - // Every remaining disallowed characters will be removed |
|
| 1342 | - $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name); |
|
| 1343 | - |
|
| 1344 | - if($name === '') { |
|
| 1345 | - throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters'); |
|
| 1346 | - } |
|
| 1347 | - |
|
| 1348 | - return $name; |
|
| 1349 | - } |
|
| 1350 | - |
|
| 1351 | - /** |
|
| 1352 | - * escapes (user provided) parts for LDAP filter |
|
| 1353 | - * @param string $input, the provided value |
|
| 1354 | - * @param bool $allowAsterisk whether in * at the beginning should be preserved |
|
| 1355 | - * @return string the escaped string |
|
| 1356 | - */ |
|
| 1357 | - public function escapeFilterPart($input, $allowAsterisk = false) { |
|
| 1358 | - $asterisk = ''; |
|
| 1359 | - if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') { |
|
| 1360 | - $asterisk = '*'; |
|
| 1361 | - $input = mb_substr($input, 1, null, 'UTF-8'); |
|
| 1362 | - } |
|
| 1363 | - $search = array('*', '\\', '(', ')'); |
|
| 1364 | - $replace = array('\\*', '\\\\', '\\(', '\\)'); |
|
| 1365 | - return $asterisk . str_replace($search, $replace, $input); |
|
| 1366 | - } |
|
| 1367 | - |
|
| 1368 | - /** |
|
| 1369 | - * combines the input filters with AND |
|
| 1370 | - * @param string[] $filters the filters to connect |
|
| 1371 | - * @return string the combined filter |
|
| 1372 | - */ |
|
| 1373 | - public function combineFilterWithAnd($filters) { |
|
| 1374 | - return $this->combineFilter($filters, '&'); |
|
| 1375 | - } |
|
| 1376 | - |
|
| 1377 | - /** |
|
| 1378 | - * combines the input filters with OR |
|
| 1379 | - * @param string[] $filters the filters to connect |
|
| 1380 | - * @return string the combined filter |
|
| 1381 | - * Combines Filter arguments with OR |
|
| 1382 | - */ |
|
| 1383 | - public function combineFilterWithOr($filters) { |
|
| 1384 | - return $this->combineFilter($filters, '|'); |
|
| 1385 | - } |
|
| 1386 | - |
|
| 1387 | - /** |
|
| 1388 | - * combines the input filters with given operator |
|
| 1389 | - * @param string[] $filters the filters to connect |
|
| 1390 | - * @param string $operator either & or | |
|
| 1391 | - * @return string the combined filter |
|
| 1392 | - */ |
|
| 1393 | - private function combineFilter($filters, $operator) { |
|
| 1394 | - $combinedFilter = '('.$operator; |
|
| 1395 | - foreach($filters as $filter) { |
|
| 1396 | - if ($filter !== '' && $filter[0] !== '(') { |
|
| 1397 | - $filter = '('.$filter.')'; |
|
| 1398 | - } |
|
| 1399 | - $combinedFilter.=$filter; |
|
| 1400 | - } |
|
| 1401 | - $combinedFilter.=')'; |
|
| 1402 | - return $combinedFilter; |
|
| 1403 | - } |
|
| 1404 | - |
|
| 1405 | - /** |
|
| 1406 | - * creates a filter part for to perform search for users |
|
| 1407 | - * @param string $search the search term |
|
| 1408 | - * @return string the final filter part to use in LDAP searches |
|
| 1409 | - */ |
|
| 1410 | - public function getFilterPartForUserSearch($search) { |
|
| 1411 | - return $this->getFilterPartForSearch($search, |
|
| 1412 | - $this->connection->ldapAttributesForUserSearch, |
|
| 1413 | - $this->connection->ldapUserDisplayName); |
|
| 1414 | - } |
|
| 1415 | - |
|
| 1416 | - /** |
|
| 1417 | - * creates a filter part for to perform search for groups |
|
| 1418 | - * @param string $search the search term |
|
| 1419 | - * @return string the final filter part to use in LDAP searches |
|
| 1420 | - */ |
|
| 1421 | - public function getFilterPartForGroupSearch($search) { |
|
| 1422 | - return $this->getFilterPartForSearch($search, |
|
| 1423 | - $this->connection->ldapAttributesForGroupSearch, |
|
| 1424 | - $this->connection->ldapGroupDisplayName); |
|
| 1425 | - } |
|
| 1426 | - |
|
| 1427 | - /** |
|
| 1428 | - * creates a filter part for searches by splitting up the given search |
|
| 1429 | - * string into single words |
|
| 1430 | - * @param string $search the search term |
|
| 1431 | - * @param string[] $searchAttributes needs to have at least two attributes, |
|
| 1432 | - * otherwise it does not make sense :) |
|
| 1433 | - * @return string the final filter part to use in LDAP searches |
|
| 1434 | - * @throws \Exception |
|
| 1435 | - */ |
|
| 1436 | - private function getAdvancedFilterPartForSearch($search, $searchAttributes) { |
|
| 1437 | - if(!is_array($searchAttributes) || count($searchAttributes) < 2) { |
|
| 1438 | - throw new \Exception('searchAttributes must be an array with at least two string'); |
|
| 1439 | - } |
|
| 1440 | - $searchWords = explode(' ', trim($search)); |
|
| 1441 | - $wordFilters = array(); |
|
| 1442 | - foreach($searchWords as $word) { |
|
| 1443 | - $word = $this->prepareSearchTerm($word); |
|
| 1444 | - //every word needs to appear at least once |
|
| 1445 | - $wordMatchOneAttrFilters = array(); |
|
| 1446 | - foreach($searchAttributes as $attr) { |
|
| 1447 | - $wordMatchOneAttrFilters[] = $attr . '=' . $word; |
|
| 1448 | - } |
|
| 1449 | - $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters); |
|
| 1450 | - } |
|
| 1451 | - return $this->combineFilterWithAnd($wordFilters); |
|
| 1452 | - } |
|
| 1453 | - |
|
| 1454 | - /** |
|
| 1455 | - * creates a filter part for searches |
|
| 1456 | - * @param string $search the search term |
|
| 1457 | - * @param string[]|null $searchAttributes |
|
| 1458 | - * @param string $fallbackAttribute a fallback attribute in case the user |
|
| 1459 | - * did not define search attributes. Typically the display name attribute. |
|
| 1460 | - * @return string the final filter part to use in LDAP searches |
|
| 1461 | - */ |
|
| 1462 | - private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) { |
|
| 1463 | - $filter = array(); |
|
| 1464 | - $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0); |
|
| 1465 | - if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) { |
|
| 1466 | - try { |
|
| 1467 | - return $this->getAdvancedFilterPartForSearch($search, $searchAttributes); |
|
| 1468 | - } catch(\Exception $e) { |
|
| 1469 | - \OCP\Util::writeLog( |
|
| 1470 | - 'user_ldap', |
|
| 1471 | - 'Creating advanced filter for search failed, falling back to simple method.', |
|
| 1472 | - ILogger::INFO |
|
| 1473 | - ); |
|
| 1474 | - } |
|
| 1475 | - } |
|
| 1476 | - |
|
| 1477 | - $search = $this->prepareSearchTerm($search); |
|
| 1478 | - if(!is_array($searchAttributes) || count($searchAttributes) === 0) { |
|
| 1479 | - if ($fallbackAttribute === '') { |
|
| 1480 | - return ''; |
|
| 1481 | - } |
|
| 1482 | - $filter[] = $fallbackAttribute . '=' . $search; |
|
| 1483 | - } else { |
|
| 1484 | - foreach($searchAttributes as $attribute) { |
|
| 1485 | - $filter[] = $attribute . '=' . $search; |
|
| 1486 | - } |
|
| 1487 | - } |
|
| 1488 | - if(count($filter) === 1) { |
|
| 1489 | - return '('.$filter[0].')'; |
|
| 1490 | - } |
|
| 1491 | - return $this->combineFilterWithOr($filter); |
|
| 1492 | - } |
|
| 1493 | - |
|
| 1494 | - /** |
|
| 1495 | - * returns the search term depending on whether we are allowed |
|
| 1496 | - * list users found by ldap with the current input appended by |
|
| 1497 | - * a * |
|
| 1498 | - * @return string |
|
| 1499 | - */ |
|
| 1500 | - private function prepareSearchTerm($term) { |
|
| 1501 | - $config = \OC::$server->getConfig(); |
|
| 1502 | - |
|
| 1503 | - $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes'); |
|
| 1504 | - |
|
| 1505 | - $result = $term; |
|
| 1506 | - if ($term === '') { |
|
| 1507 | - $result = '*'; |
|
| 1508 | - } else if ($allowEnum !== 'no') { |
|
| 1509 | - $result = $term . '*'; |
|
| 1510 | - } |
|
| 1511 | - return $result; |
|
| 1512 | - } |
|
| 1513 | - |
|
| 1514 | - /** |
|
| 1515 | - * returns the filter used for counting users |
|
| 1516 | - * @return string |
|
| 1517 | - */ |
|
| 1518 | - public function getFilterForUserCount() { |
|
| 1519 | - $filter = $this->combineFilterWithAnd(array( |
|
| 1520 | - $this->connection->ldapUserFilter, |
|
| 1521 | - $this->connection->ldapUserDisplayName . '=*' |
|
| 1522 | - )); |
|
| 1523 | - |
|
| 1524 | - return $filter; |
|
| 1525 | - } |
|
| 1526 | - |
|
| 1527 | - /** |
|
| 1528 | - * @param string $name |
|
| 1529 | - * @param string $password |
|
| 1530 | - * @return bool |
|
| 1531 | - */ |
|
| 1532 | - public function areCredentialsValid($name, $password) { |
|
| 1533 | - $name = $this->helper->DNasBaseParameter($name); |
|
| 1534 | - $testConnection = clone $this->connection; |
|
| 1535 | - $credentials = array( |
|
| 1536 | - 'ldapAgentName' => $name, |
|
| 1537 | - 'ldapAgentPassword' => $password |
|
| 1538 | - ); |
|
| 1539 | - if(!$testConnection->setConfiguration($credentials)) { |
|
| 1540 | - return false; |
|
| 1541 | - } |
|
| 1542 | - return $testConnection->bind(); |
|
| 1543 | - } |
|
| 1544 | - |
|
| 1545 | - /** |
|
| 1546 | - * reverse lookup of a DN given a known UUID |
|
| 1547 | - * |
|
| 1548 | - * @param string $uuid |
|
| 1549 | - * @return string |
|
| 1550 | - * @throws \Exception |
|
| 1551 | - */ |
|
| 1552 | - public function getUserDnByUuid($uuid) { |
|
| 1553 | - $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; |
|
| 1554 | - $filter = $this->connection->ldapUserFilter; |
|
| 1555 | - $base = $this->connection->ldapBaseUsers; |
|
| 1556 | - |
|
| 1557 | - if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') { |
|
| 1558 | - // Sacrebleu! The UUID attribute is unknown :( We need first an |
|
| 1559 | - // existing DN to be able to reliably detect it. |
|
| 1560 | - $result = $this->search($filter, $base, ['dn'], 1); |
|
| 1561 | - if(!isset($result[0]) || !isset($result[0]['dn'])) { |
|
| 1562 | - throw new \Exception('Cannot determine UUID attribute'); |
|
| 1563 | - } |
|
| 1564 | - $dn = $result[0]['dn'][0]; |
|
| 1565 | - if(!$this->detectUuidAttribute($dn, true)) { |
|
| 1566 | - throw new \Exception('Cannot determine UUID attribute'); |
|
| 1567 | - } |
|
| 1568 | - } else { |
|
| 1569 | - // The UUID attribute is either known or an override is given. |
|
| 1570 | - // By calling this method we ensure that $this->connection->$uuidAttr |
|
| 1571 | - // is definitely set |
|
| 1572 | - if(!$this->detectUuidAttribute('', true)) { |
|
| 1573 | - throw new \Exception('Cannot determine UUID attribute'); |
|
| 1574 | - } |
|
| 1575 | - } |
|
| 1576 | - |
|
| 1577 | - $uuidAttr = $this->connection->ldapUuidUserAttribute; |
|
| 1578 | - if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') { |
|
| 1579 | - $uuid = $this->formatGuid2ForFilterUser($uuid); |
|
| 1580 | - } |
|
| 1581 | - |
|
| 1582 | - $filter = $uuidAttr . '=' . $uuid; |
|
| 1583 | - $result = $this->searchUsers($filter, ['dn'], 2); |
|
| 1584 | - if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) { |
|
| 1585 | - // we put the count into account to make sure that this is |
|
| 1586 | - // really unique |
|
| 1587 | - return $result[0]['dn'][0]; |
|
| 1588 | - } |
|
| 1589 | - |
|
| 1590 | - throw new \Exception('Cannot determine UUID attribute'); |
|
| 1591 | - } |
|
| 1592 | - |
|
| 1593 | - /** |
|
| 1594 | - * auto-detects the directory's UUID attribute |
|
| 1595 | - * |
|
| 1596 | - * @param string $dn a known DN used to check against |
|
| 1597 | - * @param bool $isUser |
|
| 1598 | - * @param bool $force the detection should be run, even if it is not set to auto |
|
| 1599 | - * @param array|null $ldapRecord |
|
| 1600 | - * @return bool true on success, false otherwise |
|
| 1601 | - */ |
|
| 1602 | - private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) { |
|
| 1603 | - if($isUser) { |
|
| 1604 | - $uuidAttr = 'ldapUuidUserAttribute'; |
|
| 1605 | - $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; |
|
| 1606 | - } else { |
|
| 1607 | - $uuidAttr = 'ldapUuidGroupAttribute'; |
|
| 1608 | - $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr; |
|
| 1609 | - } |
|
| 1610 | - |
|
| 1611 | - if(($this->connection->$uuidAttr !== 'auto') && !$force) { |
|
| 1612 | - return true; |
|
| 1613 | - } |
|
| 1614 | - |
|
| 1615 | - if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) { |
|
| 1616 | - $this->connection->$uuidAttr = $uuidOverride; |
|
| 1617 | - return true; |
|
| 1618 | - } |
|
| 1619 | - |
|
| 1620 | - foreach(self::UUID_ATTRIBUTES as $attribute) { |
|
| 1621 | - if($ldapRecord !== null) { |
|
| 1622 | - // we have the info from LDAP already, we don't need to talk to the server again |
|
| 1623 | - if(isset($ldapRecord[$attribute])) { |
|
| 1624 | - $this->connection->$uuidAttr = $attribute; |
|
| 1625 | - return true; |
|
| 1626 | - } else { |
|
| 1627 | - continue; |
|
| 1628 | - } |
|
| 1629 | - } |
|
| 1630 | - |
|
| 1631 | - $value = $this->readAttribute($dn, $attribute); |
|
| 1632 | - if(is_array($value) && isset($value[0]) && !empty($value[0])) { |
|
| 1633 | - \OCP\Util::writeLog( |
|
| 1634 | - 'user_ldap', |
|
| 1635 | - 'Setting '.$attribute.' as '.$uuidAttr, |
|
| 1636 | - ILogger::DEBUG |
|
| 1637 | - ); |
|
| 1638 | - $this->connection->$uuidAttr = $attribute; |
|
| 1639 | - return true; |
|
| 1640 | - } |
|
| 1641 | - } |
|
| 1642 | - \OCP\Util::writeLog( |
|
| 1643 | - 'user_ldap', |
|
| 1644 | - 'Could not autodetect the UUID attribute', |
|
| 1645 | - ILogger::ERROR |
|
| 1646 | - ); |
|
| 1647 | - |
|
| 1648 | - return false; |
|
| 1649 | - } |
|
| 1650 | - |
|
| 1651 | - /** |
|
| 1652 | - * @param string $dn |
|
| 1653 | - * @param bool $isUser |
|
| 1654 | - * @param null $ldapRecord |
|
| 1655 | - * @return bool|string |
|
| 1656 | - */ |
|
| 1657 | - public function getUUID($dn, $isUser = true, $ldapRecord = null) { |
|
| 1658 | - if($isUser) { |
|
| 1659 | - $uuidAttr = 'ldapUuidUserAttribute'; |
|
| 1660 | - $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; |
|
| 1661 | - } else { |
|
| 1662 | - $uuidAttr = 'ldapUuidGroupAttribute'; |
|
| 1663 | - $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr; |
|
| 1664 | - } |
|
| 1665 | - |
|
| 1666 | - $uuid = false; |
|
| 1667 | - if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) { |
|
| 1668 | - $attr = $this->connection->$uuidAttr; |
|
| 1669 | - $uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr); |
|
| 1670 | - if( !is_array($uuid) |
|
| 1671 | - && $uuidOverride !== '' |
|
| 1672 | - && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord)) |
|
| 1673 | - { |
|
| 1674 | - $uuid = isset($ldapRecord[$this->connection->$uuidAttr]) |
|
| 1675 | - ? $ldapRecord[$this->connection->$uuidAttr] |
|
| 1676 | - : $this->readAttribute($dn, $this->connection->$uuidAttr); |
|
| 1677 | - } |
|
| 1678 | - if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) { |
|
| 1679 | - $uuid = $uuid[0]; |
|
| 1680 | - } |
|
| 1681 | - } |
|
| 1682 | - |
|
| 1683 | - return $uuid; |
|
| 1684 | - } |
|
| 1685 | - |
|
| 1686 | - /** |
|
| 1687 | - * converts a binary ObjectGUID into a string representation |
|
| 1688 | - * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD |
|
| 1689 | - * @return string |
|
| 1690 | - * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198 |
|
| 1691 | - */ |
|
| 1692 | - private function convertObjectGUID2Str($oguid) { |
|
| 1693 | - $hex_guid = bin2hex($oguid); |
|
| 1694 | - $hex_guid_to_guid_str = ''; |
|
| 1695 | - for($k = 1; $k <= 4; ++$k) { |
|
| 1696 | - $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2); |
|
| 1697 | - } |
|
| 1698 | - $hex_guid_to_guid_str .= '-'; |
|
| 1699 | - for($k = 1; $k <= 2; ++$k) { |
|
| 1700 | - $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2); |
|
| 1701 | - } |
|
| 1702 | - $hex_guid_to_guid_str .= '-'; |
|
| 1703 | - for($k = 1; $k <= 2; ++$k) { |
|
| 1704 | - $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2); |
|
| 1705 | - } |
|
| 1706 | - $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4); |
|
| 1707 | - $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20); |
|
| 1708 | - |
|
| 1709 | - return strtoupper($hex_guid_to_guid_str); |
|
| 1710 | - } |
|
| 1711 | - |
|
| 1712 | - /** |
|
| 1713 | - * the first three blocks of the string-converted GUID happen to be in |
|
| 1714 | - * reverse order. In order to use it in a filter, this needs to be |
|
| 1715 | - * corrected. Furthermore the dashes need to be replaced and \\ preprended |
|
| 1716 | - * to every two hax figures. |
|
| 1717 | - * |
|
| 1718 | - * If an invalid string is passed, it will be returned without change. |
|
| 1719 | - * |
|
| 1720 | - * @param string $guid |
|
| 1721 | - * @return string |
|
| 1722 | - */ |
|
| 1723 | - public function formatGuid2ForFilterUser($guid) { |
|
| 1724 | - if(!is_string($guid)) { |
|
| 1725 | - throw new \InvalidArgumentException('String expected'); |
|
| 1726 | - } |
|
| 1727 | - $blocks = explode('-', $guid); |
|
| 1728 | - if(count($blocks) !== 5) { |
|
| 1729 | - /* |
|
| 1232 | + $findings = []; |
|
| 1233 | + $savedoffset = $offset; |
|
| 1234 | + do { |
|
| 1235 | + $search = $this->executeSearch($filter, $base, $attr, $limitPerPage, $offset); |
|
| 1236 | + if($search === false) { |
|
| 1237 | + return []; |
|
| 1238 | + } |
|
| 1239 | + list($sr, $pagedSearchOK) = $search; |
|
| 1240 | + $cr = $this->connection->getConnectionResource(); |
|
| 1241 | + |
|
| 1242 | + if($skipHandling) { |
|
| 1243 | + //i.e. result do not need to be fetched, we just need the cookie |
|
| 1244 | + //thus pass 1 or any other value as $iFoundItems because it is not |
|
| 1245 | + //used |
|
| 1246 | + $this->processPagedSearchStatus($sr, $filter, $base, 1, $limitPerPage, |
|
| 1247 | + $offset, $pagedSearchOK, |
|
| 1248 | + $skipHandling); |
|
| 1249 | + return array(); |
|
| 1250 | + } |
|
| 1251 | + |
|
| 1252 | + $iFoundItems = 0; |
|
| 1253 | + foreach($sr as $res) { |
|
| 1254 | + $findings = array_merge($findings, $this->invokeLDAPMethod('getEntries', $cr, $res)); |
|
| 1255 | + $iFoundItems = max($iFoundItems, $findings['count']); |
|
| 1256 | + unset($findings['count']); |
|
| 1257 | + } |
|
| 1258 | + |
|
| 1259 | + $continue = $this->processPagedSearchStatus($sr, $filter, $base, $iFoundItems, |
|
| 1260 | + $limitPerPage, $offset, $pagedSearchOK, |
|
| 1261 | + $skipHandling); |
|
| 1262 | + $offset += $limitPerPage; |
|
| 1263 | + } while ($continue && $pagedSearchOK && ($limit === null || count($findings) < $limit)); |
|
| 1264 | + // reseting offset |
|
| 1265 | + $offset = $savedoffset; |
|
| 1266 | + |
|
| 1267 | + // if we're here, probably no connection resource is returned. |
|
| 1268 | + // to make Nextcloud behave nicely, we simply give back an empty array. |
|
| 1269 | + if(is_null($findings)) { |
|
| 1270 | + return array(); |
|
| 1271 | + } |
|
| 1272 | + |
|
| 1273 | + if(!is_null($attr)) { |
|
| 1274 | + $selection = []; |
|
| 1275 | + $i = 0; |
|
| 1276 | + foreach($findings as $item) { |
|
| 1277 | + if(!is_array($item)) { |
|
| 1278 | + continue; |
|
| 1279 | + } |
|
| 1280 | + $item = \OCP\Util::mb_array_change_key_case($item, MB_CASE_LOWER, 'UTF-8'); |
|
| 1281 | + foreach($attr as $key) { |
|
| 1282 | + if(isset($item[$key])) { |
|
| 1283 | + if(is_array($item[$key]) && isset($item[$key]['count'])) { |
|
| 1284 | + unset($item[$key]['count']); |
|
| 1285 | + } |
|
| 1286 | + if($key !== 'dn') { |
|
| 1287 | + if($this->resemblesDN($key)) { |
|
| 1288 | + $selection[$i][$key] = $this->helper->sanitizeDN($item[$key]); |
|
| 1289 | + } else if($key === 'objectguid' || $key === 'guid') { |
|
| 1290 | + $selection[$i][$key] = [$this->convertObjectGUID2Str($item[$key][0])]; |
|
| 1291 | + } else { |
|
| 1292 | + $selection[$i][$key] = $item[$key]; |
|
| 1293 | + } |
|
| 1294 | + } else { |
|
| 1295 | + $selection[$i][$key] = [$this->helper->sanitizeDN($item[$key])]; |
|
| 1296 | + } |
|
| 1297 | + } |
|
| 1298 | + |
|
| 1299 | + } |
|
| 1300 | + $i++; |
|
| 1301 | + } |
|
| 1302 | + $findings = $selection; |
|
| 1303 | + } |
|
| 1304 | + //we slice the findings, when |
|
| 1305 | + //a) paged search unsuccessful, though attempted |
|
| 1306 | + //b) no paged search, but limit set |
|
| 1307 | + if((!$this->getPagedSearchResultState() |
|
| 1308 | + && $pagedSearchOK) |
|
| 1309 | + || ( |
|
| 1310 | + !$pagedSearchOK |
|
| 1311 | + && !is_null($limit) |
|
| 1312 | + ) |
|
| 1313 | + ) { |
|
| 1314 | + $findings = array_slice($findings, (int)$offset, $limit); |
|
| 1315 | + } |
|
| 1316 | + return $findings; |
|
| 1317 | + } |
|
| 1318 | + |
|
| 1319 | + /** |
|
| 1320 | + * @param string $name |
|
| 1321 | + * @return string |
|
| 1322 | + * @throws \InvalidArgumentException |
|
| 1323 | + */ |
|
| 1324 | + public function sanitizeUsername($name) { |
|
| 1325 | + $name = trim($name); |
|
| 1326 | + |
|
| 1327 | + if($this->connection->ldapIgnoreNamingRules) { |
|
| 1328 | + return $name; |
|
| 1329 | + } |
|
| 1330 | + |
|
| 1331 | + // Transliteration to ASCII |
|
| 1332 | + $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $name); |
|
| 1333 | + if($transliterated !== false) { |
|
| 1334 | + // depending on system config iconv can work or not |
|
| 1335 | + $name = $transliterated; |
|
| 1336 | + } |
|
| 1337 | + |
|
| 1338 | + // Replacements |
|
| 1339 | + $name = str_replace(' ', '_', $name); |
|
| 1340 | + |
|
| 1341 | + // Every remaining disallowed characters will be removed |
|
| 1342 | + $name = preg_replace('/[^a-zA-Z0-9_.@-]/u', '', $name); |
|
| 1343 | + |
|
| 1344 | + if($name === '') { |
|
| 1345 | + throw new \InvalidArgumentException('provided name template for username does not contain any allowed characters'); |
|
| 1346 | + } |
|
| 1347 | + |
|
| 1348 | + return $name; |
|
| 1349 | + } |
|
| 1350 | + |
|
| 1351 | + /** |
|
| 1352 | + * escapes (user provided) parts for LDAP filter |
|
| 1353 | + * @param string $input, the provided value |
|
| 1354 | + * @param bool $allowAsterisk whether in * at the beginning should be preserved |
|
| 1355 | + * @return string the escaped string |
|
| 1356 | + */ |
|
| 1357 | + public function escapeFilterPart($input, $allowAsterisk = false) { |
|
| 1358 | + $asterisk = ''; |
|
| 1359 | + if($allowAsterisk && strlen($input) > 0 && $input[0] === '*') { |
|
| 1360 | + $asterisk = '*'; |
|
| 1361 | + $input = mb_substr($input, 1, null, 'UTF-8'); |
|
| 1362 | + } |
|
| 1363 | + $search = array('*', '\\', '(', ')'); |
|
| 1364 | + $replace = array('\\*', '\\\\', '\\(', '\\)'); |
|
| 1365 | + return $asterisk . str_replace($search, $replace, $input); |
|
| 1366 | + } |
|
| 1367 | + |
|
| 1368 | + /** |
|
| 1369 | + * combines the input filters with AND |
|
| 1370 | + * @param string[] $filters the filters to connect |
|
| 1371 | + * @return string the combined filter |
|
| 1372 | + */ |
|
| 1373 | + public function combineFilterWithAnd($filters) { |
|
| 1374 | + return $this->combineFilter($filters, '&'); |
|
| 1375 | + } |
|
| 1376 | + |
|
| 1377 | + /** |
|
| 1378 | + * combines the input filters with OR |
|
| 1379 | + * @param string[] $filters the filters to connect |
|
| 1380 | + * @return string the combined filter |
|
| 1381 | + * Combines Filter arguments with OR |
|
| 1382 | + */ |
|
| 1383 | + public function combineFilterWithOr($filters) { |
|
| 1384 | + return $this->combineFilter($filters, '|'); |
|
| 1385 | + } |
|
| 1386 | + |
|
| 1387 | + /** |
|
| 1388 | + * combines the input filters with given operator |
|
| 1389 | + * @param string[] $filters the filters to connect |
|
| 1390 | + * @param string $operator either & or | |
|
| 1391 | + * @return string the combined filter |
|
| 1392 | + */ |
|
| 1393 | + private function combineFilter($filters, $operator) { |
|
| 1394 | + $combinedFilter = '('.$operator; |
|
| 1395 | + foreach($filters as $filter) { |
|
| 1396 | + if ($filter !== '' && $filter[0] !== '(') { |
|
| 1397 | + $filter = '('.$filter.')'; |
|
| 1398 | + } |
|
| 1399 | + $combinedFilter.=$filter; |
|
| 1400 | + } |
|
| 1401 | + $combinedFilter.=')'; |
|
| 1402 | + return $combinedFilter; |
|
| 1403 | + } |
|
| 1404 | + |
|
| 1405 | + /** |
|
| 1406 | + * creates a filter part for to perform search for users |
|
| 1407 | + * @param string $search the search term |
|
| 1408 | + * @return string the final filter part to use in LDAP searches |
|
| 1409 | + */ |
|
| 1410 | + public function getFilterPartForUserSearch($search) { |
|
| 1411 | + return $this->getFilterPartForSearch($search, |
|
| 1412 | + $this->connection->ldapAttributesForUserSearch, |
|
| 1413 | + $this->connection->ldapUserDisplayName); |
|
| 1414 | + } |
|
| 1415 | + |
|
| 1416 | + /** |
|
| 1417 | + * creates a filter part for to perform search for groups |
|
| 1418 | + * @param string $search the search term |
|
| 1419 | + * @return string the final filter part to use in LDAP searches |
|
| 1420 | + */ |
|
| 1421 | + public function getFilterPartForGroupSearch($search) { |
|
| 1422 | + return $this->getFilterPartForSearch($search, |
|
| 1423 | + $this->connection->ldapAttributesForGroupSearch, |
|
| 1424 | + $this->connection->ldapGroupDisplayName); |
|
| 1425 | + } |
|
| 1426 | + |
|
| 1427 | + /** |
|
| 1428 | + * creates a filter part for searches by splitting up the given search |
|
| 1429 | + * string into single words |
|
| 1430 | + * @param string $search the search term |
|
| 1431 | + * @param string[] $searchAttributes needs to have at least two attributes, |
|
| 1432 | + * otherwise it does not make sense :) |
|
| 1433 | + * @return string the final filter part to use in LDAP searches |
|
| 1434 | + * @throws \Exception |
|
| 1435 | + */ |
|
| 1436 | + private function getAdvancedFilterPartForSearch($search, $searchAttributes) { |
|
| 1437 | + if(!is_array($searchAttributes) || count($searchAttributes) < 2) { |
|
| 1438 | + throw new \Exception('searchAttributes must be an array with at least two string'); |
|
| 1439 | + } |
|
| 1440 | + $searchWords = explode(' ', trim($search)); |
|
| 1441 | + $wordFilters = array(); |
|
| 1442 | + foreach($searchWords as $word) { |
|
| 1443 | + $word = $this->prepareSearchTerm($word); |
|
| 1444 | + //every word needs to appear at least once |
|
| 1445 | + $wordMatchOneAttrFilters = array(); |
|
| 1446 | + foreach($searchAttributes as $attr) { |
|
| 1447 | + $wordMatchOneAttrFilters[] = $attr . '=' . $word; |
|
| 1448 | + } |
|
| 1449 | + $wordFilters[] = $this->combineFilterWithOr($wordMatchOneAttrFilters); |
|
| 1450 | + } |
|
| 1451 | + return $this->combineFilterWithAnd($wordFilters); |
|
| 1452 | + } |
|
| 1453 | + |
|
| 1454 | + /** |
|
| 1455 | + * creates a filter part for searches |
|
| 1456 | + * @param string $search the search term |
|
| 1457 | + * @param string[]|null $searchAttributes |
|
| 1458 | + * @param string $fallbackAttribute a fallback attribute in case the user |
|
| 1459 | + * did not define search attributes. Typically the display name attribute. |
|
| 1460 | + * @return string the final filter part to use in LDAP searches |
|
| 1461 | + */ |
|
| 1462 | + private function getFilterPartForSearch($search, $searchAttributes, $fallbackAttribute) { |
|
| 1463 | + $filter = array(); |
|
| 1464 | + $haveMultiSearchAttributes = (is_array($searchAttributes) && count($searchAttributes) > 0); |
|
| 1465 | + if($haveMultiSearchAttributes && strpos(trim($search), ' ') !== false) { |
|
| 1466 | + try { |
|
| 1467 | + return $this->getAdvancedFilterPartForSearch($search, $searchAttributes); |
|
| 1468 | + } catch(\Exception $e) { |
|
| 1469 | + \OCP\Util::writeLog( |
|
| 1470 | + 'user_ldap', |
|
| 1471 | + 'Creating advanced filter for search failed, falling back to simple method.', |
|
| 1472 | + ILogger::INFO |
|
| 1473 | + ); |
|
| 1474 | + } |
|
| 1475 | + } |
|
| 1476 | + |
|
| 1477 | + $search = $this->prepareSearchTerm($search); |
|
| 1478 | + if(!is_array($searchAttributes) || count($searchAttributes) === 0) { |
|
| 1479 | + if ($fallbackAttribute === '') { |
|
| 1480 | + return ''; |
|
| 1481 | + } |
|
| 1482 | + $filter[] = $fallbackAttribute . '=' . $search; |
|
| 1483 | + } else { |
|
| 1484 | + foreach($searchAttributes as $attribute) { |
|
| 1485 | + $filter[] = $attribute . '=' . $search; |
|
| 1486 | + } |
|
| 1487 | + } |
|
| 1488 | + if(count($filter) === 1) { |
|
| 1489 | + return '('.$filter[0].')'; |
|
| 1490 | + } |
|
| 1491 | + return $this->combineFilterWithOr($filter); |
|
| 1492 | + } |
|
| 1493 | + |
|
| 1494 | + /** |
|
| 1495 | + * returns the search term depending on whether we are allowed |
|
| 1496 | + * list users found by ldap with the current input appended by |
|
| 1497 | + * a * |
|
| 1498 | + * @return string |
|
| 1499 | + */ |
|
| 1500 | + private function prepareSearchTerm($term) { |
|
| 1501 | + $config = \OC::$server->getConfig(); |
|
| 1502 | + |
|
| 1503 | + $allowEnum = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes'); |
|
| 1504 | + |
|
| 1505 | + $result = $term; |
|
| 1506 | + if ($term === '') { |
|
| 1507 | + $result = '*'; |
|
| 1508 | + } else if ($allowEnum !== 'no') { |
|
| 1509 | + $result = $term . '*'; |
|
| 1510 | + } |
|
| 1511 | + return $result; |
|
| 1512 | + } |
|
| 1513 | + |
|
| 1514 | + /** |
|
| 1515 | + * returns the filter used for counting users |
|
| 1516 | + * @return string |
|
| 1517 | + */ |
|
| 1518 | + public function getFilterForUserCount() { |
|
| 1519 | + $filter = $this->combineFilterWithAnd(array( |
|
| 1520 | + $this->connection->ldapUserFilter, |
|
| 1521 | + $this->connection->ldapUserDisplayName . '=*' |
|
| 1522 | + )); |
|
| 1523 | + |
|
| 1524 | + return $filter; |
|
| 1525 | + } |
|
| 1526 | + |
|
| 1527 | + /** |
|
| 1528 | + * @param string $name |
|
| 1529 | + * @param string $password |
|
| 1530 | + * @return bool |
|
| 1531 | + */ |
|
| 1532 | + public function areCredentialsValid($name, $password) { |
|
| 1533 | + $name = $this->helper->DNasBaseParameter($name); |
|
| 1534 | + $testConnection = clone $this->connection; |
|
| 1535 | + $credentials = array( |
|
| 1536 | + 'ldapAgentName' => $name, |
|
| 1537 | + 'ldapAgentPassword' => $password |
|
| 1538 | + ); |
|
| 1539 | + if(!$testConnection->setConfiguration($credentials)) { |
|
| 1540 | + return false; |
|
| 1541 | + } |
|
| 1542 | + return $testConnection->bind(); |
|
| 1543 | + } |
|
| 1544 | + |
|
| 1545 | + /** |
|
| 1546 | + * reverse lookup of a DN given a known UUID |
|
| 1547 | + * |
|
| 1548 | + * @param string $uuid |
|
| 1549 | + * @return string |
|
| 1550 | + * @throws \Exception |
|
| 1551 | + */ |
|
| 1552 | + public function getUserDnByUuid($uuid) { |
|
| 1553 | + $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; |
|
| 1554 | + $filter = $this->connection->ldapUserFilter; |
|
| 1555 | + $base = $this->connection->ldapBaseUsers; |
|
| 1556 | + |
|
| 1557 | + if ($this->connection->ldapUuidUserAttribute === 'auto' && $uuidOverride === '') { |
|
| 1558 | + // Sacrebleu! The UUID attribute is unknown :( We need first an |
|
| 1559 | + // existing DN to be able to reliably detect it. |
|
| 1560 | + $result = $this->search($filter, $base, ['dn'], 1); |
|
| 1561 | + if(!isset($result[0]) || !isset($result[0]['dn'])) { |
|
| 1562 | + throw new \Exception('Cannot determine UUID attribute'); |
|
| 1563 | + } |
|
| 1564 | + $dn = $result[0]['dn'][0]; |
|
| 1565 | + if(!$this->detectUuidAttribute($dn, true)) { |
|
| 1566 | + throw new \Exception('Cannot determine UUID attribute'); |
|
| 1567 | + } |
|
| 1568 | + } else { |
|
| 1569 | + // The UUID attribute is either known or an override is given. |
|
| 1570 | + // By calling this method we ensure that $this->connection->$uuidAttr |
|
| 1571 | + // is definitely set |
|
| 1572 | + if(!$this->detectUuidAttribute('', true)) { |
|
| 1573 | + throw new \Exception('Cannot determine UUID attribute'); |
|
| 1574 | + } |
|
| 1575 | + } |
|
| 1576 | + |
|
| 1577 | + $uuidAttr = $this->connection->ldapUuidUserAttribute; |
|
| 1578 | + if($uuidAttr === 'guid' || $uuidAttr === 'objectguid') { |
|
| 1579 | + $uuid = $this->formatGuid2ForFilterUser($uuid); |
|
| 1580 | + } |
|
| 1581 | + |
|
| 1582 | + $filter = $uuidAttr . '=' . $uuid; |
|
| 1583 | + $result = $this->searchUsers($filter, ['dn'], 2); |
|
| 1584 | + if(is_array($result) && isset($result[0]) && isset($result[0]['dn']) && count($result) === 1) { |
|
| 1585 | + // we put the count into account to make sure that this is |
|
| 1586 | + // really unique |
|
| 1587 | + return $result[0]['dn'][0]; |
|
| 1588 | + } |
|
| 1589 | + |
|
| 1590 | + throw new \Exception('Cannot determine UUID attribute'); |
|
| 1591 | + } |
|
| 1592 | + |
|
| 1593 | + /** |
|
| 1594 | + * auto-detects the directory's UUID attribute |
|
| 1595 | + * |
|
| 1596 | + * @param string $dn a known DN used to check against |
|
| 1597 | + * @param bool $isUser |
|
| 1598 | + * @param bool $force the detection should be run, even if it is not set to auto |
|
| 1599 | + * @param array|null $ldapRecord |
|
| 1600 | + * @return bool true on success, false otherwise |
|
| 1601 | + */ |
|
| 1602 | + private function detectUuidAttribute($dn, $isUser = true, $force = false, array $ldapRecord = null) { |
|
| 1603 | + if($isUser) { |
|
| 1604 | + $uuidAttr = 'ldapUuidUserAttribute'; |
|
| 1605 | + $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; |
|
| 1606 | + } else { |
|
| 1607 | + $uuidAttr = 'ldapUuidGroupAttribute'; |
|
| 1608 | + $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr; |
|
| 1609 | + } |
|
| 1610 | + |
|
| 1611 | + if(($this->connection->$uuidAttr !== 'auto') && !$force) { |
|
| 1612 | + return true; |
|
| 1613 | + } |
|
| 1614 | + |
|
| 1615 | + if (is_string($uuidOverride) && trim($uuidOverride) !== '' && !$force) { |
|
| 1616 | + $this->connection->$uuidAttr = $uuidOverride; |
|
| 1617 | + return true; |
|
| 1618 | + } |
|
| 1619 | + |
|
| 1620 | + foreach(self::UUID_ATTRIBUTES as $attribute) { |
|
| 1621 | + if($ldapRecord !== null) { |
|
| 1622 | + // we have the info from LDAP already, we don't need to talk to the server again |
|
| 1623 | + if(isset($ldapRecord[$attribute])) { |
|
| 1624 | + $this->connection->$uuidAttr = $attribute; |
|
| 1625 | + return true; |
|
| 1626 | + } else { |
|
| 1627 | + continue; |
|
| 1628 | + } |
|
| 1629 | + } |
|
| 1630 | + |
|
| 1631 | + $value = $this->readAttribute($dn, $attribute); |
|
| 1632 | + if(is_array($value) && isset($value[0]) && !empty($value[0])) { |
|
| 1633 | + \OCP\Util::writeLog( |
|
| 1634 | + 'user_ldap', |
|
| 1635 | + 'Setting '.$attribute.' as '.$uuidAttr, |
|
| 1636 | + ILogger::DEBUG |
|
| 1637 | + ); |
|
| 1638 | + $this->connection->$uuidAttr = $attribute; |
|
| 1639 | + return true; |
|
| 1640 | + } |
|
| 1641 | + } |
|
| 1642 | + \OCP\Util::writeLog( |
|
| 1643 | + 'user_ldap', |
|
| 1644 | + 'Could not autodetect the UUID attribute', |
|
| 1645 | + ILogger::ERROR |
|
| 1646 | + ); |
|
| 1647 | + |
|
| 1648 | + return false; |
|
| 1649 | + } |
|
| 1650 | + |
|
| 1651 | + /** |
|
| 1652 | + * @param string $dn |
|
| 1653 | + * @param bool $isUser |
|
| 1654 | + * @param null $ldapRecord |
|
| 1655 | + * @return bool|string |
|
| 1656 | + */ |
|
| 1657 | + public function getUUID($dn, $isUser = true, $ldapRecord = null) { |
|
| 1658 | + if($isUser) { |
|
| 1659 | + $uuidAttr = 'ldapUuidUserAttribute'; |
|
| 1660 | + $uuidOverride = $this->connection->ldapExpertUUIDUserAttr; |
|
| 1661 | + } else { |
|
| 1662 | + $uuidAttr = 'ldapUuidGroupAttribute'; |
|
| 1663 | + $uuidOverride = $this->connection->ldapExpertUUIDGroupAttr; |
|
| 1664 | + } |
|
| 1665 | + |
|
| 1666 | + $uuid = false; |
|
| 1667 | + if($this->detectUuidAttribute($dn, $isUser, false, $ldapRecord)) { |
|
| 1668 | + $attr = $this->connection->$uuidAttr; |
|
| 1669 | + $uuid = isset($ldapRecord[$attr]) ? $ldapRecord[$attr] : $this->readAttribute($dn, $attr); |
|
| 1670 | + if( !is_array($uuid) |
|
| 1671 | + && $uuidOverride !== '' |
|
| 1672 | + && $this->detectUuidAttribute($dn, $isUser, true, $ldapRecord)) |
|
| 1673 | + { |
|
| 1674 | + $uuid = isset($ldapRecord[$this->connection->$uuidAttr]) |
|
| 1675 | + ? $ldapRecord[$this->connection->$uuidAttr] |
|
| 1676 | + : $this->readAttribute($dn, $this->connection->$uuidAttr); |
|
| 1677 | + } |
|
| 1678 | + if(is_array($uuid) && isset($uuid[0]) && !empty($uuid[0])) { |
|
| 1679 | + $uuid = $uuid[0]; |
|
| 1680 | + } |
|
| 1681 | + } |
|
| 1682 | + |
|
| 1683 | + return $uuid; |
|
| 1684 | + } |
|
| 1685 | + |
|
| 1686 | + /** |
|
| 1687 | + * converts a binary ObjectGUID into a string representation |
|
| 1688 | + * @param string $oguid the ObjectGUID in it's binary form as retrieved from AD |
|
| 1689 | + * @return string |
|
| 1690 | + * @link http://www.php.net/manual/en/function.ldap-get-values-len.php#73198 |
|
| 1691 | + */ |
|
| 1692 | + private function convertObjectGUID2Str($oguid) { |
|
| 1693 | + $hex_guid = bin2hex($oguid); |
|
| 1694 | + $hex_guid_to_guid_str = ''; |
|
| 1695 | + for($k = 1; $k <= 4; ++$k) { |
|
| 1696 | + $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2); |
|
| 1697 | + } |
|
| 1698 | + $hex_guid_to_guid_str .= '-'; |
|
| 1699 | + for($k = 1; $k <= 2; ++$k) { |
|
| 1700 | + $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2); |
|
| 1701 | + } |
|
| 1702 | + $hex_guid_to_guid_str .= '-'; |
|
| 1703 | + for($k = 1; $k <= 2; ++$k) { |
|
| 1704 | + $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2); |
|
| 1705 | + } |
|
| 1706 | + $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4); |
|
| 1707 | + $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20); |
|
| 1708 | + |
|
| 1709 | + return strtoupper($hex_guid_to_guid_str); |
|
| 1710 | + } |
|
| 1711 | + |
|
| 1712 | + /** |
|
| 1713 | + * the first three blocks of the string-converted GUID happen to be in |
|
| 1714 | + * reverse order. In order to use it in a filter, this needs to be |
|
| 1715 | + * corrected. Furthermore the dashes need to be replaced and \\ preprended |
|
| 1716 | + * to every two hax figures. |
|
| 1717 | + * |
|
| 1718 | + * If an invalid string is passed, it will be returned without change. |
|
| 1719 | + * |
|
| 1720 | + * @param string $guid |
|
| 1721 | + * @return string |
|
| 1722 | + */ |
|
| 1723 | + public function formatGuid2ForFilterUser($guid) { |
|
| 1724 | + if(!is_string($guid)) { |
|
| 1725 | + throw new \InvalidArgumentException('String expected'); |
|
| 1726 | + } |
|
| 1727 | + $blocks = explode('-', $guid); |
|
| 1728 | + if(count($blocks) !== 5) { |
|
| 1729 | + /* |
|
| 1730 | 1730 | * Why not throw an Exception instead? This method is a utility |
| 1731 | 1731 | * called only when trying to figure out whether a "missing" known |
| 1732 | 1732 | * LDAP user was or was not renamed on the LDAP server. And this |
@@ -1737,270 +1737,270 @@ discard block |
||
| 1737 | 1737 | * an exception here would kill the experience for a valid, acting |
| 1738 | 1738 | * user. Instead we write a log message. |
| 1739 | 1739 | */ |
| 1740 | - \OC::$server->getLogger()->info( |
|
| 1741 | - 'Passed string does not resemble a valid GUID. Known UUID ' . |
|
| 1742 | - '({uuid}) probably does not match UUID configuration.', |
|
| 1743 | - [ 'app' => 'user_ldap', 'uuid' => $guid ] |
|
| 1744 | - ); |
|
| 1745 | - return $guid; |
|
| 1746 | - } |
|
| 1747 | - for($i=0; $i < 3; $i++) { |
|
| 1748 | - $pairs = str_split($blocks[$i], 2); |
|
| 1749 | - $pairs = array_reverse($pairs); |
|
| 1750 | - $blocks[$i] = implode('', $pairs); |
|
| 1751 | - } |
|
| 1752 | - for($i=0; $i < 5; $i++) { |
|
| 1753 | - $pairs = str_split($blocks[$i], 2); |
|
| 1754 | - $blocks[$i] = '\\' . implode('\\', $pairs); |
|
| 1755 | - } |
|
| 1756 | - return implode('', $blocks); |
|
| 1757 | - } |
|
| 1758 | - |
|
| 1759 | - /** |
|
| 1760 | - * gets a SID of the domain of the given dn |
|
| 1761 | - * @param string $dn |
|
| 1762 | - * @return string|bool |
|
| 1763 | - */ |
|
| 1764 | - public function getSID($dn) { |
|
| 1765 | - $domainDN = $this->getDomainDNFromDN($dn); |
|
| 1766 | - $cacheKey = 'getSID-'.$domainDN; |
|
| 1767 | - $sid = $this->connection->getFromCache($cacheKey); |
|
| 1768 | - if(!is_null($sid)) { |
|
| 1769 | - return $sid; |
|
| 1770 | - } |
|
| 1771 | - |
|
| 1772 | - $objectSid = $this->readAttribute($domainDN, 'objectsid'); |
|
| 1773 | - if(!is_array($objectSid) || empty($objectSid)) { |
|
| 1774 | - $this->connection->writeToCache($cacheKey, false); |
|
| 1775 | - return false; |
|
| 1776 | - } |
|
| 1777 | - $domainObjectSid = $this->convertSID2Str($objectSid[0]); |
|
| 1778 | - $this->connection->writeToCache($cacheKey, $domainObjectSid); |
|
| 1779 | - |
|
| 1780 | - return $domainObjectSid; |
|
| 1781 | - } |
|
| 1782 | - |
|
| 1783 | - /** |
|
| 1784 | - * converts a binary SID into a string representation |
|
| 1785 | - * @param string $sid |
|
| 1786 | - * @return string |
|
| 1787 | - */ |
|
| 1788 | - public function convertSID2Str($sid) { |
|
| 1789 | - // The format of a SID binary string is as follows: |
|
| 1790 | - // 1 byte for the revision level |
|
| 1791 | - // 1 byte for the number n of variable sub-ids |
|
| 1792 | - // 6 bytes for identifier authority value |
|
| 1793 | - // n*4 bytes for n sub-ids |
|
| 1794 | - // |
|
| 1795 | - // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f |
|
| 1796 | - // Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444 |
|
| 1797 | - $revision = ord($sid[0]); |
|
| 1798 | - $numberSubID = ord($sid[1]); |
|
| 1799 | - |
|
| 1800 | - $subIdStart = 8; // 1 + 1 + 6 |
|
| 1801 | - $subIdLength = 4; |
|
| 1802 | - if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) { |
|
| 1803 | - // Incorrect number of bytes present. |
|
| 1804 | - return ''; |
|
| 1805 | - } |
|
| 1806 | - |
|
| 1807 | - // 6 bytes = 48 bits can be represented using floats without loss of |
|
| 1808 | - // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71) |
|
| 1809 | - $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', ''); |
|
| 1810 | - |
|
| 1811 | - $subIDs = array(); |
|
| 1812 | - for ($i = 0; $i < $numberSubID; $i++) { |
|
| 1813 | - $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength)); |
|
| 1814 | - $subIDs[] = sprintf('%u', $subID[1]); |
|
| 1815 | - } |
|
| 1816 | - |
|
| 1817 | - // Result for example above: S-1-5-21-249921958-728525901-1594176202 |
|
| 1818 | - return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs)); |
|
| 1819 | - } |
|
| 1820 | - |
|
| 1821 | - /** |
|
| 1822 | - * checks if the given DN is part of the given base DN(s) |
|
| 1823 | - * @param string $dn the DN |
|
| 1824 | - * @param string[] $bases array containing the allowed base DN or DNs |
|
| 1825 | - * @return bool |
|
| 1826 | - */ |
|
| 1827 | - public function isDNPartOfBase($dn, $bases) { |
|
| 1828 | - $belongsToBase = false; |
|
| 1829 | - $bases = $this->helper->sanitizeDN($bases); |
|
| 1830 | - |
|
| 1831 | - foreach($bases as $base) { |
|
| 1832 | - $belongsToBase = true; |
|
| 1833 | - if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) { |
|
| 1834 | - $belongsToBase = false; |
|
| 1835 | - } |
|
| 1836 | - if($belongsToBase) { |
|
| 1837 | - break; |
|
| 1838 | - } |
|
| 1839 | - } |
|
| 1840 | - return $belongsToBase; |
|
| 1841 | - } |
|
| 1842 | - |
|
| 1843 | - /** |
|
| 1844 | - * resets a running Paged Search operation |
|
| 1845 | - */ |
|
| 1846 | - private function abandonPagedSearch() { |
|
| 1847 | - if($this->connection->hasPagedResultSupport) { |
|
| 1848 | - $cr = $this->connection->getConnectionResource(); |
|
| 1849 | - $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie); |
|
| 1850 | - $this->getPagedSearchResultState(); |
|
| 1851 | - $this->lastCookie = ''; |
|
| 1852 | - $this->cookies = array(); |
|
| 1853 | - } |
|
| 1854 | - } |
|
| 1855 | - |
|
| 1856 | - /** |
|
| 1857 | - * get a cookie for the next LDAP paged search |
|
| 1858 | - * @param string $base a string with the base DN for the search |
|
| 1859 | - * @param string $filter the search filter to identify the correct search |
|
| 1860 | - * @param int $limit the limit (or 'pageSize'), to identify the correct search well |
|
| 1861 | - * @param int $offset the offset for the new search to identify the correct search really good |
|
| 1862 | - * @return string containing the key or empty if none is cached |
|
| 1863 | - */ |
|
| 1864 | - private function getPagedResultCookie($base, $filter, $limit, $offset) { |
|
| 1865 | - if($offset === 0) { |
|
| 1866 | - return ''; |
|
| 1867 | - } |
|
| 1868 | - $offset -= $limit; |
|
| 1869 | - //we work with cache here |
|
| 1870 | - $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset; |
|
| 1871 | - $cookie = ''; |
|
| 1872 | - if(isset($this->cookies[$cacheKey])) { |
|
| 1873 | - $cookie = $this->cookies[$cacheKey]; |
|
| 1874 | - if(is_null($cookie)) { |
|
| 1875 | - $cookie = ''; |
|
| 1876 | - } |
|
| 1877 | - } |
|
| 1878 | - return $cookie; |
|
| 1879 | - } |
|
| 1880 | - |
|
| 1881 | - /** |
|
| 1882 | - * checks whether an LDAP paged search operation has more pages that can be |
|
| 1883 | - * retrieved, typically when offset and limit are provided. |
|
| 1884 | - * |
|
| 1885 | - * Be very careful to use it: the last cookie value, which is inspected, can |
|
| 1886 | - * be reset by other operations. Best, call it immediately after a search(), |
|
| 1887 | - * searchUsers() or searchGroups() call. count-methods are probably safe as |
|
| 1888 | - * well. Don't rely on it with any fetchList-method. |
|
| 1889 | - * @return bool |
|
| 1890 | - */ |
|
| 1891 | - public function hasMoreResults() { |
|
| 1892 | - if(!$this->connection->hasPagedResultSupport) { |
|
| 1893 | - return false; |
|
| 1894 | - } |
|
| 1895 | - |
|
| 1896 | - if(empty($this->lastCookie) && $this->lastCookie !== '0') { |
|
| 1897 | - // as in RFC 2696, when all results are returned, the cookie will |
|
| 1898 | - // be empty. |
|
| 1899 | - return false; |
|
| 1900 | - } |
|
| 1901 | - |
|
| 1902 | - return true; |
|
| 1903 | - } |
|
| 1904 | - |
|
| 1905 | - /** |
|
| 1906 | - * set a cookie for LDAP paged search run |
|
| 1907 | - * @param string $base a string with the base DN for the search |
|
| 1908 | - * @param string $filter the search filter to identify the correct search |
|
| 1909 | - * @param int $limit the limit (or 'pageSize'), to identify the correct search well |
|
| 1910 | - * @param int $offset the offset for the run search to identify the correct search really good |
|
| 1911 | - * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response |
|
| 1912 | - * @return void |
|
| 1913 | - */ |
|
| 1914 | - private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) { |
|
| 1915 | - // allow '0' for 389ds |
|
| 1916 | - if(!empty($cookie) || $cookie === '0') { |
|
| 1917 | - $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset; |
|
| 1918 | - $this->cookies[$cacheKey] = $cookie; |
|
| 1919 | - $this->lastCookie = $cookie; |
|
| 1920 | - } |
|
| 1921 | - } |
|
| 1922 | - |
|
| 1923 | - /** |
|
| 1924 | - * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search. |
|
| 1925 | - * @return boolean|null true on success, null or false otherwise |
|
| 1926 | - */ |
|
| 1927 | - public function getPagedSearchResultState() { |
|
| 1928 | - $result = $this->pagedSearchedSuccessful; |
|
| 1929 | - $this->pagedSearchedSuccessful = null; |
|
| 1930 | - return $result; |
|
| 1931 | - } |
|
| 1932 | - |
|
| 1933 | - /** |
|
| 1934 | - * Prepares a paged search, if possible |
|
| 1935 | - * @param string $filter the LDAP filter for the search |
|
| 1936 | - * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched |
|
| 1937 | - * @param string[] $attr optional, when a certain attribute shall be filtered outside |
|
| 1938 | - * @param int $limit |
|
| 1939 | - * @param int $offset |
|
| 1940 | - * @return bool|true |
|
| 1941 | - */ |
|
| 1942 | - private function initPagedSearch($filter, $bases, $attr, $limit, $offset) { |
|
| 1943 | - $pagedSearchOK = false; |
|
| 1944 | - if($this->connection->hasPagedResultSupport && ($limit !== 0)) { |
|
| 1945 | - $offset = (int)$offset; //can be null |
|
| 1946 | - \OCP\Util::writeLog('user_ldap', |
|
| 1947 | - 'initializing paged search for Filter '.$filter.' base '.print_r($bases, true) |
|
| 1948 | - .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset, |
|
| 1949 | - ILogger::DEBUG); |
|
| 1950 | - //get the cookie from the search for the previous search, required by LDAP |
|
| 1951 | - foreach($bases as $base) { |
|
| 1952 | - |
|
| 1953 | - $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset); |
|
| 1954 | - if(empty($cookie) && $cookie !== "0" && ($offset > 0)) { |
|
| 1955 | - // no cookie known from a potential previous search. We need |
|
| 1956 | - // to start from 0 to come to the desired page. cookie value |
|
| 1957 | - // of '0' is valid, because 389ds |
|
| 1958 | - $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit; |
|
| 1959 | - $this->search($filter, array($base), $attr, $limit, $reOffset, true); |
|
| 1960 | - $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset); |
|
| 1961 | - //still no cookie? obviously, the server does not like us. Let's skip paging efforts. |
|
| 1962 | - // '0' is valid, because 389ds |
|
| 1963 | - //TODO: remember this, probably does not change in the next request... |
|
| 1964 | - if(empty($cookie) && $cookie !== '0') { |
|
| 1965 | - $cookie = null; |
|
| 1966 | - } |
|
| 1967 | - } |
|
| 1968 | - if(!is_null($cookie)) { |
|
| 1969 | - //since offset = 0, this is a new search. We abandon other searches that might be ongoing. |
|
| 1970 | - $this->abandonPagedSearch(); |
|
| 1971 | - $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', |
|
| 1972 | - $this->connection->getConnectionResource(), $limit, |
|
| 1973 | - false, $cookie); |
|
| 1974 | - if(!$pagedSearchOK) { |
|
| 1975 | - return false; |
|
| 1976 | - } |
|
| 1977 | - \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG); |
|
| 1978 | - } else { |
|
| 1979 | - $e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset); |
|
| 1980 | - \OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]); |
|
| 1981 | - } |
|
| 1982 | - |
|
| 1983 | - } |
|
| 1984 | - /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1740 | + \OC::$server->getLogger()->info( |
|
| 1741 | + 'Passed string does not resemble a valid GUID. Known UUID ' . |
|
| 1742 | + '({uuid}) probably does not match UUID configuration.', |
|
| 1743 | + [ 'app' => 'user_ldap', 'uuid' => $guid ] |
|
| 1744 | + ); |
|
| 1745 | + return $guid; |
|
| 1746 | + } |
|
| 1747 | + for($i=0; $i < 3; $i++) { |
|
| 1748 | + $pairs = str_split($blocks[$i], 2); |
|
| 1749 | + $pairs = array_reverse($pairs); |
|
| 1750 | + $blocks[$i] = implode('', $pairs); |
|
| 1751 | + } |
|
| 1752 | + for($i=0; $i < 5; $i++) { |
|
| 1753 | + $pairs = str_split($blocks[$i], 2); |
|
| 1754 | + $blocks[$i] = '\\' . implode('\\', $pairs); |
|
| 1755 | + } |
|
| 1756 | + return implode('', $blocks); |
|
| 1757 | + } |
|
| 1758 | + |
|
| 1759 | + /** |
|
| 1760 | + * gets a SID of the domain of the given dn |
|
| 1761 | + * @param string $dn |
|
| 1762 | + * @return string|bool |
|
| 1763 | + */ |
|
| 1764 | + public function getSID($dn) { |
|
| 1765 | + $domainDN = $this->getDomainDNFromDN($dn); |
|
| 1766 | + $cacheKey = 'getSID-'.$domainDN; |
|
| 1767 | + $sid = $this->connection->getFromCache($cacheKey); |
|
| 1768 | + if(!is_null($sid)) { |
|
| 1769 | + return $sid; |
|
| 1770 | + } |
|
| 1771 | + |
|
| 1772 | + $objectSid = $this->readAttribute($domainDN, 'objectsid'); |
|
| 1773 | + if(!is_array($objectSid) || empty($objectSid)) { |
|
| 1774 | + $this->connection->writeToCache($cacheKey, false); |
|
| 1775 | + return false; |
|
| 1776 | + } |
|
| 1777 | + $domainObjectSid = $this->convertSID2Str($objectSid[0]); |
|
| 1778 | + $this->connection->writeToCache($cacheKey, $domainObjectSid); |
|
| 1779 | + |
|
| 1780 | + return $domainObjectSid; |
|
| 1781 | + } |
|
| 1782 | + |
|
| 1783 | + /** |
|
| 1784 | + * converts a binary SID into a string representation |
|
| 1785 | + * @param string $sid |
|
| 1786 | + * @return string |
|
| 1787 | + */ |
|
| 1788 | + public function convertSID2Str($sid) { |
|
| 1789 | + // The format of a SID binary string is as follows: |
|
| 1790 | + // 1 byte for the revision level |
|
| 1791 | + // 1 byte for the number n of variable sub-ids |
|
| 1792 | + // 6 bytes for identifier authority value |
|
| 1793 | + // n*4 bytes for n sub-ids |
|
| 1794 | + // |
|
| 1795 | + // Example: 010400000000000515000000a681e50e4d6c6c2bca32055f |
|
| 1796 | + // Legend: RRNNAAAAAAAAAAAA11111111222222223333333344444444 |
|
| 1797 | + $revision = ord($sid[0]); |
|
| 1798 | + $numberSubID = ord($sid[1]); |
|
| 1799 | + |
|
| 1800 | + $subIdStart = 8; // 1 + 1 + 6 |
|
| 1801 | + $subIdLength = 4; |
|
| 1802 | + if (strlen($sid) !== $subIdStart + $subIdLength * $numberSubID) { |
|
| 1803 | + // Incorrect number of bytes present. |
|
| 1804 | + return ''; |
|
| 1805 | + } |
|
| 1806 | + |
|
| 1807 | + // 6 bytes = 48 bits can be represented using floats without loss of |
|
| 1808 | + // precision (see https://gist.github.com/bantu/886ac680b0aef5812f71) |
|
| 1809 | + $iav = number_format(hexdec(bin2hex(substr($sid, 2, 6))), 0, '', ''); |
|
| 1810 | + |
|
| 1811 | + $subIDs = array(); |
|
| 1812 | + for ($i = 0; $i < $numberSubID; $i++) { |
|
| 1813 | + $subID = unpack('V', substr($sid, $subIdStart + $subIdLength * $i, $subIdLength)); |
|
| 1814 | + $subIDs[] = sprintf('%u', $subID[1]); |
|
| 1815 | + } |
|
| 1816 | + |
|
| 1817 | + // Result for example above: S-1-5-21-249921958-728525901-1594176202 |
|
| 1818 | + return sprintf('S-%d-%s-%s', $revision, $iav, implode('-', $subIDs)); |
|
| 1819 | + } |
|
| 1820 | + |
|
| 1821 | + /** |
|
| 1822 | + * checks if the given DN is part of the given base DN(s) |
|
| 1823 | + * @param string $dn the DN |
|
| 1824 | + * @param string[] $bases array containing the allowed base DN or DNs |
|
| 1825 | + * @return bool |
|
| 1826 | + */ |
|
| 1827 | + public function isDNPartOfBase($dn, $bases) { |
|
| 1828 | + $belongsToBase = false; |
|
| 1829 | + $bases = $this->helper->sanitizeDN($bases); |
|
| 1830 | + |
|
| 1831 | + foreach($bases as $base) { |
|
| 1832 | + $belongsToBase = true; |
|
| 1833 | + if(mb_strripos($dn, $base, 0, 'UTF-8') !== (mb_strlen($dn, 'UTF-8')-mb_strlen($base, 'UTF-8'))) { |
|
| 1834 | + $belongsToBase = false; |
|
| 1835 | + } |
|
| 1836 | + if($belongsToBase) { |
|
| 1837 | + break; |
|
| 1838 | + } |
|
| 1839 | + } |
|
| 1840 | + return $belongsToBase; |
|
| 1841 | + } |
|
| 1842 | + |
|
| 1843 | + /** |
|
| 1844 | + * resets a running Paged Search operation |
|
| 1845 | + */ |
|
| 1846 | + private function abandonPagedSearch() { |
|
| 1847 | + if($this->connection->hasPagedResultSupport) { |
|
| 1848 | + $cr = $this->connection->getConnectionResource(); |
|
| 1849 | + $this->invokeLDAPMethod('controlPagedResult', $cr, 0, false, $this->lastCookie); |
|
| 1850 | + $this->getPagedSearchResultState(); |
|
| 1851 | + $this->lastCookie = ''; |
|
| 1852 | + $this->cookies = array(); |
|
| 1853 | + } |
|
| 1854 | + } |
|
| 1855 | + |
|
| 1856 | + /** |
|
| 1857 | + * get a cookie for the next LDAP paged search |
|
| 1858 | + * @param string $base a string with the base DN for the search |
|
| 1859 | + * @param string $filter the search filter to identify the correct search |
|
| 1860 | + * @param int $limit the limit (or 'pageSize'), to identify the correct search well |
|
| 1861 | + * @param int $offset the offset for the new search to identify the correct search really good |
|
| 1862 | + * @return string containing the key or empty if none is cached |
|
| 1863 | + */ |
|
| 1864 | + private function getPagedResultCookie($base, $filter, $limit, $offset) { |
|
| 1865 | + if($offset === 0) { |
|
| 1866 | + return ''; |
|
| 1867 | + } |
|
| 1868 | + $offset -= $limit; |
|
| 1869 | + //we work with cache here |
|
| 1870 | + $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset; |
|
| 1871 | + $cookie = ''; |
|
| 1872 | + if(isset($this->cookies[$cacheKey])) { |
|
| 1873 | + $cookie = $this->cookies[$cacheKey]; |
|
| 1874 | + if(is_null($cookie)) { |
|
| 1875 | + $cookie = ''; |
|
| 1876 | + } |
|
| 1877 | + } |
|
| 1878 | + return $cookie; |
|
| 1879 | + } |
|
| 1880 | + |
|
| 1881 | + /** |
|
| 1882 | + * checks whether an LDAP paged search operation has more pages that can be |
|
| 1883 | + * retrieved, typically when offset and limit are provided. |
|
| 1884 | + * |
|
| 1885 | + * Be very careful to use it: the last cookie value, which is inspected, can |
|
| 1886 | + * be reset by other operations. Best, call it immediately after a search(), |
|
| 1887 | + * searchUsers() or searchGroups() call. count-methods are probably safe as |
|
| 1888 | + * well. Don't rely on it with any fetchList-method. |
|
| 1889 | + * @return bool |
|
| 1890 | + */ |
|
| 1891 | + public function hasMoreResults() { |
|
| 1892 | + if(!$this->connection->hasPagedResultSupport) { |
|
| 1893 | + return false; |
|
| 1894 | + } |
|
| 1895 | + |
|
| 1896 | + if(empty($this->lastCookie) && $this->lastCookie !== '0') { |
|
| 1897 | + // as in RFC 2696, when all results are returned, the cookie will |
|
| 1898 | + // be empty. |
|
| 1899 | + return false; |
|
| 1900 | + } |
|
| 1901 | + |
|
| 1902 | + return true; |
|
| 1903 | + } |
|
| 1904 | + |
|
| 1905 | + /** |
|
| 1906 | + * set a cookie for LDAP paged search run |
|
| 1907 | + * @param string $base a string with the base DN for the search |
|
| 1908 | + * @param string $filter the search filter to identify the correct search |
|
| 1909 | + * @param int $limit the limit (or 'pageSize'), to identify the correct search well |
|
| 1910 | + * @param int $offset the offset for the run search to identify the correct search really good |
|
| 1911 | + * @param string $cookie string containing the cookie returned by ldap_control_paged_result_response |
|
| 1912 | + * @return void |
|
| 1913 | + */ |
|
| 1914 | + private function setPagedResultCookie($base, $filter, $limit, $offset, $cookie) { |
|
| 1915 | + // allow '0' for 389ds |
|
| 1916 | + if(!empty($cookie) || $cookie === '0') { |
|
| 1917 | + $cacheKey = 'lc' . crc32($base) . '-' . crc32($filter) . '-' . (int)$limit . '-' . (int)$offset; |
|
| 1918 | + $this->cookies[$cacheKey] = $cookie; |
|
| 1919 | + $this->lastCookie = $cookie; |
|
| 1920 | + } |
|
| 1921 | + } |
|
| 1922 | + |
|
| 1923 | + /** |
|
| 1924 | + * Check whether the most recent paged search was successful. It flushed the state var. Use it always after a possible paged search. |
|
| 1925 | + * @return boolean|null true on success, null or false otherwise |
|
| 1926 | + */ |
|
| 1927 | + public function getPagedSearchResultState() { |
|
| 1928 | + $result = $this->pagedSearchedSuccessful; |
|
| 1929 | + $this->pagedSearchedSuccessful = null; |
|
| 1930 | + return $result; |
|
| 1931 | + } |
|
| 1932 | + |
|
| 1933 | + /** |
|
| 1934 | + * Prepares a paged search, if possible |
|
| 1935 | + * @param string $filter the LDAP filter for the search |
|
| 1936 | + * @param string[] $bases an array containing the LDAP subtree(s) that shall be searched |
|
| 1937 | + * @param string[] $attr optional, when a certain attribute shall be filtered outside |
|
| 1938 | + * @param int $limit |
|
| 1939 | + * @param int $offset |
|
| 1940 | + * @return bool|true |
|
| 1941 | + */ |
|
| 1942 | + private function initPagedSearch($filter, $bases, $attr, $limit, $offset) { |
|
| 1943 | + $pagedSearchOK = false; |
|
| 1944 | + if($this->connection->hasPagedResultSupport && ($limit !== 0)) { |
|
| 1945 | + $offset = (int)$offset; //can be null |
|
| 1946 | + \OCP\Util::writeLog('user_ldap', |
|
| 1947 | + 'initializing paged search for Filter '.$filter.' base '.print_r($bases, true) |
|
| 1948 | + .' attr '.print_r($attr, true). ' limit ' .$limit.' offset '.$offset, |
|
| 1949 | + ILogger::DEBUG); |
|
| 1950 | + //get the cookie from the search for the previous search, required by LDAP |
|
| 1951 | + foreach($bases as $base) { |
|
| 1952 | + |
|
| 1953 | + $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset); |
|
| 1954 | + if(empty($cookie) && $cookie !== "0" && ($offset > 0)) { |
|
| 1955 | + // no cookie known from a potential previous search. We need |
|
| 1956 | + // to start from 0 to come to the desired page. cookie value |
|
| 1957 | + // of '0' is valid, because 389ds |
|
| 1958 | + $reOffset = ($offset - $limit) < 0 ? 0 : $offset - $limit; |
|
| 1959 | + $this->search($filter, array($base), $attr, $limit, $reOffset, true); |
|
| 1960 | + $cookie = $this->getPagedResultCookie($base, $filter, $limit, $offset); |
|
| 1961 | + //still no cookie? obviously, the server does not like us. Let's skip paging efforts. |
|
| 1962 | + // '0' is valid, because 389ds |
|
| 1963 | + //TODO: remember this, probably does not change in the next request... |
|
| 1964 | + if(empty($cookie) && $cookie !== '0') { |
|
| 1965 | + $cookie = null; |
|
| 1966 | + } |
|
| 1967 | + } |
|
| 1968 | + if(!is_null($cookie)) { |
|
| 1969 | + //since offset = 0, this is a new search. We abandon other searches that might be ongoing. |
|
| 1970 | + $this->abandonPagedSearch(); |
|
| 1971 | + $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', |
|
| 1972 | + $this->connection->getConnectionResource(), $limit, |
|
| 1973 | + false, $cookie); |
|
| 1974 | + if(!$pagedSearchOK) { |
|
| 1975 | + return false; |
|
| 1976 | + } |
|
| 1977 | + \OCP\Util::writeLog('user_ldap', 'Ready for a paged search', ILogger::DEBUG); |
|
| 1978 | + } else { |
|
| 1979 | + $e = new \Exception('No paged search possible, Limit '.$limit.' Offset '.$offset); |
|
| 1980 | + \OC::$server->getLogger()->logException($e, ['level' => ILogger::DEBUG]); |
|
| 1981 | + } |
|
| 1982 | + |
|
| 1983 | + } |
|
| 1984 | + /* ++ Fixing RHDS searches with pages with zero results ++ |
|
| 1985 | 1985 | * We coudn't get paged searches working with our RHDS for login ($limit = 0), |
| 1986 | 1986 | * due to pages with zero results. |
| 1987 | 1987 | * So we added "&& !empty($this->lastCookie)" to this test to ignore pagination |
| 1988 | 1988 | * if we don't have a previous paged search. |
| 1989 | 1989 | */ |
| 1990 | - } else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) { |
|
| 1991 | - // a search without limit was requested. However, if we do use |
|
| 1992 | - // Paged Search once, we always must do it. This requires us to |
|
| 1993 | - // initialize it with the configured page size. |
|
| 1994 | - $this->abandonPagedSearch(); |
|
| 1995 | - // in case someone set it to 0 … use 500, otherwise no results will |
|
| 1996 | - // be returned. |
|
| 1997 | - $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500; |
|
| 1998 | - $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', |
|
| 1999 | - $this->connection->getConnectionResource(), |
|
| 2000 | - $pageSize, false, ''); |
|
| 2001 | - } |
|
| 2002 | - |
|
| 2003 | - return $pagedSearchOK; |
|
| 2004 | - } |
|
| 1990 | + } else if($this->connection->hasPagedResultSupport && $limit === 0 && !empty($this->lastCookie)) { |
|
| 1991 | + // a search without limit was requested. However, if we do use |
|
| 1992 | + // Paged Search once, we always must do it. This requires us to |
|
| 1993 | + // initialize it with the configured page size. |
|
| 1994 | + $this->abandonPagedSearch(); |
|
| 1995 | + // in case someone set it to 0 … use 500, otherwise no results will |
|
| 1996 | + // be returned. |
|
| 1997 | + $pageSize = (int)$this->connection->ldapPagingSize > 0 ? (int)$this->connection->ldapPagingSize : 500; |
|
| 1998 | + $pagedSearchOK = $this->invokeLDAPMethod('controlPagedResult', |
|
| 1999 | + $this->connection->getConnectionResource(), |
|
| 2000 | + $pageSize, false, ''); |
|
| 2001 | + } |
|
| 2002 | + |
|
| 2003 | + return $pagedSearchOK; |
|
| 2004 | + } |
|
| 2005 | 2005 | |
| 2006 | 2006 | } |
@@ -47,655 +47,655 @@ |
||
| 47 | 47 | * represents an LDAP user, gets and holds user-specific information from LDAP |
| 48 | 48 | */ |
| 49 | 49 | class User { |
| 50 | - /** |
|
| 51 | - * @var IUserTools |
|
| 52 | - */ |
|
| 53 | - protected $access; |
|
| 54 | - /** |
|
| 55 | - * @var Connection |
|
| 56 | - */ |
|
| 57 | - protected $connection; |
|
| 58 | - /** |
|
| 59 | - * @var IConfig |
|
| 60 | - */ |
|
| 61 | - protected $config; |
|
| 62 | - /** |
|
| 63 | - * @var FilesystemHelper |
|
| 64 | - */ |
|
| 65 | - protected $fs; |
|
| 66 | - /** |
|
| 67 | - * @var Image |
|
| 68 | - */ |
|
| 69 | - protected $image; |
|
| 70 | - /** |
|
| 71 | - * @var LogWrapper |
|
| 72 | - */ |
|
| 73 | - protected $log; |
|
| 74 | - /** |
|
| 75 | - * @var IAvatarManager |
|
| 76 | - */ |
|
| 77 | - protected $avatarManager; |
|
| 78 | - /** |
|
| 79 | - * @var IUserManager |
|
| 80 | - */ |
|
| 81 | - protected $userManager; |
|
| 82 | - /** |
|
| 83 | - * @var INotificationManager |
|
| 84 | - */ |
|
| 85 | - protected $notificationManager; |
|
| 86 | - /** |
|
| 87 | - * @var string |
|
| 88 | - */ |
|
| 89 | - protected $dn; |
|
| 90 | - /** |
|
| 91 | - * @var string |
|
| 92 | - */ |
|
| 93 | - protected $uid; |
|
| 94 | - /** |
|
| 95 | - * @var string[] |
|
| 96 | - */ |
|
| 97 | - protected $refreshedFeatures = array(); |
|
| 98 | - /** |
|
| 99 | - * @var string |
|
| 100 | - */ |
|
| 101 | - protected $avatarImage; |
|
| 102 | - |
|
| 103 | - /** |
|
| 104 | - * DB config keys for user preferences |
|
| 105 | - */ |
|
| 106 | - const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished'; |
|
| 107 | - const USER_PREFKEY_LASTREFRESH = 'lastFeatureRefresh'; |
|
| 108 | - |
|
| 109 | - /** |
|
| 110 | - * @brief constructor, make sure the subclasses call this one! |
|
| 111 | - * @param string $username the internal username |
|
| 112 | - * @param string $dn the LDAP DN |
|
| 113 | - * @param IUserTools $access an instance that implements IUserTools for |
|
| 114 | - * LDAP interaction |
|
| 115 | - * @param IConfig $config |
|
| 116 | - * @param FilesystemHelper $fs |
|
| 117 | - * @param Image $image any empty instance |
|
| 118 | - * @param LogWrapper $log |
|
| 119 | - * @param IAvatarManager $avatarManager |
|
| 120 | - * @param IUserManager $userManager |
|
| 121 | - * @param INotificationManager $notificationManager |
|
| 122 | - */ |
|
| 123 | - public function __construct($username, $dn, IUserTools $access, |
|
| 124 | - IConfig $config, FilesystemHelper $fs, Image $image, |
|
| 125 | - LogWrapper $log, IAvatarManager $avatarManager, IUserManager $userManager, |
|
| 126 | - INotificationManager $notificationManager) { |
|
| 50 | + /** |
|
| 51 | + * @var IUserTools |
|
| 52 | + */ |
|
| 53 | + protected $access; |
|
| 54 | + /** |
|
| 55 | + * @var Connection |
|
| 56 | + */ |
|
| 57 | + protected $connection; |
|
| 58 | + /** |
|
| 59 | + * @var IConfig |
|
| 60 | + */ |
|
| 61 | + protected $config; |
|
| 62 | + /** |
|
| 63 | + * @var FilesystemHelper |
|
| 64 | + */ |
|
| 65 | + protected $fs; |
|
| 66 | + /** |
|
| 67 | + * @var Image |
|
| 68 | + */ |
|
| 69 | + protected $image; |
|
| 70 | + /** |
|
| 71 | + * @var LogWrapper |
|
| 72 | + */ |
|
| 73 | + protected $log; |
|
| 74 | + /** |
|
| 75 | + * @var IAvatarManager |
|
| 76 | + */ |
|
| 77 | + protected $avatarManager; |
|
| 78 | + /** |
|
| 79 | + * @var IUserManager |
|
| 80 | + */ |
|
| 81 | + protected $userManager; |
|
| 82 | + /** |
|
| 83 | + * @var INotificationManager |
|
| 84 | + */ |
|
| 85 | + protected $notificationManager; |
|
| 86 | + /** |
|
| 87 | + * @var string |
|
| 88 | + */ |
|
| 89 | + protected $dn; |
|
| 90 | + /** |
|
| 91 | + * @var string |
|
| 92 | + */ |
|
| 93 | + protected $uid; |
|
| 94 | + /** |
|
| 95 | + * @var string[] |
|
| 96 | + */ |
|
| 97 | + protected $refreshedFeatures = array(); |
|
| 98 | + /** |
|
| 99 | + * @var string |
|
| 100 | + */ |
|
| 101 | + protected $avatarImage; |
|
| 102 | + |
|
| 103 | + /** |
|
| 104 | + * DB config keys for user preferences |
|
| 105 | + */ |
|
| 106 | + const USER_PREFKEY_FIRSTLOGIN = 'firstLoginAccomplished'; |
|
| 107 | + const USER_PREFKEY_LASTREFRESH = 'lastFeatureRefresh'; |
|
| 108 | + |
|
| 109 | + /** |
|
| 110 | + * @brief constructor, make sure the subclasses call this one! |
|
| 111 | + * @param string $username the internal username |
|
| 112 | + * @param string $dn the LDAP DN |
|
| 113 | + * @param IUserTools $access an instance that implements IUserTools for |
|
| 114 | + * LDAP interaction |
|
| 115 | + * @param IConfig $config |
|
| 116 | + * @param FilesystemHelper $fs |
|
| 117 | + * @param Image $image any empty instance |
|
| 118 | + * @param LogWrapper $log |
|
| 119 | + * @param IAvatarManager $avatarManager |
|
| 120 | + * @param IUserManager $userManager |
|
| 121 | + * @param INotificationManager $notificationManager |
|
| 122 | + */ |
|
| 123 | + public function __construct($username, $dn, IUserTools $access, |
|
| 124 | + IConfig $config, FilesystemHelper $fs, Image $image, |
|
| 125 | + LogWrapper $log, IAvatarManager $avatarManager, IUserManager $userManager, |
|
| 126 | + INotificationManager $notificationManager) { |
|
| 127 | 127 | |
| 128 | - if ($username === null) { |
|
| 129 | - $log->log("uid for '$dn' must not be null!", ILogger::ERROR); |
|
| 130 | - throw new \InvalidArgumentException('uid must not be null!'); |
|
| 131 | - } else if ($username === '') { |
|
| 132 | - $log->log("uid for '$dn' must not be an empty string", ILogger::ERROR); |
|
| 133 | - throw new \InvalidArgumentException('uid must not be an empty string!'); |
|
| 134 | - } |
|
| 135 | - |
|
| 136 | - $this->access = $access; |
|
| 137 | - $this->connection = $access->getConnection(); |
|
| 138 | - $this->config = $config; |
|
| 139 | - $this->fs = $fs; |
|
| 140 | - $this->dn = $dn; |
|
| 141 | - $this->uid = $username; |
|
| 142 | - $this->image = $image; |
|
| 143 | - $this->log = $log; |
|
| 144 | - $this->avatarManager = $avatarManager; |
|
| 145 | - $this->userManager = $userManager; |
|
| 146 | - $this->notificationManager = $notificationManager; |
|
| 147 | - |
|
| 148 | - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); |
|
| 149 | - } |
|
| 150 | - |
|
| 151 | - /** |
|
| 152 | - * @brief updates properties like email, quota or avatar provided by LDAP |
|
| 153 | - * @return null |
|
| 154 | - */ |
|
| 155 | - public function update() { |
|
| 156 | - if(is_null($this->dn)) { |
|
| 157 | - return null; |
|
| 158 | - } |
|
| 159 | - |
|
| 160 | - $hasLoggedIn = $this->config->getUserValue($this->uid, 'user_ldap', |
|
| 161 | - self::USER_PREFKEY_FIRSTLOGIN, 0); |
|
| 162 | - |
|
| 163 | - if($this->needsRefresh()) { |
|
| 164 | - $this->updateEmail(); |
|
| 165 | - $this->updateQuota(); |
|
| 166 | - if($hasLoggedIn !== 0) { |
|
| 167 | - //we do not need to try it, when the user has not been logged in |
|
| 168 | - //before, because the file system will not be ready. |
|
| 169 | - $this->updateAvatar(); |
|
| 170 | - //in order to get an avatar as soon as possible, mark the user |
|
| 171 | - //as refreshed only when updating the avatar did happen |
|
| 172 | - $this->markRefreshTime(); |
|
| 173 | - } |
|
| 174 | - } |
|
| 175 | - } |
|
| 176 | - |
|
| 177 | - /** |
|
| 178 | - * processes results from LDAP for attributes as returned by getAttributesToRead() |
|
| 179 | - * @param array $ldapEntry the user entry as retrieved from LDAP |
|
| 180 | - */ |
|
| 181 | - public function processAttributes($ldapEntry) { |
|
| 182 | - $this->markRefreshTime(); |
|
| 183 | - //Quota |
|
| 184 | - $attr = strtolower($this->connection->ldapQuotaAttribute); |
|
| 185 | - if(isset($ldapEntry[$attr])) { |
|
| 186 | - $this->updateQuota($ldapEntry[$attr][0]); |
|
| 187 | - } else { |
|
| 188 | - if ($this->connection->ldapQuotaDefault !== '') { |
|
| 189 | - $this->updateQuota(); |
|
| 190 | - } |
|
| 191 | - } |
|
| 192 | - unset($attr); |
|
| 193 | - |
|
| 194 | - //displayName |
|
| 195 | - $displayName = $displayName2 = ''; |
|
| 196 | - $attr = strtolower($this->connection->ldapUserDisplayName); |
|
| 197 | - if(isset($ldapEntry[$attr])) { |
|
| 198 | - $displayName = (string)$ldapEntry[$attr][0]; |
|
| 199 | - } |
|
| 200 | - $attr = strtolower($this->connection->ldapUserDisplayName2); |
|
| 201 | - if(isset($ldapEntry[$attr])) { |
|
| 202 | - $displayName2 = (string)$ldapEntry[$attr][0]; |
|
| 203 | - } |
|
| 204 | - if ($displayName !== '') { |
|
| 205 | - $this->composeAndStoreDisplayName($displayName); |
|
| 206 | - $this->access->cacheUserDisplayName( |
|
| 207 | - $this->getUsername(), |
|
| 208 | - $displayName, |
|
| 209 | - $displayName2 |
|
| 210 | - ); |
|
| 211 | - } |
|
| 212 | - unset($attr); |
|
| 213 | - |
|
| 214 | ||
| 215 | - //email must be stored after displayname, because it would cause a user |
|
| 216 | - //change event that will trigger fetching the display name again |
|
| 217 | - $attr = strtolower($this->connection->ldapEmailAttribute); |
|
| 218 | - if(isset($ldapEntry[$attr])) { |
|
| 219 | - $this->updateEmail($ldapEntry[$attr][0]); |
|
| 220 | - } |
|
| 221 | - unset($attr); |
|
| 222 | - |
|
| 223 | - // LDAP Username, needed for s2s sharing |
|
| 224 | - if(isset($ldapEntry['uid'])) { |
|
| 225 | - $this->storeLDAPUserName($ldapEntry['uid'][0]); |
|
| 226 | - } else if(isset($ldapEntry['samaccountname'])) { |
|
| 227 | - $this->storeLDAPUserName($ldapEntry['samaccountname'][0]); |
|
| 228 | - } |
|
| 229 | - |
|
| 230 | - //homePath |
|
| 231 | - if(strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) { |
|
| 232 | - $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:'))); |
|
| 233 | - if(isset($ldapEntry[$attr])) { |
|
| 234 | - $this->access->cacheUserHome( |
|
| 235 | - $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0])); |
|
| 236 | - } |
|
| 237 | - } |
|
| 238 | - |
|
| 239 | - //memberOf groups |
|
| 240 | - $cacheKey = 'getMemberOf'.$this->getUsername(); |
|
| 241 | - $groups = false; |
|
| 242 | - if(isset($ldapEntry['memberof'])) { |
|
| 243 | - $groups = $ldapEntry['memberof']; |
|
| 244 | - } |
|
| 245 | - $this->connection->writeToCache($cacheKey, $groups); |
|
| 246 | - |
|
| 247 | - //Avatar |
|
| 248 | - $attrs = array('jpegphoto', 'thumbnailphoto'); |
|
| 249 | - foreach ($attrs as $attr) { |
|
| 250 | - if(isset($ldapEntry[$attr])) { |
|
| 251 | - $this->avatarImage = $ldapEntry[$attr][0]; |
|
| 252 | - // the call to the method that saves the avatar in the file |
|
| 253 | - // system must be postponed after the login. It is to ensure |
|
| 254 | - // external mounts are mounted properly (e.g. with login |
|
| 255 | - // credentials from the session). |
|
| 256 | - \OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin'); |
|
| 257 | - break; |
|
| 258 | - } |
|
| 259 | - } |
|
| 260 | - } |
|
| 261 | - |
|
| 262 | - /** |
|
| 263 | - * @brief returns the LDAP DN of the user |
|
| 264 | - * @return string |
|
| 265 | - */ |
|
| 266 | - public function getDN() { |
|
| 267 | - return $this->dn; |
|
| 268 | - } |
|
| 269 | - |
|
| 270 | - /** |
|
| 271 | - * @brief returns the Nextcloud internal username of the user |
|
| 272 | - * @return string |
|
| 273 | - */ |
|
| 274 | - public function getUsername() { |
|
| 275 | - return $this->uid; |
|
| 276 | - } |
|
| 277 | - |
|
| 278 | - /** |
|
| 279 | - * returns the home directory of the user if specified by LDAP settings |
|
| 280 | - * @param string $valueFromLDAP |
|
| 281 | - * @return bool|string |
|
| 282 | - * @throws \Exception |
|
| 283 | - */ |
|
| 284 | - public function getHomePath($valueFromLDAP = null) { |
|
| 285 | - $path = (string)$valueFromLDAP; |
|
| 286 | - $attr = null; |
|
| 287 | - |
|
| 288 | - if (is_null($valueFromLDAP) |
|
| 289 | - && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0 |
|
| 290 | - && $this->access->connection->homeFolderNamingRule !== 'attr:') |
|
| 291 | - { |
|
| 292 | - $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); |
|
| 293 | - $homedir = $this->access->readAttribute( |
|
| 294 | - $this->access->username2dn($this->getUsername()), $attr); |
|
| 295 | - if ($homedir && isset($homedir[0])) { |
|
| 296 | - $path = $homedir[0]; |
|
| 297 | - } |
|
| 298 | - } |
|
| 299 | - |
|
| 300 | - if ($path !== '') { |
|
| 301 | - //if attribute's value is an absolute path take this, otherwise append it to data dir |
|
| 302 | - //check for / at the beginning or pattern c:\ resp. c:/ |
|
| 303 | - if( '/' !== $path[0] |
|
| 304 | - && !(3 < strlen($path) && ctype_alpha($path[0]) |
|
| 305 | - && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2])) |
|
| 306 | - ) { |
|
| 307 | - $path = $this->config->getSystemValue('datadirectory', |
|
| 308 | - \OC::$SERVERROOT.'/data' ) . '/' . $path; |
|
| 309 | - } |
|
| 310 | - //we need it to store it in the DB as well in case a user gets |
|
| 311 | - //deleted so we can clean up afterwards |
|
| 312 | - $this->config->setUserValue( |
|
| 313 | - $this->getUsername(), 'user_ldap', 'homePath', $path |
|
| 314 | - ); |
|
| 315 | - return $path; |
|
| 316 | - } |
|
| 317 | - |
|
| 318 | - if( !is_null($attr) |
|
| 319 | - && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true) |
|
| 320 | - ) { |
|
| 321 | - // a naming rule attribute is defined, but it doesn't exist for that LDAP user |
|
| 322 | - throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername()); |
|
| 323 | - } |
|
| 324 | - |
|
| 325 | - //false will apply default behaviour as defined and done by OC_User |
|
| 326 | - $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', ''); |
|
| 327 | - return false; |
|
| 328 | - } |
|
| 329 | - |
|
| 330 | - public function getMemberOfGroups() { |
|
| 331 | - $cacheKey = 'getMemberOf'.$this->getUsername(); |
|
| 332 | - $memberOfGroups = $this->connection->getFromCache($cacheKey); |
|
| 333 | - if(!is_null($memberOfGroups)) { |
|
| 334 | - return $memberOfGroups; |
|
| 335 | - } |
|
| 336 | - $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf'); |
|
| 337 | - $this->connection->writeToCache($cacheKey, $groupDNs); |
|
| 338 | - return $groupDNs; |
|
| 339 | - } |
|
| 340 | - |
|
| 341 | - /** |
|
| 342 | - * @brief reads the image from LDAP that shall be used as Avatar |
|
| 343 | - * @return string data (provided by LDAP) | false |
|
| 344 | - */ |
|
| 345 | - public function getAvatarImage() { |
|
| 346 | - if(!is_null($this->avatarImage)) { |
|
| 347 | - return $this->avatarImage; |
|
| 348 | - } |
|
| 349 | - |
|
| 350 | - $this->avatarImage = false; |
|
| 351 | - $attributes = array('jpegPhoto', 'thumbnailPhoto'); |
|
| 352 | - foreach($attributes as $attribute) { |
|
| 353 | - $result = $this->access->readAttribute($this->dn, $attribute); |
|
| 354 | - if($result !== false && is_array($result) && isset($result[0])) { |
|
| 355 | - $this->avatarImage = $result[0]; |
|
| 356 | - break; |
|
| 357 | - } |
|
| 358 | - } |
|
| 359 | - |
|
| 360 | - return $this->avatarImage; |
|
| 361 | - } |
|
| 362 | - |
|
| 363 | - /** |
|
| 364 | - * @brief marks the user as having logged in at least once |
|
| 365 | - * @return null |
|
| 366 | - */ |
|
| 367 | - public function markLogin() { |
|
| 368 | - $this->config->setUserValue( |
|
| 369 | - $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1); |
|
| 370 | - } |
|
| 371 | - |
|
| 372 | - /** |
|
| 373 | - * @brief marks the time when user features like email have been updated |
|
| 374 | - * @return null |
|
| 375 | - */ |
|
| 376 | - public function markRefreshTime() { |
|
| 377 | - $this->config->setUserValue( |
|
| 378 | - $this->uid, 'user_ldap', self::USER_PREFKEY_LASTREFRESH, time()); |
|
| 379 | - } |
|
| 380 | - |
|
| 381 | - /** |
|
| 382 | - * @brief checks whether user features needs to be updated again by |
|
| 383 | - * comparing the difference of time of the last refresh to now with the |
|
| 384 | - * desired interval |
|
| 385 | - * @return bool |
|
| 386 | - */ |
|
| 387 | - private function needsRefresh() { |
|
| 388 | - $lastChecked = $this->config->getUserValue($this->uid, 'user_ldap', |
|
| 389 | - self::USER_PREFKEY_LASTREFRESH, 0); |
|
| 390 | - |
|
| 391 | - if((time() - (int)$lastChecked) < (int)$this->config->getAppValue('user_ldap', 'updateAttributesInterval', 86400)) { |
|
| 392 | - return false; |
|
| 393 | - } |
|
| 394 | - return true; |
|
| 395 | - } |
|
| 396 | - |
|
| 397 | - /** |
|
| 398 | - * Stores a key-value pair in relation to this user |
|
| 399 | - * |
|
| 400 | - * @param string $key |
|
| 401 | - * @param string $value |
|
| 402 | - */ |
|
| 403 | - private function store($key, $value) { |
|
| 404 | - $this->config->setUserValue($this->uid, 'user_ldap', $key, $value); |
|
| 405 | - } |
|
| 406 | - |
|
| 407 | - /** |
|
| 408 | - * Composes the display name and stores it in the database. The final |
|
| 409 | - * display name is returned. |
|
| 410 | - * |
|
| 411 | - * @param string $displayName |
|
| 412 | - * @param string $displayName2 |
|
| 413 | - * @returns string the effective display name |
|
| 414 | - */ |
|
| 415 | - public function composeAndStoreDisplayName($displayName, $displayName2 = '') { |
|
| 416 | - $displayName2 = (string)$displayName2; |
|
| 417 | - if($displayName2 !== '') { |
|
| 418 | - $displayName .= ' (' . $displayName2 . ')'; |
|
| 419 | - } |
|
| 420 | - $this->store('displayName', $displayName); |
|
| 421 | - return $displayName; |
|
| 422 | - } |
|
| 423 | - |
|
| 424 | - /** |
|
| 425 | - * Stores the LDAP Username in the Database |
|
| 426 | - * @param string $userName |
|
| 427 | - */ |
|
| 428 | - public function storeLDAPUserName($userName) { |
|
| 429 | - $this->store('uid', $userName); |
|
| 430 | - } |
|
| 431 | - |
|
| 432 | - /** |
|
| 433 | - * @brief checks whether an update method specified by feature was run |
|
| 434 | - * already. If not, it will marked like this, because it is expected that |
|
| 435 | - * the method will be run, when false is returned. |
|
| 436 | - * @param string $feature email | quota | avatar (can be extended) |
|
| 437 | - * @return bool |
|
| 438 | - */ |
|
| 439 | - private function wasRefreshed($feature) { |
|
| 440 | - if(isset($this->refreshedFeatures[$feature])) { |
|
| 441 | - return true; |
|
| 442 | - } |
|
| 443 | - $this->refreshedFeatures[$feature] = 1; |
|
| 444 | - return false; |
|
| 445 | - } |
|
| 446 | - |
|
| 447 | - /** |
|
| 448 | - * fetches the email from LDAP and stores it as Nextcloud user value |
|
| 449 | - * @param string $valueFromLDAP if known, to save an LDAP read request |
|
| 450 | - * @return null |
|
| 451 | - */ |
|
| 452 | - public function updateEmail($valueFromLDAP = null) { |
|
| 453 | - if($this->wasRefreshed('email')) { |
|
| 454 | - return; |
|
| 455 | - } |
|
| 456 | - $email = (string)$valueFromLDAP; |
|
| 457 | - if(is_null($valueFromLDAP)) { |
|
| 458 | - $emailAttribute = $this->connection->ldapEmailAttribute; |
|
| 459 | - if ($emailAttribute !== '') { |
|
| 460 | - $aEmail = $this->access->readAttribute($this->dn, $emailAttribute); |
|
| 461 | - if(is_array($aEmail) && (count($aEmail) > 0)) { |
|
| 462 | - $email = (string)$aEmail[0]; |
|
| 463 | - } |
|
| 464 | - } |
|
| 465 | - } |
|
| 466 | - if ($email !== '') { |
|
| 467 | - $user = $this->userManager->get($this->uid); |
|
| 468 | - if (!is_null($user)) { |
|
| 469 | - $currentEmail = (string)$user->getEMailAddress(); |
|
| 470 | - if ($currentEmail !== $email) { |
|
| 471 | - $user->setEMailAddress($email); |
|
| 472 | - } |
|
| 473 | - } |
|
| 474 | - } |
|
| 475 | - } |
|
| 476 | - |
|
| 477 | - /** |
|
| 478 | - * Overall process goes as follow: |
|
| 479 | - * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function |
|
| 480 | - * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota |
|
| 481 | - * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default') |
|
| 482 | - * 4. check if the target user exists and set the quota for the user. |
|
| 483 | - * |
|
| 484 | - * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP |
|
| 485 | - * parameter can be passed with the value of the attribute. This value will be considered as the |
|
| 486 | - * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to |
|
| 487 | - * fetch all the user's attributes in one call and use the fetched values in this function. |
|
| 488 | - * The expected value for that parameter is a string describing the quota for the user. Valid |
|
| 489 | - * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in |
|
| 490 | - * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info) |
|
| 491 | - * |
|
| 492 | - * fetches the quota from LDAP and stores it as Nextcloud user value |
|
| 493 | - * @param string $valueFromLDAP the quota attribute's value can be passed, |
|
| 494 | - * to save the readAttribute request |
|
| 495 | - * @return null |
|
| 496 | - */ |
|
| 497 | - public function updateQuota($valueFromLDAP = null) { |
|
| 498 | - if($this->wasRefreshed('quota')) { |
|
| 499 | - return; |
|
| 500 | - } |
|
| 501 | - |
|
| 502 | - $quota = false; |
|
| 503 | - if(is_null($valueFromLDAP)) { |
|
| 504 | - $quotaAttribute = $this->connection->ldapQuotaAttribute; |
|
| 505 | - if ($quotaAttribute !== '') { |
|
| 506 | - $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); |
|
| 507 | - if($aQuota && (count($aQuota) > 0)) { |
|
| 508 | - if ($this->verifyQuotaValue($aQuota[0])) { |
|
| 509 | - $quota = $aQuota[0]; |
|
| 510 | - } else { |
|
| 511 | - $this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ILogger::WARN); |
|
| 512 | - } |
|
| 513 | - } |
|
| 514 | - } |
|
| 515 | - } else { |
|
| 516 | - if ($this->verifyQuotaValue($valueFromLDAP)) { |
|
| 517 | - $quota = $valueFromLDAP; |
|
| 518 | - } else { |
|
| 519 | - $this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ILogger::WARN); |
|
| 520 | - } |
|
| 521 | - } |
|
| 522 | - |
|
| 523 | - if ($quota === false) { |
|
| 524 | - // quota not found using the LDAP attribute (or not parseable). Try the default quota |
|
| 525 | - $defaultQuota = $this->connection->ldapQuotaDefault; |
|
| 526 | - if ($this->verifyQuotaValue($defaultQuota)) { |
|
| 527 | - $quota = $defaultQuota; |
|
| 528 | - } |
|
| 529 | - } |
|
| 530 | - |
|
| 531 | - $targetUser = $this->userManager->get($this->uid); |
|
| 532 | - if ($targetUser) { |
|
| 533 | - if($quota !== false) { |
|
| 534 | - $targetUser->setQuota($quota); |
|
| 535 | - } else { |
|
| 536 | - $this->log->log('not suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ILogger::WARN); |
|
| 537 | - } |
|
| 538 | - } else { |
|
| 539 | - $this->log->log('trying to set a quota for user ' . $this->uid . ' but the user is missing', ILogger::ERROR); |
|
| 540 | - } |
|
| 541 | - } |
|
| 542 | - |
|
| 543 | - private function verifyQuotaValue($quotaValue) { |
|
| 544 | - return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false; |
|
| 545 | - } |
|
| 546 | - |
|
| 547 | - /** |
|
| 548 | - * called by a post_login hook to save the avatar picture |
|
| 549 | - * |
|
| 550 | - * @param array $params |
|
| 551 | - */ |
|
| 552 | - public function updateAvatarPostLogin($params) { |
|
| 553 | - if(isset($params['uid']) && $params['uid'] === $this->getUsername()) { |
|
| 554 | - $this->updateAvatar(); |
|
| 555 | - } |
|
| 556 | - } |
|
| 557 | - |
|
| 558 | - /** |
|
| 559 | - * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar |
|
| 560 | - * @return null |
|
| 561 | - */ |
|
| 562 | - public function updateAvatar() { |
|
| 563 | - if($this->wasRefreshed('avatar')) { |
|
| 564 | - return; |
|
| 565 | - } |
|
| 566 | - $avatarImage = $this->getAvatarImage(); |
|
| 567 | - if($avatarImage === false) { |
|
| 568 | - //not set, nothing left to do; |
|
| 569 | - return; |
|
| 570 | - } |
|
| 571 | - $this->image->loadFromBase64(base64_encode($avatarImage)); |
|
| 572 | - $this->setOwnCloudAvatar(); |
|
| 573 | - } |
|
| 574 | - |
|
| 575 | - /** |
|
| 576 | - * @brief sets an image as Nextcloud avatar |
|
| 577 | - * @return null |
|
| 578 | - */ |
|
| 579 | - private function setOwnCloudAvatar() { |
|
| 580 | - if(!$this->image->valid()) { |
|
| 581 | - $this->log->log('jpegPhoto data invalid for '.$this->dn, ILogger::ERROR); |
|
| 582 | - return; |
|
| 583 | - } |
|
| 584 | - //make sure it is a square and not bigger than 128x128 |
|
| 585 | - $size = min(array($this->image->width(), $this->image->height(), 128)); |
|
| 586 | - if(!$this->image->centerCrop($size)) { |
|
| 587 | - $this->log->log('croping image for avatar failed for '.$this->dn, ILogger::ERROR); |
|
| 588 | - return; |
|
| 589 | - } |
|
| 590 | - |
|
| 591 | - if(!$this->fs->isLoaded()) { |
|
| 592 | - $this->fs->setup($this->uid); |
|
| 593 | - } |
|
| 594 | - |
|
| 595 | - try { |
|
| 596 | - $avatar = $this->avatarManager->getAvatar($this->uid); |
|
| 597 | - $avatar->set($this->image); |
|
| 598 | - } catch (\Exception $e) { |
|
| 599 | - \OC::$server->getLogger()->logException($e, [ |
|
| 600 | - 'message' => 'Could not set avatar for ' . $this->dn, |
|
| 601 | - 'level' => ILogger::INFO, |
|
| 602 | - 'app' => 'user_ldap', |
|
| 603 | - ]); |
|
| 604 | - } |
|
| 605 | - } |
|
| 606 | - |
|
| 607 | - /** |
|
| 608 | - * called by a post_login hook to handle password expiry |
|
| 609 | - * |
|
| 610 | - * @param array $params |
|
| 611 | - */ |
|
| 612 | - public function handlePasswordExpiry($params) { |
|
| 613 | - $ppolicyDN = $this->connection->ldapDefaultPPolicyDN; |
|
| 614 | - if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) { |
|
| 615 | - return;//password expiry handling disabled |
|
| 616 | - } |
|
| 617 | - $uid = $params['uid']; |
|
| 618 | - if(isset($uid) && $uid === $this->getUsername()) { |
|
| 619 | - //retrieve relevant user attributes |
|
| 620 | - $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); |
|
| 128 | + if ($username === null) { |
|
| 129 | + $log->log("uid for '$dn' must not be null!", ILogger::ERROR); |
|
| 130 | + throw new \InvalidArgumentException('uid must not be null!'); |
|
| 131 | + } else if ($username === '') { |
|
| 132 | + $log->log("uid for '$dn' must not be an empty string", ILogger::ERROR); |
|
| 133 | + throw new \InvalidArgumentException('uid must not be an empty string!'); |
|
| 134 | + } |
|
| 135 | + |
|
| 136 | + $this->access = $access; |
|
| 137 | + $this->connection = $access->getConnection(); |
|
| 138 | + $this->config = $config; |
|
| 139 | + $this->fs = $fs; |
|
| 140 | + $this->dn = $dn; |
|
| 141 | + $this->uid = $username; |
|
| 142 | + $this->image = $image; |
|
| 143 | + $this->log = $log; |
|
| 144 | + $this->avatarManager = $avatarManager; |
|
| 145 | + $this->userManager = $userManager; |
|
| 146 | + $this->notificationManager = $notificationManager; |
|
| 147 | + |
|
| 148 | + \OCP\Util::connectHook('OC_User', 'post_login', $this, 'handlePasswordExpiry'); |
|
| 149 | + } |
|
| 150 | + |
|
| 151 | + /** |
|
| 152 | + * @brief updates properties like email, quota or avatar provided by LDAP |
|
| 153 | + * @return null |
|
| 154 | + */ |
|
| 155 | + public function update() { |
|
| 156 | + if(is_null($this->dn)) { |
|
| 157 | + return null; |
|
| 158 | + } |
|
| 159 | + |
|
| 160 | + $hasLoggedIn = $this->config->getUserValue($this->uid, 'user_ldap', |
|
| 161 | + self::USER_PREFKEY_FIRSTLOGIN, 0); |
|
| 162 | + |
|
| 163 | + if($this->needsRefresh()) { |
|
| 164 | + $this->updateEmail(); |
|
| 165 | + $this->updateQuota(); |
|
| 166 | + if($hasLoggedIn !== 0) { |
|
| 167 | + //we do not need to try it, when the user has not been logged in |
|
| 168 | + //before, because the file system will not be ready. |
|
| 169 | + $this->updateAvatar(); |
|
| 170 | + //in order to get an avatar as soon as possible, mark the user |
|
| 171 | + //as refreshed only when updating the avatar did happen |
|
| 172 | + $this->markRefreshTime(); |
|
| 173 | + } |
|
| 174 | + } |
|
| 175 | + } |
|
| 176 | + |
|
| 177 | + /** |
|
| 178 | + * processes results from LDAP for attributes as returned by getAttributesToRead() |
|
| 179 | + * @param array $ldapEntry the user entry as retrieved from LDAP |
|
| 180 | + */ |
|
| 181 | + public function processAttributes($ldapEntry) { |
|
| 182 | + $this->markRefreshTime(); |
|
| 183 | + //Quota |
|
| 184 | + $attr = strtolower($this->connection->ldapQuotaAttribute); |
|
| 185 | + if(isset($ldapEntry[$attr])) { |
|
| 186 | + $this->updateQuota($ldapEntry[$attr][0]); |
|
| 187 | + } else { |
|
| 188 | + if ($this->connection->ldapQuotaDefault !== '') { |
|
| 189 | + $this->updateQuota(); |
|
| 190 | + } |
|
| 191 | + } |
|
| 192 | + unset($attr); |
|
| 193 | + |
|
| 194 | + //displayName |
|
| 195 | + $displayName = $displayName2 = ''; |
|
| 196 | + $attr = strtolower($this->connection->ldapUserDisplayName); |
|
| 197 | + if(isset($ldapEntry[$attr])) { |
|
| 198 | + $displayName = (string)$ldapEntry[$attr][0]; |
|
| 199 | + } |
|
| 200 | + $attr = strtolower($this->connection->ldapUserDisplayName2); |
|
| 201 | + if(isset($ldapEntry[$attr])) { |
|
| 202 | + $displayName2 = (string)$ldapEntry[$attr][0]; |
|
| 203 | + } |
|
| 204 | + if ($displayName !== '') { |
|
| 205 | + $this->composeAndStoreDisplayName($displayName); |
|
| 206 | + $this->access->cacheUserDisplayName( |
|
| 207 | + $this->getUsername(), |
|
| 208 | + $displayName, |
|
| 209 | + $displayName2 |
|
| 210 | + ); |
|
| 211 | + } |
|
| 212 | + unset($attr); |
|
| 213 | + |
|
| 214 | ||
| 215 | + //email must be stored after displayname, because it would cause a user |
|
| 216 | + //change event that will trigger fetching the display name again |
|
| 217 | + $attr = strtolower($this->connection->ldapEmailAttribute); |
|
| 218 | + if(isset($ldapEntry[$attr])) { |
|
| 219 | + $this->updateEmail($ldapEntry[$attr][0]); |
|
| 220 | + } |
|
| 221 | + unset($attr); |
|
| 222 | + |
|
| 223 | + // LDAP Username, needed for s2s sharing |
|
| 224 | + if(isset($ldapEntry['uid'])) { |
|
| 225 | + $this->storeLDAPUserName($ldapEntry['uid'][0]); |
|
| 226 | + } else if(isset($ldapEntry['samaccountname'])) { |
|
| 227 | + $this->storeLDAPUserName($ldapEntry['samaccountname'][0]); |
|
| 228 | + } |
|
| 229 | + |
|
| 230 | + //homePath |
|
| 231 | + if(strpos($this->connection->homeFolderNamingRule, 'attr:') === 0) { |
|
| 232 | + $attr = strtolower(substr($this->connection->homeFolderNamingRule, strlen('attr:'))); |
|
| 233 | + if(isset($ldapEntry[$attr])) { |
|
| 234 | + $this->access->cacheUserHome( |
|
| 235 | + $this->getUsername(), $this->getHomePath($ldapEntry[$attr][0])); |
|
| 236 | + } |
|
| 237 | + } |
|
| 238 | + |
|
| 239 | + //memberOf groups |
|
| 240 | + $cacheKey = 'getMemberOf'.$this->getUsername(); |
|
| 241 | + $groups = false; |
|
| 242 | + if(isset($ldapEntry['memberof'])) { |
|
| 243 | + $groups = $ldapEntry['memberof']; |
|
| 244 | + } |
|
| 245 | + $this->connection->writeToCache($cacheKey, $groups); |
|
| 246 | + |
|
| 247 | + //Avatar |
|
| 248 | + $attrs = array('jpegphoto', 'thumbnailphoto'); |
|
| 249 | + foreach ($attrs as $attr) { |
|
| 250 | + if(isset($ldapEntry[$attr])) { |
|
| 251 | + $this->avatarImage = $ldapEntry[$attr][0]; |
|
| 252 | + // the call to the method that saves the avatar in the file |
|
| 253 | + // system must be postponed after the login. It is to ensure |
|
| 254 | + // external mounts are mounted properly (e.g. with login |
|
| 255 | + // credentials from the session). |
|
| 256 | + \OCP\Util::connectHook('OC_User', 'post_login', $this, 'updateAvatarPostLogin'); |
|
| 257 | + break; |
|
| 258 | + } |
|
| 259 | + } |
|
| 260 | + } |
|
| 261 | + |
|
| 262 | + /** |
|
| 263 | + * @brief returns the LDAP DN of the user |
|
| 264 | + * @return string |
|
| 265 | + */ |
|
| 266 | + public function getDN() { |
|
| 267 | + return $this->dn; |
|
| 268 | + } |
|
| 269 | + |
|
| 270 | + /** |
|
| 271 | + * @brief returns the Nextcloud internal username of the user |
|
| 272 | + * @return string |
|
| 273 | + */ |
|
| 274 | + public function getUsername() { |
|
| 275 | + return $this->uid; |
|
| 276 | + } |
|
| 277 | + |
|
| 278 | + /** |
|
| 279 | + * returns the home directory of the user if specified by LDAP settings |
|
| 280 | + * @param string $valueFromLDAP |
|
| 281 | + * @return bool|string |
|
| 282 | + * @throws \Exception |
|
| 283 | + */ |
|
| 284 | + public function getHomePath($valueFromLDAP = null) { |
|
| 285 | + $path = (string)$valueFromLDAP; |
|
| 286 | + $attr = null; |
|
| 287 | + |
|
| 288 | + if (is_null($valueFromLDAP) |
|
| 289 | + && strpos($this->access->connection->homeFolderNamingRule, 'attr:') === 0 |
|
| 290 | + && $this->access->connection->homeFolderNamingRule !== 'attr:') |
|
| 291 | + { |
|
| 292 | + $attr = substr($this->access->connection->homeFolderNamingRule, strlen('attr:')); |
|
| 293 | + $homedir = $this->access->readAttribute( |
|
| 294 | + $this->access->username2dn($this->getUsername()), $attr); |
|
| 295 | + if ($homedir && isset($homedir[0])) { |
|
| 296 | + $path = $homedir[0]; |
|
| 297 | + } |
|
| 298 | + } |
|
| 299 | + |
|
| 300 | + if ($path !== '') { |
|
| 301 | + //if attribute's value is an absolute path take this, otherwise append it to data dir |
|
| 302 | + //check for / at the beginning or pattern c:\ resp. c:/ |
|
| 303 | + if( '/' !== $path[0] |
|
| 304 | + && !(3 < strlen($path) && ctype_alpha($path[0]) |
|
| 305 | + && $path[1] === ':' && ('\\' === $path[2] || '/' === $path[2])) |
|
| 306 | + ) { |
|
| 307 | + $path = $this->config->getSystemValue('datadirectory', |
|
| 308 | + \OC::$SERVERROOT.'/data' ) . '/' . $path; |
|
| 309 | + } |
|
| 310 | + //we need it to store it in the DB as well in case a user gets |
|
| 311 | + //deleted so we can clean up afterwards |
|
| 312 | + $this->config->setUserValue( |
|
| 313 | + $this->getUsername(), 'user_ldap', 'homePath', $path |
|
| 314 | + ); |
|
| 315 | + return $path; |
|
| 316 | + } |
|
| 317 | + |
|
| 318 | + if( !is_null($attr) |
|
| 319 | + && $this->config->getAppValue('user_ldap', 'enforce_home_folder_naming_rule', true) |
|
| 320 | + ) { |
|
| 321 | + // a naming rule attribute is defined, but it doesn't exist for that LDAP user |
|
| 322 | + throw new \Exception('Home dir attribute can\'t be read from LDAP for uid: ' . $this->getUsername()); |
|
| 323 | + } |
|
| 324 | + |
|
| 325 | + //false will apply default behaviour as defined and done by OC_User |
|
| 326 | + $this->config->setUserValue($this->getUsername(), 'user_ldap', 'homePath', ''); |
|
| 327 | + return false; |
|
| 328 | + } |
|
| 329 | + |
|
| 330 | + public function getMemberOfGroups() { |
|
| 331 | + $cacheKey = 'getMemberOf'.$this->getUsername(); |
|
| 332 | + $memberOfGroups = $this->connection->getFromCache($cacheKey); |
|
| 333 | + if(!is_null($memberOfGroups)) { |
|
| 334 | + return $memberOfGroups; |
|
| 335 | + } |
|
| 336 | + $groupDNs = $this->access->readAttribute($this->getDN(), 'memberOf'); |
|
| 337 | + $this->connection->writeToCache($cacheKey, $groupDNs); |
|
| 338 | + return $groupDNs; |
|
| 339 | + } |
|
| 340 | + |
|
| 341 | + /** |
|
| 342 | + * @brief reads the image from LDAP that shall be used as Avatar |
|
| 343 | + * @return string data (provided by LDAP) | false |
|
| 344 | + */ |
|
| 345 | + public function getAvatarImage() { |
|
| 346 | + if(!is_null($this->avatarImage)) { |
|
| 347 | + return $this->avatarImage; |
|
| 348 | + } |
|
| 349 | + |
|
| 350 | + $this->avatarImage = false; |
|
| 351 | + $attributes = array('jpegPhoto', 'thumbnailPhoto'); |
|
| 352 | + foreach($attributes as $attribute) { |
|
| 353 | + $result = $this->access->readAttribute($this->dn, $attribute); |
|
| 354 | + if($result !== false && is_array($result) && isset($result[0])) { |
|
| 355 | + $this->avatarImage = $result[0]; |
|
| 356 | + break; |
|
| 357 | + } |
|
| 358 | + } |
|
| 359 | + |
|
| 360 | + return $this->avatarImage; |
|
| 361 | + } |
|
| 362 | + |
|
| 363 | + /** |
|
| 364 | + * @brief marks the user as having logged in at least once |
|
| 365 | + * @return null |
|
| 366 | + */ |
|
| 367 | + public function markLogin() { |
|
| 368 | + $this->config->setUserValue( |
|
| 369 | + $this->uid, 'user_ldap', self::USER_PREFKEY_FIRSTLOGIN, 1); |
|
| 370 | + } |
|
| 371 | + |
|
| 372 | + /** |
|
| 373 | + * @brief marks the time when user features like email have been updated |
|
| 374 | + * @return null |
|
| 375 | + */ |
|
| 376 | + public function markRefreshTime() { |
|
| 377 | + $this->config->setUserValue( |
|
| 378 | + $this->uid, 'user_ldap', self::USER_PREFKEY_LASTREFRESH, time()); |
|
| 379 | + } |
|
| 380 | + |
|
| 381 | + /** |
|
| 382 | + * @brief checks whether user features needs to be updated again by |
|
| 383 | + * comparing the difference of time of the last refresh to now with the |
|
| 384 | + * desired interval |
|
| 385 | + * @return bool |
|
| 386 | + */ |
|
| 387 | + private function needsRefresh() { |
|
| 388 | + $lastChecked = $this->config->getUserValue($this->uid, 'user_ldap', |
|
| 389 | + self::USER_PREFKEY_LASTREFRESH, 0); |
|
| 390 | + |
|
| 391 | + if((time() - (int)$lastChecked) < (int)$this->config->getAppValue('user_ldap', 'updateAttributesInterval', 86400)) { |
|
| 392 | + return false; |
|
| 393 | + } |
|
| 394 | + return true; |
|
| 395 | + } |
|
| 396 | + |
|
| 397 | + /** |
|
| 398 | + * Stores a key-value pair in relation to this user |
|
| 399 | + * |
|
| 400 | + * @param string $key |
|
| 401 | + * @param string $value |
|
| 402 | + */ |
|
| 403 | + private function store($key, $value) { |
|
| 404 | + $this->config->setUserValue($this->uid, 'user_ldap', $key, $value); |
|
| 405 | + } |
|
| 406 | + |
|
| 407 | + /** |
|
| 408 | + * Composes the display name and stores it in the database. The final |
|
| 409 | + * display name is returned. |
|
| 410 | + * |
|
| 411 | + * @param string $displayName |
|
| 412 | + * @param string $displayName2 |
|
| 413 | + * @returns string the effective display name |
|
| 414 | + */ |
|
| 415 | + public function composeAndStoreDisplayName($displayName, $displayName2 = '') { |
|
| 416 | + $displayName2 = (string)$displayName2; |
|
| 417 | + if($displayName2 !== '') { |
|
| 418 | + $displayName .= ' (' . $displayName2 . ')'; |
|
| 419 | + } |
|
| 420 | + $this->store('displayName', $displayName); |
|
| 421 | + return $displayName; |
|
| 422 | + } |
|
| 423 | + |
|
| 424 | + /** |
|
| 425 | + * Stores the LDAP Username in the Database |
|
| 426 | + * @param string $userName |
|
| 427 | + */ |
|
| 428 | + public function storeLDAPUserName($userName) { |
|
| 429 | + $this->store('uid', $userName); |
|
| 430 | + } |
|
| 431 | + |
|
| 432 | + /** |
|
| 433 | + * @brief checks whether an update method specified by feature was run |
|
| 434 | + * already. If not, it will marked like this, because it is expected that |
|
| 435 | + * the method will be run, when false is returned. |
|
| 436 | + * @param string $feature email | quota | avatar (can be extended) |
|
| 437 | + * @return bool |
|
| 438 | + */ |
|
| 439 | + private function wasRefreshed($feature) { |
|
| 440 | + if(isset($this->refreshedFeatures[$feature])) { |
|
| 441 | + return true; |
|
| 442 | + } |
|
| 443 | + $this->refreshedFeatures[$feature] = 1; |
|
| 444 | + return false; |
|
| 445 | + } |
|
| 446 | + |
|
| 447 | + /** |
|
| 448 | + * fetches the email from LDAP and stores it as Nextcloud user value |
|
| 449 | + * @param string $valueFromLDAP if known, to save an LDAP read request |
|
| 450 | + * @return null |
|
| 451 | + */ |
|
| 452 | + public function updateEmail($valueFromLDAP = null) { |
|
| 453 | + if($this->wasRefreshed('email')) { |
|
| 454 | + return; |
|
| 455 | + } |
|
| 456 | + $email = (string)$valueFromLDAP; |
|
| 457 | + if(is_null($valueFromLDAP)) { |
|
| 458 | + $emailAttribute = $this->connection->ldapEmailAttribute; |
|
| 459 | + if ($emailAttribute !== '') { |
|
| 460 | + $aEmail = $this->access->readAttribute($this->dn, $emailAttribute); |
|
| 461 | + if(is_array($aEmail) && (count($aEmail) > 0)) { |
|
| 462 | + $email = (string)$aEmail[0]; |
|
| 463 | + } |
|
| 464 | + } |
|
| 465 | + } |
|
| 466 | + if ($email !== '') { |
|
| 467 | + $user = $this->userManager->get($this->uid); |
|
| 468 | + if (!is_null($user)) { |
|
| 469 | + $currentEmail = (string)$user->getEMailAddress(); |
|
| 470 | + if ($currentEmail !== $email) { |
|
| 471 | + $user->setEMailAddress($email); |
|
| 472 | + } |
|
| 473 | + } |
|
| 474 | + } |
|
| 475 | + } |
|
| 476 | + |
|
| 477 | + /** |
|
| 478 | + * Overall process goes as follow: |
|
| 479 | + * 1. fetch the quota from LDAP and check if it's parseable with the "verifyQuotaValue" function |
|
| 480 | + * 2. if the value can't be fetched, is empty or not parseable, use the default LDAP quota |
|
| 481 | + * 3. if the default LDAP quota can't be parsed, use the Nextcloud's default quota (use 'default') |
|
| 482 | + * 4. check if the target user exists and set the quota for the user. |
|
| 483 | + * |
|
| 484 | + * In order to improve performance and prevent an unwanted extra LDAP call, the $valueFromLDAP |
|
| 485 | + * parameter can be passed with the value of the attribute. This value will be considered as the |
|
| 486 | + * quota for the user coming from the LDAP server (step 1 of the process) It can be useful to |
|
| 487 | + * fetch all the user's attributes in one call and use the fetched values in this function. |
|
| 488 | + * The expected value for that parameter is a string describing the quota for the user. Valid |
|
| 489 | + * values are 'none' (unlimited), 'default' (the Nextcloud's default quota), '1234' (quota in |
|
| 490 | + * bytes), '1234 MB' (quota in MB - check the \OC_Helper::computerFileSize method for more info) |
|
| 491 | + * |
|
| 492 | + * fetches the quota from LDAP and stores it as Nextcloud user value |
|
| 493 | + * @param string $valueFromLDAP the quota attribute's value can be passed, |
|
| 494 | + * to save the readAttribute request |
|
| 495 | + * @return null |
|
| 496 | + */ |
|
| 497 | + public function updateQuota($valueFromLDAP = null) { |
|
| 498 | + if($this->wasRefreshed('quota')) { |
|
| 499 | + return; |
|
| 500 | + } |
|
| 501 | + |
|
| 502 | + $quota = false; |
|
| 503 | + if(is_null($valueFromLDAP)) { |
|
| 504 | + $quotaAttribute = $this->connection->ldapQuotaAttribute; |
|
| 505 | + if ($quotaAttribute !== '') { |
|
| 506 | + $aQuota = $this->access->readAttribute($this->dn, $quotaAttribute); |
|
| 507 | + if($aQuota && (count($aQuota) > 0)) { |
|
| 508 | + if ($this->verifyQuotaValue($aQuota[0])) { |
|
| 509 | + $quota = $aQuota[0]; |
|
| 510 | + } else { |
|
| 511 | + $this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $aQuota[0] . ']', ILogger::WARN); |
|
| 512 | + } |
|
| 513 | + } |
|
| 514 | + } |
|
| 515 | + } else { |
|
| 516 | + if ($this->verifyQuotaValue($valueFromLDAP)) { |
|
| 517 | + $quota = $valueFromLDAP; |
|
| 518 | + } else { |
|
| 519 | + $this->log->log('not suitable LDAP quota found for user ' . $this->uid . ': [' . $valueFromLDAP . ']', ILogger::WARN); |
|
| 520 | + } |
|
| 521 | + } |
|
| 522 | + |
|
| 523 | + if ($quota === false) { |
|
| 524 | + // quota not found using the LDAP attribute (or not parseable). Try the default quota |
|
| 525 | + $defaultQuota = $this->connection->ldapQuotaDefault; |
|
| 526 | + if ($this->verifyQuotaValue($defaultQuota)) { |
|
| 527 | + $quota = $defaultQuota; |
|
| 528 | + } |
|
| 529 | + } |
|
| 530 | + |
|
| 531 | + $targetUser = $this->userManager->get($this->uid); |
|
| 532 | + if ($targetUser) { |
|
| 533 | + if($quota !== false) { |
|
| 534 | + $targetUser->setQuota($quota); |
|
| 535 | + } else { |
|
| 536 | + $this->log->log('not suitable default quota found for user ' . $this->uid . ': [' . $defaultQuota . ']', ILogger::WARN); |
|
| 537 | + } |
|
| 538 | + } else { |
|
| 539 | + $this->log->log('trying to set a quota for user ' . $this->uid . ' but the user is missing', ILogger::ERROR); |
|
| 540 | + } |
|
| 541 | + } |
|
| 542 | + |
|
| 543 | + private function verifyQuotaValue($quotaValue) { |
|
| 544 | + return $quotaValue === 'none' || $quotaValue === 'default' || \OC_Helper::computerFileSize($quotaValue) !== false; |
|
| 545 | + } |
|
| 546 | + |
|
| 547 | + /** |
|
| 548 | + * called by a post_login hook to save the avatar picture |
|
| 549 | + * |
|
| 550 | + * @param array $params |
|
| 551 | + */ |
|
| 552 | + public function updateAvatarPostLogin($params) { |
|
| 553 | + if(isset($params['uid']) && $params['uid'] === $this->getUsername()) { |
|
| 554 | + $this->updateAvatar(); |
|
| 555 | + } |
|
| 556 | + } |
|
| 557 | + |
|
| 558 | + /** |
|
| 559 | + * @brief attempts to get an image from LDAP and sets it as Nextcloud avatar |
|
| 560 | + * @return null |
|
| 561 | + */ |
|
| 562 | + public function updateAvatar() { |
|
| 563 | + if($this->wasRefreshed('avatar')) { |
|
| 564 | + return; |
|
| 565 | + } |
|
| 566 | + $avatarImage = $this->getAvatarImage(); |
|
| 567 | + if($avatarImage === false) { |
|
| 568 | + //not set, nothing left to do; |
|
| 569 | + return; |
|
| 570 | + } |
|
| 571 | + $this->image->loadFromBase64(base64_encode($avatarImage)); |
|
| 572 | + $this->setOwnCloudAvatar(); |
|
| 573 | + } |
|
| 574 | + |
|
| 575 | + /** |
|
| 576 | + * @brief sets an image as Nextcloud avatar |
|
| 577 | + * @return null |
|
| 578 | + */ |
|
| 579 | + private function setOwnCloudAvatar() { |
|
| 580 | + if(!$this->image->valid()) { |
|
| 581 | + $this->log->log('jpegPhoto data invalid for '.$this->dn, ILogger::ERROR); |
|
| 582 | + return; |
|
| 583 | + } |
|
| 584 | + //make sure it is a square and not bigger than 128x128 |
|
| 585 | + $size = min(array($this->image->width(), $this->image->height(), 128)); |
|
| 586 | + if(!$this->image->centerCrop($size)) { |
|
| 587 | + $this->log->log('croping image for avatar failed for '.$this->dn, ILogger::ERROR); |
|
| 588 | + return; |
|
| 589 | + } |
|
| 590 | + |
|
| 591 | + if(!$this->fs->isLoaded()) { |
|
| 592 | + $this->fs->setup($this->uid); |
|
| 593 | + } |
|
| 594 | + |
|
| 595 | + try { |
|
| 596 | + $avatar = $this->avatarManager->getAvatar($this->uid); |
|
| 597 | + $avatar->set($this->image); |
|
| 598 | + } catch (\Exception $e) { |
|
| 599 | + \OC::$server->getLogger()->logException($e, [ |
|
| 600 | + 'message' => 'Could not set avatar for ' . $this->dn, |
|
| 601 | + 'level' => ILogger::INFO, |
|
| 602 | + 'app' => 'user_ldap', |
|
| 603 | + ]); |
|
| 604 | + } |
|
| 605 | + } |
|
| 606 | + |
|
| 607 | + /** |
|
| 608 | + * called by a post_login hook to handle password expiry |
|
| 609 | + * |
|
| 610 | + * @param array $params |
|
| 611 | + */ |
|
| 612 | + public function handlePasswordExpiry($params) { |
|
| 613 | + $ppolicyDN = $this->connection->ldapDefaultPPolicyDN; |
|
| 614 | + if (empty($ppolicyDN) || ((int)$this->connection->turnOnPasswordChange !== 1)) { |
|
| 615 | + return;//password expiry handling disabled |
|
| 616 | + } |
|
| 617 | + $uid = $params['uid']; |
|
| 618 | + if(isset($uid) && $uid === $this->getUsername()) { |
|
| 619 | + //retrieve relevant user attributes |
|
| 620 | + $result = $this->access->search('objectclass=*', $this->dn, ['pwdpolicysubentry', 'pwdgraceusetime', 'pwdreset', 'pwdchangedtime']); |
|
| 621 | 621 | |
| 622 | - if(array_key_exists('pwdpolicysubentry', $result[0])) { |
|
| 623 | - $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; |
|
| 624 | - if($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)){ |
|
| 625 | - $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN |
|
| 626 | - } |
|
| 627 | - } |
|
| 622 | + if(array_key_exists('pwdpolicysubentry', $result[0])) { |
|
| 623 | + $pwdPolicySubentry = $result[0]['pwdpolicysubentry']; |
|
| 624 | + if($pwdPolicySubentry && (count($pwdPolicySubentry) > 0)){ |
|
| 625 | + $ppolicyDN = $pwdPolicySubentry[0];//custom ppolicy DN |
|
| 626 | + } |
|
| 627 | + } |
|
| 628 | 628 | |
| 629 | - $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : null; |
|
| 630 | - $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : null; |
|
| 631 | - $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : null; |
|
| 629 | + $pwdGraceUseTime = array_key_exists('pwdgraceusetime', $result[0]) ? $result[0]['pwdgraceusetime'] : null; |
|
| 630 | + $pwdReset = array_key_exists('pwdreset', $result[0]) ? $result[0]['pwdreset'] : null; |
|
| 631 | + $pwdChangedTime = array_key_exists('pwdchangedtime', $result[0]) ? $result[0]['pwdchangedtime'] : null; |
|
| 632 | 632 | |
| 633 | - //retrieve relevant password policy attributes |
|
| 634 | - $cacheKey = 'ppolicyAttributes' . $ppolicyDN; |
|
| 635 | - $result = $this->connection->getFromCache($cacheKey); |
|
| 636 | - if(is_null($result)) { |
|
| 637 | - $result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']); |
|
| 638 | - $this->connection->writeToCache($cacheKey, $result); |
|
| 639 | - } |
|
| 633 | + //retrieve relevant password policy attributes |
|
| 634 | + $cacheKey = 'ppolicyAttributes' . $ppolicyDN; |
|
| 635 | + $result = $this->connection->getFromCache($cacheKey); |
|
| 636 | + if(is_null($result)) { |
|
| 637 | + $result = $this->access->search('objectclass=*', $ppolicyDN, ['pwdgraceauthnlimit', 'pwdmaxage', 'pwdexpirewarning']); |
|
| 638 | + $this->connection->writeToCache($cacheKey, $result); |
|
| 639 | + } |
|
| 640 | 640 | |
| 641 | - $pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : null; |
|
| 642 | - $pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : null; |
|
| 643 | - $pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : null; |
|
| 641 | + $pwdGraceAuthNLimit = array_key_exists('pwdgraceauthnlimit', $result[0]) ? $result[0]['pwdgraceauthnlimit'] : null; |
|
| 642 | + $pwdMaxAge = array_key_exists('pwdmaxage', $result[0]) ? $result[0]['pwdmaxage'] : null; |
|
| 643 | + $pwdExpireWarning = array_key_exists('pwdexpirewarning', $result[0]) ? $result[0]['pwdexpirewarning'] : null; |
|
| 644 | 644 | |
| 645 | - //handle grace login |
|
| 646 | - $pwdGraceUseTimeCount = count($pwdGraceUseTime); |
|
| 647 | - if($pwdGraceUseTime && $pwdGraceUseTimeCount > 0) { //was this a grace login? |
|
| 648 | - if($pwdGraceAuthNLimit |
|
| 649 | - && (count($pwdGraceAuthNLimit) > 0) |
|
| 650 | - &&($pwdGraceUseTimeCount < (int)$pwdGraceAuthNLimit[0])) { //at least one more grace login available? |
|
| 651 | - $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
| 652 | - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( |
|
| 653 | - 'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid))); |
|
| 654 | - } else { //no more grace login available |
|
| 655 | - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( |
|
| 656 | - 'user_ldap.renewPassword.showLoginFormInvalidPassword', array('user' => $uid))); |
|
| 657 | - } |
|
| 658 | - exit(); |
|
| 659 | - } |
|
| 660 | - //handle pwdReset attribute |
|
| 661 | - if($pwdReset && (count($pwdReset) > 0) && $pwdReset[0] === 'TRUE') { //user must change his password |
|
| 662 | - $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
| 663 | - header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( |
|
| 664 | - 'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid))); |
|
| 665 | - exit(); |
|
| 666 | - } |
|
| 667 | - //handle password expiry warning |
|
| 668 | - if($pwdChangedTime && (count($pwdChangedTime) > 0)) { |
|
| 669 | - if($pwdMaxAge && (count($pwdMaxAge) > 0) |
|
| 670 | - && $pwdExpireWarning && (count($pwdExpireWarning) > 0)) { |
|
| 671 | - $pwdMaxAgeInt = (int)$pwdMaxAge[0]; |
|
| 672 | - $pwdExpireWarningInt = (int)$pwdExpireWarning[0]; |
|
| 673 | - if($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0){ |
|
| 674 | - $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]); |
|
| 675 | - $pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S')); |
|
| 676 | - $currentDateTime = new \DateTime(); |
|
| 677 | - $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp(); |
|
| 678 | - if($secondsToExpiry <= $pwdExpireWarningInt) { |
|
| 679 | - //remove last password expiry warning if any |
|
| 680 | - $notification = $this->notificationManager->createNotification(); |
|
| 681 | - $notification->setApp('user_ldap') |
|
| 682 | - ->setUser($uid) |
|
| 683 | - ->setObject('pwd_exp_warn', $uid) |
|
| 684 | - ; |
|
| 685 | - $this->notificationManager->markProcessed($notification); |
|
| 686 | - //create new password expiry warning |
|
| 687 | - $notification = $this->notificationManager->createNotification(); |
|
| 688 | - $notification->setApp('user_ldap') |
|
| 689 | - ->setUser($uid) |
|
| 690 | - ->setDateTime($currentDateTime) |
|
| 691 | - ->setObject('pwd_exp_warn', $uid) |
|
| 692 | - ->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)]) |
|
| 693 | - ; |
|
| 694 | - $this->notificationManager->notify($notification); |
|
| 695 | - } |
|
| 696 | - } |
|
| 697 | - } |
|
| 698 | - } |
|
| 699 | - } |
|
| 700 | - } |
|
| 645 | + //handle grace login |
|
| 646 | + $pwdGraceUseTimeCount = count($pwdGraceUseTime); |
|
| 647 | + if($pwdGraceUseTime && $pwdGraceUseTimeCount > 0) { //was this a grace login? |
|
| 648 | + if($pwdGraceAuthNLimit |
|
| 649 | + && (count($pwdGraceAuthNLimit) > 0) |
|
| 650 | + &&($pwdGraceUseTimeCount < (int)$pwdGraceAuthNLimit[0])) { //at least one more grace login available? |
|
| 651 | + $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
| 652 | + header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( |
|
| 653 | + 'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid))); |
|
| 654 | + } else { //no more grace login available |
|
| 655 | + header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( |
|
| 656 | + 'user_ldap.renewPassword.showLoginFormInvalidPassword', array('user' => $uid))); |
|
| 657 | + } |
|
| 658 | + exit(); |
|
| 659 | + } |
|
| 660 | + //handle pwdReset attribute |
|
| 661 | + if($pwdReset && (count($pwdReset) > 0) && $pwdReset[0] === 'TRUE') { //user must change his password |
|
| 662 | + $this->config->setUserValue($uid, 'user_ldap', 'needsPasswordReset', 'true'); |
|
| 663 | + header('Location: '.\OC::$server->getURLGenerator()->linkToRouteAbsolute( |
|
| 664 | + 'user_ldap.renewPassword.showRenewPasswordForm', array('user' => $uid))); |
|
| 665 | + exit(); |
|
| 666 | + } |
|
| 667 | + //handle password expiry warning |
|
| 668 | + if($pwdChangedTime && (count($pwdChangedTime) > 0)) { |
|
| 669 | + if($pwdMaxAge && (count($pwdMaxAge) > 0) |
|
| 670 | + && $pwdExpireWarning && (count($pwdExpireWarning) > 0)) { |
|
| 671 | + $pwdMaxAgeInt = (int)$pwdMaxAge[0]; |
|
| 672 | + $pwdExpireWarningInt = (int)$pwdExpireWarning[0]; |
|
| 673 | + if($pwdMaxAgeInt > 0 && $pwdExpireWarningInt > 0){ |
|
| 674 | + $pwdChangedTimeDt = \DateTime::createFromFormat('YmdHisZ', $pwdChangedTime[0]); |
|
| 675 | + $pwdChangedTimeDt->add(new \DateInterval('PT'.$pwdMaxAgeInt.'S')); |
|
| 676 | + $currentDateTime = new \DateTime(); |
|
| 677 | + $secondsToExpiry = $pwdChangedTimeDt->getTimestamp() - $currentDateTime->getTimestamp(); |
|
| 678 | + if($secondsToExpiry <= $pwdExpireWarningInt) { |
|
| 679 | + //remove last password expiry warning if any |
|
| 680 | + $notification = $this->notificationManager->createNotification(); |
|
| 681 | + $notification->setApp('user_ldap') |
|
| 682 | + ->setUser($uid) |
|
| 683 | + ->setObject('pwd_exp_warn', $uid) |
|
| 684 | + ; |
|
| 685 | + $this->notificationManager->markProcessed($notification); |
|
| 686 | + //create new password expiry warning |
|
| 687 | + $notification = $this->notificationManager->createNotification(); |
|
| 688 | + $notification->setApp('user_ldap') |
|
| 689 | + ->setUser($uid) |
|
| 690 | + ->setDateTime($currentDateTime) |
|
| 691 | + ->setObject('pwd_exp_warn', $uid) |
|
| 692 | + ->setSubject('pwd_exp_warn_days', [(int) ceil($secondsToExpiry / 60 / 60 / 24)]) |
|
| 693 | + ; |
|
| 694 | + $this->notificationManager->notify($notification); |
|
| 695 | + } |
|
| 696 | + } |
|
| 697 | + } |
|
| 698 | + } |
|
| 699 | + } |
|
| 700 | + } |
|
| 701 | 701 | } |
@@ -40,87 +40,87 @@ |
||
| 40 | 40 | use Sabre\DAV\Exception\ServiceUnavailable; |
| 41 | 41 | |
| 42 | 42 | class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { |
| 43 | - protected $nonFatalExceptions = [ |
|
| 44 | - NotAuthenticated::class => true, |
|
| 45 | - // If tokenauth can throw this exception (which is basically as |
|
| 46 | - // NotAuthenticated. So not fatal. |
|
| 47 | - PasswordLoginForbidden::class => true, |
|
| 48 | - // basically a NotAuthenticated |
|
| 49 | - InvalidSyncToken::class => true, |
|
| 50 | - // the sync client uses this to find out whether files exist, |
|
| 51 | - // so it is not always an error, log it as debug |
|
| 52 | - NotFound::class => true, |
|
| 53 | - // this one mostly happens when the same file is uploaded at |
|
| 54 | - // exactly the same time from two clients, only one client |
|
| 55 | - // wins, the second one gets "Precondition failed" |
|
| 56 | - PreconditionFailed::class => true, |
|
| 57 | - // forbidden can be expected when trying to upload to |
|
| 58 | - // read-only folders for example |
|
| 59 | - Forbidden::class => true, |
|
| 60 | - // Happens when an external storage or federated share is temporarily |
|
| 61 | - // not available |
|
| 62 | - StorageNotAvailableException::class => true, |
|
| 63 | - // happens if some a client uses the wrong method for a given URL |
|
| 64 | - // the error message itself is visible on the client side anyways |
|
| 65 | - NotImplemented::class => true, |
|
| 66 | - // happens when the parent directory is not present (for example when a |
|
| 67 | - // move is done to a non-existent directory) |
|
| 68 | - Conflict::class => true, |
|
| 69 | - // happens when a certain method is not allowed to be called |
|
| 70 | - // for example creating a folder that already exists |
|
| 71 | - MethodNotAllowed::class => true, |
|
| 72 | - ]; |
|
| 43 | + protected $nonFatalExceptions = [ |
|
| 44 | + NotAuthenticated::class => true, |
|
| 45 | + // If tokenauth can throw this exception (which is basically as |
|
| 46 | + // NotAuthenticated. So not fatal. |
|
| 47 | + PasswordLoginForbidden::class => true, |
|
| 48 | + // basically a NotAuthenticated |
|
| 49 | + InvalidSyncToken::class => true, |
|
| 50 | + // the sync client uses this to find out whether files exist, |
|
| 51 | + // so it is not always an error, log it as debug |
|
| 52 | + NotFound::class => true, |
|
| 53 | + // this one mostly happens when the same file is uploaded at |
|
| 54 | + // exactly the same time from two clients, only one client |
|
| 55 | + // wins, the second one gets "Precondition failed" |
|
| 56 | + PreconditionFailed::class => true, |
|
| 57 | + // forbidden can be expected when trying to upload to |
|
| 58 | + // read-only folders for example |
|
| 59 | + Forbidden::class => true, |
|
| 60 | + // Happens when an external storage or federated share is temporarily |
|
| 61 | + // not available |
|
| 62 | + StorageNotAvailableException::class => true, |
|
| 63 | + // happens if some a client uses the wrong method for a given URL |
|
| 64 | + // the error message itself is visible on the client side anyways |
|
| 65 | + NotImplemented::class => true, |
|
| 66 | + // happens when the parent directory is not present (for example when a |
|
| 67 | + // move is done to a non-existent directory) |
|
| 68 | + Conflict::class => true, |
|
| 69 | + // happens when a certain method is not allowed to be called |
|
| 70 | + // for example creating a folder that already exists |
|
| 71 | + MethodNotAllowed::class => true, |
|
| 72 | + ]; |
|
| 73 | 73 | |
| 74 | - /** @var string */ |
|
| 75 | - private $appName; |
|
| 74 | + /** @var string */ |
|
| 75 | + private $appName; |
|
| 76 | 76 | |
| 77 | - /** @var ILogger */ |
|
| 78 | - private $logger; |
|
| 77 | + /** @var ILogger */ |
|
| 78 | + private $logger; |
|
| 79 | 79 | |
| 80 | - /** |
|
| 81 | - * @param string $loggerAppName app name to use when logging |
|
| 82 | - * @param ILogger $logger |
|
| 83 | - */ |
|
| 84 | - public function __construct($loggerAppName, $logger) { |
|
| 85 | - $this->appName = $loggerAppName; |
|
| 86 | - $this->logger = $logger; |
|
| 87 | - } |
|
| 80 | + /** |
|
| 81 | + * @param string $loggerAppName app name to use when logging |
|
| 82 | + * @param ILogger $logger |
|
| 83 | + */ |
|
| 84 | + public function __construct($loggerAppName, $logger) { |
|
| 85 | + $this->appName = $loggerAppName; |
|
| 86 | + $this->logger = $logger; |
|
| 87 | + } |
|
| 88 | 88 | |
| 89 | - /** |
|
| 90 | - * This initializes the plugin. |
|
| 91 | - * |
|
| 92 | - * This function is called by \Sabre\DAV\Server, after |
|
| 93 | - * addPlugin is called. |
|
| 94 | - * |
|
| 95 | - * This method should set up the required event subscriptions. |
|
| 96 | - * |
|
| 97 | - * @param \Sabre\DAV\Server $server |
|
| 98 | - * @return void |
|
| 99 | - */ |
|
| 100 | - public function initialize(\Sabre\DAV\Server $server) { |
|
| 89 | + /** |
|
| 90 | + * This initializes the plugin. |
|
| 91 | + * |
|
| 92 | + * This function is called by \Sabre\DAV\Server, after |
|
| 93 | + * addPlugin is called. |
|
| 94 | + * |
|
| 95 | + * This method should set up the required event subscriptions. |
|
| 96 | + * |
|
| 97 | + * @param \Sabre\DAV\Server $server |
|
| 98 | + * @return void |
|
| 99 | + */ |
|
| 100 | + public function initialize(\Sabre\DAV\Server $server) { |
|
| 101 | 101 | |
| 102 | - $server->on('exception', array($this, 'logException'), 10); |
|
| 103 | - } |
|
| 102 | + $server->on('exception', array($this, 'logException'), 10); |
|
| 103 | + } |
|
| 104 | 104 | |
| 105 | - /** |
|
| 106 | - * Log exception |
|
| 107 | - * |
|
| 108 | - */ |
|
| 109 | - public function logException(\Exception $ex) { |
|
| 110 | - $exceptionClass = get_class($ex); |
|
| 111 | - $level = ILogger::FATAL; |
|
| 112 | - if (isset($this->nonFatalExceptions[$exceptionClass]) || |
|
| 113 | - ( |
|
| 114 | - $exceptionClass === ServiceUnavailable::class && |
|
| 115 | - $ex->getMessage() === 'System in maintenance mode.' |
|
| 116 | - ) |
|
| 117 | - ) { |
|
| 118 | - $level = ILogger::DEBUG; |
|
| 119 | - } |
|
| 105 | + /** |
|
| 106 | + * Log exception |
|
| 107 | + * |
|
| 108 | + */ |
|
| 109 | + public function logException(\Exception $ex) { |
|
| 110 | + $exceptionClass = get_class($ex); |
|
| 111 | + $level = ILogger::FATAL; |
|
| 112 | + if (isset($this->nonFatalExceptions[$exceptionClass]) || |
|
| 113 | + ( |
|
| 114 | + $exceptionClass === ServiceUnavailable::class && |
|
| 115 | + $ex->getMessage() === 'System in maintenance mode.' |
|
| 116 | + ) |
|
| 117 | + ) { |
|
| 118 | + $level = ILogger::DEBUG; |
|
| 119 | + } |
|
| 120 | 120 | |
| 121 | - $this->logger->logException($ex, [ |
|
| 122 | - 'app' => $this->appName, |
|
| 123 | - 'level' => $level, |
|
| 124 | - ]); |
|
| 125 | - } |
|
| 121 | + $this->logger->logException($ex, [ |
|
| 122 | + 'app' => $this->appName, |
|
| 123 | + 'level' => $level, |
|
| 124 | + ]); |
|
| 125 | + } |
|
| 126 | 126 | } |
@@ -53,989 +53,989 @@ |
||
| 53 | 53 | */ |
| 54 | 54 | class FederatedShareProvider implements IShareProvider { |
| 55 | 55 | |
| 56 | - const SHARE_TYPE_REMOTE = 6; |
|
| 57 | - |
|
| 58 | - /** @var IDBConnection */ |
|
| 59 | - private $dbConnection; |
|
| 60 | - |
|
| 61 | - /** @var AddressHandler */ |
|
| 62 | - private $addressHandler; |
|
| 63 | - |
|
| 64 | - /** @var Notifications */ |
|
| 65 | - private $notifications; |
|
| 66 | - |
|
| 67 | - /** @var TokenHandler */ |
|
| 68 | - private $tokenHandler; |
|
| 69 | - |
|
| 70 | - /** @var IL10N */ |
|
| 71 | - private $l; |
|
| 72 | - |
|
| 73 | - /** @var ILogger */ |
|
| 74 | - private $logger; |
|
| 75 | - |
|
| 76 | - /** @var IRootFolder */ |
|
| 77 | - private $rootFolder; |
|
| 78 | - |
|
| 79 | - /** @var IConfig */ |
|
| 80 | - private $config; |
|
| 81 | - |
|
| 82 | - /** @var string */ |
|
| 83 | - private $externalShareTable = 'share_external'; |
|
| 84 | - |
|
| 85 | - /** @var IUserManager */ |
|
| 86 | - private $userManager; |
|
| 87 | - |
|
| 88 | - /** @var ICloudIdManager */ |
|
| 89 | - private $cloudIdManager; |
|
| 90 | - |
|
| 91 | - /** @var \OCP\GlobalScale\IConfig */ |
|
| 92 | - private $gsConfig; |
|
| 93 | - |
|
| 94 | - /** |
|
| 95 | - * DefaultShareProvider constructor. |
|
| 96 | - * |
|
| 97 | - * @param IDBConnection $connection |
|
| 98 | - * @param AddressHandler $addressHandler |
|
| 99 | - * @param Notifications $notifications |
|
| 100 | - * @param TokenHandler $tokenHandler |
|
| 101 | - * @param IL10N $l10n |
|
| 102 | - * @param ILogger $logger |
|
| 103 | - * @param IRootFolder $rootFolder |
|
| 104 | - * @param IConfig $config |
|
| 105 | - * @param IUserManager $userManager |
|
| 106 | - * @param ICloudIdManager $cloudIdManager |
|
| 107 | - * @param \OCP\GlobalScale\IConfig $globalScaleConfig |
|
| 108 | - */ |
|
| 109 | - public function __construct( |
|
| 110 | - IDBConnection $connection, |
|
| 111 | - AddressHandler $addressHandler, |
|
| 112 | - Notifications $notifications, |
|
| 113 | - TokenHandler $tokenHandler, |
|
| 114 | - IL10N $l10n, |
|
| 115 | - ILogger $logger, |
|
| 116 | - IRootFolder $rootFolder, |
|
| 117 | - IConfig $config, |
|
| 118 | - IUserManager $userManager, |
|
| 119 | - ICloudIdManager $cloudIdManager, |
|
| 120 | - \OCP\GlobalScale\IConfig $globalScaleConfig |
|
| 121 | - ) { |
|
| 122 | - $this->dbConnection = $connection; |
|
| 123 | - $this->addressHandler = $addressHandler; |
|
| 124 | - $this->notifications = $notifications; |
|
| 125 | - $this->tokenHandler = $tokenHandler; |
|
| 126 | - $this->l = $l10n; |
|
| 127 | - $this->logger = $logger; |
|
| 128 | - $this->rootFolder = $rootFolder; |
|
| 129 | - $this->config = $config; |
|
| 130 | - $this->userManager = $userManager; |
|
| 131 | - $this->cloudIdManager = $cloudIdManager; |
|
| 132 | - $this->gsConfig = $globalScaleConfig; |
|
| 133 | - } |
|
| 134 | - |
|
| 135 | - /** |
|
| 136 | - * Return the identifier of this provider. |
|
| 137 | - * |
|
| 138 | - * @return string Containing only [a-zA-Z0-9] |
|
| 139 | - */ |
|
| 140 | - public function identifier() { |
|
| 141 | - return 'ocFederatedSharing'; |
|
| 142 | - } |
|
| 143 | - |
|
| 144 | - /** |
|
| 145 | - * Share a path |
|
| 146 | - * |
|
| 147 | - * @param IShare $share |
|
| 148 | - * @return IShare The share object |
|
| 149 | - * @throws ShareNotFound |
|
| 150 | - * @throws \Exception |
|
| 151 | - */ |
|
| 152 | - public function create(IShare $share) { |
|
| 153 | - |
|
| 154 | - $shareWith = $share->getSharedWith(); |
|
| 155 | - $itemSource = $share->getNodeId(); |
|
| 156 | - $itemType = $share->getNodeType(); |
|
| 157 | - $permissions = $share->getPermissions(); |
|
| 158 | - $sharedBy = $share->getSharedBy(); |
|
| 159 | - |
|
| 160 | - /* |
|
| 56 | + const SHARE_TYPE_REMOTE = 6; |
|
| 57 | + |
|
| 58 | + /** @var IDBConnection */ |
|
| 59 | + private $dbConnection; |
|
| 60 | + |
|
| 61 | + /** @var AddressHandler */ |
|
| 62 | + private $addressHandler; |
|
| 63 | + |
|
| 64 | + /** @var Notifications */ |
|
| 65 | + private $notifications; |
|
| 66 | + |
|
| 67 | + /** @var TokenHandler */ |
|
| 68 | + private $tokenHandler; |
|
| 69 | + |
|
| 70 | + /** @var IL10N */ |
|
| 71 | + private $l; |
|
| 72 | + |
|
| 73 | + /** @var ILogger */ |
|
| 74 | + private $logger; |
|
| 75 | + |
|
| 76 | + /** @var IRootFolder */ |
|
| 77 | + private $rootFolder; |
|
| 78 | + |
|
| 79 | + /** @var IConfig */ |
|
| 80 | + private $config; |
|
| 81 | + |
|
| 82 | + /** @var string */ |
|
| 83 | + private $externalShareTable = 'share_external'; |
|
| 84 | + |
|
| 85 | + /** @var IUserManager */ |
|
| 86 | + private $userManager; |
|
| 87 | + |
|
| 88 | + /** @var ICloudIdManager */ |
|
| 89 | + private $cloudIdManager; |
|
| 90 | + |
|
| 91 | + /** @var \OCP\GlobalScale\IConfig */ |
|
| 92 | + private $gsConfig; |
|
| 93 | + |
|
| 94 | + /** |
|
| 95 | + * DefaultShareProvider constructor. |
|
| 96 | + * |
|
| 97 | + * @param IDBConnection $connection |
|
| 98 | + * @param AddressHandler $addressHandler |
|
| 99 | + * @param Notifications $notifications |
|
| 100 | + * @param TokenHandler $tokenHandler |
|
| 101 | + * @param IL10N $l10n |
|
| 102 | + * @param ILogger $logger |
|
| 103 | + * @param IRootFolder $rootFolder |
|
| 104 | + * @param IConfig $config |
|
| 105 | + * @param IUserManager $userManager |
|
| 106 | + * @param ICloudIdManager $cloudIdManager |
|
| 107 | + * @param \OCP\GlobalScale\IConfig $globalScaleConfig |
|
| 108 | + */ |
|
| 109 | + public function __construct( |
|
| 110 | + IDBConnection $connection, |
|
| 111 | + AddressHandler $addressHandler, |
|
| 112 | + Notifications $notifications, |
|
| 113 | + TokenHandler $tokenHandler, |
|
| 114 | + IL10N $l10n, |
|
| 115 | + ILogger $logger, |
|
| 116 | + IRootFolder $rootFolder, |
|
| 117 | + IConfig $config, |
|
| 118 | + IUserManager $userManager, |
|
| 119 | + ICloudIdManager $cloudIdManager, |
|
| 120 | + \OCP\GlobalScale\IConfig $globalScaleConfig |
|
| 121 | + ) { |
|
| 122 | + $this->dbConnection = $connection; |
|
| 123 | + $this->addressHandler = $addressHandler; |
|
| 124 | + $this->notifications = $notifications; |
|
| 125 | + $this->tokenHandler = $tokenHandler; |
|
| 126 | + $this->l = $l10n; |
|
| 127 | + $this->logger = $logger; |
|
| 128 | + $this->rootFolder = $rootFolder; |
|
| 129 | + $this->config = $config; |
|
| 130 | + $this->userManager = $userManager; |
|
| 131 | + $this->cloudIdManager = $cloudIdManager; |
|
| 132 | + $this->gsConfig = $globalScaleConfig; |
|
| 133 | + } |
|
| 134 | + |
|
| 135 | + /** |
|
| 136 | + * Return the identifier of this provider. |
|
| 137 | + * |
|
| 138 | + * @return string Containing only [a-zA-Z0-9] |
|
| 139 | + */ |
|
| 140 | + public function identifier() { |
|
| 141 | + return 'ocFederatedSharing'; |
|
| 142 | + } |
|
| 143 | + |
|
| 144 | + /** |
|
| 145 | + * Share a path |
|
| 146 | + * |
|
| 147 | + * @param IShare $share |
|
| 148 | + * @return IShare The share object |
|
| 149 | + * @throws ShareNotFound |
|
| 150 | + * @throws \Exception |
|
| 151 | + */ |
|
| 152 | + public function create(IShare $share) { |
|
| 153 | + |
|
| 154 | + $shareWith = $share->getSharedWith(); |
|
| 155 | + $itemSource = $share->getNodeId(); |
|
| 156 | + $itemType = $share->getNodeType(); |
|
| 157 | + $permissions = $share->getPermissions(); |
|
| 158 | + $sharedBy = $share->getSharedBy(); |
|
| 159 | + |
|
| 160 | + /* |
|
| 161 | 161 | * Check if file is not already shared with the remote user |
| 162 | 162 | */ |
| 163 | - $alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0); |
|
| 164 | - if (!empty($alreadyShared)) { |
|
| 165 | - $message = 'Sharing %s failed, because this item is already shared with %s'; |
|
| 166 | - $message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); |
|
| 167 | - $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); |
|
| 168 | - throw new \Exception($message_t); |
|
| 169 | - } |
|
| 170 | - |
|
| 171 | - |
|
| 172 | - // don't allow federated shares if source and target server are the same |
|
| 173 | - $cloudId = $this->cloudIdManager->resolveCloudId($shareWith); |
|
| 174 | - $currentServer = $this->addressHandler->generateRemoteURL(); |
|
| 175 | - $currentUser = $sharedBy; |
|
| 176 | - if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) { |
|
| 177 | - $message = 'Not allowed to create a federated share with the same user.'; |
|
| 178 | - $message_t = $this->l->t('Not allowed to create a federated share with the same user'); |
|
| 179 | - $this->logger->debug($message, ['app' => 'Federated File Sharing']); |
|
| 180 | - throw new \Exception($message_t); |
|
| 181 | - } |
|
| 182 | - |
|
| 183 | - |
|
| 184 | - $share->setSharedWith($cloudId->getId()); |
|
| 185 | - |
|
| 186 | - try { |
|
| 187 | - $remoteShare = $this->getShareFromExternalShareTable($share); |
|
| 188 | - } catch (ShareNotFound $e) { |
|
| 189 | - $remoteShare = null; |
|
| 190 | - } |
|
| 191 | - |
|
| 192 | - if ($remoteShare) { |
|
| 193 | - try { |
|
| 194 | - $ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']); |
|
| 195 | - $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time()); |
|
| 196 | - $share->setId($shareId); |
|
| 197 | - list($token, $remoteId) = $this->askOwnerToReShare($shareWith, $share, $shareId); |
|
| 198 | - // remote share was create successfully if we get a valid token as return |
|
| 199 | - $send = is_string($token) && $token !== ''; |
|
| 200 | - } catch (\Exception $e) { |
|
| 201 | - // fall back to old re-share behavior if the remote server |
|
| 202 | - // doesn't support flat re-shares (was introduced with Nextcloud 9.1) |
|
| 203 | - $this->removeShareFromTable($share); |
|
| 204 | - $shareId = $this->createFederatedShare($share); |
|
| 205 | - } |
|
| 206 | - if ($send) { |
|
| 207 | - $this->updateSuccessfulReshare($shareId, $token); |
|
| 208 | - $this->storeRemoteId($shareId, $remoteId); |
|
| 209 | - } else { |
|
| 210 | - $this->removeShareFromTable($share); |
|
| 211 | - $message_t = $this->l->t('File is already shared with %s', [$shareWith]); |
|
| 212 | - throw new \Exception($message_t); |
|
| 213 | - } |
|
| 214 | - |
|
| 215 | - } else { |
|
| 216 | - $shareId = $this->createFederatedShare($share); |
|
| 217 | - } |
|
| 218 | - |
|
| 219 | - $data = $this->getRawShare($shareId); |
|
| 220 | - return $this->createShareObject($data); |
|
| 221 | - } |
|
| 222 | - |
|
| 223 | - /** |
|
| 224 | - * create federated share and inform the recipient |
|
| 225 | - * |
|
| 226 | - * @param IShare $share |
|
| 227 | - * @return int |
|
| 228 | - * @throws ShareNotFound |
|
| 229 | - * @throws \Exception |
|
| 230 | - */ |
|
| 231 | - protected function createFederatedShare(IShare $share) { |
|
| 232 | - $token = $this->tokenHandler->generateToken(); |
|
| 233 | - $shareId = $this->addShareToDB( |
|
| 234 | - $share->getNodeId(), |
|
| 235 | - $share->getNodeType(), |
|
| 236 | - $share->getSharedWith(), |
|
| 237 | - $share->getSharedBy(), |
|
| 238 | - $share->getShareOwner(), |
|
| 239 | - $share->getPermissions(), |
|
| 240 | - $token |
|
| 241 | - ); |
|
| 242 | - |
|
| 243 | - $failure = false; |
|
| 244 | - |
|
| 245 | - try { |
|
| 246 | - $sharedByFederatedId = $share->getSharedBy(); |
|
| 247 | - if ($this->userManager->userExists($sharedByFederatedId)) { |
|
| 248 | - $cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL()); |
|
| 249 | - $sharedByFederatedId = $cloudId->getId(); |
|
| 250 | - } |
|
| 251 | - $ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL()); |
|
| 252 | - $send = $this->notifications->sendRemoteShare( |
|
| 253 | - $token, |
|
| 254 | - $share->getSharedWith(), |
|
| 255 | - $share->getNode()->getName(), |
|
| 256 | - $shareId, |
|
| 257 | - $share->getShareOwner(), |
|
| 258 | - $ownerCloudId->getId(), |
|
| 259 | - $share->getSharedBy(), |
|
| 260 | - $sharedByFederatedId |
|
| 261 | - ); |
|
| 262 | - |
|
| 263 | - if ($send === false) { |
|
| 264 | - $failure = true; |
|
| 265 | - } |
|
| 266 | - } catch (\Exception $e) { |
|
| 267 | - $this->logger->logException($e, [ |
|
| 268 | - 'message' => 'Failed to notify remote server of federated share, removing share.', |
|
| 269 | - 'level' => ILogger::ERROR, |
|
| 270 | - 'app' => 'federatedfilesharing', |
|
| 271 | - ]); |
|
| 272 | - $failure = true; |
|
| 273 | - } |
|
| 274 | - |
|
| 275 | - if($failure) { |
|
| 276 | - $this->removeShareFromTableById($shareId); |
|
| 277 | - $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable or uses a self-signed certificate.', |
|
| 278 | - [$share->getNode()->getName(), $share->getSharedWith()]); |
|
| 279 | - throw new \Exception($message_t); |
|
| 280 | - } |
|
| 281 | - |
|
| 282 | - return $shareId; |
|
| 283 | - |
|
| 284 | - } |
|
| 285 | - |
|
| 286 | - /** |
|
| 287 | - * @param string $shareWith |
|
| 288 | - * @param IShare $share |
|
| 289 | - * @param string $shareId internal share Id |
|
| 290 | - * @return array |
|
| 291 | - * @throws \Exception |
|
| 292 | - */ |
|
| 293 | - protected function askOwnerToReShare($shareWith, IShare $share, $shareId) { |
|
| 294 | - |
|
| 295 | - $remoteShare = $this->getShareFromExternalShareTable($share); |
|
| 296 | - $token = $remoteShare['share_token']; |
|
| 297 | - $remoteId = $remoteShare['remote_id']; |
|
| 298 | - $remote = $remoteShare['remote']; |
|
| 299 | - |
|
| 300 | - list($token, $remoteId) = $this->notifications->requestReShare( |
|
| 301 | - $token, |
|
| 302 | - $remoteId, |
|
| 303 | - $shareId, |
|
| 304 | - $remote, |
|
| 305 | - $shareWith, |
|
| 306 | - $share->getPermissions() |
|
| 307 | - ); |
|
| 308 | - |
|
| 309 | - return [$token, $remoteId]; |
|
| 310 | - } |
|
| 311 | - |
|
| 312 | - /** |
|
| 313 | - * get federated share from the share_external table but exclude mounted link shares |
|
| 314 | - * |
|
| 315 | - * @param IShare $share |
|
| 316 | - * @return array |
|
| 317 | - * @throws ShareNotFound |
|
| 318 | - */ |
|
| 319 | - protected function getShareFromExternalShareTable(IShare $share) { |
|
| 320 | - $query = $this->dbConnection->getQueryBuilder(); |
|
| 321 | - $query->select('*')->from($this->externalShareTable) |
|
| 322 | - ->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner()))) |
|
| 323 | - ->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget()))); |
|
| 324 | - $result = $query->execute()->fetchAll(); |
|
| 325 | - |
|
| 326 | - if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) { |
|
| 327 | - return $result[0]; |
|
| 328 | - } |
|
| 329 | - |
|
| 330 | - throw new ShareNotFound('share not found in share_external table'); |
|
| 331 | - } |
|
| 332 | - |
|
| 333 | - /** |
|
| 334 | - * add share to the database and return the ID |
|
| 335 | - * |
|
| 336 | - * @param int $itemSource |
|
| 337 | - * @param string $itemType |
|
| 338 | - * @param string $shareWith |
|
| 339 | - * @param string $sharedBy |
|
| 340 | - * @param string $uidOwner |
|
| 341 | - * @param int $permissions |
|
| 342 | - * @param string $token |
|
| 343 | - * @return int |
|
| 344 | - */ |
|
| 345 | - private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) { |
|
| 346 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 347 | - $qb->insert('share') |
|
| 348 | - ->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)) |
|
| 349 | - ->setValue('item_type', $qb->createNamedParameter($itemType)) |
|
| 350 | - ->setValue('item_source', $qb->createNamedParameter($itemSource)) |
|
| 351 | - ->setValue('file_source', $qb->createNamedParameter($itemSource)) |
|
| 352 | - ->setValue('share_with', $qb->createNamedParameter($shareWith)) |
|
| 353 | - ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) |
|
| 354 | - ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) |
|
| 355 | - ->setValue('permissions', $qb->createNamedParameter($permissions)) |
|
| 356 | - ->setValue('token', $qb->createNamedParameter($token)) |
|
| 357 | - ->setValue('stime', $qb->createNamedParameter(time())); |
|
| 358 | - |
|
| 359 | - /* |
|
| 163 | + $alreadyShared = $this->getSharedWith($shareWith, self::SHARE_TYPE_REMOTE, $share->getNode(), 1, 0); |
|
| 164 | + if (!empty($alreadyShared)) { |
|
| 165 | + $message = 'Sharing %s failed, because this item is already shared with %s'; |
|
| 166 | + $message_t = $this->l->t('Sharing %s failed, because this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); |
|
| 167 | + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); |
|
| 168 | + throw new \Exception($message_t); |
|
| 169 | + } |
|
| 170 | + |
|
| 171 | + |
|
| 172 | + // don't allow federated shares if source and target server are the same |
|
| 173 | + $cloudId = $this->cloudIdManager->resolveCloudId($shareWith); |
|
| 174 | + $currentServer = $this->addressHandler->generateRemoteURL(); |
|
| 175 | + $currentUser = $sharedBy; |
|
| 176 | + if ($this->addressHandler->compareAddresses($cloudId->getUser(), $cloudId->getRemote(), $currentUser, $currentServer)) { |
|
| 177 | + $message = 'Not allowed to create a federated share with the same user.'; |
|
| 178 | + $message_t = $this->l->t('Not allowed to create a federated share with the same user'); |
|
| 179 | + $this->logger->debug($message, ['app' => 'Federated File Sharing']); |
|
| 180 | + throw new \Exception($message_t); |
|
| 181 | + } |
|
| 182 | + |
|
| 183 | + |
|
| 184 | + $share->setSharedWith($cloudId->getId()); |
|
| 185 | + |
|
| 186 | + try { |
|
| 187 | + $remoteShare = $this->getShareFromExternalShareTable($share); |
|
| 188 | + } catch (ShareNotFound $e) { |
|
| 189 | + $remoteShare = null; |
|
| 190 | + } |
|
| 191 | + |
|
| 192 | + if ($remoteShare) { |
|
| 193 | + try { |
|
| 194 | + $ownerCloudId = $this->cloudIdManager->getCloudId($remoteShare['owner'], $remoteShare['remote']); |
|
| 195 | + $shareId = $this->addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $ownerCloudId->getId(), $permissions, 'tmp_token_' . time()); |
|
| 196 | + $share->setId($shareId); |
|
| 197 | + list($token, $remoteId) = $this->askOwnerToReShare($shareWith, $share, $shareId); |
|
| 198 | + // remote share was create successfully if we get a valid token as return |
|
| 199 | + $send = is_string($token) && $token !== ''; |
|
| 200 | + } catch (\Exception $e) { |
|
| 201 | + // fall back to old re-share behavior if the remote server |
|
| 202 | + // doesn't support flat re-shares (was introduced with Nextcloud 9.1) |
|
| 203 | + $this->removeShareFromTable($share); |
|
| 204 | + $shareId = $this->createFederatedShare($share); |
|
| 205 | + } |
|
| 206 | + if ($send) { |
|
| 207 | + $this->updateSuccessfulReshare($shareId, $token); |
|
| 208 | + $this->storeRemoteId($shareId, $remoteId); |
|
| 209 | + } else { |
|
| 210 | + $this->removeShareFromTable($share); |
|
| 211 | + $message_t = $this->l->t('File is already shared with %s', [$shareWith]); |
|
| 212 | + throw new \Exception($message_t); |
|
| 213 | + } |
|
| 214 | + |
|
| 215 | + } else { |
|
| 216 | + $shareId = $this->createFederatedShare($share); |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + $data = $this->getRawShare($shareId); |
|
| 220 | + return $this->createShareObject($data); |
|
| 221 | + } |
|
| 222 | + |
|
| 223 | + /** |
|
| 224 | + * create federated share and inform the recipient |
|
| 225 | + * |
|
| 226 | + * @param IShare $share |
|
| 227 | + * @return int |
|
| 228 | + * @throws ShareNotFound |
|
| 229 | + * @throws \Exception |
|
| 230 | + */ |
|
| 231 | + protected function createFederatedShare(IShare $share) { |
|
| 232 | + $token = $this->tokenHandler->generateToken(); |
|
| 233 | + $shareId = $this->addShareToDB( |
|
| 234 | + $share->getNodeId(), |
|
| 235 | + $share->getNodeType(), |
|
| 236 | + $share->getSharedWith(), |
|
| 237 | + $share->getSharedBy(), |
|
| 238 | + $share->getShareOwner(), |
|
| 239 | + $share->getPermissions(), |
|
| 240 | + $token |
|
| 241 | + ); |
|
| 242 | + |
|
| 243 | + $failure = false; |
|
| 244 | + |
|
| 245 | + try { |
|
| 246 | + $sharedByFederatedId = $share->getSharedBy(); |
|
| 247 | + if ($this->userManager->userExists($sharedByFederatedId)) { |
|
| 248 | + $cloudId = $this->cloudIdManager->getCloudId($sharedByFederatedId, $this->addressHandler->generateRemoteURL()); |
|
| 249 | + $sharedByFederatedId = $cloudId->getId(); |
|
| 250 | + } |
|
| 251 | + $ownerCloudId = $this->cloudIdManager->getCloudId($share->getShareOwner(), $this->addressHandler->generateRemoteURL()); |
|
| 252 | + $send = $this->notifications->sendRemoteShare( |
|
| 253 | + $token, |
|
| 254 | + $share->getSharedWith(), |
|
| 255 | + $share->getNode()->getName(), |
|
| 256 | + $shareId, |
|
| 257 | + $share->getShareOwner(), |
|
| 258 | + $ownerCloudId->getId(), |
|
| 259 | + $share->getSharedBy(), |
|
| 260 | + $sharedByFederatedId |
|
| 261 | + ); |
|
| 262 | + |
|
| 263 | + if ($send === false) { |
|
| 264 | + $failure = true; |
|
| 265 | + } |
|
| 266 | + } catch (\Exception $e) { |
|
| 267 | + $this->logger->logException($e, [ |
|
| 268 | + 'message' => 'Failed to notify remote server of federated share, removing share.', |
|
| 269 | + 'level' => ILogger::ERROR, |
|
| 270 | + 'app' => 'federatedfilesharing', |
|
| 271 | + ]); |
|
| 272 | + $failure = true; |
|
| 273 | + } |
|
| 274 | + |
|
| 275 | + if($failure) { |
|
| 276 | + $this->removeShareFromTableById($shareId); |
|
| 277 | + $message_t = $this->l->t('Sharing %s failed, could not find %s, maybe the server is currently unreachable or uses a self-signed certificate.', |
|
| 278 | + [$share->getNode()->getName(), $share->getSharedWith()]); |
|
| 279 | + throw new \Exception($message_t); |
|
| 280 | + } |
|
| 281 | + |
|
| 282 | + return $shareId; |
|
| 283 | + |
|
| 284 | + } |
|
| 285 | + |
|
| 286 | + /** |
|
| 287 | + * @param string $shareWith |
|
| 288 | + * @param IShare $share |
|
| 289 | + * @param string $shareId internal share Id |
|
| 290 | + * @return array |
|
| 291 | + * @throws \Exception |
|
| 292 | + */ |
|
| 293 | + protected function askOwnerToReShare($shareWith, IShare $share, $shareId) { |
|
| 294 | + |
|
| 295 | + $remoteShare = $this->getShareFromExternalShareTable($share); |
|
| 296 | + $token = $remoteShare['share_token']; |
|
| 297 | + $remoteId = $remoteShare['remote_id']; |
|
| 298 | + $remote = $remoteShare['remote']; |
|
| 299 | + |
|
| 300 | + list($token, $remoteId) = $this->notifications->requestReShare( |
|
| 301 | + $token, |
|
| 302 | + $remoteId, |
|
| 303 | + $shareId, |
|
| 304 | + $remote, |
|
| 305 | + $shareWith, |
|
| 306 | + $share->getPermissions() |
|
| 307 | + ); |
|
| 308 | + |
|
| 309 | + return [$token, $remoteId]; |
|
| 310 | + } |
|
| 311 | + |
|
| 312 | + /** |
|
| 313 | + * get federated share from the share_external table but exclude mounted link shares |
|
| 314 | + * |
|
| 315 | + * @param IShare $share |
|
| 316 | + * @return array |
|
| 317 | + * @throws ShareNotFound |
|
| 318 | + */ |
|
| 319 | + protected function getShareFromExternalShareTable(IShare $share) { |
|
| 320 | + $query = $this->dbConnection->getQueryBuilder(); |
|
| 321 | + $query->select('*')->from($this->externalShareTable) |
|
| 322 | + ->where($query->expr()->eq('user', $query->createNamedParameter($share->getShareOwner()))) |
|
| 323 | + ->andWhere($query->expr()->eq('mountpoint', $query->createNamedParameter($share->getTarget()))); |
|
| 324 | + $result = $query->execute()->fetchAll(); |
|
| 325 | + |
|
| 326 | + if (isset($result[0]) && (int)$result[0]['remote_id'] > 0) { |
|
| 327 | + return $result[0]; |
|
| 328 | + } |
|
| 329 | + |
|
| 330 | + throw new ShareNotFound('share not found in share_external table'); |
|
| 331 | + } |
|
| 332 | + |
|
| 333 | + /** |
|
| 334 | + * add share to the database and return the ID |
|
| 335 | + * |
|
| 336 | + * @param int $itemSource |
|
| 337 | + * @param string $itemType |
|
| 338 | + * @param string $shareWith |
|
| 339 | + * @param string $sharedBy |
|
| 340 | + * @param string $uidOwner |
|
| 341 | + * @param int $permissions |
|
| 342 | + * @param string $token |
|
| 343 | + * @return int |
|
| 344 | + */ |
|
| 345 | + private function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token) { |
|
| 346 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 347 | + $qb->insert('share') |
|
| 348 | + ->setValue('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE)) |
|
| 349 | + ->setValue('item_type', $qb->createNamedParameter($itemType)) |
|
| 350 | + ->setValue('item_source', $qb->createNamedParameter($itemSource)) |
|
| 351 | + ->setValue('file_source', $qb->createNamedParameter($itemSource)) |
|
| 352 | + ->setValue('share_with', $qb->createNamedParameter($shareWith)) |
|
| 353 | + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) |
|
| 354 | + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) |
|
| 355 | + ->setValue('permissions', $qb->createNamedParameter($permissions)) |
|
| 356 | + ->setValue('token', $qb->createNamedParameter($token)) |
|
| 357 | + ->setValue('stime', $qb->createNamedParameter(time())); |
|
| 358 | + |
|
| 359 | + /* |
|
| 360 | 360 | * Added to fix https://github.com/owncloud/core/issues/22215 |
| 361 | 361 | * Can be removed once we get rid of ajax/share.php |
| 362 | 362 | */ |
| 363 | - $qb->setValue('file_target', $qb->createNamedParameter('')); |
|
| 364 | - |
|
| 365 | - $qb->execute(); |
|
| 366 | - $id = $qb->getLastInsertId(); |
|
| 367 | - |
|
| 368 | - return (int)$id; |
|
| 369 | - } |
|
| 370 | - |
|
| 371 | - /** |
|
| 372 | - * Update a share |
|
| 373 | - * |
|
| 374 | - * @param IShare $share |
|
| 375 | - * @return IShare The share object |
|
| 376 | - */ |
|
| 377 | - public function update(IShare $share) { |
|
| 378 | - /* |
|
| 363 | + $qb->setValue('file_target', $qb->createNamedParameter('')); |
|
| 364 | + |
|
| 365 | + $qb->execute(); |
|
| 366 | + $id = $qb->getLastInsertId(); |
|
| 367 | + |
|
| 368 | + return (int)$id; |
|
| 369 | + } |
|
| 370 | + |
|
| 371 | + /** |
|
| 372 | + * Update a share |
|
| 373 | + * |
|
| 374 | + * @param IShare $share |
|
| 375 | + * @return IShare The share object |
|
| 376 | + */ |
|
| 377 | + public function update(IShare $share) { |
|
| 378 | + /* |
|
| 379 | 379 | * We allow updating the permissions of federated shares |
| 380 | 380 | */ |
| 381 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 382 | - $qb->update('share') |
|
| 383 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) |
|
| 384 | - ->set('permissions', $qb->createNamedParameter($share->getPermissions())) |
|
| 385 | - ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) |
|
| 386 | - ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) |
|
| 387 | - ->execute(); |
|
| 388 | - |
|
| 389 | - // send the updated permission to the owner/initiator, if they are not the same |
|
| 390 | - if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 391 | - $this->sendPermissionUpdate($share); |
|
| 392 | - } |
|
| 393 | - |
|
| 394 | - return $share; |
|
| 395 | - } |
|
| 396 | - |
|
| 397 | - /** |
|
| 398 | - * send the updated permission to the owner/initiator, if they are not the same |
|
| 399 | - * |
|
| 400 | - * @param IShare $share |
|
| 401 | - * @throws ShareNotFound |
|
| 402 | - * @throws \OC\HintException |
|
| 403 | - */ |
|
| 404 | - protected function sendPermissionUpdate(IShare $share) { |
|
| 405 | - $remoteId = $this->getRemoteId($share); |
|
| 406 | - // if the local user is the owner we send the permission change to the initiator |
|
| 407 | - if ($this->userManager->userExists($share->getShareOwner())) { |
|
| 408 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 409 | - } else { // ... if not we send the permission change to the owner |
|
| 410 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner()); |
|
| 411 | - } |
|
| 412 | - $this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions()); |
|
| 413 | - } |
|
| 414 | - |
|
| 415 | - |
|
| 416 | - /** |
|
| 417 | - * update successful reShare with the correct token |
|
| 418 | - * |
|
| 419 | - * @param int $shareId |
|
| 420 | - * @param string $token |
|
| 421 | - */ |
|
| 422 | - protected function updateSuccessfulReShare($shareId, $token) { |
|
| 423 | - $query = $this->dbConnection->getQueryBuilder(); |
|
| 424 | - $query->update('share') |
|
| 425 | - ->where($query->expr()->eq('id', $query->createNamedParameter($shareId))) |
|
| 426 | - ->set('token', $query->createNamedParameter($token)) |
|
| 427 | - ->execute(); |
|
| 428 | - } |
|
| 429 | - |
|
| 430 | - /** |
|
| 431 | - * store remote ID in federated reShare table |
|
| 432 | - * |
|
| 433 | - * @param $shareId |
|
| 434 | - * @param $remoteId |
|
| 435 | - */ |
|
| 436 | - public function storeRemoteId($shareId, $remoteId) { |
|
| 437 | - $query = $this->dbConnection->getQueryBuilder(); |
|
| 438 | - $query->insert('federated_reshares') |
|
| 439 | - ->values( |
|
| 440 | - [ |
|
| 441 | - 'share_id' => $query->createNamedParameter($shareId), |
|
| 442 | - 'remote_id' => $query->createNamedParameter($remoteId), |
|
| 443 | - ] |
|
| 444 | - ); |
|
| 445 | - $query->execute(); |
|
| 446 | - } |
|
| 447 | - |
|
| 448 | - /** |
|
| 449 | - * get share ID on remote server for federated re-shares |
|
| 450 | - * |
|
| 451 | - * @param IShare $share |
|
| 452 | - * @return int |
|
| 453 | - * @throws ShareNotFound |
|
| 454 | - */ |
|
| 455 | - public function getRemoteId(IShare $share) { |
|
| 456 | - $query = $this->dbConnection->getQueryBuilder(); |
|
| 457 | - $query->select('remote_id')->from('federated_reshares') |
|
| 458 | - ->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId()))); |
|
| 459 | - $data = $query->execute()->fetch(); |
|
| 460 | - |
|
| 461 | - if (!is_array($data) || !isset($data['remote_id'])) { |
|
| 462 | - throw new ShareNotFound(); |
|
| 463 | - } |
|
| 464 | - |
|
| 465 | - return (int)$data['remote_id']; |
|
| 466 | - } |
|
| 467 | - |
|
| 468 | - /** |
|
| 469 | - * @inheritdoc |
|
| 470 | - */ |
|
| 471 | - public function move(IShare $share, $recipient) { |
|
| 472 | - /* |
|
| 381 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 382 | + $qb->update('share') |
|
| 383 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) |
|
| 384 | + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) |
|
| 385 | + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) |
|
| 386 | + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) |
|
| 387 | + ->execute(); |
|
| 388 | + |
|
| 389 | + // send the updated permission to the owner/initiator, if they are not the same |
|
| 390 | + if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 391 | + $this->sendPermissionUpdate($share); |
|
| 392 | + } |
|
| 393 | + |
|
| 394 | + return $share; |
|
| 395 | + } |
|
| 396 | + |
|
| 397 | + /** |
|
| 398 | + * send the updated permission to the owner/initiator, if they are not the same |
|
| 399 | + * |
|
| 400 | + * @param IShare $share |
|
| 401 | + * @throws ShareNotFound |
|
| 402 | + * @throws \OC\HintException |
|
| 403 | + */ |
|
| 404 | + protected function sendPermissionUpdate(IShare $share) { |
|
| 405 | + $remoteId = $this->getRemoteId($share); |
|
| 406 | + // if the local user is the owner we send the permission change to the initiator |
|
| 407 | + if ($this->userManager->userExists($share->getShareOwner())) { |
|
| 408 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 409 | + } else { // ... if not we send the permission change to the owner |
|
| 410 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner()); |
|
| 411 | + } |
|
| 412 | + $this->notifications->sendPermissionChange($remote, $remoteId, $share->getToken(), $share->getPermissions()); |
|
| 413 | + } |
|
| 414 | + |
|
| 415 | + |
|
| 416 | + /** |
|
| 417 | + * update successful reShare with the correct token |
|
| 418 | + * |
|
| 419 | + * @param int $shareId |
|
| 420 | + * @param string $token |
|
| 421 | + */ |
|
| 422 | + protected function updateSuccessfulReShare($shareId, $token) { |
|
| 423 | + $query = $this->dbConnection->getQueryBuilder(); |
|
| 424 | + $query->update('share') |
|
| 425 | + ->where($query->expr()->eq('id', $query->createNamedParameter($shareId))) |
|
| 426 | + ->set('token', $query->createNamedParameter($token)) |
|
| 427 | + ->execute(); |
|
| 428 | + } |
|
| 429 | + |
|
| 430 | + /** |
|
| 431 | + * store remote ID in federated reShare table |
|
| 432 | + * |
|
| 433 | + * @param $shareId |
|
| 434 | + * @param $remoteId |
|
| 435 | + */ |
|
| 436 | + public function storeRemoteId($shareId, $remoteId) { |
|
| 437 | + $query = $this->dbConnection->getQueryBuilder(); |
|
| 438 | + $query->insert('federated_reshares') |
|
| 439 | + ->values( |
|
| 440 | + [ |
|
| 441 | + 'share_id' => $query->createNamedParameter($shareId), |
|
| 442 | + 'remote_id' => $query->createNamedParameter($remoteId), |
|
| 443 | + ] |
|
| 444 | + ); |
|
| 445 | + $query->execute(); |
|
| 446 | + } |
|
| 447 | + |
|
| 448 | + /** |
|
| 449 | + * get share ID on remote server for federated re-shares |
|
| 450 | + * |
|
| 451 | + * @param IShare $share |
|
| 452 | + * @return int |
|
| 453 | + * @throws ShareNotFound |
|
| 454 | + */ |
|
| 455 | + public function getRemoteId(IShare $share) { |
|
| 456 | + $query = $this->dbConnection->getQueryBuilder(); |
|
| 457 | + $query->select('remote_id')->from('federated_reshares') |
|
| 458 | + ->where($query->expr()->eq('share_id', $query->createNamedParameter((int)$share->getId()))); |
|
| 459 | + $data = $query->execute()->fetch(); |
|
| 460 | + |
|
| 461 | + if (!is_array($data) || !isset($data['remote_id'])) { |
|
| 462 | + throw new ShareNotFound(); |
|
| 463 | + } |
|
| 464 | + |
|
| 465 | + return (int)$data['remote_id']; |
|
| 466 | + } |
|
| 467 | + |
|
| 468 | + /** |
|
| 469 | + * @inheritdoc |
|
| 470 | + */ |
|
| 471 | + public function move(IShare $share, $recipient) { |
|
| 472 | + /* |
|
| 473 | 473 | * This function does nothing yet as it is just for outgoing |
| 474 | 474 | * federated shares. |
| 475 | 475 | */ |
| 476 | - return $share; |
|
| 477 | - } |
|
| 478 | - |
|
| 479 | - /** |
|
| 480 | - * Get all children of this share |
|
| 481 | - * |
|
| 482 | - * @param IShare $parent |
|
| 483 | - * @return IShare[] |
|
| 484 | - */ |
|
| 485 | - public function getChildren(IShare $parent) { |
|
| 486 | - $children = []; |
|
| 487 | - |
|
| 488 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 489 | - $qb->select('*') |
|
| 490 | - ->from('share') |
|
| 491 | - ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) |
|
| 492 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) |
|
| 493 | - ->orderBy('id'); |
|
| 494 | - |
|
| 495 | - $cursor = $qb->execute(); |
|
| 496 | - while($data = $cursor->fetch()) { |
|
| 497 | - $children[] = $this->createShareObject($data); |
|
| 498 | - } |
|
| 499 | - $cursor->closeCursor(); |
|
| 500 | - |
|
| 501 | - return $children; |
|
| 502 | - } |
|
| 503 | - |
|
| 504 | - /** |
|
| 505 | - * Delete a share (owner unShares the file) |
|
| 506 | - * |
|
| 507 | - * @param IShare $share |
|
| 508 | - */ |
|
| 509 | - public function delete(IShare $share) { |
|
| 510 | - |
|
| 511 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith()); |
|
| 512 | - |
|
| 513 | - $isOwner = false; |
|
| 514 | - |
|
| 515 | - $this->removeShareFromTable($share); |
|
| 516 | - |
|
| 517 | - // if the local user is the owner we can send the unShare request directly... |
|
| 518 | - if ($this->userManager->userExists($share->getShareOwner())) { |
|
| 519 | - $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken()); |
|
| 520 | - $this->revokeShare($share, true); |
|
| 521 | - $isOwner = true; |
|
| 522 | - } else { // ... if not we need to correct ID for the unShare request |
|
| 523 | - $remoteId = $this->getRemoteId($share); |
|
| 524 | - $this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken()); |
|
| 525 | - $this->revokeShare($share, false); |
|
| 526 | - } |
|
| 527 | - |
|
| 528 | - // send revoke notification to the other user, if initiator and owner are not the same user |
|
| 529 | - if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 530 | - $remoteId = $this->getRemoteId($share); |
|
| 531 | - if ($isOwner) { |
|
| 532 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 533 | - } else { |
|
| 534 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner()); |
|
| 535 | - } |
|
| 536 | - $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken()); |
|
| 537 | - } |
|
| 538 | - } |
|
| 539 | - |
|
| 540 | - /** |
|
| 541 | - * in case of a re-share we need to send the other use (initiator or owner) |
|
| 542 | - * a message that the file was unshared |
|
| 543 | - * |
|
| 544 | - * @param IShare $share |
|
| 545 | - * @param bool $isOwner the user can either be the owner or the user who re-sahred it |
|
| 546 | - * @throws ShareNotFound |
|
| 547 | - * @throws \OC\HintException |
|
| 548 | - */ |
|
| 549 | - protected function revokeShare($share, $isOwner) { |
|
| 550 | - // also send a unShare request to the initiator, if this is a different user than the owner |
|
| 551 | - if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 552 | - if ($isOwner) { |
|
| 553 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 554 | - } else { |
|
| 555 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner()); |
|
| 556 | - } |
|
| 557 | - $remoteId = $this->getRemoteId($share); |
|
| 558 | - $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken()); |
|
| 559 | - } |
|
| 560 | - } |
|
| 561 | - |
|
| 562 | - /** |
|
| 563 | - * remove share from table |
|
| 564 | - * |
|
| 565 | - * @param IShare $share |
|
| 566 | - */ |
|
| 567 | - public function removeShareFromTable(IShare $share) { |
|
| 568 | - $this->removeShareFromTableById($share->getId()); |
|
| 569 | - } |
|
| 570 | - |
|
| 571 | - /** |
|
| 572 | - * remove share from table |
|
| 573 | - * |
|
| 574 | - * @param string $shareId |
|
| 575 | - */ |
|
| 576 | - private function removeShareFromTableById($shareId) { |
|
| 577 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 578 | - $qb->delete('share') |
|
| 579 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); |
|
| 580 | - $qb->execute(); |
|
| 581 | - |
|
| 582 | - $qb->delete('federated_reshares') |
|
| 583 | - ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId))); |
|
| 584 | - $qb->execute(); |
|
| 585 | - } |
|
| 586 | - |
|
| 587 | - /** |
|
| 588 | - * @inheritdoc |
|
| 589 | - */ |
|
| 590 | - public function deleteFromSelf(IShare $share, $recipient) { |
|
| 591 | - // nothing to do here. Technically deleteFromSelf in the context of federated |
|
| 592 | - // shares is a umount of a external storage. This is handled here |
|
| 593 | - // apps/files_sharing/lib/external/manager.php |
|
| 594 | - // TODO move this code over to this app |
|
| 595 | - } |
|
| 596 | - |
|
| 597 | - |
|
| 598 | - public function getSharesInFolder($userId, Folder $node, $reshares) { |
|
| 599 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 600 | - $qb->select('*') |
|
| 601 | - ->from('share', 's') |
|
| 602 | - ->andWhere($qb->expr()->orX( |
|
| 603 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 604 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 605 | - )) |
|
| 606 | - ->andWhere( |
|
| 607 | - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)) |
|
| 608 | - ); |
|
| 609 | - |
|
| 610 | - /** |
|
| 611 | - * Reshares for this user are shares where they are the owner. |
|
| 612 | - */ |
|
| 613 | - if ($reshares === false) { |
|
| 614 | - $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); |
|
| 615 | - } else { |
|
| 616 | - $qb->andWhere( |
|
| 617 | - $qb->expr()->orX( |
|
| 618 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 619 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 620 | - ) |
|
| 621 | - ); |
|
| 622 | - } |
|
| 623 | - |
|
| 624 | - $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); |
|
| 625 | - $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); |
|
| 626 | - |
|
| 627 | - $qb->orderBy('id'); |
|
| 628 | - |
|
| 629 | - $cursor = $qb->execute(); |
|
| 630 | - $shares = []; |
|
| 631 | - while ($data = $cursor->fetch()) { |
|
| 632 | - $shares[$data['fileid']][] = $this->createShareObject($data); |
|
| 633 | - } |
|
| 634 | - $cursor->closeCursor(); |
|
| 635 | - |
|
| 636 | - return $shares; |
|
| 637 | - } |
|
| 638 | - |
|
| 639 | - /** |
|
| 640 | - * @inheritdoc |
|
| 641 | - */ |
|
| 642 | - public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { |
|
| 643 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 644 | - $qb->select('*') |
|
| 645 | - ->from('share'); |
|
| 646 | - |
|
| 647 | - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); |
|
| 648 | - |
|
| 649 | - /** |
|
| 650 | - * Reshares for this user are shares where they are the owner. |
|
| 651 | - */ |
|
| 652 | - if ($reshares === false) { |
|
| 653 | - //Special case for old shares created via the web UI |
|
| 654 | - $or1 = $qb->expr()->andX( |
|
| 655 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 656 | - $qb->expr()->isNull('uid_initiator') |
|
| 657 | - ); |
|
| 658 | - |
|
| 659 | - $qb->andWhere( |
|
| 660 | - $qb->expr()->orX( |
|
| 661 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), |
|
| 662 | - $or1 |
|
| 663 | - ) |
|
| 664 | - ); |
|
| 665 | - } else { |
|
| 666 | - $qb->andWhere( |
|
| 667 | - $qb->expr()->orX( |
|
| 668 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 669 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 670 | - ) |
|
| 671 | - ); |
|
| 672 | - } |
|
| 673 | - |
|
| 674 | - if ($node !== null) { |
|
| 675 | - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 676 | - } |
|
| 677 | - |
|
| 678 | - if ($limit !== -1) { |
|
| 679 | - $qb->setMaxResults($limit); |
|
| 680 | - } |
|
| 681 | - |
|
| 682 | - $qb->setFirstResult($offset); |
|
| 683 | - $qb->orderBy('id'); |
|
| 684 | - |
|
| 685 | - $cursor = $qb->execute(); |
|
| 686 | - $shares = []; |
|
| 687 | - while($data = $cursor->fetch()) { |
|
| 688 | - $shares[] = $this->createShareObject($data); |
|
| 689 | - } |
|
| 690 | - $cursor->closeCursor(); |
|
| 691 | - |
|
| 692 | - return $shares; |
|
| 693 | - } |
|
| 694 | - |
|
| 695 | - /** |
|
| 696 | - * @inheritdoc |
|
| 697 | - */ |
|
| 698 | - public function getShareById($id, $recipientId = null) { |
|
| 699 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 700 | - |
|
| 701 | - $qb->select('*') |
|
| 702 | - ->from('share') |
|
| 703 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) |
|
| 704 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); |
|
| 705 | - |
|
| 706 | - $cursor = $qb->execute(); |
|
| 707 | - $data = $cursor->fetch(); |
|
| 708 | - $cursor->closeCursor(); |
|
| 709 | - |
|
| 710 | - if ($data === false) { |
|
| 711 | - throw new ShareNotFound(); |
|
| 712 | - } |
|
| 713 | - |
|
| 714 | - try { |
|
| 715 | - $share = $this->createShareObject($data); |
|
| 716 | - } catch (InvalidShare $e) { |
|
| 717 | - throw new ShareNotFound(); |
|
| 718 | - } |
|
| 719 | - |
|
| 720 | - return $share; |
|
| 721 | - } |
|
| 722 | - |
|
| 723 | - /** |
|
| 724 | - * Get shares for a given path |
|
| 725 | - * |
|
| 726 | - * @param \OCP\Files\Node $path |
|
| 727 | - * @return IShare[] |
|
| 728 | - */ |
|
| 729 | - public function getSharesByPath(Node $path) { |
|
| 730 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 731 | - |
|
| 732 | - $cursor = $qb->select('*') |
|
| 733 | - ->from('share') |
|
| 734 | - ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) |
|
| 735 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) |
|
| 736 | - ->execute(); |
|
| 737 | - |
|
| 738 | - $shares = []; |
|
| 739 | - while($data = $cursor->fetch()) { |
|
| 740 | - $shares[] = $this->createShareObject($data); |
|
| 741 | - } |
|
| 742 | - $cursor->closeCursor(); |
|
| 743 | - |
|
| 744 | - return $shares; |
|
| 745 | - } |
|
| 746 | - |
|
| 747 | - /** |
|
| 748 | - * @inheritdoc |
|
| 749 | - */ |
|
| 750 | - public function getSharedWith($userId, $shareType, $node, $limit, $offset) { |
|
| 751 | - /** @var IShare[] $shares */ |
|
| 752 | - $shares = []; |
|
| 753 | - |
|
| 754 | - //Get shares directly with this user |
|
| 755 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 756 | - $qb->select('*') |
|
| 757 | - ->from('share'); |
|
| 758 | - |
|
| 759 | - // Order by id |
|
| 760 | - $qb->orderBy('id'); |
|
| 761 | - |
|
| 762 | - // Set limit and offset |
|
| 763 | - if ($limit !== -1) { |
|
| 764 | - $qb->setMaxResults($limit); |
|
| 765 | - } |
|
| 766 | - $qb->setFirstResult($offset); |
|
| 767 | - |
|
| 768 | - $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); |
|
| 769 | - $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); |
|
| 770 | - |
|
| 771 | - // Filter by node if provided |
|
| 772 | - if ($node !== null) { |
|
| 773 | - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 774 | - } |
|
| 775 | - |
|
| 776 | - $cursor = $qb->execute(); |
|
| 777 | - |
|
| 778 | - while($data = $cursor->fetch()) { |
|
| 779 | - $shares[] = $this->createShareObject($data); |
|
| 780 | - } |
|
| 781 | - $cursor->closeCursor(); |
|
| 782 | - |
|
| 783 | - |
|
| 784 | - return $shares; |
|
| 785 | - } |
|
| 786 | - |
|
| 787 | - /** |
|
| 788 | - * Get a share by token |
|
| 789 | - * |
|
| 790 | - * @param string $token |
|
| 791 | - * @return IShare |
|
| 792 | - * @throws ShareNotFound |
|
| 793 | - */ |
|
| 794 | - public function getShareByToken($token) { |
|
| 795 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 796 | - |
|
| 797 | - $cursor = $qb->select('*') |
|
| 798 | - ->from('share') |
|
| 799 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) |
|
| 800 | - ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) |
|
| 801 | - ->execute(); |
|
| 802 | - |
|
| 803 | - $data = $cursor->fetch(); |
|
| 804 | - |
|
| 805 | - if ($data === false) { |
|
| 806 | - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 807 | - } |
|
| 808 | - |
|
| 809 | - try { |
|
| 810 | - $share = $this->createShareObject($data); |
|
| 811 | - } catch (InvalidShare $e) { |
|
| 812 | - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 813 | - } |
|
| 814 | - |
|
| 815 | - return $share; |
|
| 816 | - } |
|
| 817 | - |
|
| 818 | - /** |
|
| 819 | - * get database row of a give share |
|
| 820 | - * |
|
| 821 | - * @param $id |
|
| 822 | - * @return array |
|
| 823 | - * @throws ShareNotFound |
|
| 824 | - */ |
|
| 825 | - private function getRawShare($id) { |
|
| 826 | - |
|
| 827 | - // Now fetch the inserted share and create a complete share object |
|
| 828 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 829 | - $qb->select('*') |
|
| 830 | - ->from('share') |
|
| 831 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); |
|
| 832 | - |
|
| 833 | - $cursor = $qb->execute(); |
|
| 834 | - $data = $cursor->fetch(); |
|
| 835 | - $cursor->closeCursor(); |
|
| 836 | - |
|
| 837 | - if ($data === false) { |
|
| 838 | - throw new ShareNotFound; |
|
| 839 | - } |
|
| 840 | - |
|
| 841 | - return $data; |
|
| 842 | - } |
|
| 843 | - |
|
| 844 | - /** |
|
| 845 | - * Create a share object from an database row |
|
| 846 | - * |
|
| 847 | - * @param array $data |
|
| 848 | - * @return IShare |
|
| 849 | - * @throws InvalidShare |
|
| 850 | - * @throws ShareNotFound |
|
| 851 | - */ |
|
| 852 | - private function createShareObject($data) { |
|
| 853 | - |
|
| 854 | - $share = new Share($this->rootFolder, $this->userManager); |
|
| 855 | - $share->setId((int)$data['id']) |
|
| 856 | - ->setShareType((int)$data['share_type']) |
|
| 857 | - ->setPermissions((int)$data['permissions']) |
|
| 858 | - ->setTarget($data['file_target']) |
|
| 859 | - ->setMailSend((bool)$data['mail_send']) |
|
| 860 | - ->setToken($data['token']); |
|
| 861 | - |
|
| 862 | - $shareTime = new \DateTime(); |
|
| 863 | - $shareTime->setTimestamp((int)$data['stime']); |
|
| 864 | - $share->setShareTime($shareTime); |
|
| 865 | - $share->setSharedWith($data['share_with']); |
|
| 866 | - |
|
| 867 | - if ($data['uid_initiator'] !== null) { |
|
| 868 | - $share->setShareOwner($data['uid_owner']); |
|
| 869 | - $share->setSharedBy($data['uid_initiator']); |
|
| 870 | - } else { |
|
| 871 | - //OLD SHARE |
|
| 872 | - $share->setSharedBy($data['uid_owner']); |
|
| 873 | - $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); |
|
| 874 | - |
|
| 875 | - $owner = $path->getOwner(); |
|
| 876 | - $share->setShareOwner($owner->getUID()); |
|
| 877 | - } |
|
| 878 | - |
|
| 879 | - $share->setNodeId((int)$data['file_source']); |
|
| 880 | - $share->setNodeType($data['item_type']); |
|
| 881 | - |
|
| 882 | - $share->setProviderId($this->identifier()); |
|
| 883 | - |
|
| 884 | - return $share; |
|
| 885 | - } |
|
| 886 | - |
|
| 887 | - /** |
|
| 888 | - * Get the node with file $id for $user |
|
| 889 | - * |
|
| 890 | - * @param string $userId |
|
| 891 | - * @param int $id |
|
| 892 | - * @return \OCP\Files\File|\OCP\Files\Folder |
|
| 893 | - * @throws InvalidShare |
|
| 894 | - */ |
|
| 895 | - private function getNode($userId, $id) { |
|
| 896 | - try { |
|
| 897 | - $userFolder = $this->rootFolder->getUserFolder($userId); |
|
| 898 | - } catch (NotFoundException $e) { |
|
| 899 | - throw new InvalidShare(); |
|
| 900 | - } |
|
| 901 | - |
|
| 902 | - $nodes = $userFolder->getById($id); |
|
| 903 | - |
|
| 904 | - if (empty($nodes)) { |
|
| 905 | - throw new InvalidShare(); |
|
| 906 | - } |
|
| 907 | - |
|
| 908 | - return $nodes[0]; |
|
| 909 | - } |
|
| 910 | - |
|
| 911 | - /** |
|
| 912 | - * A user is deleted from the system |
|
| 913 | - * So clean up the relevant shares. |
|
| 914 | - * |
|
| 915 | - * @param string $uid |
|
| 916 | - * @param int $shareType |
|
| 917 | - */ |
|
| 918 | - public function userDeleted($uid, $shareType) { |
|
| 919 | - //TODO: probabaly a good idea to send unshare info to remote servers |
|
| 920 | - |
|
| 921 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 922 | - |
|
| 923 | - $qb->delete('share') |
|
| 924 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))) |
|
| 925 | - ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) |
|
| 926 | - ->execute(); |
|
| 927 | - } |
|
| 928 | - |
|
| 929 | - /** |
|
| 930 | - * This provider does not handle groups |
|
| 931 | - * |
|
| 932 | - * @param string $gid |
|
| 933 | - */ |
|
| 934 | - public function groupDeleted($gid) { |
|
| 935 | - // We don't handle groups here |
|
| 936 | - } |
|
| 937 | - |
|
| 938 | - /** |
|
| 939 | - * This provider does not handle groups |
|
| 940 | - * |
|
| 941 | - * @param string $uid |
|
| 942 | - * @param string $gid |
|
| 943 | - */ |
|
| 944 | - public function userDeletedFromGroup($uid, $gid) { |
|
| 945 | - // We don't handle groups here |
|
| 946 | - } |
|
| 947 | - |
|
| 948 | - /** |
|
| 949 | - * check if users from other Nextcloud instances are allowed to mount public links share by this instance |
|
| 950 | - * |
|
| 951 | - * @return bool |
|
| 952 | - */ |
|
| 953 | - public function isOutgoingServer2serverShareEnabled() { |
|
| 954 | - if ($this->gsConfig->onlyInternalFederation()) { |
|
| 955 | - return false; |
|
| 956 | - } |
|
| 957 | - $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes'); |
|
| 958 | - return ($result === 'yes'); |
|
| 959 | - } |
|
| 960 | - |
|
| 961 | - /** |
|
| 962 | - * check if users are allowed to mount public links from other Nextclouds |
|
| 963 | - * |
|
| 964 | - * @return bool |
|
| 965 | - */ |
|
| 966 | - public function isIncomingServer2serverShareEnabled() { |
|
| 967 | - if ($this->gsConfig->onlyInternalFederation()) { |
|
| 968 | - return false; |
|
| 969 | - } |
|
| 970 | - $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes'); |
|
| 971 | - return ($result === 'yes'); |
|
| 972 | - } |
|
| 973 | - |
|
| 974 | - /** |
|
| 975 | - * Check if querying sharees on the lookup server is enabled |
|
| 976 | - * |
|
| 977 | - * @return bool |
|
| 978 | - */ |
|
| 979 | - public function isLookupServerQueriesEnabled() { |
|
| 980 | - // in a global scale setup we should always query the lookup server |
|
| 981 | - if ($this->gsConfig->isGlobalScaleEnabled()) { |
|
| 982 | - return true; |
|
| 983 | - } |
|
| 984 | - $result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no'); |
|
| 985 | - return ($result === 'yes'); |
|
| 986 | - } |
|
| 987 | - |
|
| 988 | - |
|
| 989 | - /** |
|
| 990 | - * Check if it is allowed to publish user specific data to the lookup server |
|
| 991 | - * |
|
| 992 | - * @return bool |
|
| 993 | - */ |
|
| 994 | - public function isLookupServerUploadEnabled() { |
|
| 995 | - // in a global scale setup the admin is responsible to keep the lookup server up-to-date |
|
| 996 | - if ($this->gsConfig->isGlobalScaleEnabled()) { |
|
| 997 | - return false; |
|
| 998 | - } |
|
| 999 | - $result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes'); |
|
| 1000 | - return ($result === 'yes'); |
|
| 1001 | - } |
|
| 1002 | - |
|
| 1003 | - /** |
|
| 1004 | - * @inheritdoc |
|
| 1005 | - */ |
|
| 1006 | - public function getAccessList($nodes, $currentAccess) { |
|
| 1007 | - $ids = []; |
|
| 1008 | - foreach ($nodes as $node) { |
|
| 1009 | - $ids[] = $node->getId(); |
|
| 1010 | - } |
|
| 1011 | - |
|
| 1012 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1013 | - $qb->select('share_with', 'token', 'file_source') |
|
| 1014 | - ->from('share') |
|
| 1015 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))) |
|
| 1016 | - ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) |
|
| 1017 | - ->andWhere($qb->expr()->orX( |
|
| 1018 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1019 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1020 | - )); |
|
| 1021 | - $cursor = $qb->execute(); |
|
| 1022 | - |
|
| 1023 | - if ($currentAccess === false) { |
|
| 1024 | - $remote = $cursor->fetch() !== false; |
|
| 1025 | - $cursor->closeCursor(); |
|
| 1026 | - |
|
| 1027 | - return ['remote' => $remote]; |
|
| 1028 | - } |
|
| 1029 | - |
|
| 1030 | - $remote = []; |
|
| 1031 | - while ($row = $cursor->fetch()) { |
|
| 1032 | - $remote[$row['share_with']] = [ |
|
| 1033 | - 'node_id' => $row['file_source'], |
|
| 1034 | - 'token' => $row['token'], |
|
| 1035 | - ]; |
|
| 1036 | - } |
|
| 1037 | - $cursor->closeCursor(); |
|
| 1038 | - |
|
| 1039 | - return ['remote' => $remote]; |
|
| 1040 | - } |
|
| 476 | + return $share; |
|
| 477 | + } |
|
| 478 | + |
|
| 479 | + /** |
|
| 480 | + * Get all children of this share |
|
| 481 | + * |
|
| 482 | + * @param IShare $parent |
|
| 483 | + * @return IShare[] |
|
| 484 | + */ |
|
| 485 | + public function getChildren(IShare $parent) { |
|
| 486 | + $children = []; |
|
| 487 | + |
|
| 488 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 489 | + $qb->select('*') |
|
| 490 | + ->from('share') |
|
| 491 | + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) |
|
| 492 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) |
|
| 493 | + ->orderBy('id'); |
|
| 494 | + |
|
| 495 | + $cursor = $qb->execute(); |
|
| 496 | + while($data = $cursor->fetch()) { |
|
| 497 | + $children[] = $this->createShareObject($data); |
|
| 498 | + } |
|
| 499 | + $cursor->closeCursor(); |
|
| 500 | + |
|
| 501 | + return $children; |
|
| 502 | + } |
|
| 503 | + |
|
| 504 | + /** |
|
| 505 | + * Delete a share (owner unShares the file) |
|
| 506 | + * |
|
| 507 | + * @param IShare $share |
|
| 508 | + */ |
|
| 509 | + public function delete(IShare $share) { |
|
| 510 | + |
|
| 511 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedWith()); |
|
| 512 | + |
|
| 513 | + $isOwner = false; |
|
| 514 | + |
|
| 515 | + $this->removeShareFromTable($share); |
|
| 516 | + |
|
| 517 | + // if the local user is the owner we can send the unShare request directly... |
|
| 518 | + if ($this->userManager->userExists($share->getShareOwner())) { |
|
| 519 | + $this->notifications->sendRemoteUnShare($remote, $share->getId(), $share->getToken()); |
|
| 520 | + $this->revokeShare($share, true); |
|
| 521 | + $isOwner = true; |
|
| 522 | + } else { // ... if not we need to correct ID for the unShare request |
|
| 523 | + $remoteId = $this->getRemoteId($share); |
|
| 524 | + $this->notifications->sendRemoteUnShare($remote, $remoteId, $share->getToken()); |
|
| 525 | + $this->revokeShare($share, false); |
|
| 526 | + } |
|
| 527 | + |
|
| 528 | + // send revoke notification to the other user, if initiator and owner are not the same user |
|
| 529 | + if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 530 | + $remoteId = $this->getRemoteId($share); |
|
| 531 | + if ($isOwner) { |
|
| 532 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 533 | + } else { |
|
| 534 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner()); |
|
| 535 | + } |
|
| 536 | + $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken()); |
|
| 537 | + } |
|
| 538 | + } |
|
| 539 | + |
|
| 540 | + /** |
|
| 541 | + * in case of a re-share we need to send the other use (initiator or owner) |
|
| 542 | + * a message that the file was unshared |
|
| 543 | + * |
|
| 544 | + * @param IShare $share |
|
| 545 | + * @param bool $isOwner the user can either be the owner or the user who re-sahred it |
|
| 546 | + * @throws ShareNotFound |
|
| 547 | + * @throws \OC\HintException |
|
| 548 | + */ |
|
| 549 | + protected function revokeShare($share, $isOwner) { |
|
| 550 | + // also send a unShare request to the initiator, if this is a different user than the owner |
|
| 551 | + if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 552 | + if ($isOwner) { |
|
| 553 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 554 | + } else { |
|
| 555 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getShareOwner()); |
|
| 556 | + } |
|
| 557 | + $remoteId = $this->getRemoteId($share); |
|
| 558 | + $this->notifications->sendRevokeShare($remote, $remoteId, $share->getToken()); |
|
| 559 | + } |
|
| 560 | + } |
|
| 561 | + |
|
| 562 | + /** |
|
| 563 | + * remove share from table |
|
| 564 | + * |
|
| 565 | + * @param IShare $share |
|
| 566 | + */ |
|
| 567 | + public function removeShareFromTable(IShare $share) { |
|
| 568 | + $this->removeShareFromTableById($share->getId()); |
|
| 569 | + } |
|
| 570 | + |
|
| 571 | + /** |
|
| 572 | + * remove share from table |
|
| 573 | + * |
|
| 574 | + * @param string $shareId |
|
| 575 | + */ |
|
| 576 | + private function removeShareFromTableById($shareId) { |
|
| 577 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 578 | + $qb->delete('share') |
|
| 579 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); |
|
| 580 | + $qb->execute(); |
|
| 581 | + |
|
| 582 | + $qb->delete('federated_reshares') |
|
| 583 | + ->where($qb->expr()->eq('share_id', $qb->createNamedParameter($shareId))); |
|
| 584 | + $qb->execute(); |
|
| 585 | + } |
|
| 586 | + |
|
| 587 | + /** |
|
| 588 | + * @inheritdoc |
|
| 589 | + */ |
|
| 590 | + public function deleteFromSelf(IShare $share, $recipient) { |
|
| 591 | + // nothing to do here. Technically deleteFromSelf in the context of federated |
|
| 592 | + // shares is a umount of a external storage. This is handled here |
|
| 593 | + // apps/files_sharing/lib/external/manager.php |
|
| 594 | + // TODO move this code over to this app |
|
| 595 | + } |
|
| 596 | + |
|
| 597 | + |
|
| 598 | + public function getSharesInFolder($userId, Folder $node, $reshares) { |
|
| 599 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 600 | + $qb->select('*') |
|
| 601 | + ->from('share', 's') |
|
| 602 | + ->andWhere($qb->expr()->orX( |
|
| 603 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 604 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 605 | + )) |
|
| 606 | + ->andWhere( |
|
| 607 | + $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE)) |
|
| 608 | + ); |
|
| 609 | + |
|
| 610 | + /** |
|
| 611 | + * Reshares for this user are shares where they are the owner. |
|
| 612 | + */ |
|
| 613 | + if ($reshares === false) { |
|
| 614 | + $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); |
|
| 615 | + } else { |
|
| 616 | + $qb->andWhere( |
|
| 617 | + $qb->expr()->orX( |
|
| 618 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 619 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 620 | + ) |
|
| 621 | + ); |
|
| 622 | + } |
|
| 623 | + |
|
| 624 | + $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); |
|
| 625 | + $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); |
|
| 626 | + |
|
| 627 | + $qb->orderBy('id'); |
|
| 628 | + |
|
| 629 | + $cursor = $qb->execute(); |
|
| 630 | + $shares = []; |
|
| 631 | + while ($data = $cursor->fetch()) { |
|
| 632 | + $shares[$data['fileid']][] = $this->createShareObject($data); |
|
| 633 | + } |
|
| 634 | + $cursor->closeCursor(); |
|
| 635 | + |
|
| 636 | + return $shares; |
|
| 637 | + } |
|
| 638 | + |
|
| 639 | + /** |
|
| 640 | + * @inheritdoc |
|
| 641 | + */ |
|
| 642 | + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { |
|
| 643 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 644 | + $qb->select('*') |
|
| 645 | + ->from('share'); |
|
| 646 | + |
|
| 647 | + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); |
|
| 648 | + |
|
| 649 | + /** |
|
| 650 | + * Reshares for this user are shares where they are the owner. |
|
| 651 | + */ |
|
| 652 | + if ($reshares === false) { |
|
| 653 | + //Special case for old shares created via the web UI |
|
| 654 | + $or1 = $qb->expr()->andX( |
|
| 655 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 656 | + $qb->expr()->isNull('uid_initiator') |
|
| 657 | + ); |
|
| 658 | + |
|
| 659 | + $qb->andWhere( |
|
| 660 | + $qb->expr()->orX( |
|
| 661 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), |
|
| 662 | + $or1 |
|
| 663 | + ) |
|
| 664 | + ); |
|
| 665 | + } else { |
|
| 666 | + $qb->andWhere( |
|
| 667 | + $qb->expr()->orX( |
|
| 668 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 669 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 670 | + ) |
|
| 671 | + ); |
|
| 672 | + } |
|
| 673 | + |
|
| 674 | + if ($node !== null) { |
|
| 675 | + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 676 | + } |
|
| 677 | + |
|
| 678 | + if ($limit !== -1) { |
|
| 679 | + $qb->setMaxResults($limit); |
|
| 680 | + } |
|
| 681 | + |
|
| 682 | + $qb->setFirstResult($offset); |
|
| 683 | + $qb->orderBy('id'); |
|
| 684 | + |
|
| 685 | + $cursor = $qb->execute(); |
|
| 686 | + $shares = []; |
|
| 687 | + while($data = $cursor->fetch()) { |
|
| 688 | + $shares[] = $this->createShareObject($data); |
|
| 689 | + } |
|
| 690 | + $cursor->closeCursor(); |
|
| 691 | + |
|
| 692 | + return $shares; |
|
| 693 | + } |
|
| 694 | + |
|
| 695 | + /** |
|
| 696 | + * @inheritdoc |
|
| 697 | + */ |
|
| 698 | + public function getShareById($id, $recipientId = null) { |
|
| 699 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 700 | + |
|
| 701 | + $qb->select('*') |
|
| 702 | + ->from('share') |
|
| 703 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) |
|
| 704 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); |
|
| 705 | + |
|
| 706 | + $cursor = $qb->execute(); |
|
| 707 | + $data = $cursor->fetch(); |
|
| 708 | + $cursor->closeCursor(); |
|
| 709 | + |
|
| 710 | + if ($data === false) { |
|
| 711 | + throw new ShareNotFound(); |
|
| 712 | + } |
|
| 713 | + |
|
| 714 | + try { |
|
| 715 | + $share = $this->createShareObject($data); |
|
| 716 | + } catch (InvalidShare $e) { |
|
| 717 | + throw new ShareNotFound(); |
|
| 718 | + } |
|
| 719 | + |
|
| 720 | + return $share; |
|
| 721 | + } |
|
| 722 | + |
|
| 723 | + /** |
|
| 724 | + * Get shares for a given path |
|
| 725 | + * |
|
| 726 | + * @param \OCP\Files\Node $path |
|
| 727 | + * @return IShare[] |
|
| 728 | + */ |
|
| 729 | + public function getSharesByPath(Node $path) { |
|
| 730 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 731 | + |
|
| 732 | + $cursor = $qb->select('*') |
|
| 733 | + ->from('share') |
|
| 734 | + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) |
|
| 735 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) |
|
| 736 | + ->execute(); |
|
| 737 | + |
|
| 738 | + $shares = []; |
|
| 739 | + while($data = $cursor->fetch()) { |
|
| 740 | + $shares[] = $this->createShareObject($data); |
|
| 741 | + } |
|
| 742 | + $cursor->closeCursor(); |
|
| 743 | + |
|
| 744 | + return $shares; |
|
| 745 | + } |
|
| 746 | + |
|
| 747 | + /** |
|
| 748 | + * @inheritdoc |
|
| 749 | + */ |
|
| 750 | + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { |
|
| 751 | + /** @var IShare[] $shares */ |
|
| 752 | + $shares = []; |
|
| 753 | + |
|
| 754 | + //Get shares directly with this user |
|
| 755 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 756 | + $qb->select('*') |
|
| 757 | + ->from('share'); |
|
| 758 | + |
|
| 759 | + // Order by id |
|
| 760 | + $qb->orderBy('id'); |
|
| 761 | + |
|
| 762 | + // Set limit and offset |
|
| 763 | + if ($limit !== -1) { |
|
| 764 | + $qb->setMaxResults($limit); |
|
| 765 | + } |
|
| 766 | + $qb->setFirstResult($offset); |
|
| 767 | + |
|
| 768 | + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))); |
|
| 769 | + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); |
|
| 770 | + |
|
| 771 | + // Filter by node if provided |
|
| 772 | + if ($node !== null) { |
|
| 773 | + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 774 | + } |
|
| 775 | + |
|
| 776 | + $cursor = $qb->execute(); |
|
| 777 | + |
|
| 778 | + while($data = $cursor->fetch()) { |
|
| 779 | + $shares[] = $this->createShareObject($data); |
|
| 780 | + } |
|
| 781 | + $cursor->closeCursor(); |
|
| 782 | + |
|
| 783 | + |
|
| 784 | + return $shares; |
|
| 785 | + } |
|
| 786 | + |
|
| 787 | + /** |
|
| 788 | + * Get a share by token |
|
| 789 | + * |
|
| 790 | + * @param string $token |
|
| 791 | + * @return IShare |
|
| 792 | + * @throws ShareNotFound |
|
| 793 | + */ |
|
| 794 | + public function getShareByToken($token) { |
|
| 795 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 796 | + |
|
| 797 | + $cursor = $qb->select('*') |
|
| 798 | + ->from('share') |
|
| 799 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_REMOTE))) |
|
| 800 | + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) |
|
| 801 | + ->execute(); |
|
| 802 | + |
|
| 803 | + $data = $cursor->fetch(); |
|
| 804 | + |
|
| 805 | + if ($data === false) { |
|
| 806 | + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 807 | + } |
|
| 808 | + |
|
| 809 | + try { |
|
| 810 | + $share = $this->createShareObject($data); |
|
| 811 | + } catch (InvalidShare $e) { |
|
| 812 | + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 813 | + } |
|
| 814 | + |
|
| 815 | + return $share; |
|
| 816 | + } |
|
| 817 | + |
|
| 818 | + /** |
|
| 819 | + * get database row of a give share |
|
| 820 | + * |
|
| 821 | + * @param $id |
|
| 822 | + * @return array |
|
| 823 | + * @throws ShareNotFound |
|
| 824 | + */ |
|
| 825 | + private function getRawShare($id) { |
|
| 826 | + |
|
| 827 | + // Now fetch the inserted share and create a complete share object |
|
| 828 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 829 | + $qb->select('*') |
|
| 830 | + ->from('share') |
|
| 831 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); |
|
| 832 | + |
|
| 833 | + $cursor = $qb->execute(); |
|
| 834 | + $data = $cursor->fetch(); |
|
| 835 | + $cursor->closeCursor(); |
|
| 836 | + |
|
| 837 | + if ($data === false) { |
|
| 838 | + throw new ShareNotFound; |
|
| 839 | + } |
|
| 840 | + |
|
| 841 | + return $data; |
|
| 842 | + } |
|
| 843 | + |
|
| 844 | + /** |
|
| 845 | + * Create a share object from an database row |
|
| 846 | + * |
|
| 847 | + * @param array $data |
|
| 848 | + * @return IShare |
|
| 849 | + * @throws InvalidShare |
|
| 850 | + * @throws ShareNotFound |
|
| 851 | + */ |
|
| 852 | + private function createShareObject($data) { |
|
| 853 | + |
|
| 854 | + $share = new Share($this->rootFolder, $this->userManager); |
|
| 855 | + $share->setId((int)$data['id']) |
|
| 856 | + ->setShareType((int)$data['share_type']) |
|
| 857 | + ->setPermissions((int)$data['permissions']) |
|
| 858 | + ->setTarget($data['file_target']) |
|
| 859 | + ->setMailSend((bool)$data['mail_send']) |
|
| 860 | + ->setToken($data['token']); |
|
| 861 | + |
|
| 862 | + $shareTime = new \DateTime(); |
|
| 863 | + $shareTime->setTimestamp((int)$data['stime']); |
|
| 864 | + $share->setShareTime($shareTime); |
|
| 865 | + $share->setSharedWith($data['share_with']); |
|
| 866 | + |
|
| 867 | + if ($data['uid_initiator'] !== null) { |
|
| 868 | + $share->setShareOwner($data['uid_owner']); |
|
| 869 | + $share->setSharedBy($data['uid_initiator']); |
|
| 870 | + } else { |
|
| 871 | + //OLD SHARE |
|
| 872 | + $share->setSharedBy($data['uid_owner']); |
|
| 873 | + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); |
|
| 874 | + |
|
| 875 | + $owner = $path->getOwner(); |
|
| 876 | + $share->setShareOwner($owner->getUID()); |
|
| 877 | + } |
|
| 878 | + |
|
| 879 | + $share->setNodeId((int)$data['file_source']); |
|
| 880 | + $share->setNodeType($data['item_type']); |
|
| 881 | + |
|
| 882 | + $share->setProviderId($this->identifier()); |
|
| 883 | + |
|
| 884 | + return $share; |
|
| 885 | + } |
|
| 886 | + |
|
| 887 | + /** |
|
| 888 | + * Get the node with file $id for $user |
|
| 889 | + * |
|
| 890 | + * @param string $userId |
|
| 891 | + * @param int $id |
|
| 892 | + * @return \OCP\Files\File|\OCP\Files\Folder |
|
| 893 | + * @throws InvalidShare |
|
| 894 | + */ |
|
| 895 | + private function getNode($userId, $id) { |
|
| 896 | + try { |
|
| 897 | + $userFolder = $this->rootFolder->getUserFolder($userId); |
|
| 898 | + } catch (NotFoundException $e) { |
|
| 899 | + throw new InvalidShare(); |
|
| 900 | + } |
|
| 901 | + |
|
| 902 | + $nodes = $userFolder->getById($id); |
|
| 903 | + |
|
| 904 | + if (empty($nodes)) { |
|
| 905 | + throw new InvalidShare(); |
|
| 906 | + } |
|
| 907 | + |
|
| 908 | + return $nodes[0]; |
|
| 909 | + } |
|
| 910 | + |
|
| 911 | + /** |
|
| 912 | + * A user is deleted from the system |
|
| 913 | + * So clean up the relevant shares. |
|
| 914 | + * |
|
| 915 | + * @param string $uid |
|
| 916 | + * @param int $shareType |
|
| 917 | + */ |
|
| 918 | + public function userDeleted($uid, $shareType) { |
|
| 919 | + //TODO: probabaly a good idea to send unshare info to remote servers |
|
| 920 | + |
|
| 921 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 922 | + |
|
| 923 | + $qb->delete('share') |
|
| 924 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))) |
|
| 925 | + ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) |
|
| 926 | + ->execute(); |
|
| 927 | + } |
|
| 928 | + |
|
| 929 | + /** |
|
| 930 | + * This provider does not handle groups |
|
| 931 | + * |
|
| 932 | + * @param string $gid |
|
| 933 | + */ |
|
| 934 | + public function groupDeleted($gid) { |
|
| 935 | + // We don't handle groups here |
|
| 936 | + } |
|
| 937 | + |
|
| 938 | + /** |
|
| 939 | + * This provider does not handle groups |
|
| 940 | + * |
|
| 941 | + * @param string $uid |
|
| 942 | + * @param string $gid |
|
| 943 | + */ |
|
| 944 | + public function userDeletedFromGroup($uid, $gid) { |
|
| 945 | + // We don't handle groups here |
|
| 946 | + } |
|
| 947 | + |
|
| 948 | + /** |
|
| 949 | + * check if users from other Nextcloud instances are allowed to mount public links share by this instance |
|
| 950 | + * |
|
| 951 | + * @return bool |
|
| 952 | + */ |
|
| 953 | + public function isOutgoingServer2serverShareEnabled() { |
|
| 954 | + if ($this->gsConfig->onlyInternalFederation()) { |
|
| 955 | + return false; |
|
| 956 | + } |
|
| 957 | + $result = $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes'); |
|
| 958 | + return ($result === 'yes'); |
|
| 959 | + } |
|
| 960 | + |
|
| 961 | + /** |
|
| 962 | + * check if users are allowed to mount public links from other Nextclouds |
|
| 963 | + * |
|
| 964 | + * @return bool |
|
| 965 | + */ |
|
| 966 | + public function isIncomingServer2serverShareEnabled() { |
|
| 967 | + if ($this->gsConfig->onlyInternalFederation()) { |
|
| 968 | + return false; |
|
| 969 | + } |
|
| 970 | + $result = $this->config->getAppValue('files_sharing', 'incoming_server2server_share_enabled', 'yes'); |
|
| 971 | + return ($result === 'yes'); |
|
| 972 | + } |
|
| 973 | + |
|
| 974 | + /** |
|
| 975 | + * Check if querying sharees on the lookup server is enabled |
|
| 976 | + * |
|
| 977 | + * @return bool |
|
| 978 | + */ |
|
| 979 | + public function isLookupServerQueriesEnabled() { |
|
| 980 | + // in a global scale setup we should always query the lookup server |
|
| 981 | + if ($this->gsConfig->isGlobalScaleEnabled()) { |
|
| 982 | + return true; |
|
| 983 | + } |
|
| 984 | + $result = $this->config->getAppValue('files_sharing', 'lookupServerEnabled', 'no'); |
|
| 985 | + return ($result === 'yes'); |
|
| 986 | + } |
|
| 987 | + |
|
| 988 | + |
|
| 989 | + /** |
|
| 990 | + * Check if it is allowed to publish user specific data to the lookup server |
|
| 991 | + * |
|
| 992 | + * @return bool |
|
| 993 | + */ |
|
| 994 | + public function isLookupServerUploadEnabled() { |
|
| 995 | + // in a global scale setup the admin is responsible to keep the lookup server up-to-date |
|
| 996 | + if ($this->gsConfig->isGlobalScaleEnabled()) { |
|
| 997 | + return false; |
|
| 998 | + } |
|
| 999 | + $result = $this->config->getAppValue('files_sharing', 'lookupServerUploadEnabled', 'yes'); |
|
| 1000 | + return ($result === 'yes'); |
|
| 1001 | + } |
|
| 1002 | + |
|
| 1003 | + /** |
|
| 1004 | + * @inheritdoc |
|
| 1005 | + */ |
|
| 1006 | + public function getAccessList($nodes, $currentAccess) { |
|
| 1007 | + $ids = []; |
|
| 1008 | + foreach ($nodes as $node) { |
|
| 1009 | + $ids[] = $node->getId(); |
|
| 1010 | + } |
|
| 1011 | + |
|
| 1012 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1013 | + $qb->select('share_with', 'token', 'file_source') |
|
| 1014 | + ->from('share') |
|
| 1015 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_REMOTE))) |
|
| 1016 | + ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) |
|
| 1017 | + ->andWhere($qb->expr()->orX( |
|
| 1018 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1019 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1020 | + )); |
|
| 1021 | + $cursor = $qb->execute(); |
|
| 1022 | + |
|
| 1023 | + if ($currentAccess === false) { |
|
| 1024 | + $remote = $cursor->fetch() !== false; |
|
| 1025 | + $cursor->closeCursor(); |
|
| 1026 | + |
|
| 1027 | + return ['remote' => $remote]; |
|
| 1028 | + } |
|
| 1029 | + |
|
| 1030 | + $remote = []; |
|
| 1031 | + while ($row = $cursor->fetch()) { |
|
| 1032 | + $remote[$row['share_with']] = [ |
|
| 1033 | + 'node_id' => $row['file_source'], |
|
| 1034 | + 'token' => $row['token'], |
|
| 1035 | + ]; |
|
| 1036 | + } |
|
| 1037 | + $cursor->closeCursor(); |
|
| 1038 | + |
|
| 1039 | + return ['remote' => $remote]; |
|
| 1040 | + } |
|
| 1041 | 1041 | } |
@@ -51,639 +51,639 @@ |
||
| 51 | 51 | |
| 52 | 52 | class RequestHandlerController extends OCSController { |
| 53 | 53 | |
| 54 | - /** @var FederatedShareProvider */ |
|
| 55 | - private $federatedShareProvider; |
|
| 56 | - |
|
| 57 | - /** @var IDBConnection */ |
|
| 58 | - private $connection; |
|
| 59 | - |
|
| 60 | - /** @var Share\IManager */ |
|
| 61 | - private $shareManager; |
|
| 62 | - |
|
| 63 | - /** @var Notifications */ |
|
| 64 | - private $notifications; |
|
| 65 | - |
|
| 66 | - /** @var AddressHandler */ |
|
| 67 | - private $addressHandler; |
|
| 68 | - |
|
| 69 | - /** @var IUserManager */ |
|
| 70 | - private $userManager; |
|
| 71 | - |
|
| 72 | - /** @var string */ |
|
| 73 | - private $shareTable = 'share'; |
|
| 74 | - |
|
| 75 | - /** @var ICloudIdManager */ |
|
| 76 | - private $cloudIdManager; |
|
| 77 | - |
|
| 78 | - /** @var ILogger */ |
|
| 79 | - private $logger; |
|
| 80 | - |
|
| 81 | - /** |
|
| 82 | - * Server2Server constructor. |
|
| 83 | - * |
|
| 84 | - * @param string $appName |
|
| 85 | - * @param IRequest $request |
|
| 86 | - * @param FederatedShareProvider $federatedShareProvider |
|
| 87 | - * @param IDBConnection $connection |
|
| 88 | - * @param Share\IManager $shareManager |
|
| 89 | - * @param Notifications $notifications |
|
| 90 | - * @param AddressHandler $addressHandler |
|
| 91 | - * @param IUserManager $userManager |
|
| 92 | - * @param ICloudIdManager $cloudIdManager |
|
| 93 | - */ |
|
| 94 | - public function __construct($appName, |
|
| 95 | - IRequest $request, |
|
| 96 | - FederatedShareProvider $federatedShareProvider, |
|
| 97 | - IDBConnection $connection, |
|
| 98 | - Share\IManager $shareManager, |
|
| 99 | - Notifications $notifications, |
|
| 100 | - AddressHandler $addressHandler, |
|
| 101 | - IUserManager $userManager, |
|
| 102 | - ICloudIdManager $cloudIdManager, |
|
| 103 | - ILogger $logger |
|
| 104 | - ) { |
|
| 105 | - parent::__construct($appName, $request); |
|
| 106 | - |
|
| 107 | - $this->federatedShareProvider = $federatedShareProvider; |
|
| 108 | - $this->connection = $connection; |
|
| 109 | - $this->shareManager = $shareManager; |
|
| 110 | - $this->notifications = $notifications; |
|
| 111 | - $this->addressHandler = $addressHandler; |
|
| 112 | - $this->userManager = $userManager; |
|
| 113 | - $this->cloudIdManager = $cloudIdManager; |
|
| 114 | - $this->logger = $logger; |
|
| 115 | - } |
|
| 116 | - |
|
| 117 | - /** |
|
| 118 | - * @NoCSRFRequired |
|
| 119 | - * @PublicPage |
|
| 120 | - * |
|
| 121 | - * create a new share |
|
| 122 | - * |
|
| 123 | - * @return Http\DataResponse |
|
| 124 | - * @throws OCSException |
|
| 125 | - */ |
|
| 126 | - public function createShare() { |
|
| 127 | - |
|
| 128 | - if (!$this->isS2SEnabled(true)) { |
|
| 129 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 130 | - } |
|
| 131 | - |
|
| 132 | - $remote = isset($_POST['remote']) ? $_POST['remote'] : null; |
|
| 133 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 134 | - $name = isset($_POST['name']) ? $_POST['name'] : null; |
|
| 135 | - $owner = isset($_POST['owner']) ? $_POST['owner'] : null; |
|
| 136 | - $sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null; |
|
| 137 | - $shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null; |
|
| 138 | - $remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null; |
|
| 139 | - $sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null; |
|
| 140 | - $ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null; |
|
| 141 | - |
|
| 142 | - if ($remote && $token && $name && $owner && $remoteId && $shareWith) { |
|
| 143 | - |
|
| 144 | - if (!\OCP\Util::isValidFileName($name)) { |
|
| 145 | - throw new OCSException('The mountpoint name contains invalid characters.', 400); |
|
| 146 | - } |
|
| 147 | - |
|
| 148 | - // FIXME this should be a method in the user management instead |
|
| 149 | - $this->logger->debug('shareWith before, ' . $shareWith, ['app' => 'files_sharing']); |
|
| 150 | - \OCP\Util::emitHook( |
|
| 151 | - '\OCA\Files_Sharing\API\Server2Server', |
|
| 152 | - 'preLoginNameUsedAsUserName', |
|
| 153 | - array('uid' => &$shareWith) |
|
| 154 | - ); |
|
| 155 | - $this->logger->debug('shareWith after, ' . $shareWith, ['app' => 'files_sharing']); |
|
| 156 | - |
|
| 157 | - if (!\OC::$server->getUserManager()->userExists($shareWith)) { |
|
| 158 | - throw new OCSException('User does not exists', 400); |
|
| 159 | - } |
|
| 160 | - |
|
| 161 | - \OC_Util::setupFS($shareWith); |
|
| 162 | - |
|
| 163 | - $externalManager = new \OCA\Files_Sharing\External\Manager( |
|
| 164 | - \OC::$server->getDatabaseConnection(), |
|
| 165 | - \OC\Files\Filesystem::getMountManager(), |
|
| 166 | - \OC\Files\Filesystem::getLoader(), |
|
| 167 | - \OC::$server->getHTTPClientService(), |
|
| 168 | - \OC::$server->getNotificationManager(), |
|
| 169 | - \OC::$server->query(\OCP\OCS\IDiscoveryService::class), |
|
| 170 | - $shareWith |
|
| 171 | - ); |
|
| 172 | - |
|
| 173 | - try { |
|
| 174 | - $externalManager->addShare($remote, $token, '', $name, $owner, false, $shareWith, $remoteId); |
|
| 175 | - $shareId = \OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*share_external'); |
|
| 176 | - |
|
| 177 | - if ($ownerFederatedId === null) { |
|
| 178 | - $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId(); |
|
| 179 | - } |
|
| 180 | - // if the owner of the share and the initiator are the same user |
|
| 181 | - // we also complete the federated share ID for the initiator |
|
| 182 | - if ($sharedByFederatedId === null && $owner === $sharedBy) { |
|
| 183 | - $sharedByFederatedId = $ownerFederatedId; |
|
| 184 | - } |
|
| 185 | - |
|
| 186 | - $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 187 | - $event->setApp('files_sharing') |
|
| 188 | - ->setType('remote_share') |
|
| 189 | - ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/')]) |
|
| 190 | - ->setAffectedUser($shareWith) |
|
| 191 | - ->setObject('remote_share', (int)$shareId, $name); |
|
| 192 | - \OC::$server->getActivityManager()->publish($event); |
|
| 193 | - |
|
| 194 | - $urlGenerator = \OC::$server->getURLGenerator(); |
|
| 195 | - |
|
| 196 | - $notificationManager = \OC::$server->getNotificationManager(); |
|
| 197 | - $notification = $notificationManager->createNotification(); |
|
| 198 | - $notification->setApp('files_sharing') |
|
| 199 | - ->setUser($shareWith) |
|
| 200 | - ->setDateTime(new \DateTime()) |
|
| 201 | - ->setObject('remote_share', $shareId) |
|
| 202 | - ->setSubject('remote_share', [$ownerFederatedId, $sharedByFederatedId, trim($name, '/')]); |
|
| 203 | - |
|
| 204 | - $declineAction = $notification->createAction(); |
|
| 205 | - $declineAction->setLabel('decline') |
|
| 206 | - ->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'DELETE'); |
|
| 207 | - $notification->addAction($declineAction); |
|
| 208 | - |
|
| 209 | - $acceptAction = $notification->createAction(); |
|
| 210 | - $acceptAction->setLabel('accept') |
|
| 211 | - ->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'POST'); |
|
| 212 | - $notification->addAction($acceptAction); |
|
| 213 | - |
|
| 214 | - $notificationManager->notify($notification); |
|
| 215 | - |
|
| 216 | - return new Http\DataResponse(); |
|
| 217 | - } catch (\Exception $e) { |
|
| 218 | - $this->logger->logException($e, [ |
|
| 219 | - 'message' => 'Server can not add remote share.', |
|
| 220 | - 'level' => ILogger::ERROR, |
|
| 221 | - 'app' => 'files_sharing' |
|
| 222 | - ]); |
|
| 223 | - throw new OCSException('internal server error, was not able to add share from ' . $remote, 500); |
|
| 224 | - } |
|
| 225 | - } |
|
| 226 | - |
|
| 227 | - throw new OCSException('server can not add remote share, missing parameter', 400); |
|
| 228 | - } |
|
| 229 | - |
|
| 230 | - /** |
|
| 231 | - * @NoCSRFRequired |
|
| 232 | - * @PublicPage |
|
| 233 | - * |
|
| 234 | - * create re-share on behalf of another user |
|
| 235 | - * |
|
| 236 | - * @param int $id |
|
| 237 | - * @return Http\DataResponse |
|
| 238 | - * @throws OCSBadRequestException |
|
| 239 | - * @throws OCSForbiddenException |
|
| 240 | - * @throws OCSNotFoundException |
|
| 241 | - */ |
|
| 242 | - public function reShare($id) { |
|
| 243 | - |
|
| 244 | - $token = $this->request->getParam('token', null); |
|
| 245 | - $shareWith = $this->request->getParam('shareWith', null); |
|
| 246 | - $permission = (int)$this->request->getParam('permission', null); |
|
| 247 | - $remoteId = (int)$this->request->getParam('remoteId', null); |
|
| 248 | - |
|
| 249 | - if ($id === null || |
|
| 250 | - $token === null || |
|
| 251 | - $shareWith === null || |
|
| 252 | - $permission === null || |
|
| 253 | - $remoteId === null |
|
| 254 | - ) { |
|
| 255 | - throw new OCSBadRequestException(); |
|
| 256 | - } |
|
| 257 | - |
|
| 258 | - try { |
|
| 259 | - $share = $this->federatedShareProvider->getShareById($id); |
|
| 260 | - } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 261 | - throw new OCSNotFoundException(); |
|
| 262 | - } |
|
| 263 | - |
|
| 264 | - // don't allow to share a file back to the owner |
|
| 265 | - list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); |
|
| 266 | - $owner = $share->getShareOwner(); |
|
| 267 | - $currentServer = $this->addressHandler->generateRemoteURL(); |
|
| 268 | - if ($this->addressHandler->compareAddresses($user, $remote, $owner, $currentServer)) { |
|
| 269 | - throw new OCSForbiddenException(); |
|
| 270 | - } |
|
| 271 | - |
|
| 272 | - if ($this->verifyShare($share, $token)) { |
|
| 273 | - |
|
| 274 | - // check if re-sharing is allowed |
|
| 275 | - if ($share->getPermissions() | ~Constants::PERMISSION_SHARE) { |
|
| 276 | - $share->setPermissions($share->getPermissions() & $permission); |
|
| 277 | - // the recipient of the initial share is now the initiator for the re-share |
|
| 278 | - $share->setSharedBy($share->getSharedWith()); |
|
| 279 | - $share->setSharedWith($shareWith); |
|
| 280 | - try { |
|
| 281 | - $result = $this->federatedShareProvider->create($share); |
|
| 282 | - $this->federatedShareProvider->storeRemoteId((int)$result->getId(), $remoteId); |
|
| 283 | - return new Http\DataResponse([ |
|
| 284 | - 'token' => $result->getToken(), |
|
| 285 | - 'remoteId' => $result->getId() |
|
| 286 | - ]); |
|
| 287 | - } catch (\Exception $e) { |
|
| 288 | - throw new OCSBadRequestException(); |
|
| 289 | - } |
|
| 290 | - } else { |
|
| 291 | - throw new OCSForbiddenException(); |
|
| 292 | - } |
|
| 293 | - } |
|
| 294 | - throw new OCSBadRequestException(); |
|
| 295 | - } |
|
| 296 | - |
|
| 297 | - /** |
|
| 298 | - * @NoCSRFRequired |
|
| 299 | - * @PublicPage |
|
| 300 | - * |
|
| 301 | - * accept server-to-server share |
|
| 302 | - * |
|
| 303 | - * @param int $id |
|
| 304 | - * @return Http\DataResponse |
|
| 305 | - * @throws OCSException |
|
| 306 | - */ |
|
| 307 | - public function acceptShare($id) { |
|
| 308 | - |
|
| 309 | - if (!$this->isS2SEnabled()) { |
|
| 310 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 311 | - } |
|
| 312 | - |
|
| 313 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 314 | - |
|
| 315 | - try { |
|
| 316 | - $share = $this->federatedShareProvider->getShareById($id); |
|
| 317 | - } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 318 | - return new Http\DataResponse(); |
|
| 319 | - } |
|
| 320 | - |
|
| 321 | - if ($this->verifyShare($share, $token)) { |
|
| 322 | - $this->executeAcceptShare($share); |
|
| 323 | - if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 324 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 325 | - $remoteId = $this->federatedShareProvider->getRemoteId($share); |
|
| 326 | - $this->notifications->sendAcceptShare($remote, $remoteId, $share->getToken()); |
|
| 327 | - } |
|
| 328 | - } |
|
| 329 | - |
|
| 330 | - return new Http\DataResponse(); |
|
| 331 | - } |
|
| 332 | - |
|
| 333 | - protected function executeAcceptShare(Share\IShare $share) { |
|
| 334 | - $fileId = (int) $share->getNode()->getId(); |
|
| 335 | - list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId); |
|
| 336 | - |
|
| 337 | - $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 338 | - $event->setApp('files_sharing') |
|
| 339 | - ->setType('remote_share') |
|
| 340 | - ->setAffectedUser($this->getCorrectUid($share)) |
|
| 341 | - ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_ACCEPTED, [$share->getSharedWith(), [$fileId => $file]]) |
|
| 342 | - ->setObject('files', $fileId, $file) |
|
| 343 | - ->setLink($link); |
|
| 344 | - \OC::$server->getActivityManager()->publish($event); |
|
| 345 | - } |
|
| 346 | - |
|
| 347 | - /** |
|
| 348 | - * @NoCSRFRequired |
|
| 349 | - * @PublicPage |
|
| 350 | - * |
|
| 351 | - * decline server-to-server share |
|
| 352 | - * |
|
| 353 | - * @param int $id |
|
| 354 | - * @return Http\DataResponse |
|
| 355 | - * @throws OCSException |
|
| 356 | - */ |
|
| 357 | - public function declineShare($id) { |
|
| 358 | - |
|
| 359 | - if (!$this->isS2SEnabled()) { |
|
| 360 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 361 | - } |
|
| 362 | - |
|
| 363 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 364 | - |
|
| 365 | - try { |
|
| 366 | - $share = $this->federatedShareProvider->getShareById($id); |
|
| 367 | - } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 368 | - return new Http\DataResponse(); |
|
| 369 | - } |
|
| 370 | - |
|
| 371 | - if ($this->verifyShare($share, $token)) { |
|
| 372 | - if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 373 | - list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 374 | - $remoteId = $this->federatedShareProvider->getRemoteId($share); |
|
| 375 | - $this->notifications->sendDeclineShare($remote, $remoteId, $share->getToken()); |
|
| 376 | - } |
|
| 377 | - $this->executeDeclineShare($share); |
|
| 378 | - } |
|
| 379 | - |
|
| 380 | - return new Http\DataResponse(); |
|
| 381 | - } |
|
| 382 | - |
|
| 383 | - /** |
|
| 384 | - * delete declined share and create a activity |
|
| 385 | - * |
|
| 386 | - * @param Share\IShare $share |
|
| 387 | - */ |
|
| 388 | - protected function executeDeclineShare(Share\IShare $share) { |
|
| 389 | - $this->federatedShareProvider->removeShareFromTable($share); |
|
| 390 | - $fileId = (int) $share->getNode()->getId(); |
|
| 391 | - list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId); |
|
| 392 | - |
|
| 393 | - $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 394 | - $event->setApp('files_sharing') |
|
| 395 | - ->setType('remote_share') |
|
| 396 | - ->setAffectedUser($this->getCorrectUid($share)) |
|
| 397 | - ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_DECLINED, [$share->getSharedWith(), [$fileId => $file]]) |
|
| 398 | - ->setObject('files', $fileId, $file) |
|
| 399 | - ->setLink($link); |
|
| 400 | - \OC::$server->getActivityManager()->publish($event); |
|
| 401 | - |
|
| 402 | - } |
|
| 403 | - |
|
| 404 | - /** |
|
| 405 | - * check if we are the initiator or the owner of a re-share and return the correct UID |
|
| 406 | - * |
|
| 407 | - * @param Share\IShare $share |
|
| 408 | - * @return string |
|
| 409 | - */ |
|
| 410 | - protected function getCorrectUid(Share\IShare $share) { |
|
| 411 | - if ($this->userManager->userExists($share->getShareOwner())) { |
|
| 412 | - return $share->getShareOwner(); |
|
| 413 | - } |
|
| 414 | - |
|
| 415 | - return $share->getSharedBy(); |
|
| 416 | - } |
|
| 417 | - |
|
| 418 | - /** |
|
| 419 | - * @NoCSRFRequired |
|
| 420 | - * @PublicPage |
|
| 421 | - * |
|
| 422 | - * remove server-to-server share if it was unshared by the owner |
|
| 423 | - * |
|
| 424 | - * @param int $id |
|
| 425 | - * @return Http\DataResponse |
|
| 426 | - * @throws OCSException |
|
| 427 | - */ |
|
| 428 | - public function unshare($id) { |
|
| 429 | - |
|
| 430 | - if (!$this->isS2SEnabled()) { |
|
| 431 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 432 | - } |
|
| 433 | - |
|
| 434 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 435 | - |
|
| 436 | - $qb = $this->connection->getQueryBuilder(); |
|
| 437 | - $qb->select('*') |
|
| 438 | - ->from('share_external') |
|
| 439 | - ->where( |
|
| 440 | - $qb->expr()->andX( |
|
| 441 | - $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)), |
|
| 442 | - $qb->expr()->eq('share_token', $qb->createNamedParameter($token)) |
|
| 443 | - ) |
|
| 444 | - ); |
|
| 445 | - |
|
| 446 | - $result = $qb->execute(); |
|
| 447 | - $share = $result->fetch(); |
|
| 448 | - $result->closeCursor(); |
|
| 449 | - |
|
| 450 | - if ($token && $id && !empty($share)) { |
|
| 451 | - |
|
| 452 | - $remote = $this->cleanupRemote($share['remote']); |
|
| 453 | - |
|
| 454 | - $owner = $this->cloudIdManager->getCloudId($share['owner'], $remote); |
|
| 455 | - $mountpoint = $share['mountpoint']; |
|
| 456 | - $user = $share['user']; |
|
| 457 | - |
|
| 458 | - $qb = $this->connection->getQueryBuilder(); |
|
| 459 | - $qb->delete('share_external') |
|
| 460 | - ->where( |
|
| 461 | - $qb->expr()->andX( |
|
| 462 | - $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)), |
|
| 463 | - $qb->expr()->eq('share_token', $qb->createNamedParameter($token)) |
|
| 464 | - ) |
|
| 465 | - ); |
|
| 466 | - |
|
| 467 | - $result = $qb->execute(); |
|
| 468 | - $result->closeCursor(); |
|
| 469 | - |
|
| 470 | - if ($share['accepted']) { |
|
| 471 | - $path = trim($mountpoint, '/'); |
|
| 472 | - } else { |
|
| 473 | - $path = trim($share['name'], '/'); |
|
| 474 | - } |
|
| 475 | - |
|
| 476 | - $notificationManager = \OC::$server->getNotificationManager(); |
|
| 477 | - $notification = $notificationManager->createNotification(); |
|
| 478 | - $notification->setApp('files_sharing') |
|
| 479 | - ->setUser($share['user']) |
|
| 480 | - ->setObject('remote_share', (int)$share['id']); |
|
| 481 | - $notificationManager->markProcessed($notification); |
|
| 482 | - |
|
| 483 | - $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 484 | - $event->setApp('files_sharing') |
|
| 485 | - ->setType('remote_share') |
|
| 486 | - ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_UNSHARED, [$owner->getId(), $path]) |
|
| 487 | - ->setAffectedUser($user) |
|
| 488 | - ->setObject('remote_share', (int)$share['id'], $path); |
|
| 489 | - \OC::$server->getActivityManager()->publish($event); |
|
| 490 | - } |
|
| 491 | - |
|
| 492 | - return new Http\DataResponse(); |
|
| 493 | - } |
|
| 494 | - |
|
| 495 | - private function cleanupRemote($remote) { |
|
| 496 | - $remote = substr($remote, strpos($remote, '://') + 3); |
|
| 497 | - |
|
| 498 | - return rtrim($remote, '/'); |
|
| 499 | - } |
|
| 500 | - |
|
| 501 | - |
|
| 502 | - /** |
|
| 503 | - * @NoCSRFRequired |
|
| 504 | - * @PublicPage |
|
| 505 | - * |
|
| 506 | - * federated share was revoked, either by the owner or the re-sharer |
|
| 507 | - * |
|
| 508 | - * @param int $id |
|
| 509 | - * @return Http\DataResponse |
|
| 510 | - * @throws OCSBadRequestException |
|
| 511 | - */ |
|
| 512 | - public function revoke($id) { |
|
| 513 | - $token = $this->request->getParam('token'); |
|
| 514 | - |
|
| 515 | - $share = $this->federatedShareProvider->getShareById($id); |
|
| 516 | - |
|
| 517 | - if ($this->verifyShare($share, $token)) { |
|
| 518 | - $this->federatedShareProvider->removeShareFromTable($share); |
|
| 519 | - return new Http\DataResponse(); |
|
| 520 | - } |
|
| 521 | - |
|
| 522 | - throw new OCSBadRequestException(); |
|
| 523 | - } |
|
| 524 | - |
|
| 525 | - /** |
|
| 526 | - * get share |
|
| 527 | - * |
|
| 528 | - * @param int $id |
|
| 529 | - * @param string $token |
|
| 530 | - * @return array|bool |
|
| 531 | - */ |
|
| 532 | - protected function getShare($id, $token) { |
|
| 533 | - $query = $this->connection->getQueryBuilder(); |
|
| 534 | - $query->select('*')->from($this->shareTable) |
|
| 535 | - ->where($query->expr()->eq('token', $query->createNamedParameter($token))) |
|
| 536 | - ->andWhere($query->expr()->eq('share_type', $query->createNamedParameter(FederatedShareProvider::SHARE_TYPE_REMOTE))) |
|
| 537 | - ->andWhere($query->expr()->eq('id', $query->createNamedParameter($id))); |
|
| 538 | - |
|
| 539 | - $result = $query->execute()->fetchAll(); |
|
| 540 | - |
|
| 541 | - if (!empty($result) && isset($result[0])) { |
|
| 542 | - return $result[0]; |
|
| 543 | - } |
|
| 544 | - |
|
| 545 | - return false; |
|
| 546 | - } |
|
| 547 | - |
|
| 548 | - /** |
|
| 549 | - * get file |
|
| 550 | - * |
|
| 551 | - * @param string $user |
|
| 552 | - * @param int $fileSource |
|
| 553 | - * @return array with internal path of the file and a absolute link to it |
|
| 554 | - */ |
|
| 555 | - private function getFile($user, $fileSource) { |
|
| 556 | - \OC_Util::setupFS($user); |
|
| 557 | - |
|
| 558 | - try { |
|
| 559 | - $file = \OC\Files\Filesystem::getPath($fileSource); |
|
| 560 | - } catch (NotFoundException $e) { |
|
| 561 | - $file = null; |
|
| 562 | - } |
|
| 563 | - $args = \OC\Files\Filesystem::is_dir($file) ? array('dir' => $file) : array('dir' => dirname($file), 'scrollto' => $file); |
|
| 564 | - $link = \OCP\Util::linkToAbsolute('files', 'index.php', $args); |
|
| 565 | - |
|
| 566 | - return array($file, $link); |
|
| 567 | - |
|
| 568 | - } |
|
| 569 | - |
|
| 570 | - /** |
|
| 571 | - * check if server-to-server sharing is enabled |
|
| 572 | - * |
|
| 573 | - * @param bool $incoming |
|
| 574 | - * @return bool |
|
| 575 | - */ |
|
| 576 | - private function isS2SEnabled($incoming = false) { |
|
| 577 | - |
|
| 578 | - $result = \OCP\App::isEnabled('files_sharing'); |
|
| 579 | - |
|
| 580 | - if ($incoming) { |
|
| 581 | - $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled(); |
|
| 582 | - } else { |
|
| 583 | - $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); |
|
| 584 | - } |
|
| 585 | - |
|
| 586 | - return $result; |
|
| 587 | - } |
|
| 588 | - |
|
| 589 | - /** |
|
| 590 | - * check if we got the right share |
|
| 591 | - * |
|
| 592 | - * @param Share\IShare $share |
|
| 593 | - * @param string $token |
|
| 594 | - * @return bool |
|
| 595 | - */ |
|
| 596 | - protected function verifyShare(Share\IShare $share, $token) { |
|
| 597 | - if ( |
|
| 598 | - $share->getShareType() === FederatedShareProvider::SHARE_TYPE_REMOTE && |
|
| 599 | - $share->getToken() === $token |
|
| 600 | - ) { |
|
| 601 | - return true; |
|
| 602 | - } |
|
| 603 | - |
|
| 604 | - return false; |
|
| 605 | - } |
|
| 606 | - |
|
| 607 | - /** |
|
| 608 | - * @NoCSRFRequired |
|
| 609 | - * @PublicPage |
|
| 610 | - * |
|
| 611 | - * update share information to keep federated re-shares in sync |
|
| 612 | - * |
|
| 613 | - * @param int $id |
|
| 614 | - * @return Http\DataResponse |
|
| 615 | - * @throws OCSBadRequestException |
|
| 616 | - */ |
|
| 617 | - public function updatePermissions($id) { |
|
| 618 | - $token = $this->request->getParam('token', null); |
|
| 619 | - $permissions = $this->request->getParam('permissions', null); |
|
| 620 | - |
|
| 621 | - try { |
|
| 622 | - $share = $this->federatedShareProvider->getShareById($id); |
|
| 623 | - } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 624 | - throw new OCSBadRequestException(); |
|
| 625 | - } |
|
| 626 | - |
|
| 627 | - $validPermission = ctype_digit($permissions); |
|
| 628 | - $validToken = $this->verifyShare($share, $token); |
|
| 629 | - if ($validPermission && $validToken) { |
|
| 630 | - $this->updatePermissionsInDatabase($share, (int)$permissions); |
|
| 631 | - } else { |
|
| 632 | - throw new OCSBadRequestException(); |
|
| 633 | - } |
|
| 634 | - |
|
| 635 | - return new Http\DataResponse(); |
|
| 636 | - } |
|
| 637 | - |
|
| 638 | - /** |
|
| 639 | - * update permissions in database |
|
| 640 | - * |
|
| 641 | - * @param IShare $share |
|
| 642 | - * @param int $permissions |
|
| 643 | - */ |
|
| 644 | - protected function updatePermissionsInDatabase(IShare $share, $permissions) { |
|
| 645 | - $query = $this->connection->getQueryBuilder(); |
|
| 646 | - $query->update('share') |
|
| 647 | - ->where($query->expr()->eq('id', $query->createNamedParameter($share->getId()))) |
|
| 648 | - ->set('permissions', $query->createNamedParameter($permissions)) |
|
| 649 | - ->execute(); |
|
| 650 | - } |
|
| 651 | - |
|
| 652 | - /** |
|
| 653 | - * @NoCSRFRequired |
|
| 654 | - * @PublicPage |
|
| 655 | - * |
|
| 656 | - * change the owner of a server-to-server share |
|
| 657 | - * |
|
| 658 | - * @param int $id |
|
| 659 | - * @return Http\DataResponse |
|
| 660 | - * @throws \InvalidArgumentException |
|
| 661 | - * @throws OCSException |
|
| 662 | - */ |
|
| 663 | - public function move($id) { |
|
| 664 | - |
|
| 665 | - if (!$this->isS2SEnabled()) { |
|
| 666 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 667 | - } |
|
| 668 | - |
|
| 669 | - $token = $this->request->getParam('token'); |
|
| 670 | - $remote = $this->request->getParam('remote'); |
|
| 671 | - $newRemoteId = $this->request->getParam('remote_id', $id); |
|
| 672 | - $cloudId = $this->cloudIdManager->resolveCloudId($remote); |
|
| 673 | - |
|
| 674 | - $qb = $this->connection->getQueryBuilder(); |
|
| 675 | - $query = $qb->update('share_external') |
|
| 676 | - ->set('remote', $qb->createNamedParameter($cloudId->getRemote())) |
|
| 677 | - ->set('owner', $qb->createNamedParameter($cloudId->getUser())) |
|
| 678 | - ->set('remote_id', $qb->createNamedParameter($newRemoteId)) |
|
| 679 | - ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id))) |
|
| 680 | - ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token))); |
|
| 681 | - $affected = $query->execute(); |
|
| 682 | - |
|
| 683 | - if ($affected > 0) { |
|
| 684 | - return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]); |
|
| 685 | - } else { |
|
| 686 | - throw new OCSBadRequestException('Share not found or token invalid'); |
|
| 687 | - } |
|
| 688 | - } |
|
| 54 | + /** @var FederatedShareProvider */ |
|
| 55 | + private $federatedShareProvider; |
|
| 56 | + |
|
| 57 | + /** @var IDBConnection */ |
|
| 58 | + private $connection; |
|
| 59 | + |
|
| 60 | + /** @var Share\IManager */ |
|
| 61 | + private $shareManager; |
|
| 62 | + |
|
| 63 | + /** @var Notifications */ |
|
| 64 | + private $notifications; |
|
| 65 | + |
|
| 66 | + /** @var AddressHandler */ |
|
| 67 | + private $addressHandler; |
|
| 68 | + |
|
| 69 | + /** @var IUserManager */ |
|
| 70 | + private $userManager; |
|
| 71 | + |
|
| 72 | + /** @var string */ |
|
| 73 | + private $shareTable = 'share'; |
|
| 74 | + |
|
| 75 | + /** @var ICloudIdManager */ |
|
| 76 | + private $cloudIdManager; |
|
| 77 | + |
|
| 78 | + /** @var ILogger */ |
|
| 79 | + private $logger; |
|
| 80 | + |
|
| 81 | + /** |
|
| 82 | + * Server2Server constructor. |
|
| 83 | + * |
|
| 84 | + * @param string $appName |
|
| 85 | + * @param IRequest $request |
|
| 86 | + * @param FederatedShareProvider $federatedShareProvider |
|
| 87 | + * @param IDBConnection $connection |
|
| 88 | + * @param Share\IManager $shareManager |
|
| 89 | + * @param Notifications $notifications |
|
| 90 | + * @param AddressHandler $addressHandler |
|
| 91 | + * @param IUserManager $userManager |
|
| 92 | + * @param ICloudIdManager $cloudIdManager |
|
| 93 | + */ |
|
| 94 | + public function __construct($appName, |
|
| 95 | + IRequest $request, |
|
| 96 | + FederatedShareProvider $federatedShareProvider, |
|
| 97 | + IDBConnection $connection, |
|
| 98 | + Share\IManager $shareManager, |
|
| 99 | + Notifications $notifications, |
|
| 100 | + AddressHandler $addressHandler, |
|
| 101 | + IUserManager $userManager, |
|
| 102 | + ICloudIdManager $cloudIdManager, |
|
| 103 | + ILogger $logger |
|
| 104 | + ) { |
|
| 105 | + parent::__construct($appName, $request); |
|
| 106 | + |
|
| 107 | + $this->federatedShareProvider = $federatedShareProvider; |
|
| 108 | + $this->connection = $connection; |
|
| 109 | + $this->shareManager = $shareManager; |
|
| 110 | + $this->notifications = $notifications; |
|
| 111 | + $this->addressHandler = $addressHandler; |
|
| 112 | + $this->userManager = $userManager; |
|
| 113 | + $this->cloudIdManager = $cloudIdManager; |
|
| 114 | + $this->logger = $logger; |
|
| 115 | + } |
|
| 116 | + |
|
| 117 | + /** |
|
| 118 | + * @NoCSRFRequired |
|
| 119 | + * @PublicPage |
|
| 120 | + * |
|
| 121 | + * create a new share |
|
| 122 | + * |
|
| 123 | + * @return Http\DataResponse |
|
| 124 | + * @throws OCSException |
|
| 125 | + */ |
|
| 126 | + public function createShare() { |
|
| 127 | + |
|
| 128 | + if (!$this->isS2SEnabled(true)) { |
|
| 129 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 130 | + } |
|
| 131 | + |
|
| 132 | + $remote = isset($_POST['remote']) ? $_POST['remote'] : null; |
|
| 133 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 134 | + $name = isset($_POST['name']) ? $_POST['name'] : null; |
|
| 135 | + $owner = isset($_POST['owner']) ? $_POST['owner'] : null; |
|
| 136 | + $sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null; |
|
| 137 | + $shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null; |
|
| 138 | + $remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null; |
|
| 139 | + $sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null; |
|
| 140 | + $ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null; |
|
| 141 | + |
|
| 142 | + if ($remote && $token && $name && $owner && $remoteId && $shareWith) { |
|
| 143 | + |
|
| 144 | + if (!\OCP\Util::isValidFileName($name)) { |
|
| 145 | + throw new OCSException('The mountpoint name contains invalid characters.', 400); |
|
| 146 | + } |
|
| 147 | + |
|
| 148 | + // FIXME this should be a method in the user management instead |
|
| 149 | + $this->logger->debug('shareWith before, ' . $shareWith, ['app' => 'files_sharing']); |
|
| 150 | + \OCP\Util::emitHook( |
|
| 151 | + '\OCA\Files_Sharing\API\Server2Server', |
|
| 152 | + 'preLoginNameUsedAsUserName', |
|
| 153 | + array('uid' => &$shareWith) |
|
| 154 | + ); |
|
| 155 | + $this->logger->debug('shareWith after, ' . $shareWith, ['app' => 'files_sharing']); |
|
| 156 | + |
|
| 157 | + if (!\OC::$server->getUserManager()->userExists($shareWith)) { |
|
| 158 | + throw new OCSException('User does not exists', 400); |
|
| 159 | + } |
|
| 160 | + |
|
| 161 | + \OC_Util::setupFS($shareWith); |
|
| 162 | + |
|
| 163 | + $externalManager = new \OCA\Files_Sharing\External\Manager( |
|
| 164 | + \OC::$server->getDatabaseConnection(), |
|
| 165 | + \OC\Files\Filesystem::getMountManager(), |
|
| 166 | + \OC\Files\Filesystem::getLoader(), |
|
| 167 | + \OC::$server->getHTTPClientService(), |
|
| 168 | + \OC::$server->getNotificationManager(), |
|
| 169 | + \OC::$server->query(\OCP\OCS\IDiscoveryService::class), |
|
| 170 | + $shareWith |
|
| 171 | + ); |
|
| 172 | + |
|
| 173 | + try { |
|
| 174 | + $externalManager->addShare($remote, $token, '', $name, $owner, false, $shareWith, $remoteId); |
|
| 175 | + $shareId = \OC::$server->getDatabaseConnection()->lastInsertId('*PREFIX*share_external'); |
|
| 176 | + |
|
| 177 | + if ($ownerFederatedId === null) { |
|
| 178 | + $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId(); |
|
| 179 | + } |
|
| 180 | + // if the owner of the share and the initiator are the same user |
|
| 181 | + // we also complete the federated share ID for the initiator |
|
| 182 | + if ($sharedByFederatedId === null && $owner === $sharedBy) { |
|
| 183 | + $sharedByFederatedId = $ownerFederatedId; |
|
| 184 | + } |
|
| 185 | + |
|
| 186 | + $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 187 | + $event->setApp('files_sharing') |
|
| 188 | + ->setType('remote_share') |
|
| 189 | + ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_RECEIVED, [$ownerFederatedId, trim($name, '/')]) |
|
| 190 | + ->setAffectedUser($shareWith) |
|
| 191 | + ->setObject('remote_share', (int)$shareId, $name); |
|
| 192 | + \OC::$server->getActivityManager()->publish($event); |
|
| 193 | + |
|
| 194 | + $urlGenerator = \OC::$server->getURLGenerator(); |
|
| 195 | + |
|
| 196 | + $notificationManager = \OC::$server->getNotificationManager(); |
|
| 197 | + $notification = $notificationManager->createNotification(); |
|
| 198 | + $notification->setApp('files_sharing') |
|
| 199 | + ->setUser($shareWith) |
|
| 200 | + ->setDateTime(new \DateTime()) |
|
| 201 | + ->setObject('remote_share', $shareId) |
|
| 202 | + ->setSubject('remote_share', [$ownerFederatedId, $sharedByFederatedId, trim($name, '/')]); |
|
| 203 | + |
|
| 204 | + $declineAction = $notification->createAction(); |
|
| 205 | + $declineAction->setLabel('decline') |
|
| 206 | + ->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'DELETE'); |
|
| 207 | + $notification->addAction($declineAction); |
|
| 208 | + |
|
| 209 | + $acceptAction = $notification->createAction(); |
|
| 210 | + $acceptAction->setLabel('accept') |
|
| 211 | + ->setLink($urlGenerator->getAbsoluteURL($urlGenerator->linkTo('', 'ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/' . $shareId)), 'POST'); |
|
| 212 | + $notification->addAction($acceptAction); |
|
| 213 | + |
|
| 214 | + $notificationManager->notify($notification); |
|
| 215 | + |
|
| 216 | + return new Http\DataResponse(); |
|
| 217 | + } catch (\Exception $e) { |
|
| 218 | + $this->logger->logException($e, [ |
|
| 219 | + 'message' => 'Server can not add remote share.', |
|
| 220 | + 'level' => ILogger::ERROR, |
|
| 221 | + 'app' => 'files_sharing' |
|
| 222 | + ]); |
|
| 223 | + throw new OCSException('internal server error, was not able to add share from ' . $remote, 500); |
|
| 224 | + } |
|
| 225 | + } |
|
| 226 | + |
|
| 227 | + throw new OCSException('server can not add remote share, missing parameter', 400); |
|
| 228 | + } |
|
| 229 | + |
|
| 230 | + /** |
|
| 231 | + * @NoCSRFRequired |
|
| 232 | + * @PublicPage |
|
| 233 | + * |
|
| 234 | + * create re-share on behalf of another user |
|
| 235 | + * |
|
| 236 | + * @param int $id |
|
| 237 | + * @return Http\DataResponse |
|
| 238 | + * @throws OCSBadRequestException |
|
| 239 | + * @throws OCSForbiddenException |
|
| 240 | + * @throws OCSNotFoundException |
|
| 241 | + */ |
|
| 242 | + public function reShare($id) { |
|
| 243 | + |
|
| 244 | + $token = $this->request->getParam('token', null); |
|
| 245 | + $shareWith = $this->request->getParam('shareWith', null); |
|
| 246 | + $permission = (int)$this->request->getParam('permission', null); |
|
| 247 | + $remoteId = (int)$this->request->getParam('remoteId', null); |
|
| 248 | + |
|
| 249 | + if ($id === null || |
|
| 250 | + $token === null || |
|
| 251 | + $shareWith === null || |
|
| 252 | + $permission === null || |
|
| 253 | + $remoteId === null |
|
| 254 | + ) { |
|
| 255 | + throw new OCSBadRequestException(); |
|
| 256 | + } |
|
| 257 | + |
|
| 258 | + try { |
|
| 259 | + $share = $this->federatedShareProvider->getShareById($id); |
|
| 260 | + } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 261 | + throw new OCSNotFoundException(); |
|
| 262 | + } |
|
| 263 | + |
|
| 264 | + // don't allow to share a file back to the owner |
|
| 265 | + list($user, $remote) = $this->addressHandler->splitUserRemote($shareWith); |
|
| 266 | + $owner = $share->getShareOwner(); |
|
| 267 | + $currentServer = $this->addressHandler->generateRemoteURL(); |
|
| 268 | + if ($this->addressHandler->compareAddresses($user, $remote, $owner, $currentServer)) { |
|
| 269 | + throw new OCSForbiddenException(); |
|
| 270 | + } |
|
| 271 | + |
|
| 272 | + if ($this->verifyShare($share, $token)) { |
|
| 273 | + |
|
| 274 | + // check if re-sharing is allowed |
|
| 275 | + if ($share->getPermissions() | ~Constants::PERMISSION_SHARE) { |
|
| 276 | + $share->setPermissions($share->getPermissions() & $permission); |
|
| 277 | + // the recipient of the initial share is now the initiator for the re-share |
|
| 278 | + $share->setSharedBy($share->getSharedWith()); |
|
| 279 | + $share->setSharedWith($shareWith); |
|
| 280 | + try { |
|
| 281 | + $result = $this->federatedShareProvider->create($share); |
|
| 282 | + $this->federatedShareProvider->storeRemoteId((int)$result->getId(), $remoteId); |
|
| 283 | + return new Http\DataResponse([ |
|
| 284 | + 'token' => $result->getToken(), |
|
| 285 | + 'remoteId' => $result->getId() |
|
| 286 | + ]); |
|
| 287 | + } catch (\Exception $e) { |
|
| 288 | + throw new OCSBadRequestException(); |
|
| 289 | + } |
|
| 290 | + } else { |
|
| 291 | + throw new OCSForbiddenException(); |
|
| 292 | + } |
|
| 293 | + } |
|
| 294 | + throw new OCSBadRequestException(); |
|
| 295 | + } |
|
| 296 | + |
|
| 297 | + /** |
|
| 298 | + * @NoCSRFRequired |
|
| 299 | + * @PublicPage |
|
| 300 | + * |
|
| 301 | + * accept server-to-server share |
|
| 302 | + * |
|
| 303 | + * @param int $id |
|
| 304 | + * @return Http\DataResponse |
|
| 305 | + * @throws OCSException |
|
| 306 | + */ |
|
| 307 | + public function acceptShare($id) { |
|
| 308 | + |
|
| 309 | + if (!$this->isS2SEnabled()) { |
|
| 310 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 311 | + } |
|
| 312 | + |
|
| 313 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 314 | + |
|
| 315 | + try { |
|
| 316 | + $share = $this->federatedShareProvider->getShareById($id); |
|
| 317 | + } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 318 | + return new Http\DataResponse(); |
|
| 319 | + } |
|
| 320 | + |
|
| 321 | + if ($this->verifyShare($share, $token)) { |
|
| 322 | + $this->executeAcceptShare($share); |
|
| 323 | + if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 324 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 325 | + $remoteId = $this->federatedShareProvider->getRemoteId($share); |
|
| 326 | + $this->notifications->sendAcceptShare($remote, $remoteId, $share->getToken()); |
|
| 327 | + } |
|
| 328 | + } |
|
| 329 | + |
|
| 330 | + return new Http\DataResponse(); |
|
| 331 | + } |
|
| 332 | + |
|
| 333 | + protected function executeAcceptShare(Share\IShare $share) { |
|
| 334 | + $fileId = (int) $share->getNode()->getId(); |
|
| 335 | + list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId); |
|
| 336 | + |
|
| 337 | + $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 338 | + $event->setApp('files_sharing') |
|
| 339 | + ->setType('remote_share') |
|
| 340 | + ->setAffectedUser($this->getCorrectUid($share)) |
|
| 341 | + ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_ACCEPTED, [$share->getSharedWith(), [$fileId => $file]]) |
|
| 342 | + ->setObject('files', $fileId, $file) |
|
| 343 | + ->setLink($link); |
|
| 344 | + \OC::$server->getActivityManager()->publish($event); |
|
| 345 | + } |
|
| 346 | + |
|
| 347 | + /** |
|
| 348 | + * @NoCSRFRequired |
|
| 349 | + * @PublicPage |
|
| 350 | + * |
|
| 351 | + * decline server-to-server share |
|
| 352 | + * |
|
| 353 | + * @param int $id |
|
| 354 | + * @return Http\DataResponse |
|
| 355 | + * @throws OCSException |
|
| 356 | + */ |
|
| 357 | + public function declineShare($id) { |
|
| 358 | + |
|
| 359 | + if (!$this->isS2SEnabled()) { |
|
| 360 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 361 | + } |
|
| 362 | + |
|
| 363 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 364 | + |
|
| 365 | + try { |
|
| 366 | + $share = $this->federatedShareProvider->getShareById($id); |
|
| 367 | + } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 368 | + return new Http\DataResponse(); |
|
| 369 | + } |
|
| 370 | + |
|
| 371 | + if ($this->verifyShare($share, $token)) { |
|
| 372 | + if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 373 | + list(, $remote) = $this->addressHandler->splitUserRemote($share->getSharedBy()); |
|
| 374 | + $remoteId = $this->federatedShareProvider->getRemoteId($share); |
|
| 375 | + $this->notifications->sendDeclineShare($remote, $remoteId, $share->getToken()); |
|
| 376 | + } |
|
| 377 | + $this->executeDeclineShare($share); |
|
| 378 | + } |
|
| 379 | + |
|
| 380 | + return new Http\DataResponse(); |
|
| 381 | + } |
|
| 382 | + |
|
| 383 | + /** |
|
| 384 | + * delete declined share and create a activity |
|
| 385 | + * |
|
| 386 | + * @param Share\IShare $share |
|
| 387 | + */ |
|
| 388 | + protected function executeDeclineShare(Share\IShare $share) { |
|
| 389 | + $this->federatedShareProvider->removeShareFromTable($share); |
|
| 390 | + $fileId = (int) $share->getNode()->getId(); |
|
| 391 | + list($file, $link) = $this->getFile($this->getCorrectUid($share), $fileId); |
|
| 392 | + |
|
| 393 | + $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 394 | + $event->setApp('files_sharing') |
|
| 395 | + ->setType('remote_share') |
|
| 396 | + ->setAffectedUser($this->getCorrectUid($share)) |
|
| 397 | + ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_DECLINED, [$share->getSharedWith(), [$fileId => $file]]) |
|
| 398 | + ->setObject('files', $fileId, $file) |
|
| 399 | + ->setLink($link); |
|
| 400 | + \OC::$server->getActivityManager()->publish($event); |
|
| 401 | + |
|
| 402 | + } |
|
| 403 | + |
|
| 404 | + /** |
|
| 405 | + * check if we are the initiator or the owner of a re-share and return the correct UID |
|
| 406 | + * |
|
| 407 | + * @param Share\IShare $share |
|
| 408 | + * @return string |
|
| 409 | + */ |
|
| 410 | + protected function getCorrectUid(Share\IShare $share) { |
|
| 411 | + if ($this->userManager->userExists($share->getShareOwner())) { |
|
| 412 | + return $share->getShareOwner(); |
|
| 413 | + } |
|
| 414 | + |
|
| 415 | + return $share->getSharedBy(); |
|
| 416 | + } |
|
| 417 | + |
|
| 418 | + /** |
|
| 419 | + * @NoCSRFRequired |
|
| 420 | + * @PublicPage |
|
| 421 | + * |
|
| 422 | + * remove server-to-server share if it was unshared by the owner |
|
| 423 | + * |
|
| 424 | + * @param int $id |
|
| 425 | + * @return Http\DataResponse |
|
| 426 | + * @throws OCSException |
|
| 427 | + */ |
|
| 428 | + public function unshare($id) { |
|
| 429 | + |
|
| 430 | + if (!$this->isS2SEnabled()) { |
|
| 431 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 432 | + } |
|
| 433 | + |
|
| 434 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 435 | + |
|
| 436 | + $qb = $this->connection->getQueryBuilder(); |
|
| 437 | + $qb->select('*') |
|
| 438 | + ->from('share_external') |
|
| 439 | + ->where( |
|
| 440 | + $qb->expr()->andX( |
|
| 441 | + $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)), |
|
| 442 | + $qb->expr()->eq('share_token', $qb->createNamedParameter($token)) |
|
| 443 | + ) |
|
| 444 | + ); |
|
| 445 | + |
|
| 446 | + $result = $qb->execute(); |
|
| 447 | + $share = $result->fetch(); |
|
| 448 | + $result->closeCursor(); |
|
| 449 | + |
|
| 450 | + if ($token && $id && !empty($share)) { |
|
| 451 | + |
|
| 452 | + $remote = $this->cleanupRemote($share['remote']); |
|
| 453 | + |
|
| 454 | + $owner = $this->cloudIdManager->getCloudId($share['owner'], $remote); |
|
| 455 | + $mountpoint = $share['mountpoint']; |
|
| 456 | + $user = $share['user']; |
|
| 457 | + |
|
| 458 | + $qb = $this->connection->getQueryBuilder(); |
|
| 459 | + $qb->delete('share_external') |
|
| 460 | + ->where( |
|
| 461 | + $qb->expr()->andX( |
|
| 462 | + $qb->expr()->eq('remote_id', $qb->createNamedParameter($id)), |
|
| 463 | + $qb->expr()->eq('share_token', $qb->createNamedParameter($token)) |
|
| 464 | + ) |
|
| 465 | + ); |
|
| 466 | + |
|
| 467 | + $result = $qb->execute(); |
|
| 468 | + $result->closeCursor(); |
|
| 469 | + |
|
| 470 | + if ($share['accepted']) { |
|
| 471 | + $path = trim($mountpoint, '/'); |
|
| 472 | + } else { |
|
| 473 | + $path = trim($share['name'], '/'); |
|
| 474 | + } |
|
| 475 | + |
|
| 476 | + $notificationManager = \OC::$server->getNotificationManager(); |
|
| 477 | + $notification = $notificationManager->createNotification(); |
|
| 478 | + $notification->setApp('files_sharing') |
|
| 479 | + ->setUser($share['user']) |
|
| 480 | + ->setObject('remote_share', (int)$share['id']); |
|
| 481 | + $notificationManager->markProcessed($notification); |
|
| 482 | + |
|
| 483 | + $event = \OC::$server->getActivityManager()->generateEvent(); |
|
| 484 | + $event->setApp('files_sharing') |
|
| 485 | + ->setType('remote_share') |
|
| 486 | + ->setSubject(RemoteShares::SUBJECT_REMOTE_SHARE_UNSHARED, [$owner->getId(), $path]) |
|
| 487 | + ->setAffectedUser($user) |
|
| 488 | + ->setObject('remote_share', (int)$share['id'], $path); |
|
| 489 | + \OC::$server->getActivityManager()->publish($event); |
|
| 490 | + } |
|
| 491 | + |
|
| 492 | + return new Http\DataResponse(); |
|
| 493 | + } |
|
| 494 | + |
|
| 495 | + private function cleanupRemote($remote) { |
|
| 496 | + $remote = substr($remote, strpos($remote, '://') + 3); |
|
| 497 | + |
|
| 498 | + return rtrim($remote, '/'); |
|
| 499 | + } |
|
| 500 | + |
|
| 501 | + |
|
| 502 | + /** |
|
| 503 | + * @NoCSRFRequired |
|
| 504 | + * @PublicPage |
|
| 505 | + * |
|
| 506 | + * federated share was revoked, either by the owner or the re-sharer |
|
| 507 | + * |
|
| 508 | + * @param int $id |
|
| 509 | + * @return Http\DataResponse |
|
| 510 | + * @throws OCSBadRequestException |
|
| 511 | + */ |
|
| 512 | + public function revoke($id) { |
|
| 513 | + $token = $this->request->getParam('token'); |
|
| 514 | + |
|
| 515 | + $share = $this->federatedShareProvider->getShareById($id); |
|
| 516 | + |
|
| 517 | + if ($this->verifyShare($share, $token)) { |
|
| 518 | + $this->federatedShareProvider->removeShareFromTable($share); |
|
| 519 | + return new Http\DataResponse(); |
|
| 520 | + } |
|
| 521 | + |
|
| 522 | + throw new OCSBadRequestException(); |
|
| 523 | + } |
|
| 524 | + |
|
| 525 | + /** |
|
| 526 | + * get share |
|
| 527 | + * |
|
| 528 | + * @param int $id |
|
| 529 | + * @param string $token |
|
| 530 | + * @return array|bool |
|
| 531 | + */ |
|
| 532 | + protected function getShare($id, $token) { |
|
| 533 | + $query = $this->connection->getQueryBuilder(); |
|
| 534 | + $query->select('*')->from($this->shareTable) |
|
| 535 | + ->where($query->expr()->eq('token', $query->createNamedParameter($token))) |
|
| 536 | + ->andWhere($query->expr()->eq('share_type', $query->createNamedParameter(FederatedShareProvider::SHARE_TYPE_REMOTE))) |
|
| 537 | + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($id))); |
|
| 538 | + |
|
| 539 | + $result = $query->execute()->fetchAll(); |
|
| 540 | + |
|
| 541 | + if (!empty($result) && isset($result[0])) { |
|
| 542 | + return $result[0]; |
|
| 543 | + } |
|
| 544 | + |
|
| 545 | + return false; |
|
| 546 | + } |
|
| 547 | + |
|
| 548 | + /** |
|
| 549 | + * get file |
|
| 550 | + * |
|
| 551 | + * @param string $user |
|
| 552 | + * @param int $fileSource |
|
| 553 | + * @return array with internal path of the file and a absolute link to it |
|
| 554 | + */ |
|
| 555 | + private function getFile($user, $fileSource) { |
|
| 556 | + \OC_Util::setupFS($user); |
|
| 557 | + |
|
| 558 | + try { |
|
| 559 | + $file = \OC\Files\Filesystem::getPath($fileSource); |
|
| 560 | + } catch (NotFoundException $e) { |
|
| 561 | + $file = null; |
|
| 562 | + } |
|
| 563 | + $args = \OC\Files\Filesystem::is_dir($file) ? array('dir' => $file) : array('dir' => dirname($file), 'scrollto' => $file); |
|
| 564 | + $link = \OCP\Util::linkToAbsolute('files', 'index.php', $args); |
|
| 565 | + |
|
| 566 | + return array($file, $link); |
|
| 567 | + |
|
| 568 | + } |
|
| 569 | + |
|
| 570 | + /** |
|
| 571 | + * check if server-to-server sharing is enabled |
|
| 572 | + * |
|
| 573 | + * @param bool $incoming |
|
| 574 | + * @return bool |
|
| 575 | + */ |
|
| 576 | + private function isS2SEnabled($incoming = false) { |
|
| 577 | + |
|
| 578 | + $result = \OCP\App::isEnabled('files_sharing'); |
|
| 579 | + |
|
| 580 | + if ($incoming) { |
|
| 581 | + $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled(); |
|
| 582 | + } else { |
|
| 583 | + $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); |
|
| 584 | + } |
|
| 585 | + |
|
| 586 | + return $result; |
|
| 587 | + } |
|
| 588 | + |
|
| 589 | + /** |
|
| 590 | + * check if we got the right share |
|
| 591 | + * |
|
| 592 | + * @param Share\IShare $share |
|
| 593 | + * @param string $token |
|
| 594 | + * @return bool |
|
| 595 | + */ |
|
| 596 | + protected function verifyShare(Share\IShare $share, $token) { |
|
| 597 | + if ( |
|
| 598 | + $share->getShareType() === FederatedShareProvider::SHARE_TYPE_REMOTE && |
|
| 599 | + $share->getToken() === $token |
|
| 600 | + ) { |
|
| 601 | + return true; |
|
| 602 | + } |
|
| 603 | + |
|
| 604 | + return false; |
|
| 605 | + } |
|
| 606 | + |
|
| 607 | + /** |
|
| 608 | + * @NoCSRFRequired |
|
| 609 | + * @PublicPage |
|
| 610 | + * |
|
| 611 | + * update share information to keep federated re-shares in sync |
|
| 612 | + * |
|
| 613 | + * @param int $id |
|
| 614 | + * @return Http\DataResponse |
|
| 615 | + * @throws OCSBadRequestException |
|
| 616 | + */ |
|
| 617 | + public function updatePermissions($id) { |
|
| 618 | + $token = $this->request->getParam('token', null); |
|
| 619 | + $permissions = $this->request->getParam('permissions', null); |
|
| 620 | + |
|
| 621 | + try { |
|
| 622 | + $share = $this->federatedShareProvider->getShareById($id); |
|
| 623 | + } catch (Share\Exceptions\ShareNotFound $e) { |
|
| 624 | + throw new OCSBadRequestException(); |
|
| 625 | + } |
|
| 626 | + |
|
| 627 | + $validPermission = ctype_digit($permissions); |
|
| 628 | + $validToken = $this->verifyShare($share, $token); |
|
| 629 | + if ($validPermission && $validToken) { |
|
| 630 | + $this->updatePermissionsInDatabase($share, (int)$permissions); |
|
| 631 | + } else { |
|
| 632 | + throw new OCSBadRequestException(); |
|
| 633 | + } |
|
| 634 | + |
|
| 635 | + return new Http\DataResponse(); |
|
| 636 | + } |
|
| 637 | + |
|
| 638 | + /** |
|
| 639 | + * update permissions in database |
|
| 640 | + * |
|
| 641 | + * @param IShare $share |
|
| 642 | + * @param int $permissions |
|
| 643 | + */ |
|
| 644 | + protected function updatePermissionsInDatabase(IShare $share, $permissions) { |
|
| 645 | + $query = $this->connection->getQueryBuilder(); |
|
| 646 | + $query->update('share') |
|
| 647 | + ->where($query->expr()->eq('id', $query->createNamedParameter($share->getId()))) |
|
| 648 | + ->set('permissions', $query->createNamedParameter($permissions)) |
|
| 649 | + ->execute(); |
|
| 650 | + } |
|
| 651 | + |
|
| 652 | + /** |
|
| 653 | + * @NoCSRFRequired |
|
| 654 | + * @PublicPage |
|
| 655 | + * |
|
| 656 | + * change the owner of a server-to-server share |
|
| 657 | + * |
|
| 658 | + * @param int $id |
|
| 659 | + * @return Http\DataResponse |
|
| 660 | + * @throws \InvalidArgumentException |
|
| 661 | + * @throws OCSException |
|
| 662 | + */ |
|
| 663 | + public function move($id) { |
|
| 664 | + |
|
| 665 | + if (!$this->isS2SEnabled()) { |
|
| 666 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 667 | + } |
|
| 668 | + |
|
| 669 | + $token = $this->request->getParam('token'); |
|
| 670 | + $remote = $this->request->getParam('remote'); |
|
| 671 | + $newRemoteId = $this->request->getParam('remote_id', $id); |
|
| 672 | + $cloudId = $this->cloudIdManager->resolveCloudId($remote); |
|
| 673 | + |
|
| 674 | + $qb = $this->connection->getQueryBuilder(); |
|
| 675 | + $query = $qb->update('share_external') |
|
| 676 | + ->set('remote', $qb->createNamedParameter($cloudId->getRemote())) |
|
| 677 | + ->set('owner', $qb->createNamedParameter($cloudId->getUser())) |
|
| 678 | + ->set('remote_id', $qb->createNamedParameter($newRemoteId)) |
|
| 679 | + ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id))) |
|
| 680 | + ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token))); |
|
| 681 | + $affected = $query->execute(); |
|
| 682 | + |
|
| 683 | + if ($affected > 0) { |
|
| 684 | + return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]); |
|
| 685 | + } else { |
|
| 686 | + throw new OCSBadRequestException('Share not found or token invalid'); |
|
| 687 | + } |
|
| 688 | + } |
|
| 689 | 689 | } |
@@ -58,177 +58,177 @@ |
||
| 58 | 58 | */ |
| 59 | 59 | class MountPublicLinkController extends Controller { |
| 60 | 60 | |
| 61 | - /** @var FederatedShareProvider */ |
|
| 62 | - private $federatedShareProvider; |
|
| 63 | - |
|
| 64 | - /** @var AddressHandler */ |
|
| 65 | - private $addressHandler; |
|
| 66 | - |
|
| 67 | - /** @var IManager */ |
|
| 68 | - private $shareManager; |
|
| 69 | - |
|
| 70 | - /** @var ISession */ |
|
| 71 | - private $session; |
|
| 72 | - |
|
| 73 | - /** @var IL10N */ |
|
| 74 | - private $l; |
|
| 75 | - |
|
| 76 | - /** @var IUserSession */ |
|
| 77 | - private $userSession; |
|
| 78 | - |
|
| 79 | - /** @var IClientService */ |
|
| 80 | - private $clientService; |
|
| 81 | - |
|
| 82 | - /** @var ICloudIdManager */ |
|
| 83 | - private $cloudIdManager; |
|
| 84 | - |
|
| 85 | - /** |
|
| 86 | - * MountPublicLinkController constructor. |
|
| 87 | - * |
|
| 88 | - * @param string $appName |
|
| 89 | - * @param IRequest $request |
|
| 90 | - * @param FederatedShareProvider $federatedShareProvider |
|
| 91 | - * @param IManager $shareManager |
|
| 92 | - * @param AddressHandler $addressHandler |
|
| 93 | - * @param ISession $session |
|
| 94 | - * @param IL10N $l |
|
| 95 | - * @param IUserSession $userSession |
|
| 96 | - * @param IClientService $clientService |
|
| 97 | - * @param ICloudIdManager $cloudIdManager |
|
| 98 | - */ |
|
| 99 | - public function __construct($appName, |
|
| 100 | - IRequest $request, |
|
| 101 | - FederatedShareProvider $federatedShareProvider, |
|
| 102 | - IManager $shareManager, |
|
| 103 | - AddressHandler $addressHandler, |
|
| 104 | - ISession $session, |
|
| 105 | - IL10N $l, |
|
| 106 | - IUserSession $userSession, |
|
| 107 | - IClientService $clientService, |
|
| 108 | - ICloudIdManager $cloudIdManager |
|
| 109 | - ) { |
|
| 110 | - parent::__construct($appName, $request); |
|
| 111 | - |
|
| 112 | - $this->federatedShareProvider = $federatedShareProvider; |
|
| 113 | - $this->shareManager = $shareManager; |
|
| 114 | - $this->addressHandler = $addressHandler; |
|
| 115 | - $this->session = $session; |
|
| 116 | - $this->l = $l; |
|
| 117 | - $this->userSession = $userSession; |
|
| 118 | - $this->clientService = $clientService; |
|
| 119 | - $this->cloudIdManager = $cloudIdManager; |
|
| 120 | - } |
|
| 121 | - |
|
| 122 | - /** |
|
| 123 | - * send federated share to a user of a public link |
|
| 124 | - * |
|
| 125 | - * @NoCSRFRequired |
|
| 126 | - * @PublicPage |
|
| 127 | - * @BruteForceProtection(action=publicLink2FederatedShare) |
|
| 128 | - * |
|
| 129 | - * @param string $shareWith |
|
| 130 | - * @param string $token |
|
| 131 | - * @param string $password |
|
| 132 | - * @return JSONResponse |
|
| 133 | - */ |
|
| 134 | - public function createFederatedShare($shareWith, $token, $password = '') { |
|
| 135 | - |
|
| 136 | - if (!$this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { |
|
| 137 | - return new JSONResponse( |
|
| 138 | - ['message' => 'This server doesn\'t support outgoing federated shares'], |
|
| 139 | - Http::STATUS_BAD_REQUEST |
|
| 140 | - ); |
|
| 141 | - } |
|
| 142 | - |
|
| 143 | - try { |
|
| 144 | - list(, $server) = $this->addressHandler->splitUserRemote($shareWith); |
|
| 145 | - $share = $this->shareManager->getShareByToken($token); |
|
| 146 | - } catch (HintException $e) { |
|
| 147 | - return new JSONResponse(['message' => $e->getHint()], Http::STATUS_BAD_REQUEST); |
|
| 148 | - } |
|
| 149 | - |
|
| 150 | - // make sure that user is authenticated in case of a password protected link |
|
| 151 | - $storedPassword = $share->getPassword(); |
|
| 152 | - $authenticated = $this->session->get('public_link_authenticated') === $share->getId() || |
|
| 153 | - $this->shareManager->checkPassword($share, $password); |
|
| 154 | - if (!empty($storedPassword) && !$authenticated ) { |
|
| 155 | - $response = new JSONResponse( |
|
| 156 | - ['message' => 'No permission to access the share'], |
|
| 157 | - Http::STATUS_BAD_REQUEST |
|
| 158 | - ); |
|
| 159 | - $response->throttle(); |
|
| 160 | - return $response; |
|
| 161 | - } |
|
| 162 | - |
|
| 163 | - $share->setSharedWith($shareWith); |
|
| 164 | - |
|
| 165 | - try { |
|
| 166 | - $this->federatedShareProvider->create($share); |
|
| 167 | - } catch (\Exception $e) { |
|
| 168 | - \OC::$server->getLogger()->logException($e, [ |
|
| 169 | - 'level' => ILogger::WARN, |
|
| 170 | - 'app' => 'federatedfilesharing', |
|
| 171 | - ]); |
|
| 172 | - return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); |
|
| 173 | - } |
|
| 174 | - |
|
| 175 | - return new JSONResponse(['remoteUrl' => $server]); |
|
| 176 | - } |
|
| 177 | - |
|
| 178 | - /** |
|
| 179 | - * ask other server to get a federated share |
|
| 180 | - * |
|
| 181 | - * @NoAdminRequired |
|
| 182 | - * |
|
| 183 | - * @param string $token |
|
| 184 | - * @param string $remote |
|
| 185 | - * @param string $password |
|
| 186 | - * @param string $owner (only for legacy reasons, can be removed with legacyMountPublicLink()) |
|
| 187 | - * @param string $ownerDisplayName (only for legacy reasons, can be removed with legacyMountPublicLink()) |
|
| 188 | - * @param string $name (only for legacy reasons, can be removed with legacyMountPublicLink()) |
|
| 189 | - * @return JSONResponse |
|
| 190 | - */ |
|
| 191 | - public function askForFederatedShare($token, $remote, $password = '', $owner = '', $ownerDisplayName = '', $name = '') { |
|
| 192 | - // check if server admin allows to mount public links from other servers |
|
| 193 | - if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled() === false) { |
|
| 194 | - return new JSONResponse(['message' => $this->l->t('Server to server sharing is not enabled on this server')], Http::STATUS_BAD_REQUEST); |
|
| 195 | - } |
|
| 196 | - |
|
| 197 | - $cloudId = $this->cloudIdManager->getCloudId($this->userSession->getUser()->getUID(), $this->addressHandler->generateRemoteURL()); |
|
| 198 | - |
|
| 199 | - $httpClient = $this->clientService->newClient(); |
|
| 200 | - |
|
| 201 | - try { |
|
| 202 | - $response = $httpClient->post($remote . '/index.php/apps/federatedfilesharing/createFederatedShare', |
|
| 203 | - [ |
|
| 204 | - 'body' => |
|
| 205 | - [ |
|
| 206 | - 'token' => $token, |
|
| 207 | - 'shareWith' => rtrim($cloudId->getId(), '/'), |
|
| 208 | - 'password' => $password |
|
| 209 | - ], |
|
| 210 | - 'connect_timeout' => 10, |
|
| 211 | - ] |
|
| 212 | - ); |
|
| 213 | - } catch (\Exception $e) { |
|
| 214 | - if (empty($password)) { |
|
| 215 | - $message = $this->l->t("Couldn't establish a federated share."); |
|
| 216 | - } else { |
|
| 217 | - $message = $this->l->t("Couldn't establish a federated share, maybe the password was wrong."); |
|
| 218 | - } |
|
| 219 | - return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST); |
|
| 220 | - } |
|
| 221 | - |
|
| 222 | - $body = $response->getBody(); |
|
| 223 | - $result = json_decode($body, true); |
|
| 224 | - |
|
| 225 | - if (is_array($result) && isset($result['remoteUrl'])) { |
|
| 226 | - return new JSONResponse(['message' => $this->l->t('Federated Share request sent, you will receive an invitation. Check your notifications.')]); |
|
| 227 | - } |
|
| 228 | - |
|
| 229 | - // if we doesn't get the expected response we assume that we try to add |
|
| 230 | - // a federated share from a Nextcloud <= 9 server |
|
| 231 | - $message = $this->l->t("Couldn't establish a federated share, it looks like the server to federate with is too old (Nextcloud <= 9)."); |
|
| 232 | - return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST); |
|
| 233 | - } |
|
| 61 | + /** @var FederatedShareProvider */ |
|
| 62 | + private $federatedShareProvider; |
|
| 63 | + |
|
| 64 | + /** @var AddressHandler */ |
|
| 65 | + private $addressHandler; |
|
| 66 | + |
|
| 67 | + /** @var IManager */ |
|
| 68 | + private $shareManager; |
|
| 69 | + |
|
| 70 | + /** @var ISession */ |
|
| 71 | + private $session; |
|
| 72 | + |
|
| 73 | + /** @var IL10N */ |
|
| 74 | + private $l; |
|
| 75 | + |
|
| 76 | + /** @var IUserSession */ |
|
| 77 | + private $userSession; |
|
| 78 | + |
|
| 79 | + /** @var IClientService */ |
|
| 80 | + private $clientService; |
|
| 81 | + |
|
| 82 | + /** @var ICloudIdManager */ |
|
| 83 | + private $cloudIdManager; |
|
| 84 | + |
|
| 85 | + /** |
|
| 86 | + * MountPublicLinkController constructor. |
|
| 87 | + * |
|
| 88 | + * @param string $appName |
|
| 89 | + * @param IRequest $request |
|
| 90 | + * @param FederatedShareProvider $federatedShareProvider |
|
| 91 | + * @param IManager $shareManager |
|
| 92 | + * @param AddressHandler $addressHandler |
|
| 93 | + * @param ISession $session |
|
| 94 | + * @param IL10N $l |
|
| 95 | + * @param IUserSession $userSession |
|
| 96 | + * @param IClientService $clientService |
|
| 97 | + * @param ICloudIdManager $cloudIdManager |
|
| 98 | + */ |
|
| 99 | + public function __construct($appName, |
|
| 100 | + IRequest $request, |
|
| 101 | + FederatedShareProvider $federatedShareProvider, |
|
| 102 | + IManager $shareManager, |
|
| 103 | + AddressHandler $addressHandler, |
|
| 104 | + ISession $session, |
|
| 105 | + IL10N $l, |
|
| 106 | + IUserSession $userSession, |
|
| 107 | + IClientService $clientService, |
|
| 108 | + ICloudIdManager $cloudIdManager |
|
| 109 | + ) { |
|
| 110 | + parent::__construct($appName, $request); |
|
| 111 | + |
|
| 112 | + $this->federatedShareProvider = $federatedShareProvider; |
|
| 113 | + $this->shareManager = $shareManager; |
|
| 114 | + $this->addressHandler = $addressHandler; |
|
| 115 | + $this->session = $session; |
|
| 116 | + $this->l = $l; |
|
| 117 | + $this->userSession = $userSession; |
|
| 118 | + $this->clientService = $clientService; |
|
| 119 | + $this->cloudIdManager = $cloudIdManager; |
|
| 120 | + } |
|
| 121 | + |
|
| 122 | + /** |
|
| 123 | + * send federated share to a user of a public link |
|
| 124 | + * |
|
| 125 | + * @NoCSRFRequired |
|
| 126 | + * @PublicPage |
|
| 127 | + * @BruteForceProtection(action=publicLink2FederatedShare) |
|
| 128 | + * |
|
| 129 | + * @param string $shareWith |
|
| 130 | + * @param string $token |
|
| 131 | + * @param string $password |
|
| 132 | + * @return JSONResponse |
|
| 133 | + */ |
|
| 134 | + public function createFederatedShare($shareWith, $token, $password = '') { |
|
| 135 | + |
|
| 136 | + if (!$this->federatedShareProvider->isOutgoingServer2serverShareEnabled()) { |
|
| 137 | + return new JSONResponse( |
|
| 138 | + ['message' => 'This server doesn\'t support outgoing federated shares'], |
|
| 139 | + Http::STATUS_BAD_REQUEST |
|
| 140 | + ); |
|
| 141 | + } |
|
| 142 | + |
|
| 143 | + try { |
|
| 144 | + list(, $server) = $this->addressHandler->splitUserRemote($shareWith); |
|
| 145 | + $share = $this->shareManager->getShareByToken($token); |
|
| 146 | + } catch (HintException $e) { |
|
| 147 | + return new JSONResponse(['message' => $e->getHint()], Http::STATUS_BAD_REQUEST); |
|
| 148 | + } |
|
| 149 | + |
|
| 150 | + // make sure that user is authenticated in case of a password protected link |
|
| 151 | + $storedPassword = $share->getPassword(); |
|
| 152 | + $authenticated = $this->session->get('public_link_authenticated') === $share->getId() || |
|
| 153 | + $this->shareManager->checkPassword($share, $password); |
|
| 154 | + if (!empty($storedPassword) && !$authenticated ) { |
|
| 155 | + $response = new JSONResponse( |
|
| 156 | + ['message' => 'No permission to access the share'], |
|
| 157 | + Http::STATUS_BAD_REQUEST |
|
| 158 | + ); |
|
| 159 | + $response->throttle(); |
|
| 160 | + return $response; |
|
| 161 | + } |
|
| 162 | + |
|
| 163 | + $share->setSharedWith($shareWith); |
|
| 164 | + |
|
| 165 | + try { |
|
| 166 | + $this->federatedShareProvider->create($share); |
|
| 167 | + } catch (\Exception $e) { |
|
| 168 | + \OC::$server->getLogger()->logException($e, [ |
|
| 169 | + 'level' => ILogger::WARN, |
|
| 170 | + 'app' => 'federatedfilesharing', |
|
| 171 | + ]); |
|
| 172 | + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); |
|
| 173 | + } |
|
| 174 | + |
|
| 175 | + return new JSONResponse(['remoteUrl' => $server]); |
|
| 176 | + } |
|
| 177 | + |
|
| 178 | + /** |
|
| 179 | + * ask other server to get a federated share |
|
| 180 | + * |
|
| 181 | + * @NoAdminRequired |
|
| 182 | + * |
|
| 183 | + * @param string $token |
|
| 184 | + * @param string $remote |
|
| 185 | + * @param string $password |
|
| 186 | + * @param string $owner (only for legacy reasons, can be removed with legacyMountPublicLink()) |
|
| 187 | + * @param string $ownerDisplayName (only for legacy reasons, can be removed with legacyMountPublicLink()) |
|
| 188 | + * @param string $name (only for legacy reasons, can be removed with legacyMountPublicLink()) |
|
| 189 | + * @return JSONResponse |
|
| 190 | + */ |
|
| 191 | + public function askForFederatedShare($token, $remote, $password = '', $owner = '', $ownerDisplayName = '', $name = '') { |
|
| 192 | + // check if server admin allows to mount public links from other servers |
|
| 193 | + if ($this->federatedShareProvider->isIncomingServer2serverShareEnabled() === false) { |
|
| 194 | + return new JSONResponse(['message' => $this->l->t('Server to server sharing is not enabled on this server')], Http::STATUS_BAD_REQUEST); |
|
| 195 | + } |
|
| 196 | + |
|
| 197 | + $cloudId = $this->cloudIdManager->getCloudId($this->userSession->getUser()->getUID(), $this->addressHandler->generateRemoteURL()); |
|
| 198 | + |
|
| 199 | + $httpClient = $this->clientService->newClient(); |
|
| 200 | + |
|
| 201 | + try { |
|
| 202 | + $response = $httpClient->post($remote . '/index.php/apps/federatedfilesharing/createFederatedShare', |
|
| 203 | + [ |
|
| 204 | + 'body' => |
|
| 205 | + [ |
|
| 206 | + 'token' => $token, |
|
| 207 | + 'shareWith' => rtrim($cloudId->getId(), '/'), |
|
| 208 | + 'password' => $password |
|
| 209 | + ], |
|
| 210 | + 'connect_timeout' => 10, |
|
| 211 | + ] |
|
| 212 | + ); |
|
| 213 | + } catch (\Exception $e) { |
|
| 214 | + if (empty($password)) { |
|
| 215 | + $message = $this->l->t("Couldn't establish a federated share."); |
|
| 216 | + } else { |
|
| 217 | + $message = $this->l->t("Couldn't establish a federated share, maybe the password was wrong."); |
|
| 218 | + } |
|
| 219 | + return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST); |
|
| 220 | + } |
|
| 221 | + |
|
| 222 | + $body = $response->getBody(); |
|
| 223 | + $result = json_decode($body, true); |
|
| 224 | + |
|
| 225 | + if (is_array($result) && isset($result['remoteUrl'])) { |
|
| 226 | + return new JSONResponse(['message' => $this->l->t('Federated Share request sent, you will receive an invitation. Check your notifications.')]); |
|
| 227 | + } |
|
| 228 | + |
|
| 229 | + // if we doesn't get the expected response we assume that we try to add |
|
| 230 | + // a federated share from a Nextcloud <= 9 server |
|
| 231 | + $message = $this->l->t("Couldn't establish a federated share, it looks like the server to federate with is too old (Nextcloud <= 9)."); |
|
| 232 | + return new JSONResponse(['message' => $message], Http::STATUS_BAD_REQUEST); |
|
| 233 | + } |
|
| 234 | 234 | } |
@@ -53,1036 +53,1036 @@ |
||
| 53 | 53 | */ |
| 54 | 54 | class ShareByMailProvider implements IShareProvider { |
| 55 | 55 | |
| 56 | - /** @var IDBConnection */ |
|
| 57 | - private $dbConnection; |
|
| 58 | - |
|
| 59 | - /** @var ILogger */ |
|
| 60 | - private $logger; |
|
| 61 | - |
|
| 62 | - /** @var ISecureRandom */ |
|
| 63 | - private $secureRandom; |
|
| 64 | - |
|
| 65 | - /** @var IUserManager */ |
|
| 66 | - private $userManager; |
|
| 67 | - |
|
| 68 | - /** @var IRootFolder */ |
|
| 69 | - private $rootFolder; |
|
| 70 | - |
|
| 71 | - /** @var IL10N */ |
|
| 72 | - private $l; |
|
| 73 | - |
|
| 74 | - /** @var IMailer */ |
|
| 75 | - private $mailer; |
|
| 76 | - |
|
| 77 | - /** @var IURLGenerator */ |
|
| 78 | - private $urlGenerator; |
|
| 79 | - |
|
| 80 | - /** @var IManager */ |
|
| 81 | - private $activityManager; |
|
| 82 | - |
|
| 83 | - /** @var SettingsManager */ |
|
| 84 | - private $settingsManager; |
|
| 85 | - |
|
| 86 | - /** @var Defaults */ |
|
| 87 | - private $defaults; |
|
| 88 | - |
|
| 89 | - /** @var IHasher */ |
|
| 90 | - private $hasher; |
|
| 91 | - |
|
| 92 | - /** @var CapabilitiesManager */ |
|
| 93 | - private $capabilitiesManager; |
|
| 94 | - |
|
| 95 | - /** |
|
| 96 | - * Return the identifier of this provider. |
|
| 97 | - * |
|
| 98 | - * @return string Containing only [a-zA-Z0-9] |
|
| 99 | - */ |
|
| 100 | - public function identifier() { |
|
| 101 | - return 'ocMailShare'; |
|
| 102 | - } |
|
| 103 | - |
|
| 104 | - /** |
|
| 105 | - * DefaultShareProvider constructor. |
|
| 106 | - * |
|
| 107 | - * @param IDBConnection $connection |
|
| 108 | - * @param ISecureRandom $secureRandom |
|
| 109 | - * @param IUserManager $userManager |
|
| 110 | - * @param IRootFolder $rootFolder |
|
| 111 | - * @param IL10N $l |
|
| 112 | - * @param ILogger $logger |
|
| 113 | - * @param IMailer $mailer |
|
| 114 | - * @param IURLGenerator $urlGenerator |
|
| 115 | - * @param IManager $activityManager |
|
| 116 | - * @param SettingsManager $settingsManager |
|
| 117 | - * @param Defaults $defaults |
|
| 118 | - * @param IHasher $hasher |
|
| 119 | - * @param CapabilitiesManager $capabilitiesManager |
|
| 120 | - */ |
|
| 121 | - public function __construct( |
|
| 122 | - IDBConnection $connection, |
|
| 123 | - ISecureRandom $secureRandom, |
|
| 124 | - IUserManager $userManager, |
|
| 125 | - IRootFolder $rootFolder, |
|
| 126 | - IL10N $l, |
|
| 127 | - ILogger $logger, |
|
| 128 | - IMailer $mailer, |
|
| 129 | - IURLGenerator $urlGenerator, |
|
| 130 | - IManager $activityManager, |
|
| 131 | - SettingsManager $settingsManager, |
|
| 132 | - Defaults $defaults, |
|
| 133 | - IHasher $hasher, |
|
| 134 | - CapabilitiesManager $capabilitiesManager |
|
| 135 | - ) { |
|
| 136 | - $this->dbConnection = $connection; |
|
| 137 | - $this->secureRandom = $secureRandom; |
|
| 138 | - $this->userManager = $userManager; |
|
| 139 | - $this->rootFolder = $rootFolder; |
|
| 140 | - $this->l = $l; |
|
| 141 | - $this->logger = $logger; |
|
| 142 | - $this->mailer = $mailer; |
|
| 143 | - $this->urlGenerator = $urlGenerator; |
|
| 144 | - $this->activityManager = $activityManager; |
|
| 145 | - $this->settingsManager = $settingsManager; |
|
| 146 | - $this->defaults = $defaults; |
|
| 147 | - $this->hasher = $hasher; |
|
| 148 | - $this->capabilitiesManager = $capabilitiesManager; |
|
| 149 | - } |
|
| 150 | - |
|
| 151 | - /** |
|
| 152 | - * Share a path |
|
| 153 | - * |
|
| 154 | - * @param IShare $share |
|
| 155 | - * @return IShare The share object |
|
| 156 | - * @throws ShareNotFound |
|
| 157 | - * @throws \Exception |
|
| 158 | - */ |
|
| 159 | - public function create(IShare $share) { |
|
| 160 | - |
|
| 161 | - $shareWith = $share->getSharedWith(); |
|
| 162 | - /* |
|
| 56 | + /** @var IDBConnection */ |
|
| 57 | + private $dbConnection; |
|
| 58 | + |
|
| 59 | + /** @var ILogger */ |
|
| 60 | + private $logger; |
|
| 61 | + |
|
| 62 | + /** @var ISecureRandom */ |
|
| 63 | + private $secureRandom; |
|
| 64 | + |
|
| 65 | + /** @var IUserManager */ |
|
| 66 | + private $userManager; |
|
| 67 | + |
|
| 68 | + /** @var IRootFolder */ |
|
| 69 | + private $rootFolder; |
|
| 70 | + |
|
| 71 | + /** @var IL10N */ |
|
| 72 | + private $l; |
|
| 73 | + |
|
| 74 | + /** @var IMailer */ |
|
| 75 | + private $mailer; |
|
| 76 | + |
|
| 77 | + /** @var IURLGenerator */ |
|
| 78 | + private $urlGenerator; |
|
| 79 | + |
|
| 80 | + /** @var IManager */ |
|
| 81 | + private $activityManager; |
|
| 82 | + |
|
| 83 | + /** @var SettingsManager */ |
|
| 84 | + private $settingsManager; |
|
| 85 | + |
|
| 86 | + /** @var Defaults */ |
|
| 87 | + private $defaults; |
|
| 88 | + |
|
| 89 | + /** @var IHasher */ |
|
| 90 | + private $hasher; |
|
| 91 | + |
|
| 92 | + /** @var CapabilitiesManager */ |
|
| 93 | + private $capabilitiesManager; |
|
| 94 | + |
|
| 95 | + /** |
|
| 96 | + * Return the identifier of this provider. |
|
| 97 | + * |
|
| 98 | + * @return string Containing only [a-zA-Z0-9] |
|
| 99 | + */ |
|
| 100 | + public function identifier() { |
|
| 101 | + return 'ocMailShare'; |
|
| 102 | + } |
|
| 103 | + |
|
| 104 | + /** |
|
| 105 | + * DefaultShareProvider constructor. |
|
| 106 | + * |
|
| 107 | + * @param IDBConnection $connection |
|
| 108 | + * @param ISecureRandom $secureRandom |
|
| 109 | + * @param IUserManager $userManager |
|
| 110 | + * @param IRootFolder $rootFolder |
|
| 111 | + * @param IL10N $l |
|
| 112 | + * @param ILogger $logger |
|
| 113 | + * @param IMailer $mailer |
|
| 114 | + * @param IURLGenerator $urlGenerator |
|
| 115 | + * @param IManager $activityManager |
|
| 116 | + * @param SettingsManager $settingsManager |
|
| 117 | + * @param Defaults $defaults |
|
| 118 | + * @param IHasher $hasher |
|
| 119 | + * @param CapabilitiesManager $capabilitiesManager |
|
| 120 | + */ |
|
| 121 | + public function __construct( |
|
| 122 | + IDBConnection $connection, |
|
| 123 | + ISecureRandom $secureRandom, |
|
| 124 | + IUserManager $userManager, |
|
| 125 | + IRootFolder $rootFolder, |
|
| 126 | + IL10N $l, |
|
| 127 | + ILogger $logger, |
|
| 128 | + IMailer $mailer, |
|
| 129 | + IURLGenerator $urlGenerator, |
|
| 130 | + IManager $activityManager, |
|
| 131 | + SettingsManager $settingsManager, |
|
| 132 | + Defaults $defaults, |
|
| 133 | + IHasher $hasher, |
|
| 134 | + CapabilitiesManager $capabilitiesManager |
|
| 135 | + ) { |
|
| 136 | + $this->dbConnection = $connection; |
|
| 137 | + $this->secureRandom = $secureRandom; |
|
| 138 | + $this->userManager = $userManager; |
|
| 139 | + $this->rootFolder = $rootFolder; |
|
| 140 | + $this->l = $l; |
|
| 141 | + $this->logger = $logger; |
|
| 142 | + $this->mailer = $mailer; |
|
| 143 | + $this->urlGenerator = $urlGenerator; |
|
| 144 | + $this->activityManager = $activityManager; |
|
| 145 | + $this->settingsManager = $settingsManager; |
|
| 146 | + $this->defaults = $defaults; |
|
| 147 | + $this->hasher = $hasher; |
|
| 148 | + $this->capabilitiesManager = $capabilitiesManager; |
|
| 149 | + } |
|
| 150 | + |
|
| 151 | + /** |
|
| 152 | + * Share a path |
|
| 153 | + * |
|
| 154 | + * @param IShare $share |
|
| 155 | + * @return IShare The share object |
|
| 156 | + * @throws ShareNotFound |
|
| 157 | + * @throws \Exception |
|
| 158 | + */ |
|
| 159 | + public function create(IShare $share) { |
|
| 160 | + |
|
| 161 | + $shareWith = $share->getSharedWith(); |
|
| 162 | + /* |
|
| 163 | 163 | * Check if file is not already shared with the remote user |
| 164 | 164 | */ |
| 165 | - $alreadyShared = $this->getSharedWith($shareWith, \OCP\Share::SHARE_TYPE_EMAIL, $share->getNode(), 1, 0); |
|
| 166 | - if (!empty($alreadyShared)) { |
|
| 167 | - $message = 'Sharing %s failed, this item is already shared with %s'; |
|
| 168 | - $message_t = $this->l->t('Sharing %s failed, this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); |
|
| 169 | - $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); |
|
| 170 | - throw new \Exception($message_t); |
|
| 171 | - } |
|
| 172 | - |
|
| 173 | - // if the admin enforces a password for all mail shares we create a |
|
| 174 | - // random password and send it to the recipient |
|
| 175 | - $password = ''; |
|
| 176 | - $passwordEnforced = $this->settingsManager->enforcePasswordProtection(); |
|
| 177 | - if ($passwordEnforced) { |
|
| 178 | - $password = $this->autoGeneratePassword($share); |
|
| 179 | - } |
|
| 180 | - |
|
| 181 | - $shareId = $this->createMailShare($share); |
|
| 182 | - $send = $this->sendPassword($share, $password); |
|
| 183 | - if ($passwordEnforced && $send === false) { |
|
| 184 | - $this->sendPasswordToOwner($share, $password); |
|
| 185 | - } |
|
| 186 | - |
|
| 187 | - $this->createShareActivity($share); |
|
| 188 | - $data = $this->getRawShare($shareId); |
|
| 189 | - |
|
| 190 | - return $this->createShareObject($data); |
|
| 191 | - |
|
| 192 | - } |
|
| 193 | - |
|
| 194 | - /** |
|
| 195 | - * auto generate password in case of password enforcement on mail shares |
|
| 196 | - * |
|
| 197 | - * @param IShare $share |
|
| 198 | - * @return string |
|
| 199 | - * @throws \Exception |
|
| 200 | - */ |
|
| 201 | - protected function autoGeneratePassword($share) { |
|
| 202 | - $initiatorUser = $this->userManager->get($share->getSharedBy()); |
|
| 203 | - $initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 204 | - $allowPasswordByMail = $this->settingsManager->sendPasswordByMail(); |
|
| 205 | - |
|
| 206 | - if ($initiatorEMailAddress === null && !$allowPasswordByMail) { |
|
| 207 | - throw new \Exception( |
|
| 208 | - $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 209 | - ); |
|
| 210 | - } |
|
| 211 | - |
|
| 212 | - $passwordPolicy = $this->getPasswordPolicy(); |
|
| 213 | - $passwordCharset = ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS; |
|
| 214 | - $passwordLength = 8; |
|
| 215 | - if (!empty($passwordPolicy)) { |
|
| 216 | - $passwordLength = (int)$passwordPolicy['minLength'] > 0 ? (int)$passwordPolicy['minLength'] : $passwordLength; |
|
| 217 | - $passwordCharset .= $passwordPolicy['enforceSpecialCharacters'] ? ISecureRandom::CHAR_SYMBOLS : ''; |
|
| 218 | - } |
|
| 219 | - |
|
| 220 | - $password = $this->secureRandom->generate($passwordLength, $passwordCharset); |
|
| 221 | - |
|
| 222 | - $share->setPassword($this->hasher->hash($password)); |
|
| 223 | - |
|
| 224 | - return $password; |
|
| 225 | - } |
|
| 226 | - |
|
| 227 | - /** |
|
| 228 | - * get password policy |
|
| 229 | - * |
|
| 230 | - * @return array |
|
| 231 | - */ |
|
| 232 | - protected function getPasswordPolicy() { |
|
| 233 | - $capabilities = $this->capabilitiesManager->getCapabilities(); |
|
| 234 | - if (isset($capabilities['password_policy'])) { |
|
| 235 | - return $capabilities['password_policy']; |
|
| 236 | - } |
|
| 237 | - |
|
| 238 | - return []; |
|
| 239 | - } |
|
| 240 | - |
|
| 241 | - /** |
|
| 242 | - * create activity if a file/folder was shared by mail |
|
| 243 | - * |
|
| 244 | - * @param IShare $share |
|
| 245 | - */ |
|
| 246 | - protected function createShareActivity(IShare $share) { |
|
| 247 | - |
|
| 248 | - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 249 | - |
|
| 250 | - $this->publishActivity( |
|
| 251 | - Activity::SUBJECT_SHARED_EMAIL_SELF, |
|
| 252 | - [$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()], |
|
| 253 | - $share->getSharedBy(), |
|
| 254 | - $share->getNode()->getId(), |
|
| 255 | - $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 256 | - ); |
|
| 257 | - |
|
| 258 | - if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 259 | - $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); |
|
| 260 | - $fileId = $share->getNode()->getId(); |
|
| 261 | - $nodes = $ownerFolder->getById($fileId); |
|
| 262 | - $ownerPath = $nodes[0]->getPath(); |
|
| 263 | - $this->publishActivity( |
|
| 264 | - Activity::SUBJECT_SHARED_EMAIL_BY, |
|
| 265 | - [$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()], |
|
| 266 | - $share->getShareOwner(), |
|
| 267 | - $fileId, |
|
| 268 | - $ownerFolder->getRelativePath($ownerPath) |
|
| 269 | - ); |
|
| 270 | - } |
|
| 271 | - |
|
| 272 | - } |
|
| 273 | - |
|
| 274 | - /** |
|
| 275 | - * create activity if a file/folder was shared by mail |
|
| 276 | - * |
|
| 277 | - * @param IShare $share |
|
| 278 | - * @param string $sharedWith |
|
| 279 | - * @param bool $sendToSelf |
|
| 280 | - */ |
|
| 281 | - protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) { |
|
| 282 | - |
|
| 283 | - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 284 | - |
|
| 285 | - if ($sendToSelf) { |
|
| 286 | - $this->publishActivity( |
|
| 287 | - Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF, |
|
| 288 | - [$userFolder->getRelativePath($share->getNode()->getPath())], |
|
| 289 | - $share->getSharedBy(), |
|
| 290 | - $share->getNode()->getId(), |
|
| 291 | - $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 292 | - ); |
|
| 293 | - } else { |
|
| 294 | - $this->publishActivity( |
|
| 295 | - Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND, |
|
| 296 | - [$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith], |
|
| 297 | - $share->getSharedBy(), |
|
| 298 | - $share->getNode()->getId(), |
|
| 299 | - $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 300 | - ); |
|
| 301 | - } |
|
| 302 | - } |
|
| 303 | - |
|
| 304 | - |
|
| 305 | - /** |
|
| 306 | - * publish activity if a file/folder was shared by mail |
|
| 307 | - * |
|
| 308 | - * @param $subject |
|
| 309 | - * @param $parameters |
|
| 310 | - * @param $affectedUser |
|
| 311 | - * @param $fileId |
|
| 312 | - * @param $filePath |
|
| 313 | - */ |
|
| 314 | - protected function publishActivity($subject, $parameters, $affectedUser, $fileId, $filePath) { |
|
| 315 | - $event = $this->activityManager->generateEvent(); |
|
| 316 | - $event->setApp('sharebymail') |
|
| 317 | - ->setType('shared') |
|
| 318 | - ->setSubject($subject, $parameters) |
|
| 319 | - ->setAffectedUser($affectedUser) |
|
| 320 | - ->setObject('files', $fileId, $filePath); |
|
| 321 | - $this->activityManager->publish($event); |
|
| 322 | - |
|
| 323 | - } |
|
| 324 | - |
|
| 325 | - /** |
|
| 326 | - * @param IShare $share |
|
| 327 | - * @return int |
|
| 328 | - * @throws \Exception |
|
| 329 | - */ |
|
| 330 | - protected function createMailShare(IShare $share) { |
|
| 331 | - $share->setToken($this->generateToken()); |
|
| 332 | - $shareId = $this->addShareToDB( |
|
| 333 | - $share->getNodeId(), |
|
| 334 | - $share->getNodeType(), |
|
| 335 | - $share->getSharedWith(), |
|
| 336 | - $share->getSharedBy(), |
|
| 337 | - $share->getShareOwner(), |
|
| 338 | - $share->getPermissions(), |
|
| 339 | - $share->getToken(), |
|
| 340 | - $share->getPassword() |
|
| 341 | - ); |
|
| 342 | - |
|
| 343 | - try { |
|
| 344 | - $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', |
|
| 345 | - ['token' => $share->getToken()]); |
|
| 346 | - $this->sendMailNotification( |
|
| 347 | - $share->getNode()->getName(), |
|
| 348 | - $link, |
|
| 349 | - $share->getSharedBy(), |
|
| 350 | - $share->getSharedWith(), |
|
| 351 | - $share->getExpirationDate() |
|
| 352 | - ); |
|
| 353 | - } catch (HintException $hintException) { |
|
| 354 | - $this->logger->logException($hintException, [ |
|
| 355 | - 'message' => 'Failed to send share by mail.', |
|
| 356 | - 'level' => ILogger::ERROR, |
|
| 357 | - 'app' => 'sharebymail', |
|
| 358 | - ]); |
|
| 359 | - $this->removeShareFromTable($shareId); |
|
| 360 | - throw $hintException; |
|
| 361 | - } catch (\Exception $e) { |
|
| 362 | - $this->logger->logException($e, [ |
|
| 363 | - 'message' => 'Failed to send share by mail.', |
|
| 364 | - 'level' => ILogger::ERROR, |
|
| 365 | - 'app' => 'sharebymail', |
|
| 366 | - ]); |
|
| 367 | - $this->removeShareFromTable($shareId); |
|
| 368 | - throw new HintException('Failed to send share by mail', |
|
| 369 | - $this->l->t('Failed to send share by email')); |
|
| 370 | - } |
|
| 371 | - |
|
| 372 | - return $shareId; |
|
| 373 | - |
|
| 374 | - } |
|
| 375 | - |
|
| 376 | - /** |
|
| 377 | - * @param string $filename |
|
| 378 | - * @param string $link |
|
| 379 | - * @param string $initiator |
|
| 380 | - * @param string $shareWith |
|
| 381 | - * @param \DateTime|null $expiration |
|
| 382 | - * @throws \Exception If mail couldn't be sent |
|
| 383 | - */ |
|
| 384 | - protected function sendMailNotification($filename, |
|
| 385 | - $link, |
|
| 386 | - $initiator, |
|
| 387 | - $shareWith, |
|
| 388 | - \DateTime $expiration = null) { |
|
| 389 | - $initiatorUser = $this->userManager->get($initiator); |
|
| 390 | - $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 391 | - $message = $this->mailer->createMessage(); |
|
| 392 | - |
|
| 393 | - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [ |
|
| 394 | - 'filename' => $filename, |
|
| 395 | - 'link' => $link, |
|
| 396 | - 'initiator' => $initiatorDisplayName, |
|
| 397 | - 'expiration' => $expiration, |
|
| 398 | - 'shareWith' => $shareWith, |
|
| 399 | - ]); |
|
| 400 | - |
|
| 401 | - $emailTemplate->setSubject($this->l->t('%s shared »%s« with you', array($initiatorDisplayName, $filename))); |
|
| 402 | - $emailTemplate->addHeader(); |
|
| 403 | - $emailTemplate->addHeading($this->l->t('%s shared »%s« with you', [$initiatorDisplayName, $filename]), false); |
|
| 404 | - $text = $this->l->t('%s shared »%s« with you.', [$initiatorDisplayName, $filename]); |
|
| 405 | - |
|
| 406 | - $emailTemplate->addBodyText( |
|
| 407 | - htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')), |
|
| 408 | - $text |
|
| 409 | - ); |
|
| 410 | - $emailTemplate->addBodyButton( |
|
| 411 | - $this->l->t('Open »%s«', [$filename]), |
|
| 412 | - $link |
|
| 413 | - ); |
|
| 414 | - |
|
| 415 | - $message->setTo([$shareWith]); |
|
| 416 | - |
|
| 417 | - // The "From" contains the sharers name |
|
| 418 | - $instanceName = $this->defaults->getName(); |
|
| 419 | - $senderName = $this->l->t( |
|
| 420 | - '%s via %s', |
|
| 421 | - [ |
|
| 422 | - $initiatorDisplayName, |
|
| 423 | - $instanceName |
|
| 424 | - ] |
|
| 425 | - ); |
|
| 426 | - $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 427 | - |
|
| 428 | - // The "Reply-To" is set to the sharer if an mail address is configured |
|
| 429 | - // also the default footer contains a "Do not reply" which needs to be adjusted. |
|
| 430 | - $initiatorEmail = $initiatorUser->getEMailAddress(); |
|
| 431 | - if($initiatorEmail !== null) { |
|
| 432 | - $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]); |
|
| 433 | - $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : '')); |
|
| 434 | - } else { |
|
| 435 | - $emailTemplate->addFooter(); |
|
| 436 | - } |
|
| 437 | - |
|
| 438 | - $message->useTemplate($emailTemplate); |
|
| 439 | - $this->mailer->send($message); |
|
| 440 | - } |
|
| 441 | - |
|
| 442 | - /** |
|
| 443 | - * send password to recipient of a mail share |
|
| 444 | - * |
|
| 445 | - * @param IShare $share |
|
| 446 | - * @param string $password |
|
| 447 | - * @return bool |
|
| 448 | - */ |
|
| 449 | - protected function sendPassword(IShare $share, $password) { |
|
| 450 | - |
|
| 451 | - $filename = $share->getNode()->getName(); |
|
| 452 | - $initiator = $share->getSharedBy(); |
|
| 453 | - $shareWith = $share->getSharedWith(); |
|
| 454 | - |
|
| 455 | - if ($password === '' || $this->settingsManager->sendPasswordByMail() === false) { |
|
| 456 | - return false; |
|
| 457 | - } |
|
| 458 | - |
|
| 459 | - $initiatorUser = $this->userManager->get($initiator); |
|
| 460 | - $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 461 | - $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 462 | - |
|
| 463 | - $plainBodyPart = $this->l->t("%s shared »%s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]); |
|
| 464 | - $htmlBodyPart = $this->l->t('%s shared »%s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]); |
|
| 465 | - |
|
| 466 | - $message = $this->mailer->createMessage(); |
|
| 467 | - |
|
| 468 | - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [ |
|
| 469 | - 'filename' => $filename, |
|
| 470 | - 'password' => $password, |
|
| 471 | - 'initiator' => $initiatorDisplayName, |
|
| 472 | - 'initiatorEmail' => $initiatorEmailAddress, |
|
| 473 | - 'shareWith' => $shareWith, |
|
| 474 | - ]); |
|
| 475 | - |
|
| 476 | - $emailTemplate->setSubject($this->l->t('Password to access »%s« shared to you by %s', [$filename, $initiatorDisplayName])); |
|
| 477 | - $emailTemplate->addHeader(); |
|
| 478 | - $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 479 | - $emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart); |
|
| 480 | - $emailTemplate->addBodyText($this->l->t('It is protected with the following password: %s', [$password])); |
|
| 481 | - |
|
| 482 | - // The "From" contains the sharers name |
|
| 483 | - $instanceName = $this->defaults->getName(); |
|
| 484 | - $senderName = $this->l->t( |
|
| 485 | - '%s via %s', |
|
| 486 | - [ |
|
| 487 | - $initiatorDisplayName, |
|
| 488 | - $instanceName |
|
| 489 | - ] |
|
| 490 | - ); |
|
| 491 | - $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 492 | - if ($initiatorEmailAddress !== null) { |
|
| 493 | - $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); |
|
| 494 | - $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); |
|
| 495 | - } else { |
|
| 496 | - $emailTemplate->addFooter(); |
|
| 497 | - } |
|
| 498 | - |
|
| 499 | - $message->setTo([$shareWith]); |
|
| 500 | - $message->useTemplate($emailTemplate); |
|
| 501 | - $this->mailer->send($message); |
|
| 502 | - |
|
| 503 | - $this->createPasswordSendActivity($share, $shareWith, false); |
|
| 504 | - |
|
| 505 | - return true; |
|
| 506 | - } |
|
| 507 | - |
|
| 508 | - /** |
|
| 509 | - * send auto generated password to the owner. This happens if the admin enforces |
|
| 510 | - * a password for mail shares and forbid to send the password by mail to the recipient |
|
| 511 | - * |
|
| 512 | - * @param IShare $share |
|
| 513 | - * @param string $password |
|
| 514 | - * @return bool |
|
| 515 | - * @throws \Exception |
|
| 516 | - */ |
|
| 517 | - protected function sendPasswordToOwner(IShare $share, $password) { |
|
| 518 | - |
|
| 519 | - $filename = $share->getNode()->getName(); |
|
| 520 | - $initiator = $this->userManager->get($share->getSharedBy()); |
|
| 521 | - $initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null; |
|
| 522 | - $initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy(); |
|
| 523 | - $shareWith = $share->getSharedWith(); |
|
| 524 | - |
|
| 525 | - if ($initiatorEMailAddress === null) { |
|
| 526 | - throw new \Exception( |
|
| 527 | - $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 528 | - ); |
|
| 529 | - } |
|
| 530 | - |
|
| 531 | - $bodyPart = $this->l->t("You just shared »%s« with %s. The share was already send to the recipient. Due to the security policies defined by the administrator of %s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.", [$filename, $shareWith, $this->defaults->getName()]); |
|
| 532 | - |
|
| 533 | - $message = $this->mailer->createMessage(); |
|
| 534 | - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [ |
|
| 535 | - 'filename' => $filename, |
|
| 536 | - 'password' => $password, |
|
| 537 | - 'initiator' => $initiatorDisplayName, |
|
| 538 | - 'initiatorEmail' => $initiatorEMailAddress, |
|
| 539 | - 'shareWith' => $shareWith, |
|
| 540 | - ]); |
|
| 541 | - |
|
| 542 | - $emailTemplate->setSubject($this->l->t('Password to access »%s« shared with %s', [$filename, $shareWith])); |
|
| 543 | - $emailTemplate->addHeader(); |
|
| 544 | - $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 545 | - $emailTemplate->addBodyText($bodyPart); |
|
| 546 | - $emailTemplate->addBodyText($this->l->t('This is the password: %s', [$password])); |
|
| 547 | - $emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.')); |
|
| 548 | - $emailTemplate->addFooter(); |
|
| 549 | - |
|
| 550 | - if ($initiatorEMailAddress) { |
|
| 551 | - $message->setFrom([$initiatorEMailAddress => $initiatorDisplayName]); |
|
| 552 | - } |
|
| 553 | - $message->setTo([$initiatorEMailAddress => $initiatorDisplayName]); |
|
| 554 | - $message->useTemplate($emailTemplate); |
|
| 555 | - $this->mailer->send($message); |
|
| 556 | - |
|
| 557 | - $this->createPasswordSendActivity($share, $shareWith, true); |
|
| 558 | - |
|
| 559 | - return true; |
|
| 560 | - } |
|
| 561 | - |
|
| 562 | - /** |
|
| 563 | - * generate share token |
|
| 564 | - * |
|
| 565 | - * @return string |
|
| 566 | - */ |
|
| 567 | - protected function generateToken($size = 15) { |
|
| 568 | - $token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE); |
|
| 569 | - return $token; |
|
| 570 | - } |
|
| 571 | - |
|
| 572 | - /** |
|
| 573 | - * Get all children of this share |
|
| 574 | - * |
|
| 575 | - * @param IShare $parent |
|
| 576 | - * @return IShare[] |
|
| 577 | - */ |
|
| 578 | - public function getChildren(IShare $parent) { |
|
| 579 | - $children = []; |
|
| 580 | - |
|
| 581 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 582 | - $qb->select('*') |
|
| 583 | - ->from('share') |
|
| 584 | - ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) |
|
| 585 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 586 | - ->orderBy('id'); |
|
| 587 | - |
|
| 588 | - $cursor = $qb->execute(); |
|
| 589 | - while($data = $cursor->fetch()) { |
|
| 590 | - $children[] = $this->createShareObject($data); |
|
| 591 | - } |
|
| 592 | - $cursor->closeCursor(); |
|
| 593 | - |
|
| 594 | - return $children; |
|
| 595 | - } |
|
| 596 | - |
|
| 597 | - /** |
|
| 598 | - * add share to the database and return the ID |
|
| 599 | - * |
|
| 600 | - * @param int $itemSource |
|
| 601 | - * @param string $itemType |
|
| 602 | - * @param string $shareWith |
|
| 603 | - * @param string $sharedBy |
|
| 604 | - * @param string $uidOwner |
|
| 605 | - * @param int $permissions |
|
| 606 | - * @param string $token |
|
| 607 | - * @return int |
|
| 608 | - */ |
|
| 609 | - protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password) { |
|
| 610 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 611 | - $qb->insert('share') |
|
| 612 | - ->setValue('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)) |
|
| 613 | - ->setValue('item_type', $qb->createNamedParameter($itemType)) |
|
| 614 | - ->setValue('item_source', $qb->createNamedParameter($itemSource)) |
|
| 615 | - ->setValue('file_source', $qb->createNamedParameter($itemSource)) |
|
| 616 | - ->setValue('share_with', $qb->createNamedParameter($shareWith)) |
|
| 617 | - ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) |
|
| 618 | - ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) |
|
| 619 | - ->setValue('permissions', $qb->createNamedParameter($permissions)) |
|
| 620 | - ->setValue('token', $qb->createNamedParameter($token)) |
|
| 621 | - ->setValue('password', $qb->createNamedParameter($password)) |
|
| 622 | - ->setValue('stime', $qb->createNamedParameter(time())); |
|
| 623 | - |
|
| 624 | - /* |
|
| 165 | + $alreadyShared = $this->getSharedWith($shareWith, \OCP\Share::SHARE_TYPE_EMAIL, $share->getNode(), 1, 0); |
|
| 166 | + if (!empty($alreadyShared)) { |
|
| 167 | + $message = 'Sharing %s failed, this item is already shared with %s'; |
|
| 168 | + $message_t = $this->l->t('Sharing %s failed, this item is already shared with %s', array($share->getNode()->getName(), $shareWith)); |
|
| 169 | + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); |
|
| 170 | + throw new \Exception($message_t); |
|
| 171 | + } |
|
| 172 | + |
|
| 173 | + // if the admin enforces a password for all mail shares we create a |
|
| 174 | + // random password and send it to the recipient |
|
| 175 | + $password = ''; |
|
| 176 | + $passwordEnforced = $this->settingsManager->enforcePasswordProtection(); |
|
| 177 | + if ($passwordEnforced) { |
|
| 178 | + $password = $this->autoGeneratePassword($share); |
|
| 179 | + } |
|
| 180 | + |
|
| 181 | + $shareId = $this->createMailShare($share); |
|
| 182 | + $send = $this->sendPassword($share, $password); |
|
| 183 | + if ($passwordEnforced && $send === false) { |
|
| 184 | + $this->sendPasswordToOwner($share, $password); |
|
| 185 | + } |
|
| 186 | + |
|
| 187 | + $this->createShareActivity($share); |
|
| 188 | + $data = $this->getRawShare($shareId); |
|
| 189 | + |
|
| 190 | + return $this->createShareObject($data); |
|
| 191 | + |
|
| 192 | + } |
|
| 193 | + |
|
| 194 | + /** |
|
| 195 | + * auto generate password in case of password enforcement on mail shares |
|
| 196 | + * |
|
| 197 | + * @param IShare $share |
|
| 198 | + * @return string |
|
| 199 | + * @throws \Exception |
|
| 200 | + */ |
|
| 201 | + protected function autoGeneratePassword($share) { |
|
| 202 | + $initiatorUser = $this->userManager->get($share->getSharedBy()); |
|
| 203 | + $initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 204 | + $allowPasswordByMail = $this->settingsManager->sendPasswordByMail(); |
|
| 205 | + |
|
| 206 | + if ($initiatorEMailAddress === null && !$allowPasswordByMail) { |
|
| 207 | + throw new \Exception( |
|
| 208 | + $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 209 | + ); |
|
| 210 | + } |
|
| 211 | + |
|
| 212 | + $passwordPolicy = $this->getPasswordPolicy(); |
|
| 213 | + $passwordCharset = ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS; |
|
| 214 | + $passwordLength = 8; |
|
| 215 | + if (!empty($passwordPolicy)) { |
|
| 216 | + $passwordLength = (int)$passwordPolicy['minLength'] > 0 ? (int)$passwordPolicy['minLength'] : $passwordLength; |
|
| 217 | + $passwordCharset .= $passwordPolicy['enforceSpecialCharacters'] ? ISecureRandom::CHAR_SYMBOLS : ''; |
|
| 218 | + } |
|
| 219 | + |
|
| 220 | + $password = $this->secureRandom->generate($passwordLength, $passwordCharset); |
|
| 221 | + |
|
| 222 | + $share->setPassword($this->hasher->hash($password)); |
|
| 223 | + |
|
| 224 | + return $password; |
|
| 225 | + } |
|
| 226 | + |
|
| 227 | + /** |
|
| 228 | + * get password policy |
|
| 229 | + * |
|
| 230 | + * @return array |
|
| 231 | + */ |
|
| 232 | + protected function getPasswordPolicy() { |
|
| 233 | + $capabilities = $this->capabilitiesManager->getCapabilities(); |
|
| 234 | + if (isset($capabilities['password_policy'])) { |
|
| 235 | + return $capabilities['password_policy']; |
|
| 236 | + } |
|
| 237 | + |
|
| 238 | + return []; |
|
| 239 | + } |
|
| 240 | + |
|
| 241 | + /** |
|
| 242 | + * create activity if a file/folder was shared by mail |
|
| 243 | + * |
|
| 244 | + * @param IShare $share |
|
| 245 | + */ |
|
| 246 | + protected function createShareActivity(IShare $share) { |
|
| 247 | + |
|
| 248 | + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 249 | + |
|
| 250 | + $this->publishActivity( |
|
| 251 | + Activity::SUBJECT_SHARED_EMAIL_SELF, |
|
| 252 | + [$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()], |
|
| 253 | + $share->getSharedBy(), |
|
| 254 | + $share->getNode()->getId(), |
|
| 255 | + $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 256 | + ); |
|
| 257 | + |
|
| 258 | + if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 259 | + $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); |
|
| 260 | + $fileId = $share->getNode()->getId(); |
|
| 261 | + $nodes = $ownerFolder->getById($fileId); |
|
| 262 | + $ownerPath = $nodes[0]->getPath(); |
|
| 263 | + $this->publishActivity( |
|
| 264 | + Activity::SUBJECT_SHARED_EMAIL_BY, |
|
| 265 | + [$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()], |
|
| 266 | + $share->getShareOwner(), |
|
| 267 | + $fileId, |
|
| 268 | + $ownerFolder->getRelativePath($ownerPath) |
|
| 269 | + ); |
|
| 270 | + } |
|
| 271 | + |
|
| 272 | + } |
|
| 273 | + |
|
| 274 | + /** |
|
| 275 | + * create activity if a file/folder was shared by mail |
|
| 276 | + * |
|
| 277 | + * @param IShare $share |
|
| 278 | + * @param string $sharedWith |
|
| 279 | + * @param bool $sendToSelf |
|
| 280 | + */ |
|
| 281 | + protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) { |
|
| 282 | + |
|
| 283 | + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 284 | + |
|
| 285 | + if ($sendToSelf) { |
|
| 286 | + $this->publishActivity( |
|
| 287 | + Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF, |
|
| 288 | + [$userFolder->getRelativePath($share->getNode()->getPath())], |
|
| 289 | + $share->getSharedBy(), |
|
| 290 | + $share->getNode()->getId(), |
|
| 291 | + $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 292 | + ); |
|
| 293 | + } else { |
|
| 294 | + $this->publishActivity( |
|
| 295 | + Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND, |
|
| 296 | + [$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith], |
|
| 297 | + $share->getSharedBy(), |
|
| 298 | + $share->getNode()->getId(), |
|
| 299 | + $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 300 | + ); |
|
| 301 | + } |
|
| 302 | + } |
|
| 303 | + |
|
| 304 | + |
|
| 305 | + /** |
|
| 306 | + * publish activity if a file/folder was shared by mail |
|
| 307 | + * |
|
| 308 | + * @param $subject |
|
| 309 | + * @param $parameters |
|
| 310 | + * @param $affectedUser |
|
| 311 | + * @param $fileId |
|
| 312 | + * @param $filePath |
|
| 313 | + */ |
|
| 314 | + protected function publishActivity($subject, $parameters, $affectedUser, $fileId, $filePath) { |
|
| 315 | + $event = $this->activityManager->generateEvent(); |
|
| 316 | + $event->setApp('sharebymail') |
|
| 317 | + ->setType('shared') |
|
| 318 | + ->setSubject($subject, $parameters) |
|
| 319 | + ->setAffectedUser($affectedUser) |
|
| 320 | + ->setObject('files', $fileId, $filePath); |
|
| 321 | + $this->activityManager->publish($event); |
|
| 322 | + |
|
| 323 | + } |
|
| 324 | + |
|
| 325 | + /** |
|
| 326 | + * @param IShare $share |
|
| 327 | + * @return int |
|
| 328 | + * @throws \Exception |
|
| 329 | + */ |
|
| 330 | + protected function createMailShare(IShare $share) { |
|
| 331 | + $share->setToken($this->generateToken()); |
|
| 332 | + $shareId = $this->addShareToDB( |
|
| 333 | + $share->getNodeId(), |
|
| 334 | + $share->getNodeType(), |
|
| 335 | + $share->getSharedWith(), |
|
| 336 | + $share->getSharedBy(), |
|
| 337 | + $share->getShareOwner(), |
|
| 338 | + $share->getPermissions(), |
|
| 339 | + $share->getToken(), |
|
| 340 | + $share->getPassword() |
|
| 341 | + ); |
|
| 342 | + |
|
| 343 | + try { |
|
| 344 | + $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', |
|
| 345 | + ['token' => $share->getToken()]); |
|
| 346 | + $this->sendMailNotification( |
|
| 347 | + $share->getNode()->getName(), |
|
| 348 | + $link, |
|
| 349 | + $share->getSharedBy(), |
|
| 350 | + $share->getSharedWith(), |
|
| 351 | + $share->getExpirationDate() |
|
| 352 | + ); |
|
| 353 | + } catch (HintException $hintException) { |
|
| 354 | + $this->logger->logException($hintException, [ |
|
| 355 | + 'message' => 'Failed to send share by mail.', |
|
| 356 | + 'level' => ILogger::ERROR, |
|
| 357 | + 'app' => 'sharebymail', |
|
| 358 | + ]); |
|
| 359 | + $this->removeShareFromTable($shareId); |
|
| 360 | + throw $hintException; |
|
| 361 | + } catch (\Exception $e) { |
|
| 362 | + $this->logger->logException($e, [ |
|
| 363 | + 'message' => 'Failed to send share by mail.', |
|
| 364 | + 'level' => ILogger::ERROR, |
|
| 365 | + 'app' => 'sharebymail', |
|
| 366 | + ]); |
|
| 367 | + $this->removeShareFromTable($shareId); |
|
| 368 | + throw new HintException('Failed to send share by mail', |
|
| 369 | + $this->l->t('Failed to send share by email')); |
|
| 370 | + } |
|
| 371 | + |
|
| 372 | + return $shareId; |
|
| 373 | + |
|
| 374 | + } |
|
| 375 | + |
|
| 376 | + /** |
|
| 377 | + * @param string $filename |
|
| 378 | + * @param string $link |
|
| 379 | + * @param string $initiator |
|
| 380 | + * @param string $shareWith |
|
| 381 | + * @param \DateTime|null $expiration |
|
| 382 | + * @throws \Exception If mail couldn't be sent |
|
| 383 | + */ |
|
| 384 | + protected function sendMailNotification($filename, |
|
| 385 | + $link, |
|
| 386 | + $initiator, |
|
| 387 | + $shareWith, |
|
| 388 | + \DateTime $expiration = null) { |
|
| 389 | + $initiatorUser = $this->userManager->get($initiator); |
|
| 390 | + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 391 | + $message = $this->mailer->createMessage(); |
|
| 392 | + |
|
| 393 | + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [ |
|
| 394 | + 'filename' => $filename, |
|
| 395 | + 'link' => $link, |
|
| 396 | + 'initiator' => $initiatorDisplayName, |
|
| 397 | + 'expiration' => $expiration, |
|
| 398 | + 'shareWith' => $shareWith, |
|
| 399 | + ]); |
|
| 400 | + |
|
| 401 | + $emailTemplate->setSubject($this->l->t('%s shared »%s« with you', array($initiatorDisplayName, $filename))); |
|
| 402 | + $emailTemplate->addHeader(); |
|
| 403 | + $emailTemplate->addHeading($this->l->t('%s shared »%s« with you', [$initiatorDisplayName, $filename]), false); |
|
| 404 | + $text = $this->l->t('%s shared »%s« with you.', [$initiatorDisplayName, $filename]); |
|
| 405 | + |
|
| 406 | + $emailTemplate->addBodyText( |
|
| 407 | + htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')), |
|
| 408 | + $text |
|
| 409 | + ); |
|
| 410 | + $emailTemplate->addBodyButton( |
|
| 411 | + $this->l->t('Open »%s«', [$filename]), |
|
| 412 | + $link |
|
| 413 | + ); |
|
| 414 | + |
|
| 415 | + $message->setTo([$shareWith]); |
|
| 416 | + |
|
| 417 | + // The "From" contains the sharers name |
|
| 418 | + $instanceName = $this->defaults->getName(); |
|
| 419 | + $senderName = $this->l->t( |
|
| 420 | + '%s via %s', |
|
| 421 | + [ |
|
| 422 | + $initiatorDisplayName, |
|
| 423 | + $instanceName |
|
| 424 | + ] |
|
| 425 | + ); |
|
| 426 | + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 427 | + |
|
| 428 | + // The "Reply-To" is set to the sharer if an mail address is configured |
|
| 429 | + // also the default footer contains a "Do not reply" which needs to be adjusted. |
|
| 430 | + $initiatorEmail = $initiatorUser->getEMailAddress(); |
|
| 431 | + if($initiatorEmail !== null) { |
|
| 432 | + $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]); |
|
| 433 | + $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : '')); |
|
| 434 | + } else { |
|
| 435 | + $emailTemplate->addFooter(); |
|
| 436 | + } |
|
| 437 | + |
|
| 438 | + $message->useTemplate($emailTemplate); |
|
| 439 | + $this->mailer->send($message); |
|
| 440 | + } |
|
| 441 | + |
|
| 442 | + /** |
|
| 443 | + * send password to recipient of a mail share |
|
| 444 | + * |
|
| 445 | + * @param IShare $share |
|
| 446 | + * @param string $password |
|
| 447 | + * @return bool |
|
| 448 | + */ |
|
| 449 | + protected function sendPassword(IShare $share, $password) { |
|
| 450 | + |
|
| 451 | + $filename = $share->getNode()->getName(); |
|
| 452 | + $initiator = $share->getSharedBy(); |
|
| 453 | + $shareWith = $share->getSharedWith(); |
|
| 454 | + |
|
| 455 | + if ($password === '' || $this->settingsManager->sendPasswordByMail() === false) { |
|
| 456 | + return false; |
|
| 457 | + } |
|
| 458 | + |
|
| 459 | + $initiatorUser = $this->userManager->get($initiator); |
|
| 460 | + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 461 | + $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 462 | + |
|
| 463 | + $plainBodyPart = $this->l->t("%s shared »%s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]); |
|
| 464 | + $htmlBodyPart = $this->l->t('%s shared »%s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]); |
|
| 465 | + |
|
| 466 | + $message = $this->mailer->createMessage(); |
|
| 467 | + |
|
| 468 | + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [ |
|
| 469 | + 'filename' => $filename, |
|
| 470 | + 'password' => $password, |
|
| 471 | + 'initiator' => $initiatorDisplayName, |
|
| 472 | + 'initiatorEmail' => $initiatorEmailAddress, |
|
| 473 | + 'shareWith' => $shareWith, |
|
| 474 | + ]); |
|
| 475 | + |
|
| 476 | + $emailTemplate->setSubject($this->l->t('Password to access »%s« shared to you by %s', [$filename, $initiatorDisplayName])); |
|
| 477 | + $emailTemplate->addHeader(); |
|
| 478 | + $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 479 | + $emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart); |
|
| 480 | + $emailTemplate->addBodyText($this->l->t('It is protected with the following password: %s', [$password])); |
|
| 481 | + |
|
| 482 | + // The "From" contains the sharers name |
|
| 483 | + $instanceName = $this->defaults->getName(); |
|
| 484 | + $senderName = $this->l->t( |
|
| 485 | + '%s via %s', |
|
| 486 | + [ |
|
| 487 | + $initiatorDisplayName, |
|
| 488 | + $instanceName |
|
| 489 | + ] |
|
| 490 | + ); |
|
| 491 | + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 492 | + if ($initiatorEmailAddress !== null) { |
|
| 493 | + $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); |
|
| 494 | + $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); |
|
| 495 | + } else { |
|
| 496 | + $emailTemplate->addFooter(); |
|
| 497 | + } |
|
| 498 | + |
|
| 499 | + $message->setTo([$shareWith]); |
|
| 500 | + $message->useTemplate($emailTemplate); |
|
| 501 | + $this->mailer->send($message); |
|
| 502 | + |
|
| 503 | + $this->createPasswordSendActivity($share, $shareWith, false); |
|
| 504 | + |
|
| 505 | + return true; |
|
| 506 | + } |
|
| 507 | + |
|
| 508 | + /** |
|
| 509 | + * send auto generated password to the owner. This happens if the admin enforces |
|
| 510 | + * a password for mail shares and forbid to send the password by mail to the recipient |
|
| 511 | + * |
|
| 512 | + * @param IShare $share |
|
| 513 | + * @param string $password |
|
| 514 | + * @return bool |
|
| 515 | + * @throws \Exception |
|
| 516 | + */ |
|
| 517 | + protected function sendPasswordToOwner(IShare $share, $password) { |
|
| 518 | + |
|
| 519 | + $filename = $share->getNode()->getName(); |
|
| 520 | + $initiator = $this->userManager->get($share->getSharedBy()); |
|
| 521 | + $initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null; |
|
| 522 | + $initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy(); |
|
| 523 | + $shareWith = $share->getSharedWith(); |
|
| 524 | + |
|
| 525 | + if ($initiatorEMailAddress === null) { |
|
| 526 | + throw new \Exception( |
|
| 527 | + $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 528 | + ); |
|
| 529 | + } |
|
| 530 | + |
|
| 531 | + $bodyPart = $this->l->t("You just shared »%s« with %s. The share was already send to the recipient. Due to the security policies defined by the administrator of %s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.", [$filename, $shareWith, $this->defaults->getName()]); |
|
| 532 | + |
|
| 533 | + $message = $this->mailer->createMessage(); |
|
| 534 | + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [ |
|
| 535 | + 'filename' => $filename, |
|
| 536 | + 'password' => $password, |
|
| 537 | + 'initiator' => $initiatorDisplayName, |
|
| 538 | + 'initiatorEmail' => $initiatorEMailAddress, |
|
| 539 | + 'shareWith' => $shareWith, |
|
| 540 | + ]); |
|
| 541 | + |
|
| 542 | + $emailTemplate->setSubject($this->l->t('Password to access »%s« shared with %s', [$filename, $shareWith])); |
|
| 543 | + $emailTemplate->addHeader(); |
|
| 544 | + $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 545 | + $emailTemplate->addBodyText($bodyPart); |
|
| 546 | + $emailTemplate->addBodyText($this->l->t('This is the password: %s', [$password])); |
|
| 547 | + $emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.')); |
|
| 548 | + $emailTemplate->addFooter(); |
|
| 549 | + |
|
| 550 | + if ($initiatorEMailAddress) { |
|
| 551 | + $message->setFrom([$initiatorEMailAddress => $initiatorDisplayName]); |
|
| 552 | + } |
|
| 553 | + $message->setTo([$initiatorEMailAddress => $initiatorDisplayName]); |
|
| 554 | + $message->useTemplate($emailTemplate); |
|
| 555 | + $this->mailer->send($message); |
|
| 556 | + |
|
| 557 | + $this->createPasswordSendActivity($share, $shareWith, true); |
|
| 558 | + |
|
| 559 | + return true; |
|
| 560 | + } |
|
| 561 | + |
|
| 562 | + /** |
|
| 563 | + * generate share token |
|
| 564 | + * |
|
| 565 | + * @return string |
|
| 566 | + */ |
|
| 567 | + protected function generateToken($size = 15) { |
|
| 568 | + $token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE); |
|
| 569 | + return $token; |
|
| 570 | + } |
|
| 571 | + |
|
| 572 | + /** |
|
| 573 | + * Get all children of this share |
|
| 574 | + * |
|
| 575 | + * @param IShare $parent |
|
| 576 | + * @return IShare[] |
|
| 577 | + */ |
|
| 578 | + public function getChildren(IShare $parent) { |
|
| 579 | + $children = []; |
|
| 580 | + |
|
| 581 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 582 | + $qb->select('*') |
|
| 583 | + ->from('share') |
|
| 584 | + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) |
|
| 585 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 586 | + ->orderBy('id'); |
|
| 587 | + |
|
| 588 | + $cursor = $qb->execute(); |
|
| 589 | + while($data = $cursor->fetch()) { |
|
| 590 | + $children[] = $this->createShareObject($data); |
|
| 591 | + } |
|
| 592 | + $cursor->closeCursor(); |
|
| 593 | + |
|
| 594 | + return $children; |
|
| 595 | + } |
|
| 596 | + |
|
| 597 | + /** |
|
| 598 | + * add share to the database and return the ID |
|
| 599 | + * |
|
| 600 | + * @param int $itemSource |
|
| 601 | + * @param string $itemType |
|
| 602 | + * @param string $shareWith |
|
| 603 | + * @param string $sharedBy |
|
| 604 | + * @param string $uidOwner |
|
| 605 | + * @param int $permissions |
|
| 606 | + * @param string $token |
|
| 607 | + * @return int |
|
| 608 | + */ |
|
| 609 | + protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password) { |
|
| 610 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 611 | + $qb->insert('share') |
|
| 612 | + ->setValue('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)) |
|
| 613 | + ->setValue('item_type', $qb->createNamedParameter($itemType)) |
|
| 614 | + ->setValue('item_source', $qb->createNamedParameter($itemSource)) |
|
| 615 | + ->setValue('file_source', $qb->createNamedParameter($itemSource)) |
|
| 616 | + ->setValue('share_with', $qb->createNamedParameter($shareWith)) |
|
| 617 | + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) |
|
| 618 | + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) |
|
| 619 | + ->setValue('permissions', $qb->createNamedParameter($permissions)) |
|
| 620 | + ->setValue('token', $qb->createNamedParameter($token)) |
|
| 621 | + ->setValue('password', $qb->createNamedParameter($password)) |
|
| 622 | + ->setValue('stime', $qb->createNamedParameter(time())); |
|
| 623 | + |
|
| 624 | + /* |
|
| 625 | 625 | * Added to fix https://github.com/owncloud/core/issues/22215 |
| 626 | 626 | * Can be removed once we get rid of ajax/share.php |
| 627 | 627 | */ |
| 628 | - $qb->setValue('file_target', $qb->createNamedParameter('')); |
|
| 628 | + $qb->setValue('file_target', $qb->createNamedParameter('')); |
|
| 629 | 629 | |
| 630 | - $qb->execute(); |
|
| 631 | - $id = $qb->getLastInsertId(); |
|
| 630 | + $qb->execute(); |
|
| 631 | + $id = $qb->getLastInsertId(); |
|
| 632 | 632 | |
| 633 | - return (int)$id; |
|
| 634 | - } |
|
| 633 | + return (int)$id; |
|
| 634 | + } |
|
| 635 | 635 | |
| 636 | - /** |
|
| 637 | - * Update a share |
|
| 638 | - * |
|
| 639 | - * @param IShare $share |
|
| 640 | - * @param string|null $plainTextPassword |
|
| 641 | - * @return IShare The share object |
|
| 642 | - */ |
|
| 643 | - public function update(IShare $share, $plainTextPassword = null) { |
|
| 636 | + /** |
|
| 637 | + * Update a share |
|
| 638 | + * |
|
| 639 | + * @param IShare $share |
|
| 640 | + * @param string|null $plainTextPassword |
|
| 641 | + * @return IShare The share object |
|
| 642 | + */ |
|
| 643 | + public function update(IShare $share, $plainTextPassword = null) { |
|
| 644 | 644 | |
| 645 | - $originalShare = $this->getShareById($share->getId()); |
|
| 645 | + $originalShare = $this->getShareById($share->getId()); |
|
| 646 | 646 | |
| 647 | - // a real password was given |
|
| 648 | - $validPassword = $plainTextPassword !== null && $plainTextPassword !== ''; |
|
| 647 | + // a real password was given |
|
| 648 | + $validPassword = $plainTextPassword !== null && $plainTextPassword !== ''; |
|
| 649 | 649 | |
| 650 | - if($validPassword && $originalShare->getPassword() !== $share->getPassword()) { |
|
| 651 | - $this->sendPassword($share, $plainTextPassword); |
|
| 652 | - } |
|
| 653 | - /* |
|
| 650 | + if($validPassword && $originalShare->getPassword() !== $share->getPassword()) { |
|
| 651 | + $this->sendPassword($share, $plainTextPassword); |
|
| 652 | + } |
|
| 653 | + /* |
|
| 654 | 654 | * We allow updating the permissions and password of mail shares |
| 655 | 655 | */ |
| 656 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 657 | - $qb->update('share') |
|
| 658 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) |
|
| 659 | - ->set('permissions', $qb->createNamedParameter($share->getPermissions())) |
|
| 660 | - ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) |
|
| 661 | - ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) |
|
| 662 | - ->set('password', $qb->createNamedParameter($share->getPassword())) |
|
| 663 | - ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) |
|
| 664 | - ->execute(); |
|
| 665 | - |
|
| 666 | - return $share; |
|
| 667 | - } |
|
| 668 | - |
|
| 669 | - /** |
|
| 670 | - * @inheritdoc |
|
| 671 | - */ |
|
| 672 | - public function move(IShare $share, $recipient) { |
|
| 673 | - /** |
|
| 674 | - * nothing to do here, mail shares are only outgoing shares |
|
| 675 | - */ |
|
| 676 | - return $share; |
|
| 677 | - } |
|
| 678 | - |
|
| 679 | - /** |
|
| 680 | - * Delete a share (owner unShares the file) |
|
| 681 | - * |
|
| 682 | - * @param IShare $share |
|
| 683 | - */ |
|
| 684 | - public function delete(IShare $share) { |
|
| 685 | - $this->removeShareFromTable($share->getId()); |
|
| 686 | - } |
|
| 687 | - |
|
| 688 | - /** |
|
| 689 | - * @inheritdoc |
|
| 690 | - */ |
|
| 691 | - public function deleteFromSelf(IShare $share, $recipient) { |
|
| 692 | - // nothing to do here, mail shares are only outgoing shares |
|
| 693 | - } |
|
| 694 | - |
|
| 695 | - /** |
|
| 696 | - * @inheritdoc |
|
| 697 | - */ |
|
| 698 | - public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { |
|
| 699 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 700 | - $qb->select('*') |
|
| 701 | - ->from('share'); |
|
| 702 | - |
|
| 703 | - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); |
|
| 704 | - |
|
| 705 | - /** |
|
| 706 | - * Reshares for this user are shares where they are the owner. |
|
| 707 | - */ |
|
| 708 | - if ($reshares === false) { |
|
| 709 | - //Special case for old shares created via the web UI |
|
| 710 | - $or1 = $qb->expr()->andX( |
|
| 711 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 712 | - $qb->expr()->isNull('uid_initiator') |
|
| 713 | - ); |
|
| 714 | - |
|
| 715 | - $qb->andWhere( |
|
| 716 | - $qb->expr()->orX( |
|
| 717 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), |
|
| 718 | - $or1 |
|
| 719 | - ) |
|
| 720 | - ); |
|
| 721 | - } else { |
|
| 722 | - $qb->andWhere( |
|
| 723 | - $qb->expr()->orX( |
|
| 724 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 725 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 726 | - ) |
|
| 727 | - ); |
|
| 728 | - } |
|
| 729 | - |
|
| 730 | - if ($node !== null) { |
|
| 731 | - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 732 | - } |
|
| 733 | - |
|
| 734 | - if ($limit !== -1) { |
|
| 735 | - $qb->setMaxResults($limit); |
|
| 736 | - } |
|
| 737 | - |
|
| 738 | - $qb->setFirstResult($offset); |
|
| 739 | - $qb->orderBy('id'); |
|
| 740 | - |
|
| 741 | - $cursor = $qb->execute(); |
|
| 742 | - $shares = []; |
|
| 743 | - while($data = $cursor->fetch()) { |
|
| 744 | - $shares[] = $this->createShareObject($data); |
|
| 745 | - } |
|
| 746 | - $cursor->closeCursor(); |
|
| 747 | - |
|
| 748 | - return $shares; |
|
| 749 | - } |
|
| 750 | - |
|
| 751 | - /** |
|
| 752 | - * @inheritdoc |
|
| 753 | - */ |
|
| 754 | - public function getShareById($id, $recipientId = null) { |
|
| 755 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 756 | - |
|
| 757 | - $qb->select('*') |
|
| 758 | - ->from('share') |
|
| 759 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) |
|
| 760 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); |
|
| 761 | - |
|
| 762 | - $cursor = $qb->execute(); |
|
| 763 | - $data = $cursor->fetch(); |
|
| 764 | - $cursor->closeCursor(); |
|
| 765 | - |
|
| 766 | - if ($data === false) { |
|
| 767 | - throw new ShareNotFound(); |
|
| 768 | - } |
|
| 769 | - |
|
| 770 | - try { |
|
| 771 | - $share = $this->createShareObject($data); |
|
| 772 | - } catch (InvalidShare $e) { |
|
| 773 | - throw new ShareNotFound(); |
|
| 774 | - } |
|
| 775 | - |
|
| 776 | - return $share; |
|
| 777 | - } |
|
| 778 | - |
|
| 779 | - /** |
|
| 780 | - * Get shares for a given path |
|
| 781 | - * |
|
| 782 | - * @param \OCP\Files\Node $path |
|
| 783 | - * @return IShare[] |
|
| 784 | - */ |
|
| 785 | - public function getSharesByPath(Node $path) { |
|
| 786 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 787 | - |
|
| 788 | - $cursor = $qb->select('*') |
|
| 789 | - ->from('share') |
|
| 790 | - ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) |
|
| 791 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 792 | - ->execute(); |
|
| 793 | - |
|
| 794 | - $shares = []; |
|
| 795 | - while($data = $cursor->fetch()) { |
|
| 796 | - $shares[] = $this->createShareObject($data); |
|
| 797 | - } |
|
| 798 | - $cursor->closeCursor(); |
|
| 799 | - |
|
| 800 | - return $shares; |
|
| 801 | - } |
|
| 802 | - |
|
| 803 | - /** |
|
| 804 | - * @inheritdoc |
|
| 805 | - */ |
|
| 806 | - public function getSharedWith($userId, $shareType, $node, $limit, $offset) { |
|
| 807 | - /** @var IShare[] $shares */ |
|
| 808 | - $shares = []; |
|
| 809 | - |
|
| 810 | - //Get shares directly with this user |
|
| 811 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 812 | - $qb->select('*') |
|
| 813 | - ->from('share'); |
|
| 814 | - |
|
| 815 | - // Order by id |
|
| 816 | - $qb->orderBy('id'); |
|
| 817 | - |
|
| 818 | - // Set limit and offset |
|
| 819 | - if ($limit !== -1) { |
|
| 820 | - $qb->setMaxResults($limit); |
|
| 821 | - } |
|
| 822 | - $qb->setFirstResult($offset); |
|
| 823 | - |
|
| 824 | - $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); |
|
| 825 | - $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); |
|
| 826 | - |
|
| 827 | - // Filter by node if provided |
|
| 828 | - if ($node !== null) { |
|
| 829 | - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 830 | - } |
|
| 831 | - |
|
| 832 | - $cursor = $qb->execute(); |
|
| 833 | - |
|
| 834 | - while($data = $cursor->fetch()) { |
|
| 835 | - $shares[] = $this->createShareObject($data); |
|
| 836 | - } |
|
| 837 | - $cursor->closeCursor(); |
|
| 838 | - |
|
| 839 | - |
|
| 840 | - return $shares; |
|
| 841 | - } |
|
| 842 | - |
|
| 843 | - /** |
|
| 844 | - * Get a share by token |
|
| 845 | - * |
|
| 846 | - * @param string $token |
|
| 847 | - * @return IShare |
|
| 848 | - * @throws ShareNotFound |
|
| 849 | - */ |
|
| 850 | - public function getShareByToken($token) { |
|
| 851 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 852 | - |
|
| 853 | - $cursor = $qb->select('*') |
|
| 854 | - ->from('share') |
|
| 855 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 856 | - ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) |
|
| 857 | - ->execute(); |
|
| 858 | - |
|
| 859 | - $data = $cursor->fetch(); |
|
| 860 | - |
|
| 861 | - if ($data === false) { |
|
| 862 | - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 863 | - } |
|
| 864 | - |
|
| 865 | - try { |
|
| 866 | - $share = $this->createShareObject($data); |
|
| 867 | - } catch (InvalidShare $e) { |
|
| 868 | - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 869 | - } |
|
| 870 | - |
|
| 871 | - return $share; |
|
| 872 | - } |
|
| 873 | - |
|
| 874 | - /** |
|
| 875 | - * remove share from table |
|
| 876 | - * |
|
| 877 | - * @param string $shareId |
|
| 878 | - */ |
|
| 879 | - protected function removeShareFromTable($shareId) { |
|
| 880 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 881 | - $qb->delete('share') |
|
| 882 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); |
|
| 883 | - $qb->execute(); |
|
| 884 | - } |
|
| 885 | - |
|
| 886 | - /** |
|
| 887 | - * Create a share object from an database row |
|
| 888 | - * |
|
| 889 | - * @param array $data |
|
| 890 | - * @return IShare |
|
| 891 | - * @throws InvalidShare |
|
| 892 | - * @throws ShareNotFound |
|
| 893 | - */ |
|
| 894 | - protected function createShareObject($data) { |
|
| 895 | - |
|
| 896 | - $share = new Share($this->rootFolder, $this->userManager); |
|
| 897 | - $share->setId((int)$data['id']) |
|
| 898 | - ->setShareType((int)$data['share_type']) |
|
| 899 | - ->setPermissions((int)$data['permissions']) |
|
| 900 | - ->setTarget($data['file_target']) |
|
| 901 | - ->setMailSend((bool)$data['mail_send']) |
|
| 902 | - ->setToken($data['token']); |
|
| 903 | - |
|
| 904 | - $shareTime = new \DateTime(); |
|
| 905 | - $shareTime->setTimestamp((int)$data['stime']); |
|
| 906 | - $share->setShareTime($shareTime); |
|
| 907 | - $share->setSharedWith($data['share_with']); |
|
| 908 | - $share->setPassword($data['password']); |
|
| 909 | - |
|
| 910 | - if ($data['uid_initiator'] !== null) { |
|
| 911 | - $share->setShareOwner($data['uid_owner']); |
|
| 912 | - $share->setSharedBy($data['uid_initiator']); |
|
| 913 | - } else { |
|
| 914 | - //OLD SHARE |
|
| 915 | - $share->setSharedBy($data['uid_owner']); |
|
| 916 | - $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); |
|
| 917 | - |
|
| 918 | - $owner = $path->getOwner(); |
|
| 919 | - $share->setShareOwner($owner->getUID()); |
|
| 920 | - } |
|
| 921 | - |
|
| 922 | - if ($data['expiration'] !== null) { |
|
| 923 | - $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']); |
|
| 924 | - if ($expiration !== false) { |
|
| 925 | - $share->setExpirationDate($expiration); |
|
| 926 | - } |
|
| 927 | - } |
|
| 928 | - |
|
| 929 | - $share->setNodeId((int)$data['file_source']); |
|
| 930 | - $share->setNodeType($data['item_type']); |
|
| 931 | - |
|
| 932 | - $share->setProviderId($this->identifier()); |
|
| 933 | - |
|
| 934 | - return $share; |
|
| 935 | - } |
|
| 936 | - |
|
| 937 | - /** |
|
| 938 | - * Get the node with file $id for $user |
|
| 939 | - * |
|
| 940 | - * @param string $userId |
|
| 941 | - * @param int $id |
|
| 942 | - * @return \OCP\Files\File|\OCP\Files\Folder |
|
| 943 | - * @throws InvalidShare |
|
| 944 | - */ |
|
| 945 | - private function getNode($userId, $id) { |
|
| 946 | - try { |
|
| 947 | - $userFolder = $this->rootFolder->getUserFolder($userId); |
|
| 948 | - } catch (NoUserException $e) { |
|
| 949 | - throw new InvalidShare(); |
|
| 950 | - } |
|
| 951 | - |
|
| 952 | - $nodes = $userFolder->getById($id); |
|
| 953 | - |
|
| 954 | - if (empty($nodes)) { |
|
| 955 | - throw new InvalidShare(); |
|
| 956 | - } |
|
| 957 | - |
|
| 958 | - return $nodes[0]; |
|
| 959 | - } |
|
| 960 | - |
|
| 961 | - /** |
|
| 962 | - * A user is deleted from the system |
|
| 963 | - * So clean up the relevant shares. |
|
| 964 | - * |
|
| 965 | - * @param string $uid |
|
| 966 | - * @param int $shareType |
|
| 967 | - */ |
|
| 968 | - public function userDeleted($uid, $shareType) { |
|
| 969 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 970 | - |
|
| 971 | - $qb->delete('share') |
|
| 972 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 973 | - ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) |
|
| 974 | - ->execute(); |
|
| 975 | - } |
|
| 976 | - |
|
| 977 | - /** |
|
| 978 | - * This provider does not support group shares |
|
| 979 | - * |
|
| 980 | - * @param string $gid |
|
| 981 | - */ |
|
| 982 | - public function groupDeleted($gid) { |
|
| 983 | - } |
|
| 984 | - |
|
| 985 | - /** |
|
| 986 | - * This provider does not support group shares |
|
| 987 | - * |
|
| 988 | - * @param string $uid |
|
| 989 | - * @param string $gid |
|
| 990 | - */ |
|
| 991 | - public function userDeletedFromGroup($uid, $gid) { |
|
| 992 | - } |
|
| 993 | - |
|
| 994 | - /** |
|
| 995 | - * get database row of a give share |
|
| 996 | - * |
|
| 997 | - * @param $id |
|
| 998 | - * @return array |
|
| 999 | - * @throws ShareNotFound |
|
| 1000 | - */ |
|
| 1001 | - protected function getRawShare($id) { |
|
| 1002 | - |
|
| 1003 | - // Now fetch the inserted share and create a complete share object |
|
| 1004 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1005 | - $qb->select('*') |
|
| 1006 | - ->from('share') |
|
| 1007 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); |
|
| 1008 | - |
|
| 1009 | - $cursor = $qb->execute(); |
|
| 1010 | - $data = $cursor->fetch(); |
|
| 1011 | - $cursor->closeCursor(); |
|
| 1012 | - |
|
| 1013 | - if ($data === false) { |
|
| 1014 | - throw new ShareNotFound; |
|
| 1015 | - } |
|
| 1016 | - |
|
| 1017 | - return $data; |
|
| 1018 | - } |
|
| 1019 | - |
|
| 1020 | - public function getSharesInFolder($userId, Folder $node, $reshares) { |
|
| 1021 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1022 | - $qb->select('*') |
|
| 1023 | - ->from('share', 's') |
|
| 1024 | - ->andWhere($qb->expr()->orX( |
|
| 1025 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1026 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1027 | - )) |
|
| 1028 | - ->andWhere( |
|
| 1029 | - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)) |
|
| 1030 | - ); |
|
| 1031 | - |
|
| 1032 | - /** |
|
| 1033 | - * Reshares for this user are shares where they are the owner. |
|
| 1034 | - */ |
|
| 1035 | - if ($reshares === false) { |
|
| 1036 | - $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); |
|
| 1037 | - } else { |
|
| 1038 | - $qb->andWhere( |
|
| 1039 | - $qb->expr()->orX( |
|
| 1040 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 1041 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 1042 | - ) |
|
| 1043 | - ); |
|
| 1044 | - } |
|
| 1045 | - |
|
| 1046 | - $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); |
|
| 1047 | - $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); |
|
| 1048 | - |
|
| 1049 | - $qb->orderBy('id'); |
|
| 1050 | - |
|
| 1051 | - $cursor = $qb->execute(); |
|
| 1052 | - $shares = []; |
|
| 1053 | - while ($data = $cursor->fetch()) { |
|
| 1054 | - $shares[$data['fileid']][] = $this->createShareObject($data); |
|
| 1055 | - } |
|
| 1056 | - $cursor->closeCursor(); |
|
| 1057 | - |
|
| 1058 | - return $shares; |
|
| 1059 | - } |
|
| 1060 | - |
|
| 1061 | - /** |
|
| 1062 | - * @inheritdoc |
|
| 1063 | - */ |
|
| 1064 | - public function getAccessList($nodes, $currentAccess) { |
|
| 1065 | - $ids = []; |
|
| 1066 | - foreach ($nodes as $node) { |
|
| 1067 | - $ids[] = $node->getId(); |
|
| 1068 | - } |
|
| 1069 | - |
|
| 1070 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1071 | - $qb->select('share_with') |
|
| 1072 | - ->from('share') |
|
| 1073 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 1074 | - ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) |
|
| 1075 | - ->andWhere($qb->expr()->orX( |
|
| 1076 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1077 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1078 | - )) |
|
| 1079 | - ->setMaxResults(1); |
|
| 1080 | - $cursor = $qb->execute(); |
|
| 1081 | - |
|
| 1082 | - $mail = $cursor->fetch() !== false; |
|
| 1083 | - $cursor->closeCursor(); |
|
| 1084 | - |
|
| 1085 | - return ['public' => $mail]; |
|
| 1086 | - } |
|
| 656 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 657 | + $qb->update('share') |
|
| 658 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) |
|
| 659 | + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) |
|
| 660 | + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) |
|
| 661 | + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) |
|
| 662 | + ->set('password', $qb->createNamedParameter($share->getPassword())) |
|
| 663 | + ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) |
|
| 664 | + ->execute(); |
|
| 665 | + |
|
| 666 | + return $share; |
|
| 667 | + } |
|
| 668 | + |
|
| 669 | + /** |
|
| 670 | + * @inheritdoc |
|
| 671 | + */ |
|
| 672 | + public function move(IShare $share, $recipient) { |
|
| 673 | + /** |
|
| 674 | + * nothing to do here, mail shares are only outgoing shares |
|
| 675 | + */ |
|
| 676 | + return $share; |
|
| 677 | + } |
|
| 678 | + |
|
| 679 | + /** |
|
| 680 | + * Delete a share (owner unShares the file) |
|
| 681 | + * |
|
| 682 | + * @param IShare $share |
|
| 683 | + */ |
|
| 684 | + public function delete(IShare $share) { |
|
| 685 | + $this->removeShareFromTable($share->getId()); |
|
| 686 | + } |
|
| 687 | + |
|
| 688 | + /** |
|
| 689 | + * @inheritdoc |
|
| 690 | + */ |
|
| 691 | + public function deleteFromSelf(IShare $share, $recipient) { |
|
| 692 | + // nothing to do here, mail shares are only outgoing shares |
|
| 693 | + } |
|
| 694 | + |
|
| 695 | + /** |
|
| 696 | + * @inheritdoc |
|
| 697 | + */ |
|
| 698 | + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { |
|
| 699 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 700 | + $qb->select('*') |
|
| 701 | + ->from('share'); |
|
| 702 | + |
|
| 703 | + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); |
|
| 704 | + |
|
| 705 | + /** |
|
| 706 | + * Reshares for this user are shares where they are the owner. |
|
| 707 | + */ |
|
| 708 | + if ($reshares === false) { |
|
| 709 | + //Special case for old shares created via the web UI |
|
| 710 | + $or1 = $qb->expr()->andX( |
|
| 711 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 712 | + $qb->expr()->isNull('uid_initiator') |
|
| 713 | + ); |
|
| 714 | + |
|
| 715 | + $qb->andWhere( |
|
| 716 | + $qb->expr()->orX( |
|
| 717 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), |
|
| 718 | + $or1 |
|
| 719 | + ) |
|
| 720 | + ); |
|
| 721 | + } else { |
|
| 722 | + $qb->andWhere( |
|
| 723 | + $qb->expr()->orX( |
|
| 724 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 725 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 726 | + ) |
|
| 727 | + ); |
|
| 728 | + } |
|
| 729 | + |
|
| 730 | + if ($node !== null) { |
|
| 731 | + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 732 | + } |
|
| 733 | + |
|
| 734 | + if ($limit !== -1) { |
|
| 735 | + $qb->setMaxResults($limit); |
|
| 736 | + } |
|
| 737 | + |
|
| 738 | + $qb->setFirstResult($offset); |
|
| 739 | + $qb->orderBy('id'); |
|
| 740 | + |
|
| 741 | + $cursor = $qb->execute(); |
|
| 742 | + $shares = []; |
|
| 743 | + while($data = $cursor->fetch()) { |
|
| 744 | + $shares[] = $this->createShareObject($data); |
|
| 745 | + } |
|
| 746 | + $cursor->closeCursor(); |
|
| 747 | + |
|
| 748 | + return $shares; |
|
| 749 | + } |
|
| 750 | + |
|
| 751 | + /** |
|
| 752 | + * @inheritdoc |
|
| 753 | + */ |
|
| 754 | + public function getShareById($id, $recipientId = null) { |
|
| 755 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 756 | + |
|
| 757 | + $qb->select('*') |
|
| 758 | + ->from('share') |
|
| 759 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) |
|
| 760 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); |
|
| 761 | + |
|
| 762 | + $cursor = $qb->execute(); |
|
| 763 | + $data = $cursor->fetch(); |
|
| 764 | + $cursor->closeCursor(); |
|
| 765 | + |
|
| 766 | + if ($data === false) { |
|
| 767 | + throw new ShareNotFound(); |
|
| 768 | + } |
|
| 769 | + |
|
| 770 | + try { |
|
| 771 | + $share = $this->createShareObject($data); |
|
| 772 | + } catch (InvalidShare $e) { |
|
| 773 | + throw new ShareNotFound(); |
|
| 774 | + } |
|
| 775 | + |
|
| 776 | + return $share; |
|
| 777 | + } |
|
| 778 | + |
|
| 779 | + /** |
|
| 780 | + * Get shares for a given path |
|
| 781 | + * |
|
| 782 | + * @param \OCP\Files\Node $path |
|
| 783 | + * @return IShare[] |
|
| 784 | + */ |
|
| 785 | + public function getSharesByPath(Node $path) { |
|
| 786 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 787 | + |
|
| 788 | + $cursor = $qb->select('*') |
|
| 789 | + ->from('share') |
|
| 790 | + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) |
|
| 791 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 792 | + ->execute(); |
|
| 793 | + |
|
| 794 | + $shares = []; |
|
| 795 | + while($data = $cursor->fetch()) { |
|
| 796 | + $shares[] = $this->createShareObject($data); |
|
| 797 | + } |
|
| 798 | + $cursor->closeCursor(); |
|
| 799 | + |
|
| 800 | + return $shares; |
|
| 801 | + } |
|
| 802 | + |
|
| 803 | + /** |
|
| 804 | + * @inheritdoc |
|
| 805 | + */ |
|
| 806 | + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { |
|
| 807 | + /** @var IShare[] $shares */ |
|
| 808 | + $shares = []; |
|
| 809 | + |
|
| 810 | + //Get shares directly with this user |
|
| 811 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 812 | + $qb->select('*') |
|
| 813 | + ->from('share'); |
|
| 814 | + |
|
| 815 | + // Order by id |
|
| 816 | + $qb->orderBy('id'); |
|
| 817 | + |
|
| 818 | + // Set limit and offset |
|
| 819 | + if ($limit !== -1) { |
|
| 820 | + $qb->setMaxResults($limit); |
|
| 821 | + } |
|
| 822 | + $qb->setFirstResult($offset); |
|
| 823 | + |
|
| 824 | + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))); |
|
| 825 | + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); |
|
| 826 | + |
|
| 827 | + // Filter by node if provided |
|
| 828 | + if ($node !== null) { |
|
| 829 | + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 830 | + } |
|
| 831 | + |
|
| 832 | + $cursor = $qb->execute(); |
|
| 833 | + |
|
| 834 | + while($data = $cursor->fetch()) { |
|
| 835 | + $shares[] = $this->createShareObject($data); |
|
| 836 | + } |
|
| 837 | + $cursor->closeCursor(); |
|
| 838 | + |
|
| 839 | + |
|
| 840 | + return $shares; |
|
| 841 | + } |
|
| 842 | + |
|
| 843 | + /** |
|
| 844 | + * Get a share by token |
|
| 845 | + * |
|
| 846 | + * @param string $token |
|
| 847 | + * @return IShare |
|
| 848 | + * @throws ShareNotFound |
|
| 849 | + */ |
|
| 850 | + public function getShareByToken($token) { |
|
| 851 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 852 | + |
|
| 853 | + $cursor = $qb->select('*') |
|
| 854 | + ->from('share') |
|
| 855 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 856 | + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) |
|
| 857 | + ->execute(); |
|
| 858 | + |
|
| 859 | + $data = $cursor->fetch(); |
|
| 860 | + |
|
| 861 | + if ($data === false) { |
|
| 862 | + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 863 | + } |
|
| 864 | + |
|
| 865 | + try { |
|
| 866 | + $share = $this->createShareObject($data); |
|
| 867 | + } catch (InvalidShare $e) { |
|
| 868 | + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 869 | + } |
|
| 870 | + |
|
| 871 | + return $share; |
|
| 872 | + } |
|
| 873 | + |
|
| 874 | + /** |
|
| 875 | + * remove share from table |
|
| 876 | + * |
|
| 877 | + * @param string $shareId |
|
| 878 | + */ |
|
| 879 | + protected function removeShareFromTable($shareId) { |
|
| 880 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 881 | + $qb->delete('share') |
|
| 882 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); |
|
| 883 | + $qb->execute(); |
|
| 884 | + } |
|
| 885 | + |
|
| 886 | + /** |
|
| 887 | + * Create a share object from an database row |
|
| 888 | + * |
|
| 889 | + * @param array $data |
|
| 890 | + * @return IShare |
|
| 891 | + * @throws InvalidShare |
|
| 892 | + * @throws ShareNotFound |
|
| 893 | + */ |
|
| 894 | + protected function createShareObject($data) { |
|
| 895 | + |
|
| 896 | + $share = new Share($this->rootFolder, $this->userManager); |
|
| 897 | + $share->setId((int)$data['id']) |
|
| 898 | + ->setShareType((int)$data['share_type']) |
|
| 899 | + ->setPermissions((int)$data['permissions']) |
|
| 900 | + ->setTarget($data['file_target']) |
|
| 901 | + ->setMailSend((bool)$data['mail_send']) |
|
| 902 | + ->setToken($data['token']); |
|
| 903 | + |
|
| 904 | + $shareTime = new \DateTime(); |
|
| 905 | + $shareTime->setTimestamp((int)$data['stime']); |
|
| 906 | + $share->setShareTime($shareTime); |
|
| 907 | + $share->setSharedWith($data['share_with']); |
|
| 908 | + $share->setPassword($data['password']); |
|
| 909 | + |
|
| 910 | + if ($data['uid_initiator'] !== null) { |
|
| 911 | + $share->setShareOwner($data['uid_owner']); |
|
| 912 | + $share->setSharedBy($data['uid_initiator']); |
|
| 913 | + } else { |
|
| 914 | + //OLD SHARE |
|
| 915 | + $share->setSharedBy($data['uid_owner']); |
|
| 916 | + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); |
|
| 917 | + |
|
| 918 | + $owner = $path->getOwner(); |
|
| 919 | + $share->setShareOwner($owner->getUID()); |
|
| 920 | + } |
|
| 921 | + |
|
| 922 | + if ($data['expiration'] !== null) { |
|
| 923 | + $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']); |
|
| 924 | + if ($expiration !== false) { |
|
| 925 | + $share->setExpirationDate($expiration); |
|
| 926 | + } |
|
| 927 | + } |
|
| 928 | + |
|
| 929 | + $share->setNodeId((int)$data['file_source']); |
|
| 930 | + $share->setNodeType($data['item_type']); |
|
| 931 | + |
|
| 932 | + $share->setProviderId($this->identifier()); |
|
| 933 | + |
|
| 934 | + return $share; |
|
| 935 | + } |
|
| 936 | + |
|
| 937 | + /** |
|
| 938 | + * Get the node with file $id for $user |
|
| 939 | + * |
|
| 940 | + * @param string $userId |
|
| 941 | + * @param int $id |
|
| 942 | + * @return \OCP\Files\File|\OCP\Files\Folder |
|
| 943 | + * @throws InvalidShare |
|
| 944 | + */ |
|
| 945 | + private function getNode($userId, $id) { |
|
| 946 | + try { |
|
| 947 | + $userFolder = $this->rootFolder->getUserFolder($userId); |
|
| 948 | + } catch (NoUserException $e) { |
|
| 949 | + throw new InvalidShare(); |
|
| 950 | + } |
|
| 951 | + |
|
| 952 | + $nodes = $userFolder->getById($id); |
|
| 953 | + |
|
| 954 | + if (empty($nodes)) { |
|
| 955 | + throw new InvalidShare(); |
|
| 956 | + } |
|
| 957 | + |
|
| 958 | + return $nodes[0]; |
|
| 959 | + } |
|
| 960 | + |
|
| 961 | + /** |
|
| 962 | + * A user is deleted from the system |
|
| 963 | + * So clean up the relevant shares. |
|
| 964 | + * |
|
| 965 | + * @param string $uid |
|
| 966 | + * @param int $shareType |
|
| 967 | + */ |
|
| 968 | + public function userDeleted($uid, $shareType) { |
|
| 969 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 970 | + |
|
| 971 | + $qb->delete('share') |
|
| 972 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 973 | + ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) |
|
| 974 | + ->execute(); |
|
| 975 | + } |
|
| 976 | + |
|
| 977 | + /** |
|
| 978 | + * This provider does not support group shares |
|
| 979 | + * |
|
| 980 | + * @param string $gid |
|
| 981 | + */ |
|
| 982 | + public function groupDeleted($gid) { |
|
| 983 | + } |
|
| 984 | + |
|
| 985 | + /** |
|
| 986 | + * This provider does not support group shares |
|
| 987 | + * |
|
| 988 | + * @param string $uid |
|
| 989 | + * @param string $gid |
|
| 990 | + */ |
|
| 991 | + public function userDeletedFromGroup($uid, $gid) { |
|
| 992 | + } |
|
| 993 | + |
|
| 994 | + /** |
|
| 995 | + * get database row of a give share |
|
| 996 | + * |
|
| 997 | + * @param $id |
|
| 998 | + * @return array |
|
| 999 | + * @throws ShareNotFound |
|
| 1000 | + */ |
|
| 1001 | + protected function getRawShare($id) { |
|
| 1002 | + |
|
| 1003 | + // Now fetch the inserted share and create a complete share object |
|
| 1004 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1005 | + $qb->select('*') |
|
| 1006 | + ->from('share') |
|
| 1007 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); |
|
| 1008 | + |
|
| 1009 | + $cursor = $qb->execute(); |
|
| 1010 | + $data = $cursor->fetch(); |
|
| 1011 | + $cursor->closeCursor(); |
|
| 1012 | + |
|
| 1013 | + if ($data === false) { |
|
| 1014 | + throw new ShareNotFound; |
|
| 1015 | + } |
|
| 1016 | + |
|
| 1017 | + return $data; |
|
| 1018 | + } |
|
| 1019 | + |
|
| 1020 | + public function getSharesInFolder($userId, Folder $node, $reshares) { |
|
| 1021 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1022 | + $qb->select('*') |
|
| 1023 | + ->from('share', 's') |
|
| 1024 | + ->andWhere($qb->expr()->orX( |
|
| 1025 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1026 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1027 | + )) |
|
| 1028 | + ->andWhere( |
|
| 1029 | + $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL)) |
|
| 1030 | + ); |
|
| 1031 | + |
|
| 1032 | + /** |
|
| 1033 | + * Reshares for this user are shares where they are the owner. |
|
| 1034 | + */ |
|
| 1035 | + if ($reshares === false) { |
|
| 1036 | + $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); |
|
| 1037 | + } else { |
|
| 1038 | + $qb->andWhere( |
|
| 1039 | + $qb->expr()->orX( |
|
| 1040 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 1041 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 1042 | + ) |
|
| 1043 | + ); |
|
| 1044 | + } |
|
| 1045 | + |
|
| 1046 | + $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); |
|
| 1047 | + $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); |
|
| 1048 | + |
|
| 1049 | + $qb->orderBy('id'); |
|
| 1050 | + |
|
| 1051 | + $cursor = $qb->execute(); |
|
| 1052 | + $shares = []; |
|
| 1053 | + while ($data = $cursor->fetch()) { |
|
| 1054 | + $shares[$data['fileid']][] = $this->createShareObject($data); |
|
| 1055 | + } |
|
| 1056 | + $cursor->closeCursor(); |
|
| 1057 | + |
|
| 1058 | + return $shares; |
|
| 1059 | + } |
|
| 1060 | + |
|
| 1061 | + /** |
|
| 1062 | + * @inheritdoc |
|
| 1063 | + */ |
|
| 1064 | + public function getAccessList($nodes, $currentAccess) { |
|
| 1065 | + $ids = []; |
|
| 1066 | + foreach ($nodes as $node) { |
|
| 1067 | + $ids[] = $node->getId(); |
|
| 1068 | + } |
|
| 1069 | + |
|
| 1070 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1071 | + $qb->select('share_with') |
|
| 1072 | + ->from('share') |
|
| 1073 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share::SHARE_TYPE_EMAIL))) |
|
| 1074 | + ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) |
|
| 1075 | + ->andWhere($qb->expr()->orX( |
|
| 1076 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1077 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1078 | + )) |
|
| 1079 | + ->setMaxResults(1); |
|
| 1080 | + $cursor = $qb->execute(); |
|
| 1081 | + |
|
| 1082 | + $mail = $cursor->fetch() !== false; |
|
| 1083 | + $cursor->closeCursor(); |
|
| 1084 | + |
|
| 1085 | + return ['public' => $mail]; |
|
| 1086 | + } |
|
| 1087 | 1087 | |
| 1088 | 1088 | } |
@@ -28,32 +28,32 @@ |
||
| 28 | 28 | |
| 29 | 29 | class SyncJob extends TimedJob { |
| 30 | 30 | |
| 31 | - /** @var SyncFederationAddressBooks */ |
|
| 32 | - protected $syncService; |
|
| 31 | + /** @var SyncFederationAddressBooks */ |
|
| 32 | + protected $syncService; |
|
| 33 | 33 | |
| 34 | - /** @var ILogger */ |
|
| 35 | - protected $logger; |
|
| 34 | + /** @var ILogger */ |
|
| 35 | + protected $logger; |
|
| 36 | 36 | |
| 37 | - /** |
|
| 38 | - * @param SyncFederationAddressBooks $syncService |
|
| 39 | - * @param ILogger $logger |
|
| 40 | - */ |
|
| 41 | - public function __construct(SyncFederationAddressBooks $syncService, ILogger $logger) { |
|
| 42 | - // Run once a day |
|
| 43 | - $this->setInterval(24 * 60 * 60); |
|
| 44 | - $this->syncService = $syncService; |
|
| 45 | - $this->logger = $logger; |
|
| 46 | - } |
|
| 37 | + /** |
|
| 38 | + * @param SyncFederationAddressBooks $syncService |
|
| 39 | + * @param ILogger $logger |
|
| 40 | + */ |
|
| 41 | + public function __construct(SyncFederationAddressBooks $syncService, ILogger $logger) { |
|
| 42 | + // Run once a day |
|
| 43 | + $this->setInterval(24 * 60 * 60); |
|
| 44 | + $this->syncService = $syncService; |
|
| 45 | + $this->logger = $logger; |
|
| 46 | + } |
|
| 47 | 47 | |
| 48 | - protected function run($argument) { |
|
| 49 | - $this->syncService->syncThemAll(function($url, $ex) { |
|
| 50 | - if ($ex instanceof \Exception) { |
|
| 51 | - $this->logger->logException($ex, [ |
|
| 52 | - 'message' => "Error while syncing $url.", |
|
| 53 | - 'level' => ILogger::ERROR, |
|
| 54 | - 'app' => 'fed-sync', |
|
| 55 | - ]); |
|
| 56 | - } |
|
| 57 | - }); |
|
| 58 | - } |
|
| 48 | + protected function run($argument) { |
|
| 49 | + $this->syncService->syncThemAll(function($url, $ex) { |
|
| 50 | + if ($ex instanceof \Exception) { |
|
| 51 | + $this->logger->logException($ex, [ |
|
| 52 | + 'message' => "Error while syncing $url.", |
|
| 53 | + 'level' => ILogger::ERROR, |
|
| 54 | + 'app' => 'fed-sync', |
|
| 55 | + ]); |
|
| 56 | + } |
|
| 57 | + }); |
|
| 58 | + } |
|
| 59 | 59 | } |