1<?php 2/** 3 * @copyright Copyright (c) 2016, ownCloud, Inc. 4 * 5 * @author Aaron Wood <aaronjwood@gmail.com> 6 * @author Arthur Schiwon <blizzz@arthur-schiwon.de> 7 * @author blizzz <blizzz@arthur-schiwon.de> 8 * @author Christoph Wurst <christoph@winzerhof-wurst.at> 9 * @author Joas Schilling <coding@schilljs.com> 10 * @author Roeland Jago Douma <roeland@famdouma.nl> 11 * 12 * @license AGPL-3.0 13 * 14 * This code is free software: you can redistribute it and/or modify 15 * it under the terms of the GNU Affero General Public License, version 3, 16 * as published by the Free Software Foundation. 17 * 18 * This program is distributed in the hope that it will be useful, 19 * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 * GNU Affero General Public License for more details. 22 * 23 * You should have received a copy of the GNU Affero General Public License, version 3, 24 * along with this program. If not, see <http://www.gnu.org/licenses/> 25 * 26 */ 27namespace OCA\User_LDAP\Mapping; 28 29use Doctrine\DBAL\Exception; 30use OC\DB\QueryBuilder\QueryBuilder; 31use OCP\DB\IPreparedStatement; 32use OCP\DB\QueryBuilder\IQueryBuilder; 33 34/** 35 * Class AbstractMapping 36 * 37 * @package OCA\User_LDAP\Mapping 38 */ 39abstract class AbstractMapping { 40 /** 41 * @var \OCP\IDBConnection $dbc 42 */ 43 protected $dbc; 44 45 /** 46 * returns the DB table name which holds the mappings 47 * 48 * @return string 49 */ 50 abstract protected function getTableName(bool $includePrefix = true); 51 52 /** 53 * @param \OCP\IDBConnection $dbc 54 */ 55 public function __construct(\OCP\IDBConnection $dbc) { 56 $this->dbc = $dbc; 57 } 58 59 /** @var array caches Names (value) by DN (key) */ 60 protected $cache = []; 61 62 /** 63 * checks whether a provided string represents an existing table col 64 * 65 * @param string $col 66 * @return bool 67 */ 68 public function isColNameValid($col) { 69 switch ($col) { 70 case 'ldap_dn': 71 case 'owncloud_name': 72 case 'directory_uuid': 73 return true; 74 default: 75 return false; 76 } 77 } 78 79 /** 80 * Gets the value of one column based on a provided value of another column 81 * 82 * @param string $fetchCol 83 * @param string $compareCol 84 * @param string $search 85 * @return string|false 86 * @throws \Exception 87 */ 88 protected function getXbyY($fetchCol, $compareCol, $search) { 89 if (!$this->isColNameValid($fetchCol)) { 90 //this is used internally only, but we don't want to risk 91 //having SQL injection at all. 92 throw new \Exception('Invalid Column Name'); 93 } 94 $query = $this->dbc->prepare(' 95 SELECT `' . $fetchCol . '` 96 FROM `' . $this->getTableName() . '` 97 WHERE `' . $compareCol . '` = ? 98 '); 99 100 try { 101 $res = $query->execute([$search]); 102 $data = $res->fetchOne(); 103 $res->closeCursor(); 104 return $data; 105 } catch (Exception $e) { 106 return false; 107 } 108 } 109 110 /** 111 * Performs a DELETE or UPDATE query to the database. 112 * 113 * @param IPreparedStatement $statement 114 * @param array $parameters 115 * @return bool true if at least one row was modified, false otherwise 116 */ 117 protected function modify(IPreparedStatement $statement, $parameters) { 118 try { 119 $result = $statement->execute($parameters); 120 $updated = $result->rowCount() > 0; 121 $result->closeCursor(); 122 return $updated; 123 } catch (Exception $e) { 124 return false; 125 } 126 } 127 128 /** 129 * Gets the LDAP DN based on the provided name. 130 * Replaces Access::ocname2dn 131 * 132 * @param string $name 133 * @return string|false 134 */ 135 public function getDNByName($name) { 136 $dn = array_search($name, $this->cache); 137 if ($dn === false && ($dn = $this->getXbyY('ldap_dn', 'owncloud_name', $name)) !== false) { 138 $this->cache[$dn] = $name; 139 } 140 return $dn; 141 } 142 143 /** 144 * Updates the DN based on the given UUID 145 * 146 * @param string $fdn 147 * @param string $uuid 148 * @return bool 149 */ 150 public function setDNbyUUID($fdn, $uuid) { 151 $oldDn = $this->getDnByUUID($uuid); 152 $statement = $this->dbc->prepare(' 153 UPDATE `' . $this->getTableName() . '` 154 SET `ldap_dn` = ? 155 WHERE `directory_uuid` = ? 156 '); 157 158 $r = $this->modify($statement, [$fdn, $uuid]); 159 160 if ($r && is_string($oldDn) && isset($this->cache[$oldDn])) { 161 $this->cache[$fdn] = $this->cache[$oldDn]; 162 unset($this->cache[$oldDn]); 163 } 164 165 return $r; 166 } 167 168 /** 169 * Updates the UUID based on the given DN 170 * 171 * required by Migration/UUIDFix 172 * 173 * @param $uuid 174 * @param $fdn 175 * @return bool 176 */ 177 public function setUUIDbyDN($uuid, $fdn) { 178 $statement = $this->dbc->prepare(' 179 UPDATE `' . $this->getTableName() . '` 180 SET `directory_uuid` = ? 181 WHERE `ldap_dn` = ? 182 '); 183 184 unset($this->cache[$fdn]); 185 186 return $this->modify($statement, [$uuid, $fdn]); 187 } 188 189 /** 190 * Gets the name based on the provided LDAP DN. 191 * 192 * @param string $fdn 193 * @return string|false 194 */ 195 public function getNameByDN($fdn) { 196 if (!isset($this->cache[$fdn])) { 197 $this->cache[$fdn] = $this->getXbyY('owncloud_name', 'ldap_dn', $fdn); 198 } 199 return $this->cache[$fdn]; 200 } 201 202 protected function prepareListOfIdsQuery(array $dnList): IQueryBuilder { 203 $qb = $this->dbc->getQueryBuilder(); 204 $qb->select('owncloud_name', 'ldap_dn') 205 ->from($this->getTableName(false)) 206 ->where($qb->expr()->in('ldap_dn', $qb->createNamedParameter($dnList, QueryBuilder::PARAM_STR_ARRAY))); 207 return $qb; 208 } 209 210 protected function collectResultsFromListOfIdsQuery(IQueryBuilder $qb, array &$results): void { 211 $stmt = $qb->execute(); 212 while ($entry = $stmt->fetch(\Doctrine\DBAL\FetchMode::ASSOCIATIVE)) { 213 $results[$entry['ldap_dn']] = $entry['owncloud_name']; 214 $this->cache[$entry['ldap_dn']] = $entry['owncloud_name']; 215 } 216 $stmt->closeCursor(); 217 } 218 219 public function getListOfIdsByDn(array $fdns): array { 220 $totalDBParamLimit = 65000; 221 $sliceSize = 1000; 222 $maxSlices = $totalDBParamLimit / $sliceSize; 223 $results = []; 224 225 $slice = 1; 226 $fdnsSlice = count($fdns) > $sliceSize ? array_slice($fdns, 0, $sliceSize) : $fdns; 227 $qb = $this->prepareListOfIdsQuery($fdnsSlice); 228 229 while (isset($fdnsSlice[999])) { 230 // Oracle does not allow more than 1000 values in the IN list, 231 // but allows slicing 232 $slice++; 233 $fdnsSlice = array_slice($fdns, $sliceSize * ($slice - 1), $sliceSize); 234 235 /** @see https://github.com/vimeo/psalm/issues/4995 */ 236 /** @psalm-suppress TypeDoesNotContainType */ 237 if (!isset($qb)) { 238 $qb = $this->prepareListOfIdsQuery($fdnsSlice); 239 continue; 240 } 241 242 if (!empty($fdnsSlice)) { 243 $qb->orWhere($qb->expr()->in('ldap_dn', $qb->createNamedParameter($fdnsSlice, QueryBuilder::PARAM_STR_ARRAY))); 244 } 245 246 if ($slice % $maxSlices === 0) { 247 $this->collectResultsFromListOfIdsQuery($qb, $results); 248 unset($qb); 249 } 250 } 251 252 if (isset($qb)) { 253 $this->collectResultsFromListOfIdsQuery($qb, $results); 254 } 255 256 return $results; 257 } 258 259 /** 260 * Searches mapped names by the giving string in the name column 261 * 262 * @param string $search 263 * @param string $prefixMatch 264 * @param string $postfixMatch 265 * @return string[] 266 */ 267 public function getNamesBySearch($search, $prefixMatch = "", $postfixMatch = "") { 268 $statement = $this->dbc->prepare(' 269 SELECT `owncloud_name` 270 FROM `' . $this->getTableName() . '` 271 WHERE `owncloud_name` LIKE ? 272 '); 273 274 try { 275 $res = $statement->execute([$prefixMatch . $this->dbc->escapeLikeParameter($search) . $postfixMatch]); 276 } catch (Exception $e) { 277 return []; 278 } 279 $names = []; 280 while ($row = $res->fetch()) { 281 $names[] = $row['owncloud_name']; 282 } 283 return $names; 284 } 285 286 /** 287 * Gets the name based on the provided LDAP UUID. 288 * 289 * @param string $uuid 290 * @return string|false 291 */ 292 public function getNameByUUID($uuid) { 293 return $this->getXbyY('owncloud_name', 'directory_uuid', $uuid); 294 } 295 296 public function getDnByUUID($uuid) { 297 return $this->getXbyY('ldap_dn', 'directory_uuid', $uuid); 298 } 299 300 /** 301 * Gets the UUID based on the provided LDAP DN 302 * 303 * @param string $dn 304 * @return false|string 305 * @throws \Exception 306 */ 307 public function getUUIDByDN($dn) { 308 return $this->getXbyY('directory_uuid', 'ldap_dn', $dn); 309 } 310 311 /** 312 * gets a piece of the mapping list 313 * 314 * @param int $offset 315 * @param int $limit 316 * @return array 317 */ 318 public function getList($offset = null, $limit = null) { 319 $query = $this->dbc->prepare(' 320 SELECT 321 `ldap_dn` AS `dn`, 322 `owncloud_name` AS `name`, 323 `directory_uuid` AS `uuid` 324 FROM `' . $this->getTableName() . '`', 325 $limit, 326 $offset 327 ); 328 329 $query->execute(); 330 return $query->fetchAll(); 331 } 332 333 /** 334 * attempts to map the given entry 335 * 336 * @param string $fdn fully distinguished name (from LDAP) 337 * @param string $name 338 * @param string $uuid a unique identifier as used in LDAP 339 * @return bool 340 */ 341 public function map($fdn, $name, $uuid) { 342 if (mb_strlen($fdn) > 255) { 343 \OC::$server->getLogger()->error( 344 'Cannot map, because the DN exceeds 255 characters: {dn}', 345 [ 346 'app' => 'user_ldap', 347 'dn' => $fdn, 348 ] 349 ); 350 return false; 351 } 352 353 $row = [ 354 'ldap_dn' => $fdn, 355 'owncloud_name' => $name, 356 'directory_uuid' => $uuid 357 ]; 358 359 try { 360 $result = $this->dbc->insertIfNotExist($this->getTableName(), $row); 361 if ((bool)$result === true) { 362 $this->cache[$fdn] = $name; 363 } 364 // insertIfNotExist returns values as int 365 return (bool)$result; 366 } catch (\Exception $e) { 367 return false; 368 } 369 } 370 371 /** 372 * removes a mapping based on the owncloud_name of the entry 373 * 374 * @param string $name 375 * @return bool 376 */ 377 public function unmap($name) { 378 $statement = $this->dbc->prepare(' 379 DELETE FROM `' . $this->getTableName() . '` 380 WHERE `owncloud_name` = ?'); 381 382 $dn = array_search($name, $this->cache); 383 if ($dn !== false) { 384 unset($this->cache[$dn]); 385 } 386 387 return $this->modify($statement, [$name]); 388 } 389 390 /** 391 * Truncates the mapping table 392 * 393 * @return bool 394 */ 395 public function clear() { 396 $sql = $this->dbc 397 ->getDatabasePlatform() 398 ->getTruncateTableSQL('`' . $this->getTableName() . '`'); 399 try { 400 $this->dbc->executeQuery($sql); 401 402 return true; 403 } catch (Exception $e) { 404 return false; 405 } 406 } 407 408 /** 409 * clears the mapping table one by one and executing a callback with 410 * each row's id (=owncloud_name col) 411 * 412 * @param callable $preCallback 413 * @param callable $postCallback 414 * @return bool true on success, false when at least one row was not 415 * deleted 416 */ 417 public function clearCb(callable $preCallback, callable $postCallback): bool { 418 $picker = $this->dbc->getQueryBuilder(); 419 $picker->select('owncloud_name') 420 ->from($this->getTableName()); 421 $cursor = $picker->execute(); 422 $result = true; 423 while ($id = $cursor->fetchOne()) { 424 $preCallback($id); 425 if ($isUnmapped = $this->unmap($id)) { 426 $postCallback($id); 427 } 428 $result &= $isUnmapped; 429 } 430 $cursor->closeCursor(); 431 return $result; 432 } 433 434 /** 435 * returns the number of entries in the mappings table 436 * 437 * @return int 438 */ 439 public function count() { 440 $qb = $this->dbc->getQueryBuilder(); 441 $query = $qb->select($qb->func()->count('ldap_dn')) 442 ->from($this->getTableName()); 443 $res = $query->execute(); 444 $count = $res->fetchOne(); 445 $res->closeCursor(); 446 return (int)$count; 447 } 448} 449