CalDavBackend.php 125 KB

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