2222use OCP \DB \QueryBuilder \IQueryBuilder ;
2323use OCP \IDBConnection ;
2424use OCP \IUser ;
25+ use Override ;
2526use Sabre \CalDAV \Schedule \Inbox ;
2627use Sabre \DAV \Exception as DavException ;
2728use Sabre \DAV \PropertyStorage \Backend \BackendInterface ;
@@ -98,13 +99,15 @@ class CustomPropertiesBackend implements BackendInterface {
9899 /**
99100 * Map of custom XML elements to parse when trying to deserialize an instance of
100101 * \Sabre\DAV\Xml\Property\Complex to find a more specialized PROPERTY_TYPE_*
102+ * @var array<string, class-string>
101103 */
102104 private const COMPLEX_XML_ELEMENT_MAP = [
103105 '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL ' => Href::class,
104106 ];
105107
106108 /**
107109 * Map of well-known property names to default values
110+ * @var array<string, string>
108111 */
109112 private const PROPERTY_DEFAULT_VALUES = [
110113 '{http://owncloud.org/ns}calendar-enabled ' => '1 ' ,
@@ -118,17 +121,15 @@ class CustomPropertiesBackend implements BackendInterface {
118121 private XmlService $ xmlService ;
119122
120123 /**
121- * @param Tree $tree node tree
122- * @param IDBConnection $connection database connection
123124 * @param IUser $user owner of the tree and properties
124125 */
125126 public function __construct (
126- private Server $ server ,
127- private Tree $ tree ,
128- private IDBConnection $ connection ,
129- private IUser $ user ,
130- private PropertyMapper $ propertyMapper ,
131- private DefaultCalendarValidator $ defaultCalendarValidator ,
127+ private readonly Server $ server ,
128+ private readonly Tree $ tree ,
129+ private readonly IDBConnection $ connection ,
130+ private readonly IUser $ user ,
131+ private readonly PropertyMapper $ propertyMapper ,
132+ private readonly DefaultCalendarValidator $ defaultCalendarValidator ,
132133 ) {
133134 $ this ->xmlService = new XmlService ();
134135 $ this ->xmlService ->elementMap = array_merge (
@@ -142,9 +143,9 @@ public function __construct(
142143 *
143144 * @param string $path
144145 * @param PropFind $propFind
145- * @return void
146146 */
147- public function propFind ($ path , PropFind $ propFind ) {
147+ #[Override]
148+ public function propFind ($ path , PropFind $ propFind ): void {
148149 $ requestedProps = $ propFind ->get404Properties ();
149150
150151 $ requestedProps = array_filter (
@@ -257,12 +258,10 @@ private function isPropertyAllowed(string $property): bool {
257258 * Updates properties for a path
258259 *
259260 * @param string $path
260- * @param PropPatch $propPatch
261- *
262- * @return void
263261 */
264- public function propPatch ($ path , PropPatch $ propPatch ) {
265- $ propPatch ->handleRemaining (function ($ changedProps ) use ($ path ) {
262+ #[Override]
263+ public function propPatch ($ path , PropPatch $ propPatch ): void {
264+ $ propPatch ->handleRemaining (function (array $ changedProps ) use ($ path ) {
266265 return $ this ->updateProperties ($ path , $ changedProps );
267266 });
268267 }
@@ -272,13 +271,13 @@ public function propPatch($path, PropPatch $propPatch) {
272271 *
273272 * @param string $path path of node for which to delete properties
274273 */
275- public function delete ( $ path ) {
276- $ statement = $ this -> connection -> prepare (
277- ' DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ? '
278- );
279- $ statement -> execute ([ $ this -> user -> getUID () , $ this -> formatPath ( $ path )]);
280- $ statement -> closeCursor ( );
281-
274+ #[Override]
275+ public function delete ( $ path ): void {
276+ $ qb = $ this -> connection -> getQueryBuilder ();
277+ $ qb -> delete ( ' properties ' )
278+ -> where ( $ qb -> expr ()-> eq ( ' userid ' , $ qb -> createNamedParameter ( $ this -> user -> getUID ())))
279+ -> andWhere ( $ qb -> expr ()-> eq ( ' propertypath ' , $ qb -> createNamedParameter ( $ this -> formatPath ( $ path ))) );
280+ $ qb -> executeStatement ();
282281 unset($ this ->userCache [$ path ]);
283282 }
284283
@@ -287,16 +286,15 @@ public function delete($path) {
287286 *
288287 * @param string $source
289288 * @param string $destination
290- *
291- * @return void
292289 */
293- public function move ($ source , $ destination ) {
294- $ statement = $ this ->connection ->prepare (
295- 'UPDATE `*PREFIX*properties` SET `propertypath` = ? '
296- . ' WHERE `userid` = ? AND `propertypath` = ? '
297- );
298- $ statement ->execute ([$ this ->formatPath ($ destination ), $ this ->user ->getUID (), $ this ->formatPath ($ source )]);
299- $ statement ->closeCursor ();
290+ #[Override]
291+ public function move ($ source , $ destination ): void {
292+ $ qb = $ this ->connection ->getQueryBuilder ();
293+ $ qb ->update ('properties ' )
294+ ->set ('propertypath ' , $ qb ->createNamedParameter ($ this ->formatPath ($ destination )))
295+ ->where ($ qb ->expr ()->eq ('userid ' , $ qb ->createNamedParameter ($ this ->user ->getUID ())))
296+ ->andWhere ($ qb ->expr ()->eq ('propertypath ' , $ qb ->createNamedParameter ($ this ->formatPath ($ source ))));
297+ $ qb ->executeStatement ();
300298 }
301299
302300 /**
@@ -325,10 +323,10 @@ private function validateProperty(string $path, string $propName, mixed $propVal
325323 }
326324
327325 /**
328- * @param string $path
329326 * @param string[] $requestedProperties
330327 *
331- * @return array
328+ * @return array<string, mixed|Complex|Href|string>
329+ * @throws \OCP\DB\Exception
332330 */
333331 private function getPublishedProperties (string $ path , array $ requestedProperties ): array {
334332 $ allowedProps = array_intersect (self ::PUBLISHED_READ_ONLY_PROPERTIES , $ requestedProperties );
@@ -356,7 +354,7 @@ private function getPublishedProperties(string $path, array $requestedProperties
356354 }
357355
358356 /**
359- * prefetch all user properties in a directory
357+ * Prefetch all user properties in a directory
360358 */
361359 private function cacheDirectory (string $ path , Directory $ node ): void {
362360 $ prefix = ltrim ($ path . '/ ' , '/ ' );
@@ -449,45 +447,44 @@ private function cacheCalendars(CalendarHome $node, array $requestedProperties):
449447 /**
450448 * Returns a list of properties for the given path and current user
451449 *
452- * @param string $path
453450 * @param array $requestedProperties requested properties or empty array for "all"
454- * @return array
451+ * @return array<string, mixed>
455452 * @note The properties list is a list of propertynames the client
456453 * requested, encoded as xmlnamespace#tagName, for example:
457454 * http://www.example.org/namespace#author If the array is empty, all
458455 * properties should be returned
459456 */
460- private function getUserProperties (string $ path , array $ requestedProperties ) {
457+ private function getUserProperties (string $ path , array $ requestedProperties ): array {
461458 if (isset ($ this ->userCache [$ path ])) {
462459 return $ this ->userCache [$ path ];
463460 }
464461
465- // TODO: chunking if more than 1000 properties
466- $ sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ? ' ;
462+ $ props = [];
467463
468- $ whereValues = [$ this ->user ->getUID (), $ this ->formatPath ($ path )];
469- $ whereTypes = [null , null ];
464+ $ qb = $ this ->connection ->getQueryBuilder ();
465+ $ qb ->select ('* ' )
466+ ->from ('properties ' )
467+ ->where ($ qb ->expr ()->eq ('userid ' , $ qb ->createNamedParameter ($ this ->user ->getUID (), IQueryBuilder::PARAM_STR )))
468+ ->andWhere ($ qb ->expr ()->eq ('propertypath ' , $ qb ->createNamedParameter ($ this ->formatPath ($ path ), IQueryBuilder::PARAM_STR )));
470469
471470 if (!empty ($ requestedProperties )) {
472471 // request only a subset
473- $ sql .= ' AND ` propertyname` in (?) ' ;
474- $ whereValues [] = $ requestedProperties ;
475- $ whereTypes [] = IQueryBuilder:: PARAM_STR_ARRAY ;
476- }
477-
478- $ result = $ this -> connection -> executeQuery (
479- $ sql ,
480- $ whereValues ,
481- $ whereTypes
482- );
483-
484- $ props = [];
485- while ( $ row = $ result -> fetch ()) {
486- $ props [ $ row [ ' propertyname ' ]] = $ this -> decodeValueFromDatabase ( $ row [ ' propertyvalue ' ], $ row [ ' valuetype ' ]);
472+ $ qb -> andWhere ( $ qb -> expr ()-> in ( ' propertyname ' , $ qb -> createParameter ( ' requestedProperties ' ))) ;
473+ $ chunks = array_chunk ( $ requestedProperties, 1000 ) ;
474+ foreach ( $ chunks as $ chunk ) {
475+ $ qb -> setParameter ( ' requestedProperties ' , $ chunk , IQueryBuilder:: PARAM_STR_ARRAY );
476+ $ result = $ qb -> executeQuery ();
477+ while ( $ row = $ result -> fetch ()) {
478+ $ props [ $ row [ ' propertyname ' ]] = $ this -> decodeValueFromDatabase ( $ row [ ' propertyvalue ' ], $ row [ ' valuetype ' ]);
479+ }
480+ }
481+ } else {
482+ $ result = $ qb -> executeQuery ();
483+ while ( $ row = $ result -> fetch ()) {
484+ $ props [ $ row[ ' propertyname ' ]] = $ this -> decodeValueFromDatabase ( $ row [ ' propertyvalue ' ], $ row [ ' valuetype ' ]);
485+ }
487486 }
488487
489- $ result ->closeCursor ();
490-
491488 $ this ->userCache [$ path ] = $ props ;
492489 return $ props ;
493490 }
@@ -501,6 +498,7 @@ private function isPropertyDefaultValue(string $name, mixed $value): bool {
501498 }
502499
503500 /**
501+ * @param array<string, string> $properties
504502 * @throws Exception
505503 */
506504 private function updateProperties (string $ path , array $ properties ): bool {
@@ -558,10 +556,7 @@ private function updateProperties(string $path, array $properties): bool {
558556 }
559557
560558 /**
561- * long paths are hashed to ensure they fit in the database
562- *
563- * @param string $path
564- * @return string
559+ * Long paths are hashed to ensure they fit in the database
565560 */
566561 private function formatPath (string $ path ): string {
567562 if (strlen ($ path ) > 250 ) {
@@ -616,20 +611,13 @@ private function encodeValueForDatabase(string $path, string $name, mixed $value
616611 /**
617612 * @return mixed|Complex|string
618613 */
619- private function decodeValueFromDatabase (string $ value , int $ valueType ) {
620- switch ($ valueType ) {
621- case self ::PROPERTY_TYPE_XML :
622- return new Complex ($ value );
623- case self ::PROPERTY_TYPE_HREF :
624- return new Href ($ value );
625- case self ::PROPERTY_TYPE_OBJECT :
626- // some databases can not handel null characters, these are custom encoded during serialization
627- // this custom encoding needs to be first reversed before unserializing
628- return unserialize (str_replace ('\x00 ' , chr (0 ), $ value ));
629- case self ::PROPERTY_TYPE_STRING :
630- default :
631- return $ value ;
632- }
614+ private function decodeValueFromDatabase (string $ value , int $ valueType ): mixed {
615+ return match ($ valueType ) {
616+ self ::PROPERTY_TYPE_XML => new Complex ($ value ),
617+ self ::PROPERTY_TYPE_HREF => new Href ($ value ),
618+ self ::PROPERTY_TYPE_OBJECT => unserialize (str_replace ('\x00 ' , chr (0 ), $ value )),
619+ default => $ value ,
620+ };
633621 }
634622
635623 private function encodeDefaultCalendarUrl (Href $ value ): Href {
0 commit comments