CalDavBackend.php 130 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\DAV\CalDAV;
  8. use DateTime;
  9. use DateTimeImmutable;
  10. use DateTimeInterface;
  11. use OCA\DAV\AppInfo\Application;
  12. use OCA\DAV\CalDAV\Sharing\Backend;
  13. use OCA\DAV\Connector\Sabre\Principal;
  14. use OCA\DAV\DAV\Sharing\IShareable;
  15. use OCA\DAV\Events\CachedCalendarObjectCreatedEvent;
  16. use OCA\DAV\Events\CachedCalendarObjectDeletedEvent;
  17. use OCA\DAV\Events\CachedCalendarObjectUpdatedEvent;
  18. use OCA\DAV\Events\CalendarCreatedEvent;
  19. use OCA\DAV\Events\CalendarDeletedEvent;
  20. use OCA\DAV\Events\CalendarMovedToTrashEvent;
  21. use OCA\DAV\Events\CalendarObjectCreatedEvent;
  22. use OCA\DAV\Events\CalendarObjectDeletedEvent;
  23. use OCA\DAV\Events\CalendarObjectMovedEvent;
  24. use OCA\DAV\Events\CalendarObjectMovedToTrashEvent;
  25. use OCA\DAV\Events\CalendarObjectRestoredEvent;
  26. use OCA\DAV\Events\CalendarObjectUpdatedEvent;
  27. use OCA\DAV\Events\CalendarPublishedEvent;
  28. use OCA\DAV\Events\CalendarRestoredEvent;
  29. use OCA\DAV\Events\CalendarShareUpdatedEvent;
  30. use OCA\DAV\Events\CalendarUnpublishedEvent;
  31. use OCA\DAV\Events\CalendarUpdatedEvent;
  32. use OCA\DAV\Events\SubscriptionCreatedEvent;
  33. use OCA\DAV\Events\SubscriptionDeletedEvent;
  34. use OCA\DAV\Events\SubscriptionUpdatedEvent;
  35. use OCP\AppFramework\Db\TTransactional;
  36. use OCP\Calendar\Exceptions\CalendarException;
  37. use OCP\DB\Exception;
  38. use OCP\DB\QueryBuilder\IQueryBuilder;
  39. use OCP\EventDispatcher\IEventDispatcher;
  40. use OCP\IConfig;
  41. use OCP\IDBConnection;
  42. use OCP\IUserManager;
  43. use OCP\Security\ISecureRandom;
  44. use Psr\Log\LoggerInterface;
  45. use RuntimeException;
  46. use Sabre\CalDAV\Backend\AbstractBackend;
  47. use Sabre\CalDAV\Backend\SchedulingSupport;
  48. use Sabre\CalDAV\Backend\SubscriptionSupport;
  49. use Sabre\CalDAV\Backend\SyncSupport;
  50. use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
  51. use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
  52. use Sabre\DAV;
  53. use Sabre\DAV\Exception\BadRequest;
  54. use Sabre\DAV\Exception\Forbidden;
  55. use Sabre\DAV\Exception\NotFound;
  56. use Sabre\DAV\PropPatch;
  57. use Sabre\Uri;
  58. use Sabre\VObject\Component;
  59. use Sabre\VObject\Component\VCalendar;
  60. use Sabre\VObject\Component\VTimeZone;
  61. use Sabre\VObject\DateTimeParser;
  62. use Sabre\VObject\InvalidDataException;
  63. use Sabre\VObject\ParseException;
  64. use Sabre\VObject\Property;
  65. use Sabre\VObject\Reader;
  66. use Sabre\VObject\Recur\EventIterator;
  67. use Sabre\VObject\Recur\MaxInstancesExceededException;
  68. use Sabre\VObject\Recur\NoInstancesException;
  69. use function array_column;
  70. use function array_map;
  71. use function array_merge;
  72. use function array_values;
  73. use function explode;
  74. use function is_array;
  75. use function is_resource;
  76. use function pathinfo;
  77. use function rewind;
  78. use function settype;
  79. use function sprintf;
  80. use function str_replace;
  81. use function strtolower;
  82. use function time;
  83. /**
  84. * Class CalDavBackend
  85. *
  86. * Code is heavily inspired by https://github.com/fruux/sabre-dav/blob/master/lib/CalDAV/Backend/PDO.php
  87. *
  88. * @package OCA\DAV\CalDAV
  89. */
  90. class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport {
  91. use TTransactional;
  92. public const CALENDAR_TYPE_CALENDAR = 0;
  93. public const CALENDAR_TYPE_SUBSCRIPTION = 1;
  94. public const PERSONAL_CALENDAR_URI = 'personal';
  95. public const PERSONAL_CALENDAR_NAME = 'Personal';
  96. public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar';
  97. public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar';
  98. /**
  99. * We need to specify a max date, because we need to stop *somewhere*
  100. *
  101. * On 32 bit system the maximum for a signed integer is 2147483647, so
  102. * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
  103. * in 2038-01-19 to avoid problems when the date is converted
  104. * to a unix timestamp.
  105. */
  106. public const MAX_DATE = '2038-01-01';
  107. public const ACCESS_PUBLIC = 4;
  108. public const CLASSIFICATION_PUBLIC = 0;
  109. public const CLASSIFICATION_PRIVATE = 1;
  110. public const CLASSIFICATION_CONFIDENTIAL = 2;
  111. /**
  112. * List of CalDAV properties, and how they map to database field names and their type
  113. * Add your own properties by simply adding on to this array.
  114. *
  115. * @var array
  116. * @psalm-var array<string, string[]>
  117. */
  118. public array $propertyMap = [
  119. '{DAV:}displayname' => ['displayname', 'string'],
  120. '{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'],
  121. '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'],
  122. '{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
  123. '{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
  124. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'],
  125. ];
  126. /**
  127. * List of subscription properties, and how they map to database field names.
  128. *
  129. * @var array
  130. */
  131. public array $subscriptionPropertyMap = [
  132. '{DAV:}displayname' => ['displayname', 'string'],
  133. '{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'],
  134. '{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'],
  135. '{http://apple.com/ns/ical/}calendar-color' => ['calendarcolor', 'string'],
  136. '{http://calendarserver.org/ns/}subscribed-strip-todos' => ['striptodos', 'bool'],
  137. '{http://calendarserver.org/ns/}subscribed-strip-alarms' => ['stripalarms', 'string'],
  138. '{http://calendarserver.org/ns/}subscribed-strip-attachments' => ['stripattachments', 'string'],
  139. ];
  140. /**
  141. * properties to index
  142. *
  143. * This list has to be kept in sync with ICalendarQuery::SEARCH_PROPERTY_*
  144. *
  145. * @see \OCP\Calendar\ICalendarQuery
  146. */
  147. private const INDEXED_PROPERTIES = [
  148. 'CATEGORIES',
  149. 'COMMENT',
  150. 'DESCRIPTION',
  151. 'LOCATION',
  152. 'RESOURCES',
  153. 'STATUS',
  154. 'SUMMARY',
  155. 'ATTENDEE',
  156. 'CONTACT',
  157. 'ORGANIZER'
  158. ];
  159. /** @var array parameters to index */
  160. public static array $indexParameters = [
  161. 'ATTENDEE' => ['CN'],
  162. 'ORGANIZER' => ['CN'],
  163. ];
  164. /**
  165. * @var string[] Map of uid => display name
  166. */
  167. protected array $userDisplayNames;
  168. private string $dbObjectsTable = 'calendarobjects';
  169. private string $dbObjectPropertiesTable = 'calendarobjects_props';
  170. private string $dbObjectInvitationsTable = 'calendar_invitations';
  171. private array $cachedObjects = [];
  172. public function __construct(
  173. private IDBConnection $db,
  174. private Principal $principalBackend,
  175. private IUserManager $userManager,
  176. private ISecureRandom $random,
  177. private LoggerInterface $logger,
  178. private IEventDispatcher $dispatcher,
  179. private IConfig $config,
  180. private Sharing\Backend $calendarSharingBackend,
  181. private bool $legacyEndpoint = false,
  182. ) {
  183. }
  184. /**
  185. * Return the number of calendars for a principal
  186. *
  187. * By default this excludes the automatically generated birthday calendar
  188. *
  189. * @param $principalUri
  190. * @param bool $excludeBirthday
  191. * @return int
  192. */
  193. public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) {
  194. $principalUri = $this->convertPrincipal($principalUri, true);
  195. $query = $this->db->getQueryBuilder();
  196. $query->select($query->func()->count('*'))
  197. ->from('calendars');
  198. if ($principalUri === '') {
  199. $query->where($query->expr()->emptyString('principaluri'));
  200. } else {
  201. $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
  202. }
  203. if ($excludeBirthday) {
  204. $query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)));
  205. }
  206. $result = $query->executeQuery();
  207. $column = (int)$result->fetchOne();
  208. $result->closeCursor();
  209. return $column;
  210. }
  211. /**
  212. * Return the number of subscriptions for a principal
  213. */
  214. public function getSubscriptionsForUserCount(string $principalUri): int {
  215. $principalUri = $this->convertPrincipal($principalUri, true);
  216. $query = $this->db->getQueryBuilder();
  217. $query->select($query->func()->count('*'))
  218. ->from('calendarsubscriptions');
  219. if ($principalUri === '') {
  220. $query->where($query->expr()->emptyString('principaluri'));
  221. } else {
  222. $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
  223. }
  224. $result = $query->executeQuery();
  225. $column = (int)$result->fetchOne();
  226. $result->closeCursor();
  227. return $column;
  228. }
  229. /**
  230. * @return array{id: int, deleted_at: int}[]
  231. */
  232. public function getDeletedCalendars(int $deletedBefore): array {
  233. $qb = $this->db->getQueryBuilder();
  234. $qb->select(['id', 'deleted_at'])
  235. ->from('calendars')
  236. ->where($qb->expr()->isNotNull('deleted_at'))
  237. ->andWhere($qb->expr()->lt('deleted_at', $qb->createNamedParameter($deletedBefore)));
  238. $result = $qb->executeQuery();
  239. $calendars = [];
  240. while (($row = $result->fetch()) !== false) {
  241. $calendars[] = [
  242. 'id' => (int)$row['id'],
  243. 'deleted_at' => (int)$row['deleted_at'],
  244. ];
  245. }
  246. $result->closeCursor();
  247. return $calendars;
  248. }
  249. /**
  250. * Returns a list of calendars for a principal.
  251. *
  252. * Every project is an array with the following keys:
  253. * * id, a unique id that will be used by other functions to modify the
  254. * calendar. This can be the same as the uri or a database key.
  255. * * uri, which the basename of the uri with which the calendar is
  256. * accessed.
  257. * * principaluri. The owner of the calendar. Almost always the same as
  258. * principalUri passed to this method.
  259. *
  260. * Furthermore it can contain webdav properties in clark notation. A very
  261. * common one is '{DAV:}displayname'.
  262. *
  263. * Many clients also require:
  264. * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
  265. * For this property, you can just return an instance of
  266. * Sabre\CalDAV\Property\SupportedCalendarComponentSet.
  267. *
  268. * If you return {http://sabredav.org/ns}read-only and set the value to 1,
  269. * ACL will automatically be put in read-only mode.
  270. *
  271. * @param string $principalUri
  272. * @return array
  273. */
  274. public function getCalendarsForUser($principalUri) {
  275. return $this->atomic(function () use ($principalUri) {
  276. $principalUriOriginal = $principalUri;
  277. $principalUri = $this->convertPrincipal($principalUri, true);
  278. $fields = array_column($this->propertyMap, 0);
  279. $fields[] = 'id';
  280. $fields[] = 'uri';
  281. $fields[] = 'synctoken';
  282. $fields[] = 'components';
  283. $fields[] = 'principaluri';
  284. $fields[] = 'transparent';
  285. // Making fields a comma-delimited list
  286. $query = $this->db->getQueryBuilder();
  287. $query->select($fields)
  288. ->from('calendars')
  289. ->orderBy('calendarorder', 'ASC');
  290. if ($principalUri === '') {
  291. $query->where($query->expr()->emptyString('principaluri'));
  292. } else {
  293. $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)));
  294. }
  295. $result = $query->executeQuery();
  296. $calendars = [];
  297. while ($row = $result->fetch()) {
  298. $row['principaluri'] = (string)$row['principaluri'];
  299. $components = [];
  300. if ($row['components']) {
  301. $components = explode(',', $row['components']);
  302. }
  303. $calendar = [
  304. 'id' => $row['id'],
  305. 'uri' => $row['uri'],
  306. 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  307. '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
  308. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  309. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
  310. '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
  311. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
  312. ];
  313. $calendar = $this->rowToCalendar($row, $calendar);
  314. $calendar = $this->addOwnerPrincipalToCalendar($calendar);
  315. $calendar = $this->addResourceTypeToCalendar($row, $calendar);
  316. if (!isset($calendars[$calendar['id']])) {
  317. $calendars[$calendar['id']] = $calendar;
  318. }
  319. }
  320. $result->closeCursor();
  321. // query for shared calendars
  322. $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true);
  323. $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal));
  324. $principals[] = $principalUri;
  325. $fields = array_column($this->propertyMap, 0);
  326. $fields = array_map(function (string $field) {
  327. return 'a.' . $field;
  328. }, $fields);
  329. $fields[] = 'a.id';
  330. $fields[] = 'a.uri';
  331. $fields[] = 'a.synctoken';
  332. $fields[] = 'a.components';
  333. $fields[] = 'a.principaluri';
  334. $fields[] = 'a.transparent';
  335. $fields[] = 's.access';
  336. $select = $this->db->getQueryBuilder();
  337. $subSelect = $this->db->getQueryBuilder();
  338. $subSelect->select('resourceid')
  339. ->from('dav_shares', 'd')
  340. ->where($subSelect->expr()->eq('d.access', $select->createNamedParameter(Backend::ACCESS_UNSHARED, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
  341. ->andWhere($subSelect->expr()->in('d.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY));
  342. $select->select($fields)
  343. ->from('dav_shares', 's')
  344. ->join('s', 'calendars', 'a', $select->expr()->eq('s.resourceid', 'a.id', IQueryBuilder::PARAM_INT))
  345. ->where($select->expr()->in('s.principaluri', $select->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
  346. ->andWhere($select->expr()->eq('s.type', $select->createNamedParameter('calendar', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR))
  347. ->andWhere($select->expr()->notIn('a.id', $select->createFunction($subSelect->getSQL()), IQueryBuilder::PARAM_INT_ARRAY));
  348. $results = $select->executeQuery();
  349. $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only';
  350. while ($row = $results->fetch()) {
  351. $row['principaluri'] = (string)$row['principaluri'];
  352. if ($row['principaluri'] === $principalUri) {
  353. continue;
  354. }
  355. $readOnly = (int)$row['access'] === Backend::ACCESS_READ;
  356. if (isset($calendars[$row['id']])) {
  357. if ($readOnly) {
  358. // New share can not have more permissions than the old one.
  359. continue;
  360. }
  361. if (isset($calendars[$row['id']][$readOnlyPropertyName]) &&
  362. $calendars[$row['id']][$readOnlyPropertyName] === 0) {
  363. // Old share is already read-write, no more permissions can be gained
  364. continue;
  365. }
  366. }
  367. [, $name] = Uri\split($row['principaluri']);
  368. $uri = $row['uri'] . '_shared_by_' . $name;
  369. $row['displayname'] = $row['displayname'] . ' (' . ($this->userManager->getDisplayName($name) ?? ($name ?? '')) . ')';
  370. $components = [];
  371. if ($row['components']) {
  372. $components = explode(',', $row['components']);
  373. }
  374. $calendar = [
  375. 'id' => $row['id'],
  376. 'uri' => $uri,
  377. 'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint),
  378. '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
  379. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  380. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
  381. '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'),
  382. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  383. $readOnlyPropertyName => $readOnly,
  384. ];
  385. $calendar = $this->rowToCalendar($row, $calendar);
  386. $calendar = $this->addOwnerPrincipalToCalendar($calendar);
  387. $calendar = $this->addResourceTypeToCalendar($row, $calendar);
  388. $calendars[$calendar['id']] = $calendar;
  389. }
  390. $result->closeCursor();
  391. return array_values($calendars);
  392. }, $this->db);
  393. }
  394. /**
  395. * @param $principalUri
  396. * @return array
  397. */
  398. public function getUsersOwnCalendars($principalUri) {
  399. $principalUri = $this->convertPrincipal($principalUri, true);
  400. $fields = array_column($this->propertyMap, 0);
  401. $fields[] = 'id';
  402. $fields[] = 'uri';
  403. $fields[] = 'synctoken';
  404. $fields[] = 'components';
  405. $fields[] = 'principaluri';
  406. $fields[] = 'transparent';
  407. // Making fields a comma-delimited list
  408. $query = $this->db->getQueryBuilder();
  409. $query->select($fields)->from('calendars')
  410. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
  411. ->orderBy('calendarorder', 'ASC');
  412. $stmt = $query->executeQuery();
  413. $calendars = [];
  414. while ($row = $stmt->fetch()) {
  415. $row['principaluri'] = (string)$row['principaluri'];
  416. $components = [];
  417. if ($row['components']) {
  418. $components = explode(',', $row['components']);
  419. }
  420. $calendar = [
  421. 'id' => $row['id'],
  422. 'uri' => $row['uri'],
  423. 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  424. '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
  425. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  426. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
  427. '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
  428. ];
  429. $calendar = $this->rowToCalendar($row, $calendar);
  430. $calendar = $this->addOwnerPrincipalToCalendar($calendar);
  431. $calendar = $this->addResourceTypeToCalendar($row, $calendar);
  432. if (!isset($calendars[$calendar['id']])) {
  433. $calendars[$calendar['id']] = $calendar;
  434. }
  435. }
  436. $stmt->closeCursor();
  437. return array_values($calendars);
  438. }
  439. /**
  440. * @return array
  441. */
  442. public function getPublicCalendars() {
  443. $fields = array_column($this->propertyMap, 0);
  444. $fields[] = 'a.id';
  445. $fields[] = 'a.uri';
  446. $fields[] = 'a.synctoken';
  447. $fields[] = 'a.components';
  448. $fields[] = 'a.principaluri';
  449. $fields[] = 'a.transparent';
  450. $fields[] = 's.access';
  451. $fields[] = 's.publicuri';
  452. $calendars = [];
  453. $query = $this->db->getQueryBuilder();
  454. $result = $query->select($fields)
  455. ->from('dav_shares', 's')
  456. ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
  457. ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
  458. ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
  459. ->executeQuery();
  460. while ($row = $result->fetch()) {
  461. $row['principaluri'] = (string)$row['principaluri'];
  462. [, $name] = Uri\split($row['principaluri']);
  463. $row['displayname'] = $row['displayname'] . "($name)";
  464. $components = [];
  465. if ($row['components']) {
  466. $components = explode(',', $row['components']);
  467. }
  468. $calendar = [
  469. 'id' => $row['id'],
  470. 'uri' => $row['publicuri'],
  471. 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  472. '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
  473. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  474. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
  475. '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
  476. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint),
  477. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
  478. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
  479. ];
  480. $calendar = $this->rowToCalendar($row, $calendar);
  481. $calendar = $this->addOwnerPrincipalToCalendar($calendar);
  482. $calendar = $this->addResourceTypeToCalendar($row, $calendar);
  483. if (!isset($calendars[$calendar['id']])) {
  484. $calendars[$calendar['id']] = $calendar;
  485. }
  486. }
  487. $result->closeCursor();
  488. return array_values($calendars);
  489. }
  490. /**
  491. * @param string $uri
  492. * @return array
  493. * @throws NotFound
  494. */
  495. public function getPublicCalendar($uri) {
  496. $fields = array_column($this->propertyMap, 0);
  497. $fields[] = 'a.id';
  498. $fields[] = 'a.uri';
  499. $fields[] = 'a.synctoken';
  500. $fields[] = 'a.components';
  501. $fields[] = 'a.principaluri';
  502. $fields[] = 'a.transparent';
  503. $fields[] = 's.access';
  504. $fields[] = 's.publicuri';
  505. $query = $this->db->getQueryBuilder();
  506. $result = $query->select($fields)
  507. ->from('dav_shares', 's')
  508. ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id'))
  509. ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
  510. ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar')))
  511. ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri)))
  512. ->executeQuery();
  513. $row = $result->fetch();
  514. $result->closeCursor();
  515. if ($row === false) {
  516. throw new NotFound('Node with name \'' . $uri . '\' could not be found');
  517. }
  518. $row['principaluri'] = (string)$row['principaluri'];
  519. [, $name] = Uri\split($row['principaluri']);
  520. $row['displayname'] = $row['displayname'] . ' ' . "($name)";
  521. $components = [];
  522. if ($row['components']) {
  523. $components = explode(',', $row['components']);
  524. }
  525. $calendar = [
  526. 'id' => $row['id'],
  527. 'uri' => $row['publicuri'],
  528. 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  529. '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
  530. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  531. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
  532. '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
  533. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  534. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ,
  535. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC,
  536. ];
  537. $calendar = $this->rowToCalendar($row, $calendar);
  538. $calendar = $this->addOwnerPrincipalToCalendar($calendar);
  539. $calendar = $this->addResourceTypeToCalendar($row, $calendar);
  540. return $calendar;
  541. }
  542. /**
  543. * @param string $principal
  544. * @param string $uri
  545. * @return array|null
  546. */
  547. public function getCalendarByUri($principal, $uri) {
  548. $fields = array_column($this->propertyMap, 0);
  549. $fields[] = 'id';
  550. $fields[] = 'uri';
  551. $fields[] = 'synctoken';
  552. $fields[] = 'components';
  553. $fields[] = 'principaluri';
  554. $fields[] = 'transparent';
  555. // Making fields a comma-delimited list
  556. $query = $this->db->getQueryBuilder();
  557. $query->select($fields)->from('calendars')
  558. ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
  559. ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal)))
  560. ->setMaxResults(1);
  561. $stmt = $query->executeQuery();
  562. $row = $stmt->fetch();
  563. $stmt->closeCursor();
  564. if ($row === false) {
  565. return null;
  566. }
  567. $row['principaluri'] = (string)$row['principaluri'];
  568. $components = [];
  569. if ($row['components']) {
  570. $components = explode(',', $row['components']);
  571. }
  572. $calendar = [
  573. 'id' => $row['id'],
  574. 'uri' => $row['uri'],
  575. 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  576. '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
  577. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  578. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
  579. '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
  580. ];
  581. $calendar = $this->rowToCalendar($row, $calendar);
  582. $calendar = $this->addOwnerPrincipalToCalendar($calendar);
  583. $calendar = $this->addResourceTypeToCalendar($row, $calendar);
  584. return $calendar;
  585. }
  586. /**
  587. * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp, '{urn:ietf:params:xml:ns:caldav}calendar-timezone': ?string }|null
  588. */
  589. public function getCalendarById(int $calendarId): ?array {
  590. $fields = array_column($this->propertyMap, 0);
  591. $fields[] = 'id';
  592. $fields[] = 'uri';
  593. $fields[] = 'synctoken';
  594. $fields[] = 'components';
  595. $fields[] = 'principaluri';
  596. $fields[] = 'transparent';
  597. // Making fields a comma-delimited list
  598. $query = $this->db->getQueryBuilder();
  599. $query->select($fields)->from('calendars')
  600. ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
  601. ->setMaxResults(1);
  602. $stmt = $query->executeQuery();
  603. $row = $stmt->fetch();
  604. $stmt->closeCursor();
  605. if ($row === false) {
  606. return null;
  607. }
  608. $row['principaluri'] = (string)$row['principaluri'];
  609. $components = [];
  610. if ($row['components']) {
  611. $components = explode(',', $row['components']);
  612. }
  613. $calendar = [
  614. 'id' => $row['id'],
  615. 'uri' => $row['uri'],
  616. 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint),
  617. '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken'] ?: '0'),
  618. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0,
  619. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components),
  620. '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'),
  621. ];
  622. $calendar = $this->rowToCalendar($row, $calendar);
  623. $calendar = $this->addOwnerPrincipalToCalendar($calendar);
  624. $calendar = $this->addResourceTypeToCalendar($row, $calendar);
  625. return $calendar;
  626. }
  627. /**
  628. * @param $subscriptionId
  629. */
  630. public function getSubscriptionById($subscriptionId) {
  631. $fields = array_column($this->subscriptionPropertyMap, 0);
  632. $fields[] = 'id';
  633. $fields[] = 'uri';
  634. $fields[] = 'source';
  635. $fields[] = 'synctoken';
  636. $fields[] = 'principaluri';
  637. $fields[] = 'lastmodified';
  638. $query = $this->db->getQueryBuilder();
  639. $query->select($fields)
  640. ->from('calendarsubscriptions')
  641. ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
  642. ->orderBy('calendarorder', 'asc');
  643. $stmt = $query->executeQuery();
  644. $row = $stmt->fetch();
  645. $stmt->closeCursor();
  646. if ($row === false) {
  647. return null;
  648. }
  649. $row['principaluri'] = (string)$row['principaluri'];
  650. $subscription = [
  651. 'id' => $row['id'],
  652. 'uri' => $row['uri'],
  653. 'principaluri' => $row['principaluri'],
  654. 'source' => $row['source'],
  655. 'lastmodified' => $row['lastmodified'],
  656. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
  657. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  658. ];
  659. return $this->rowToSubscription($row, $subscription);
  660. }
  661. /**
  662. * Creates a new calendar for a principal.
  663. *
  664. * If the creation was a success, an id must be returned that can be used to reference
  665. * this calendar in other methods, such as updateCalendar.
  666. *
  667. * @param string $principalUri
  668. * @param string $calendarUri
  669. * @param array $properties
  670. * @return int
  671. *
  672. * @throws CalendarException
  673. */
  674. public function createCalendar($principalUri, $calendarUri, array $properties) {
  675. if (strlen($calendarUri) > 255) {
  676. throw new CalendarException('URI too long. Calendar not created');
  677. }
  678. $values = [
  679. 'principaluri' => $this->convertPrincipal($principalUri, true),
  680. 'uri' => $calendarUri,
  681. 'synctoken' => 1,
  682. 'transparent' => 0,
  683. 'components' => 'VEVENT,VTODO,VJOURNAL',
  684. 'displayname' => $calendarUri
  685. ];
  686. // Default value
  687. $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set';
  688. if (isset($properties[$sccs])) {
  689. if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) {
  690. throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet');
  691. }
  692. $values['components'] = implode(',', $properties[$sccs]->getValue());
  693. } elseif (isset($properties['components'])) {
  694. // Allow to provide components internally without having
  695. // to create a SupportedCalendarComponentSet object
  696. $values['components'] = $properties['components'];
  697. }
  698. $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
  699. if (isset($properties[$transp])) {
  700. $values['transparent'] = (int)($properties[$transp]->getValue() === 'transparent');
  701. }
  702. foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
  703. if (isset($properties[$xmlName])) {
  704. $values[$dbName] = $properties[$xmlName];
  705. }
  706. }
  707. [$calendarId, $calendarData] = $this->atomic(function () use ($values) {
  708. $query = $this->db->getQueryBuilder();
  709. $query->insert('calendars');
  710. foreach ($values as $column => $value) {
  711. $query->setValue($column, $query->createNamedParameter($value));
  712. }
  713. $query->executeStatement();
  714. $calendarId = $query->getLastInsertId();
  715. $calendarData = $this->getCalendarById($calendarId);
  716. return [$calendarId, $calendarData];
  717. }, $this->db);
  718. $this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData));
  719. return $calendarId;
  720. }
  721. /**
  722. * Updates properties for a calendar.
  723. *
  724. * The list of mutations is stored in a Sabre\DAV\PropPatch object.
  725. * To do the actual updates, you must tell this object which properties
  726. * you're going to process with the handle() method.
  727. *
  728. * Calling the handle method is like telling the PropPatch object "I
  729. * promise I can handle updating this property".
  730. *
  731. * Read the PropPatch documentation for more info and examples.
  732. *
  733. * @param mixed $calendarId
  734. * @param PropPatch $propPatch
  735. * @return void
  736. */
  737. public function updateCalendar($calendarId, PropPatch $propPatch) {
  738. $supportedProperties = array_keys($this->propertyMap);
  739. $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp';
  740. $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) {
  741. $newValues = [];
  742. foreach ($mutations as $propertyName => $propertyValue) {
  743. switch ($propertyName) {
  744. case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp':
  745. $fieldName = 'transparent';
  746. $newValues[$fieldName] = (int)($propertyValue->getValue() === 'transparent');
  747. break;
  748. default:
  749. $fieldName = $this->propertyMap[$propertyName][0];
  750. $newValues[$fieldName] = $propertyValue;
  751. break;
  752. }
  753. }
  754. [$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) {
  755. $query = $this->db->getQueryBuilder();
  756. $query->update('calendars');
  757. foreach ($newValues as $fieldName => $value) {
  758. $query->set($fieldName, $query->createNamedParameter($value));
  759. }
  760. $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
  761. $query->executeStatement();
  762. $this->addChanges($calendarId, [''], 2);
  763. $calendarData = $this->getCalendarById($calendarId);
  764. $shares = $this->getShares($calendarId);
  765. return [$calendarData, $shares];
  766. }, $this->db);
  767. $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations));
  768. return true;
  769. });
  770. }
  771. /**
  772. * Delete a calendar and all it's objects
  773. *
  774. * @param mixed $calendarId
  775. * @return void
  776. */
  777. public function deleteCalendar($calendarId, bool $forceDeletePermanently = false) {
  778. $this->atomic(function () use ($calendarId, $forceDeletePermanently): void {
  779. // The calendar is deleted right away if this is either enforced by the caller
  780. // or the special contacts birthday calendar or when the preference of an empty
  781. // retention (0 seconds) is set, which signals a disabled trashbin.
  782. $calendarData = $this->getCalendarById($calendarId);
  783. $isBirthdayCalendar = isset($calendarData['uri']) && $calendarData['uri'] === BirthdayService::BIRTHDAY_CALENDAR_URI;
  784. $trashbinDisabled = $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0';
  785. if ($forceDeletePermanently || $isBirthdayCalendar || $trashbinDisabled) {
  786. $calendarData = $this->getCalendarById($calendarId);
  787. $shares = $this->getShares($calendarId);
  788. $this->purgeCalendarInvitations($calendarId);
  789. $qbDeleteCalendarObjectProps = $this->db->getQueryBuilder();
  790. $qbDeleteCalendarObjectProps->delete($this->dbObjectPropertiesTable)
  791. ->where($qbDeleteCalendarObjectProps->expr()->eq('calendarid', $qbDeleteCalendarObjectProps->createNamedParameter($calendarId)))
  792. ->andWhere($qbDeleteCalendarObjectProps->expr()->eq('calendartype', $qbDeleteCalendarObjectProps->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
  793. ->executeStatement();
  794. $qbDeleteCalendarObjects = $this->db->getQueryBuilder();
  795. $qbDeleteCalendarObjects->delete('calendarobjects')
  796. ->where($qbDeleteCalendarObjects->expr()->eq('calendarid', $qbDeleteCalendarObjects->createNamedParameter($calendarId)))
  797. ->andWhere($qbDeleteCalendarObjects->expr()->eq('calendartype', $qbDeleteCalendarObjects->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
  798. ->executeStatement();
  799. $qbDeleteCalendarChanges = $this->db->getQueryBuilder();
  800. $qbDeleteCalendarChanges->delete('calendarchanges')
  801. ->where($qbDeleteCalendarChanges->expr()->eq('calendarid', $qbDeleteCalendarChanges->createNamedParameter($calendarId)))
  802. ->andWhere($qbDeleteCalendarChanges->expr()->eq('calendartype', $qbDeleteCalendarChanges->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)))
  803. ->executeStatement();
  804. $this->calendarSharingBackend->deleteAllShares($calendarId);
  805. $qbDeleteCalendar = $this->db->getQueryBuilder();
  806. $qbDeleteCalendar->delete('calendars')
  807. ->where($qbDeleteCalendar->expr()->eq('id', $qbDeleteCalendar->createNamedParameter($calendarId)))
  808. ->executeStatement();
  809. // Only dispatch if we actually deleted anything
  810. if ($calendarData) {
  811. $this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares));
  812. }
  813. } else {
  814. $qbMarkCalendarDeleted = $this->db->getQueryBuilder();
  815. $qbMarkCalendarDeleted->update('calendars')
  816. ->set('deleted_at', $qbMarkCalendarDeleted->createNamedParameter(time()))
  817. ->where($qbMarkCalendarDeleted->expr()->eq('id', $qbMarkCalendarDeleted->createNamedParameter($calendarId)))
  818. ->executeStatement();
  819. $calendarData = $this->getCalendarById($calendarId);
  820. $shares = $this->getShares($calendarId);
  821. if ($calendarData) {
  822. $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent(
  823. $calendarId,
  824. $calendarData,
  825. $shares
  826. ));
  827. }
  828. }
  829. }, $this->db);
  830. }
  831. public function restoreCalendar(int $id): void {
  832. $this->atomic(function () use ($id): void {
  833. $qb = $this->db->getQueryBuilder();
  834. $update = $qb->update('calendars')
  835. ->set('deleted_at', $qb->createNamedParameter(null))
  836. ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
  837. $update->executeStatement();
  838. $calendarData = $this->getCalendarById($id);
  839. $shares = $this->getShares($id);
  840. if ($calendarData === null) {
  841. throw new RuntimeException('Calendar data that was just written can\'t be read back. Check your database configuration.');
  842. }
  843. $this->dispatcher->dispatchTyped(new CalendarRestoredEvent(
  844. $id,
  845. $calendarData,
  846. $shares
  847. ));
  848. }, $this->db);
  849. }
  850. /**
  851. * Returns all calendar objects with limited metadata for a calendar
  852. *
  853. * Every item contains an array with the following keys:
  854. * * id - the table row id
  855. * * etag - An arbitrary string
  856. * * uri - a unique key which will be used to construct the uri. This can
  857. * be any arbitrary string.
  858. * * calendardata - The iCalendar-compatible calendar data
  859. *
  860. * @param mixed $calendarId
  861. * @param int $calendarType
  862. * @return array
  863. */
  864. public function getLimitedCalendarObjects(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
  865. $query = $this->db->getQueryBuilder();
  866. $query->select(['id','uid', 'etag', 'uri', 'calendardata'])
  867. ->from('calendarobjects')
  868. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
  869. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
  870. ->andWhere($query->expr()->isNull('deleted_at'));
  871. $stmt = $query->executeQuery();
  872. $result = [];
  873. while (($row = $stmt->fetch()) !== false) {
  874. $result[$row['uid']] = [
  875. 'id' => $row['id'],
  876. 'etag' => $row['etag'],
  877. 'uri' => $row['uri'],
  878. 'calendardata' => $row['calendardata'],
  879. ];
  880. }
  881. $stmt->closeCursor();
  882. return $result;
  883. }
  884. /**
  885. * Delete all of an user's shares
  886. *
  887. * @param string $principaluri
  888. * @return void
  889. */
  890. public function deleteAllSharesByUser($principaluri) {
  891. $this->calendarSharingBackend->deleteAllSharesByUser($principaluri);
  892. }
  893. /**
  894. * Returns all calendar objects within a calendar.
  895. *
  896. * Every item contains an array with the following keys:
  897. * * calendardata - The iCalendar-compatible calendar data
  898. * * uri - a unique key which will be used to construct the uri. This can
  899. * be any arbitrary string, but making sure it ends with '.ics' is a
  900. * good idea. This is only the basename, or filename, not the full
  901. * path.
  902. * * lastmodified - a timestamp of the last modification time
  903. * * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
  904. * '"abcdef"')
  905. * * size - The size of the calendar objects, in bytes.
  906. * * component - optional, a string containing the type of object, such
  907. * as 'vevent' or 'vtodo'. If specified, this will be used to populate
  908. * the Content-Type header.
  909. *
  910. * Note that the etag is optional, but it's highly encouraged to return for
  911. * speed reasons.
  912. *
  913. * The calendardata is also optional. If it's not returned
  914. * 'getCalendarObject' will be called later, which *is* expected to return
  915. * calendardata.
  916. *
  917. * If neither etag or size are specified, the calendardata will be
  918. * used/fetched to determine these numbers. If both are specified the
  919. * amount of times this is needed is reduced by a great degree.
  920. *
  921. * @param mixed $calendarId
  922. * @param int $calendarType
  923. * @return array
  924. */
  925. public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
  926. $query = $this->db->getQueryBuilder();
  927. $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification'])
  928. ->from('calendarobjects')
  929. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
  930. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
  931. ->andWhere($query->expr()->isNull('deleted_at'));
  932. $stmt = $query->executeQuery();
  933. $result = [];
  934. while (($row = $stmt->fetch()) !== false) {
  935. $result[] = [
  936. 'id' => $row['id'],
  937. 'uri' => $row['uri'],
  938. 'lastmodified' => $row['lastmodified'],
  939. 'etag' => '"' . $row['etag'] . '"',
  940. 'calendarid' => $row['calendarid'],
  941. 'size' => (int)$row['size'],
  942. 'component' => strtolower($row['componenttype']),
  943. 'classification' => (int)$row['classification']
  944. ];
  945. }
  946. $stmt->closeCursor();
  947. return $result;
  948. }
  949. public function getDeletedCalendarObjects(int $deletedBefore): array {
  950. $query = $this->db->getQueryBuilder();
  951. $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.calendartype', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
  952. ->from('calendarobjects', 'co')
  953. ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
  954. ->where($query->expr()->isNotNull('co.deleted_at'))
  955. ->andWhere($query->expr()->lt('co.deleted_at', $query->createNamedParameter($deletedBefore)));
  956. $stmt = $query->executeQuery();
  957. $result = [];
  958. while (($row = $stmt->fetch()) !== false) {
  959. $result[] = [
  960. 'id' => $row['id'],
  961. 'uri' => $row['uri'],
  962. 'lastmodified' => $row['lastmodified'],
  963. 'etag' => '"' . $row['etag'] . '"',
  964. 'calendarid' => (int)$row['calendarid'],
  965. 'calendartype' => (int)$row['calendartype'],
  966. 'size' => (int)$row['size'],
  967. 'component' => strtolower($row['componenttype']),
  968. 'classification' => (int)$row['classification'],
  969. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
  970. ];
  971. }
  972. $stmt->closeCursor();
  973. return $result;
  974. }
  975. /**
  976. * Return all deleted calendar objects by the given principal that are not
  977. * in deleted calendars.
  978. *
  979. * @param string $principalUri
  980. * @return array
  981. * @throws Exception
  982. */
  983. public function getDeletedCalendarObjectsByPrincipal(string $principalUri): array {
  984. $query = $this->db->getQueryBuilder();
  985. $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.componenttype', 'co.classification', 'co.deleted_at'])
  986. ->selectAlias('c.uri', 'calendaruri')
  987. ->from('calendarobjects', 'co')
  988. ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
  989. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
  990. ->andWhere($query->expr()->isNotNull('co.deleted_at'))
  991. ->andWhere($query->expr()->isNull('c.deleted_at'));
  992. $stmt = $query->executeQuery();
  993. $result = [];
  994. while ($row = $stmt->fetch()) {
  995. $result[] = [
  996. 'id' => $row['id'],
  997. 'uri' => $row['uri'],
  998. 'lastmodified' => $row['lastmodified'],
  999. 'etag' => '"' . $row['etag'] . '"',
  1000. 'calendarid' => $row['calendarid'],
  1001. 'calendaruri' => $row['calendaruri'],
  1002. 'size' => (int)$row['size'],
  1003. 'component' => strtolower($row['componenttype']),
  1004. 'classification' => (int)$row['classification'],
  1005. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
  1006. ];
  1007. }
  1008. $stmt->closeCursor();
  1009. return $result;
  1010. }
  1011. /**
  1012. * Returns information from a single calendar object, based on it's object
  1013. * uri.
  1014. *
  1015. * The object uri is only the basename, or filename and not a full path.
  1016. *
  1017. * The returned array must have the same keys as getCalendarObjects. The
  1018. * 'calendardata' object is required here though, while it's not required
  1019. * for getCalendarObjects.
  1020. *
  1021. * This method must return null if the object did not exist.
  1022. *
  1023. * @param mixed $calendarId
  1024. * @param string $objectUri
  1025. * @param int $calendarType
  1026. * @return array|null
  1027. */
  1028. public function getCalendarObject($calendarId, $objectUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR) {
  1029. $key = $calendarId . '::' . $objectUri . '::' . $calendarType;
  1030. if (isset($this->cachedObjects[$key])) {
  1031. return $this->cachedObjects[$key];
  1032. }
  1033. $query = $this->db->getQueryBuilder();
  1034. $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
  1035. ->from('calendarobjects')
  1036. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
  1037. ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
  1038. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
  1039. $stmt = $query->executeQuery();
  1040. $row = $stmt->fetch();
  1041. $stmt->closeCursor();
  1042. if (!$row) {
  1043. return null;
  1044. }
  1045. $object = $this->rowToCalendarObject($row);
  1046. $this->cachedObjects[$key] = $object;
  1047. return $object;
  1048. }
  1049. private function rowToCalendarObject(array $row): array {
  1050. return [
  1051. 'id' => $row['id'],
  1052. 'uri' => $row['uri'],
  1053. 'uid' => $row['uid'],
  1054. 'lastmodified' => $row['lastmodified'],
  1055. 'etag' => '"' . $row['etag'] . '"',
  1056. 'calendarid' => $row['calendarid'],
  1057. 'size' => (int)$row['size'],
  1058. 'calendardata' => $this->readBlob($row['calendardata']),
  1059. 'component' => strtolower($row['componenttype']),
  1060. 'classification' => (int)$row['classification'],
  1061. '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => $row['deleted_at'] === null ? $row['deleted_at'] : (int)$row['deleted_at'],
  1062. ];
  1063. }
  1064. /**
  1065. * Returns a list of calendar objects.
  1066. *
  1067. * This method should work identical to getCalendarObject, but instead
  1068. * return all the calendar objects in the list as an array.
  1069. *
  1070. * If the backend supports this, it may allow for some speed-ups.
  1071. *
  1072. * @param mixed $calendarId
  1073. * @param string[] $uris
  1074. * @param int $calendarType
  1075. * @return array
  1076. */
  1077. public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
  1078. if (empty($uris)) {
  1079. return [];
  1080. }
  1081. $chunks = array_chunk($uris, 100);
  1082. $objects = [];
  1083. $query = $this->db->getQueryBuilder();
  1084. $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification'])
  1085. ->from('calendarobjects')
  1086. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
  1087. ->andWhere($query->expr()->in('uri', $query->createParameter('uri')))
  1088. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
  1089. ->andWhere($query->expr()->isNull('deleted_at'));
  1090. foreach ($chunks as $uris) {
  1091. $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY);
  1092. $result = $query->executeQuery();
  1093. while ($row = $result->fetch()) {
  1094. $objects[] = [
  1095. 'id' => $row['id'],
  1096. 'uri' => $row['uri'],
  1097. 'lastmodified' => $row['lastmodified'],
  1098. 'etag' => '"' . $row['etag'] . '"',
  1099. 'calendarid' => $row['calendarid'],
  1100. 'size' => (int)$row['size'],
  1101. 'calendardata' => $this->readBlob($row['calendardata']),
  1102. 'component' => strtolower($row['componenttype']),
  1103. 'classification' => (int)$row['classification']
  1104. ];
  1105. }
  1106. $result->closeCursor();
  1107. }
  1108. return $objects;
  1109. }
  1110. /**
  1111. * Creates a new calendar object.
  1112. *
  1113. * The object uri is only the basename, or filename and not a full path.
  1114. *
  1115. * It is possible return an etag from this function, which will be used in
  1116. * the response to this PUT request. Note that the ETag must be surrounded
  1117. * by double-quotes.
  1118. *
  1119. * However, you should only really return this ETag if you don't mangle the
  1120. * calendar-data. If the result of a subsequent GET to this object is not
  1121. * the exact same as this request body, you should omit the ETag.
  1122. *
  1123. * @param mixed $calendarId
  1124. * @param string $objectUri
  1125. * @param string $calendarData
  1126. * @param int $calendarType
  1127. * @return string
  1128. */
  1129. public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
  1130. $this->cachedObjects = [];
  1131. $extraData = $this->getDenormalizedData($calendarData);
  1132. return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
  1133. // Try to detect duplicates
  1134. $qb = $this->db->getQueryBuilder();
  1135. $qb->select($qb->func()->count('*'))
  1136. ->from('calendarobjects')
  1137. ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
  1138. ->andWhere($qb->expr()->eq('uid', $qb->createNamedParameter($extraData['uid'])))
  1139. ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
  1140. ->andWhere($qb->expr()->isNull('deleted_at'));
  1141. $result = $qb->executeQuery();
  1142. $count = (int)$result->fetchOne();
  1143. $result->closeCursor();
  1144. if ($count !== 0) {
  1145. throw new BadRequest('Calendar object with uid already exists in this calendar collection.');
  1146. }
  1147. // For a more specific error message we also try to explicitly look up the UID but as a deleted entry
  1148. $qbDel = $this->db->getQueryBuilder();
  1149. $qbDel->select('*')
  1150. ->from('calendarobjects')
  1151. ->where($qbDel->expr()->eq('calendarid', $qbDel->createNamedParameter($calendarId)))
  1152. ->andWhere($qbDel->expr()->eq('uid', $qbDel->createNamedParameter($extraData['uid'])))
  1153. ->andWhere($qbDel->expr()->eq('calendartype', $qbDel->createNamedParameter($calendarType)))
  1154. ->andWhere($qbDel->expr()->isNotNull('deleted_at'));
  1155. $result = $qbDel->executeQuery();
  1156. $found = $result->fetch();
  1157. $result->closeCursor();
  1158. if ($found !== false) {
  1159. // the object existed previously but has been deleted
  1160. // remove the trashbin entry and continue as if it was a new object
  1161. $this->deleteCalendarObject($calendarId, $found['uri']);
  1162. }
  1163. $query = $this->db->getQueryBuilder();
  1164. $query->insert('calendarobjects')
  1165. ->values([
  1166. 'calendarid' => $query->createNamedParameter($calendarId),
  1167. 'uri' => $query->createNamedParameter($objectUri),
  1168. 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB),
  1169. 'lastmodified' => $query->createNamedParameter(time()),
  1170. 'etag' => $query->createNamedParameter($extraData['etag']),
  1171. 'size' => $query->createNamedParameter($extraData['size']),
  1172. 'componenttype' => $query->createNamedParameter($extraData['componentType']),
  1173. 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']),
  1174. 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']),
  1175. 'classification' => $query->createNamedParameter($extraData['classification']),
  1176. 'uid' => $query->createNamedParameter($extraData['uid']),
  1177. 'calendartype' => $query->createNamedParameter($calendarType),
  1178. ])
  1179. ->executeStatement();
  1180. $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
  1181. $this->addChanges($calendarId, [$objectUri], 1, $calendarType);
  1182. $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
  1183. assert($objectRow !== null);
  1184. if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
  1185. $calendarRow = $this->getCalendarById($calendarId);
  1186. $shares = $this->getShares($calendarId);
  1187. $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow));
  1188. } else {
  1189. $subscriptionRow = $this->getSubscriptionById($calendarId);
  1190. $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow));
  1191. }
  1192. return '"' . $extraData['etag'] . '"';
  1193. }, $this->db);
  1194. }
  1195. /**
  1196. * Updates an existing calendarobject, based on it's uri.
  1197. *
  1198. * The object uri is only the basename, or filename and not a full path.
  1199. *
  1200. * It is possible return an etag from this function, which will be used in
  1201. * the response to this PUT request. Note that the ETag must be surrounded
  1202. * by double-quotes.
  1203. *
  1204. * However, you should only really return this ETag if you don't mangle the
  1205. * calendar-data. If the result of a subsequent GET to this object is not
  1206. * the exact same as this request body, you should omit the ETag.
  1207. *
  1208. * @param mixed $calendarId
  1209. * @param string $objectUri
  1210. * @param string $calendarData
  1211. * @param int $calendarType
  1212. * @return string
  1213. */
  1214. public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
  1215. $this->cachedObjects = [];
  1216. $extraData = $this->getDenormalizedData($calendarData);
  1217. return $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $extraData, $calendarType) {
  1218. $query = $this->db->getQueryBuilder();
  1219. $query->update('calendarobjects')
  1220. ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB))
  1221. ->set('lastmodified', $query->createNamedParameter(time()))
  1222. ->set('etag', $query->createNamedParameter($extraData['etag']))
  1223. ->set('size', $query->createNamedParameter($extraData['size']))
  1224. ->set('componenttype', $query->createNamedParameter($extraData['componentType']))
  1225. ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence']))
  1226. ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence']))
  1227. ->set('classification', $query->createNamedParameter($extraData['classification']))
  1228. ->set('uid', $query->createNamedParameter($extraData['uid']))
  1229. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
  1230. ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
  1231. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
  1232. ->executeStatement();
  1233. $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType);
  1234. $this->addChanges($calendarId, [$objectUri], 2, $calendarType);
  1235. $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
  1236. if (is_array($objectRow)) {
  1237. if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
  1238. $calendarRow = $this->getCalendarById($calendarId);
  1239. $shares = $this->getShares($calendarId);
  1240. $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow));
  1241. } else {
  1242. $subscriptionRow = $this->getSubscriptionById($calendarId);
  1243. $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow));
  1244. }
  1245. }
  1246. return '"' . $extraData['etag'] . '"';
  1247. }, $this->db);
  1248. }
  1249. /**
  1250. * Moves a calendar object from calendar to calendar.
  1251. *
  1252. * @param int $sourceCalendarId
  1253. * @param int $targetCalendarId
  1254. * @param int $objectId
  1255. * @param string $oldPrincipalUri
  1256. * @param string $newPrincipalUri
  1257. * @param int $calendarType
  1258. * @return bool
  1259. * @throws Exception
  1260. */
  1261. public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool {
  1262. $this->cachedObjects = [];
  1263. return $this->atomic(function () use ($sourceCalendarId, $targetCalendarId, $objectId, $oldPrincipalUri, $newPrincipalUri, $calendarType) {
  1264. $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId);
  1265. if (empty($object)) {
  1266. return false;
  1267. }
  1268. $query = $this->db->getQueryBuilder();
  1269. $query->update('calendarobjects')
  1270. ->set('calendarid', $query->createNamedParameter($targetCalendarId, IQueryBuilder::PARAM_INT))
  1271. ->where($query->expr()->eq('id', $query->createNamedParameter($objectId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
  1272. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT))
  1273. ->executeStatement();
  1274. $this->purgeProperties($sourceCalendarId, $objectId);
  1275. $this->updateProperties($targetCalendarId, $object['uri'], $object['calendardata'], $calendarType);
  1276. $this->addChanges($sourceCalendarId, [$object['uri']], 3, $calendarType);
  1277. $this->addChanges($targetCalendarId, [$object['uri']], 1, $calendarType);
  1278. $object = $this->getCalendarObjectById($newPrincipalUri, $objectId);
  1279. // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client
  1280. if (empty($object)) {
  1281. return false;
  1282. }
  1283. $targetCalendarRow = $this->getCalendarById($targetCalendarId);
  1284. // the calendar this event is being moved to does not exist any longer
  1285. if (empty($targetCalendarRow)) {
  1286. return false;
  1287. }
  1288. if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
  1289. $sourceShares = $this->getShares($sourceCalendarId);
  1290. $targetShares = $this->getShares($targetCalendarId);
  1291. $sourceCalendarRow = $this->getCalendarById($sourceCalendarId);
  1292. $this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object));
  1293. }
  1294. return true;
  1295. }, $this->db);
  1296. }
  1297. /**
  1298. * @param int $calendarObjectId
  1299. * @param int $classification
  1300. */
  1301. public function setClassification($calendarObjectId, $classification) {
  1302. $this->cachedObjects = [];
  1303. if (!in_array($classification, [
  1304. self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL
  1305. ])) {
  1306. throw new \InvalidArgumentException();
  1307. }
  1308. $query = $this->db->getQueryBuilder();
  1309. $query->update('calendarobjects')
  1310. ->set('classification', $query->createNamedParameter($classification))
  1311. ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId)))
  1312. ->executeStatement();
  1313. }
  1314. /**
  1315. * Deletes an existing calendar object.
  1316. *
  1317. * The object uri is only the basename, or filename and not a full path.
  1318. *
  1319. * @param mixed $calendarId
  1320. * @param string $objectUri
  1321. * @param int $calendarType
  1322. * @param bool $forceDeletePermanently
  1323. * @return void
  1324. */
  1325. public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR, bool $forceDeletePermanently = false) {
  1326. $this->cachedObjects = [];
  1327. $this->atomic(function () use ($calendarId, $objectUri, $calendarType, $forceDeletePermanently): void {
  1328. $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType);
  1329. if ($data === null) {
  1330. // Nothing to delete
  1331. return;
  1332. }
  1333. if ($forceDeletePermanently || $this->config->getAppValue(Application::APP_ID, RetentionService::RETENTION_CONFIG_KEY) === '0') {
  1334. $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?');
  1335. $stmt->execute([$calendarId, $objectUri, $calendarType]);
  1336. $this->purgeProperties($calendarId, $data['id']);
  1337. $this->purgeObjectInvitations($data['uid']);
  1338. if ($calendarType === self::CALENDAR_TYPE_CALENDAR) {
  1339. $calendarRow = $this->getCalendarById($calendarId);
  1340. $shares = $this->getShares($calendarId);
  1341. $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data));
  1342. } else {
  1343. $subscriptionRow = $this->getSubscriptionById($calendarId);
  1344. $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data));
  1345. }
  1346. } else {
  1347. $pathInfo = pathinfo($data['uri']);
  1348. if (!empty($pathInfo['extension'])) {
  1349. // Append a suffix to "free" the old URI for recreation
  1350. $newUri = sprintf(
  1351. '%s-deleted.%s',
  1352. $pathInfo['filename'],
  1353. $pathInfo['extension']
  1354. );
  1355. } else {
  1356. $newUri = sprintf(
  1357. '%s-deleted',
  1358. $pathInfo['filename']
  1359. );
  1360. }
  1361. // Try to detect conflicts before the DB does
  1362. // As unlikely as it seems, this can happen when the user imports, then deletes, imports and deletes again
  1363. $newObject = $this->getCalendarObject($calendarId, $newUri, $calendarType);
  1364. if ($newObject !== null) {
  1365. throw new Forbidden("A calendar object with URI $newUri already exists in calendar $calendarId, therefore this object can't be moved into the trashbin");
  1366. }
  1367. $qb = $this->db->getQueryBuilder();
  1368. $markObjectDeletedQuery = $qb->update('calendarobjects')
  1369. ->set('deleted_at', $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT))
  1370. ->set('uri', $qb->createNamedParameter($newUri))
  1371. ->where(
  1372. $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)),
  1373. $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
  1374. $qb->expr()->eq('uri', $qb->createNamedParameter($objectUri))
  1375. );
  1376. $markObjectDeletedQuery->executeStatement();
  1377. $calendarData = $this->getCalendarById($calendarId);
  1378. if ($calendarData !== null) {
  1379. $this->dispatcher->dispatchTyped(
  1380. new CalendarObjectMovedToTrashEvent(
  1381. $calendarId,
  1382. $calendarData,
  1383. $this->getShares($calendarId),
  1384. $data
  1385. )
  1386. );
  1387. }
  1388. }
  1389. $this->addChanges($calendarId, [$objectUri], 3, $calendarType);
  1390. }, $this->db);
  1391. }
  1392. /**
  1393. * @param mixed $objectData
  1394. *
  1395. * @throws Forbidden
  1396. */
  1397. public function restoreCalendarObject(array $objectData): void {
  1398. $this->cachedObjects = [];
  1399. $this->atomic(function () use ($objectData): void {
  1400. $id = (int)$objectData['id'];
  1401. $restoreUri = str_replace('-deleted.ics', '.ics', $objectData['uri']);
  1402. $targetObject = $this->getCalendarObject(
  1403. $objectData['calendarid'],
  1404. $restoreUri
  1405. );
  1406. if ($targetObject !== null) {
  1407. throw new Forbidden("Can not restore calendar $id because a calendar object with the URI $restoreUri already exists");
  1408. }
  1409. $qb = $this->db->getQueryBuilder();
  1410. $update = $qb->update('calendarobjects')
  1411. ->set('uri', $qb->createNamedParameter($restoreUri))
  1412. ->set('deleted_at', $qb->createNamedParameter(null))
  1413. ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
  1414. $update->executeStatement();
  1415. // Make sure this change is tracked in the changes table
  1416. $qb2 = $this->db->getQueryBuilder();
  1417. $selectObject = $qb2->select('calendardata', 'uri', 'calendarid', 'calendartype')
  1418. ->selectAlias('componenttype', 'component')
  1419. ->from('calendarobjects')
  1420. ->where($qb2->expr()->eq('id', $qb2->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
  1421. $result = $selectObject->executeQuery();
  1422. $row = $result->fetch();
  1423. $result->closeCursor();
  1424. if ($row === false) {
  1425. // Welp, this should possibly not have happened, but let's ignore
  1426. return;
  1427. }
  1428. $this->addChanges($row['calendarid'], [$row['uri']], 1, (int)$row['calendartype']);
  1429. $calendarRow = $this->getCalendarById((int)$row['calendarid']);
  1430. if ($calendarRow === null) {
  1431. throw new RuntimeException('Calendar object data that was just written can\'t be read back. Check your database configuration.');
  1432. }
  1433. $this->dispatcher->dispatchTyped(
  1434. new CalendarObjectRestoredEvent(
  1435. (int)$objectData['calendarid'],
  1436. $calendarRow,
  1437. $this->getShares((int)$row['calendarid']),
  1438. $row
  1439. )
  1440. );
  1441. }, $this->db);
  1442. }
  1443. /**
  1444. * Performs a calendar-query on the contents of this calendar.
  1445. *
  1446. * The calendar-query is defined in RFC4791 : CalDAV. Using the
  1447. * calendar-query it is possible for a client to request a specific set of
  1448. * object, based on contents of iCalendar properties, date-ranges and
  1449. * iCalendar component types (VTODO, VEVENT).
  1450. *
  1451. * This method should just return a list of (relative) urls that match this
  1452. * query.
  1453. *
  1454. * The list of filters are specified as an array. The exact array is
  1455. * documented by Sabre\CalDAV\CalendarQueryParser.
  1456. *
  1457. * Note that it is extremely likely that getCalendarObject for every path
  1458. * returned from this method will be called almost immediately after. You
  1459. * may want to anticipate this to speed up these requests.
  1460. *
  1461. * This method provides a default implementation, which parses *all* the
  1462. * iCalendar objects in the specified calendar.
  1463. *
  1464. * This default may well be good enough for personal use, and calendars
  1465. * that aren't very large. But if you anticipate high usage, big calendars
  1466. * or high loads, you are strongly advised to optimize certain paths.
  1467. *
  1468. * The best way to do so is override this method and to optimize
  1469. * specifically for 'common filters'.
  1470. *
  1471. * Requests that are extremely common are:
  1472. * * requests for just VEVENTS
  1473. * * requests for just VTODO
  1474. * * requests with a time-range-filter on either VEVENT or VTODO.
  1475. *
  1476. * ..and combinations of these requests. It may not be worth it to try to
  1477. * handle every possible situation and just rely on the (relatively
  1478. * easy to use) CalendarQueryValidator to handle the rest.
  1479. *
  1480. * Note that especially time-range-filters may be difficult to parse. A
  1481. * time-range filter specified on a VEVENT must for instance also handle
  1482. * recurrence rules correctly.
  1483. * A good example of how to interpret all these filters can also simply
  1484. * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
  1485. * as possible, so it gives you a good idea on what type of stuff you need
  1486. * to think of.
  1487. *
  1488. * @param mixed $calendarId
  1489. * @param array $filters
  1490. * @param int $calendarType
  1491. * @return array
  1492. */
  1493. public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array {
  1494. $componentType = null;
  1495. $requirePostFilter = true;
  1496. $timeRange = null;
  1497. // if no filters were specified, we don't need to filter after a query
  1498. if (!$filters['prop-filters'] && !$filters['comp-filters']) {
  1499. $requirePostFilter = false;
  1500. }
  1501. // Figuring out if there's a component filter
  1502. if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) {
  1503. $componentType = $filters['comp-filters'][0]['name'];
  1504. // Checking if we need post-filters
  1505. if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) {
  1506. $requirePostFilter = false;
  1507. }
  1508. // There was a time-range filter
  1509. if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) {
  1510. $timeRange = $filters['comp-filters'][0]['time-range'];
  1511. // If start time OR the end time is not specified, we can do a
  1512. // 100% accurate mysql query.
  1513. if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) {
  1514. $requirePostFilter = false;
  1515. }
  1516. }
  1517. }
  1518. $query = $this->db->getQueryBuilder();
  1519. $query->select(['id', 'uri', 'uid', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification', 'deleted_at'])
  1520. ->from('calendarobjects')
  1521. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
  1522. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)))
  1523. ->andWhere($query->expr()->isNull('deleted_at'));
  1524. if ($componentType) {
  1525. $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType)));
  1526. }
  1527. if ($timeRange && $timeRange['start']) {
  1528. $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp())));
  1529. }
  1530. if ($timeRange && $timeRange['end']) {
  1531. $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp())));
  1532. }
  1533. $stmt = $query->executeQuery();
  1534. $result = [];
  1535. while ($row = $stmt->fetch()) {
  1536. // if we leave it as a blob we can't read it both from the post filter and the rowToCalendarObject
  1537. if (isset($row['calendardata'])) {
  1538. $row['calendardata'] = $this->readBlob($row['calendardata']);
  1539. }
  1540. if ($requirePostFilter) {
  1541. // validateFilterForObject will parse the calendar data
  1542. // catch parsing errors
  1543. try {
  1544. $matches = $this->validateFilterForObject($row, $filters);
  1545. } catch (ParseException $ex) {
  1546. $this->logger->error('Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
  1547. 'app' => 'dav',
  1548. 'exception' => $ex,
  1549. ]);
  1550. continue;
  1551. } catch (InvalidDataException $ex) {
  1552. $this->logger->error('Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:' . $calendarId . ' uri:' . $row['uri'], [
  1553. 'app' => 'dav',
  1554. 'exception' => $ex,
  1555. ]);
  1556. continue;
  1557. } catch (MaxInstancesExceededException $ex) {
  1558. $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
  1559. 'app' => 'dav',
  1560. 'exception' => $ex,
  1561. ]);
  1562. continue;
  1563. }
  1564. if (!$matches) {
  1565. continue;
  1566. }
  1567. }
  1568. $result[] = $row['uri'];
  1569. $key = $calendarId . '::' . $row['uri'] . '::' . $calendarType;
  1570. $this->cachedObjects[$key] = $this->rowToCalendarObject($row);
  1571. }
  1572. return $result;
  1573. }
  1574. /**
  1575. * custom Nextcloud search extension for CalDAV
  1576. *
  1577. * TODO - this should optionally cover cached calendar objects as well
  1578. *
  1579. * @param string $principalUri
  1580. * @param array $filters
  1581. * @param integer|null $limit
  1582. * @param integer|null $offset
  1583. * @return array
  1584. */
  1585. public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) {
  1586. return $this->atomic(function () use ($principalUri, $filters, $limit, $offset) {
  1587. $calendars = $this->getCalendarsForUser($principalUri);
  1588. $ownCalendars = [];
  1589. $sharedCalendars = [];
  1590. $uriMapper = [];
  1591. foreach ($calendars as $calendar) {
  1592. if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) {
  1593. $ownCalendars[] = $calendar['id'];
  1594. } else {
  1595. $sharedCalendars[] = $calendar['id'];
  1596. }
  1597. $uriMapper[$calendar['id']] = $calendar['uri'];
  1598. }
  1599. if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) {
  1600. return [];
  1601. }
  1602. $query = $this->db->getQueryBuilder();
  1603. // Calendar id expressions
  1604. $calendarExpressions = [];
  1605. foreach ($ownCalendars as $id) {
  1606. $calendarExpressions[] = $query->expr()->andX(
  1607. $query->expr()->eq('c.calendarid',
  1608. $query->createNamedParameter($id)),
  1609. $query->expr()->eq('c.calendartype',
  1610. $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
  1611. }
  1612. foreach ($sharedCalendars as $id) {
  1613. $calendarExpressions[] = $query->expr()->andX(
  1614. $query->expr()->eq('c.calendarid',
  1615. $query->createNamedParameter($id)),
  1616. $query->expr()->eq('c.classification',
  1617. $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)),
  1618. $query->expr()->eq('c.calendartype',
  1619. $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)));
  1620. }
  1621. if (count($calendarExpressions) === 1) {
  1622. $calExpr = $calendarExpressions[0];
  1623. } else {
  1624. $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions);
  1625. }
  1626. // Component expressions
  1627. $compExpressions = [];
  1628. foreach ($filters['comps'] as $comp) {
  1629. $compExpressions[] = $query->expr()
  1630. ->eq('c.componenttype', $query->createNamedParameter($comp));
  1631. }
  1632. if (count($compExpressions) === 1) {
  1633. $compExpr = $compExpressions[0];
  1634. } else {
  1635. $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions);
  1636. }
  1637. if (!isset($filters['props'])) {
  1638. $filters['props'] = [];
  1639. }
  1640. if (!isset($filters['params'])) {
  1641. $filters['params'] = [];
  1642. }
  1643. $propParamExpressions = [];
  1644. foreach ($filters['props'] as $prop) {
  1645. $propParamExpressions[] = $query->expr()->andX(
  1646. $query->expr()->eq('i.name', $query->createNamedParameter($prop)),
  1647. $query->expr()->isNull('i.parameter')
  1648. );
  1649. }
  1650. foreach ($filters['params'] as $param) {
  1651. $propParamExpressions[] = $query->expr()->andX(
  1652. $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])),
  1653. $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter']))
  1654. );
  1655. }
  1656. if (count($propParamExpressions) === 1) {
  1657. $propParamExpr = $propParamExpressions[0];
  1658. } else {
  1659. $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions);
  1660. }
  1661. $query->select(['c.calendarid', 'c.uri'])
  1662. ->from($this->dbObjectPropertiesTable, 'i')
  1663. ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id'))
  1664. ->where($calExpr)
  1665. ->andWhere($compExpr)
  1666. ->andWhere($propParamExpr)
  1667. ->andWhere($query->expr()->iLike('i.value',
  1668. $query->createNamedParameter('%' . $this->db->escapeLikeParameter($filters['search-term']) . '%')))
  1669. ->andWhere($query->expr()->isNull('deleted_at'));
  1670. if ($offset) {
  1671. $query->setFirstResult($offset);
  1672. }
  1673. if ($limit) {
  1674. $query->setMaxResults($limit);
  1675. }
  1676. $stmt = $query->executeQuery();
  1677. $result = [];
  1678. while ($row = $stmt->fetch()) {
  1679. $path = $uriMapper[$row['calendarid']] . '/' . $row['uri'];
  1680. if (!in_array($path, $result)) {
  1681. $result[] = $path;
  1682. }
  1683. }
  1684. return $result;
  1685. }, $this->db);
  1686. }
  1687. /**
  1688. * used for Nextcloud's calendar API
  1689. *
  1690. * @param array $calendarInfo
  1691. * @param string $pattern
  1692. * @param array $searchProperties
  1693. * @param array $options
  1694. * @param integer|null $limit
  1695. * @param integer|null $offset
  1696. *
  1697. * @return array
  1698. */
  1699. public function search(
  1700. array $calendarInfo,
  1701. $pattern,
  1702. array $searchProperties,
  1703. array $options,
  1704. $limit,
  1705. $offset,
  1706. ) {
  1707. $outerQuery = $this->db->getQueryBuilder();
  1708. $innerQuery = $this->db->getQueryBuilder();
  1709. if (isset($calendarInfo['source'])) {
  1710. $calendarType = self::CALENDAR_TYPE_SUBSCRIPTION;
  1711. } else {
  1712. $calendarType = self::CALENDAR_TYPE_CALENDAR;
  1713. }
  1714. $innerQuery->selectDistinct('op.objectid')
  1715. ->from($this->dbObjectPropertiesTable, 'op')
  1716. ->andWhere($innerQuery->expr()->eq('op.calendarid',
  1717. $outerQuery->createNamedParameter($calendarInfo['id'])))
  1718. ->andWhere($innerQuery->expr()->eq('op.calendartype',
  1719. $outerQuery->createNamedParameter($calendarType)));
  1720. $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri')
  1721. ->from('calendarobjects', 'c')
  1722. ->where($outerQuery->expr()->isNull('deleted_at'));
  1723. // only return public items for shared calendars for now
  1724. if (isset($calendarInfo['{http://owncloud.org/ns}owner-principal']) === false || $calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) {
  1725. $outerQuery->andWhere($outerQuery->expr()->eq('c.classification',
  1726. $outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
  1727. }
  1728. if (!empty($searchProperties)) {
  1729. $or = [];
  1730. foreach ($searchProperties as $searchProperty) {
  1731. $or[] = $innerQuery->expr()->eq('op.name',
  1732. $outerQuery->createNamedParameter($searchProperty));
  1733. }
  1734. $innerQuery->andWhere($innerQuery->expr()->orX(...$or));
  1735. }
  1736. if ($pattern !== '') {
  1737. $innerQuery->andWhere($innerQuery->expr()->iLike('op.value',
  1738. $outerQuery->createNamedParameter('%' .
  1739. $this->db->escapeLikeParameter($pattern) . '%')));
  1740. }
  1741. $start = null;
  1742. $end = null;
  1743. $hasLimit = is_int($limit);
  1744. $hasTimeRange = false;
  1745. if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
  1746. /** @var DateTimeInterface $start */
  1747. $start = $options['timerange']['start'];
  1748. $outerQuery->andWhere(
  1749. $outerQuery->expr()->gt(
  1750. 'lastoccurence',
  1751. $outerQuery->createNamedParameter($start->getTimestamp())
  1752. )
  1753. );
  1754. $hasTimeRange = true;
  1755. }
  1756. if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
  1757. /** @var DateTimeInterface $end */
  1758. $end = $options['timerange']['end'];
  1759. $outerQuery->andWhere(
  1760. $outerQuery->expr()->lt(
  1761. 'firstoccurence',
  1762. $outerQuery->createNamedParameter($end->getTimestamp())
  1763. )
  1764. );
  1765. $hasTimeRange = true;
  1766. }
  1767. if (isset($options['uid'])) {
  1768. $outerQuery->andWhere($outerQuery->expr()->eq('uid', $outerQuery->createNamedParameter($options['uid'])));
  1769. }
  1770. if (!empty($options['types'])) {
  1771. $or = [];
  1772. foreach ($options['types'] as $type) {
  1773. $or[] = $outerQuery->expr()->eq('componenttype',
  1774. $outerQuery->createNamedParameter($type));
  1775. }
  1776. $outerQuery->andWhere($outerQuery->expr()->orX(...$or));
  1777. }
  1778. $outerQuery->andWhere($outerQuery->expr()->in('c.id', $outerQuery->createFunction($innerQuery->getSQL())));
  1779. // Without explicit order by its undefined in which order the SQL server returns the events.
  1780. // For the pagination with hasLimit and hasTimeRange, a stable ordering is helpful.
  1781. $outerQuery->addOrderBy('id');
  1782. $offset = (int)$offset;
  1783. $outerQuery->setFirstResult($offset);
  1784. $calendarObjects = [];
  1785. if ($hasLimit && $hasTimeRange) {
  1786. /**
  1787. * Event recurrences are evaluated at runtime because the database only knows the first and last occurrence.
  1788. *
  1789. * Given, a user created 8 events with a yearly reoccurrence and two for events tomorrow.
  1790. * The upcoming event widget asks the CalDAV backend for 7 events within the next 14 days.
  1791. *
  1792. * If limit 7 is applied to the SQL query, we find the 7 events with a yearly reoccurrence
  1793. * and discard the events after evaluating the reoccurrence rules because they are not due within
  1794. * the next 14 days and end up with an empty result even if there are two events to show.
  1795. *
  1796. * The workaround for search requests with a limit and time range is asking for more row than requested
  1797. * and retrying if we have not reached the limit.
  1798. *
  1799. * 25 rows and 3 retries is entirely arbitrary.
  1800. */
  1801. $maxResults = (int)max($limit, 25);
  1802. $outerQuery->setMaxResults($maxResults);
  1803. for ($attempt = $objectsCount = 0; $attempt < 3 && $objectsCount < $limit; $attempt++) {
  1804. $objectsCount = array_push($calendarObjects, ...$this->searchCalendarObjects($outerQuery, $start, $end));
  1805. $outerQuery->setFirstResult($offset += $maxResults);
  1806. }
  1807. $calendarObjects = array_slice($calendarObjects, 0, $limit, false);
  1808. } else {
  1809. $outerQuery->setMaxResults($limit);
  1810. $calendarObjects = $this->searchCalendarObjects($outerQuery, $start, $end);
  1811. }
  1812. $calendarObjects = array_map(function ($o) use ($options) {
  1813. $calendarData = Reader::read($o['calendardata']);
  1814. // Expand recurrences if an explicit time range is requested
  1815. if ($calendarData instanceof VCalendar
  1816. && isset($options['timerange']['start'], $options['timerange']['end'])) {
  1817. $calendarData = $calendarData->expand(
  1818. $options['timerange']['start'],
  1819. $options['timerange']['end'],
  1820. );
  1821. }
  1822. $comps = $calendarData->getComponents();
  1823. $objects = [];
  1824. $timezones = [];
  1825. foreach ($comps as $comp) {
  1826. if ($comp instanceof VTimeZone) {
  1827. $timezones[] = $comp;
  1828. } else {
  1829. $objects[] = $comp;
  1830. }
  1831. }
  1832. return [
  1833. 'id' => $o['id'],
  1834. 'type' => $o['componenttype'],
  1835. 'uid' => $o['uid'],
  1836. 'uri' => $o['uri'],
  1837. 'objects' => array_map(function ($c) {
  1838. return $this->transformSearchData($c);
  1839. }, $objects),
  1840. 'timezones' => array_map(function ($c) {
  1841. return $this->transformSearchData($c);
  1842. }, $timezones),
  1843. ];
  1844. }, $calendarObjects);
  1845. usort($calendarObjects, function (array $a, array $b) {
  1846. /** @var DateTimeImmutable $startA */
  1847. $startA = $a['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
  1848. /** @var DateTimeImmutable $startB */
  1849. $startB = $b['objects'][0]['DTSTART'][0] ?? new DateTimeImmutable(self::MAX_DATE);
  1850. return $startA->getTimestamp() <=> $startB->getTimestamp();
  1851. });
  1852. return $calendarObjects;
  1853. }
  1854. private function searchCalendarObjects(IQueryBuilder $query, ?DateTimeInterface $start, ?DateTimeInterface $end): array {
  1855. $calendarObjects = [];
  1856. $filterByTimeRange = ($start instanceof DateTimeInterface) || ($end instanceof DateTimeInterface);
  1857. $result = $query->executeQuery();
  1858. while (($row = $result->fetch()) !== false) {
  1859. if ($filterByTimeRange === false) {
  1860. // No filter required
  1861. $calendarObjects[] = $row;
  1862. continue;
  1863. }
  1864. try {
  1865. $isValid = $this->validateFilterForObject($row, [
  1866. 'name' => 'VCALENDAR',
  1867. 'comp-filters' => [
  1868. [
  1869. 'name' => 'VEVENT',
  1870. 'comp-filters' => [],
  1871. 'prop-filters' => [],
  1872. 'is-not-defined' => false,
  1873. 'time-range' => [
  1874. 'start' => $start,
  1875. 'end' => $end,
  1876. ],
  1877. ],
  1878. ],
  1879. 'prop-filters' => [],
  1880. 'is-not-defined' => false,
  1881. 'time-range' => null,
  1882. ]);
  1883. } catch (MaxInstancesExceededException $ex) {
  1884. $this->logger->warning('Caught max instances exceeded exception for calendar data. This usually indicates too much recurring (more than 3500) event in calendar data. Object uri: ' . $row['uri'], [
  1885. 'app' => 'dav',
  1886. 'exception' => $ex,
  1887. ]);
  1888. continue;
  1889. }
  1890. if (is_resource($row['calendardata'])) {
  1891. // Put the stream back to the beginning so it can be read another time
  1892. rewind($row['calendardata']);
  1893. }
  1894. if ($isValid) {
  1895. $calendarObjects[] = $row;
  1896. }
  1897. }
  1898. $result->closeCursor();
  1899. return $calendarObjects;
  1900. }
  1901. /**
  1902. * @param Component $comp
  1903. * @return array
  1904. */
  1905. private function transformSearchData(Component $comp) {
  1906. $data = [];
  1907. /** @var Component[] $subComponents */
  1908. $subComponents = $comp->getComponents();
  1909. /** @var Property[] $properties */
  1910. $properties = array_filter($comp->children(), function ($c) {
  1911. return $c instanceof Property;
  1912. });
  1913. $validationRules = $comp->getValidationRules();
  1914. foreach ($subComponents as $subComponent) {
  1915. $name = $subComponent->name;
  1916. if (!isset($data[$name])) {
  1917. $data[$name] = [];
  1918. }
  1919. $data[$name][] = $this->transformSearchData($subComponent);
  1920. }
  1921. foreach ($properties as $property) {
  1922. $name = $property->name;
  1923. if (!isset($validationRules[$name])) {
  1924. $validationRules[$name] = '*';
  1925. }
  1926. $rule = $validationRules[$property->name];
  1927. if ($rule === '+' || $rule === '*') { // multiple
  1928. if (!isset($data[$name])) {
  1929. $data[$name] = [];
  1930. }
  1931. $data[$name][] = $this->transformSearchProperty($property);
  1932. } else { // once
  1933. $data[$name] = $this->transformSearchProperty($property);
  1934. }
  1935. }
  1936. return $data;
  1937. }
  1938. /**
  1939. * @param Property $prop
  1940. * @return array
  1941. */
  1942. private function transformSearchProperty(Property $prop) {
  1943. // No need to check Date, as it extends DateTime
  1944. if ($prop instanceof Property\ICalendar\DateTime) {
  1945. $value = $prop->getDateTime();
  1946. } else {
  1947. $value = $prop->getValue();
  1948. }
  1949. return [
  1950. $value,
  1951. $prop->parameters()
  1952. ];
  1953. }
  1954. /**
  1955. * @param string $principalUri
  1956. * @param string $pattern
  1957. * @param array $componentTypes
  1958. * @param array $searchProperties
  1959. * @param array $searchParameters
  1960. * @param array $options
  1961. * @return array
  1962. */
  1963. public function searchPrincipalUri(string $principalUri,
  1964. string $pattern,
  1965. array $componentTypes,
  1966. array $searchProperties,
  1967. array $searchParameters,
  1968. array $options = [],
  1969. ): array {
  1970. return $this->atomic(function () use ($principalUri, $pattern, $componentTypes, $searchProperties, $searchParameters, $options) {
  1971. $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false;
  1972. $calendarObjectIdQuery = $this->db->getQueryBuilder();
  1973. $calendarOr = [];
  1974. $searchOr = [];
  1975. // Fetch calendars and subscription
  1976. $calendars = $this->getCalendarsForUser($principalUri);
  1977. $subscriptions = $this->getSubscriptionsForUser($principalUri);
  1978. foreach ($calendars as $calendar) {
  1979. $calendarAnd = $calendarObjectIdQuery->expr()->andX(
  1980. $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id'])),
  1981. $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR)),
  1982. );
  1983. // If it's shared, limit search to public events
  1984. if (isset($calendar['{http://owncloud.org/ns}owner-principal'])
  1985. && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) {
  1986. $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
  1987. }
  1988. $calendarOr[] = $calendarAnd;
  1989. }
  1990. foreach ($subscriptions as $subscription) {
  1991. $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(
  1992. $calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id'])),
  1993. $calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)),
  1994. );
  1995. // If it's shared, limit search to public events
  1996. if (isset($subscription['{http://owncloud.org/ns}owner-principal'])
  1997. && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) {
  1998. $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC)));
  1999. }
  2000. $calendarOr[] = $subscriptionAnd;
  2001. }
  2002. foreach ($searchProperties as $property) {
  2003. $propertyAnd = $calendarObjectIdQuery->expr()->andX(
  2004. $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
  2005. $calendarObjectIdQuery->expr()->isNull('cob.parameter'),
  2006. );
  2007. $searchOr[] = $propertyAnd;
  2008. }
  2009. foreach ($searchParameters as $property => $parameter) {
  2010. $parameterAnd = $calendarObjectIdQuery->expr()->andX(
  2011. $calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR)),
  2012. $calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY)),
  2013. );
  2014. $searchOr[] = $parameterAnd;
  2015. }
  2016. if (empty($calendarOr)) {
  2017. return [];
  2018. }
  2019. if (empty($searchOr)) {
  2020. return [];
  2021. }
  2022. $calendarObjectIdQuery->selectDistinct('cob.objectid')
  2023. ->from($this->dbObjectPropertiesTable, 'cob')
  2024. ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid'))
  2025. ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY)))
  2026. ->andWhere($calendarObjectIdQuery->expr()->orX(...$calendarOr))
  2027. ->andWhere($calendarObjectIdQuery->expr()->orX(...$searchOr))
  2028. ->andWhere($calendarObjectIdQuery->expr()->isNull('deleted_at'));
  2029. if ($pattern !== '') {
  2030. if (!$escapePattern) {
  2031. $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern)));
  2032. } else {
  2033. $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%')));
  2034. }
  2035. }
  2036. if (isset($options['limit'])) {
  2037. $calendarObjectIdQuery->setMaxResults($options['limit']);
  2038. }
  2039. if (isset($options['offset'])) {
  2040. $calendarObjectIdQuery->setFirstResult($options['offset']);
  2041. }
  2042. if (isset($options['timerange'])) {
  2043. if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTimeInterface) {
  2044. $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->gt(
  2045. 'lastoccurence',
  2046. $calendarObjectIdQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()),
  2047. ));
  2048. }
  2049. if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTimeInterface) {
  2050. $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->lt(
  2051. 'firstoccurence',
  2052. $calendarObjectIdQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()),
  2053. ));
  2054. }
  2055. }
  2056. $result = $calendarObjectIdQuery->executeQuery();
  2057. $matches = [];
  2058. while (($row = $result->fetch()) !== false) {
  2059. $matches[] = (int)$row['objectid'];
  2060. }
  2061. $result->closeCursor();
  2062. $query = $this->db->getQueryBuilder();
  2063. $query->select('calendardata', 'uri', 'calendarid', 'calendartype')
  2064. ->from('calendarobjects')
  2065. ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY)));
  2066. $result = $query->executeQuery();
  2067. $calendarObjects = [];
  2068. while (($array = $result->fetch()) !== false) {
  2069. $array['calendarid'] = (int)$array['calendarid'];
  2070. $array['calendartype'] = (int)$array['calendartype'];
  2071. $array['calendardata'] = $this->readBlob($array['calendardata']);
  2072. $calendarObjects[] = $array;
  2073. }
  2074. $result->closeCursor();
  2075. return $calendarObjects;
  2076. }, $this->db);
  2077. }
  2078. /**
  2079. * Searches through all of a users calendars and calendar objects to find
  2080. * an object with a specific UID.
  2081. *
  2082. * This method should return the path to this object, relative to the
  2083. * calendar home, so this path usually only contains two parts:
  2084. *
  2085. * calendarpath/objectpath.ics
  2086. *
  2087. * If the uid is not found, return null.
  2088. *
  2089. * This method should only consider * objects that the principal owns, so
  2090. * any calendars owned by other principals that also appear in this
  2091. * collection should be ignored.
  2092. *
  2093. * @param string $principalUri
  2094. * @param string $uid
  2095. * @return string|null
  2096. */
  2097. public function getCalendarObjectByUID($principalUri, $uid) {
  2098. $query = $this->db->getQueryBuilder();
  2099. $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi')
  2100. ->from('calendarobjects', 'co')
  2101. ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id'))
  2102. ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
  2103. ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid)))
  2104. ->andWhere($query->expr()->isNull('co.deleted_at'));
  2105. $stmt = $query->executeQuery();
  2106. $row = $stmt->fetch();
  2107. $stmt->closeCursor();
  2108. if ($row) {
  2109. return $row['calendaruri'] . '/' . $row['objecturi'];
  2110. }
  2111. return null;
  2112. }
  2113. public function getCalendarObjectById(string $principalUri, int $id): ?array {
  2114. $query = $this->db->getQueryBuilder();
  2115. $query->select(['co.id', 'co.uri', 'co.lastmodified', 'co.etag', 'co.calendarid', 'co.size', 'co.calendardata', 'co.componenttype', 'co.classification', 'co.deleted_at'])
  2116. ->selectAlias('c.uri', 'calendaruri')
  2117. ->from('calendarobjects', 'co')
  2118. ->join('co', 'calendars', 'c', $query->expr()->eq('c.id', 'co.calendarid', IQueryBuilder::PARAM_INT))
  2119. ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri)))
  2120. ->andWhere($query->expr()->eq('co.id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
  2121. $stmt = $query->executeQuery();
  2122. $row = $stmt->fetch();
  2123. $stmt->closeCursor();
  2124. if (!$row) {
  2125. return null;
  2126. }
  2127. return [
  2128. 'id' => $row['id'],
  2129. 'uri' => $row['uri'],
  2130. 'lastmodified' => $row['lastmodified'],
  2131. 'etag' => '"' . $row['etag'] . '"',
  2132. 'calendarid' => $row['calendarid'],
  2133. 'calendaruri' => $row['calendaruri'],
  2134. 'size' => (int)$row['size'],
  2135. 'calendardata' => $this->readBlob($row['calendardata']),
  2136. 'component' => strtolower($row['componenttype']),
  2137. 'classification' => (int)$row['classification'],
  2138. 'deleted_at' => isset($row['deleted_at']) ? ((int)$row['deleted_at']) : null,
  2139. ];
  2140. }
  2141. /**
  2142. * The getChanges method returns all the changes that have happened, since
  2143. * the specified syncToken in the specified calendar.
  2144. *
  2145. * This function should return an array, such as the following:
  2146. *
  2147. * [
  2148. * 'syncToken' => 'The current synctoken',
  2149. * 'added' => [
  2150. * 'new.txt',
  2151. * ],
  2152. * 'modified' => [
  2153. * 'modified.txt',
  2154. * ],
  2155. * 'deleted' => [
  2156. * 'foo.php.bak',
  2157. * 'old.txt'
  2158. * ]
  2159. * );
  2160. *
  2161. * The returned syncToken property should reflect the *current* syncToken
  2162. * of the calendar, as reported in the {http://sabredav.org/ns}sync-token
  2163. * property This is * needed here too, to ensure the operation is atomic.
  2164. *
  2165. * If the $syncToken argument is specified as null, this is an initial
  2166. * sync, and all members should be reported.
  2167. *
  2168. * The modified property is an array of nodenames that have changed since
  2169. * the last token.
  2170. *
  2171. * The deleted property is an array with nodenames, that have been deleted
  2172. * from collection.
  2173. *
  2174. * The $syncLevel argument is basically the 'depth' of the report. If it's
  2175. * 1, you only have to report changes that happened only directly in
  2176. * immediate descendants. If it's 2, it should also include changes from
  2177. * the nodes below the child collections. (grandchildren)
  2178. *
  2179. * The $limit argument allows a client to specify how many results should
  2180. * be returned at most. If the limit is not specified, it should be treated
  2181. * as infinite.
  2182. *
  2183. * If the limit (infinite or not) is higher than you're willing to return,
  2184. * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception.
  2185. *
  2186. * If the syncToken is expired (due to data cleanup) or unknown, you must
  2187. * return null.
  2188. *
  2189. * The limit is 'suggestive'. You are free to ignore it.
  2190. *
  2191. * @param string $calendarId
  2192. * @param string $syncToken
  2193. * @param int $syncLevel
  2194. * @param int|null $limit
  2195. * @param int $calendarType
  2196. * @return ?array
  2197. */
  2198. public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
  2199. $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
  2200. return $this->atomic(function () use ($calendarId, $syncToken, $syncLevel, $limit, $calendarType, $table) {
  2201. // Current synctoken
  2202. $qb = $this->db->getQueryBuilder();
  2203. $qb->select('synctoken')
  2204. ->from($table)
  2205. ->where(
  2206. $qb->expr()->eq('id', $qb->createNamedParameter($calendarId))
  2207. );
  2208. $stmt = $qb->executeQuery();
  2209. $currentToken = $stmt->fetchOne();
  2210. $initialSync = !is_numeric($syncToken);
  2211. if ($currentToken === false) {
  2212. return null;
  2213. }
  2214. // evaluate if this is a initial sync and construct appropriate command
  2215. if ($initialSync) {
  2216. $qb = $this->db->getQueryBuilder();
  2217. $qb->select('uri')
  2218. ->from('calendarobjects')
  2219. ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
  2220. ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
  2221. ->andWhere($qb->expr()->isNull('deleted_at'));
  2222. } else {
  2223. $qb = $this->db->getQueryBuilder();
  2224. $qb->select('uri', $qb->func()->max('operation'))
  2225. ->from('calendarchanges')
  2226. ->where($qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)))
  2227. ->andWhere($qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)))
  2228. ->andWhere($qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)))
  2229. ->andWhere($qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)))
  2230. ->groupBy('uri');
  2231. }
  2232. // evaluate if limit exists
  2233. if (is_numeric($limit)) {
  2234. $qb->setMaxResults($limit);
  2235. }
  2236. // execute command
  2237. $stmt = $qb->executeQuery();
  2238. // build results
  2239. $result = ['syncToken' => $currentToken, 'added' => [], 'modified' => [], 'deleted' => []];
  2240. // retrieve results
  2241. if ($initialSync) {
  2242. $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN);
  2243. } else {
  2244. // \PDO::FETCH_NUM is needed due to the inconsistent field names
  2245. // produced by doctrine for MAX() with different databases
  2246. while ($entry = $stmt->fetch(\PDO::FETCH_NUM)) {
  2247. // assign uri (column 0) to appropriate mutation based on operation (column 1)
  2248. // forced (int) is needed as doctrine with OCI returns the operation field as string not integer
  2249. match ((int)$entry[1]) {
  2250. 1 => $result['added'][] = $entry[0],
  2251. 2 => $result['modified'][] = $entry[0],
  2252. 3 => $result['deleted'][] = $entry[0],
  2253. default => $this->logger->debug('Unknown calendar change operation detected')
  2254. };
  2255. }
  2256. }
  2257. $stmt->closeCursor();
  2258. return $result;
  2259. }, $this->db);
  2260. }
  2261. /**
  2262. * Returns a list of subscriptions for a principal.
  2263. *
  2264. * Every subscription is an array with the following keys:
  2265. * * id, a unique id that will be used by other functions to modify the
  2266. * subscription. This can be the same as the uri or a database key.
  2267. * * uri. This is just the 'base uri' or 'filename' of the subscription.
  2268. * * principaluri. The owner of the subscription. Almost always the same as
  2269. * principalUri passed to this method.
  2270. *
  2271. * Furthermore, all the subscription info must be returned too:
  2272. *
  2273. * 1. {DAV:}displayname
  2274. * 2. {http://apple.com/ns/ical/}refreshrate
  2275. * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
  2276. * should not be stripped).
  2277. * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
  2278. * should not be stripped).
  2279. * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
  2280. * attachments should not be stripped).
  2281. * 6. {http://calendarserver.org/ns/}source (Must be a
  2282. * Sabre\DAV\Property\Href).
  2283. * 7. {http://apple.com/ns/ical/}calendar-color
  2284. * 8. {http://apple.com/ns/ical/}calendar-order
  2285. * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
  2286. * (should just be an instance of
  2287. * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
  2288. * default components).
  2289. *
  2290. * @param string $principalUri
  2291. * @return array
  2292. */
  2293. public function getSubscriptionsForUser($principalUri) {
  2294. $fields = array_column($this->subscriptionPropertyMap, 0);
  2295. $fields[] = 'id';
  2296. $fields[] = 'uri';
  2297. $fields[] = 'source';
  2298. $fields[] = 'principaluri';
  2299. $fields[] = 'lastmodified';
  2300. $fields[] = 'synctoken';
  2301. $query = $this->db->getQueryBuilder();
  2302. $query->select($fields)
  2303. ->from('calendarsubscriptions')
  2304. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
  2305. ->orderBy('calendarorder', 'asc');
  2306. $stmt = $query->executeQuery();
  2307. $subscriptions = [];
  2308. while ($row = $stmt->fetch()) {
  2309. $subscription = [
  2310. 'id' => $row['id'],
  2311. 'uri' => $row['uri'],
  2312. 'principaluri' => $row['principaluri'],
  2313. 'source' => $row['source'],
  2314. 'lastmodified' => $row['lastmodified'],
  2315. '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']),
  2316. '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?: '0',
  2317. ];
  2318. $subscriptions[] = $this->rowToSubscription($row, $subscription);
  2319. }
  2320. return $subscriptions;
  2321. }
  2322. /**
  2323. * Creates a new subscription for a principal.
  2324. *
  2325. * If the creation was a success, an id must be returned that can be used to reference
  2326. * this subscription in other methods, such as updateSubscription.
  2327. *
  2328. * @param string $principalUri
  2329. * @param string $uri
  2330. * @param array $properties
  2331. * @return mixed
  2332. */
  2333. public function createSubscription($principalUri, $uri, array $properties) {
  2334. if (!isset($properties['{http://calendarserver.org/ns/}source'])) {
  2335. throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions');
  2336. }
  2337. $values = [
  2338. 'principaluri' => $principalUri,
  2339. 'uri' => $uri,
  2340. 'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(),
  2341. 'lastmodified' => time(),
  2342. ];
  2343. $propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments'];
  2344. foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
  2345. if (array_key_exists($xmlName, $properties)) {
  2346. $values[$dbName] = $properties[$xmlName];
  2347. if (in_array($dbName, $propertiesBoolean)) {
  2348. $values[$dbName] = true;
  2349. }
  2350. }
  2351. }
  2352. [$subscriptionId, $subscriptionRow] = $this->atomic(function () use ($values) {
  2353. $valuesToInsert = [];
  2354. $query = $this->db->getQueryBuilder();
  2355. foreach (array_keys($values) as $name) {
  2356. $valuesToInsert[$name] = $query->createNamedParameter($values[$name]);
  2357. }
  2358. $query->insert('calendarsubscriptions')
  2359. ->values($valuesToInsert)
  2360. ->executeStatement();
  2361. $subscriptionId = $query->getLastInsertId();
  2362. $subscriptionRow = $this->getSubscriptionById($subscriptionId);
  2363. return [$subscriptionId, $subscriptionRow];
  2364. }, $this->db);
  2365. $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow));
  2366. return $subscriptionId;
  2367. }
  2368. /**
  2369. * Updates a subscription
  2370. *
  2371. * The list of mutations is stored in a Sabre\DAV\PropPatch object.
  2372. * To do the actual updates, you must tell this object which properties
  2373. * you're going to process with the handle() method.
  2374. *
  2375. * Calling the handle method is like telling the PropPatch object "I
  2376. * promise I can handle updating this property".
  2377. *
  2378. * Read the PropPatch documentation for more info and examples.
  2379. *
  2380. * @param mixed $subscriptionId
  2381. * @param PropPatch $propPatch
  2382. * @return void
  2383. */
  2384. public function updateSubscription($subscriptionId, PropPatch $propPatch) {
  2385. $supportedProperties = array_keys($this->subscriptionPropertyMap);
  2386. $supportedProperties[] = '{http://calendarserver.org/ns/}source';
  2387. $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) {
  2388. $newValues = [];
  2389. foreach ($mutations as $propertyName => $propertyValue) {
  2390. if ($propertyName === '{http://calendarserver.org/ns/}source') {
  2391. $newValues['source'] = $propertyValue->getHref();
  2392. } else {
  2393. $fieldName = $this->subscriptionPropertyMap[$propertyName][0];
  2394. $newValues[$fieldName] = $propertyValue;
  2395. }
  2396. }
  2397. $subscriptionRow = $this->atomic(function () use ($subscriptionId, $newValues) {
  2398. $query = $this->db->getQueryBuilder();
  2399. $query->update('calendarsubscriptions')
  2400. ->set('lastmodified', $query->createNamedParameter(time()));
  2401. foreach ($newValues as $fieldName => $value) {
  2402. $query->set($fieldName, $query->createNamedParameter($value));
  2403. }
  2404. $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
  2405. ->executeStatement();
  2406. return $this->getSubscriptionById($subscriptionId);
  2407. }, $this->db);
  2408. $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations));
  2409. return true;
  2410. });
  2411. }
  2412. /**
  2413. * Deletes a subscription.
  2414. *
  2415. * @param mixed $subscriptionId
  2416. * @return void
  2417. */
  2418. public function deleteSubscription($subscriptionId) {
  2419. $this->atomic(function () use ($subscriptionId): void {
  2420. $subscriptionRow = $this->getSubscriptionById($subscriptionId);
  2421. $query = $this->db->getQueryBuilder();
  2422. $query->delete('calendarsubscriptions')
  2423. ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId)))
  2424. ->executeStatement();
  2425. $query = $this->db->getQueryBuilder();
  2426. $query->delete('calendarobjects')
  2427. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2428. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2429. ->executeStatement();
  2430. $query->delete('calendarchanges')
  2431. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2432. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2433. ->executeStatement();
  2434. $query->delete($this->dbObjectPropertiesTable)
  2435. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2436. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2437. ->executeStatement();
  2438. if ($subscriptionRow) {
  2439. $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, []));
  2440. }
  2441. }, $this->db);
  2442. }
  2443. /**
  2444. * Returns a single scheduling object for the inbox collection.
  2445. *
  2446. * The returned array should contain the following elements:
  2447. * * uri - A unique basename for the object. This will be used to
  2448. * construct a full uri.
  2449. * * calendardata - The iCalendar object
  2450. * * lastmodified - The last modification date. Can be an int for a unix
  2451. * timestamp, or a PHP DateTime object.
  2452. * * etag - A unique token that must change if the object changed.
  2453. * * size - The size of the object, in bytes.
  2454. *
  2455. * @param string $principalUri
  2456. * @param string $objectUri
  2457. * @return array
  2458. */
  2459. public function getSchedulingObject($principalUri, $objectUri) {
  2460. $query = $this->db->getQueryBuilder();
  2461. $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
  2462. ->from('schedulingobjects')
  2463. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
  2464. ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
  2465. ->executeQuery();
  2466. $row = $stmt->fetch();
  2467. if (!$row) {
  2468. return null;
  2469. }
  2470. return [
  2471. 'uri' => $row['uri'],
  2472. 'calendardata' => $row['calendardata'],
  2473. 'lastmodified' => $row['lastmodified'],
  2474. 'etag' => '"' . $row['etag'] . '"',
  2475. 'size' => (int)$row['size'],
  2476. ];
  2477. }
  2478. /**
  2479. * Returns all scheduling objects for the inbox collection.
  2480. *
  2481. * These objects should be returned as an array. Every item in the array
  2482. * should follow the same structure as returned from getSchedulingObject.
  2483. *
  2484. * The main difference is that 'calendardata' is optional.
  2485. *
  2486. * @param string $principalUri
  2487. * @return array
  2488. */
  2489. public function getSchedulingObjects($principalUri) {
  2490. $query = $this->db->getQueryBuilder();
  2491. $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size'])
  2492. ->from('schedulingobjects')
  2493. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
  2494. ->executeQuery();
  2495. $results = [];
  2496. while (($row = $stmt->fetch()) !== false) {
  2497. $results[] = [
  2498. 'calendardata' => $row['calendardata'],
  2499. 'uri' => $row['uri'],
  2500. 'lastmodified' => $row['lastmodified'],
  2501. 'etag' => '"' . $row['etag'] . '"',
  2502. 'size' => (int)$row['size'],
  2503. ];
  2504. }
  2505. $stmt->closeCursor();
  2506. return $results;
  2507. }
  2508. /**
  2509. * Deletes a scheduling object from the inbox collection.
  2510. *
  2511. * @param string $principalUri
  2512. * @param string $objectUri
  2513. * @return void
  2514. */
  2515. public function deleteSchedulingObject($principalUri, $objectUri) {
  2516. $this->cachedObjects = [];
  2517. $query = $this->db->getQueryBuilder();
  2518. $query->delete('schedulingobjects')
  2519. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri)))
  2520. ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri)))
  2521. ->executeStatement();
  2522. }
  2523. /**
  2524. * Deletes all scheduling objects last modified before $modifiedBefore from the inbox collection.
  2525. *
  2526. * @param int $modifiedBefore
  2527. * @param int $limit
  2528. * @return void
  2529. */
  2530. public function deleteOutdatedSchedulingObjects(int $modifiedBefore, int $limit): void {
  2531. $query = $this->db->getQueryBuilder();
  2532. $query->select('id')
  2533. ->from('schedulingobjects')
  2534. ->where($query->expr()->lt('lastmodified', $query->createNamedParameter($modifiedBefore)))
  2535. ->setMaxResults($limit);
  2536. $result = $query->executeQuery();
  2537. $count = $result->rowCount();
  2538. if ($count === 0) {
  2539. return;
  2540. }
  2541. $ids = array_map(static function (array $id) {
  2542. return (int)$id[0];
  2543. }, $result->fetchAll(\PDO::FETCH_NUM));
  2544. $result->closeCursor();
  2545. $numDeleted = 0;
  2546. $deleteQuery = $this->db->getQueryBuilder();
  2547. $deleteQuery->delete('schedulingobjects')
  2548. ->where($deleteQuery->expr()->in('id', $deleteQuery->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY));
  2549. foreach (array_chunk($ids, 1000) as $chunk) {
  2550. $deleteQuery->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
  2551. $numDeleted += $deleteQuery->executeStatement();
  2552. }
  2553. if ($numDeleted === $limit) {
  2554. $this->logger->info("Deleted $limit scheduling objects, continuing with next batch");
  2555. $this->deleteOutdatedSchedulingObjects($modifiedBefore, $limit);
  2556. }
  2557. }
  2558. /**
  2559. * Creates a new scheduling object. This should land in a users' inbox.
  2560. *
  2561. * @param string $principalUri
  2562. * @param string $objectUri
  2563. * @param string $objectData
  2564. * @return void
  2565. */
  2566. public function createSchedulingObject($principalUri, $objectUri, $objectData) {
  2567. $this->cachedObjects = [];
  2568. $query = $this->db->getQueryBuilder();
  2569. $query->insert('schedulingobjects')
  2570. ->values([
  2571. 'principaluri' => $query->createNamedParameter($principalUri),
  2572. 'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB),
  2573. 'uri' => $query->createNamedParameter($objectUri),
  2574. 'lastmodified' => $query->createNamedParameter(time()),
  2575. 'etag' => $query->createNamedParameter(md5($objectData)),
  2576. 'size' => $query->createNamedParameter(strlen($objectData))
  2577. ])
  2578. ->executeStatement();
  2579. }
  2580. /**
  2581. * Adds a change record to the calendarchanges table.
  2582. *
  2583. * @param mixed $calendarId
  2584. * @param string[] $objectUris
  2585. * @param int $operation 1 = add, 2 = modify, 3 = delete.
  2586. * @param int $calendarType
  2587. * @return void
  2588. */
  2589. protected function addChanges(int $calendarId, array $objectUris, int $operation, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
  2590. $this->cachedObjects = [];
  2591. $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions';
  2592. $this->atomic(function () use ($calendarId, $objectUris, $operation, $calendarType, $table): void {
  2593. $query = $this->db->getQueryBuilder();
  2594. $query->select('synctoken')
  2595. ->from($table)
  2596. ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)));
  2597. $result = $query->executeQuery();
  2598. $syncToken = (int)$result->fetchOne();
  2599. $result->closeCursor();
  2600. $query = $this->db->getQueryBuilder();
  2601. $query->insert('calendarchanges')
  2602. ->values([
  2603. 'uri' => $query->createParameter('uri'),
  2604. 'synctoken' => $query->createNamedParameter($syncToken),
  2605. 'calendarid' => $query->createNamedParameter($calendarId),
  2606. 'operation' => $query->createNamedParameter($operation),
  2607. 'calendartype' => $query->createNamedParameter($calendarType),
  2608. 'created_at' => time(),
  2609. ]);
  2610. foreach ($objectUris as $uri) {
  2611. $query->setParameter('uri', $uri);
  2612. $query->executeStatement();
  2613. }
  2614. $query = $this->db->getQueryBuilder();
  2615. $query->update($table)
  2616. ->set('synctoken', $query->createNamedParameter($syncToken + 1, IQueryBuilder::PARAM_INT))
  2617. ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId)))
  2618. ->executeStatement();
  2619. }, $this->db);
  2620. }
  2621. public function restoreChanges(int $calendarId, int $calendarType = self::CALENDAR_TYPE_CALENDAR): void {
  2622. $this->cachedObjects = [];
  2623. $this->atomic(function () use ($calendarId, $calendarType): void {
  2624. $qbAdded = $this->db->getQueryBuilder();
  2625. $qbAdded->select('uri')
  2626. ->from('calendarobjects')
  2627. ->where(
  2628. $qbAdded->expr()->andX(
  2629. $qbAdded->expr()->eq('calendarid', $qbAdded->createNamedParameter($calendarId)),
  2630. $qbAdded->expr()->eq('calendartype', $qbAdded->createNamedParameter($calendarType)),
  2631. $qbAdded->expr()->isNull('deleted_at'),
  2632. )
  2633. );
  2634. $resultAdded = $qbAdded->executeQuery();
  2635. $addedUris = $resultAdded->fetchAll(\PDO::FETCH_COLUMN);
  2636. $resultAdded->closeCursor();
  2637. // Track everything as changed
  2638. // Tracking the creation is not necessary because \OCA\DAV\CalDAV\CalDavBackend::getChangesForCalendar
  2639. // only returns the last change per object.
  2640. $this->addChanges($calendarId, $addedUris, 2, $calendarType);
  2641. $qbDeleted = $this->db->getQueryBuilder();
  2642. $qbDeleted->select('uri')
  2643. ->from('calendarobjects')
  2644. ->where(
  2645. $qbDeleted->expr()->andX(
  2646. $qbDeleted->expr()->eq('calendarid', $qbDeleted->createNamedParameter($calendarId)),
  2647. $qbDeleted->expr()->eq('calendartype', $qbDeleted->createNamedParameter($calendarType)),
  2648. $qbDeleted->expr()->isNotNull('deleted_at'),
  2649. )
  2650. );
  2651. $resultDeleted = $qbDeleted->executeQuery();
  2652. $deletedUris = array_map(function (string $uri) {
  2653. return str_replace('-deleted.ics', '.ics', $uri);
  2654. }, $resultDeleted->fetchAll(\PDO::FETCH_COLUMN));
  2655. $resultDeleted->closeCursor();
  2656. $this->addChanges($calendarId, $deletedUris, 3, $calendarType);
  2657. }, $this->db);
  2658. }
  2659. /**
  2660. * Parses some information from calendar objects, used for optimized
  2661. * calendar-queries.
  2662. *
  2663. * Returns an array with the following keys:
  2664. * * etag - An md5 checksum of the object without the quotes.
  2665. * * size - Size of the object in bytes
  2666. * * componentType - VEVENT, VTODO or VJOURNAL
  2667. * * firstOccurence
  2668. * * lastOccurence
  2669. * * uid - value of the UID property
  2670. *
  2671. * @param string $calendarData
  2672. * @return array
  2673. */
  2674. public function getDenormalizedData(string $calendarData): array {
  2675. $vObject = Reader::read($calendarData);
  2676. $vEvents = [];
  2677. $componentType = null;
  2678. $component = null;
  2679. $firstOccurrence = null;
  2680. $lastOccurrence = null;
  2681. $uid = null;
  2682. $classification = self::CLASSIFICATION_PUBLIC;
  2683. $hasDTSTART = false;
  2684. foreach ($vObject->getComponents() as $component) {
  2685. if ($component->name !== 'VTIMEZONE') {
  2686. // Finding all VEVENTs, and track them
  2687. if ($component->name === 'VEVENT') {
  2688. $vEvents[] = $component;
  2689. if ($component->DTSTART) {
  2690. $hasDTSTART = true;
  2691. }
  2692. }
  2693. // Track first component type and uid
  2694. if ($uid === null) {
  2695. $componentType = $component->name;
  2696. $uid = (string)$component->UID;
  2697. }
  2698. }
  2699. }
  2700. if (!$componentType) {
  2701. throw new BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component');
  2702. }
  2703. if ($hasDTSTART) {
  2704. $component = $vEvents[0];
  2705. // Finding the last occurrence is a bit harder
  2706. if (!isset($component->RRULE) && count($vEvents) === 1) {
  2707. $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp();
  2708. if (isset($component->DTEND)) {
  2709. $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp();
  2710. } elseif (isset($component->DURATION)) {
  2711. $endDate = clone $component->DTSTART->getDateTime();
  2712. $endDate->add(DateTimeParser::parse($component->DURATION->getValue()));
  2713. $lastOccurrence = $endDate->getTimeStamp();
  2714. } elseif (!$component->DTSTART->hasTime()) {
  2715. $endDate = clone $component->DTSTART->getDateTime();
  2716. $endDate->modify('+1 day');
  2717. $lastOccurrence = $endDate->getTimeStamp();
  2718. } else {
  2719. $lastOccurrence = $firstOccurrence;
  2720. }
  2721. } else {
  2722. try {
  2723. $it = new EventIterator($vEvents);
  2724. } catch (NoInstancesException $e) {
  2725. $this->logger->debug('Caught no instance exception for calendar data. This usually indicates invalid calendar data.', [
  2726. 'app' => 'dav',
  2727. 'exception' => $e,
  2728. ]);
  2729. throw new Forbidden($e->getMessage());
  2730. }
  2731. $maxDate = new DateTime(self::MAX_DATE);
  2732. $firstOccurrence = $it->getDtStart()->getTimestamp();
  2733. if ($it->isInfinite()) {
  2734. $lastOccurrence = $maxDate->getTimestamp();
  2735. } else {
  2736. $end = $it->getDtEnd();
  2737. while ($it->valid() && $end < $maxDate) {
  2738. $end = $it->getDtEnd();
  2739. $it->next();
  2740. }
  2741. $lastOccurrence = $end->getTimestamp();
  2742. }
  2743. }
  2744. }
  2745. if ($component->CLASS) {
  2746. $classification = CalDavBackend::CLASSIFICATION_PRIVATE;
  2747. switch ($component->CLASS->getValue()) {
  2748. case 'PUBLIC':
  2749. $classification = CalDavBackend::CLASSIFICATION_PUBLIC;
  2750. break;
  2751. case 'CONFIDENTIAL':
  2752. $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL;
  2753. break;
  2754. }
  2755. }
  2756. return [
  2757. 'etag' => md5($calendarData),
  2758. 'size' => strlen($calendarData),
  2759. 'componentType' => $componentType,
  2760. 'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence),
  2761. 'lastOccurence' => is_null($lastOccurrence) ? null : max(0, $lastOccurrence),
  2762. 'uid' => $uid,
  2763. 'classification' => $classification
  2764. ];
  2765. }
  2766. /**
  2767. * @param $cardData
  2768. * @return bool|string
  2769. */
  2770. private function readBlob($cardData) {
  2771. if (is_resource($cardData)) {
  2772. return stream_get_contents($cardData);
  2773. }
  2774. return $cardData;
  2775. }
  2776. /**
  2777. * @param list<array{href: string, commonName: string, readOnly: bool}> $add
  2778. * @param list<string> $remove
  2779. */
  2780. public function updateShares(IShareable $shareable, array $add, array $remove): void {
  2781. $this->atomic(function () use ($shareable, $add, $remove): void {
  2782. $calendarId = $shareable->getResourceId();
  2783. $calendarRow = $this->getCalendarById($calendarId);
  2784. if ($calendarRow === null) {
  2785. throw new \RuntimeException('Trying to update shares for non-existing calendar: ' . $calendarId);
  2786. }
  2787. $oldShares = $this->getShares($calendarId);
  2788. $this->calendarSharingBackend->updateShares($shareable, $add, $remove, $oldShares);
  2789. $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove));
  2790. }, $this->db);
  2791. }
  2792. /**
  2793. * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}>
  2794. */
  2795. public function getShares(int $resourceId): array {
  2796. return $this->calendarSharingBackend->getShares($resourceId);
  2797. }
  2798. public function preloadShares(array $resourceIds): void {
  2799. $this->calendarSharingBackend->preloadShares($resourceIds);
  2800. }
  2801. /**
  2802. * @param boolean $value
  2803. * @param Calendar $calendar
  2804. * @return string|null
  2805. */
  2806. public function setPublishStatus($value, $calendar) {
  2807. return $this->atomic(function () use ($value, $calendar) {
  2808. $calendarId = $calendar->getResourceId();
  2809. $calendarData = $this->getCalendarById($calendarId);
  2810. $query = $this->db->getQueryBuilder();
  2811. if ($value) {
  2812. $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE);
  2813. $query->insert('dav_shares')
  2814. ->values([
  2815. 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()),
  2816. 'type' => $query->createNamedParameter('calendar'),
  2817. 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC),
  2818. 'resourceid' => $query->createNamedParameter($calendar->getResourceId()),
  2819. 'publicuri' => $query->createNamedParameter($publicUri)
  2820. ]);
  2821. $query->executeStatement();
  2822. $this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri));
  2823. return $publicUri;
  2824. }
  2825. $query->delete('dav_shares')
  2826. ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
  2827. ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)));
  2828. $query->executeStatement();
  2829. $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData));
  2830. return null;
  2831. }, $this->db);
  2832. }
  2833. /**
  2834. * @param Calendar $calendar
  2835. * @return mixed
  2836. */
  2837. public function getPublishStatus($calendar) {
  2838. $query = $this->db->getQueryBuilder();
  2839. $result = $query->select('publicuri')
  2840. ->from('dav_shares')
  2841. ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId())))
  2842. ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC)))
  2843. ->executeQuery();
  2844. $row = $result->fetch();
  2845. $result->closeCursor();
  2846. return $row ? reset($row) : false;
  2847. }
  2848. /**
  2849. * @param int $resourceId
  2850. * @param list<array{privilege: string, principal: string, protected: bool}> $acl
  2851. * @return list<array{privilege: string, principal: string, protected: bool}>
  2852. */
  2853. public function applyShareAcl(int $resourceId, array $acl): array {
  2854. $shares = $this->calendarSharingBackend->getShares($resourceId);
  2855. return $this->calendarSharingBackend->applyShareAcl($shares, $acl);
  2856. }
  2857. /**
  2858. * update properties table
  2859. *
  2860. * @param int $calendarId
  2861. * @param string $objectUri
  2862. * @param string $calendarData
  2863. * @param int $calendarType
  2864. */
  2865. public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) {
  2866. $this->cachedObjects = [];
  2867. $this->atomic(function () use ($calendarId, $objectUri, $calendarData, $calendarType): void {
  2868. $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType);
  2869. try {
  2870. $vCalendar = $this->readCalendarData($calendarData);
  2871. } catch (\Exception $ex) {
  2872. return;
  2873. }
  2874. $this->purgeProperties($calendarId, $objectId);
  2875. $query = $this->db->getQueryBuilder();
  2876. $query->insert($this->dbObjectPropertiesTable)
  2877. ->values(
  2878. [
  2879. 'calendarid' => $query->createNamedParameter($calendarId),
  2880. 'calendartype' => $query->createNamedParameter($calendarType),
  2881. 'objectid' => $query->createNamedParameter($objectId),
  2882. 'name' => $query->createParameter('name'),
  2883. 'parameter' => $query->createParameter('parameter'),
  2884. 'value' => $query->createParameter('value'),
  2885. ]
  2886. );
  2887. $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO'];
  2888. foreach ($vCalendar->getComponents() as $component) {
  2889. if (!in_array($component->name, $indexComponents)) {
  2890. continue;
  2891. }
  2892. foreach ($component->children() as $property) {
  2893. if (in_array($property->name, self::INDEXED_PROPERTIES, true)) {
  2894. $value = $property->getValue();
  2895. // is this a shitty db?
  2896. if (!$this->db->supports4ByteText()) {
  2897. $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
  2898. }
  2899. $value = mb_strcut($value, 0, 254);
  2900. $query->setParameter('name', $property->name);
  2901. $query->setParameter('parameter', null);
  2902. $query->setParameter('value', mb_strcut($value, 0, 254));
  2903. $query->executeStatement();
  2904. }
  2905. if (array_key_exists($property->name, self::$indexParameters)) {
  2906. $parameters = $property->parameters();
  2907. $indexedParametersForProperty = self::$indexParameters[$property->name];
  2908. foreach ($parameters as $key => $value) {
  2909. if (in_array($key, $indexedParametersForProperty)) {
  2910. // is this a shitty db?
  2911. if ($this->db->supports4ByteText()) {
  2912. $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value);
  2913. }
  2914. $query->setParameter('name', $property->name);
  2915. $query->setParameter('parameter', mb_strcut($key, 0, 254));
  2916. $query->setParameter('value', mb_strcut($value, 0, 254));
  2917. $query->executeStatement();
  2918. }
  2919. }
  2920. }
  2921. }
  2922. }
  2923. }, $this->db);
  2924. }
  2925. /**
  2926. * deletes all birthday calendars
  2927. */
  2928. public function deleteAllBirthdayCalendars() {
  2929. $this->atomic(function (): void {
  2930. $query = $this->db->getQueryBuilder();
  2931. $result = $query->select(['id'])->from('calendars')
  2932. ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI)))
  2933. ->executeQuery();
  2934. while (($row = $result->fetch()) !== false) {
  2935. $this->deleteCalendar(
  2936. $row['id'],
  2937. true // No data to keep in the trashbin, if the user re-enables then we regenerate
  2938. );
  2939. }
  2940. $result->closeCursor();
  2941. }, $this->db);
  2942. }
  2943. /**
  2944. * @param $subscriptionId
  2945. */
  2946. public function purgeAllCachedEventsForSubscription($subscriptionId) {
  2947. $this->atomic(function () use ($subscriptionId): void {
  2948. $query = $this->db->getQueryBuilder();
  2949. $query->select('uri')
  2950. ->from('calendarobjects')
  2951. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2952. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)));
  2953. $stmt = $query->executeQuery();
  2954. $uris = [];
  2955. while (($row = $stmt->fetch()) !== false) {
  2956. $uris[] = $row['uri'];
  2957. }
  2958. $stmt->closeCursor();
  2959. $query = $this->db->getQueryBuilder();
  2960. $query->delete('calendarobjects')
  2961. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2962. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2963. ->executeStatement();
  2964. $query = $this->db->getQueryBuilder();
  2965. $query->delete('calendarchanges')
  2966. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2967. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2968. ->executeStatement();
  2969. $query = $this->db->getQueryBuilder();
  2970. $query->delete($this->dbObjectPropertiesTable)
  2971. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2972. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2973. ->executeStatement();
  2974. $this->addChanges($subscriptionId, $uris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
  2975. }, $this->db);
  2976. }
  2977. /**
  2978. * @param int $subscriptionId
  2979. * @param array<int> $calendarObjectIds
  2980. * @param array<string> $calendarObjectUris
  2981. */
  2982. public function purgeCachedEventsForSubscription(int $subscriptionId, array $calendarObjectIds, array $calendarObjectUris): void {
  2983. if (empty($calendarObjectUris)) {
  2984. return;
  2985. }
  2986. $this->atomic(function () use ($subscriptionId, $calendarObjectIds, $calendarObjectUris): void {
  2987. foreach (array_chunk($calendarObjectIds, 1000) as $chunk) {
  2988. $query = $this->db->getQueryBuilder();
  2989. $query->delete($this->dbObjectPropertiesTable)
  2990. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2991. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2992. ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
  2993. ->executeStatement();
  2994. $query = $this->db->getQueryBuilder();
  2995. $query->delete('calendarobjects')
  2996. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  2997. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  2998. ->andWhere($query->expr()->in('id', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY), IQueryBuilder::PARAM_INT_ARRAY))
  2999. ->executeStatement();
  3000. }
  3001. foreach (array_chunk($calendarObjectUris, 1000) as $chunk) {
  3002. $query = $this->db->getQueryBuilder();
  3003. $query->delete('calendarchanges')
  3004. ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId)))
  3005. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION)))
  3006. ->andWhere($query->expr()->in('uri', $query->createNamedParameter($chunk, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR_ARRAY))
  3007. ->executeStatement();
  3008. }
  3009. $this->addChanges($subscriptionId, $calendarObjectUris, 3, self::CALENDAR_TYPE_SUBSCRIPTION);
  3010. }, $this->db);
  3011. }
  3012. /**
  3013. * Move a calendar from one user to another
  3014. *
  3015. * @param string $uriName
  3016. * @param string $uriOrigin
  3017. * @param string $uriDestination
  3018. * @param string $newUriName (optional) the new uriName
  3019. */
  3020. public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) {
  3021. $query = $this->db->getQueryBuilder();
  3022. $query->update('calendars')
  3023. ->set('principaluri', $query->createNamedParameter($uriDestination))
  3024. ->set('uri', $query->createNamedParameter($newUriName ?: $uriName))
  3025. ->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin)))
  3026. ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName)))
  3027. ->executeStatement();
  3028. }
  3029. /**
  3030. * read VCalendar data into a VCalendar object
  3031. *
  3032. * @param string $objectData
  3033. * @return VCalendar
  3034. */
  3035. protected function readCalendarData($objectData) {
  3036. return Reader::read($objectData);
  3037. }
  3038. /**
  3039. * delete all properties from a given calendar object
  3040. *
  3041. * @param int $calendarId
  3042. * @param int $objectId
  3043. */
  3044. protected function purgeProperties($calendarId, $objectId) {
  3045. $this->cachedObjects = [];
  3046. $query = $this->db->getQueryBuilder();
  3047. $query->delete($this->dbObjectPropertiesTable)
  3048. ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId)))
  3049. ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)));
  3050. $query->executeStatement();
  3051. }
  3052. /**
  3053. * get ID from a given calendar object
  3054. *
  3055. * @param int $calendarId
  3056. * @param string $uri
  3057. * @param int $calendarType
  3058. * @return int
  3059. */
  3060. protected function getCalendarObjectId($calendarId, $uri, $calendarType):int {
  3061. $query = $this->db->getQueryBuilder();
  3062. $query->select('id')
  3063. ->from('calendarobjects')
  3064. ->where($query->expr()->eq('uri', $query->createNamedParameter($uri)))
  3065. ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId)))
  3066. ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType)));
  3067. $result = $query->executeQuery();
  3068. $objectIds = $result->fetch();
  3069. $result->closeCursor();
  3070. if (!isset($objectIds['id'])) {
  3071. throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri);
  3072. }
  3073. return (int)$objectIds['id'];
  3074. }
  3075. /**
  3076. * @throws \InvalidArgumentException
  3077. */
  3078. public function pruneOutdatedSyncTokens(int $keep, int $retention): int {
  3079. if ($keep < 0) {
  3080. throw new \InvalidArgumentException();
  3081. }
  3082. $query = $this->db->getQueryBuilder();
  3083. $query->select($query->func()->max('id'))
  3084. ->from('calendarchanges');
  3085. $result = $query->executeQuery();
  3086. $maxId = (int)$result->fetchOne();
  3087. $result->closeCursor();
  3088. if (!$maxId || $maxId < $keep) {
  3089. return 0;
  3090. }
  3091. $query = $this->db->getQueryBuilder();
  3092. $query->delete('calendarchanges')
  3093. ->where(
  3094. $query->expr()->lte('id', $query->createNamedParameter($maxId - $keep, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT),
  3095. $query->expr()->lte('created_at', $query->createNamedParameter($retention)),
  3096. );
  3097. return $query->executeStatement();
  3098. }
  3099. /**
  3100. * return legacy endpoint principal name to new principal name
  3101. *
  3102. * @param $principalUri
  3103. * @param $toV2
  3104. * @return string
  3105. */
  3106. private function convertPrincipal($principalUri, $toV2) {
  3107. if ($this->principalBackend->getPrincipalPrefix() === 'principals') {
  3108. [, $name] = Uri\split($principalUri);
  3109. if ($toV2 === true) {
  3110. return "principals/users/$name";
  3111. }
  3112. return "principals/$name";
  3113. }
  3114. return $principalUri;
  3115. }
  3116. /**
  3117. * adds information about an owner to the calendar data
  3118. *
  3119. */
  3120. private function addOwnerPrincipalToCalendar(array $calendarInfo): array {
  3121. $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal';
  3122. $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname';
  3123. if (isset($calendarInfo[$ownerPrincipalKey])) {
  3124. $uri = $calendarInfo[$ownerPrincipalKey];
  3125. } else {
  3126. $uri = $calendarInfo['principaluri'];
  3127. }
  3128. $principalInformation = $this->principalBackend->getPrincipalByPath($uri);
  3129. if (isset($principalInformation['{DAV:}displayname'])) {
  3130. $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname'];
  3131. }
  3132. return $calendarInfo;
  3133. }
  3134. private function addResourceTypeToCalendar(array $row, array $calendar): array {
  3135. if (isset($row['deleted_at'])) {
  3136. // Columns is set and not null -> this is a deleted calendar
  3137. // we send a custom resourcetype to hide the deleted calendar
  3138. // from ordinary DAV clients, but the Calendar app will know
  3139. // how to handle this special resource.
  3140. $calendar['{DAV:}resourcetype'] = new DAV\Xml\Property\ResourceType([
  3141. '{DAV:}collection',
  3142. sprintf('{%s}deleted-calendar', \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD),
  3143. ]);
  3144. }
  3145. return $calendar;
  3146. }
  3147. /**
  3148. * Amend the calendar info with database row data
  3149. *
  3150. * @param array $row
  3151. * @param array $calendar
  3152. *
  3153. * @return array
  3154. */
  3155. private function rowToCalendar($row, array $calendar): array {
  3156. foreach ($this->propertyMap as $xmlName => [$dbName, $type]) {
  3157. $value = $row[$dbName];
  3158. if ($value !== null) {
  3159. settype($value, $type);
  3160. }
  3161. $calendar[$xmlName] = $value;
  3162. }
  3163. return $calendar;
  3164. }
  3165. /**
  3166. * Amend the subscription info with database row data
  3167. *
  3168. * @param array $row
  3169. * @param array $subscription
  3170. *
  3171. * @return array
  3172. */
  3173. private function rowToSubscription($row, array $subscription): array {
  3174. foreach ($this->subscriptionPropertyMap as $xmlName => [$dbName, $type]) {
  3175. $value = $row[$dbName];
  3176. if ($value !== null) {
  3177. settype($value, $type);
  3178. }
  3179. $subscription[$xmlName] = $value;
  3180. }
  3181. return $subscription;
  3182. }
  3183. /**
  3184. * delete all invitations from a given calendar
  3185. *
  3186. * @since 31.0.0
  3187. *
  3188. * @param int $calendarId
  3189. *
  3190. * @return void
  3191. */
  3192. protected function purgeCalendarInvitations(int $calendarId): void {
  3193. // select all calendar object uid's
  3194. $cmd = $this->db->getQueryBuilder();
  3195. $cmd->select('uid')
  3196. ->from($this->dbObjectsTable)
  3197. ->where($cmd->expr()->eq('calendarid', $cmd->createNamedParameter($calendarId)));
  3198. $allIds = $cmd->executeQuery()->fetchAll(\PDO::FETCH_COLUMN);
  3199. // delete all links that match object uid's
  3200. $cmd = $this->db->getQueryBuilder();
  3201. $cmd->delete($this->dbObjectInvitationsTable)
  3202. ->where($cmd->expr()->in('uid', $cmd->createParameter('uids'), IQueryBuilder::PARAM_STR_ARRAY));
  3203. foreach (array_chunk($allIds, 1000) as $chunkIds) {
  3204. $cmd->setParameter('uids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY);
  3205. $cmd->executeStatement();
  3206. }
  3207. }
  3208. /**
  3209. * Delete all invitations from a given calendar event
  3210. *
  3211. * @since 31.0.0
  3212. *
  3213. * @param string $eventId UID of the event
  3214. *
  3215. * @return void
  3216. */
  3217. protected function purgeObjectInvitations(string $eventId): void {
  3218. $cmd = $this->db->getQueryBuilder();
  3219. $cmd->delete($this->dbObjectInvitationsTable)
  3220. ->where($cmd->expr()->eq('uid', $cmd->createNamedParameter($eventId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR));
  3221. $cmd->executeStatement();
  3222. }
  3223. }