PDO.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\DAVACL\PrincipalBackend;
  4. use Sabre\DAV;
  5. use Sabre\DAV\MkCol;
  6. use Sabre\Uri;
  7. /**
  8. * PDO principal backend.
  9. *
  10. * This backend assumes all principals are in a single collection. The default collection
  11. * is 'principals/', but this can be overridden.
  12. *
  13. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  14. * @author Evert Pot (http://evertpot.com/)
  15. * @license http://sabre.io/license/ Modified BSD License
  16. */
  17. class PDO extends AbstractBackend implements CreatePrincipalSupport
  18. {
  19. /**
  20. * PDO table name for 'principals'.
  21. *
  22. * @var string
  23. */
  24. public $tableName = 'principals';
  25. /**
  26. * PDO table name for 'group members'.
  27. *
  28. * @var string
  29. */
  30. public $groupMembersTableName = 'groupmembers';
  31. /**
  32. * pdo.
  33. *
  34. * @var PDO
  35. */
  36. protected $pdo;
  37. /**
  38. * A list of additional fields to support.
  39. *
  40. * @var array
  41. */
  42. protected $fieldMap = [
  43. /*
  44. * This property can be used to display the users' real name.
  45. */
  46. '{DAV:}displayname' => [
  47. 'dbField' => 'displayname',
  48. ],
  49. /*
  50. * This is the users' primary email-address.
  51. */
  52. '{http://sabredav.org/ns}email-address' => [
  53. 'dbField' => 'email',
  54. ],
  55. ];
  56. /**
  57. * Sets up the backend.
  58. */
  59. public function __construct(\PDO $pdo)
  60. {
  61. $this->pdo = $pdo;
  62. }
  63. /**
  64. * Returns a list of principals based on a prefix.
  65. *
  66. * This prefix will often contain something like 'principals'. You are only
  67. * expected to return principals that are in this base path.
  68. *
  69. * You are expected to return at least a 'uri' for every user, you can
  70. * return any additional properties if you wish so. Common properties are:
  71. * {DAV:}displayname
  72. * {http://sabredav.org/ns}email-address - This is a custom SabreDAV
  73. * field that's actualy injected in a number of other properties. If
  74. * you have an email address, use this property.
  75. *
  76. * @param string $prefixPath
  77. *
  78. * @return array
  79. */
  80. public function getPrincipalsByPrefix($prefixPath)
  81. {
  82. $fields = [
  83. 'uri',
  84. ];
  85. foreach ($this->fieldMap as $key => $value) {
  86. $fields[] = $value['dbField'];
  87. }
  88. $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '.$this->tableName);
  89. $principals = [];
  90. while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
  91. // Checking if the principal is in the prefix
  92. list($rowPrefix) = Uri\split($row['uri']);
  93. if ($rowPrefix !== $prefixPath) {
  94. continue;
  95. }
  96. $principal = [
  97. 'uri' => $row['uri'],
  98. ];
  99. foreach ($this->fieldMap as $key => $value) {
  100. if ($row[$value['dbField']]) {
  101. $principal[$key] = $row[$value['dbField']];
  102. }
  103. }
  104. $principals[] = $principal;
  105. }
  106. return $principals;
  107. }
  108. /**
  109. * Returns a specific principal, specified by it's path.
  110. * The returned structure should be the exact same as from
  111. * getPrincipalsByPrefix.
  112. *
  113. * @param string $path
  114. *
  115. * @return array
  116. */
  117. public function getPrincipalByPath($path)
  118. {
  119. $fields = [
  120. 'id',
  121. 'uri',
  122. ];
  123. foreach ($this->fieldMap as $key => $value) {
  124. $fields[] = $value['dbField'];
  125. }
  126. $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '.$this->tableName.' WHERE uri = ?');
  127. $stmt->execute([$path]);
  128. $row = $stmt->fetch(\PDO::FETCH_ASSOC);
  129. if (!$row) {
  130. return;
  131. }
  132. $principal = [
  133. 'id' => $row['id'],
  134. 'uri' => $row['uri'],
  135. ];
  136. foreach ($this->fieldMap as $key => $value) {
  137. if ($row[$value['dbField']]) {
  138. $principal[$key] = $row[$value['dbField']];
  139. }
  140. }
  141. return $principal;
  142. }
  143. /**
  144. * Updates one ore more webdav properties on a principal.
  145. *
  146. * The list of mutations is stored in a Sabre\DAV\PropPatch object.
  147. * To do the actual updates, you must tell this object which properties
  148. * you're going to process with the handle() method.
  149. *
  150. * Calling the handle method is like telling the PropPatch object "I
  151. * promise I can handle updating this property".
  152. *
  153. * Read the PropPatch documentation for more info and examples.
  154. *
  155. * @param string $path
  156. */
  157. public function updatePrincipal($path, DAV\PropPatch $propPatch)
  158. {
  159. $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) {
  160. $query = 'UPDATE '.$this->tableName.' SET ';
  161. $first = true;
  162. $values = [];
  163. foreach ($properties as $key => $value) {
  164. $dbField = $this->fieldMap[$key]['dbField'];
  165. if (!$first) {
  166. $query .= ', ';
  167. }
  168. $first = false;
  169. $query .= $dbField.' = :'.$dbField;
  170. $values[$dbField] = $value;
  171. }
  172. $query .= ' WHERE uri = :uri';
  173. $values['uri'] = $path;
  174. $stmt = $this->pdo->prepare($query);
  175. $stmt->execute($values);
  176. return true;
  177. });
  178. }
  179. /**
  180. * This method is used to search for principals matching a set of
  181. * properties.
  182. *
  183. * This search is specifically used by RFC3744's principal-property-search
  184. * REPORT.
  185. *
  186. * The actual search should be a unicode-non-case-sensitive search. The
  187. * keys in searchProperties are the WebDAV property names, while the values
  188. * are the property values to search on.
  189. *
  190. * By default, if multiple properties are submitted to this method, the
  191. * various properties should be combined with 'AND'. If $test is set to
  192. * 'anyof', it should be combined using 'OR'.
  193. *
  194. * This method should simply return an array with full principal uri's.
  195. *
  196. * If somebody attempted to search on a property the backend does not
  197. * support, you should simply return 0 results.
  198. *
  199. * You can also just return 0 results if you choose to not support
  200. * searching at all, but keep in mind that this may stop certain features
  201. * from working.
  202. *
  203. * @param string $prefixPath
  204. * @param string $test
  205. *
  206. * @return array
  207. */
  208. public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
  209. {
  210. if (0 == count($searchProperties)) {
  211. return [];
  212. } //No criteria
  213. $query = 'SELECT uri FROM '.$this->tableName.' WHERE ';
  214. $values = [];
  215. foreach ($searchProperties as $property => $value) {
  216. switch ($property) {
  217. case '{DAV:}displayname':
  218. $column = 'displayname';
  219. break;
  220. case '{http://sabredav.org/ns}email-address':
  221. $column = 'email';
  222. break;
  223. default:
  224. // Unsupported property
  225. return [];
  226. }
  227. if (count($values) > 0) {
  228. $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND ');
  229. }
  230. $query .= 'lower('.$column.') LIKE lower(?)';
  231. $values[] = '%'.$value.'%';
  232. }
  233. $stmt = $this->pdo->prepare($query);
  234. $stmt->execute($values);
  235. $principals = [];
  236. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  237. // Checking if the principal is in the prefix
  238. list($rowPrefix) = Uri\split($row['uri']);
  239. if ($rowPrefix !== $prefixPath) {
  240. continue;
  241. }
  242. $principals[] = $row['uri'];
  243. }
  244. return $principals;
  245. }
  246. /**
  247. * Finds a principal by its URI.
  248. *
  249. * This method may receive any type of uri, but mailto: addresses will be
  250. * the most common.
  251. *
  252. * Implementation of this API is optional. It is currently used by the
  253. * CalDAV system to find principals based on their email addresses. If this
  254. * API is not implemented, some features may not work correctly.
  255. *
  256. * This method must return a relative principal path, or null, if the
  257. * principal was not found or you refuse to find it.
  258. *
  259. * @param string $uri
  260. * @param string $principalPrefix
  261. *
  262. * @return string
  263. */
  264. public function findByUri($uri, $principalPrefix)
  265. {
  266. $uriParts = Uri\parse($uri);
  267. // Only two types of uri are supported :
  268. // - the "mailto:" scheme with some non-empty address
  269. // - a principals uri, in the form "principals/NAME"
  270. // In both cases, `path` must not be empty.
  271. if (empty($uriParts['path'])) {
  272. return null;
  273. }
  274. $uri = null;
  275. if ('mailto' === $uriParts['scheme']) {
  276. $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)';
  277. $stmt = $this->pdo->prepare($query);
  278. $stmt->execute([$uriParts['path']]);
  279. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  280. // Checking if the principal is in the prefix
  281. list($rowPrefix) = Uri\split($row['uri']);
  282. if ($rowPrefix !== $principalPrefix) {
  283. continue;
  284. }
  285. $uri = $row['uri'];
  286. break; //Stop on first match
  287. }
  288. } else {
  289. $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null
  290. if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) {
  291. // Checking that this uri exists
  292. $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?';
  293. $stmt = $this->pdo->prepare($query);
  294. $stmt->execute([$uriParts['path']]);
  295. $rows = $stmt->fetchAll();
  296. if (count($rows) > 0) {
  297. $uri = $uriParts['path'];
  298. }
  299. }
  300. }
  301. return $uri;
  302. }
  303. /**
  304. * Returns the list of members for a group-principal.
  305. *
  306. * @param string $principal
  307. *
  308. * @return array
  309. */
  310. public function getGroupMemberSet($principal)
  311. {
  312. $principal = $this->getPrincipalByPath($principal);
  313. if (!$principal) {
  314. throw new DAV\Exception('Principal not found');
  315. }
  316. $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?');
  317. $stmt->execute([$principal['id']]);
  318. $result = [];
  319. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  320. $result[] = $row['uri'];
  321. }
  322. return $result;
  323. }
  324. /**
  325. * Returns the list of groups a principal is a member of.
  326. *
  327. * @param string $principal
  328. *
  329. * @return array
  330. */
  331. public function getGroupMembership($principal)
  332. {
  333. $principal = $this->getPrincipalByPath($principal);
  334. if (!$principal) {
  335. throw new DAV\Exception('Principal not found');
  336. }
  337. $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?');
  338. $stmt->execute([$principal['id']]);
  339. $result = [];
  340. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  341. $result[] = $row['uri'];
  342. }
  343. return $result;
  344. }
  345. /**
  346. * Updates the list of group members for a group principal.
  347. *
  348. * The principals should be passed as a list of uri's.
  349. *
  350. * @param string $principal
  351. */
  352. public function setGroupMemberSet($principal, array $members)
  353. {
  354. // Grabbing the list of principal id's.
  355. $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');');
  356. $stmt->execute(array_merge([$principal], $members));
  357. $memberIds = [];
  358. $principalId = null;
  359. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  360. if ($row['uri'] == $principal) {
  361. $principalId = $row['id'];
  362. } else {
  363. $memberIds[] = $row['id'];
  364. }
  365. }
  366. if (!$principalId) {
  367. throw new DAV\Exception('Principal not found');
  368. }
  369. // Wiping out old members
  370. $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;');
  371. $stmt->execute([$principalId]);
  372. foreach ($memberIds as $memberId) {
  373. $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);');
  374. $stmt->execute([$principalId, $memberId]);
  375. }
  376. }
  377. /**
  378. * Creates a new principal.
  379. *
  380. * This method receives a full path for the new principal. The mkCol object
  381. * contains any additional webdav properties specified during the creation
  382. * of the principal.
  383. *
  384. * @param string $path
  385. */
  386. public function createPrincipal($path, MkCol $mkCol)
  387. {
  388. $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)');
  389. $stmt->execute([$path]);
  390. $this->updatePrincipal($path, $mkCol);
  391. }
  392. }