@@ -39,44 +39,44 @@ |
||
| 39 | 39 | */ |
| 40 | 40 | class AccessTokenMapper extends QBMapper { |
| 41 | 41 | |
| 42 | - /** |
|
| 43 | - * @param IDBConnection $db |
|
| 44 | - */ |
|
| 45 | - public function __construct(IDBConnection $db) { |
|
| 46 | - parent::__construct($db, 'oauth2_access_tokens'); |
|
| 47 | - } |
|
| 42 | + /** |
|
| 43 | + * @param IDBConnection $db |
|
| 44 | + */ |
|
| 45 | + public function __construct(IDBConnection $db) { |
|
| 46 | + parent::__construct($db, 'oauth2_access_tokens'); |
|
| 47 | + } |
|
| 48 | 48 | |
| 49 | - /** |
|
| 50 | - * @param string $code |
|
| 51 | - * @return AccessToken |
|
| 52 | - * @throws AccessTokenNotFoundException |
|
| 53 | - */ |
|
| 54 | - public function getByCode(string $code): AccessToken { |
|
| 55 | - $qb = $this->db->getQueryBuilder(); |
|
| 56 | - $qb |
|
| 57 | - ->select('*') |
|
| 58 | - ->from($this->tableName) |
|
| 59 | - ->where($qb->expr()->eq('hashed_code', $qb->createNamedParameter(hash('sha512', $code)))); |
|
| 49 | + /** |
|
| 50 | + * @param string $code |
|
| 51 | + * @return AccessToken |
|
| 52 | + * @throws AccessTokenNotFoundException |
|
| 53 | + */ |
|
| 54 | + public function getByCode(string $code): AccessToken { |
|
| 55 | + $qb = $this->db->getQueryBuilder(); |
|
| 56 | + $qb |
|
| 57 | + ->select('*') |
|
| 58 | + ->from($this->tableName) |
|
| 59 | + ->where($qb->expr()->eq('hashed_code', $qb->createNamedParameter(hash('sha512', $code)))); |
|
| 60 | 60 | |
| 61 | - try { |
|
| 62 | - $token = $this->findEntity($qb); |
|
| 63 | - } catch (IMapperException $e) { |
|
| 64 | - throw new AccessTokenNotFoundException('Could not find access token', 0, $e); |
|
| 65 | - } |
|
| 61 | + try { |
|
| 62 | + $token = $this->findEntity($qb); |
|
| 63 | + } catch (IMapperException $e) { |
|
| 64 | + throw new AccessTokenNotFoundException('Could not find access token', 0, $e); |
|
| 65 | + } |
|
| 66 | 66 | |
| 67 | - return $token; |
|
| 68 | - } |
|
| 67 | + return $token; |
|
| 68 | + } |
|
| 69 | 69 | |
| 70 | - /** |
|
| 71 | - * delete all access token from a given client |
|
| 72 | - * |
|
| 73 | - * @param int $id |
|
| 74 | - */ |
|
| 75 | - public function deleteByClientId(int $id) { |
|
| 76 | - $qb = $this->db->getQueryBuilder(); |
|
| 77 | - $qb |
|
| 78 | - ->delete($this->tableName) |
|
| 79 | - ->where($qb->expr()->eq('client_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); |
|
| 80 | - $qb->executeStatement(); |
|
| 81 | - } |
|
| 70 | + /** |
|
| 71 | + * delete all access token from a given client |
|
| 72 | + * |
|
| 73 | + * @param int $id |
|
| 74 | + */ |
|
| 75 | + public function deleteByClientId(int $id) { |
|
| 76 | + $qb = $this->db->getQueryBuilder(); |
|
| 77 | + $qb |
|
| 78 | + ->delete($this->tableName) |
|
| 79 | + ->where($qb->expr()->eq('client_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))); |
|
| 80 | + $qb->executeStatement(); |
|
| 81 | + } |
|
| 82 | 82 | } |
@@ -96,2768 +96,2768 @@ |
||
| 96 | 96 | * @package OCA\DAV\CalDAV |
| 97 | 97 | */ |
| 98 | 98 | class CalDavBackend extends AbstractBackend implements SyncSupport, SubscriptionSupport, SchedulingSupport { |
| 99 | - public const CALENDAR_TYPE_CALENDAR = 0; |
|
| 100 | - public const CALENDAR_TYPE_SUBSCRIPTION = 1; |
|
| 101 | - |
|
| 102 | - public const PERSONAL_CALENDAR_URI = 'personal'; |
|
| 103 | - public const PERSONAL_CALENDAR_NAME = 'Personal'; |
|
| 104 | - |
|
| 105 | - public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar'; |
|
| 106 | - public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar'; |
|
| 107 | - |
|
| 108 | - /** |
|
| 109 | - * We need to specify a max date, because we need to stop *somewhere* |
|
| 110 | - * |
|
| 111 | - * On 32 bit system the maximum for a signed integer is 2147483647, so |
|
| 112 | - * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results |
|
| 113 | - * in 2038-01-19 to avoid problems when the date is converted |
|
| 114 | - * to a unix timestamp. |
|
| 115 | - */ |
|
| 116 | - public const MAX_DATE = '2038-01-01'; |
|
| 117 | - |
|
| 118 | - public const ACCESS_PUBLIC = 4; |
|
| 119 | - public const CLASSIFICATION_PUBLIC = 0; |
|
| 120 | - public const CLASSIFICATION_PRIVATE = 1; |
|
| 121 | - public const CLASSIFICATION_CONFIDENTIAL = 2; |
|
| 122 | - |
|
| 123 | - /** |
|
| 124 | - * List of CalDAV properties, and how they map to database field names |
|
| 125 | - * Add your own properties by simply adding on to this array. |
|
| 126 | - * |
|
| 127 | - * Note that only string-based properties are supported here. |
|
| 128 | - * |
|
| 129 | - * @var array |
|
| 130 | - */ |
|
| 131 | - public $propertyMap = [ |
|
| 132 | - '{DAV:}displayname' => 'displayname', |
|
| 133 | - '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', |
|
| 134 | - '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', |
|
| 135 | - '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', |
|
| 136 | - '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', |
|
| 137 | - ]; |
|
| 138 | - |
|
| 139 | - /** |
|
| 140 | - * List of subscription properties, and how they map to database field names. |
|
| 141 | - * |
|
| 142 | - * @var array |
|
| 143 | - */ |
|
| 144 | - public $subscriptionPropertyMap = [ |
|
| 145 | - '{DAV:}displayname' => 'displayname', |
|
| 146 | - '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', |
|
| 147 | - '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', |
|
| 148 | - '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', |
|
| 149 | - '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', |
|
| 150 | - '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', |
|
| 151 | - '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', |
|
| 152 | - ]; |
|
| 153 | - |
|
| 154 | - /** @var array properties to index */ |
|
| 155 | - public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION', |
|
| 156 | - 'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT', |
|
| 157 | - 'ORGANIZER']; |
|
| 158 | - |
|
| 159 | - /** @var array parameters to index */ |
|
| 160 | - public static $indexParameters = [ |
|
| 161 | - 'ATTENDEE' => ['CN'], |
|
| 162 | - 'ORGANIZER' => ['CN'], |
|
| 163 | - ]; |
|
| 164 | - |
|
| 165 | - /** |
|
| 166 | - * @var string[] Map of uid => display name |
|
| 167 | - */ |
|
| 168 | - protected $userDisplayNames; |
|
| 169 | - |
|
| 170 | - /** @var IDBConnection */ |
|
| 171 | - private $db; |
|
| 172 | - |
|
| 173 | - /** @var Backend */ |
|
| 174 | - private $calendarSharingBackend; |
|
| 175 | - |
|
| 176 | - /** @var Principal */ |
|
| 177 | - private $principalBackend; |
|
| 178 | - |
|
| 179 | - /** @var IUserManager */ |
|
| 180 | - private $userManager; |
|
| 181 | - |
|
| 182 | - /** @var ISecureRandom */ |
|
| 183 | - private $random; |
|
| 184 | - |
|
| 185 | - /** @var ILogger */ |
|
| 186 | - private $logger; |
|
| 187 | - |
|
| 188 | - /** @var IEventDispatcher */ |
|
| 189 | - private $dispatcher; |
|
| 190 | - |
|
| 191 | - /** @var EventDispatcherInterface */ |
|
| 192 | - private $legacyDispatcher; |
|
| 193 | - |
|
| 194 | - /** @var bool */ |
|
| 195 | - private $legacyEndpoint; |
|
| 196 | - |
|
| 197 | - /** @var string */ |
|
| 198 | - private $dbObjectPropertiesTable = 'calendarobjects_props'; |
|
| 199 | - |
|
| 200 | - /** |
|
| 201 | - * CalDavBackend constructor. |
|
| 202 | - * |
|
| 203 | - * @param IDBConnection $db |
|
| 204 | - * @param Principal $principalBackend |
|
| 205 | - * @param IUserManager $userManager |
|
| 206 | - * @param IGroupManager $groupManager |
|
| 207 | - * @param ISecureRandom $random |
|
| 208 | - * @param ILogger $logger |
|
| 209 | - * @param IEventDispatcher $dispatcher |
|
| 210 | - * @param EventDispatcherInterface $legacyDispatcher |
|
| 211 | - * @param bool $legacyEndpoint |
|
| 212 | - */ |
|
| 213 | - public function __construct(IDBConnection $db, |
|
| 214 | - Principal $principalBackend, |
|
| 215 | - IUserManager $userManager, |
|
| 216 | - IGroupManager $groupManager, |
|
| 217 | - ISecureRandom $random, |
|
| 218 | - ILogger $logger, |
|
| 219 | - IEventDispatcher $dispatcher, |
|
| 220 | - EventDispatcherInterface $legacyDispatcher, |
|
| 221 | - bool $legacyEndpoint = false) { |
|
| 222 | - $this->db = $db; |
|
| 223 | - $this->principalBackend = $principalBackend; |
|
| 224 | - $this->userManager = $userManager; |
|
| 225 | - $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar'); |
|
| 226 | - $this->random = $random; |
|
| 227 | - $this->logger = $logger; |
|
| 228 | - $this->dispatcher = $dispatcher; |
|
| 229 | - $this->legacyDispatcher = $legacyDispatcher; |
|
| 230 | - $this->legacyEndpoint = $legacyEndpoint; |
|
| 231 | - } |
|
| 232 | - |
|
| 233 | - /** |
|
| 234 | - * Return the number of calendars for a principal |
|
| 235 | - * |
|
| 236 | - * By default this excludes the automatically generated birthday calendar |
|
| 237 | - * |
|
| 238 | - * @param $principalUri |
|
| 239 | - * @param bool $excludeBirthday |
|
| 240 | - * @return int |
|
| 241 | - */ |
|
| 242 | - public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) { |
|
| 243 | - $principalUri = $this->convertPrincipal($principalUri, true); |
|
| 244 | - $query = $this->db->getQueryBuilder(); |
|
| 245 | - $query->select($query->func()->count('*')) |
|
| 246 | - ->from('calendars'); |
|
| 247 | - |
|
| 248 | - if ($principalUri === '') { |
|
| 249 | - $query->where($query->expr()->emptyString('principaluri')); |
|
| 250 | - } else { |
|
| 251 | - $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
| 252 | - } |
|
| 253 | - |
|
| 254 | - if ($excludeBirthday) { |
|
| 255 | - $query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))); |
|
| 256 | - } |
|
| 257 | - |
|
| 258 | - $result = $query->executeQuery(); |
|
| 259 | - $column = (int)$result->fetchOne(); |
|
| 260 | - $result->closeCursor(); |
|
| 261 | - return $column; |
|
| 262 | - } |
|
| 263 | - |
|
| 264 | - /** |
|
| 265 | - * Returns a list of calendars for a principal. |
|
| 266 | - * |
|
| 267 | - * Every project is an array with the following keys: |
|
| 268 | - * * id, a unique id that will be used by other functions to modify the |
|
| 269 | - * calendar. This can be the same as the uri or a database key. |
|
| 270 | - * * uri, which the basename of the uri with which the calendar is |
|
| 271 | - * accessed. |
|
| 272 | - * * principaluri. The owner of the calendar. Almost always the same as |
|
| 273 | - * principalUri passed to this method. |
|
| 274 | - * |
|
| 275 | - * Furthermore it can contain webdav properties in clark notation. A very |
|
| 276 | - * common one is '{DAV:}displayname'. |
|
| 277 | - * |
|
| 278 | - * Many clients also require: |
|
| 279 | - * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set |
|
| 280 | - * For this property, you can just return an instance of |
|
| 281 | - * Sabre\CalDAV\Property\SupportedCalendarComponentSet. |
|
| 282 | - * |
|
| 283 | - * If you return {http://sabredav.org/ns}read-only and set the value to 1, |
|
| 284 | - * ACL will automatically be put in read-only mode. |
|
| 285 | - * |
|
| 286 | - * @param string $principalUri |
|
| 287 | - * @return array |
|
| 288 | - */ |
|
| 289 | - public function getCalendarsForUser($principalUri) { |
|
| 290 | - $principalUriOriginal = $principalUri; |
|
| 291 | - $principalUri = $this->convertPrincipal($principalUri, true); |
|
| 292 | - $fields = array_values($this->propertyMap); |
|
| 293 | - $fields[] = 'id'; |
|
| 294 | - $fields[] = 'uri'; |
|
| 295 | - $fields[] = 'synctoken'; |
|
| 296 | - $fields[] = 'components'; |
|
| 297 | - $fields[] = 'principaluri'; |
|
| 298 | - $fields[] = 'transparent'; |
|
| 299 | - |
|
| 300 | - // Making fields a comma-delimited list |
|
| 301 | - $query = $this->db->getQueryBuilder(); |
|
| 302 | - $query->select($fields) |
|
| 303 | - ->from('calendars') |
|
| 304 | - ->orderBy('calendarorder', 'ASC'); |
|
| 305 | - |
|
| 306 | - if ($principalUri === '') { |
|
| 307 | - $query->where($query->expr()->emptyString('principaluri')); |
|
| 308 | - } else { |
|
| 309 | - $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
| 310 | - } |
|
| 311 | - |
|
| 312 | - $result = $query->executeQuery(); |
|
| 313 | - |
|
| 314 | - $calendars = []; |
|
| 315 | - while ($row = $result->fetch()) { |
|
| 316 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 317 | - $components = []; |
|
| 318 | - if ($row['components']) { |
|
| 319 | - $components = explode(',',$row['components']); |
|
| 320 | - } |
|
| 321 | - |
|
| 322 | - $calendar = [ |
|
| 323 | - 'id' => $row['id'], |
|
| 324 | - 'uri' => $row['uri'], |
|
| 325 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 326 | - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 327 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 328 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 329 | - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 330 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), |
|
| 331 | - ]; |
|
| 332 | - |
|
| 333 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 334 | - $calendar[$xmlName] = $row[$dbName]; |
|
| 335 | - } |
|
| 336 | - |
|
| 337 | - $this->addOwnerPrincipal($calendar); |
|
| 338 | - |
|
| 339 | - if (!isset($calendars[$calendar['id']])) { |
|
| 340 | - $calendars[$calendar['id']] = $calendar; |
|
| 341 | - } |
|
| 342 | - } |
|
| 343 | - $result->closeCursor(); |
|
| 344 | - |
|
| 345 | - // query for shared calendars |
|
| 346 | - $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); |
|
| 347 | - $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); |
|
| 348 | - |
|
| 349 | - $principals[] = $principalUri; |
|
| 350 | - |
|
| 351 | - $fields = array_values($this->propertyMap); |
|
| 352 | - $fields[] = 'a.id'; |
|
| 353 | - $fields[] = 'a.uri'; |
|
| 354 | - $fields[] = 'a.synctoken'; |
|
| 355 | - $fields[] = 'a.components'; |
|
| 356 | - $fields[] = 'a.principaluri'; |
|
| 357 | - $fields[] = 'a.transparent'; |
|
| 358 | - $fields[] = 's.access'; |
|
| 359 | - $query = $this->db->getQueryBuilder(); |
|
| 360 | - $query->select($fields) |
|
| 361 | - ->from('dav_shares', 's') |
|
| 362 | - ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
| 363 | - ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) |
|
| 364 | - ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) |
|
| 365 | - ->setParameter('type', 'calendar') |
|
| 366 | - ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); |
|
| 367 | - |
|
| 368 | - $result = $query->executeQuery(); |
|
| 369 | - |
|
| 370 | - $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; |
|
| 371 | - while ($row = $result->fetch()) { |
|
| 372 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 373 | - if ($row['principaluri'] === $principalUri) { |
|
| 374 | - continue; |
|
| 375 | - } |
|
| 376 | - |
|
| 377 | - $readOnly = (int) $row['access'] === Backend::ACCESS_READ; |
|
| 378 | - if (isset($calendars[$row['id']])) { |
|
| 379 | - if ($readOnly) { |
|
| 380 | - // New share can not have more permissions then the old one. |
|
| 381 | - continue; |
|
| 382 | - } |
|
| 383 | - if (isset($calendars[$row['id']][$readOnlyPropertyName]) && |
|
| 384 | - $calendars[$row['id']][$readOnlyPropertyName] === 0) { |
|
| 385 | - // Old share is already read-write, no more permissions can be gained |
|
| 386 | - continue; |
|
| 387 | - } |
|
| 388 | - } |
|
| 389 | - |
|
| 390 | - [, $name] = Uri\split($row['principaluri']); |
|
| 391 | - $uri = $row['uri'] . '_shared_by_' . $name; |
|
| 392 | - $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')'; |
|
| 393 | - $components = []; |
|
| 394 | - if ($row['components']) { |
|
| 395 | - $components = explode(',',$row['components']); |
|
| 396 | - } |
|
| 397 | - $calendar = [ |
|
| 398 | - 'id' => $row['id'], |
|
| 399 | - 'uri' => $uri, |
|
| 400 | - 'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), |
|
| 401 | - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 402 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 403 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 404 | - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'), |
|
| 405 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 406 | - $readOnlyPropertyName => $readOnly, |
|
| 407 | - ]; |
|
| 408 | - |
|
| 409 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 410 | - $calendar[$xmlName] = $row[$dbName]; |
|
| 411 | - } |
|
| 412 | - |
|
| 413 | - $this->addOwnerPrincipal($calendar); |
|
| 414 | - |
|
| 415 | - $calendars[$calendar['id']] = $calendar; |
|
| 416 | - } |
|
| 417 | - $result->closeCursor(); |
|
| 418 | - |
|
| 419 | - return array_values($calendars); |
|
| 420 | - } |
|
| 421 | - |
|
| 422 | - /** |
|
| 423 | - * @param $principalUri |
|
| 424 | - * @return array |
|
| 425 | - */ |
|
| 426 | - public function getUsersOwnCalendars($principalUri) { |
|
| 427 | - $principalUri = $this->convertPrincipal($principalUri, true); |
|
| 428 | - $fields = array_values($this->propertyMap); |
|
| 429 | - $fields[] = 'id'; |
|
| 430 | - $fields[] = 'uri'; |
|
| 431 | - $fields[] = 'synctoken'; |
|
| 432 | - $fields[] = 'components'; |
|
| 433 | - $fields[] = 'principaluri'; |
|
| 434 | - $fields[] = 'transparent'; |
|
| 435 | - // Making fields a comma-delimited list |
|
| 436 | - $query = $this->db->getQueryBuilder(); |
|
| 437 | - $query->select($fields)->from('calendars') |
|
| 438 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 439 | - ->orderBy('calendarorder', 'ASC'); |
|
| 440 | - $stmt = $query->executeQuery(); |
|
| 441 | - $calendars = []; |
|
| 442 | - while ($row = $stmt->fetch()) { |
|
| 443 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 444 | - $components = []; |
|
| 445 | - if ($row['components']) { |
|
| 446 | - $components = explode(',',$row['components']); |
|
| 447 | - } |
|
| 448 | - $calendar = [ |
|
| 449 | - 'id' => $row['id'], |
|
| 450 | - 'uri' => $row['uri'], |
|
| 451 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 452 | - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 453 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 454 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 455 | - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 456 | - ]; |
|
| 457 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 458 | - $calendar[$xmlName] = $row[$dbName]; |
|
| 459 | - } |
|
| 460 | - |
|
| 461 | - $this->addOwnerPrincipal($calendar); |
|
| 462 | - |
|
| 463 | - if (!isset($calendars[$calendar['id']])) { |
|
| 464 | - $calendars[$calendar['id']] = $calendar; |
|
| 465 | - } |
|
| 466 | - } |
|
| 467 | - $stmt->closeCursor(); |
|
| 468 | - return array_values($calendars); |
|
| 469 | - } |
|
| 470 | - |
|
| 471 | - |
|
| 472 | - /** |
|
| 473 | - * @param $uid |
|
| 474 | - * @return string |
|
| 475 | - */ |
|
| 476 | - private function getUserDisplayName($uid) { |
|
| 477 | - if (!isset($this->userDisplayNames[$uid])) { |
|
| 478 | - $user = $this->userManager->get($uid); |
|
| 479 | - |
|
| 480 | - if ($user instanceof IUser) { |
|
| 481 | - $this->userDisplayNames[$uid] = $user->getDisplayName(); |
|
| 482 | - } else { |
|
| 483 | - $this->userDisplayNames[$uid] = $uid; |
|
| 484 | - } |
|
| 485 | - } |
|
| 486 | - |
|
| 487 | - return $this->userDisplayNames[$uid]; |
|
| 488 | - } |
|
| 489 | - |
|
| 490 | - /** |
|
| 491 | - * @return array |
|
| 492 | - */ |
|
| 493 | - public function getPublicCalendars() { |
|
| 494 | - $fields = array_values($this->propertyMap); |
|
| 495 | - $fields[] = 'a.id'; |
|
| 496 | - $fields[] = 'a.uri'; |
|
| 497 | - $fields[] = 'a.synctoken'; |
|
| 498 | - $fields[] = 'a.components'; |
|
| 499 | - $fields[] = 'a.principaluri'; |
|
| 500 | - $fields[] = 'a.transparent'; |
|
| 501 | - $fields[] = 's.access'; |
|
| 502 | - $fields[] = 's.publicuri'; |
|
| 503 | - $calendars = []; |
|
| 504 | - $query = $this->db->getQueryBuilder(); |
|
| 505 | - $result = $query->select($fields) |
|
| 506 | - ->from('dav_shares', 's') |
|
| 507 | - ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
| 508 | - ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC))) |
|
| 509 | - ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar'))) |
|
| 510 | - ->executeQuery(); |
|
| 511 | - |
|
| 512 | - while ($row = $result->fetch()) { |
|
| 513 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 514 | - [, $name] = Uri\split($row['principaluri']); |
|
| 515 | - $row['displayname'] = $row['displayname'] . "($name)"; |
|
| 516 | - $components = []; |
|
| 517 | - if ($row['components']) { |
|
| 518 | - $components = explode(',',$row['components']); |
|
| 519 | - } |
|
| 520 | - $calendar = [ |
|
| 521 | - 'id' => $row['id'], |
|
| 522 | - 'uri' => $row['publicuri'], |
|
| 523 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 524 | - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 525 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 526 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 527 | - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 528 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint), |
|
| 529 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, |
|
| 530 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC, |
|
| 531 | - ]; |
|
| 532 | - |
|
| 533 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 534 | - $calendar[$xmlName] = $row[$dbName]; |
|
| 535 | - } |
|
| 536 | - |
|
| 537 | - $this->addOwnerPrincipal($calendar); |
|
| 538 | - |
|
| 539 | - if (!isset($calendars[$calendar['id']])) { |
|
| 540 | - $calendars[$calendar['id']] = $calendar; |
|
| 541 | - } |
|
| 542 | - } |
|
| 543 | - $result->closeCursor(); |
|
| 544 | - |
|
| 545 | - return array_values($calendars); |
|
| 546 | - } |
|
| 547 | - |
|
| 548 | - /** |
|
| 549 | - * @param string $uri |
|
| 550 | - * @return array |
|
| 551 | - * @throws NotFound |
|
| 552 | - */ |
|
| 553 | - public function getPublicCalendar($uri) { |
|
| 554 | - $fields = array_values($this->propertyMap); |
|
| 555 | - $fields[] = 'a.id'; |
|
| 556 | - $fields[] = 'a.uri'; |
|
| 557 | - $fields[] = 'a.synctoken'; |
|
| 558 | - $fields[] = 'a.components'; |
|
| 559 | - $fields[] = 'a.principaluri'; |
|
| 560 | - $fields[] = 'a.transparent'; |
|
| 561 | - $fields[] = 's.access'; |
|
| 562 | - $fields[] = 's.publicuri'; |
|
| 563 | - $query = $this->db->getQueryBuilder(); |
|
| 564 | - $result = $query->select($fields) |
|
| 565 | - ->from('dav_shares', 's') |
|
| 566 | - ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
| 567 | - ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC))) |
|
| 568 | - ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar'))) |
|
| 569 | - ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri))) |
|
| 570 | - ->executeQuery(); |
|
| 571 | - |
|
| 572 | - $row = $result->fetch(); |
|
| 573 | - |
|
| 574 | - $result->closeCursor(); |
|
| 575 | - |
|
| 576 | - if ($row === false) { |
|
| 577 | - throw new NotFound('Node with name \'' . $uri . '\' could not be found'); |
|
| 578 | - } |
|
| 579 | - |
|
| 580 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 581 | - [, $name] = Uri\split($row['principaluri']); |
|
| 582 | - $row['displayname'] = $row['displayname'] . ' ' . "($name)"; |
|
| 583 | - $components = []; |
|
| 584 | - if ($row['components']) { |
|
| 585 | - $components = explode(',',$row['components']); |
|
| 586 | - } |
|
| 587 | - $calendar = [ |
|
| 588 | - 'id' => $row['id'], |
|
| 589 | - 'uri' => $row['publicuri'], |
|
| 590 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 591 | - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 592 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 593 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 594 | - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 595 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 596 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, |
|
| 597 | - '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC, |
|
| 598 | - ]; |
|
| 599 | - |
|
| 600 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 601 | - $calendar[$xmlName] = $row[$dbName]; |
|
| 602 | - } |
|
| 603 | - |
|
| 604 | - $this->addOwnerPrincipal($calendar); |
|
| 605 | - |
|
| 606 | - return $calendar; |
|
| 607 | - } |
|
| 608 | - |
|
| 609 | - /** |
|
| 610 | - * @param string $principal |
|
| 611 | - * @param string $uri |
|
| 612 | - * @return array|null |
|
| 613 | - */ |
|
| 614 | - public function getCalendarByUri($principal, $uri) { |
|
| 615 | - $fields = array_values($this->propertyMap); |
|
| 616 | - $fields[] = 'id'; |
|
| 617 | - $fields[] = 'uri'; |
|
| 618 | - $fields[] = 'synctoken'; |
|
| 619 | - $fields[] = 'components'; |
|
| 620 | - $fields[] = 'principaluri'; |
|
| 621 | - $fields[] = 'transparent'; |
|
| 622 | - |
|
| 623 | - // Making fields a comma-delimited list |
|
| 624 | - $query = $this->db->getQueryBuilder(); |
|
| 625 | - $query->select($fields)->from('calendars') |
|
| 626 | - ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
| 627 | - ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) |
|
| 628 | - ->setMaxResults(1); |
|
| 629 | - $stmt = $query->executeQuery(); |
|
| 630 | - |
|
| 631 | - $row = $stmt->fetch(); |
|
| 632 | - $stmt->closeCursor(); |
|
| 633 | - if ($row === false) { |
|
| 634 | - return null; |
|
| 635 | - } |
|
| 636 | - |
|
| 637 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 638 | - $components = []; |
|
| 639 | - if ($row['components']) { |
|
| 640 | - $components = explode(',',$row['components']); |
|
| 641 | - } |
|
| 642 | - |
|
| 643 | - $calendar = [ |
|
| 644 | - 'id' => $row['id'], |
|
| 645 | - 'uri' => $row['uri'], |
|
| 646 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 647 | - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 648 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 649 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 650 | - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 651 | - ]; |
|
| 652 | - |
|
| 653 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 654 | - $calendar[$xmlName] = $row[$dbName]; |
|
| 655 | - } |
|
| 656 | - |
|
| 657 | - $this->addOwnerPrincipal($calendar); |
|
| 658 | - |
|
| 659 | - return $calendar; |
|
| 660 | - } |
|
| 661 | - |
|
| 662 | - /** |
|
| 663 | - * @param $calendarId |
|
| 664 | - * @return array|null |
|
| 665 | - */ |
|
| 666 | - public function getCalendarById($calendarId) { |
|
| 667 | - $fields = array_values($this->propertyMap); |
|
| 668 | - $fields[] = 'id'; |
|
| 669 | - $fields[] = 'uri'; |
|
| 670 | - $fields[] = 'synctoken'; |
|
| 671 | - $fields[] = 'components'; |
|
| 672 | - $fields[] = 'principaluri'; |
|
| 673 | - $fields[] = 'transparent'; |
|
| 674 | - |
|
| 675 | - // Making fields a comma-delimited list |
|
| 676 | - $query = $this->db->getQueryBuilder(); |
|
| 677 | - $query->select($fields)->from('calendars') |
|
| 678 | - ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))) |
|
| 679 | - ->setMaxResults(1); |
|
| 680 | - $stmt = $query->executeQuery(); |
|
| 681 | - |
|
| 682 | - $row = $stmt->fetch(); |
|
| 683 | - $stmt->closeCursor(); |
|
| 684 | - if ($row === false) { |
|
| 685 | - return null; |
|
| 686 | - } |
|
| 687 | - |
|
| 688 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 689 | - $components = []; |
|
| 690 | - if ($row['components']) { |
|
| 691 | - $components = explode(',',$row['components']); |
|
| 692 | - } |
|
| 693 | - |
|
| 694 | - $calendar = [ |
|
| 695 | - 'id' => $row['id'], |
|
| 696 | - 'uri' => $row['uri'], |
|
| 697 | - 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 698 | - '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 699 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 700 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 701 | - '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 702 | - ]; |
|
| 703 | - |
|
| 704 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 705 | - $calendar[$xmlName] = $row[$dbName]; |
|
| 706 | - } |
|
| 707 | - |
|
| 708 | - $this->addOwnerPrincipal($calendar); |
|
| 709 | - |
|
| 710 | - return $calendar; |
|
| 711 | - } |
|
| 712 | - |
|
| 713 | - /** |
|
| 714 | - * @param $subscriptionId |
|
| 715 | - */ |
|
| 716 | - public function getSubscriptionById($subscriptionId) { |
|
| 717 | - $fields = array_values($this->subscriptionPropertyMap); |
|
| 718 | - $fields[] = 'id'; |
|
| 719 | - $fields[] = 'uri'; |
|
| 720 | - $fields[] = 'source'; |
|
| 721 | - $fields[] = 'synctoken'; |
|
| 722 | - $fields[] = 'principaluri'; |
|
| 723 | - $fields[] = 'lastmodified'; |
|
| 724 | - |
|
| 725 | - $query = $this->db->getQueryBuilder(); |
|
| 726 | - $query->select($fields) |
|
| 727 | - ->from('calendarsubscriptions') |
|
| 728 | - ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) |
|
| 729 | - ->orderBy('calendarorder', 'asc'); |
|
| 730 | - $stmt = $query->executeQuery(); |
|
| 731 | - |
|
| 732 | - $row = $stmt->fetch(); |
|
| 733 | - $stmt->closeCursor(); |
|
| 734 | - if ($row === false) { |
|
| 735 | - return null; |
|
| 736 | - } |
|
| 737 | - |
|
| 738 | - $row['principaluri'] = (string) $row['principaluri']; |
|
| 739 | - $subscription = [ |
|
| 740 | - 'id' => $row['id'], |
|
| 741 | - 'uri' => $row['uri'], |
|
| 742 | - 'principaluri' => $row['principaluri'], |
|
| 743 | - 'source' => $row['source'], |
|
| 744 | - 'lastmodified' => $row['lastmodified'], |
|
| 745 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), |
|
| 746 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 747 | - ]; |
|
| 748 | - |
|
| 749 | - foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { |
|
| 750 | - if (!is_null($row[$dbName])) { |
|
| 751 | - $subscription[$xmlName] = $row[$dbName]; |
|
| 752 | - } |
|
| 753 | - } |
|
| 754 | - |
|
| 755 | - return $subscription; |
|
| 756 | - } |
|
| 757 | - |
|
| 758 | - /** |
|
| 759 | - * Creates a new calendar for a principal. |
|
| 760 | - * |
|
| 761 | - * If the creation was a success, an id must be returned that can be used to reference |
|
| 762 | - * this calendar in other methods, such as updateCalendar. |
|
| 763 | - * |
|
| 764 | - * @param string $principalUri |
|
| 765 | - * @param string $calendarUri |
|
| 766 | - * @param array $properties |
|
| 767 | - * @return int |
|
| 768 | - */ |
|
| 769 | - public function createCalendar($principalUri, $calendarUri, array $properties) { |
|
| 770 | - $values = [ |
|
| 771 | - 'principaluri' => $this->convertPrincipal($principalUri, true), |
|
| 772 | - 'uri' => $calendarUri, |
|
| 773 | - 'synctoken' => 1, |
|
| 774 | - 'transparent' => 0, |
|
| 775 | - 'components' => 'VEVENT,VTODO', |
|
| 776 | - 'displayname' => $calendarUri |
|
| 777 | - ]; |
|
| 778 | - |
|
| 779 | - // Default value |
|
| 780 | - $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; |
|
| 781 | - if (isset($properties[$sccs])) { |
|
| 782 | - if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) { |
|
| 783 | - throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet'); |
|
| 784 | - } |
|
| 785 | - $values['components'] = implode(',',$properties[$sccs]->getValue()); |
|
| 786 | - } elseif (isset($properties['components'])) { |
|
| 787 | - // Allow to provide components internally without having |
|
| 788 | - // to create a SupportedCalendarComponentSet object |
|
| 789 | - $values['components'] = $properties['components']; |
|
| 790 | - } |
|
| 791 | - |
|
| 792 | - $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; |
|
| 793 | - if (isset($properties[$transp])) { |
|
| 794 | - $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent'); |
|
| 795 | - } |
|
| 796 | - |
|
| 797 | - foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 798 | - if (isset($properties[$xmlName])) { |
|
| 799 | - $values[$dbName] = $properties[$xmlName]; |
|
| 800 | - } |
|
| 801 | - } |
|
| 802 | - |
|
| 803 | - $query = $this->db->getQueryBuilder(); |
|
| 804 | - $query->insert('calendars'); |
|
| 805 | - foreach ($values as $column => $value) { |
|
| 806 | - $query->setValue($column, $query->createNamedParameter($value)); |
|
| 807 | - } |
|
| 808 | - $query->executeStatement(); |
|
| 809 | - $calendarId = $query->getLastInsertId(); |
|
| 810 | - |
|
| 811 | - $calendarData = $this->getCalendarById($calendarId); |
|
| 812 | - $this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData)); |
|
| 813 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent( |
|
| 814 | - '\OCA\DAV\CalDAV\CalDavBackend::createCalendar', |
|
| 815 | - [ |
|
| 816 | - 'calendarId' => $calendarId, |
|
| 817 | - 'calendarData' => $calendarData, |
|
| 818 | - ])); |
|
| 819 | - |
|
| 820 | - return $calendarId; |
|
| 821 | - } |
|
| 822 | - |
|
| 823 | - /** |
|
| 824 | - * Updates properties for a calendar. |
|
| 825 | - * |
|
| 826 | - * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
| 827 | - * To do the actual updates, you must tell this object which properties |
|
| 828 | - * you're going to process with the handle() method. |
|
| 829 | - * |
|
| 830 | - * Calling the handle method is like telling the PropPatch object "I |
|
| 831 | - * promise I can handle updating this property". |
|
| 832 | - * |
|
| 833 | - * Read the PropPatch documentation for more info and examples. |
|
| 834 | - * |
|
| 835 | - * @param mixed $calendarId |
|
| 836 | - * @param PropPatch $propPatch |
|
| 837 | - * @return void |
|
| 838 | - */ |
|
| 839 | - public function updateCalendar($calendarId, PropPatch $propPatch) { |
|
| 840 | - $supportedProperties = array_keys($this->propertyMap); |
|
| 841 | - $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; |
|
| 842 | - |
|
| 843 | - $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) { |
|
| 844 | - $newValues = []; |
|
| 845 | - foreach ($mutations as $propertyName => $propertyValue) { |
|
| 846 | - switch ($propertyName) { |
|
| 847 | - case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp': |
|
| 848 | - $fieldName = 'transparent'; |
|
| 849 | - $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent'); |
|
| 850 | - break; |
|
| 851 | - default: |
|
| 852 | - $fieldName = $this->propertyMap[$propertyName]; |
|
| 853 | - $newValues[$fieldName] = $propertyValue; |
|
| 854 | - break; |
|
| 855 | - } |
|
| 856 | - } |
|
| 857 | - $query = $this->db->getQueryBuilder(); |
|
| 858 | - $query->update('calendars'); |
|
| 859 | - foreach ($newValues as $fieldName => $value) { |
|
| 860 | - $query->set($fieldName, $query->createNamedParameter($value)); |
|
| 861 | - } |
|
| 862 | - $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); |
|
| 863 | - $query->executeStatement(); |
|
| 864 | - |
|
| 865 | - $this->addChange($calendarId, "", 2); |
|
| 866 | - |
|
| 867 | - $calendarData = $this->getCalendarById($calendarId); |
|
| 868 | - $shares = $this->getShares($calendarId); |
|
| 869 | - $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations)); |
|
| 870 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent( |
|
| 871 | - '\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', |
|
| 872 | - [ |
|
| 873 | - 'calendarId' => $calendarId, |
|
| 874 | - 'calendarData' => $calendarData, |
|
| 875 | - 'shares' => $shares, |
|
| 876 | - 'propertyMutations' => $mutations, |
|
| 877 | - ])); |
|
| 878 | - |
|
| 879 | - return true; |
|
| 880 | - }); |
|
| 881 | - } |
|
| 882 | - |
|
| 883 | - /** |
|
| 884 | - * Delete a calendar and all it's objects |
|
| 885 | - * |
|
| 886 | - * @param mixed $calendarId |
|
| 887 | - * @return void |
|
| 888 | - */ |
|
| 889 | - public function deleteCalendar($calendarId) { |
|
| 890 | - $calendarData = $this->getCalendarById($calendarId); |
|
| 891 | - $shares = $this->getShares($calendarId); |
|
| 892 | - |
|
| 893 | - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?'); |
|
| 894 | - $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]); |
|
| 895 | - |
|
| 896 | - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?'); |
|
| 897 | - $stmt->execute([$calendarId]); |
|
| 898 | - |
|
| 899 | - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?'); |
|
| 900 | - $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]); |
|
| 901 | - |
|
| 902 | - $this->calendarSharingBackend->deleteAllShares($calendarId); |
|
| 903 | - |
|
| 904 | - $query = $this->db->getQueryBuilder(); |
|
| 905 | - $query->delete($this->dbObjectPropertiesTable) |
|
| 906 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 907 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))) |
|
| 908 | - ->executeStatement(); |
|
| 909 | - |
|
| 910 | - // Only dispatch if we actually deleted anything |
|
| 911 | - if ($calendarData) { |
|
| 912 | - $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares)); |
|
| 913 | - } |
|
| 914 | - } |
|
| 915 | - |
|
| 916 | - /** |
|
| 917 | - * Delete all of an user's shares |
|
| 918 | - * |
|
| 919 | - * @param string $principaluri |
|
| 920 | - * @return void |
|
| 921 | - */ |
|
| 922 | - public function deleteAllSharesByUser($principaluri) { |
|
| 923 | - $this->calendarSharingBackend->deleteAllSharesByUser($principaluri); |
|
| 924 | - } |
|
| 925 | - |
|
| 926 | - /** |
|
| 927 | - * Returns all calendar objects within a calendar. |
|
| 928 | - * |
|
| 929 | - * Every item contains an array with the following keys: |
|
| 930 | - * * calendardata - The iCalendar-compatible calendar data |
|
| 931 | - * * uri - a unique key which will be used to construct the uri. This can |
|
| 932 | - * be any arbitrary string, but making sure it ends with '.ics' is a |
|
| 933 | - * good idea. This is only the basename, or filename, not the full |
|
| 934 | - * path. |
|
| 935 | - * * lastmodified - a timestamp of the last modification time |
|
| 936 | - * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: |
|
| 937 | - * '"abcdef"') |
|
| 938 | - * * size - The size of the calendar objects, in bytes. |
|
| 939 | - * * component - optional, a string containing the type of object, such |
|
| 940 | - * as 'vevent' or 'vtodo'. If specified, this will be used to populate |
|
| 941 | - * the Content-Type header. |
|
| 942 | - * |
|
| 943 | - * Note that the etag is optional, but it's highly encouraged to return for |
|
| 944 | - * speed reasons. |
|
| 945 | - * |
|
| 946 | - * The calendardata is also optional. If it's not returned |
|
| 947 | - * 'getCalendarObject' will be called later, which *is* expected to return |
|
| 948 | - * calendardata. |
|
| 949 | - * |
|
| 950 | - * If neither etag or size are specified, the calendardata will be |
|
| 951 | - * used/fetched to determine these numbers. If both are specified the |
|
| 952 | - * amount of times this is needed is reduced by a great degree. |
|
| 953 | - * |
|
| 954 | - * @param mixed $calendarId |
|
| 955 | - * @param int $calendarType |
|
| 956 | - * @return array |
|
| 957 | - */ |
|
| 958 | - public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array { |
|
| 959 | - $query = $this->db->getQueryBuilder(); |
|
| 960 | - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification']) |
|
| 961 | - ->from('calendarobjects') |
|
| 962 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 963 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 964 | - $stmt = $query->executeQuery(); |
|
| 965 | - |
|
| 966 | - $result = []; |
|
| 967 | - foreach ($stmt->fetchAll() as $row) { |
|
| 968 | - $result[] = [ |
|
| 969 | - 'id' => $row['id'], |
|
| 970 | - 'uri' => $row['uri'], |
|
| 971 | - 'lastmodified' => $row['lastmodified'], |
|
| 972 | - 'etag' => '"' . $row['etag'] . '"', |
|
| 973 | - 'calendarid' => $row['calendarid'], |
|
| 974 | - 'size' => (int)$row['size'], |
|
| 975 | - 'component' => strtolower($row['componenttype']), |
|
| 976 | - 'classification' => (int)$row['classification'] |
|
| 977 | - ]; |
|
| 978 | - } |
|
| 979 | - $stmt->closeCursor(); |
|
| 980 | - |
|
| 981 | - return $result; |
|
| 982 | - } |
|
| 983 | - |
|
| 984 | - /** |
|
| 985 | - * Returns information from a single calendar object, based on it's object |
|
| 986 | - * uri. |
|
| 987 | - * |
|
| 988 | - * The object uri is only the basename, or filename and not a full path. |
|
| 989 | - * |
|
| 990 | - * The returned array must have the same keys as getCalendarObjects. The |
|
| 991 | - * 'calendardata' object is required here though, while it's not required |
|
| 992 | - * for getCalendarObjects. |
|
| 993 | - * |
|
| 994 | - * This method must return null if the object did not exist. |
|
| 995 | - * |
|
| 996 | - * @param mixed $calendarId |
|
| 997 | - * @param string $objectUri |
|
| 998 | - * @param int $calendarType |
|
| 999 | - * @return array|null |
|
| 1000 | - */ |
|
| 1001 | - public function getCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1002 | - $query = $this->db->getQueryBuilder(); |
|
| 1003 | - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification']) |
|
| 1004 | - ->from('calendarobjects') |
|
| 1005 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1006 | - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 1007 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 1008 | - $stmt = $query->executeQuery(); |
|
| 1009 | - $row = $stmt->fetch(); |
|
| 1010 | - $stmt->closeCursor(); |
|
| 1011 | - |
|
| 1012 | - if (!$row) { |
|
| 1013 | - return null; |
|
| 1014 | - } |
|
| 1015 | - |
|
| 1016 | - return [ |
|
| 1017 | - 'id' => $row['id'], |
|
| 1018 | - 'uri' => $row['uri'], |
|
| 1019 | - 'lastmodified' => $row['lastmodified'], |
|
| 1020 | - 'etag' => '"' . $row['etag'] . '"', |
|
| 1021 | - 'calendarid' => $row['calendarid'], |
|
| 1022 | - 'size' => (int)$row['size'], |
|
| 1023 | - 'calendardata' => $this->readBlob($row['calendardata']), |
|
| 1024 | - 'component' => strtolower($row['componenttype']), |
|
| 1025 | - 'classification' => (int)$row['classification'] |
|
| 1026 | - ]; |
|
| 1027 | - } |
|
| 1028 | - |
|
| 1029 | - /** |
|
| 1030 | - * Returns a list of calendar objects. |
|
| 1031 | - * |
|
| 1032 | - * This method should work identical to getCalendarObject, but instead |
|
| 1033 | - * return all the calendar objects in the list as an array. |
|
| 1034 | - * |
|
| 1035 | - * If the backend supports this, it may allow for some speed-ups. |
|
| 1036 | - * |
|
| 1037 | - * @param mixed $calendarId |
|
| 1038 | - * @param string[] $uris |
|
| 1039 | - * @param int $calendarType |
|
| 1040 | - * @return array |
|
| 1041 | - */ |
|
| 1042 | - public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array { |
|
| 1043 | - if (empty($uris)) { |
|
| 1044 | - return []; |
|
| 1045 | - } |
|
| 1046 | - |
|
| 1047 | - $chunks = array_chunk($uris, 100); |
|
| 1048 | - $objects = []; |
|
| 1049 | - |
|
| 1050 | - $query = $this->db->getQueryBuilder(); |
|
| 1051 | - $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification']) |
|
| 1052 | - ->from('calendarobjects') |
|
| 1053 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1054 | - ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) |
|
| 1055 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 1056 | - |
|
| 1057 | - foreach ($chunks as $uris) { |
|
| 1058 | - $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); |
|
| 1059 | - $result = $query->executeQuery(); |
|
| 1060 | - |
|
| 1061 | - while ($row = $result->fetch()) { |
|
| 1062 | - $objects[] = [ |
|
| 1063 | - 'id' => $row['id'], |
|
| 1064 | - 'uri' => $row['uri'], |
|
| 1065 | - 'lastmodified' => $row['lastmodified'], |
|
| 1066 | - 'etag' => '"' . $row['etag'] . '"', |
|
| 1067 | - 'calendarid' => $row['calendarid'], |
|
| 1068 | - 'size' => (int)$row['size'], |
|
| 1069 | - 'calendardata' => $this->readBlob($row['calendardata']), |
|
| 1070 | - 'component' => strtolower($row['componenttype']), |
|
| 1071 | - 'classification' => (int)$row['classification'] |
|
| 1072 | - ]; |
|
| 1073 | - } |
|
| 1074 | - $result->closeCursor(); |
|
| 1075 | - } |
|
| 1076 | - |
|
| 1077 | - return $objects; |
|
| 1078 | - } |
|
| 1079 | - |
|
| 1080 | - /** |
|
| 1081 | - * Creates a new calendar object. |
|
| 1082 | - * |
|
| 1083 | - * The object uri is only the basename, or filename and not a full path. |
|
| 1084 | - * |
|
| 1085 | - * It is possible return an etag from this function, which will be used in |
|
| 1086 | - * the response to this PUT request. Note that the ETag must be surrounded |
|
| 1087 | - * by double-quotes. |
|
| 1088 | - * |
|
| 1089 | - * However, you should only really return this ETag if you don't mangle the |
|
| 1090 | - * calendar-data. If the result of a subsequent GET to this object is not |
|
| 1091 | - * the exact same as this request body, you should omit the ETag. |
|
| 1092 | - * |
|
| 1093 | - * @param mixed $calendarId |
|
| 1094 | - * @param string $objectUri |
|
| 1095 | - * @param string $calendarData |
|
| 1096 | - * @param int $calendarType |
|
| 1097 | - * @return string |
|
| 1098 | - */ |
|
| 1099 | - public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1100 | - $extraData = $this->getDenormalizedData($calendarData); |
|
| 1101 | - |
|
| 1102 | - $q = $this->db->getQueryBuilder(); |
|
| 1103 | - $q->select($q->func()->count('*')) |
|
| 1104 | - ->from('calendarobjects') |
|
| 1105 | - ->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId))) |
|
| 1106 | - ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid']))) |
|
| 1107 | - ->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType))); |
|
| 1108 | - |
|
| 1109 | - $result = $q->executeQuery(); |
|
| 1110 | - $count = (int) $result->fetchOne(); |
|
| 1111 | - $result->closeCursor(); |
|
| 1112 | - |
|
| 1113 | - if ($count !== 0) { |
|
| 1114 | - throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.'); |
|
| 1115 | - } |
|
| 1116 | - |
|
| 1117 | - $query = $this->db->getQueryBuilder(); |
|
| 1118 | - $query->insert('calendarobjects') |
|
| 1119 | - ->values([ |
|
| 1120 | - 'calendarid' => $query->createNamedParameter($calendarId), |
|
| 1121 | - 'uri' => $query->createNamedParameter($objectUri), |
|
| 1122 | - 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB), |
|
| 1123 | - 'lastmodified' => $query->createNamedParameter(time()), |
|
| 1124 | - 'etag' => $query->createNamedParameter($extraData['etag']), |
|
| 1125 | - 'size' => $query->createNamedParameter($extraData['size']), |
|
| 1126 | - 'componenttype' => $query->createNamedParameter($extraData['componentType']), |
|
| 1127 | - 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']), |
|
| 1128 | - 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']), |
|
| 1129 | - 'classification' => $query->createNamedParameter($extraData['classification']), |
|
| 1130 | - 'uid' => $query->createNamedParameter($extraData['uid']), |
|
| 1131 | - 'calendartype' => $query->createNamedParameter($calendarType), |
|
| 1132 | - ]) |
|
| 1133 | - ->executeStatement(); |
|
| 1134 | - |
|
| 1135 | - $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); |
|
| 1136 | - $this->addChange($calendarId, $objectUri, 1, $calendarType); |
|
| 1137 | - |
|
| 1138 | - $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); |
|
| 1139 | - if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { |
|
| 1140 | - $calendarRow = $this->getCalendarById($calendarId); |
|
| 1141 | - $shares = $this->getShares($calendarId); |
|
| 1142 | - |
|
| 1143 | - $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow)); |
|
| 1144 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent( |
|
| 1145 | - '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', |
|
| 1146 | - [ |
|
| 1147 | - 'calendarId' => $calendarId, |
|
| 1148 | - 'calendarData' => $calendarRow, |
|
| 1149 | - 'shares' => $shares, |
|
| 1150 | - 'objectData' => $objectRow, |
|
| 1151 | - ] |
|
| 1152 | - )); |
|
| 1153 | - } else { |
|
| 1154 | - $subscriptionRow = $this->getSubscriptionById($calendarId); |
|
| 1155 | - |
|
| 1156 | - $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow)); |
|
| 1157 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent( |
|
| 1158 | - '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', |
|
| 1159 | - [ |
|
| 1160 | - 'subscriptionId' => $calendarId, |
|
| 1161 | - 'calendarData' => $subscriptionRow, |
|
| 1162 | - 'shares' => [], |
|
| 1163 | - 'objectData' => $objectRow, |
|
| 1164 | - ] |
|
| 1165 | - )); |
|
| 1166 | - } |
|
| 1167 | - |
|
| 1168 | - return '"' . $extraData['etag'] . '"'; |
|
| 1169 | - } |
|
| 1170 | - |
|
| 1171 | - /** |
|
| 1172 | - * Updates an existing calendarobject, based on it's uri. |
|
| 1173 | - * |
|
| 1174 | - * The object uri is only the basename, or filename and not a full path. |
|
| 1175 | - * |
|
| 1176 | - * It is possible return an etag from this function, which will be used in |
|
| 1177 | - * the response to this PUT request. Note that the ETag must be surrounded |
|
| 1178 | - * by double-quotes. |
|
| 1179 | - * |
|
| 1180 | - * However, you should only really return this ETag if you don't mangle the |
|
| 1181 | - * calendar-data. If the result of a subsequent GET to this object is not |
|
| 1182 | - * the exact same as this request body, you should omit the ETag. |
|
| 1183 | - * |
|
| 1184 | - * @param mixed $calendarId |
|
| 1185 | - * @param string $objectUri |
|
| 1186 | - * @param string $calendarData |
|
| 1187 | - * @param int $calendarType |
|
| 1188 | - * @return string |
|
| 1189 | - */ |
|
| 1190 | - public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1191 | - $extraData = $this->getDenormalizedData($calendarData); |
|
| 1192 | - $query = $this->db->getQueryBuilder(); |
|
| 1193 | - $query->update('calendarobjects') |
|
| 1194 | - ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) |
|
| 1195 | - ->set('lastmodified', $query->createNamedParameter(time())) |
|
| 1196 | - ->set('etag', $query->createNamedParameter($extraData['etag'])) |
|
| 1197 | - ->set('size', $query->createNamedParameter($extraData['size'])) |
|
| 1198 | - ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) |
|
| 1199 | - ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) |
|
| 1200 | - ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) |
|
| 1201 | - ->set('classification', $query->createNamedParameter($extraData['classification'])) |
|
| 1202 | - ->set('uid', $query->createNamedParameter($extraData['uid'])) |
|
| 1203 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1204 | - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 1205 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) |
|
| 1206 | - ->executeStatement(); |
|
| 1207 | - |
|
| 1208 | - $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); |
|
| 1209 | - $this->addChange($calendarId, $objectUri, 2, $calendarType); |
|
| 1210 | - |
|
| 1211 | - $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); |
|
| 1212 | - if (is_array($objectRow)) { |
|
| 1213 | - if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { |
|
| 1214 | - $calendarRow = $this->getCalendarById($calendarId); |
|
| 1215 | - $shares = $this->getShares($calendarId); |
|
| 1216 | - |
|
| 1217 | - $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow)); |
|
| 1218 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent( |
|
| 1219 | - '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', |
|
| 1220 | - [ |
|
| 1221 | - 'calendarId' => $calendarId, |
|
| 1222 | - 'calendarData' => $calendarRow, |
|
| 1223 | - 'shares' => $shares, |
|
| 1224 | - 'objectData' => $objectRow, |
|
| 1225 | - ] |
|
| 1226 | - )); |
|
| 1227 | - } else { |
|
| 1228 | - $subscriptionRow = $this->getSubscriptionById($calendarId); |
|
| 1229 | - |
|
| 1230 | - $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow)); |
|
| 1231 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent( |
|
| 1232 | - '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', |
|
| 1233 | - [ |
|
| 1234 | - 'subscriptionId' => $calendarId, |
|
| 1235 | - 'calendarData' => $subscriptionRow, |
|
| 1236 | - 'shares' => [], |
|
| 1237 | - 'objectData' => $objectRow, |
|
| 1238 | - ] |
|
| 1239 | - )); |
|
| 1240 | - } |
|
| 1241 | - } |
|
| 1242 | - |
|
| 1243 | - return '"' . $extraData['etag'] . '"'; |
|
| 1244 | - } |
|
| 1245 | - |
|
| 1246 | - /** |
|
| 1247 | - * @param int $calendarObjectId |
|
| 1248 | - * @param int $classification |
|
| 1249 | - */ |
|
| 1250 | - public function setClassification($calendarObjectId, $classification) { |
|
| 1251 | - if (!in_array($classification, [ |
|
| 1252 | - self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL |
|
| 1253 | - ])) { |
|
| 1254 | - throw new \InvalidArgumentException(); |
|
| 1255 | - } |
|
| 1256 | - $query = $this->db->getQueryBuilder(); |
|
| 1257 | - $query->update('calendarobjects') |
|
| 1258 | - ->set('classification', $query->createNamedParameter($classification)) |
|
| 1259 | - ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId))) |
|
| 1260 | - ->executeStatement(); |
|
| 1261 | - } |
|
| 1262 | - |
|
| 1263 | - /** |
|
| 1264 | - * Deletes an existing calendar object. |
|
| 1265 | - * |
|
| 1266 | - * The object uri is only the basename, or filename and not a full path. |
|
| 1267 | - * |
|
| 1268 | - * @param mixed $calendarId |
|
| 1269 | - * @param string $objectUri |
|
| 1270 | - * @param int $calendarType |
|
| 1271 | - * @return void |
|
| 1272 | - */ |
|
| 1273 | - public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1274 | - $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); |
|
| 1275 | - if (is_array($data)) { |
|
| 1276 | - if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { |
|
| 1277 | - $calendarRow = $this->getCalendarById($calendarId); |
|
| 1278 | - $shares = $this->getShares($calendarId); |
|
| 1279 | - |
|
| 1280 | - $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data)); |
|
| 1281 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent( |
|
| 1282 | - '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', |
|
| 1283 | - [ |
|
| 1284 | - 'calendarId' => $calendarId, |
|
| 1285 | - 'calendarData' => $calendarRow, |
|
| 1286 | - 'shares' => $shares, |
|
| 1287 | - 'objectData' => $data, |
|
| 1288 | - ] |
|
| 1289 | - )); |
|
| 1290 | - } else { |
|
| 1291 | - $subscriptionRow = $this->getSubscriptionById($calendarId); |
|
| 1292 | - |
|
| 1293 | - $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data)); |
|
| 1294 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent( |
|
| 1295 | - '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', |
|
| 1296 | - [ |
|
| 1297 | - 'subscriptionId' => $calendarId, |
|
| 1298 | - 'calendarData' => $subscriptionRow, |
|
| 1299 | - 'shares' => [], |
|
| 1300 | - 'objectData' => $data, |
|
| 1301 | - ] |
|
| 1302 | - )); |
|
| 1303 | - } |
|
| 1304 | - } |
|
| 1305 | - |
|
| 1306 | - $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?'); |
|
| 1307 | - $stmt->execute([$calendarId, $objectUri, $calendarType]); |
|
| 1308 | - |
|
| 1309 | - if (is_array($data)) { |
|
| 1310 | - $this->purgeProperties($calendarId, $data['id'], $calendarType); |
|
| 1311 | - } |
|
| 1312 | - |
|
| 1313 | - $this->addChange($calendarId, $objectUri, 3, $calendarType); |
|
| 1314 | - } |
|
| 1315 | - |
|
| 1316 | - /** |
|
| 1317 | - * Performs a calendar-query on the contents of this calendar. |
|
| 1318 | - * |
|
| 1319 | - * The calendar-query is defined in RFC4791 : CalDAV. Using the |
|
| 1320 | - * calendar-query it is possible for a client to request a specific set of |
|
| 1321 | - * object, based on contents of iCalendar properties, date-ranges and |
|
| 1322 | - * iCalendar component types (VTODO, VEVENT). |
|
| 1323 | - * |
|
| 1324 | - * This method should just return a list of (relative) urls that match this |
|
| 1325 | - * query. |
|
| 1326 | - * |
|
| 1327 | - * The list of filters are specified as an array. The exact array is |
|
| 1328 | - * documented by Sabre\CalDAV\CalendarQueryParser. |
|
| 1329 | - * |
|
| 1330 | - * Note that it is extremely likely that getCalendarObject for every path |
|
| 1331 | - * returned from this method will be called almost immediately after. You |
|
| 1332 | - * may want to anticipate this to speed up these requests. |
|
| 1333 | - * |
|
| 1334 | - * This method provides a default implementation, which parses *all* the |
|
| 1335 | - * iCalendar objects in the specified calendar. |
|
| 1336 | - * |
|
| 1337 | - * This default may well be good enough for personal use, and calendars |
|
| 1338 | - * that aren't very large. But if you anticipate high usage, big calendars |
|
| 1339 | - * or high loads, you are strongly advised to optimize certain paths. |
|
| 1340 | - * |
|
| 1341 | - * The best way to do so is override this method and to optimize |
|
| 1342 | - * specifically for 'common filters'. |
|
| 1343 | - * |
|
| 1344 | - * Requests that are extremely common are: |
|
| 1345 | - * * requests for just VEVENTS |
|
| 1346 | - * * requests for just VTODO |
|
| 1347 | - * * requests with a time-range-filter on either VEVENT or VTODO. |
|
| 1348 | - * |
|
| 1349 | - * ..and combinations of these requests. It may not be worth it to try to |
|
| 1350 | - * handle every possible situation and just rely on the (relatively |
|
| 1351 | - * easy to use) CalendarQueryValidator to handle the rest. |
|
| 1352 | - * |
|
| 1353 | - * Note that especially time-range-filters may be difficult to parse. A |
|
| 1354 | - * time-range filter specified on a VEVENT must for instance also handle |
|
| 1355 | - * recurrence rules correctly. |
|
| 1356 | - * A good example of how to interprete all these filters can also simply |
|
| 1357 | - * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct |
|
| 1358 | - * as possible, so it gives you a good idea on what type of stuff you need |
|
| 1359 | - * to think of. |
|
| 1360 | - * |
|
| 1361 | - * @param mixed $calendarId |
|
| 1362 | - * @param array $filters |
|
| 1363 | - * @param int $calendarType |
|
| 1364 | - * @return array |
|
| 1365 | - */ |
|
| 1366 | - public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array { |
|
| 1367 | - $componentType = null; |
|
| 1368 | - $requirePostFilter = true; |
|
| 1369 | - $timeRange = null; |
|
| 1370 | - |
|
| 1371 | - // if no filters were specified, we don't need to filter after a query |
|
| 1372 | - if (!$filters['prop-filters'] && !$filters['comp-filters']) { |
|
| 1373 | - $requirePostFilter = false; |
|
| 1374 | - } |
|
| 1375 | - |
|
| 1376 | - // Figuring out if there's a component filter |
|
| 1377 | - if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { |
|
| 1378 | - $componentType = $filters['comp-filters'][0]['name']; |
|
| 1379 | - |
|
| 1380 | - // Checking if we need post-filters |
|
| 1381 | - if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) { |
|
| 1382 | - $requirePostFilter = false; |
|
| 1383 | - } |
|
| 1384 | - // There was a time-range filter |
|
| 1385 | - if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) { |
|
| 1386 | - $timeRange = $filters['comp-filters'][0]['time-range']; |
|
| 1387 | - |
|
| 1388 | - // If start time OR the end time is not specified, we can do a |
|
| 1389 | - // 100% accurate mysql query. |
|
| 1390 | - if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) { |
|
| 1391 | - $requirePostFilter = false; |
|
| 1392 | - } |
|
| 1393 | - } |
|
| 1394 | - } |
|
| 1395 | - $columns = ['uri']; |
|
| 1396 | - if ($requirePostFilter) { |
|
| 1397 | - $columns = ['uri', 'calendardata']; |
|
| 1398 | - } |
|
| 1399 | - $query = $this->db->getQueryBuilder(); |
|
| 1400 | - $query->select($columns) |
|
| 1401 | - ->from('calendarobjects') |
|
| 1402 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1403 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 1404 | - |
|
| 1405 | - if ($componentType) { |
|
| 1406 | - $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType))); |
|
| 1407 | - } |
|
| 1408 | - |
|
| 1409 | - if ($timeRange && $timeRange['start']) { |
|
| 1410 | - $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp()))); |
|
| 1411 | - } |
|
| 1412 | - if ($timeRange && $timeRange['end']) { |
|
| 1413 | - $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp()))); |
|
| 1414 | - } |
|
| 1415 | - |
|
| 1416 | - $stmt = $query->executeQuery(); |
|
| 1417 | - |
|
| 1418 | - $result = []; |
|
| 1419 | - while ($row = $stmt->fetch()) { |
|
| 1420 | - if ($requirePostFilter) { |
|
| 1421 | - // validateFilterForObject will parse the calendar data |
|
| 1422 | - // catch parsing errors |
|
| 1423 | - try { |
|
| 1424 | - $matches = $this->validateFilterForObject($row, $filters); |
|
| 1425 | - } catch (ParseException $ex) { |
|
| 1426 | - $this->logger->logException($ex, [ |
|
| 1427 | - 'app' => 'dav', |
|
| 1428 | - 'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'] |
|
| 1429 | - ]); |
|
| 1430 | - continue; |
|
| 1431 | - } catch (InvalidDataException $ex) { |
|
| 1432 | - $this->logger->logException($ex, [ |
|
| 1433 | - 'app' => 'dav', |
|
| 1434 | - 'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'] |
|
| 1435 | - ]); |
|
| 1436 | - continue; |
|
| 1437 | - } |
|
| 1438 | - |
|
| 1439 | - if (!$matches) { |
|
| 1440 | - continue; |
|
| 1441 | - } |
|
| 1442 | - } |
|
| 1443 | - $result[] = $row['uri']; |
|
| 1444 | - } |
|
| 1445 | - |
|
| 1446 | - return $result; |
|
| 1447 | - } |
|
| 1448 | - |
|
| 1449 | - /** |
|
| 1450 | - * custom Nextcloud search extension for CalDAV |
|
| 1451 | - * |
|
| 1452 | - * TODO - this should optionally cover cached calendar objects as well |
|
| 1453 | - * |
|
| 1454 | - * @param string $principalUri |
|
| 1455 | - * @param array $filters |
|
| 1456 | - * @param integer|null $limit |
|
| 1457 | - * @param integer|null $offset |
|
| 1458 | - * @return array |
|
| 1459 | - */ |
|
| 1460 | - public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) { |
|
| 1461 | - $calendars = $this->getCalendarsForUser($principalUri); |
|
| 1462 | - $ownCalendars = []; |
|
| 1463 | - $sharedCalendars = []; |
|
| 1464 | - |
|
| 1465 | - $uriMapper = []; |
|
| 1466 | - |
|
| 1467 | - foreach ($calendars as $calendar) { |
|
| 1468 | - if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) { |
|
| 1469 | - $ownCalendars[] = $calendar['id']; |
|
| 1470 | - } else { |
|
| 1471 | - $sharedCalendars[] = $calendar['id']; |
|
| 1472 | - } |
|
| 1473 | - $uriMapper[$calendar['id']] = $calendar['uri']; |
|
| 1474 | - } |
|
| 1475 | - if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) { |
|
| 1476 | - return []; |
|
| 1477 | - } |
|
| 1478 | - |
|
| 1479 | - $query = $this->db->getQueryBuilder(); |
|
| 1480 | - // Calendar id expressions |
|
| 1481 | - $calendarExpressions = []; |
|
| 1482 | - foreach ($ownCalendars as $id) { |
|
| 1483 | - $calendarExpressions[] = $query->expr()->andX( |
|
| 1484 | - $query->expr()->eq('c.calendarid', |
|
| 1485 | - $query->createNamedParameter($id)), |
|
| 1486 | - $query->expr()->eq('c.calendartype', |
|
| 1487 | - $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1488 | - } |
|
| 1489 | - foreach ($sharedCalendars as $id) { |
|
| 1490 | - $calendarExpressions[] = $query->expr()->andX( |
|
| 1491 | - $query->expr()->eq('c.calendarid', |
|
| 1492 | - $query->createNamedParameter($id)), |
|
| 1493 | - $query->expr()->eq('c.classification', |
|
| 1494 | - $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)), |
|
| 1495 | - $query->expr()->eq('c.calendartype', |
|
| 1496 | - $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1497 | - } |
|
| 1498 | - |
|
| 1499 | - if (count($calendarExpressions) === 1) { |
|
| 1500 | - $calExpr = $calendarExpressions[0]; |
|
| 1501 | - } else { |
|
| 1502 | - $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions); |
|
| 1503 | - } |
|
| 1504 | - |
|
| 1505 | - // Component expressions |
|
| 1506 | - $compExpressions = []; |
|
| 1507 | - foreach ($filters['comps'] as $comp) { |
|
| 1508 | - $compExpressions[] = $query->expr() |
|
| 1509 | - ->eq('c.componenttype', $query->createNamedParameter($comp)); |
|
| 1510 | - } |
|
| 1511 | - |
|
| 1512 | - if (count($compExpressions) === 1) { |
|
| 1513 | - $compExpr = $compExpressions[0]; |
|
| 1514 | - } else { |
|
| 1515 | - $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions); |
|
| 1516 | - } |
|
| 1517 | - |
|
| 1518 | - if (!isset($filters['props'])) { |
|
| 1519 | - $filters['props'] = []; |
|
| 1520 | - } |
|
| 1521 | - if (!isset($filters['params'])) { |
|
| 1522 | - $filters['params'] = []; |
|
| 1523 | - } |
|
| 1524 | - |
|
| 1525 | - $propParamExpressions = []; |
|
| 1526 | - foreach ($filters['props'] as $prop) { |
|
| 1527 | - $propParamExpressions[] = $query->expr()->andX( |
|
| 1528 | - $query->expr()->eq('i.name', $query->createNamedParameter($prop)), |
|
| 1529 | - $query->expr()->isNull('i.parameter') |
|
| 1530 | - ); |
|
| 1531 | - } |
|
| 1532 | - foreach ($filters['params'] as $param) { |
|
| 1533 | - $propParamExpressions[] = $query->expr()->andX( |
|
| 1534 | - $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])), |
|
| 1535 | - $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter'])) |
|
| 1536 | - ); |
|
| 1537 | - } |
|
| 1538 | - |
|
| 1539 | - if (count($propParamExpressions) === 1) { |
|
| 1540 | - $propParamExpr = $propParamExpressions[0]; |
|
| 1541 | - } else { |
|
| 1542 | - $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions); |
|
| 1543 | - } |
|
| 1544 | - |
|
| 1545 | - $query->select(['c.calendarid', 'c.uri']) |
|
| 1546 | - ->from($this->dbObjectPropertiesTable, 'i') |
|
| 1547 | - ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id')) |
|
| 1548 | - ->where($calExpr) |
|
| 1549 | - ->andWhere($compExpr) |
|
| 1550 | - ->andWhere($propParamExpr) |
|
| 1551 | - ->andWhere($query->expr()->iLike('i.value', |
|
| 1552 | - $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))); |
|
| 1553 | - |
|
| 1554 | - if ($offset) { |
|
| 1555 | - $query->setFirstResult($offset); |
|
| 1556 | - } |
|
| 1557 | - if ($limit) { |
|
| 1558 | - $query->setMaxResults($limit); |
|
| 1559 | - } |
|
| 1560 | - |
|
| 1561 | - $stmt = $query->executeQuery(); |
|
| 1562 | - |
|
| 1563 | - $result = []; |
|
| 1564 | - while ($row = $stmt->fetch()) { |
|
| 1565 | - $path = $uriMapper[$row['calendarid']] . '/' . $row['uri']; |
|
| 1566 | - if (!in_array($path, $result)) { |
|
| 1567 | - $result[] = $path; |
|
| 1568 | - } |
|
| 1569 | - } |
|
| 1570 | - |
|
| 1571 | - return $result; |
|
| 1572 | - } |
|
| 1573 | - |
|
| 1574 | - /** |
|
| 1575 | - * used for Nextcloud's calendar API |
|
| 1576 | - * |
|
| 1577 | - * @param array $calendarInfo |
|
| 1578 | - * @param string $pattern |
|
| 1579 | - * @param array $searchProperties |
|
| 1580 | - * @param array $options |
|
| 1581 | - * @param integer|null $limit |
|
| 1582 | - * @param integer|null $offset |
|
| 1583 | - * |
|
| 1584 | - * @return array |
|
| 1585 | - */ |
|
| 1586 | - public function search(array $calendarInfo, $pattern, array $searchProperties, |
|
| 1587 | - array $options, $limit, $offset) { |
|
| 1588 | - $outerQuery = $this->db->getQueryBuilder(); |
|
| 1589 | - $innerQuery = $this->db->getQueryBuilder(); |
|
| 1590 | - |
|
| 1591 | - $innerQuery->selectDistinct('op.objectid') |
|
| 1592 | - ->from($this->dbObjectPropertiesTable, 'op') |
|
| 1593 | - ->andWhere($innerQuery->expr()->eq('op.calendarid', |
|
| 1594 | - $outerQuery->createNamedParameter($calendarInfo['id']))) |
|
| 1595 | - ->andWhere($innerQuery->expr()->eq('op.calendartype', |
|
| 1596 | - $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1597 | - |
|
| 1598 | - // only return public items for shared calendars for now |
|
| 1599 | - if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) { |
|
| 1600 | - $innerQuery->andWhere($innerQuery->expr()->eq('c.classification', |
|
| 1601 | - $outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); |
|
| 1602 | - } |
|
| 1603 | - |
|
| 1604 | - $or = $innerQuery->expr()->orX(); |
|
| 1605 | - foreach ($searchProperties as $searchProperty) { |
|
| 1606 | - $or->add($innerQuery->expr()->eq('op.name', |
|
| 1607 | - $outerQuery->createNamedParameter($searchProperty))); |
|
| 1608 | - } |
|
| 1609 | - $innerQuery->andWhere($or); |
|
| 1610 | - |
|
| 1611 | - if ($pattern !== '') { |
|
| 1612 | - $innerQuery->andWhere($innerQuery->expr()->iLike('op.value', |
|
| 1613 | - $outerQuery->createNamedParameter('%' . |
|
| 1614 | - $this->db->escapeLikeParameter($pattern) . '%'))); |
|
| 1615 | - } |
|
| 1616 | - |
|
| 1617 | - $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri') |
|
| 1618 | - ->from('calendarobjects', 'c'); |
|
| 1619 | - |
|
| 1620 | - if (isset($options['timerange'])) { |
|
| 1621 | - if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) { |
|
| 1622 | - $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence', |
|
| 1623 | - $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()))); |
|
| 1624 | - } |
|
| 1625 | - if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTime) { |
|
| 1626 | - $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence', |
|
| 1627 | - $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()))); |
|
| 1628 | - } |
|
| 1629 | - } |
|
| 1630 | - |
|
| 1631 | - if (isset($options['types'])) { |
|
| 1632 | - $or = $outerQuery->expr()->orX(); |
|
| 1633 | - foreach ($options['types'] as $type) { |
|
| 1634 | - $or->add($outerQuery->expr()->eq('componenttype', |
|
| 1635 | - $outerQuery->createNamedParameter($type))); |
|
| 1636 | - } |
|
| 1637 | - $outerQuery->andWhere($or); |
|
| 1638 | - } |
|
| 1639 | - |
|
| 1640 | - $outerQuery->andWhere($outerQuery->expr()->in('c.id', |
|
| 1641 | - $outerQuery->createFunction($innerQuery->getSQL()))); |
|
| 1642 | - |
|
| 1643 | - if ($offset) { |
|
| 1644 | - $outerQuery->setFirstResult($offset); |
|
| 1645 | - } |
|
| 1646 | - if ($limit) { |
|
| 1647 | - $outerQuery->setMaxResults($limit); |
|
| 1648 | - } |
|
| 1649 | - |
|
| 1650 | - $result = $outerQuery->executeQuery(); |
|
| 1651 | - $calendarObjects = $result->fetchAll(); |
|
| 1652 | - |
|
| 1653 | - return array_map(function ($o) { |
|
| 1654 | - $calendarData = Reader::read($o['calendardata']); |
|
| 1655 | - $comps = $calendarData->getComponents(); |
|
| 1656 | - $objects = []; |
|
| 1657 | - $timezones = []; |
|
| 1658 | - foreach ($comps as $comp) { |
|
| 1659 | - if ($comp instanceof VTimeZone) { |
|
| 1660 | - $timezones[] = $comp; |
|
| 1661 | - } else { |
|
| 1662 | - $objects[] = $comp; |
|
| 1663 | - } |
|
| 1664 | - } |
|
| 1665 | - |
|
| 1666 | - return [ |
|
| 1667 | - 'id' => $o['id'], |
|
| 1668 | - 'type' => $o['componenttype'], |
|
| 1669 | - 'uid' => $o['uid'], |
|
| 1670 | - 'uri' => $o['uri'], |
|
| 1671 | - 'objects' => array_map(function ($c) { |
|
| 1672 | - return $this->transformSearchData($c); |
|
| 1673 | - }, $objects), |
|
| 1674 | - 'timezones' => array_map(function ($c) { |
|
| 1675 | - return $this->transformSearchData($c); |
|
| 1676 | - }, $timezones), |
|
| 1677 | - ]; |
|
| 1678 | - }, $calendarObjects); |
|
| 1679 | - } |
|
| 1680 | - |
|
| 1681 | - /** |
|
| 1682 | - * @param Component $comp |
|
| 1683 | - * @return array |
|
| 1684 | - */ |
|
| 1685 | - private function transformSearchData(Component $comp) { |
|
| 1686 | - $data = []; |
|
| 1687 | - /** @var Component[] $subComponents */ |
|
| 1688 | - $subComponents = $comp->getComponents(); |
|
| 1689 | - /** @var Property[] $properties */ |
|
| 1690 | - $properties = array_filter($comp->children(), function ($c) { |
|
| 1691 | - return $c instanceof Property; |
|
| 1692 | - }); |
|
| 1693 | - $validationRules = $comp->getValidationRules(); |
|
| 1694 | - |
|
| 1695 | - foreach ($subComponents as $subComponent) { |
|
| 1696 | - $name = $subComponent->name; |
|
| 1697 | - if (!isset($data[$name])) { |
|
| 1698 | - $data[$name] = []; |
|
| 1699 | - } |
|
| 1700 | - $data[$name][] = $this->transformSearchData($subComponent); |
|
| 1701 | - } |
|
| 1702 | - |
|
| 1703 | - foreach ($properties as $property) { |
|
| 1704 | - $name = $property->name; |
|
| 1705 | - if (!isset($validationRules[$name])) { |
|
| 1706 | - $validationRules[$name] = '*'; |
|
| 1707 | - } |
|
| 1708 | - |
|
| 1709 | - $rule = $validationRules[$property->name]; |
|
| 1710 | - if ($rule === '+' || $rule === '*') { // multiple |
|
| 1711 | - if (!isset($data[$name])) { |
|
| 1712 | - $data[$name] = []; |
|
| 1713 | - } |
|
| 1714 | - |
|
| 1715 | - $data[$name][] = $this->transformSearchProperty($property); |
|
| 1716 | - } else { // once |
|
| 1717 | - $data[$name] = $this->transformSearchProperty($property); |
|
| 1718 | - } |
|
| 1719 | - } |
|
| 1720 | - |
|
| 1721 | - return $data; |
|
| 1722 | - } |
|
| 1723 | - |
|
| 1724 | - /** |
|
| 1725 | - * @param Property $prop |
|
| 1726 | - * @return array |
|
| 1727 | - */ |
|
| 1728 | - private function transformSearchProperty(Property $prop) { |
|
| 1729 | - // No need to check Date, as it extends DateTime |
|
| 1730 | - if ($prop instanceof Property\ICalendar\DateTime) { |
|
| 1731 | - $value = $prop->getDateTime(); |
|
| 1732 | - } else { |
|
| 1733 | - $value = $prop->getValue(); |
|
| 1734 | - } |
|
| 1735 | - |
|
| 1736 | - return [ |
|
| 1737 | - $value, |
|
| 1738 | - $prop->parameters() |
|
| 1739 | - ]; |
|
| 1740 | - } |
|
| 1741 | - |
|
| 1742 | - /** |
|
| 1743 | - * @param string $principalUri |
|
| 1744 | - * @param string $pattern |
|
| 1745 | - * @param array $componentTypes |
|
| 1746 | - * @param array $searchProperties |
|
| 1747 | - * @param array $searchParameters |
|
| 1748 | - * @param array $options |
|
| 1749 | - * @return array |
|
| 1750 | - */ |
|
| 1751 | - public function searchPrincipalUri(string $principalUri, |
|
| 1752 | - string $pattern, |
|
| 1753 | - array $componentTypes, |
|
| 1754 | - array $searchProperties, |
|
| 1755 | - array $searchParameters, |
|
| 1756 | - array $options = []): array { |
|
| 1757 | - $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; |
|
| 1758 | - |
|
| 1759 | - $calendarObjectIdQuery = $this->db->getQueryBuilder(); |
|
| 1760 | - $calendarOr = $calendarObjectIdQuery->expr()->orX(); |
|
| 1761 | - $searchOr = $calendarObjectIdQuery->expr()->orX(); |
|
| 1762 | - |
|
| 1763 | - // Fetch calendars and subscription |
|
| 1764 | - $calendars = $this->getCalendarsForUser($principalUri); |
|
| 1765 | - $subscriptions = $this->getSubscriptionsForUser($principalUri); |
|
| 1766 | - foreach ($calendars as $calendar) { |
|
| 1767 | - $calendarAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1768 | - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id']))); |
|
| 1769 | - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1770 | - |
|
| 1771 | - // If it's shared, limit search to public events |
|
| 1772 | - if (isset($calendar['{http://owncloud.org/ns}owner-principal']) |
|
| 1773 | - && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) { |
|
| 1774 | - $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); |
|
| 1775 | - } |
|
| 1776 | - |
|
| 1777 | - $calendarOr->add($calendarAnd); |
|
| 1778 | - } |
|
| 1779 | - foreach ($subscriptions as $subscription) { |
|
| 1780 | - $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1781 | - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id']))); |
|
| 1782 | - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); |
|
| 1783 | - |
|
| 1784 | - // If it's shared, limit search to public events |
|
| 1785 | - if (isset($subscription['{http://owncloud.org/ns}owner-principal']) |
|
| 1786 | - && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) { |
|
| 1787 | - $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); |
|
| 1788 | - } |
|
| 1789 | - |
|
| 1790 | - $calendarOr->add($subscriptionAnd); |
|
| 1791 | - } |
|
| 1792 | - |
|
| 1793 | - foreach ($searchProperties as $property) { |
|
| 1794 | - $propertyAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1795 | - $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); |
|
| 1796 | - $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter')); |
|
| 1797 | - |
|
| 1798 | - $searchOr->add($propertyAnd); |
|
| 1799 | - } |
|
| 1800 | - foreach ($searchParameters as $property => $parameter) { |
|
| 1801 | - $parameterAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1802 | - $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); |
|
| 1803 | - $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY))); |
|
| 1804 | - |
|
| 1805 | - $searchOr->add($parameterAnd); |
|
| 1806 | - } |
|
| 1807 | - |
|
| 1808 | - if ($calendarOr->count() === 0) { |
|
| 1809 | - return []; |
|
| 1810 | - } |
|
| 1811 | - if ($searchOr->count() === 0) { |
|
| 1812 | - return []; |
|
| 1813 | - } |
|
| 1814 | - |
|
| 1815 | - $calendarObjectIdQuery->selectDistinct('cob.objectid') |
|
| 1816 | - ->from($this->dbObjectPropertiesTable, 'cob') |
|
| 1817 | - ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid')) |
|
| 1818 | - ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY))) |
|
| 1819 | - ->andWhere($calendarOr) |
|
| 1820 | - ->andWhere($searchOr); |
|
| 1821 | - |
|
| 1822 | - if ('' !== $pattern) { |
|
| 1823 | - if (!$escapePattern) { |
|
| 1824 | - $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern))); |
|
| 1825 | - } else { |
|
| 1826 | - $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); |
|
| 1827 | - } |
|
| 1828 | - } |
|
| 1829 | - |
|
| 1830 | - if (isset($options['limit'])) { |
|
| 1831 | - $calendarObjectIdQuery->setMaxResults($options['limit']); |
|
| 1832 | - } |
|
| 1833 | - if (isset($options['offset'])) { |
|
| 1834 | - $calendarObjectIdQuery->setFirstResult($options['offset']); |
|
| 1835 | - } |
|
| 1836 | - |
|
| 1837 | - $result = $calendarObjectIdQuery->executeQuery(); |
|
| 1838 | - $matches = $result->fetchAll(); |
|
| 1839 | - $result->closeCursor(); |
|
| 1840 | - $matches = array_map(static function (array $match):int { |
|
| 1841 | - return (int) $match['objectid']; |
|
| 1842 | - }, $matches); |
|
| 1843 | - |
|
| 1844 | - $query = $this->db->getQueryBuilder(); |
|
| 1845 | - $query->select('calendardata', 'uri', 'calendarid', 'calendartype') |
|
| 1846 | - ->from('calendarobjects') |
|
| 1847 | - ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY))); |
|
| 1848 | - |
|
| 1849 | - $result = $query->executeQuery(); |
|
| 1850 | - $calendarObjects = $result->fetchAll(); |
|
| 1851 | - $result->closeCursor(); |
|
| 1852 | - |
|
| 1853 | - return array_map(function (array $array): array { |
|
| 1854 | - $array['calendarid'] = (int)$array['calendarid']; |
|
| 1855 | - $array['calendartype'] = (int)$array['calendartype']; |
|
| 1856 | - $array['calendardata'] = $this->readBlob($array['calendardata']); |
|
| 1857 | - |
|
| 1858 | - return $array; |
|
| 1859 | - }, $calendarObjects); |
|
| 1860 | - } |
|
| 1861 | - |
|
| 1862 | - /** |
|
| 1863 | - * Searches through all of a users calendars and calendar objects to find |
|
| 1864 | - * an object with a specific UID. |
|
| 1865 | - * |
|
| 1866 | - * This method should return the path to this object, relative to the |
|
| 1867 | - * calendar home, so this path usually only contains two parts: |
|
| 1868 | - * |
|
| 1869 | - * calendarpath/objectpath.ics |
|
| 1870 | - * |
|
| 1871 | - * If the uid is not found, return null. |
|
| 1872 | - * |
|
| 1873 | - * This method should only consider * objects that the principal owns, so |
|
| 1874 | - * any calendars owned by other principals that also appear in this |
|
| 1875 | - * collection should be ignored. |
|
| 1876 | - * |
|
| 1877 | - * @param string $principalUri |
|
| 1878 | - * @param string $uid |
|
| 1879 | - * @return string|null |
|
| 1880 | - */ |
|
| 1881 | - public function getCalendarObjectByUID($principalUri, $uid) { |
|
| 1882 | - $query = $this->db->getQueryBuilder(); |
|
| 1883 | - $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi') |
|
| 1884 | - ->from('calendarobjects', 'co') |
|
| 1885 | - ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id')) |
|
| 1886 | - ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri))) |
|
| 1887 | - ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid))); |
|
| 1888 | - |
|
| 1889 | - $stmt = $query->executeQuery(); |
|
| 1890 | - $row = $stmt->fetch(); |
|
| 1891 | - $stmt->closeCursor(); |
|
| 1892 | - if ($row) { |
|
| 1893 | - return $row['calendaruri'] . '/' . $row['objecturi']; |
|
| 1894 | - } |
|
| 1895 | - |
|
| 1896 | - return null; |
|
| 1897 | - } |
|
| 1898 | - |
|
| 1899 | - /** |
|
| 1900 | - * The getChanges method returns all the changes that have happened, since |
|
| 1901 | - * the specified syncToken in the specified calendar. |
|
| 1902 | - * |
|
| 1903 | - * This function should return an array, such as the following: |
|
| 1904 | - * |
|
| 1905 | - * [ |
|
| 1906 | - * 'syncToken' => 'The current synctoken', |
|
| 1907 | - * 'added' => [ |
|
| 1908 | - * 'new.txt', |
|
| 1909 | - * ], |
|
| 1910 | - * 'modified' => [ |
|
| 1911 | - * 'modified.txt', |
|
| 1912 | - * ], |
|
| 1913 | - * 'deleted' => [ |
|
| 1914 | - * 'foo.php.bak', |
|
| 1915 | - * 'old.txt' |
|
| 1916 | - * ] |
|
| 1917 | - * ); |
|
| 1918 | - * |
|
| 1919 | - * The returned syncToken property should reflect the *current* syncToken |
|
| 1920 | - * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
| 1921 | - * property This is * needed here too, to ensure the operation is atomic. |
|
| 1922 | - * |
|
| 1923 | - * If the $syncToken argument is specified as null, this is an initial |
|
| 1924 | - * sync, and all members should be reported. |
|
| 1925 | - * |
|
| 1926 | - * The modified property is an array of nodenames that have changed since |
|
| 1927 | - * the last token. |
|
| 1928 | - * |
|
| 1929 | - * The deleted property is an array with nodenames, that have been deleted |
|
| 1930 | - * from collection. |
|
| 1931 | - * |
|
| 1932 | - * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
| 1933 | - * 1, you only have to report changes that happened only directly in |
|
| 1934 | - * immediate descendants. If it's 2, it should also include changes from |
|
| 1935 | - * the nodes below the child collections. (grandchildren) |
|
| 1936 | - * |
|
| 1937 | - * The $limit argument allows a client to specify how many results should |
|
| 1938 | - * be returned at most. If the limit is not specified, it should be treated |
|
| 1939 | - * as infinite. |
|
| 1940 | - * |
|
| 1941 | - * If the limit (infinite or not) is higher than you're willing to return, |
|
| 1942 | - * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
| 1943 | - * |
|
| 1944 | - * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
| 1945 | - * return null. |
|
| 1946 | - * |
|
| 1947 | - * The limit is 'suggestive'. You are free to ignore it. |
|
| 1948 | - * |
|
| 1949 | - * @param string $calendarId |
|
| 1950 | - * @param string $syncToken |
|
| 1951 | - * @param int $syncLevel |
|
| 1952 | - * @param int|null $limit |
|
| 1953 | - * @param int $calendarType |
|
| 1954 | - * @return array |
|
| 1955 | - */ |
|
| 1956 | - public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1957 | - // Current synctoken |
|
| 1958 | - $qb = $this->db->getQueryBuilder(); |
|
| 1959 | - $qb->select('synctoken') |
|
| 1960 | - ->from('calendars') |
|
| 1961 | - ->where( |
|
| 1962 | - $qb->expr()->eq('id', $qb->createNamedParameter($calendarId)) |
|
| 1963 | - ); |
|
| 1964 | - $stmt = $qb->executeQuery(); |
|
| 1965 | - $currentToken = $stmt->fetchOne(); |
|
| 1966 | - |
|
| 1967 | - if ($currentToken === false) { |
|
| 1968 | - return null; |
|
| 1969 | - } |
|
| 1970 | - |
|
| 1971 | - $result = [ |
|
| 1972 | - 'syncToken' => $currentToken, |
|
| 1973 | - 'added' => [], |
|
| 1974 | - 'modified' => [], |
|
| 1975 | - 'deleted' => [], |
|
| 1976 | - ]; |
|
| 1977 | - |
|
| 1978 | - if ($syncToken) { |
|
| 1979 | - $qb = $this->db->getQueryBuilder(); |
|
| 1980 | - |
|
| 1981 | - $qb->select('uri', 'operation') |
|
| 1982 | - ->from('calendarchanges') |
|
| 1983 | - ->where( |
|
| 1984 | - $qb->expr()->andX( |
|
| 1985 | - $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)), |
|
| 1986 | - $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)), |
|
| 1987 | - $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), |
|
| 1988 | - $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) |
|
| 1989 | - ) |
|
| 1990 | - )->orderBy('synctoken'); |
|
| 1991 | - if (is_int($limit) && $limit > 0) { |
|
| 1992 | - $qb->setMaxResults($limit); |
|
| 1993 | - } |
|
| 1994 | - |
|
| 1995 | - // Fetching all changes |
|
| 1996 | - $stmt = $qb->executeQuery(); |
|
| 1997 | - $changes = []; |
|
| 1998 | - |
|
| 1999 | - // This loop ensures that any duplicates are overwritten, only the |
|
| 2000 | - // last change on a node is relevant. |
|
| 2001 | - while ($row = $stmt->fetch()) { |
|
| 2002 | - $changes[$row['uri']] = $row['operation']; |
|
| 2003 | - } |
|
| 2004 | - $stmt->closeCursor(); |
|
| 2005 | - |
|
| 2006 | - foreach ($changes as $uri => $operation) { |
|
| 2007 | - switch ($operation) { |
|
| 2008 | - case 1: |
|
| 2009 | - $result['added'][] = $uri; |
|
| 2010 | - break; |
|
| 2011 | - case 2: |
|
| 2012 | - $result['modified'][] = $uri; |
|
| 2013 | - break; |
|
| 2014 | - case 3: |
|
| 2015 | - $result['deleted'][] = $uri; |
|
| 2016 | - break; |
|
| 2017 | - } |
|
| 2018 | - } |
|
| 2019 | - } else { |
|
| 2020 | - // No synctoken supplied, this is the initial sync. |
|
| 2021 | - $qb = $this->db->getQueryBuilder(); |
|
| 2022 | - $qb->select('uri') |
|
| 2023 | - ->from('calendarobjects') |
|
| 2024 | - ->where( |
|
| 2025 | - $qb->expr()->andX( |
|
| 2026 | - $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), |
|
| 2027 | - $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) |
|
| 2028 | - ) |
|
| 2029 | - ); |
|
| 2030 | - $stmt = $qb->executeQuery(); |
|
| 2031 | - $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); |
|
| 2032 | - $stmt->closeCursor(); |
|
| 2033 | - } |
|
| 2034 | - return $result; |
|
| 2035 | - } |
|
| 2036 | - |
|
| 2037 | - /** |
|
| 2038 | - * Returns a list of subscriptions for a principal. |
|
| 2039 | - * |
|
| 2040 | - * Every subscription is an array with the following keys: |
|
| 2041 | - * * id, a unique id that will be used by other functions to modify the |
|
| 2042 | - * subscription. This can be the same as the uri or a database key. |
|
| 2043 | - * * uri. This is just the 'base uri' or 'filename' of the subscription. |
|
| 2044 | - * * principaluri. The owner of the subscription. Almost always the same as |
|
| 2045 | - * principalUri passed to this method. |
|
| 2046 | - * |
|
| 2047 | - * Furthermore, all the subscription info must be returned too: |
|
| 2048 | - * |
|
| 2049 | - * 1. {DAV:}displayname |
|
| 2050 | - * 2. {http://apple.com/ns/ical/}refreshrate |
|
| 2051 | - * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos |
|
| 2052 | - * should not be stripped). |
|
| 2053 | - * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms |
|
| 2054 | - * should not be stripped). |
|
| 2055 | - * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if |
|
| 2056 | - * attachments should not be stripped). |
|
| 2057 | - * 6. {http://calendarserver.org/ns/}source (Must be a |
|
| 2058 | - * Sabre\DAV\Property\Href). |
|
| 2059 | - * 7. {http://apple.com/ns/ical/}calendar-color |
|
| 2060 | - * 8. {http://apple.com/ns/ical/}calendar-order |
|
| 2061 | - * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set |
|
| 2062 | - * (should just be an instance of |
|
| 2063 | - * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of |
|
| 2064 | - * default components). |
|
| 2065 | - * |
|
| 2066 | - * @param string $principalUri |
|
| 2067 | - * @return array |
|
| 2068 | - */ |
|
| 2069 | - public function getSubscriptionsForUser($principalUri) { |
|
| 2070 | - $fields = array_values($this->subscriptionPropertyMap); |
|
| 2071 | - $fields[] = 'id'; |
|
| 2072 | - $fields[] = 'uri'; |
|
| 2073 | - $fields[] = 'source'; |
|
| 2074 | - $fields[] = 'principaluri'; |
|
| 2075 | - $fields[] = 'lastmodified'; |
|
| 2076 | - $fields[] = 'synctoken'; |
|
| 2077 | - |
|
| 2078 | - $query = $this->db->getQueryBuilder(); |
|
| 2079 | - $query->select($fields) |
|
| 2080 | - ->from('calendarsubscriptions') |
|
| 2081 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2082 | - ->orderBy('calendarorder', 'asc'); |
|
| 2083 | - $stmt = $query->executeQuery(); |
|
| 2084 | - |
|
| 2085 | - $subscriptions = []; |
|
| 2086 | - while ($row = $stmt->fetch()) { |
|
| 2087 | - $subscription = [ |
|
| 2088 | - 'id' => $row['id'], |
|
| 2089 | - 'uri' => $row['uri'], |
|
| 2090 | - 'principaluri' => $row['principaluri'], |
|
| 2091 | - 'source' => $row['source'], |
|
| 2092 | - 'lastmodified' => $row['lastmodified'], |
|
| 2093 | - |
|
| 2094 | - '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), |
|
| 2095 | - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 2096 | - ]; |
|
| 2097 | - |
|
| 2098 | - foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { |
|
| 2099 | - if (!is_null($row[$dbName])) { |
|
| 2100 | - $subscription[$xmlName] = $row[$dbName]; |
|
| 2101 | - } |
|
| 2102 | - } |
|
| 2103 | - |
|
| 2104 | - $subscriptions[] = $subscription; |
|
| 2105 | - } |
|
| 2106 | - |
|
| 2107 | - return $subscriptions; |
|
| 2108 | - } |
|
| 2109 | - |
|
| 2110 | - /** |
|
| 2111 | - * Creates a new subscription for a principal. |
|
| 2112 | - * |
|
| 2113 | - * If the creation was a success, an id must be returned that can be used to reference |
|
| 2114 | - * this subscription in other methods, such as updateSubscription. |
|
| 2115 | - * |
|
| 2116 | - * @param string $principalUri |
|
| 2117 | - * @param string $uri |
|
| 2118 | - * @param array $properties |
|
| 2119 | - * @return mixed |
|
| 2120 | - */ |
|
| 2121 | - public function createSubscription($principalUri, $uri, array $properties) { |
|
| 2122 | - if (!isset($properties['{http://calendarserver.org/ns/}source'])) { |
|
| 2123 | - throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); |
|
| 2124 | - } |
|
| 2125 | - |
|
| 2126 | - $values = [ |
|
| 2127 | - 'principaluri' => $principalUri, |
|
| 2128 | - 'uri' => $uri, |
|
| 2129 | - 'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), |
|
| 2130 | - 'lastmodified' => time(), |
|
| 2131 | - ]; |
|
| 2132 | - |
|
| 2133 | - $propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments']; |
|
| 2134 | - |
|
| 2135 | - foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { |
|
| 2136 | - if (array_key_exists($xmlName, $properties)) { |
|
| 2137 | - $values[$dbName] = $properties[$xmlName]; |
|
| 2138 | - if (in_array($dbName, $propertiesBoolean)) { |
|
| 2139 | - $values[$dbName] = true; |
|
| 2140 | - } |
|
| 2141 | - } |
|
| 2142 | - } |
|
| 2143 | - |
|
| 2144 | - $valuesToInsert = []; |
|
| 2145 | - |
|
| 2146 | - $query = $this->db->getQueryBuilder(); |
|
| 2147 | - |
|
| 2148 | - foreach (array_keys($values) as $name) { |
|
| 2149 | - $valuesToInsert[$name] = $query->createNamedParameter($values[$name]); |
|
| 2150 | - } |
|
| 2151 | - |
|
| 2152 | - $query->insert('calendarsubscriptions') |
|
| 2153 | - ->values($valuesToInsert) |
|
| 2154 | - ->executeStatement(); |
|
| 2155 | - |
|
| 2156 | - $subscriptionId = $query->getLastInsertId(); |
|
| 2157 | - |
|
| 2158 | - $subscriptionRow = $this->getSubscriptionById($subscriptionId); |
|
| 2159 | - $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow)); |
|
| 2160 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent( |
|
| 2161 | - '\OCA\DAV\CalDAV\CalDavBackend::createSubscription', |
|
| 2162 | - [ |
|
| 2163 | - 'subscriptionId' => $subscriptionId, |
|
| 2164 | - 'subscriptionData' => $subscriptionRow, |
|
| 2165 | - ])); |
|
| 2166 | - |
|
| 2167 | - return $subscriptionId; |
|
| 2168 | - } |
|
| 2169 | - |
|
| 2170 | - /** |
|
| 2171 | - * Updates a subscription |
|
| 2172 | - * |
|
| 2173 | - * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
| 2174 | - * To do the actual updates, you must tell this object which properties |
|
| 2175 | - * you're going to process with the handle() method. |
|
| 2176 | - * |
|
| 2177 | - * Calling the handle method is like telling the PropPatch object "I |
|
| 2178 | - * promise I can handle updating this property". |
|
| 2179 | - * |
|
| 2180 | - * Read the PropPatch documentation for more info and examples. |
|
| 2181 | - * |
|
| 2182 | - * @param mixed $subscriptionId |
|
| 2183 | - * @param PropPatch $propPatch |
|
| 2184 | - * @return void |
|
| 2185 | - */ |
|
| 2186 | - public function updateSubscription($subscriptionId, PropPatch $propPatch) { |
|
| 2187 | - $supportedProperties = array_keys($this->subscriptionPropertyMap); |
|
| 2188 | - $supportedProperties[] = '{http://calendarserver.org/ns/}source'; |
|
| 2189 | - |
|
| 2190 | - $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) { |
|
| 2191 | - $newValues = []; |
|
| 2192 | - |
|
| 2193 | - foreach ($mutations as $propertyName => $propertyValue) { |
|
| 2194 | - if ($propertyName === '{http://calendarserver.org/ns/}source') { |
|
| 2195 | - $newValues['source'] = $propertyValue->getHref(); |
|
| 2196 | - } else { |
|
| 2197 | - $fieldName = $this->subscriptionPropertyMap[$propertyName]; |
|
| 2198 | - $newValues[$fieldName] = $propertyValue; |
|
| 2199 | - } |
|
| 2200 | - } |
|
| 2201 | - |
|
| 2202 | - $query = $this->db->getQueryBuilder(); |
|
| 2203 | - $query->update('calendarsubscriptions') |
|
| 2204 | - ->set('lastmodified', $query->createNamedParameter(time())); |
|
| 2205 | - foreach ($newValues as $fieldName => $value) { |
|
| 2206 | - $query->set($fieldName, $query->createNamedParameter($value)); |
|
| 2207 | - } |
|
| 2208 | - $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) |
|
| 2209 | - ->executeStatement(); |
|
| 2210 | - |
|
| 2211 | - $subscriptionRow = $this->getSubscriptionById($subscriptionId); |
|
| 2212 | - $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations)); |
|
| 2213 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent( |
|
| 2214 | - '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', |
|
| 2215 | - [ |
|
| 2216 | - 'subscriptionId' => $subscriptionId, |
|
| 2217 | - 'subscriptionData' => $subscriptionRow, |
|
| 2218 | - 'propertyMutations' => $mutations, |
|
| 2219 | - ])); |
|
| 2220 | - |
|
| 2221 | - return true; |
|
| 2222 | - }); |
|
| 2223 | - } |
|
| 2224 | - |
|
| 2225 | - /** |
|
| 2226 | - * Deletes a subscription. |
|
| 2227 | - * |
|
| 2228 | - * @param mixed $subscriptionId |
|
| 2229 | - * @return void |
|
| 2230 | - */ |
|
| 2231 | - public function deleteSubscription($subscriptionId) { |
|
| 2232 | - $subscriptionRow = $this->getSubscriptionById($subscriptionId); |
|
| 2233 | - |
|
| 2234 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent( |
|
| 2235 | - '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', |
|
| 2236 | - [ |
|
| 2237 | - 'subscriptionId' => $subscriptionId, |
|
| 2238 | - 'subscriptionData' => $this->getSubscriptionById($subscriptionId), |
|
| 2239 | - ])); |
|
| 2240 | - |
|
| 2241 | - $query = $this->db->getQueryBuilder(); |
|
| 2242 | - $query->delete('calendarsubscriptions') |
|
| 2243 | - ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) |
|
| 2244 | - ->executeStatement(); |
|
| 2245 | - |
|
| 2246 | - $query = $this->db->getQueryBuilder(); |
|
| 2247 | - $query->delete('calendarobjects') |
|
| 2248 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2249 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2250 | - ->executeStatement(); |
|
| 2251 | - |
|
| 2252 | - $query->delete('calendarchanges') |
|
| 2253 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2254 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2255 | - ->executeStatement(); |
|
| 2256 | - |
|
| 2257 | - $query->delete($this->dbObjectPropertiesTable) |
|
| 2258 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2259 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2260 | - ->executeStatement(); |
|
| 2261 | - |
|
| 2262 | - if ($subscriptionRow) { |
|
| 2263 | - $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, [])); |
|
| 2264 | - } |
|
| 2265 | - } |
|
| 2266 | - |
|
| 2267 | - /** |
|
| 2268 | - * Returns a single scheduling object for the inbox collection. |
|
| 2269 | - * |
|
| 2270 | - * The returned array should contain the following elements: |
|
| 2271 | - * * uri - A unique basename for the object. This will be used to |
|
| 2272 | - * construct a full uri. |
|
| 2273 | - * * calendardata - The iCalendar object |
|
| 2274 | - * * lastmodified - The last modification date. Can be an int for a unix |
|
| 2275 | - * timestamp, or a PHP DateTime object. |
|
| 2276 | - * * etag - A unique token that must change if the object changed. |
|
| 2277 | - * * size - The size of the object, in bytes. |
|
| 2278 | - * |
|
| 2279 | - * @param string $principalUri |
|
| 2280 | - * @param string $objectUri |
|
| 2281 | - * @return array |
|
| 2282 | - */ |
|
| 2283 | - public function getSchedulingObject($principalUri, $objectUri) { |
|
| 2284 | - $query = $this->db->getQueryBuilder(); |
|
| 2285 | - $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) |
|
| 2286 | - ->from('schedulingobjects') |
|
| 2287 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2288 | - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 2289 | - ->executeQuery(); |
|
| 2290 | - |
|
| 2291 | - $row = $stmt->fetch(); |
|
| 2292 | - |
|
| 2293 | - if (!$row) { |
|
| 2294 | - return null; |
|
| 2295 | - } |
|
| 2296 | - |
|
| 2297 | - return [ |
|
| 2298 | - 'uri' => $row['uri'], |
|
| 2299 | - 'calendardata' => $row['calendardata'], |
|
| 2300 | - 'lastmodified' => $row['lastmodified'], |
|
| 2301 | - 'etag' => '"' . $row['etag'] . '"', |
|
| 2302 | - 'size' => (int)$row['size'], |
|
| 2303 | - ]; |
|
| 2304 | - } |
|
| 2305 | - |
|
| 2306 | - /** |
|
| 2307 | - * Returns all scheduling objects for the inbox collection. |
|
| 2308 | - * |
|
| 2309 | - * These objects should be returned as an array. Every item in the array |
|
| 2310 | - * should follow the same structure as returned from getSchedulingObject. |
|
| 2311 | - * |
|
| 2312 | - * The main difference is that 'calendardata' is optional. |
|
| 2313 | - * |
|
| 2314 | - * @param string $principalUri |
|
| 2315 | - * @return array |
|
| 2316 | - */ |
|
| 2317 | - public function getSchedulingObjects($principalUri) { |
|
| 2318 | - $query = $this->db->getQueryBuilder(); |
|
| 2319 | - $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) |
|
| 2320 | - ->from('schedulingobjects') |
|
| 2321 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2322 | - ->executeQuery(); |
|
| 2323 | - |
|
| 2324 | - $result = []; |
|
| 2325 | - foreach ($stmt->fetchAll() as $row) { |
|
| 2326 | - $result[] = [ |
|
| 2327 | - 'calendardata' => $row['calendardata'], |
|
| 2328 | - 'uri' => $row['uri'], |
|
| 2329 | - 'lastmodified' => $row['lastmodified'], |
|
| 2330 | - 'etag' => '"' . $row['etag'] . '"', |
|
| 2331 | - 'size' => (int)$row['size'], |
|
| 2332 | - ]; |
|
| 2333 | - } |
|
| 2334 | - |
|
| 2335 | - return $result; |
|
| 2336 | - } |
|
| 2337 | - |
|
| 2338 | - /** |
|
| 2339 | - * Deletes a scheduling object from the inbox collection. |
|
| 2340 | - * |
|
| 2341 | - * @param string $principalUri |
|
| 2342 | - * @param string $objectUri |
|
| 2343 | - * @return void |
|
| 2344 | - */ |
|
| 2345 | - public function deleteSchedulingObject($principalUri, $objectUri) { |
|
| 2346 | - $query = $this->db->getQueryBuilder(); |
|
| 2347 | - $query->delete('schedulingobjects') |
|
| 2348 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2349 | - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 2350 | - ->executeStatement(); |
|
| 2351 | - } |
|
| 2352 | - |
|
| 2353 | - /** |
|
| 2354 | - * Creates a new scheduling object. This should land in a users' inbox. |
|
| 2355 | - * |
|
| 2356 | - * @param string $principalUri |
|
| 2357 | - * @param string $objectUri |
|
| 2358 | - * @param string $objectData |
|
| 2359 | - * @return void |
|
| 2360 | - */ |
|
| 2361 | - public function createSchedulingObject($principalUri, $objectUri, $objectData) { |
|
| 2362 | - $query = $this->db->getQueryBuilder(); |
|
| 2363 | - $query->insert('schedulingobjects') |
|
| 2364 | - ->values([ |
|
| 2365 | - 'principaluri' => $query->createNamedParameter($principalUri), |
|
| 2366 | - 'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB), |
|
| 2367 | - 'uri' => $query->createNamedParameter($objectUri), |
|
| 2368 | - 'lastmodified' => $query->createNamedParameter(time()), |
|
| 2369 | - 'etag' => $query->createNamedParameter(md5($objectData)), |
|
| 2370 | - 'size' => $query->createNamedParameter(strlen($objectData)) |
|
| 2371 | - ]) |
|
| 2372 | - ->executeStatement(); |
|
| 2373 | - } |
|
| 2374 | - |
|
| 2375 | - /** |
|
| 2376 | - * Adds a change record to the calendarchanges table. |
|
| 2377 | - * |
|
| 2378 | - * @param mixed $calendarId |
|
| 2379 | - * @param string $objectUri |
|
| 2380 | - * @param int $operation 1 = add, 2 = modify, 3 = delete. |
|
| 2381 | - * @param int $calendarType |
|
| 2382 | - * @return void |
|
| 2383 | - */ |
|
| 2384 | - protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 2385 | - $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions'; |
|
| 2386 | - |
|
| 2387 | - $query = $this->db->getQueryBuilder(); |
|
| 2388 | - $query->select('synctoken') |
|
| 2389 | - ->from($table) |
|
| 2390 | - ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); |
|
| 2391 | - $result = $query->executeQuery(); |
|
| 2392 | - $syncToken = (int)$result->fetchOne(); |
|
| 2393 | - $result->closeCursor(); |
|
| 2394 | - |
|
| 2395 | - $query = $this->db->getQueryBuilder(); |
|
| 2396 | - $query->insert('calendarchanges') |
|
| 2397 | - ->values([ |
|
| 2398 | - 'uri' => $query->createNamedParameter($objectUri), |
|
| 2399 | - 'synctoken' => $query->createNamedParameter($syncToken), |
|
| 2400 | - 'calendarid' => $query->createNamedParameter($calendarId), |
|
| 2401 | - 'operation' => $query->createNamedParameter($operation), |
|
| 2402 | - 'calendartype' => $query->createNamedParameter($calendarType), |
|
| 2403 | - ]) |
|
| 2404 | - ->executeStatement(); |
|
| 2405 | - |
|
| 2406 | - $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?"); |
|
| 2407 | - $stmt->execute([ |
|
| 2408 | - $calendarId |
|
| 2409 | - ]); |
|
| 2410 | - } |
|
| 2411 | - |
|
| 2412 | - /** |
|
| 2413 | - * Parses some information from calendar objects, used for optimized |
|
| 2414 | - * calendar-queries. |
|
| 2415 | - * |
|
| 2416 | - * Returns an array with the following keys: |
|
| 2417 | - * * etag - An md5 checksum of the object without the quotes. |
|
| 2418 | - * * size - Size of the object in bytes |
|
| 2419 | - * * componentType - VEVENT, VTODO or VJOURNAL |
|
| 2420 | - * * firstOccurence |
|
| 2421 | - * * lastOccurence |
|
| 2422 | - * * uid - value of the UID property |
|
| 2423 | - * |
|
| 2424 | - * @param string $calendarData |
|
| 2425 | - * @return array |
|
| 2426 | - */ |
|
| 2427 | - public function getDenormalizedData($calendarData) { |
|
| 2428 | - $vObject = Reader::read($calendarData); |
|
| 2429 | - $vEvents = []; |
|
| 2430 | - $componentType = null; |
|
| 2431 | - $component = null; |
|
| 2432 | - $firstOccurrence = null; |
|
| 2433 | - $lastOccurrence = null; |
|
| 2434 | - $uid = null; |
|
| 2435 | - $classification = self::CLASSIFICATION_PUBLIC; |
|
| 2436 | - $hasDTSTART = false; |
|
| 2437 | - foreach ($vObject->getComponents() as $component) { |
|
| 2438 | - if ($component->name !== 'VTIMEZONE') { |
|
| 2439 | - // Finding all VEVENTs, and track them |
|
| 2440 | - if ($component->name === 'VEVENT') { |
|
| 2441 | - array_push($vEvents, $component); |
|
| 2442 | - if ($component->DTSTART) { |
|
| 2443 | - $hasDTSTART = true; |
|
| 2444 | - } |
|
| 2445 | - } |
|
| 2446 | - // Track first component type and uid |
|
| 2447 | - if ($uid === null) { |
|
| 2448 | - $componentType = $component->name; |
|
| 2449 | - $uid = (string)$component->UID; |
|
| 2450 | - } |
|
| 2451 | - } |
|
| 2452 | - } |
|
| 2453 | - if (!$componentType) { |
|
| 2454 | - throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); |
|
| 2455 | - } |
|
| 2456 | - |
|
| 2457 | - if ($hasDTSTART) { |
|
| 2458 | - $component = $vEvents[0]; |
|
| 2459 | - |
|
| 2460 | - // Finding the last occurrence is a bit harder |
|
| 2461 | - if (!isset($component->RRULE) && count($vEvents) === 1) { |
|
| 2462 | - $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp(); |
|
| 2463 | - if (isset($component->DTEND)) { |
|
| 2464 | - $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp(); |
|
| 2465 | - } elseif (isset($component->DURATION)) { |
|
| 2466 | - $endDate = clone $component->DTSTART->getDateTime(); |
|
| 2467 | - $endDate->add(DateTimeParser::parse($component->DURATION->getValue())); |
|
| 2468 | - $lastOccurrence = $endDate->getTimeStamp(); |
|
| 2469 | - } elseif (!$component->DTSTART->hasTime()) { |
|
| 2470 | - $endDate = clone $component->DTSTART->getDateTime(); |
|
| 2471 | - $endDate->modify('+1 day'); |
|
| 2472 | - $lastOccurrence = $endDate->getTimeStamp(); |
|
| 2473 | - } else { |
|
| 2474 | - $lastOccurrence = $firstOccurrence; |
|
| 2475 | - } |
|
| 2476 | - } else { |
|
| 2477 | - $it = new EventIterator($vEvents); |
|
| 2478 | - $maxDate = new DateTime(self::MAX_DATE); |
|
| 2479 | - $firstOccurrence = $it->getDtStart()->getTimestamp(); |
|
| 2480 | - if ($it->isInfinite()) { |
|
| 2481 | - $lastOccurrence = $maxDate->getTimestamp(); |
|
| 2482 | - } else { |
|
| 2483 | - $end = $it->getDtEnd(); |
|
| 2484 | - while ($it->valid() && $end < $maxDate) { |
|
| 2485 | - $end = $it->getDtEnd(); |
|
| 2486 | - $it->next(); |
|
| 2487 | - } |
|
| 2488 | - $lastOccurrence = $end->getTimestamp(); |
|
| 2489 | - } |
|
| 2490 | - } |
|
| 2491 | - } |
|
| 2492 | - |
|
| 2493 | - if ($component->CLASS) { |
|
| 2494 | - $classification = CalDavBackend::CLASSIFICATION_PRIVATE; |
|
| 2495 | - switch ($component->CLASS->getValue()) { |
|
| 2496 | - case 'PUBLIC': |
|
| 2497 | - $classification = CalDavBackend::CLASSIFICATION_PUBLIC; |
|
| 2498 | - break; |
|
| 2499 | - case 'CONFIDENTIAL': |
|
| 2500 | - $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL; |
|
| 2501 | - break; |
|
| 2502 | - } |
|
| 2503 | - } |
|
| 2504 | - return [ |
|
| 2505 | - 'etag' => md5($calendarData), |
|
| 2506 | - 'size' => strlen($calendarData), |
|
| 2507 | - 'componentType' => $componentType, |
|
| 2508 | - 'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence), |
|
| 2509 | - 'lastOccurence' => $lastOccurrence, |
|
| 2510 | - 'uid' => $uid, |
|
| 2511 | - 'classification' => $classification |
|
| 2512 | - ]; |
|
| 2513 | - } |
|
| 2514 | - |
|
| 2515 | - /** |
|
| 2516 | - * @param $cardData |
|
| 2517 | - * @return bool|string |
|
| 2518 | - */ |
|
| 2519 | - private function readBlob($cardData) { |
|
| 2520 | - if (is_resource($cardData)) { |
|
| 2521 | - return stream_get_contents($cardData); |
|
| 2522 | - } |
|
| 2523 | - |
|
| 2524 | - return $cardData; |
|
| 2525 | - } |
|
| 2526 | - |
|
| 2527 | - /** |
|
| 2528 | - * @param IShareable $shareable |
|
| 2529 | - * @param array $add |
|
| 2530 | - * @param array $remove |
|
| 2531 | - */ |
|
| 2532 | - public function updateShares($shareable, $add, $remove) { |
|
| 2533 | - $calendarId = $shareable->getResourceId(); |
|
| 2534 | - $calendarRow = $this->getCalendarById($calendarId); |
|
| 2535 | - $oldShares = $this->getShares($calendarId); |
|
| 2536 | - |
|
| 2537 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent( |
|
| 2538 | - '\OCA\DAV\CalDAV\CalDavBackend::updateShares', |
|
| 2539 | - [ |
|
| 2540 | - 'calendarId' => $calendarId, |
|
| 2541 | - 'calendarData' => $calendarRow, |
|
| 2542 | - 'shares' => $oldShares, |
|
| 2543 | - 'add' => $add, |
|
| 2544 | - 'remove' => $remove, |
|
| 2545 | - ])); |
|
| 2546 | - $this->calendarSharingBackend->updateShares($shareable, $add, $remove); |
|
| 2547 | - |
|
| 2548 | - $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove)); |
|
| 2549 | - } |
|
| 2550 | - |
|
| 2551 | - /** |
|
| 2552 | - * @param int $resourceId |
|
| 2553 | - * @param int $calendarType |
|
| 2554 | - * @return array |
|
| 2555 | - */ |
|
| 2556 | - public function getShares($resourceId, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 2557 | - return $this->calendarSharingBackend->getShares($resourceId); |
|
| 2558 | - } |
|
| 2559 | - |
|
| 2560 | - /** |
|
| 2561 | - * @param boolean $value |
|
| 2562 | - * @param \OCA\DAV\CalDAV\Calendar $calendar |
|
| 2563 | - * @return string|null |
|
| 2564 | - */ |
|
| 2565 | - public function setPublishStatus($value, $calendar) { |
|
| 2566 | - $calendarId = $calendar->getResourceId(); |
|
| 2567 | - $calendarData = $this->getCalendarById($calendarId); |
|
| 2568 | - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent( |
|
| 2569 | - '\OCA\DAV\CalDAV\CalDavBackend::updateShares', |
|
| 2570 | - [ |
|
| 2571 | - 'calendarId' => $calendarId, |
|
| 2572 | - 'calendarData' => $calendarData, |
|
| 2573 | - 'public' => $value, |
|
| 2574 | - ])); |
|
| 2575 | - |
|
| 2576 | - $query = $this->db->getQueryBuilder(); |
|
| 2577 | - if ($value) { |
|
| 2578 | - $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE); |
|
| 2579 | - $query->insert('dav_shares') |
|
| 2580 | - ->values([ |
|
| 2581 | - 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()), |
|
| 2582 | - 'type' => $query->createNamedParameter('calendar'), |
|
| 2583 | - 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC), |
|
| 2584 | - 'resourceid' => $query->createNamedParameter($calendar->getResourceId()), |
|
| 2585 | - 'publicuri' => $query->createNamedParameter($publicUri) |
|
| 2586 | - ]); |
|
| 2587 | - $query->executeStatement(); |
|
| 2588 | - |
|
| 2589 | - $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri)); |
|
| 2590 | - return $publicUri; |
|
| 2591 | - } |
|
| 2592 | - $query->delete('dav_shares') |
|
| 2593 | - ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) |
|
| 2594 | - ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))); |
|
| 2595 | - $query->executeStatement(); |
|
| 2596 | - |
|
| 2597 | - $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData)); |
|
| 2598 | - return null; |
|
| 2599 | - } |
|
| 2600 | - |
|
| 2601 | - /** |
|
| 2602 | - * @param \OCA\DAV\CalDAV\Calendar $calendar |
|
| 2603 | - * @return mixed |
|
| 2604 | - */ |
|
| 2605 | - public function getPublishStatus($calendar) { |
|
| 2606 | - $query = $this->db->getQueryBuilder(); |
|
| 2607 | - $result = $query->select('publicuri') |
|
| 2608 | - ->from('dav_shares') |
|
| 2609 | - ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) |
|
| 2610 | - ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))) |
|
| 2611 | - ->executeQuery(); |
|
| 2612 | - |
|
| 2613 | - $row = $result->fetch(); |
|
| 2614 | - $result->closeCursor(); |
|
| 2615 | - return $row ? reset($row) : false; |
|
| 2616 | - } |
|
| 2617 | - |
|
| 2618 | - /** |
|
| 2619 | - * @param int $resourceId |
|
| 2620 | - * @param array $acl |
|
| 2621 | - * @return array |
|
| 2622 | - */ |
|
| 2623 | - public function applyShareAcl($resourceId, $acl) { |
|
| 2624 | - return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl); |
|
| 2625 | - } |
|
| 2626 | - |
|
| 2627 | - |
|
| 2628 | - |
|
| 2629 | - /** |
|
| 2630 | - * update properties table |
|
| 2631 | - * |
|
| 2632 | - * @param int $calendarId |
|
| 2633 | - * @param string $objectUri |
|
| 2634 | - * @param string $calendarData |
|
| 2635 | - * @param int $calendarType |
|
| 2636 | - */ |
|
| 2637 | - public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 2638 | - $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType); |
|
| 2639 | - |
|
| 2640 | - try { |
|
| 2641 | - $vCalendar = $this->readCalendarData($calendarData); |
|
| 2642 | - } catch (\Exception $ex) { |
|
| 2643 | - return; |
|
| 2644 | - } |
|
| 2645 | - |
|
| 2646 | - $this->purgeProperties($calendarId, $objectId); |
|
| 2647 | - |
|
| 2648 | - $query = $this->db->getQueryBuilder(); |
|
| 2649 | - $query->insert($this->dbObjectPropertiesTable) |
|
| 2650 | - ->values( |
|
| 2651 | - [ |
|
| 2652 | - 'calendarid' => $query->createNamedParameter($calendarId), |
|
| 2653 | - 'calendartype' => $query->createNamedParameter($calendarType), |
|
| 2654 | - 'objectid' => $query->createNamedParameter($objectId), |
|
| 2655 | - 'name' => $query->createParameter('name'), |
|
| 2656 | - 'parameter' => $query->createParameter('parameter'), |
|
| 2657 | - 'value' => $query->createParameter('value'), |
|
| 2658 | - ] |
|
| 2659 | - ); |
|
| 2660 | - |
|
| 2661 | - $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO']; |
|
| 2662 | - foreach ($vCalendar->getComponents() as $component) { |
|
| 2663 | - if (!in_array($component->name, $indexComponents)) { |
|
| 2664 | - continue; |
|
| 2665 | - } |
|
| 2666 | - |
|
| 2667 | - foreach ($component->children() as $property) { |
|
| 2668 | - if (in_array($property->name, self::$indexProperties)) { |
|
| 2669 | - $value = $property->getValue(); |
|
| 2670 | - // is this a shitty db? |
|
| 2671 | - if (!$this->db->supports4ByteText()) { |
|
| 2672 | - $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); |
|
| 2673 | - } |
|
| 2674 | - $value = mb_strcut($value, 0, 254); |
|
| 2675 | - |
|
| 2676 | - $query->setParameter('name', $property->name); |
|
| 2677 | - $query->setParameter('parameter', null); |
|
| 2678 | - $query->setParameter('value', $value); |
|
| 2679 | - $query->executeStatement(); |
|
| 2680 | - } |
|
| 2681 | - |
|
| 2682 | - if (array_key_exists($property->name, self::$indexParameters)) { |
|
| 2683 | - $parameters = $property->parameters(); |
|
| 2684 | - $indexedParametersForProperty = self::$indexParameters[$property->name]; |
|
| 2685 | - |
|
| 2686 | - foreach ($parameters as $key => $value) { |
|
| 2687 | - if (in_array($key, $indexedParametersForProperty)) { |
|
| 2688 | - // is this a shitty db? |
|
| 2689 | - if ($this->db->supports4ByteText()) { |
|
| 2690 | - $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); |
|
| 2691 | - } |
|
| 2692 | - |
|
| 2693 | - $query->setParameter('name', $property->name); |
|
| 2694 | - $query->setParameter('parameter', mb_strcut($key, 0, 254)); |
|
| 2695 | - $query->setParameter('value', mb_strcut($value, 0, 254)); |
|
| 2696 | - $query->executeStatement(); |
|
| 2697 | - } |
|
| 2698 | - } |
|
| 2699 | - } |
|
| 2700 | - } |
|
| 2701 | - } |
|
| 2702 | - } |
|
| 2703 | - |
|
| 2704 | - /** |
|
| 2705 | - * deletes all birthday calendars |
|
| 2706 | - */ |
|
| 2707 | - public function deleteAllBirthdayCalendars() { |
|
| 2708 | - $query = $this->db->getQueryBuilder(); |
|
| 2709 | - $result = $query->select(['id'])->from('calendars') |
|
| 2710 | - ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))) |
|
| 2711 | - ->executeQuery(); |
|
| 2712 | - |
|
| 2713 | - $ids = $result->fetchAll(); |
|
| 2714 | - foreach ($ids as $id) { |
|
| 2715 | - $this->deleteCalendar($id['id']); |
|
| 2716 | - } |
|
| 2717 | - } |
|
| 2718 | - |
|
| 2719 | - /** |
|
| 2720 | - * @param $subscriptionId |
|
| 2721 | - */ |
|
| 2722 | - public function purgeAllCachedEventsForSubscription($subscriptionId) { |
|
| 2723 | - $query = $this->db->getQueryBuilder(); |
|
| 2724 | - $query->select('uri') |
|
| 2725 | - ->from('calendarobjects') |
|
| 2726 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2727 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); |
|
| 2728 | - $stmt = $query->executeQuery(); |
|
| 2729 | - |
|
| 2730 | - $uris = []; |
|
| 2731 | - foreach ($stmt->fetchAll() as $row) { |
|
| 2732 | - $uris[] = $row['uri']; |
|
| 2733 | - } |
|
| 2734 | - $stmt->closeCursor(); |
|
| 2735 | - |
|
| 2736 | - $query = $this->db->getQueryBuilder(); |
|
| 2737 | - $query->delete('calendarobjects') |
|
| 2738 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2739 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2740 | - ->executeStatement(); |
|
| 2741 | - |
|
| 2742 | - $query->delete('calendarchanges') |
|
| 2743 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2744 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2745 | - ->executeStatement(); |
|
| 2746 | - |
|
| 2747 | - $query->delete($this->dbObjectPropertiesTable) |
|
| 2748 | - ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2749 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2750 | - ->executeStatement(); |
|
| 2751 | - |
|
| 2752 | - foreach ($uris as $uri) { |
|
| 2753 | - $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION); |
|
| 2754 | - } |
|
| 2755 | - } |
|
| 2756 | - |
|
| 2757 | - /** |
|
| 2758 | - * Move a calendar from one user to another |
|
| 2759 | - * |
|
| 2760 | - * @param string $uriName |
|
| 2761 | - * @param string $uriOrigin |
|
| 2762 | - * @param string $uriDestination |
|
| 2763 | - * @param string $newUriName (optional) the new uriName |
|
| 2764 | - */ |
|
| 2765 | - public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) { |
|
| 2766 | - $query = $this->db->getQueryBuilder(); |
|
| 2767 | - $query->update('calendars') |
|
| 2768 | - ->set('principaluri', $query->createNamedParameter($uriDestination)) |
|
| 2769 | - ->set('uri', $query->createNamedParameter($newUriName ?: $uriName)) |
|
| 2770 | - ->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin))) |
|
| 2771 | - ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName))) |
|
| 2772 | - ->executeStatement(); |
|
| 2773 | - } |
|
| 2774 | - |
|
| 2775 | - /** |
|
| 2776 | - * read VCalendar data into a VCalendar object |
|
| 2777 | - * |
|
| 2778 | - * @param string $objectData |
|
| 2779 | - * @return VCalendar |
|
| 2780 | - */ |
|
| 2781 | - protected function readCalendarData($objectData) { |
|
| 2782 | - return Reader::read($objectData); |
|
| 2783 | - } |
|
| 2784 | - |
|
| 2785 | - /** |
|
| 2786 | - * delete all properties from a given calendar object |
|
| 2787 | - * |
|
| 2788 | - * @param int $calendarId |
|
| 2789 | - * @param int $objectId |
|
| 2790 | - */ |
|
| 2791 | - protected function purgeProperties($calendarId, $objectId) { |
|
| 2792 | - $query = $this->db->getQueryBuilder(); |
|
| 2793 | - $query->delete($this->dbObjectPropertiesTable) |
|
| 2794 | - ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId))) |
|
| 2795 | - ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); |
|
| 2796 | - $query->executeStatement(); |
|
| 2797 | - } |
|
| 2798 | - |
|
| 2799 | - /** |
|
| 2800 | - * get ID from a given calendar object |
|
| 2801 | - * |
|
| 2802 | - * @param int $calendarId |
|
| 2803 | - * @param string $uri |
|
| 2804 | - * @param int $calendarType |
|
| 2805 | - * @return int |
|
| 2806 | - */ |
|
| 2807 | - protected function getCalendarObjectId($calendarId, $uri, $calendarType):int { |
|
| 2808 | - $query = $this->db->getQueryBuilder(); |
|
| 2809 | - $query->select('id') |
|
| 2810 | - ->from('calendarobjects') |
|
| 2811 | - ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
| 2812 | - ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 2813 | - ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 2814 | - |
|
| 2815 | - $result = $query->executeQuery(); |
|
| 2816 | - $objectIds = $result->fetch(); |
|
| 2817 | - $result->closeCursor(); |
|
| 2818 | - |
|
| 2819 | - if (!isset($objectIds['id'])) { |
|
| 2820 | - throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri); |
|
| 2821 | - } |
|
| 2822 | - |
|
| 2823 | - return (int)$objectIds['id']; |
|
| 2824 | - } |
|
| 2825 | - |
|
| 2826 | - /** |
|
| 2827 | - * return legacy endpoint principal name to new principal name |
|
| 2828 | - * |
|
| 2829 | - * @param $principalUri |
|
| 2830 | - * @param $toV2 |
|
| 2831 | - * @return string |
|
| 2832 | - */ |
|
| 2833 | - private function convertPrincipal($principalUri, $toV2) { |
|
| 2834 | - if ($this->principalBackend->getPrincipalPrefix() === 'principals') { |
|
| 2835 | - [, $name] = Uri\split($principalUri); |
|
| 2836 | - if ($toV2 === true) { |
|
| 2837 | - return "principals/users/$name"; |
|
| 2838 | - } |
|
| 2839 | - return "principals/$name"; |
|
| 2840 | - } |
|
| 2841 | - return $principalUri; |
|
| 2842 | - } |
|
| 2843 | - |
|
| 2844 | - /** |
|
| 2845 | - * adds information about an owner to the calendar data |
|
| 2846 | - * |
|
| 2847 | - * @param $calendarInfo |
|
| 2848 | - */ |
|
| 2849 | - private function addOwnerPrincipal(&$calendarInfo) { |
|
| 2850 | - $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'; |
|
| 2851 | - $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'; |
|
| 2852 | - if (isset($calendarInfo[$ownerPrincipalKey])) { |
|
| 2853 | - $uri = $calendarInfo[$ownerPrincipalKey]; |
|
| 2854 | - } else { |
|
| 2855 | - $uri = $calendarInfo['principaluri']; |
|
| 2856 | - } |
|
| 2857 | - |
|
| 2858 | - $principalInformation = $this->principalBackend->getPrincipalByPath($uri); |
|
| 2859 | - if (isset($principalInformation['{DAV:}displayname'])) { |
|
| 2860 | - $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname']; |
|
| 2861 | - } |
|
| 2862 | - } |
|
| 99 | + public const CALENDAR_TYPE_CALENDAR = 0; |
|
| 100 | + public const CALENDAR_TYPE_SUBSCRIPTION = 1; |
|
| 101 | + |
|
| 102 | + public const PERSONAL_CALENDAR_URI = 'personal'; |
|
| 103 | + public const PERSONAL_CALENDAR_NAME = 'Personal'; |
|
| 104 | + |
|
| 105 | + public const RESOURCE_BOOKING_CALENDAR_URI = 'calendar'; |
|
| 106 | + public const RESOURCE_BOOKING_CALENDAR_NAME = 'Calendar'; |
|
| 107 | + |
|
| 108 | + /** |
|
| 109 | + * We need to specify a max date, because we need to stop *somewhere* |
|
| 110 | + * |
|
| 111 | + * On 32 bit system the maximum for a signed integer is 2147483647, so |
|
| 112 | + * MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results |
|
| 113 | + * in 2038-01-19 to avoid problems when the date is converted |
|
| 114 | + * to a unix timestamp. |
|
| 115 | + */ |
|
| 116 | + public const MAX_DATE = '2038-01-01'; |
|
| 117 | + |
|
| 118 | + public const ACCESS_PUBLIC = 4; |
|
| 119 | + public const CLASSIFICATION_PUBLIC = 0; |
|
| 120 | + public const CLASSIFICATION_PRIVATE = 1; |
|
| 121 | + public const CLASSIFICATION_CONFIDENTIAL = 2; |
|
| 122 | + |
|
| 123 | + /** |
|
| 124 | + * List of CalDAV properties, and how they map to database field names |
|
| 125 | + * Add your own properties by simply adding on to this array. |
|
| 126 | + * |
|
| 127 | + * Note that only string-based properties are supported here. |
|
| 128 | + * |
|
| 129 | + * @var array |
|
| 130 | + */ |
|
| 131 | + public $propertyMap = [ |
|
| 132 | + '{DAV:}displayname' => 'displayname', |
|
| 133 | + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', |
|
| 134 | + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', |
|
| 135 | + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', |
|
| 136 | + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', |
|
| 137 | + ]; |
|
| 138 | + |
|
| 139 | + /** |
|
| 140 | + * List of subscription properties, and how they map to database field names. |
|
| 141 | + * |
|
| 142 | + * @var array |
|
| 143 | + */ |
|
| 144 | + public $subscriptionPropertyMap = [ |
|
| 145 | + '{DAV:}displayname' => 'displayname', |
|
| 146 | + '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', |
|
| 147 | + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', |
|
| 148 | + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', |
|
| 149 | + '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', |
|
| 150 | + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', |
|
| 151 | + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', |
|
| 152 | + ]; |
|
| 153 | + |
|
| 154 | + /** @var array properties to index */ |
|
| 155 | + public static $indexProperties = ['CATEGORIES', 'COMMENT', 'DESCRIPTION', |
|
| 156 | + 'LOCATION', 'RESOURCES', 'STATUS', 'SUMMARY', 'ATTENDEE', 'CONTACT', |
|
| 157 | + 'ORGANIZER']; |
|
| 158 | + |
|
| 159 | + /** @var array parameters to index */ |
|
| 160 | + public static $indexParameters = [ |
|
| 161 | + 'ATTENDEE' => ['CN'], |
|
| 162 | + 'ORGANIZER' => ['CN'], |
|
| 163 | + ]; |
|
| 164 | + |
|
| 165 | + /** |
|
| 166 | + * @var string[] Map of uid => display name |
|
| 167 | + */ |
|
| 168 | + protected $userDisplayNames; |
|
| 169 | + |
|
| 170 | + /** @var IDBConnection */ |
|
| 171 | + private $db; |
|
| 172 | + |
|
| 173 | + /** @var Backend */ |
|
| 174 | + private $calendarSharingBackend; |
|
| 175 | + |
|
| 176 | + /** @var Principal */ |
|
| 177 | + private $principalBackend; |
|
| 178 | + |
|
| 179 | + /** @var IUserManager */ |
|
| 180 | + private $userManager; |
|
| 181 | + |
|
| 182 | + /** @var ISecureRandom */ |
|
| 183 | + private $random; |
|
| 184 | + |
|
| 185 | + /** @var ILogger */ |
|
| 186 | + private $logger; |
|
| 187 | + |
|
| 188 | + /** @var IEventDispatcher */ |
|
| 189 | + private $dispatcher; |
|
| 190 | + |
|
| 191 | + /** @var EventDispatcherInterface */ |
|
| 192 | + private $legacyDispatcher; |
|
| 193 | + |
|
| 194 | + /** @var bool */ |
|
| 195 | + private $legacyEndpoint; |
|
| 196 | + |
|
| 197 | + /** @var string */ |
|
| 198 | + private $dbObjectPropertiesTable = 'calendarobjects_props'; |
|
| 199 | + |
|
| 200 | + /** |
|
| 201 | + * CalDavBackend constructor. |
|
| 202 | + * |
|
| 203 | + * @param IDBConnection $db |
|
| 204 | + * @param Principal $principalBackend |
|
| 205 | + * @param IUserManager $userManager |
|
| 206 | + * @param IGroupManager $groupManager |
|
| 207 | + * @param ISecureRandom $random |
|
| 208 | + * @param ILogger $logger |
|
| 209 | + * @param IEventDispatcher $dispatcher |
|
| 210 | + * @param EventDispatcherInterface $legacyDispatcher |
|
| 211 | + * @param bool $legacyEndpoint |
|
| 212 | + */ |
|
| 213 | + public function __construct(IDBConnection $db, |
|
| 214 | + Principal $principalBackend, |
|
| 215 | + IUserManager $userManager, |
|
| 216 | + IGroupManager $groupManager, |
|
| 217 | + ISecureRandom $random, |
|
| 218 | + ILogger $logger, |
|
| 219 | + IEventDispatcher $dispatcher, |
|
| 220 | + EventDispatcherInterface $legacyDispatcher, |
|
| 221 | + bool $legacyEndpoint = false) { |
|
| 222 | + $this->db = $db; |
|
| 223 | + $this->principalBackend = $principalBackend; |
|
| 224 | + $this->userManager = $userManager; |
|
| 225 | + $this->calendarSharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'calendar'); |
|
| 226 | + $this->random = $random; |
|
| 227 | + $this->logger = $logger; |
|
| 228 | + $this->dispatcher = $dispatcher; |
|
| 229 | + $this->legacyDispatcher = $legacyDispatcher; |
|
| 230 | + $this->legacyEndpoint = $legacyEndpoint; |
|
| 231 | + } |
|
| 232 | + |
|
| 233 | + /** |
|
| 234 | + * Return the number of calendars for a principal |
|
| 235 | + * |
|
| 236 | + * By default this excludes the automatically generated birthday calendar |
|
| 237 | + * |
|
| 238 | + * @param $principalUri |
|
| 239 | + * @param bool $excludeBirthday |
|
| 240 | + * @return int |
|
| 241 | + */ |
|
| 242 | + public function getCalendarsForUserCount($principalUri, $excludeBirthday = true) { |
|
| 243 | + $principalUri = $this->convertPrincipal($principalUri, true); |
|
| 244 | + $query = $this->db->getQueryBuilder(); |
|
| 245 | + $query->select($query->func()->count('*')) |
|
| 246 | + ->from('calendars'); |
|
| 247 | + |
|
| 248 | + if ($principalUri === '') { |
|
| 249 | + $query->where($query->expr()->emptyString('principaluri')); |
|
| 250 | + } else { |
|
| 251 | + $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
| 252 | + } |
|
| 253 | + |
|
| 254 | + if ($excludeBirthday) { |
|
| 255 | + $query->andWhere($query->expr()->neq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))); |
|
| 256 | + } |
|
| 257 | + |
|
| 258 | + $result = $query->executeQuery(); |
|
| 259 | + $column = (int)$result->fetchOne(); |
|
| 260 | + $result->closeCursor(); |
|
| 261 | + return $column; |
|
| 262 | + } |
|
| 263 | + |
|
| 264 | + /** |
|
| 265 | + * Returns a list of calendars for a principal. |
|
| 266 | + * |
|
| 267 | + * Every project is an array with the following keys: |
|
| 268 | + * * id, a unique id that will be used by other functions to modify the |
|
| 269 | + * calendar. This can be the same as the uri or a database key. |
|
| 270 | + * * uri, which the basename of the uri with which the calendar is |
|
| 271 | + * accessed. |
|
| 272 | + * * principaluri. The owner of the calendar. Almost always the same as |
|
| 273 | + * principalUri passed to this method. |
|
| 274 | + * |
|
| 275 | + * Furthermore it can contain webdav properties in clark notation. A very |
|
| 276 | + * common one is '{DAV:}displayname'. |
|
| 277 | + * |
|
| 278 | + * Many clients also require: |
|
| 279 | + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set |
|
| 280 | + * For this property, you can just return an instance of |
|
| 281 | + * Sabre\CalDAV\Property\SupportedCalendarComponentSet. |
|
| 282 | + * |
|
| 283 | + * If you return {http://sabredav.org/ns}read-only and set the value to 1, |
|
| 284 | + * ACL will automatically be put in read-only mode. |
|
| 285 | + * |
|
| 286 | + * @param string $principalUri |
|
| 287 | + * @return array |
|
| 288 | + */ |
|
| 289 | + public function getCalendarsForUser($principalUri) { |
|
| 290 | + $principalUriOriginal = $principalUri; |
|
| 291 | + $principalUri = $this->convertPrincipal($principalUri, true); |
|
| 292 | + $fields = array_values($this->propertyMap); |
|
| 293 | + $fields[] = 'id'; |
|
| 294 | + $fields[] = 'uri'; |
|
| 295 | + $fields[] = 'synctoken'; |
|
| 296 | + $fields[] = 'components'; |
|
| 297 | + $fields[] = 'principaluri'; |
|
| 298 | + $fields[] = 'transparent'; |
|
| 299 | + |
|
| 300 | + // Making fields a comma-delimited list |
|
| 301 | + $query = $this->db->getQueryBuilder(); |
|
| 302 | + $query->select($fields) |
|
| 303 | + ->from('calendars') |
|
| 304 | + ->orderBy('calendarorder', 'ASC'); |
|
| 305 | + |
|
| 306 | + if ($principalUri === '') { |
|
| 307 | + $query->where($query->expr()->emptyString('principaluri')); |
|
| 308 | + } else { |
|
| 309 | + $query->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))); |
|
| 310 | + } |
|
| 311 | + |
|
| 312 | + $result = $query->executeQuery(); |
|
| 313 | + |
|
| 314 | + $calendars = []; |
|
| 315 | + while ($row = $result->fetch()) { |
|
| 316 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 317 | + $components = []; |
|
| 318 | + if ($row['components']) { |
|
| 319 | + $components = explode(',',$row['components']); |
|
| 320 | + } |
|
| 321 | + |
|
| 322 | + $calendar = [ |
|
| 323 | + 'id' => $row['id'], |
|
| 324 | + 'uri' => $row['uri'], |
|
| 325 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 326 | + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 327 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 328 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 329 | + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 330 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), |
|
| 331 | + ]; |
|
| 332 | + |
|
| 333 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 334 | + $calendar[$xmlName] = $row[$dbName]; |
|
| 335 | + } |
|
| 336 | + |
|
| 337 | + $this->addOwnerPrincipal($calendar); |
|
| 338 | + |
|
| 339 | + if (!isset($calendars[$calendar['id']])) { |
|
| 340 | + $calendars[$calendar['id']] = $calendar; |
|
| 341 | + } |
|
| 342 | + } |
|
| 343 | + $result->closeCursor(); |
|
| 344 | + |
|
| 345 | + // query for shared calendars |
|
| 346 | + $principals = $this->principalBackend->getGroupMembership($principalUriOriginal, true); |
|
| 347 | + $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUriOriginal)); |
|
| 348 | + |
|
| 349 | + $principals[] = $principalUri; |
|
| 350 | + |
|
| 351 | + $fields = array_values($this->propertyMap); |
|
| 352 | + $fields[] = 'a.id'; |
|
| 353 | + $fields[] = 'a.uri'; |
|
| 354 | + $fields[] = 'a.synctoken'; |
|
| 355 | + $fields[] = 'a.components'; |
|
| 356 | + $fields[] = 'a.principaluri'; |
|
| 357 | + $fields[] = 'a.transparent'; |
|
| 358 | + $fields[] = 's.access'; |
|
| 359 | + $query = $this->db->getQueryBuilder(); |
|
| 360 | + $query->select($fields) |
|
| 361 | + ->from('dav_shares', 's') |
|
| 362 | + ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
| 363 | + ->where($query->expr()->in('s.principaluri', $query->createParameter('principaluri'))) |
|
| 364 | + ->andWhere($query->expr()->eq('s.type', $query->createParameter('type'))) |
|
| 365 | + ->setParameter('type', 'calendar') |
|
| 366 | + ->setParameter('principaluri', $principals, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); |
|
| 367 | + |
|
| 368 | + $result = $query->executeQuery(); |
|
| 369 | + |
|
| 370 | + $readOnlyPropertyName = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only'; |
|
| 371 | + while ($row = $result->fetch()) { |
|
| 372 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 373 | + if ($row['principaluri'] === $principalUri) { |
|
| 374 | + continue; |
|
| 375 | + } |
|
| 376 | + |
|
| 377 | + $readOnly = (int) $row['access'] === Backend::ACCESS_READ; |
|
| 378 | + if (isset($calendars[$row['id']])) { |
|
| 379 | + if ($readOnly) { |
|
| 380 | + // New share can not have more permissions then the old one. |
|
| 381 | + continue; |
|
| 382 | + } |
|
| 383 | + if (isset($calendars[$row['id']][$readOnlyPropertyName]) && |
|
| 384 | + $calendars[$row['id']][$readOnlyPropertyName] === 0) { |
|
| 385 | + // Old share is already read-write, no more permissions can be gained |
|
| 386 | + continue; |
|
| 387 | + } |
|
| 388 | + } |
|
| 389 | + |
|
| 390 | + [, $name] = Uri\split($row['principaluri']); |
|
| 391 | + $uri = $row['uri'] . '_shared_by_' . $name; |
|
| 392 | + $row['displayname'] = $row['displayname'] . ' (' . $this->getUserDisplayName($name) . ')'; |
|
| 393 | + $components = []; |
|
| 394 | + if ($row['components']) { |
|
| 395 | + $components = explode(',',$row['components']); |
|
| 396 | + } |
|
| 397 | + $calendar = [ |
|
| 398 | + 'id' => $row['id'], |
|
| 399 | + 'uri' => $uri, |
|
| 400 | + 'principaluri' => $this->convertPrincipal($principalUri, !$this->legacyEndpoint), |
|
| 401 | + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 402 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 403 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 404 | + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp('transparent'), |
|
| 405 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 406 | + $readOnlyPropertyName => $readOnly, |
|
| 407 | + ]; |
|
| 408 | + |
|
| 409 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 410 | + $calendar[$xmlName] = $row[$dbName]; |
|
| 411 | + } |
|
| 412 | + |
|
| 413 | + $this->addOwnerPrincipal($calendar); |
|
| 414 | + |
|
| 415 | + $calendars[$calendar['id']] = $calendar; |
|
| 416 | + } |
|
| 417 | + $result->closeCursor(); |
|
| 418 | + |
|
| 419 | + return array_values($calendars); |
|
| 420 | + } |
|
| 421 | + |
|
| 422 | + /** |
|
| 423 | + * @param $principalUri |
|
| 424 | + * @return array |
|
| 425 | + */ |
|
| 426 | + public function getUsersOwnCalendars($principalUri) { |
|
| 427 | + $principalUri = $this->convertPrincipal($principalUri, true); |
|
| 428 | + $fields = array_values($this->propertyMap); |
|
| 429 | + $fields[] = 'id'; |
|
| 430 | + $fields[] = 'uri'; |
|
| 431 | + $fields[] = 'synctoken'; |
|
| 432 | + $fields[] = 'components'; |
|
| 433 | + $fields[] = 'principaluri'; |
|
| 434 | + $fields[] = 'transparent'; |
|
| 435 | + // Making fields a comma-delimited list |
|
| 436 | + $query = $this->db->getQueryBuilder(); |
|
| 437 | + $query->select($fields)->from('calendars') |
|
| 438 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 439 | + ->orderBy('calendarorder', 'ASC'); |
|
| 440 | + $stmt = $query->executeQuery(); |
|
| 441 | + $calendars = []; |
|
| 442 | + while ($row = $stmt->fetch()) { |
|
| 443 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 444 | + $components = []; |
|
| 445 | + if ($row['components']) { |
|
| 446 | + $components = explode(',',$row['components']); |
|
| 447 | + } |
|
| 448 | + $calendar = [ |
|
| 449 | + 'id' => $row['id'], |
|
| 450 | + 'uri' => $row['uri'], |
|
| 451 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 452 | + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 453 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 454 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 455 | + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 456 | + ]; |
|
| 457 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 458 | + $calendar[$xmlName] = $row[$dbName]; |
|
| 459 | + } |
|
| 460 | + |
|
| 461 | + $this->addOwnerPrincipal($calendar); |
|
| 462 | + |
|
| 463 | + if (!isset($calendars[$calendar['id']])) { |
|
| 464 | + $calendars[$calendar['id']] = $calendar; |
|
| 465 | + } |
|
| 466 | + } |
|
| 467 | + $stmt->closeCursor(); |
|
| 468 | + return array_values($calendars); |
|
| 469 | + } |
|
| 470 | + |
|
| 471 | + |
|
| 472 | + /** |
|
| 473 | + * @param $uid |
|
| 474 | + * @return string |
|
| 475 | + */ |
|
| 476 | + private function getUserDisplayName($uid) { |
|
| 477 | + if (!isset($this->userDisplayNames[$uid])) { |
|
| 478 | + $user = $this->userManager->get($uid); |
|
| 479 | + |
|
| 480 | + if ($user instanceof IUser) { |
|
| 481 | + $this->userDisplayNames[$uid] = $user->getDisplayName(); |
|
| 482 | + } else { |
|
| 483 | + $this->userDisplayNames[$uid] = $uid; |
|
| 484 | + } |
|
| 485 | + } |
|
| 486 | + |
|
| 487 | + return $this->userDisplayNames[$uid]; |
|
| 488 | + } |
|
| 489 | + |
|
| 490 | + /** |
|
| 491 | + * @return array |
|
| 492 | + */ |
|
| 493 | + public function getPublicCalendars() { |
|
| 494 | + $fields = array_values($this->propertyMap); |
|
| 495 | + $fields[] = 'a.id'; |
|
| 496 | + $fields[] = 'a.uri'; |
|
| 497 | + $fields[] = 'a.synctoken'; |
|
| 498 | + $fields[] = 'a.components'; |
|
| 499 | + $fields[] = 'a.principaluri'; |
|
| 500 | + $fields[] = 'a.transparent'; |
|
| 501 | + $fields[] = 's.access'; |
|
| 502 | + $fields[] = 's.publicuri'; |
|
| 503 | + $calendars = []; |
|
| 504 | + $query = $this->db->getQueryBuilder(); |
|
| 505 | + $result = $query->select($fields) |
|
| 506 | + ->from('dav_shares', 's') |
|
| 507 | + ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
| 508 | + ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC))) |
|
| 509 | + ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar'))) |
|
| 510 | + ->executeQuery(); |
|
| 511 | + |
|
| 512 | + while ($row = $result->fetch()) { |
|
| 513 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 514 | + [, $name] = Uri\split($row['principaluri']); |
|
| 515 | + $row['displayname'] = $row['displayname'] . "($name)"; |
|
| 516 | + $components = []; |
|
| 517 | + if ($row['components']) { |
|
| 518 | + $components = explode(',',$row['components']); |
|
| 519 | + } |
|
| 520 | + $calendar = [ |
|
| 521 | + 'id' => $row['id'], |
|
| 522 | + 'uri' => $row['publicuri'], |
|
| 523 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 524 | + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 525 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 526 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 527 | + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 528 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], $this->legacyEndpoint), |
|
| 529 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, |
|
| 530 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC, |
|
| 531 | + ]; |
|
| 532 | + |
|
| 533 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 534 | + $calendar[$xmlName] = $row[$dbName]; |
|
| 535 | + } |
|
| 536 | + |
|
| 537 | + $this->addOwnerPrincipal($calendar); |
|
| 538 | + |
|
| 539 | + if (!isset($calendars[$calendar['id']])) { |
|
| 540 | + $calendars[$calendar['id']] = $calendar; |
|
| 541 | + } |
|
| 542 | + } |
|
| 543 | + $result->closeCursor(); |
|
| 544 | + |
|
| 545 | + return array_values($calendars); |
|
| 546 | + } |
|
| 547 | + |
|
| 548 | + /** |
|
| 549 | + * @param string $uri |
|
| 550 | + * @return array |
|
| 551 | + * @throws NotFound |
|
| 552 | + */ |
|
| 553 | + public function getPublicCalendar($uri) { |
|
| 554 | + $fields = array_values($this->propertyMap); |
|
| 555 | + $fields[] = 'a.id'; |
|
| 556 | + $fields[] = 'a.uri'; |
|
| 557 | + $fields[] = 'a.synctoken'; |
|
| 558 | + $fields[] = 'a.components'; |
|
| 559 | + $fields[] = 'a.principaluri'; |
|
| 560 | + $fields[] = 'a.transparent'; |
|
| 561 | + $fields[] = 's.access'; |
|
| 562 | + $fields[] = 's.publicuri'; |
|
| 563 | + $query = $this->db->getQueryBuilder(); |
|
| 564 | + $result = $query->select($fields) |
|
| 565 | + ->from('dav_shares', 's') |
|
| 566 | + ->join('s', 'calendars', 'a', $query->expr()->eq('s.resourceid', 'a.id')) |
|
| 567 | + ->where($query->expr()->in('s.access', $query->createNamedParameter(self::ACCESS_PUBLIC))) |
|
| 568 | + ->andWhere($query->expr()->eq('s.type', $query->createNamedParameter('calendar'))) |
|
| 569 | + ->andWhere($query->expr()->eq('s.publicuri', $query->createNamedParameter($uri))) |
|
| 570 | + ->executeQuery(); |
|
| 571 | + |
|
| 572 | + $row = $result->fetch(); |
|
| 573 | + |
|
| 574 | + $result->closeCursor(); |
|
| 575 | + |
|
| 576 | + if ($row === false) { |
|
| 577 | + throw new NotFound('Node with name \'' . $uri . '\' could not be found'); |
|
| 578 | + } |
|
| 579 | + |
|
| 580 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 581 | + [, $name] = Uri\split($row['principaluri']); |
|
| 582 | + $row['displayname'] = $row['displayname'] . ' ' . "($name)"; |
|
| 583 | + $components = []; |
|
| 584 | + if ($row['components']) { |
|
| 585 | + $components = explode(',',$row['components']); |
|
| 586 | + } |
|
| 587 | + $calendar = [ |
|
| 588 | + 'id' => $row['id'], |
|
| 589 | + 'uri' => $row['publicuri'], |
|
| 590 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 591 | + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 592 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 593 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 594 | + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 595 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 596 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}read-only' => (int)$row['access'] === Backend::ACCESS_READ, |
|
| 597 | + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}public' => (int)$row['access'] === self::ACCESS_PUBLIC, |
|
| 598 | + ]; |
|
| 599 | + |
|
| 600 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 601 | + $calendar[$xmlName] = $row[$dbName]; |
|
| 602 | + } |
|
| 603 | + |
|
| 604 | + $this->addOwnerPrincipal($calendar); |
|
| 605 | + |
|
| 606 | + return $calendar; |
|
| 607 | + } |
|
| 608 | + |
|
| 609 | + /** |
|
| 610 | + * @param string $principal |
|
| 611 | + * @param string $uri |
|
| 612 | + * @return array|null |
|
| 613 | + */ |
|
| 614 | + public function getCalendarByUri($principal, $uri) { |
|
| 615 | + $fields = array_values($this->propertyMap); |
|
| 616 | + $fields[] = 'id'; |
|
| 617 | + $fields[] = 'uri'; |
|
| 618 | + $fields[] = 'synctoken'; |
|
| 619 | + $fields[] = 'components'; |
|
| 620 | + $fields[] = 'principaluri'; |
|
| 621 | + $fields[] = 'transparent'; |
|
| 622 | + |
|
| 623 | + // Making fields a comma-delimited list |
|
| 624 | + $query = $this->db->getQueryBuilder(); |
|
| 625 | + $query->select($fields)->from('calendars') |
|
| 626 | + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
| 627 | + ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) |
|
| 628 | + ->setMaxResults(1); |
|
| 629 | + $stmt = $query->executeQuery(); |
|
| 630 | + |
|
| 631 | + $row = $stmt->fetch(); |
|
| 632 | + $stmt->closeCursor(); |
|
| 633 | + if ($row === false) { |
|
| 634 | + return null; |
|
| 635 | + } |
|
| 636 | + |
|
| 637 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 638 | + $components = []; |
|
| 639 | + if ($row['components']) { |
|
| 640 | + $components = explode(',',$row['components']); |
|
| 641 | + } |
|
| 642 | + |
|
| 643 | + $calendar = [ |
|
| 644 | + 'id' => $row['id'], |
|
| 645 | + 'uri' => $row['uri'], |
|
| 646 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 647 | + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 648 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 649 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 650 | + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 651 | + ]; |
|
| 652 | + |
|
| 653 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 654 | + $calendar[$xmlName] = $row[$dbName]; |
|
| 655 | + } |
|
| 656 | + |
|
| 657 | + $this->addOwnerPrincipal($calendar); |
|
| 658 | + |
|
| 659 | + return $calendar; |
|
| 660 | + } |
|
| 661 | + |
|
| 662 | + /** |
|
| 663 | + * @param $calendarId |
|
| 664 | + * @return array|null |
|
| 665 | + */ |
|
| 666 | + public function getCalendarById($calendarId) { |
|
| 667 | + $fields = array_values($this->propertyMap); |
|
| 668 | + $fields[] = 'id'; |
|
| 669 | + $fields[] = 'uri'; |
|
| 670 | + $fields[] = 'synctoken'; |
|
| 671 | + $fields[] = 'components'; |
|
| 672 | + $fields[] = 'principaluri'; |
|
| 673 | + $fields[] = 'transparent'; |
|
| 674 | + |
|
| 675 | + // Making fields a comma-delimited list |
|
| 676 | + $query = $this->db->getQueryBuilder(); |
|
| 677 | + $query->select($fields)->from('calendars') |
|
| 678 | + ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))) |
|
| 679 | + ->setMaxResults(1); |
|
| 680 | + $stmt = $query->executeQuery(); |
|
| 681 | + |
|
| 682 | + $row = $stmt->fetch(); |
|
| 683 | + $stmt->closeCursor(); |
|
| 684 | + if ($row === false) { |
|
| 685 | + return null; |
|
| 686 | + } |
|
| 687 | + |
|
| 688 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 689 | + $components = []; |
|
| 690 | + if ($row['components']) { |
|
| 691 | + $components = explode(',',$row['components']); |
|
| 692 | + } |
|
| 693 | + |
|
| 694 | + $calendar = [ |
|
| 695 | + 'id' => $row['id'], |
|
| 696 | + 'uri' => $row['uri'], |
|
| 697 | + 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), |
|
| 698 | + '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), |
|
| 699 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 700 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), |
|
| 701 | + '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), |
|
| 702 | + ]; |
|
| 703 | + |
|
| 704 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 705 | + $calendar[$xmlName] = $row[$dbName]; |
|
| 706 | + } |
|
| 707 | + |
|
| 708 | + $this->addOwnerPrincipal($calendar); |
|
| 709 | + |
|
| 710 | + return $calendar; |
|
| 711 | + } |
|
| 712 | + |
|
| 713 | + /** |
|
| 714 | + * @param $subscriptionId |
|
| 715 | + */ |
|
| 716 | + public function getSubscriptionById($subscriptionId) { |
|
| 717 | + $fields = array_values($this->subscriptionPropertyMap); |
|
| 718 | + $fields[] = 'id'; |
|
| 719 | + $fields[] = 'uri'; |
|
| 720 | + $fields[] = 'source'; |
|
| 721 | + $fields[] = 'synctoken'; |
|
| 722 | + $fields[] = 'principaluri'; |
|
| 723 | + $fields[] = 'lastmodified'; |
|
| 724 | + |
|
| 725 | + $query = $this->db->getQueryBuilder(); |
|
| 726 | + $query->select($fields) |
|
| 727 | + ->from('calendarsubscriptions') |
|
| 728 | + ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) |
|
| 729 | + ->orderBy('calendarorder', 'asc'); |
|
| 730 | + $stmt = $query->executeQuery(); |
|
| 731 | + |
|
| 732 | + $row = $stmt->fetch(); |
|
| 733 | + $stmt->closeCursor(); |
|
| 734 | + if ($row === false) { |
|
| 735 | + return null; |
|
| 736 | + } |
|
| 737 | + |
|
| 738 | + $row['principaluri'] = (string) $row['principaluri']; |
|
| 739 | + $subscription = [ |
|
| 740 | + 'id' => $row['id'], |
|
| 741 | + 'uri' => $row['uri'], |
|
| 742 | + 'principaluri' => $row['principaluri'], |
|
| 743 | + 'source' => $row['source'], |
|
| 744 | + 'lastmodified' => $row['lastmodified'], |
|
| 745 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), |
|
| 746 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 747 | + ]; |
|
| 748 | + |
|
| 749 | + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { |
|
| 750 | + if (!is_null($row[$dbName])) { |
|
| 751 | + $subscription[$xmlName] = $row[$dbName]; |
|
| 752 | + } |
|
| 753 | + } |
|
| 754 | + |
|
| 755 | + return $subscription; |
|
| 756 | + } |
|
| 757 | + |
|
| 758 | + /** |
|
| 759 | + * Creates a new calendar for a principal. |
|
| 760 | + * |
|
| 761 | + * If the creation was a success, an id must be returned that can be used to reference |
|
| 762 | + * this calendar in other methods, such as updateCalendar. |
|
| 763 | + * |
|
| 764 | + * @param string $principalUri |
|
| 765 | + * @param string $calendarUri |
|
| 766 | + * @param array $properties |
|
| 767 | + * @return int |
|
| 768 | + */ |
|
| 769 | + public function createCalendar($principalUri, $calendarUri, array $properties) { |
|
| 770 | + $values = [ |
|
| 771 | + 'principaluri' => $this->convertPrincipal($principalUri, true), |
|
| 772 | + 'uri' => $calendarUri, |
|
| 773 | + 'synctoken' => 1, |
|
| 774 | + 'transparent' => 0, |
|
| 775 | + 'components' => 'VEVENT,VTODO', |
|
| 776 | + 'displayname' => $calendarUri |
|
| 777 | + ]; |
|
| 778 | + |
|
| 779 | + // Default value |
|
| 780 | + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; |
|
| 781 | + if (isset($properties[$sccs])) { |
|
| 782 | + if (!($properties[$sccs] instanceof SupportedCalendarComponentSet)) { |
|
| 783 | + throw new DAV\Exception('The ' . $sccs . ' property must be of type: \Sabre\CalDAV\Property\SupportedCalendarComponentSet'); |
|
| 784 | + } |
|
| 785 | + $values['components'] = implode(',',$properties[$sccs]->getValue()); |
|
| 786 | + } elseif (isset($properties['components'])) { |
|
| 787 | + // Allow to provide components internally without having |
|
| 788 | + // to create a SupportedCalendarComponentSet object |
|
| 789 | + $values['components'] = $properties['components']; |
|
| 790 | + } |
|
| 791 | + |
|
| 792 | + $transp = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; |
|
| 793 | + if (isset($properties[$transp])) { |
|
| 794 | + $values['transparent'] = (int) ($properties[$transp]->getValue() === 'transparent'); |
|
| 795 | + } |
|
| 796 | + |
|
| 797 | + foreach ($this->propertyMap as $xmlName => $dbName) { |
|
| 798 | + if (isset($properties[$xmlName])) { |
|
| 799 | + $values[$dbName] = $properties[$xmlName]; |
|
| 800 | + } |
|
| 801 | + } |
|
| 802 | + |
|
| 803 | + $query = $this->db->getQueryBuilder(); |
|
| 804 | + $query->insert('calendars'); |
|
| 805 | + foreach ($values as $column => $value) { |
|
| 806 | + $query->setValue($column, $query->createNamedParameter($value)); |
|
| 807 | + } |
|
| 808 | + $query->executeStatement(); |
|
| 809 | + $calendarId = $query->getLastInsertId(); |
|
| 810 | + |
|
| 811 | + $calendarData = $this->getCalendarById($calendarId); |
|
| 812 | + $this->dispatcher->dispatchTyped(new CalendarCreatedEvent((int)$calendarId, $calendarData)); |
|
| 813 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendar', new GenericEvent( |
|
| 814 | + '\OCA\DAV\CalDAV\CalDavBackend::createCalendar', |
|
| 815 | + [ |
|
| 816 | + 'calendarId' => $calendarId, |
|
| 817 | + 'calendarData' => $calendarData, |
|
| 818 | + ])); |
|
| 819 | + |
|
| 820 | + return $calendarId; |
|
| 821 | + } |
|
| 822 | + |
|
| 823 | + /** |
|
| 824 | + * Updates properties for a calendar. |
|
| 825 | + * |
|
| 826 | + * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
| 827 | + * To do the actual updates, you must tell this object which properties |
|
| 828 | + * you're going to process with the handle() method. |
|
| 829 | + * |
|
| 830 | + * Calling the handle method is like telling the PropPatch object "I |
|
| 831 | + * promise I can handle updating this property". |
|
| 832 | + * |
|
| 833 | + * Read the PropPatch documentation for more info and examples. |
|
| 834 | + * |
|
| 835 | + * @param mixed $calendarId |
|
| 836 | + * @param PropPatch $propPatch |
|
| 837 | + * @return void |
|
| 838 | + */ |
|
| 839 | + public function updateCalendar($calendarId, PropPatch $propPatch) { |
|
| 840 | + $supportedProperties = array_keys($this->propertyMap); |
|
| 841 | + $supportedProperties[] = '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp'; |
|
| 842 | + |
|
| 843 | + $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId) { |
|
| 844 | + $newValues = []; |
|
| 845 | + foreach ($mutations as $propertyName => $propertyValue) { |
|
| 846 | + switch ($propertyName) { |
|
| 847 | + case '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp': |
|
| 848 | + $fieldName = 'transparent'; |
|
| 849 | + $newValues[$fieldName] = (int) ($propertyValue->getValue() === 'transparent'); |
|
| 850 | + break; |
|
| 851 | + default: |
|
| 852 | + $fieldName = $this->propertyMap[$propertyName]; |
|
| 853 | + $newValues[$fieldName] = $propertyValue; |
|
| 854 | + break; |
|
| 855 | + } |
|
| 856 | + } |
|
| 857 | + $query = $this->db->getQueryBuilder(); |
|
| 858 | + $query->update('calendars'); |
|
| 859 | + foreach ($newValues as $fieldName => $value) { |
|
| 860 | + $query->set($fieldName, $query->createNamedParameter($value)); |
|
| 861 | + } |
|
| 862 | + $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); |
|
| 863 | + $query->executeStatement(); |
|
| 864 | + |
|
| 865 | + $this->addChange($calendarId, "", 2); |
|
| 866 | + |
|
| 867 | + $calendarData = $this->getCalendarById($calendarId); |
|
| 868 | + $shares = $this->getShares($calendarId); |
|
| 869 | + $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations)); |
|
| 870 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', new GenericEvent( |
|
| 871 | + '\OCA\DAV\CalDAV\CalDavBackend::updateCalendar', |
|
| 872 | + [ |
|
| 873 | + 'calendarId' => $calendarId, |
|
| 874 | + 'calendarData' => $calendarData, |
|
| 875 | + 'shares' => $shares, |
|
| 876 | + 'propertyMutations' => $mutations, |
|
| 877 | + ])); |
|
| 878 | + |
|
| 879 | + return true; |
|
| 880 | + }); |
|
| 881 | + } |
|
| 882 | + |
|
| 883 | + /** |
|
| 884 | + * Delete a calendar and all it's objects |
|
| 885 | + * |
|
| 886 | + * @param mixed $calendarId |
|
| 887 | + * @return void |
|
| 888 | + */ |
|
| 889 | + public function deleteCalendar($calendarId) { |
|
| 890 | + $calendarData = $this->getCalendarById($calendarId); |
|
| 891 | + $shares = $this->getShares($calendarId); |
|
| 892 | + |
|
| 893 | + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `calendartype` = ?'); |
|
| 894 | + $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]); |
|
| 895 | + |
|
| 896 | + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendars` WHERE `id` = ?'); |
|
| 897 | + $stmt->execute([$calendarId]); |
|
| 898 | + |
|
| 899 | + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarchanges` WHERE `calendarid` = ? AND `calendartype` = ?'); |
|
| 900 | + $stmt->execute([$calendarId, self::CALENDAR_TYPE_CALENDAR]); |
|
| 901 | + |
|
| 902 | + $this->calendarSharingBackend->deleteAllShares($calendarId); |
|
| 903 | + |
|
| 904 | + $query = $this->db->getQueryBuilder(); |
|
| 905 | + $query->delete($this->dbObjectPropertiesTable) |
|
| 906 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 907 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))) |
|
| 908 | + ->executeStatement(); |
|
| 909 | + |
|
| 910 | + // Only dispatch if we actually deleted anything |
|
| 911 | + if ($calendarData) { |
|
| 912 | + $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares)); |
|
| 913 | + } |
|
| 914 | + } |
|
| 915 | + |
|
| 916 | + /** |
|
| 917 | + * Delete all of an user's shares |
|
| 918 | + * |
|
| 919 | + * @param string $principaluri |
|
| 920 | + * @return void |
|
| 921 | + */ |
|
| 922 | + public function deleteAllSharesByUser($principaluri) { |
|
| 923 | + $this->calendarSharingBackend->deleteAllSharesByUser($principaluri); |
|
| 924 | + } |
|
| 925 | + |
|
| 926 | + /** |
|
| 927 | + * Returns all calendar objects within a calendar. |
|
| 928 | + * |
|
| 929 | + * Every item contains an array with the following keys: |
|
| 930 | + * * calendardata - The iCalendar-compatible calendar data |
|
| 931 | + * * uri - a unique key which will be used to construct the uri. This can |
|
| 932 | + * be any arbitrary string, but making sure it ends with '.ics' is a |
|
| 933 | + * good idea. This is only the basename, or filename, not the full |
|
| 934 | + * path. |
|
| 935 | + * * lastmodified - a timestamp of the last modification time |
|
| 936 | + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: |
|
| 937 | + * '"abcdef"') |
|
| 938 | + * * size - The size of the calendar objects, in bytes. |
|
| 939 | + * * component - optional, a string containing the type of object, such |
|
| 940 | + * as 'vevent' or 'vtodo'. If specified, this will be used to populate |
|
| 941 | + * the Content-Type header. |
|
| 942 | + * |
|
| 943 | + * Note that the etag is optional, but it's highly encouraged to return for |
|
| 944 | + * speed reasons. |
|
| 945 | + * |
|
| 946 | + * The calendardata is also optional. If it's not returned |
|
| 947 | + * 'getCalendarObject' will be called later, which *is* expected to return |
|
| 948 | + * calendardata. |
|
| 949 | + * |
|
| 950 | + * If neither etag or size are specified, the calendardata will be |
|
| 951 | + * used/fetched to determine these numbers. If both are specified the |
|
| 952 | + * amount of times this is needed is reduced by a great degree. |
|
| 953 | + * |
|
| 954 | + * @param mixed $calendarId |
|
| 955 | + * @param int $calendarType |
|
| 956 | + * @return array |
|
| 957 | + */ |
|
| 958 | + public function getCalendarObjects($calendarId, $calendarType = self::CALENDAR_TYPE_CALENDAR):array { |
|
| 959 | + $query = $this->db->getQueryBuilder(); |
|
| 960 | + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'componenttype', 'classification']) |
|
| 961 | + ->from('calendarobjects') |
|
| 962 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 963 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 964 | + $stmt = $query->executeQuery(); |
|
| 965 | + |
|
| 966 | + $result = []; |
|
| 967 | + foreach ($stmt->fetchAll() as $row) { |
|
| 968 | + $result[] = [ |
|
| 969 | + 'id' => $row['id'], |
|
| 970 | + 'uri' => $row['uri'], |
|
| 971 | + 'lastmodified' => $row['lastmodified'], |
|
| 972 | + 'etag' => '"' . $row['etag'] . '"', |
|
| 973 | + 'calendarid' => $row['calendarid'], |
|
| 974 | + 'size' => (int)$row['size'], |
|
| 975 | + 'component' => strtolower($row['componenttype']), |
|
| 976 | + 'classification' => (int)$row['classification'] |
|
| 977 | + ]; |
|
| 978 | + } |
|
| 979 | + $stmt->closeCursor(); |
|
| 980 | + |
|
| 981 | + return $result; |
|
| 982 | + } |
|
| 983 | + |
|
| 984 | + /** |
|
| 985 | + * Returns information from a single calendar object, based on it's object |
|
| 986 | + * uri. |
|
| 987 | + * |
|
| 988 | + * The object uri is only the basename, or filename and not a full path. |
|
| 989 | + * |
|
| 990 | + * The returned array must have the same keys as getCalendarObjects. The |
|
| 991 | + * 'calendardata' object is required here though, while it's not required |
|
| 992 | + * for getCalendarObjects. |
|
| 993 | + * |
|
| 994 | + * This method must return null if the object did not exist. |
|
| 995 | + * |
|
| 996 | + * @param mixed $calendarId |
|
| 997 | + * @param string $objectUri |
|
| 998 | + * @param int $calendarType |
|
| 999 | + * @return array|null |
|
| 1000 | + */ |
|
| 1001 | + public function getCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1002 | + $query = $this->db->getQueryBuilder(); |
|
| 1003 | + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification']) |
|
| 1004 | + ->from('calendarobjects') |
|
| 1005 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1006 | + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 1007 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 1008 | + $stmt = $query->executeQuery(); |
|
| 1009 | + $row = $stmt->fetch(); |
|
| 1010 | + $stmt->closeCursor(); |
|
| 1011 | + |
|
| 1012 | + if (!$row) { |
|
| 1013 | + return null; |
|
| 1014 | + } |
|
| 1015 | + |
|
| 1016 | + return [ |
|
| 1017 | + 'id' => $row['id'], |
|
| 1018 | + 'uri' => $row['uri'], |
|
| 1019 | + 'lastmodified' => $row['lastmodified'], |
|
| 1020 | + 'etag' => '"' . $row['etag'] . '"', |
|
| 1021 | + 'calendarid' => $row['calendarid'], |
|
| 1022 | + 'size' => (int)$row['size'], |
|
| 1023 | + 'calendardata' => $this->readBlob($row['calendardata']), |
|
| 1024 | + 'component' => strtolower($row['componenttype']), |
|
| 1025 | + 'classification' => (int)$row['classification'] |
|
| 1026 | + ]; |
|
| 1027 | + } |
|
| 1028 | + |
|
| 1029 | + /** |
|
| 1030 | + * Returns a list of calendar objects. |
|
| 1031 | + * |
|
| 1032 | + * This method should work identical to getCalendarObject, but instead |
|
| 1033 | + * return all the calendar objects in the list as an array. |
|
| 1034 | + * |
|
| 1035 | + * If the backend supports this, it may allow for some speed-ups. |
|
| 1036 | + * |
|
| 1037 | + * @param mixed $calendarId |
|
| 1038 | + * @param string[] $uris |
|
| 1039 | + * @param int $calendarType |
|
| 1040 | + * @return array |
|
| 1041 | + */ |
|
| 1042 | + public function getMultipleCalendarObjects($calendarId, array $uris, $calendarType = self::CALENDAR_TYPE_CALENDAR):array { |
|
| 1043 | + if (empty($uris)) { |
|
| 1044 | + return []; |
|
| 1045 | + } |
|
| 1046 | + |
|
| 1047 | + $chunks = array_chunk($uris, 100); |
|
| 1048 | + $objects = []; |
|
| 1049 | + |
|
| 1050 | + $query = $this->db->getQueryBuilder(); |
|
| 1051 | + $query->select(['id', 'uri', 'lastmodified', 'etag', 'calendarid', 'size', 'calendardata', 'componenttype', 'classification']) |
|
| 1052 | + ->from('calendarobjects') |
|
| 1053 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1054 | + ->andWhere($query->expr()->in('uri', $query->createParameter('uri'))) |
|
| 1055 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 1056 | + |
|
| 1057 | + foreach ($chunks as $uris) { |
|
| 1058 | + $query->setParameter('uri', $uris, IQueryBuilder::PARAM_STR_ARRAY); |
|
| 1059 | + $result = $query->executeQuery(); |
|
| 1060 | + |
|
| 1061 | + while ($row = $result->fetch()) { |
|
| 1062 | + $objects[] = [ |
|
| 1063 | + 'id' => $row['id'], |
|
| 1064 | + 'uri' => $row['uri'], |
|
| 1065 | + 'lastmodified' => $row['lastmodified'], |
|
| 1066 | + 'etag' => '"' . $row['etag'] . '"', |
|
| 1067 | + 'calendarid' => $row['calendarid'], |
|
| 1068 | + 'size' => (int)$row['size'], |
|
| 1069 | + 'calendardata' => $this->readBlob($row['calendardata']), |
|
| 1070 | + 'component' => strtolower($row['componenttype']), |
|
| 1071 | + 'classification' => (int)$row['classification'] |
|
| 1072 | + ]; |
|
| 1073 | + } |
|
| 1074 | + $result->closeCursor(); |
|
| 1075 | + } |
|
| 1076 | + |
|
| 1077 | + return $objects; |
|
| 1078 | + } |
|
| 1079 | + |
|
| 1080 | + /** |
|
| 1081 | + * Creates a new calendar object. |
|
| 1082 | + * |
|
| 1083 | + * The object uri is only the basename, or filename and not a full path. |
|
| 1084 | + * |
|
| 1085 | + * It is possible return an etag from this function, which will be used in |
|
| 1086 | + * the response to this PUT request. Note that the ETag must be surrounded |
|
| 1087 | + * by double-quotes. |
|
| 1088 | + * |
|
| 1089 | + * However, you should only really return this ETag if you don't mangle the |
|
| 1090 | + * calendar-data. If the result of a subsequent GET to this object is not |
|
| 1091 | + * the exact same as this request body, you should omit the ETag. |
|
| 1092 | + * |
|
| 1093 | + * @param mixed $calendarId |
|
| 1094 | + * @param string $objectUri |
|
| 1095 | + * @param string $calendarData |
|
| 1096 | + * @param int $calendarType |
|
| 1097 | + * @return string |
|
| 1098 | + */ |
|
| 1099 | + public function createCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1100 | + $extraData = $this->getDenormalizedData($calendarData); |
|
| 1101 | + |
|
| 1102 | + $q = $this->db->getQueryBuilder(); |
|
| 1103 | + $q->select($q->func()->count('*')) |
|
| 1104 | + ->from('calendarobjects') |
|
| 1105 | + ->where($q->expr()->eq('calendarid', $q->createNamedParameter($calendarId))) |
|
| 1106 | + ->andWhere($q->expr()->eq('uid', $q->createNamedParameter($extraData['uid']))) |
|
| 1107 | + ->andWhere($q->expr()->eq('calendartype', $q->createNamedParameter($calendarType))); |
|
| 1108 | + |
|
| 1109 | + $result = $q->executeQuery(); |
|
| 1110 | + $count = (int) $result->fetchOne(); |
|
| 1111 | + $result->closeCursor(); |
|
| 1112 | + |
|
| 1113 | + if ($count !== 0) { |
|
| 1114 | + throw new \Sabre\DAV\Exception\BadRequest('Calendar object with uid already exists in this calendar collection.'); |
|
| 1115 | + } |
|
| 1116 | + |
|
| 1117 | + $query = $this->db->getQueryBuilder(); |
|
| 1118 | + $query->insert('calendarobjects') |
|
| 1119 | + ->values([ |
|
| 1120 | + 'calendarid' => $query->createNamedParameter($calendarId), |
|
| 1121 | + 'uri' => $query->createNamedParameter($objectUri), |
|
| 1122 | + 'calendardata' => $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB), |
|
| 1123 | + 'lastmodified' => $query->createNamedParameter(time()), |
|
| 1124 | + 'etag' => $query->createNamedParameter($extraData['etag']), |
|
| 1125 | + 'size' => $query->createNamedParameter($extraData['size']), |
|
| 1126 | + 'componenttype' => $query->createNamedParameter($extraData['componentType']), |
|
| 1127 | + 'firstoccurence' => $query->createNamedParameter($extraData['firstOccurence']), |
|
| 1128 | + 'lastoccurence' => $query->createNamedParameter($extraData['lastOccurence']), |
|
| 1129 | + 'classification' => $query->createNamedParameter($extraData['classification']), |
|
| 1130 | + 'uid' => $query->createNamedParameter($extraData['uid']), |
|
| 1131 | + 'calendartype' => $query->createNamedParameter($calendarType), |
|
| 1132 | + ]) |
|
| 1133 | + ->executeStatement(); |
|
| 1134 | + |
|
| 1135 | + $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); |
|
| 1136 | + $this->addChange($calendarId, $objectUri, 1, $calendarType); |
|
| 1137 | + |
|
| 1138 | + $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); |
|
| 1139 | + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { |
|
| 1140 | + $calendarRow = $this->getCalendarById($calendarId); |
|
| 1141 | + $shares = $this->getShares($calendarId); |
|
| 1142 | + |
|
| 1143 | + $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow)); |
|
| 1144 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', new GenericEvent( |
|
| 1145 | + '\OCA\DAV\CalDAV\CalDavBackend::createCalendarObject', |
|
| 1146 | + [ |
|
| 1147 | + 'calendarId' => $calendarId, |
|
| 1148 | + 'calendarData' => $calendarRow, |
|
| 1149 | + 'shares' => $shares, |
|
| 1150 | + 'objectData' => $objectRow, |
|
| 1151 | + ] |
|
| 1152 | + )); |
|
| 1153 | + } else { |
|
| 1154 | + $subscriptionRow = $this->getSubscriptionById($calendarId); |
|
| 1155 | + |
|
| 1156 | + $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow)); |
|
| 1157 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent( |
|
| 1158 | + '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', |
|
| 1159 | + [ |
|
| 1160 | + 'subscriptionId' => $calendarId, |
|
| 1161 | + 'calendarData' => $subscriptionRow, |
|
| 1162 | + 'shares' => [], |
|
| 1163 | + 'objectData' => $objectRow, |
|
| 1164 | + ] |
|
| 1165 | + )); |
|
| 1166 | + } |
|
| 1167 | + |
|
| 1168 | + return '"' . $extraData['etag'] . '"'; |
|
| 1169 | + } |
|
| 1170 | + |
|
| 1171 | + /** |
|
| 1172 | + * Updates an existing calendarobject, based on it's uri. |
|
| 1173 | + * |
|
| 1174 | + * The object uri is only the basename, or filename and not a full path. |
|
| 1175 | + * |
|
| 1176 | + * It is possible return an etag from this function, which will be used in |
|
| 1177 | + * the response to this PUT request. Note that the ETag must be surrounded |
|
| 1178 | + * by double-quotes. |
|
| 1179 | + * |
|
| 1180 | + * However, you should only really return this ETag if you don't mangle the |
|
| 1181 | + * calendar-data. If the result of a subsequent GET to this object is not |
|
| 1182 | + * the exact same as this request body, you should omit the ETag. |
|
| 1183 | + * |
|
| 1184 | + * @param mixed $calendarId |
|
| 1185 | + * @param string $objectUri |
|
| 1186 | + * @param string $calendarData |
|
| 1187 | + * @param int $calendarType |
|
| 1188 | + * @return string |
|
| 1189 | + */ |
|
| 1190 | + public function updateCalendarObject($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1191 | + $extraData = $this->getDenormalizedData($calendarData); |
|
| 1192 | + $query = $this->db->getQueryBuilder(); |
|
| 1193 | + $query->update('calendarobjects') |
|
| 1194 | + ->set('calendardata', $query->createNamedParameter($calendarData, IQueryBuilder::PARAM_LOB)) |
|
| 1195 | + ->set('lastmodified', $query->createNamedParameter(time())) |
|
| 1196 | + ->set('etag', $query->createNamedParameter($extraData['etag'])) |
|
| 1197 | + ->set('size', $query->createNamedParameter($extraData['size'])) |
|
| 1198 | + ->set('componenttype', $query->createNamedParameter($extraData['componentType'])) |
|
| 1199 | + ->set('firstoccurence', $query->createNamedParameter($extraData['firstOccurence'])) |
|
| 1200 | + ->set('lastoccurence', $query->createNamedParameter($extraData['lastOccurence'])) |
|
| 1201 | + ->set('classification', $query->createNamedParameter($extraData['classification'])) |
|
| 1202 | + ->set('uid', $query->createNamedParameter($extraData['uid'])) |
|
| 1203 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1204 | + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 1205 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))) |
|
| 1206 | + ->executeStatement(); |
|
| 1207 | + |
|
| 1208 | + $this->updateProperties($calendarId, $objectUri, $calendarData, $calendarType); |
|
| 1209 | + $this->addChange($calendarId, $objectUri, 2, $calendarType); |
|
| 1210 | + |
|
| 1211 | + $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); |
|
| 1212 | + if (is_array($objectRow)) { |
|
| 1213 | + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { |
|
| 1214 | + $calendarRow = $this->getCalendarById($calendarId); |
|
| 1215 | + $shares = $this->getShares($calendarId); |
|
| 1216 | + |
|
| 1217 | + $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow)); |
|
| 1218 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', new GenericEvent( |
|
| 1219 | + '\OCA\DAV\CalDAV\CalDavBackend::updateCalendarObject', |
|
| 1220 | + [ |
|
| 1221 | + 'calendarId' => $calendarId, |
|
| 1222 | + 'calendarData' => $calendarRow, |
|
| 1223 | + 'shares' => $shares, |
|
| 1224 | + 'objectData' => $objectRow, |
|
| 1225 | + ] |
|
| 1226 | + )); |
|
| 1227 | + } else { |
|
| 1228 | + $subscriptionRow = $this->getSubscriptionById($calendarId); |
|
| 1229 | + |
|
| 1230 | + $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow)); |
|
| 1231 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent( |
|
| 1232 | + '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', |
|
| 1233 | + [ |
|
| 1234 | + 'subscriptionId' => $calendarId, |
|
| 1235 | + 'calendarData' => $subscriptionRow, |
|
| 1236 | + 'shares' => [], |
|
| 1237 | + 'objectData' => $objectRow, |
|
| 1238 | + ] |
|
| 1239 | + )); |
|
| 1240 | + } |
|
| 1241 | + } |
|
| 1242 | + |
|
| 1243 | + return '"' . $extraData['etag'] . '"'; |
|
| 1244 | + } |
|
| 1245 | + |
|
| 1246 | + /** |
|
| 1247 | + * @param int $calendarObjectId |
|
| 1248 | + * @param int $classification |
|
| 1249 | + */ |
|
| 1250 | + public function setClassification($calendarObjectId, $classification) { |
|
| 1251 | + if (!in_array($classification, [ |
|
| 1252 | + self::CLASSIFICATION_PUBLIC, self::CLASSIFICATION_PRIVATE, self::CLASSIFICATION_CONFIDENTIAL |
|
| 1253 | + ])) { |
|
| 1254 | + throw new \InvalidArgumentException(); |
|
| 1255 | + } |
|
| 1256 | + $query = $this->db->getQueryBuilder(); |
|
| 1257 | + $query->update('calendarobjects') |
|
| 1258 | + ->set('classification', $query->createNamedParameter($classification)) |
|
| 1259 | + ->where($query->expr()->eq('id', $query->createNamedParameter($calendarObjectId))) |
|
| 1260 | + ->executeStatement(); |
|
| 1261 | + } |
|
| 1262 | + |
|
| 1263 | + /** |
|
| 1264 | + * Deletes an existing calendar object. |
|
| 1265 | + * |
|
| 1266 | + * The object uri is only the basename, or filename and not a full path. |
|
| 1267 | + * |
|
| 1268 | + * @param mixed $calendarId |
|
| 1269 | + * @param string $objectUri |
|
| 1270 | + * @param int $calendarType |
|
| 1271 | + * @return void |
|
| 1272 | + */ |
|
| 1273 | + public function deleteCalendarObject($calendarId, $objectUri, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1274 | + $data = $this->getCalendarObject($calendarId, $objectUri, $calendarType); |
|
| 1275 | + if (is_array($data)) { |
|
| 1276 | + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { |
|
| 1277 | + $calendarRow = $this->getCalendarById($calendarId); |
|
| 1278 | + $shares = $this->getShares($calendarId); |
|
| 1279 | + |
|
| 1280 | + $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data)); |
|
| 1281 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', new GenericEvent( |
|
| 1282 | + '\OCA\DAV\CalDAV\CalDavBackend::deleteCalendarObject', |
|
| 1283 | + [ |
|
| 1284 | + 'calendarId' => $calendarId, |
|
| 1285 | + 'calendarData' => $calendarRow, |
|
| 1286 | + 'shares' => $shares, |
|
| 1287 | + 'objectData' => $data, |
|
| 1288 | + ] |
|
| 1289 | + )); |
|
| 1290 | + } else { |
|
| 1291 | + $subscriptionRow = $this->getSubscriptionById($calendarId); |
|
| 1292 | + |
|
| 1293 | + $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data)); |
|
| 1294 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent( |
|
| 1295 | + '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', |
|
| 1296 | + [ |
|
| 1297 | + 'subscriptionId' => $calendarId, |
|
| 1298 | + 'calendarData' => $subscriptionRow, |
|
| 1299 | + 'shares' => [], |
|
| 1300 | + 'objectData' => $data, |
|
| 1301 | + ] |
|
| 1302 | + )); |
|
| 1303 | + } |
|
| 1304 | + } |
|
| 1305 | + |
|
| 1306 | + $stmt = $this->db->prepare('DELETE FROM `*PREFIX*calendarobjects` WHERE `calendarid` = ? AND `uri` = ? AND `calendartype` = ?'); |
|
| 1307 | + $stmt->execute([$calendarId, $objectUri, $calendarType]); |
|
| 1308 | + |
|
| 1309 | + if (is_array($data)) { |
|
| 1310 | + $this->purgeProperties($calendarId, $data['id'], $calendarType); |
|
| 1311 | + } |
|
| 1312 | + |
|
| 1313 | + $this->addChange($calendarId, $objectUri, 3, $calendarType); |
|
| 1314 | + } |
|
| 1315 | + |
|
| 1316 | + /** |
|
| 1317 | + * Performs a calendar-query on the contents of this calendar. |
|
| 1318 | + * |
|
| 1319 | + * The calendar-query is defined in RFC4791 : CalDAV. Using the |
|
| 1320 | + * calendar-query it is possible for a client to request a specific set of |
|
| 1321 | + * object, based on contents of iCalendar properties, date-ranges and |
|
| 1322 | + * iCalendar component types (VTODO, VEVENT). |
|
| 1323 | + * |
|
| 1324 | + * This method should just return a list of (relative) urls that match this |
|
| 1325 | + * query. |
|
| 1326 | + * |
|
| 1327 | + * The list of filters are specified as an array. The exact array is |
|
| 1328 | + * documented by Sabre\CalDAV\CalendarQueryParser. |
|
| 1329 | + * |
|
| 1330 | + * Note that it is extremely likely that getCalendarObject for every path |
|
| 1331 | + * returned from this method will be called almost immediately after. You |
|
| 1332 | + * may want to anticipate this to speed up these requests. |
|
| 1333 | + * |
|
| 1334 | + * This method provides a default implementation, which parses *all* the |
|
| 1335 | + * iCalendar objects in the specified calendar. |
|
| 1336 | + * |
|
| 1337 | + * This default may well be good enough for personal use, and calendars |
|
| 1338 | + * that aren't very large. But if you anticipate high usage, big calendars |
|
| 1339 | + * or high loads, you are strongly advised to optimize certain paths. |
|
| 1340 | + * |
|
| 1341 | + * The best way to do so is override this method and to optimize |
|
| 1342 | + * specifically for 'common filters'. |
|
| 1343 | + * |
|
| 1344 | + * Requests that are extremely common are: |
|
| 1345 | + * * requests for just VEVENTS |
|
| 1346 | + * * requests for just VTODO |
|
| 1347 | + * * requests with a time-range-filter on either VEVENT or VTODO. |
|
| 1348 | + * |
|
| 1349 | + * ..and combinations of these requests. It may not be worth it to try to |
|
| 1350 | + * handle every possible situation and just rely on the (relatively |
|
| 1351 | + * easy to use) CalendarQueryValidator to handle the rest. |
|
| 1352 | + * |
|
| 1353 | + * Note that especially time-range-filters may be difficult to parse. A |
|
| 1354 | + * time-range filter specified on a VEVENT must for instance also handle |
|
| 1355 | + * recurrence rules correctly. |
|
| 1356 | + * A good example of how to interprete all these filters can also simply |
|
| 1357 | + * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct |
|
| 1358 | + * as possible, so it gives you a good idea on what type of stuff you need |
|
| 1359 | + * to think of. |
|
| 1360 | + * |
|
| 1361 | + * @param mixed $calendarId |
|
| 1362 | + * @param array $filters |
|
| 1363 | + * @param int $calendarType |
|
| 1364 | + * @return array |
|
| 1365 | + */ |
|
| 1366 | + public function calendarQuery($calendarId, array $filters, $calendarType = self::CALENDAR_TYPE_CALENDAR):array { |
|
| 1367 | + $componentType = null; |
|
| 1368 | + $requirePostFilter = true; |
|
| 1369 | + $timeRange = null; |
|
| 1370 | + |
|
| 1371 | + // if no filters were specified, we don't need to filter after a query |
|
| 1372 | + if (!$filters['prop-filters'] && !$filters['comp-filters']) { |
|
| 1373 | + $requirePostFilter = false; |
|
| 1374 | + } |
|
| 1375 | + |
|
| 1376 | + // Figuring out if there's a component filter |
|
| 1377 | + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { |
|
| 1378 | + $componentType = $filters['comp-filters'][0]['name']; |
|
| 1379 | + |
|
| 1380 | + // Checking if we need post-filters |
|
| 1381 | + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['time-range'] && !$filters['comp-filters'][0]['prop-filters']) { |
|
| 1382 | + $requirePostFilter = false; |
|
| 1383 | + } |
|
| 1384 | + // There was a time-range filter |
|
| 1385 | + if ($componentType === 'VEVENT' && isset($filters['comp-filters'][0]['time-range']) && is_array($filters['comp-filters'][0]['time-range'])) { |
|
| 1386 | + $timeRange = $filters['comp-filters'][0]['time-range']; |
|
| 1387 | + |
|
| 1388 | + // If start time OR the end time is not specified, we can do a |
|
| 1389 | + // 100% accurate mysql query. |
|
| 1390 | + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && (!$timeRange['start'] || !$timeRange['end'])) { |
|
| 1391 | + $requirePostFilter = false; |
|
| 1392 | + } |
|
| 1393 | + } |
|
| 1394 | + } |
|
| 1395 | + $columns = ['uri']; |
|
| 1396 | + if ($requirePostFilter) { |
|
| 1397 | + $columns = ['uri', 'calendardata']; |
|
| 1398 | + } |
|
| 1399 | + $query = $this->db->getQueryBuilder(); |
|
| 1400 | + $query->select($columns) |
|
| 1401 | + ->from('calendarobjects') |
|
| 1402 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 1403 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 1404 | + |
|
| 1405 | + if ($componentType) { |
|
| 1406 | + $query->andWhere($query->expr()->eq('componenttype', $query->createNamedParameter($componentType))); |
|
| 1407 | + } |
|
| 1408 | + |
|
| 1409 | + if ($timeRange && $timeRange['start']) { |
|
| 1410 | + $query->andWhere($query->expr()->gt('lastoccurence', $query->createNamedParameter($timeRange['start']->getTimeStamp()))); |
|
| 1411 | + } |
|
| 1412 | + if ($timeRange && $timeRange['end']) { |
|
| 1413 | + $query->andWhere($query->expr()->lt('firstoccurence', $query->createNamedParameter($timeRange['end']->getTimeStamp()))); |
|
| 1414 | + } |
|
| 1415 | + |
|
| 1416 | + $stmt = $query->executeQuery(); |
|
| 1417 | + |
|
| 1418 | + $result = []; |
|
| 1419 | + while ($row = $stmt->fetch()) { |
|
| 1420 | + if ($requirePostFilter) { |
|
| 1421 | + // validateFilterForObject will parse the calendar data |
|
| 1422 | + // catch parsing errors |
|
| 1423 | + try { |
|
| 1424 | + $matches = $this->validateFilterForObject($row, $filters); |
|
| 1425 | + } catch (ParseException $ex) { |
|
| 1426 | + $this->logger->logException($ex, [ |
|
| 1427 | + 'app' => 'dav', |
|
| 1428 | + 'message' => 'Caught parsing exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'] |
|
| 1429 | + ]); |
|
| 1430 | + continue; |
|
| 1431 | + } catch (InvalidDataException $ex) { |
|
| 1432 | + $this->logger->logException($ex, [ |
|
| 1433 | + 'app' => 'dav', |
|
| 1434 | + 'message' => 'Caught invalid data exception for calendar data. This usually indicates invalid calendar data. calendar-id:'.$calendarId.' uri:'.$row['uri'] |
|
| 1435 | + ]); |
|
| 1436 | + continue; |
|
| 1437 | + } |
|
| 1438 | + |
|
| 1439 | + if (!$matches) { |
|
| 1440 | + continue; |
|
| 1441 | + } |
|
| 1442 | + } |
|
| 1443 | + $result[] = $row['uri']; |
|
| 1444 | + } |
|
| 1445 | + |
|
| 1446 | + return $result; |
|
| 1447 | + } |
|
| 1448 | + |
|
| 1449 | + /** |
|
| 1450 | + * custom Nextcloud search extension for CalDAV |
|
| 1451 | + * |
|
| 1452 | + * TODO - this should optionally cover cached calendar objects as well |
|
| 1453 | + * |
|
| 1454 | + * @param string $principalUri |
|
| 1455 | + * @param array $filters |
|
| 1456 | + * @param integer|null $limit |
|
| 1457 | + * @param integer|null $offset |
|
| 1458 | + * @return array |
|
| 1459 | + */ |
|
| 1460 | + public function calendarSearch($principalUri, array $filters, $limit = null, $offset = null) { |
|
| 1461 | + $calendars = $this->getCalendarsForUser($principalUri); |
|
| 1462 | + $ownCalendars = []; |
|
| 1463 | + $sharedCalendars = []; |
|
| 1464 | + |
|
| 1465 | + $uriMapper = []; |
|
| 1466 | + |
|
| 1467 | + foreach ($calendars as $calendar) { |
|
| 1468 | + if ($calendar['{http://owncloud.org/ns}owner-principal'] === $principalUri) { |
|
| 1469 | + $ownCalendars[] = $calendar['id']; |
|
| 1470 | + } else { |
|
| 1471 | + $sharedCalendars[] = $calendar['id']; |
|
| 1472 | + } |
|
| 1473 | + $uriMapper[$calendar['id']] = $calendar['uri']; |
|
| 1474 | + } |
|
| 1475 | + if (count($ownCalendars) === 0 && count($sharedCalendars) === 0) { |
|
| 1476 | + return []; |
|
| 1477 | + } |
|
| 1478 | + |
|
| 1479 | + $query = $this->db->getQueryBuilder(); |
|
| 1480 | + // Calendar id expressions |
|
| 1481 | + $calendarExpressions = []; |
|
| 1482 | + foreach ($ownCalendars as $id) { |
|
| 1483 | + $calendarExpressions[] = $query->expr()->andX( |
|
| 1484 | + $query->expr()->eq('c.calendarid', |
|
| 1485 | + $query->createNamedParameter($id)), |
|
| 1486 | + $query->expr()->eq('c.calendartype', |
|
| 1487 | + $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1488 | + } |
|
| 1489 | + foreach ($sharedCalendars as $id) { |
|
| 1490 | + $calendarExpressions[] = $query->expr()->andX( |
|
| 1491 | + $query->expr()->eq('c.calendarid', |
|
| 1492 | + $query->createNamedParameter($id)), |
|
| 1493 | + $query->expr()->eq('c.classification', |
|
| 1494 | + $query->createNamedParameter(self::CLASSIFICATION_PUBLIC)), |
|
| 1495 | + $query->expr()->eq('c.calendartype', |
|
| 1496 | + $query->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1497 | + } |
|
| 1498 | + |
|
| 1499 | + if (count($calendarExpressions) === 1) { |
|
| 1500 | + $calExpr = $calendarExpressions[0]; |
|
| 1501 | + } else { |
|
| 1502 | + $calExpr = call_user_func_array([$query->expr(), 'orX'], $calendarExpressions); |
|
| 1503 | + } |
|
| 1504 | + |
|
| 1505 | + // Component expressions |
|
| 1506 | + $compExpressions = []; |
|
| 1507 | + foreach ($filters['comps'] as $comp) { |
|
| 1508 | + $compExpressions[] = $query->expr() |
|
| 1509 | + ->eq('c.componenttype', $query->createNamedParameter($comp)); |
|
| 1510 | + } |
|
| 1511 | + |
|
| 1512 | + if (count($compExpressions) === 1) { |
|
| 1513 | + $compExpr = $compExpressions[0]; |
|
| 1514 | + } else { |
|
| 1515 | + $compExpr = call_user_func_array([$query->expr(), 'orX'], $compExpressions); |
|
| 1516 | + } |
|
| 1517 | + |
|
| 1518 | + if (!isset($filters['props'])) { |
|
| 1519 | + $filters['props'] = []; |
|
| 1520 | + } |
|
| 1521 | + if (!isset($filters['params'])) { |
|
| 1522 | + $filters['params'] = []; |
|
| 1523 | + } |
|
| 1524 | + |
|
| 1525 | + $propParamExpressions = []; |
|
| 1526 | + foreach ($filters['props'] as $prop) { |
|
| 1527 | + $propParamExpressions[] = $query->expr()->andX( |
|
| 1528 | + $query->expr()->eq('i.name', $query->createNamedParameter($prop)), |
|
| 1529 | + $query->expr()->isNull('i.parameter') |
|
| 1530 | + ); |
|
| 1531 | + } |
|
| 1532 | + foreach ($filters['params'] as $param) { |
|
| 1533 | + $propParamExpressions[] = $query->expr()->andX( |
|
| 1534 | + $query->expr()->eq('i.name', $query->createNamedParameter($param['property'])), |
|
| 1535 | + $query->expr()->eq('i.parameter', $query->createNamedParameter($param['parameter'])) |
|
| 1536 | + ); |
|
| 1537 | + } |
|
| 1538 | + |
|
| 1539 | + if (count($propParamExpressions) === 1) { |
|
| 1540 | + $propParamExpr = $propParamExpressions[0]; |
|
| 1541 | + } else { |
|
| 1542 | + $propParamExpr = call_user_func_array([$query->expr(), 'orX'], $propParamExpressions); |
|
| 1543 | + } |
|
| 1544 | + |
|
| 1545 | + $query->select(['c.calendarid', 'c.uri']) |
|
| 1546 | + ->from($this->dbObjectPropertiesTable, 'i') |
|
| 1547 | + ->join('i', 'calendarobjects', 'c', $query->expr()->eq('i.objectid', 'c.id')) |
|
| 1548 | + ->where($calExpr) |
|
| 1549 | + ->andWhere($compExpr) |
|
| 1550 | + ->andWhere($propParamExpr) |
|
| 1551 | + ->andWhere($query->expr()->iLike('i.value', |
|
| 1552 | + $query->createNamedParameter('%'.$this->db->escapeLikeParameter($filters['search-term']).'%'))); |
|
| 1553 | + |
|
| 1554 | + if ($offset) { |
|
| 1555 | + $query->setFirstResult($offset); |
|
| 1556 | + } |
|
| 1557 | + if ($limit) { |
|
| 1558 | + $query->setMaxResults($limit); |
|
| 1559 | + } |
|
| 1560 | + |
|
| 1561 | + $stmt = $query->executeQuery(); |
|
| 1562 | + |
|
| 1563 | + $result = []; |
|
| 1564 | + while ($row = $stmt->fetch()) { |
|
| 1565 | + $path = $uriMapper[$row['calendarid']] . '/' . $row['uri']; |
|
| 1566 | + if (!in_array($path, $result)) { |
|
| 1567 | + $result[] = $path; |
|
| 1568 | + } |
|
| 1569 | + } |
|
| 1570 | + |
|
| 1571 | + return $result; |
|
| 1572 | + } |
|
| 1573 | + |
|
| 1574 | + /** |
|
| 1575 | + * used for Nextcloud's calendar API |
|
| 1576 | + * |
|
| 1577 | + * @param array $calendarInfo |
|
| 1578 | + * @param string $pattern |
|
| 1579 | + * @param array $searchProperties |
|
| 1580 | + * @param array $options |
|
| 1581 | + * @param integer|null $limit |
|
| 1582 | + * @param integer|null $offset |
|
| 1583 | + * |
|
| 1584 | + * @return array |
|
| 1585 | + */ |
|
| 1586 | + public function search(array $calendarInfo, $pattern, array $searchProperties, |
|
| 1587 | + array $options, $limit, $offset) { |
|
| 1588 | + $outerQuery = $this->db->getQueryBuilder(); |
|
| 1589 | + $innerQuery = $this->db->getQueryBuilder(); |
|
| 1590 | + |
|
| 1591 | + $innerQuery->selectDistinct('op.objectid') |
|
| 1592 | + ->from($this->dbObjectPropertiesTable, 'op') |
|
| 1593 | + ->andWhere($innerQuery->expr()->eq('op.calendarid', |
|
| 1594 | + $outerQuery->createNamedParameter($calendarInfo['id']))) |
|
| 1595 | + ->andWhere($innerQuery->expr()->eq('op.calendartype', |
|
| 1596 | + $outerQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1597 | + |
|
| 1598 | + // only return public items for shared calendars for now |
|
| 1599 | + if ($calendarInfo['principaluri'] !== $calendarInfo['{http://owncloud.org/ns}owner-principal']) { |
|
| 1600 | + $innerQuery->andWhere($innerQuery->expr()->eq('c.classification', |
|
| 1601 | + $outerQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); |
|
| 1602 | + } |
|
| 1603 | + |
|
| 1604 | + $or = $innerQuery->expr()->orX(); |
|
| 1605 | + foreach ($searchProperties as $searchProperty) { |
|
| 1606 | + $or->add($innerQuery->expr()->eq('op.name', |
|
| 1607 | + $outerQuery->createNamedParameter($searchProperty))); |
|
| 1608 | + } |
|
| 1609 | + $innerQuery->andWhere($or); |
|
| 1610 | + |
|
| 1611 | + if ($pattern !== '') { |
|
| 1612 | + $innerQuery->andWhere($innerQuery->expr()->iLike('op.value', |
|
| 1613 | + $outerQuery->createNamedParameter('%' . |
|
| 1614 | + $this->db->escapeLikeParameter($pattern) . '%'))); |
|
| 1615 | + } |
|
| 1616 | + |
|
| 1617 | + $outerQuery->select('c.id', 'c.calendardata', 'c.componenttype', 'c.uid', 'c.uri') |
|
| 1618 | + ->from('calendarobjects', 'c'); |
|
| 1619 | + |
|
| 1620 | + if (isset($options['timerange'])) { |
|
| 1621 | + if (isset($options['timerange']['start']) && $options['timerange']['start'] instanceof DateTime) { |
|
| 1622 | + $outerQuery->andWhere($outerQuery->expr()->gt('lastoccurence', |
|
| 1623 | + $outerQuery->createNamedParameter($options['timerange']['start']->getTimeStamp()))); |
|
| 1624 | + } |
|
| 1625 | + if (isset($options['timerange']['end']) && $options['timerange']['end'] instanceof DateTime) { |
|
| 1626 | + $outerQuery->andWhere($outerQuery->expr()->lt('firstoccurence', |
|
| 1627 | + $outerQuery->createNamedParameter($options['timerange']['end']->getTimeStamp()))); |
|
| 1628 | + } |
|
| 1629 | + } |
|
| 1630 | + |
|
| 1631 | + if (isset($options['types'])) { |
|
| 1632 | + $or = $outerQuery->expr()->orX(); |
|
| 1633 | + foreach ($options['types'] as $type) { |
|
| 1634 | + $or->add($outerQuery->expr()->eq('componenttype', |
|
| 1635 | + $outerQuery->createNamedParameter($type))); |
|
| 1636 | + } |
|
| 1637 | + $outerQuery->andWhere($or); |
|
| 1638 | + } |
|
| 1639 | + |
|
| 1640 | + $outerQuery->andWhere($outerQuery->expr()->in('c.id', |
|
| 1641 | + $outerQuery->createFunction($innerQuery->getSQL()))); |
|
| 1642 | + |
|
| 1643 | + if ($offset) { |
|
| 1644 | + $outerQuery->setFirstResult($offset); |
|
| 1645 | + } |
|
| 1646 | + if ($limit) { |
|
| 1647 | + $outerQuery->setMaxResults($limit); |
|
| 1648 | + } |
|
| 1649 | + |
|
| 1650 | + $result = $outerQuery->executeQuery(); |
|
| 1651 | + $calendarObjects = $result->fetchAll(); |
|
| 1652 | + |
|
| 1653 | + return array_map(function ($o) { |
|
| 1654 | + $calendarData = Reader::read($o['calendardata']); |
|
| 1655 | + $comps = $calendarData->getComponents(); |
|
| 1656 | + $objects = []; |
|
| 1657 | + $timezones = []; |
|
| 1658 | + foreach ($comps as $comp) { |
|
| 1659 | + if ($comp instanceof VTimeZone) { |
|
| 1660 | + $timezones[] = $comp; |
|
| 1661 | + } else { |
|
| 1662 | + $objects[] = $comp; |
|
| 1663 | + } |
|
| 1664 | + } |
|
| 1665 | + |
|
| 1666 | + return [ |
|
| 1667 | + 'id' => $o['id'], |
|
| 1668 | + 'type' => $o['componenttype'], |
|
| 1669 | + 'uid' => $o['uid'], |
|
| 1670 | + 'uri' => $o['uri'], |
|
| 1671 | + 'objects' => array_map(function ($c) { |
|
| 1672 | + return $this->transformSearchData($c); |
|
| 1673 | + }, $objects), |
|
| 1674 | + 'timezones' => array_map(function ($c) { |
|
| 1675 | + return $this->transformSearchData($c); |
|
| 1676 | + }, $timezones), |
|
| 1677 | + ]; |
|
| 1678 | + }, $calendarObjects); |
|
| 1679 | + } |
|
| 1680 | + |
|
| 1681 | + /** |
|
| 1682 | + * @param Component $comp |
|
| 1683 | + * @return array |
|
| 1684 | + */ |
|
| 1685 | + private function transformSearchData(Component $comp) { |
|
| 1686 | + $data = []; |
|
| 1687 | + /** @var Component[] $subComponents */ |
|
| 1688 | + $subComponents = $comp->getComponents(); |
|
| 1689 | + /** @var Property[] $properties */ |
|
| 1690 | + $properties = array_filter($comp->children(), function ($c) { |
|
| 1691 | + return $c instanceof Property; |
|
| 1692 | + }); |
|
| 1693 | + $validationRules = $comp->getValidationRules(); |
|
| 1694 | + |
|
| 1695 | + foreach ($subComponents as $subComponent) { |
|
| 1696 | + $name = $subComponent->name; |
|
| 1697 | + if (!isset($data[$name])) { |
|
| 1698 | + $data[$name] = []; |
|
| 1699 | + } |
|
| 1700 | + $data[$name][] = $this->transformSearchData($subComponent); |
|
| 1701 | + } |
|
| 1702 | + |
|
| 1703 | + foreach ($properties as $property) { |
|
| 1704 | + $name = $property->name; |
|
| 1705 | + if (!isset($validationRules[$name])) { |
|
| 1706 | + $validationRules[$name] = '*'; |
|
| 1707 | + } |
|
| 1708 | + |
|
| 1709 | + $rule = $validationRules[$property->name]; |
|
| 1710 | + if ($rule === '+' || $rule === '*') { // multiple |
|
| 1711 | + if (!isset($data[$name])) { |
|
| 1712 | + $data[$name] = []; |
|
| 1713 | + } |
|
| 1714 | + |
|
| 1715 | + $data[$name][] = $this->transformSearchProperty($property); |
|
| 1716 | + } else { // once |
|
| 1717 | + $data[$name] = $this->transformSearchProperty($property); |
|
| 1718 | + } |
|
| 1719 | + } |
|
| 1720 | + |
|
| 1721 | + return $data; |
|
| 1722 | + } |
|
| 1723 | + |
|
| 1724 | + /** |
|
| 1725 | + * @param Property $prop |
|
| 1726 | + * @return array |
|
| 1727 | + */ |
|
| 1728 | + private function transformSearchProperty(Property $prop) { |
|
| 1729 | + // No need to check Date, as it extends DateTime |
|
| 1730 | + if ($prop instanceof Property\ICalendar\DateTime) { |
|
| 1731 | + $value = $prop->getDateTime(); |
|
| 1732 | + } else { |
|
| 1733 | + $value = $prop->getValue(); |
|
| 1734 | + } |
|
| 1735 | + |
|
| 1736 | + return [ |
|
| 1737 | + $value, |
|
| 1738 | + $prop->parameters() |
|
| 1739 | + ]; |
|
| 1740 | + } |
|
| 1741 | + |
|
| 1742 | + /** |
|
| 1743 | + * @param string $principalUri |
|
| 1744 | + * @param string $pattern |
|
| 1745 | + * @param array $componentTypes |
|
| 1746 | + * @param array $searchProperties |
|
| 1747 | + * @param array $searchParameters |
|
| 1748 | + * @param array $options |
|
| 1749 | + * @return array |
|
| 1750 | + */ |
|
| 1751 | + public function searchPrincipalUri(string $principalUri, |
|
| 1752 | + string $pattern, |
|
| 1753 | + array $componentTypes, |
|
| 1754 | + array $searchProperties, |
|
| 1755 | + array $searchParameters, |
|
| 1756 | + array $options = []): array { |
|
| 1757 | + $escapePattern = !\array_key_exists('escape_like_param', $options) || $options['escape_like_param'] !== false; |
|
| 1758 | + |
|
| 1759 | + $calendarObjectIdQuery = $this->db->getQueryBuilder(); |
|
| 1760 | + $calendarOr = $calendarObjectIdQuery->expr()->orX(); |
|
| 1761 | + $searchOr = $calendarObjectIdQuery->expr()->orX(); |
|
| 1762 | + |
|
| 1763 | + // Fetch calendars and subscription |
|
| 1764 | + $calendars = $this->getCalendarsForUser($principalUri); |
|
| 1765 | + $subscriptions = $this->getSubscriptionsForUser($principalUri); |
|
| 1766 | + foreach ($calendars as $calendar) { |
|
| 1767 | + $calendarAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1768 | + $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$calendar['id']))); |
|
| 1769 | + $calendarAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_CALENDAR))); |
|
| 1770 | + |
|
| 1771 | + // If it's shared, limit search to public events |
|
| 1772 | + if (isset($calendar['{http://owncloud.org/ns}owner-principal']) |
|
| 1773 | + && $calendar['principaluri'] !== $calendar['{http://owncloud.org/ns}owner-principal']) { |
|
| 1774 | + $calendarAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); |
|
| 1775 | + } |
|
| 1776 | + |
|
| 1777 | + $calendarOr->add($calendarAnd); |
|
| 1778 | + } |
|
| 1779 | + foreach ($subscriptions as $subscription) { |
|
| 1780 | + $subscriptionAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1781 | + $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendarid', $calendarObjectIdQuery->createNamedParameter((int)$subscription['id']))); |
|
| 1782 | + $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('cob.calendartype', $calendarObjectIdQuery->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); |
|
| 1783 | + |
|
| 1784 | + // If it's shared, limit search to public events |
|
| 1785 | + if (isset($subscription['{http://owncloud.org/ns}owner-principal']) |
|
| 1786 | + && $subscription['principaluri'] !== $subscription['{http://owncloud.org/ns}owner-principal']) { |
|
| 1787 | + $subscriptionAnd->add($calendarObjectIdQuery->expr()->eq('co.classification', $calendarObjectIdQuery->createNamedParameter(self::CLASSIFICATION_PUBLIC))); |
|
| 1788 | + } |
|
| 1789 | + |
|
| 1790 | + $calendarOr->add($subscriptionAnd); |
|
| 1791 | + } |
|
| 1792 | + |
|
| 1793 | + foreach ($searchProperties as $property) { |
|
| 1794 | + $propertyAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1795 | + $propertyAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); |
|
| 1796 | + $propertyAnd->add($calendarObjectIdQuery->expr()->isNull('cob.parameter')); |
|
| 1797 | + |
|
| 1798 | + $searchOr->add($propertyAnd); |
|
| 1799 | + } |
|
| 1800 | + foreach ($searchParameters as $property => $parameter) { |
|
| 1801 | + $parameterAnd = $calendarObjectIdQuery->expr()->andX(); |
|
| 1802 | + $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.name', $calendarObjectIdQuery->createNamedParameter($property, IQueryBuilder::PARAM_STR))); |
|
| 1803 | + $parameterAnd->add($calendarObjectIdQuery->expr()->eq('cob.parameter', $calendarObjectIdQuery->createNamedParameter($parameter, IQueryBuilder::PARAM_STR_ARRAY))); |
|
| 1804 | + |
|
| 1805 | + $searchOr->add($parameterAnd); |
|
| 1806 | + } |
|
| 1807 | + |
|
| 1808 | + if ($calendarOr->count() === 0) { |
|
| 1809 | + return []; |
|
| 1810 | + } |
|
| 1811 | + if ($searchOr->count() === 0) { |
|
| 1812 | + return []; |
|
| 1813 | + } |
|
| 1814 | + |
|
| 1815 | + $calendarObjectIdQuery->selectDistinct('cob.objectid') |
|
| 1816 | + ->from($this->dbObjectPropertiesTable, 'cob') |
|
| 1817 | + ->leftJoin('cob', 'calendarobjects', 'co', $calendarObjectIdQuery->expr()->eq('co.id', 'cob.objectid')) |
|
| 1818 | + ->andWhere($calendarObjectIdQuery->expr()->in('co.componenttype', $calendarObjectIdQuery->createNamedParameter($componentTypes, IQueryBuilder::PARAM_STR_ARRAY))) |
|
| 1819 | + ->andWhere($calendarOr) |
|
| 1820 | + ->andWhere($searchOr); |
|
| 1821 | + |
|
| 1822 | + if ('' !== $pattern) { |
|
| 1823 | + if (!$escapePattern) { |
|
| 1824 | + $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter($pattern))); |
|
| 1825 | + } else { |
|
| 1826 | + $calendarObjectIdQuery->andWhere($calendarObjectIdQuery->expr()->ilike('cob.value', $calendarObjectIdQuery->createNamedParameter('%' . $this->db->escapeLikeParameter($pattern) . '%'))); |
|
| 1827 | + } |
|
| 1828 | + } |
|
| 1829 | + |
|
| 1830 | + if (isset($options['limit'])) { |
|
| 1831 | + $calendarObjectIdQuery->setMaxResults($options['limit']); |
|
| 1832 | + } |
|
| 1833 | + if (isset($options['offset'])) { |
|
| 1834 | + $calendarObjectIdQuery->setFirstResult($options['offset']); |
|
| 1835 | + } |
|
| 1836 | + |
|
| 1837 | + $result = $calendarObjectIdQuery->executeQuery(); |
|
| 1838 | + $matches = $result->fetchAll(); |
|
| 1839 | + $result->closeCursor(); |
|
| 1840 | + $matches = array_map(static function (array $match):int { |
|
| 1841 | + return (int) $match['objectid']; |
|
| 1842 | + }, $matches); |
|
| 1843 | + |
|
| 1844 | + $query = $this->db->getQueryBuilder(); |
|
| 1845 | + $query->select('calendardata', 'uri', 'calendarid', 'calendartype') |
|
| 1846 | + ->from('calendarobjects') |
|
| 1847 | + ->where($query->expr()->in('id', $query->createNamedParameter($matches, IQueryBuilder::PARAM_INT_ARRAY))); |
|
| 1848 | + |
|
| 1849 | + $result = $query->executeQuery(); |
|
| 1850 | + $calendarObjects = $result->fetchAll(); |
|
| 1851 | + $result->closeCursor(); |
|
| 1852 | + |
|
| 1853 | + return array_map(function (array $array): array { |
|
| 1854 | + $array['calendarid'] = (int)$array['calendarid']; |
|
| 1855 | + $array['calendartype'] = (int)$array['calendartype']; |
|
| 1856 | + $array['calendardata'] = $this->readBlob($array['calendardata']); |
|
| 1857 | + |
|
| 1858 | + return $array; |
|
| 1859 | + }, $calendarObjects); |
|
| 1860 | + } |
|
| 1861 | + |
|
| 1862 | + /** |
|
| 1863 | + * Searches through all of a users calendars and calendar objects to find |
|
| 1864 | + * an object with a specific UID. |
|
| 1865 | + * |
|
| 1866 | + * This method should return the path to this object, relative to the |
|
| 1867 | + * calendar home, so this path usually only contains two parts: |
|
| 1868 | + * |
|
| 1869 | + * calendarpath/objectpath.ics |
|
| 1870 | + * |
|
| 1871 | + * If the uid is not found, return null. |
|
| 1872 | + * |
|
| 1873 | + * This method should only consider * objects that the principal owns, so |
|
| 1874 | + * any calendars owned by other principals that also appear in this |
|
| 1875 | + * collection should be ignored. |
|
| 1876 | + * |
|
| 1877 | + * @param string $principalUri |
|
| 1878 | + * @param string $uid |
|
| 1879 | + * @return string|null |
|
| 1880 | + */ |
|
| 1881 | + public function getCalendarObjectByUID($principalUri, $uid) { |
|
| 1882 | + $query = $this->db->getQueryBuilder(); |
|
| 1883 | + $query->selectAlias('c.uri', 'calendaruri')->selectAlias('co.uri', 'objecturi') |
|
| 1884 | + ->from('calendarobjects', 'co') |
|
| 1885 | + ->leftJoin('co', 'calendars', 'c', $query->expr()->eq('co.calendarid', 'c.id')) |
|
| 1886 | + ->where($query->expr()->eq('c.principaluri', $query->createNamedParameter($principalUri))) |
|
| 1887 | + ->andWhere($query->expr()->eq('co.uid', $query->createNamedParameter($uid))); |
|
| 1888 | + |
|
| 1889 | + $stmt = $query->executeQuery(); |
|
| 1890 | + $row = $stmt->fetch(); |
|
| 1891 | + $stmt->closeCursor(); |
|
| 1892 | + if ($row) { |
|
| 1893 | + return $row['calendaruri'] . '/' . $row['objecturi']; |
|
| 1894 | + } |
|
| 1895 | + |
|
| 1896 | + return null; |
|
| 1897 | + } |
|
| 1898 | + |
|
| 1899 | + /** |
|
| 1900 | + * The getChanges method returns all the changes that have happened, since |
|
| 1901 | + * the specified syncToken in the specified calendar. |
|
| 1902 | + * |
|
| 1903 | + * This function should return an array, such as the following: |
|
| 1904 | + * |
|
| 1905 | + * [ |
|
| 1906 | + * 'syncToken' => 'The current synctoken', |
|
| 1907 | + * 'added' => [ |
|
| 1908 | + * 'new.txt', |
|
| 1909 | + * ], |
|
| 1910 | + * 'modified' => [ |
|
| 1911 | + * 'modified.txt', |
|
| 1912 | + * ], |
|
| 1913 | + * 'deleted' => [ |
|
| 1914 | + * 'foo.php.bak', |
|
| 1915 | + * 'old.txt' |
|
| 1916 | + * ] |
|
| 1917 | + * ); |
|
| 1918 | + * |
|
| 1919 | + * The returned syncToken property should reflect the *current* syncToken |
|
| 1920 | + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token |
|
| 1921 | + * property This is * needed here too, to ensure the operation is atomic. |
|
| 1922 | + * |
|
| 1923 | + * If the $syncToken argument is specified as null, this is an initial |
|
| 1924 | + * sync, and all members should be reported. |
|
| 1925 | + * |
|
| 1926 | + * The modified property is an array of nodenames that have changed since |
|
| 1927 | + * the last token. |
|
| 1928 | + * |
|
| 1929 | + * The deleted property is an array with nodenames, that have been deleted |
|
| 1930 | + * from collection. |
|
| 1931 | + * |
|
| 1932 | + * The $syncLevel argument is basically the 'depth' of the report. If it's |
|
| 1933 | + * 1, you only have to report changes that happened only directly in |
|
| 1934 | + * immediate descendants. If it's 2, it should also include changes from |
|
| 1935 | + * the nodes below the child collections. (grandchildren) |
|
| 1936 | + * |
|
| 1937 | + * The $limit argument allows a client to specify how many results should |
|
| 1938 | + * be returned at most. If the limit is not specified, it should be treated |
|
| 1939 | + * as infinite. |
|
| 1940 | + * |
|
| 1941 | + * If the limit (infinite or not) is higher than you're willing to return, |
|
| 1942 | + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. |
|
| 1943 | + * |
|
| 1944 | + * If the syncToken is expired (due to data cleanup) or unknown, you must |
|
| 1945 | + * return null. |
|
| 1946 | + * |
|
| 1947 | + * The limit is 'suggestive'. You are free to ignore it. |
|
| 1948 | + * |
|
| 1949 | + * @param string $calendarId |
|
| 1950 | + * @param string $syncToken |
|
| 1951 | + * @param int $syncLevel |
|
| 1952 | + * @param int|null $limit |
|
| 1953 | + * @param int $calendarType |
|
| 1954 | + * @return array |
|
| 1955 | + */ |
|
| 1956 | + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 1957 | + // Current synctoken |
|
| 1958 | + $qb = $this->db->getQueryBuilder(); |
|
| 1959 | + $qb->select('synctoken') |
|
| 1960 | + ->from('calendars') |
|
| 1961 | + ->where( |
|
| 1962 | + $qb->expr()->eq('id', $qb->createNamedParameter($calendarId)) |
|
| 1963 | + ); |
|
| 1964 | + $stmt = $qb->executeQuery(); |
|
| 1965 | + $currentToken = $stmt->fetchOne(); |
|
| 1966 | + |
|
| 1967 | + if ($currentToken === false) { |
|
| 1968 | + return null; |
|
| 1969 | + } |
|
| 1970 | + |
|
| 1971 | + $result = [ |
|
| 1972 | + 'syncToken' => $currentToken, |
|
| 1973 | + 'added' => [], |
|
| 1974 | + 'modified' => [], |
|
| 1975 | + 'deleted' => [], |
|
| 1976 | + ]; |
|
| 1977 | + |
|
| 1978 | + if ($syncToken) { |
|
| 1979 | + $qb = $this->db->getQueryBuilder(); |
|
| 1980 | + |
|
| 1981 | + $qb->select('uri', 'operation') |
|
| 1982 | + ->from('calendarchanges') |
|
| 1983 | + ->where( |
|
| 1984 | + $qb->expr()->andX( |
|
| 1985 | + $qb->expr()->gte('synctoken', $qb->createNamedParameter($syncToken)), |
|
| 1986 | + $qb->expr()->lt('synctoken', $qb->createNamedParameter($currentToken)), |
|
| 1987 | + $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), |
|
| 1988 | + $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) |
|
| 1989 | + ) |
|
| 1990 | + )->orderBy('synctoken'); |
|
| 1991 | + if (is_int($limit) && $limit > 0) { |
|
| 1992 | + $qb->setMaxResults($limit); |
|
| 1993 | + } |
|
| 1994 | + |
|
| 1995 | + // Fetching all changes |
|
| 1996 | + $stmt = $qb->executeQuery(); |
|
| 1997 | + $changes = []; |
|
| 1998 | + |
|
| 1999 | + // This loop ensures that any duplicates are overwritten, only the |
|
| 2000 | + // last change on a node is relevant. |
|
| 2001 | + while ($row = $stmt->fetch()) { |
|
| 2002 | + $changes[$row['uri']] = $row['operation']; |
|
| 2003 | + } |
|
| 2004 | + $stmt->closeCursor(); |
|
| 2005 | + |
|
| 2006 | + foreach ($changes as $uri => $operation) { |
|
| 2007 | + switch ($operation) { |
|
| 2008 | + case 1: |
|
| 2009 | + $result['added'][] = $uri; |
|
| 2010 | + break; |
|
| 2011 | + case 2: |
|
| 2012 | + $result['modified'][] = $uri; |
|
| 2013 | + break; |
|
| 2014 | + case 3: |
|
| 2015 | + $result['deleted'][] = $uri; |
|
| 2016 | + break; |
|
| 2017 | + } |
|
| 2018 | + } |
|
| 2019 | + } else { |
|
| 2020 | + // No synctoken supplied, this is the initial sync. |
|
| 2021 | + $qb = $this->db->getQueryBuilder(); |
|
| 2022 | + $qb->select('uri') |
|
| 2023 | + ->from('calendarobjects') |
|
| 2024 | + ->where( |
|
| 2025 | + $qb->expr()->andX( |
|
| 2026 | + $qb->expr()->eq('calendarid', $qb->createNamedParameter($calendarId)), |
|
| 2027 | + $qb->expr()->eq('calendartype', $qb->createNamedParameter($calendarType)) |
|
| 2028 | + ) |
|
| 2029 | + ); |
|
| 2030 | + $stmt = $qb->executeQuery(); |
|
| 2031 | + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); |
|
| 2032 | + $stmt->closeCursor(); |
|
| 2033 | + } |
|
| 2034 | + return $result; |
|
| 2035 | + } |
|
| 2036 | + |
|
| 2037 | + /** |
|
| 2038 | + * Returns a list of subscriptions for a principal. |
|
| 2039 | + * |
|
| 2040 | + * Every subscription is an array with the following keys: |
|
| 2041 | + * * id, a unique id that will be used by other functions to modify the |
|
| 2042 | + * subscription. This can be the same as the uri or a database key. |
|
| 2043 | + * * uri. This is just the 'base uri' or 'filename' of the subscription. |
|
| 2044 | + * * principaluri. The owner of the subscription. Almost always the same as |
|
| 2045 | + * principalUri passed to this method. |
|
| 2046 | + * |
|
| 2047 | + * Furthermore, all the subscription info must be returned too: |
|
| 2048 | + * |
|
| 2049 | + * 1. {DAV:}displayname |
|
| 2050 | + * 2. {http://apple.com/ns/ical/}refreshrate |
|
| 2051 | + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos |
|
| 2052 | + * should not be stripped). |
|
| 2053 | + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms |
|
| 2054 | + * should not be stripped). |
|
| 2055 | + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if |
|
| 2056 | + * attachments should not be stripped). |
|
| 2057 | + * 6. {http://calendarserver.org/ns/}source (Must be a |
|
| 2058 | + * Sabre\DAV\Property\Href). |
|
| 2059 | + * 7. {http://apple.com/ns/ical/}calendar-color |
|
| 2060 | + * 8. {http://apple.com/ns/ical/}calendar-order |
|
| 2061 | + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set |
|
| 2062 | + * (should just be an instance of |
|
| 2063 | + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of |
|
| 2064 | + * default components). |
|
| 2065 | + * |
|
| 2066 | + * @param string $principalUri |
|
| 2067 | + * @return array |
|
| 2068 | + */ |
|
| 2069 | + public function getSubscriptionsForUser($principalUri) { |
|
| 2070 | + $fields = array_values($this->subscriptionPropertyMap); |
|
| 2071 | + $fields[] = 'id'; |
|
| 2072 | + $fields[] = 'uri'; |
|
| 2073 | + $fields[] = 'source'; |
|
| 2074 | + $fields[] = 'principaluri'; |
|
| 2075 | + $fields[] = 'lastmodified'; |
|
| 2076 | + $fields[] = 'synctoken'; |
|
| 2077 | + |
|
| 2078 | + $query = $this->db->getQueryBuilder(); |
|
| 2079 | + $query->select($fields) |
|
| 2080 | + ->from('calendarsubscriptions') |
|
| 2081 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2082 | + ->orderBy('calendarorder', 'asc'); |
|
| 2083 | + $stmt = $query->executeQuery(); |
|
| 2084 | + |
|
| 2085 | + $subscriptions = []; |
|
| 2086 | + while ($row = $stmt->fetch()) { |
|
| 2087 | + $subscription = [ |
|
| 2088 | + 'id' => $row['id'], |
|
| 2089 | + 'uri' => $row['uri'], |
|
| 2090 | + 'principaluri' => $row['principaluri'], |
|
| 2091 | + 'source' => $row['source'], |
|
| 2092 | + 'lastmodified' => $row['lastmodified'], |
|
| 2093 | + |
|
| 2094 | + '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO', 'VEVENT']), |
|
| 2095 | + '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', |
|
| 2096 | + ]; |
|
| 2097 | + |
|
| 2098 | + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { |
|
| 2099 | + if (!is_null($row[$dbName])) { |
|
| 2100 | + $subscription[$xmlName] = $row[$dbName]; |
|
| 2101 | + } |
|
| 2102 | + } |
|
| 2103 | + |
|
| 2104 | + $subscriptions[] = $subscription; |
|
| 2105 | + } |
|
| 2106 | + |
|
| 2107 | + return $subscriptions; |
|
| 2108 | + } |
|
| 2109 | + |
|
| 2110 | + /** |
|
| 2111 | + * Creates a new subscription for a principal. |
|
| 2112 | + * |
|
| 2113 | + * If the creation was a success, an id must be returned that can be used to reference |
|
| 2114 | + * this subscription in other methods, such as updateSubscription. |
|
| 2115 | + * |
|
| 2116 | + * @param string $principalUri |
|
| 2117 | + * @param string $uri |
|
| 2118 | + * @param array $properties |
|
| 2119 | + * @return mixed |
|
| 2120 | + */ |
|
| 2121 | + public function createSubscription($principalUri, $uri, array $properties) { |
|
| 2122 | + if (!isset($properties['{http://calendarserver.org/ns/}source'])) { |
|
| 2123 | + throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); |
|
| 2124 | + } |
|
| 2125 | + |
|
| 2126 | + $values = [ |
|
| 2127 | + 'principaluri' => $principalUri, |
|
| 2128 | + 'uri' => $uri, |
|
| 2129 | + 'source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), |
|
| 2130 | + 'lastmodified' => time(), |
|
| 2131 | + ]; |
|
| 2132 | + |
|
| 2133 | + $propertiesBoolean = ['striptodos', 'stripalarms', 'stripattachments']; |
|
| 2134 | + |
|
| 2135 | + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { |
|
| 2136 | + if (array_key_exists($xmlName, $properties)) { |
|
| 2137 | + $values[$dbName] = $properties[$xmlName]; |
|
| 2138 | + if (in_array($dbName, $propertiesBoolean)) { |
|
| 2139 | + $values[$dbName] = true; |
|
| 2140 | + } |
|
| 2141 | + } |
|
| 2142 | + } |
|
| 2143 | + |
|
| 2144 | + $valuesToInsert = []; |
|
| 2145 | + |
|
| 2146 | + $query = $this->db->getQueryBuilder(); |
|
| 2147 | + |
|
| 2148 | + foreach (array_keys($values) as $name) { |
|
| 2149 | + $valuesToInsert[$name] = $query->createNamedParameter($values[$name]); |
|
| 2150 | + } |
|
| 2151 | + |
|
| 2152 | + $query->insert('calendarsubscriptions') |
|
| 2153 | + ->values($valuesToInsert) |
|
| 2154 | + ->executeStatement(); |
|
| 2155 | + |
|
| 2156 | + $subscriptionId = $query->getLastInsertId(); |
|
| 2157 | + |
|
| 2158 | + $subscriptionRow = $this->getSubscriptionById($subscriptionId); |
|
| 2159 | + $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow)); |
|
| 2160 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent( |
|
| 2161 | + '\OCA\DAV\CalDAV\CalDavBackend::createSubscription', |
|
| 2162 | + [ |
|
| 2163 | + 'subscriptionId' => $subscriptionId, |
|
| 2164 | + 'subscriptionData' => $subscriptionRow, |
|
| 2165 | + ])); |
|
| 2166 | + |
|
| 2167 | + return $subscriptionId; |
|
| 2168 | + } |
|
| 2169 | + |
|
| 2170 | + /** |
|
| 2171 | + * Updates a subscription |
|
| 2172 | + * |
|
| 2173 | + * The list of mutations is stored in a Sabre\DAV\PropPatch object. |
|
| 2174 | + * To do the actual updates, you must tell this object which properties |
|
| 2175 | + * you're going to process with the handle() method. |
|
| 2176 | + * |
|
| 2177 | + * Calling the handle method is like telling the PropPatch object "I |
|
| 2178 | + * promise I can handle updating this property". |
|
| 2179 | + * |
|
| 2180 | + * Read the PropPatch documentation for more info and examples. |
|
| 2181 | + * |
|
| 2182 | + * @param mixed $subscriptionId |
|
| 2183 | + * @param PropPatch $propPatch |
|
| 2184 | + * @return void |
|
| 2185 | + */ |
|
| 2186 | + public function updateSubscription($subscriptionId, PropPatch $propPatch) { |
|
| 2187 | + $supportedProperties = array_keys($this->subscriptionPropertyMap); |
|
| 2188 | + $supportedProperties[] = '{http://calendarserver.org/ns/}source'; |
|
| 2189 | + |
|
| 2190 | + $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) { |
|
| 2191 | + $newValues = []; |
|
| 2192 | + |
|
| 2193 | + foreach ($mutations as $propertyName => $propertyValue) { |
|
| 2194 | + if ($propertyName === '{http://calendarserver.org/ns/}source') { |
|
| 2195 | + $newValues['source'] = $propertyValue->getHref(); |
|
| 2196 | + } else { |
|
| 2197 | + $fieldName = $this->subscriptionPropertyMap[$propertyName]; |
|
| 2198 | + $newValues[$fieldName] = $propertyValue; |
|
| 2199 | + } |
|
| 2200 | + } |
|
| 2201 | + |
|
| 2202 | + $query = $this->db->getQueryBuilder(); |
|
| 2203 | + $query->update('calendarsubscriptions') |
|
| 2204 | + ->set('lastmodified', $query->createNamedParameter(time())); |
|
| 2205 | + foreach ($newValues as $fieldName => $value) { |
|
| 2206 | + $query->set($fieldName, $query->createNamedParameter($value)); |
|
| 2207 | + } |
|
| 2208 | + $query->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) |
|
| 2209 | + ->executeStatement(); |
|
| 2210 | + |
|
| 2211 | + $subscriptionRow = $this->getSubscriptionById($subscriptionId); |
|
| 2212 | + $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations)); |
|
| 2213 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent( |
|
| 2214 | + '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', |
|
| 2215 | + [ |
|
| 2216 | + 'subscriptionId' => $subscriptionId, |
|
| 2217 | + 'subscriptionData' => $subscriptionRow, |
|
| 2218 | + 'propertyMutations' => $mutations, |
|
| 2219 | + ])); |
|
| 2220 | + |
|
| 2221 | + return true; |
|
| 2222 | + }); |
|
| 2223 | + } |
|
| 2224 | + |
|
| 2225 | + /** |
|
| 2226 | + * Deletes a subscription. |
|
| 2227 | + * |
|
| 2228 | + * @param mixed $subscriptionId |
|
| 2229 | + * @return void |
|
| 2230 | + */ |
|
| 2231 | + public function deleteSubscription($subscriptionId) { |
|
| 2232 | + $subscriptionRow = $this->getSubscriptionById($subscriptionId); |
|
| 2233 | + |
|
| 2234 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent( |
|
| 2235 | + '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', |
|
| 2236 | + [ |
|
| 2237 | + 'subscriptionId' => $subscriptionId, |
|
| 2238 | + 'subscriptionData' => $this->getSubscriptionById($subscriptionId), |
|
| 2239 | + ])); |
|
| 2240 | + |
|
| 2241 | + $query = $this->db->getQueryBuilder(); |
|
| 2242 | + $query->delete('calendarsubscriptions') |
|
| 2243 | + ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) |
|
| 2244 | + ->executeStatement(); |
|
| 2245 | + |
|
| 2246 | + $query = $this->db->getQueryBuilder(); |
|
| 2247 | + $query->delete('calendarobjects') |
|
| 2248 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2249 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2250 | + ->executeStatement(); |
|
| 2251 | + |
|
| 2252 | + $query->delete('calendarchanges') |
|
| 2253 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2254 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2255 | + ->executeStatement(); |
|
| 2256 | + |
|
| 2257 | + $query->delete($this->dbObjectPropertiesTable) |
|
| 2258 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2259 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2260 | + ->executeStatement(); |
|
| 2261 | + |
|
| 2262 | + if ($subscriptionRow) { |
|
| 2263 | + $this->dispatcher->dispatchTyped(new SubscriptionDeletedEvent((int)$subscriptionId, $subscriptionRow, [])); |
|
| 2264 | + } |
|
| 2265 | + } |
|
| 2266 | + |
|
| 2267 | + /** |
|
| 2268 | + * Returns a single scheduling object for the inbox collection. |
|
| 2269 | + * |
|
| 2270 | + * The returned array should contain the following elements: |
|
| 2271 | + * * uri - A unique basename for the object. This will be used to |
|
| 2272 | + * construct a full uri. |
|
| 2273 | + * * calendardata - The iCalendar object |
|
| 2274 | + * * lastmodified - The last modification date. Can be an int for a unix |
|
| 2275 | + * timestamp, or a PHP DateTime object. |
|
| 2276 | + * * etag - A unique token that must change if the object changed. |
|
| 2277 | + * * size - The size of the object, in bytes. |
|
| 2278 | + * |
|
| 2279 | + * @param string $principalUri |
|
| 2280 | + * @param string $objectUri |
|
| 2281 | + * @return array |
|
| 2282 | + */ |
|
| 2283 | + public function getSchedulingObject($principalUri, $objectUri) { |
|
| 2284 | + $query = $this->db->getQueryBuilder(); |
|
| 2285 | + $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) |
|
| 2286 | + ->from('schedulingobjects') |
|
| 2287 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2288 | + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 2289 | + ->executeQuery(); |
|
| 2290 | + |
|
| 2291 | + $row = $stmt->fetch(); |
|
| 2292 | + |
|
| 2293 | + if (!$row) { |
|
| 2294 | + return null; |
|
| 2295 | + } |
|
| 2296 | + |
|
| 2297 | + return [ |
|
| 2298 | + 'uri' => $row['uri'], |
|
| 2299 | + 'calendardata' => $row['calendardata'], |
|
| 2300 | + 'lastmodified' => $row['lastmodified'], |
|
| 2301 | + 'etag' => '"' . $row['etag'] . '"', |
|
| 2302 | + 'size' => (int)$row['size'], |
|
| 2303 | + ]; |
|
| 2304 | + } |
|
| 2305 | + |
|
| 2306 | + /** |
|
| 2307 | + * Returns all scheduling objects for the inbox collection. |
|
| 2308 | + * |
|
| 2309 | + * These objects should be returned as an array. Every item in the array |
|
| 2310 | + * should follow the same structure as returned from getSchedulingObject. |
|
| 2311 | + * |
|
| 2312 | + * The main difference is that 'calendardata' is optional. |
|
| 2313 | + * |
|
| 2314 | + * @param string $principalUri |
|
| 2315 | + * @return array |
|
| 2316 | + */ |
|
| 2317 | + public function getSchedulingObjects($principalUri) { |
|
| 2318 | + $query = $this->db->getQueryBuilder(); |
|
| 2319 | + $stmt = $query->select(['uri', 'calendardata', 'lastmodified', 'etag', 'size']) |
|
| 2320 | + ->from('schedulingobjects') |
|
| 2321 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2322 | + ->executeQuery(); |
|
| 2323 | + |
|
| 2324 | + $result = []; |
|
| 2325 | + foreach ($stmt->fetchAll() as $row) { |
|
| 2326 | + $result[] = [ |
|
| 2327 | + 'calendardata' => $row['calendardata'], |
|
| 2328 | + 'uri' => $row['uri'], |
|
| 2329 | + 'lastmodified' => $row['lastmodified'], |
|
| 2330 | + 'etag' => '"' . $row['etag'] . '"', |
|
| 2331 | + 'size' => (int)$row['size'], |
|
| 2332 | + ]; |
|
| 2333 | + } |
|
| 2334 | + |
|
| 2335 | + return $result; |
|
| 2336 | + } |
|
| 2337 | + |
|
| 2338 | + /** |
|
| 2339 | + * Deletes a scheduling object from the inbox collection. |
|
| 2340 | + * |
|
| 2341 | + * @param string $principalUri |
|
| 2342 | + * @param string $objectUri |
|
| 2343 | + * @return void |
|
| 2344 | + */ |
|
| 2345 | + public function deleteSchedulingObject($principalUri, $objectUri) { |
|
| 2346 | + $query = $this->db->getQueryBuilder(); |
|
| 2347 | + $query->delete('schedulingobjects') |
|
| 2348 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principalUri))) |
|
| 2349 | + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($objectUri))) |
|
| 2350 | + ->executeStatement(); |
|
| 2351 | + } |
|
| 2352 | + |
|
| 2353 | + /** |
|
| 2354 | + * Creates a new scheduling object. This should land in a users' inbox. |
|
| 2355 | + * |
|
| 2356 | + * @param string $principalUri |
|
| 2357 | + * @param string $objectUri |
|
| 2358 | + * @param string $objectData |
|
| 2359 | + * @return void |
|
| 2360 | + */ |
|
| 2361 | + public function createSchedulingObject($principalUri, $objectUri, $objectData) { |
|
| 2362 | + $query = $this->db->getQueryBuilder(); |
|
| 2363 | + $query->insert('schedulingobjects') |
|
| 2364 | + ->values([ |
|
| 2365 | + 'principaluri' => $query->createNamedParameter($principalUri), |
|
| 2366 | + 'calendardata' => $query->createNamedParameter($objectData, IQueryBuilder::PARAM_LOB), |
|
| 2367 | + 'uri' => $query->createNamedParameter($objectUri), |
|
| 2368 | + 'lastmodified' => $query->createNamedParameter(time()), |
|
| 2369 | + 'etag' => $query->createNamedParameter(md5($objectData)), |
|
| 2370 | + 'size' => $query->createNamedParameter(strlen($objectData)) |
|
| 2371 | + ]) |
|
| 2372 | + ->executeStatement(); |
|
| 2373 | + } |
|
| 2374 | + |
|
| 2375 | + /** |
|
| 2376 | + * Adds a change record to the calendarchanges table. |
|
| 2377 | + * |
|
| 2378 | + * @param mixed $calendarId |
|
| 2379 | + * @param string $objectUri |
|
| 2380 | + * @param int $operation 1 = add, 2 = modify, 3 = delete. |
|
| 2381 | + * @param int $calendarType |
|
| 2382 | + * @return void |
|
| 2383 | + */ |
|
| 2384 | + protected function addChange($calendarId, $objectUri, $operation, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 2385 | + $table = $calendarType === self::CALENDAR_TYPE_CALENDAR ? 'calendars': 'calendarsubscriptions'; |
|
| 2386 | + |
|
| 2387 | + $query = $this->db->getQueryBuilder(); |
|
| 2388 | + $query->select('synctoken') |
|
| 2389 | + ->from($table) |
|
| 2390 | + ->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); |
|
| 2391 | + $result = $query->executeQuery(); |
|
| 2392 | + $syncToken = (int)$result->fetchOne(); |
|
| 2393 | + $result->closeCursor(); |
|
| 2394 | + |
|
| 2395 | + $query = $this->db->getQueryBuilder(); |
|
| 2396 | + $query->insert('calendarchanges') |
|
| 2397 | + ->values([ |
|
| 2398 | + 'uri' => $query->createNamedParameter($objectUri), |
|
| 2399 | + 'synctoken' => $query->createNamedParameter($syncToken), |
|
| 2400 | + 'calendarid' => $query->createNamedParameter($calendarId), |
|
| 2401 | + 'operation' => $query->createNamedParameter($operation), |
|
| 2402 | + 'calendartype' => $query->createNamedParameter($calendarType), |
|
| 2403 | + ]) |
|
| 2404 | + ->executeStatement(); |
|
| 2405 | + |
|
| 2406 | + $stmt = $this->db->prepare("UPDATE `*PREFIX*$table` SET `synctoken` = `synctoken` + 1 WHERE `id` = ?"); |
|
| 2407 | + $stmt->execute([ |
|
| 2408 | + $calendarId |
|
| 2409 | + ]); |
|
| 2410 | + } |
|
| 2411 | + |
|
| 2412 | + /** |
|
| 2413 | + * Parses some information from calendar objects, used for optimized |
|
| 2414 | + * calendar-queries. |
|
| 2415 | + * |
|
| 2416 | + * Returns an array with the following keys: |
|
| 2417 | + * * etag - An md5 checksum of the object without the quotes. |
|
| 2418 | + * * size - Size of the object in bytes |
|
| 2419 | + * * componentType - VEVENT, VTODO or VJOURNAL |
|
| 2420 | + * * firstOccurence |
|
| 2421 | + * * lastOccurence |
|
| 2422 | + * * uid - value of the UID property |
|
| 2423 | + * |
|
| 2424 | + * @param string $calendarData |
|
| 2425 | + * @return array |
|
| 2426 | + */ |
|
| 2427 | + public function getDenormalizedData($calendarData) { |
|
| 2428 | + $vObject = Reader::read($calendarData); |
|
| 2429 | + $vEvents = []; |
|
| 2430 | + $componentType = null; |
|
| 2431 | + $component = null; |
|
| 2432 | + $firstOccurrence = null; |
|
| 2433 | + $lastOccurrence = null; |
|
| 2434 | + $uid = null; |
|
| 2435 | + $classification = self::CLASSIFICATION_PUBLIC; |
|
| 2436 | + $hasDTSTART = false; |
|
| 2437 | + foreach ($vObject->getComponents() as $component) { |
|
| 2438 | + if ($component->name !== 'VTIMEZONE') { |
|
| 2439 | + // Finding all VEVENTs, and track them |
|
| 2440 | + if ($component->name === 'VEVENT') { |
|
| 2441 | + array_push($vEvents, $component); |
|
| 2442 | + if ($component->DTSTART) { |
|
| 2443 | + $hasDTSTART = true; |
|
| 2444 | + } |
|
| 2445 | + } |
|
| 2446 | + // Track first component type and uid |
|
| 2447 | + if ($uid === null) { |
|
| 2448 | + $componentType = $component->name; |
|
| 2449 | + $uid = (string)$component->UID; |
|
| 2450 | + } |
|
| 2451 | + } |
|
| 2452 | + } |
|
| 2453 | + if (!$componentType) { |
|
| 2454 | + throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); |
|
| 2455 | + } |
|
| 2456 | + |
|
| 2457 | + if ($hasDTSTART) { |
|
| 2458 | + $component = $vEvents[0]; |
|
| 2459 | + |
|
| 2460 | + // Finding the last occurrence is a bit harder |
|
| 2461 | + if (!isset($component->RRULE) && count($vEvents) === 1) { |
|
| 2462 | + $firstOccurrence = $component->DTSTART->getDateTime()->getTimeStamp(); |
|
| 2463 | + if (isset($component->DTEND)) { |
|
| 2464 | + $lastOccurrence = $component->DTEND->getDateTime()->getTimeStamp(); |
|
| 2465 | + } elseif (isset($component->DURATION)) { |
|
| 2466 | + $endDate = clone $component->DTSTART->getDateTime(); |
|
| 2467 | + $endDate->add(DateTimeParser::parse($component->DURATION->getValue())); |
|
| 2468 | + $lastOccurrence = $endDate->getTimeStamp(); |
|
| 2469 | + } elseif (!$component->DTSTART->hasTime()) { |
|
| 2470 | + $endDate = clone $component->DTSTART->getDateTime(); |
|
| 2471 | + $endDate->modify('+1 day'); |
|
| 2472 | + $lastOccurrence = $endDate->getTimeStamp(); |
|
| 2473 | + } else { |
|
| 2474 | + $lastOccurrence = $firstOccurrence; |
|
| 2475 | + } |
|
| 2476 | + } else { |
|
| 2477 | + $it = new EventIterator($vEvents); |
|
| 2478 | + $maxDate = new DateTime(self::MAX_DATE); |
|
| 2479 | + $firstOccurrence = $it->getDtStart()->getTimestamp(); |
|
| 2480 | + if ($it->isInfinite()) { |
|
| 2481 | + $lastOccurrence = $maxDate->getTimestamp(); |
|
| 2482 | + } else { |
|
| 2483 | + $end = $it->getDtEnd(); |
|
| 2484 | + while ($it->valid() && $end < $maxDate) { |
|
| 2485 | + $end = $it->getDtEnd(); |
|
| 2486 | + $it->next(); |
|
| 2487 | + } |
|
| 2488 | + $lastOccurrence = $end->getTimestamp(); |
|
| 2489 | + } |
|
| 2490 | + } |
|
| 2491 | + } |
|
| 2492 | + |
|
| 2493 | + if ($component->CLASS) { |
|
| 2494 | + $classification = CalDavBackend::CLASSIFICATION_PRIVATE; |
|
| 2495 | + switch ($component->CLASS->getValue()) { |
|
| 2496 | + case 'PUBLIC': |
|
| 2497 | + $classification = CalDavBackend::CLASSIFICATION_PUBLIC; |
|
| 2498 | + break; |
|
| 2499 | + case 'CONFIDENTIAL': |
|
| 2500 | + $classification = CalDavBackend::CLASSIFICATION_CONFIDENTIAL; |
|
| 2501 | + break; |
|
| 2502 | + } |
|
| 2503 | + } |
|
| 2504 | + return [ |
|
| 2505 | + 'etag' => md5($calendarData), |
|
| 2506 | + 'size' => strlen($calendarData), |
|
| 2507 | + 'componentType' => $componentType, |
|
| 2508 | + 'firstOccurence' => is_null($firstOccurrence) ? null : max(0, $firstOccurrence), |
|
| 2509 | + 'lastOccurence' => $lastOccurrence, |
|
| 2510 | + 'uid' => $uid, |
|
| 2511 | + 'classification' => $classification |
|
| 2512 | + ]; |
|
| 2513 | + } |
|
| 2514 | + |
|
| 2515 | + /** |
|
| 2516 | + * @param $cardData |
|
| 2517 | + * @return bool|string |
|
| 2518 | + */ |
|
| 2519 | + private function readBlob($cardData) { |
|
| 2520 | + if (is_resource($cardData)) { |
|
| 2521 | + return stream_get_contents($cardData); |
|
| 2522 | + } |
|
| 2523 | + |
|
| 2524 | + return $cardData; |
|
| 2525 | + } |
|
| 2526 | + |
|
| 2527 | + /** |
|
| 2528 | + * @param IShareable $shareable |
|
| 2529 | + * @param array $add |
|
| 2530 | + * @param array $remove |
|
| 2531 | + */ |
|
| 2532 | + public function updateShares($shareable, $add, $remove) { |
|
| 2533 | + $calendarId = $shareable->getResourceId(); |
|
| 2534 | + $calendarRow = $this->getCalendarById($calendarId); |
|
| 2535 | + $oldShares = $this->getShares($calendarId); |
|
| 2536 | + |
|
| 2537 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent( |
|
| 2538 | + '\OCA\DAV\CalDAV\CalDavBackend::updateShares', |
|
| 2539 | + [ |
|
| 2540 | + 'calendarId' => $calendarId, |
|
| 2541 | + 'calendarData' => $calendarRow, |
|
| 2542 | + 'shares' => $oldShares, |
|
| 2543 | + 'add' => $add, |
|
| 2544 | + 'remove' => $remove, |
|
| 2545 | + ])); |
|
| 2546 | + $this->calendarSharingBackend->updateShares($shareable, $add, $remove); |
|
| 2547 | + |
|
| 2548 | + $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove)); |
|
| 2549 | + } |
|
| 2550 | + |
|
| 2551 | + /** |
|
| 2552 | + * @param int $resourceId |
|
| 2553 | + * @param int $calendarType |
|
| 2554 | + * @return array |
|
| 2555 | + */ |
|
| 2556 | + public function getShares($resourceId, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 2557 | + return $this->calendarSharingBackend->getShares($resourceId); |
|
| 2558 | + } |
|
| 2559 | + |
|
| 2560 | + /** |
|
| 2561 | + * @param boolean $value |
|
| 2562 | + * @param \OCA\DAV\CalDAV\Calendar $calendar |
|
| 2563 | + * @return string|null |
|
| 2564 | + */ |
|
| 2565 | + public function setPublishStatus($value, $calendar) { |
|
| 2566 | + $calendarId = $calendar->getResourceId(); |
|
| 2567 | + $calendarData = $this->getCalendarById($calendarId); |
|
| 2568 | + $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent( |
|
| 2569 | + '\OCA\DAV\CalDAV\CalDavBackend::updateShares', |
|
| 2570 | + [ |
|
| 2571 | + 'calendarId' => $calendarId, |
|
| 2572 | + 'calendarData' => $calendarData, |
|
| 2573 | + 'public' => $value, |
|
| 2574 | + ])); |
|
| 2575 | + |
|
| 2576 | + $query = $this->db->getQueryBuilder(); |
|
| 2577 | + if ($value) { |
|
| 2578 | + $publicUri = $this->random->generate(16, ISecureRandom::CHAR_HUMAN_READABLE); |
|
| 2579 | + $query->insert('dav_shares') |
|
| 2580 | + ->values([ |
|
| 2581 | + 'principaluri' => $query->createNamedParameter($calendar->getPrincipalURI()), |
|
| 2582 | + 'type' => $query->createNamedParameter('calendar'), |
|
| 2583 | + 'access' => $query->createNamedParameter(self::ACCESS_PUBLIC), |
|
| 2584 | + 'resourceid' => $query->createNamedParameter($calendar->getResourceId()), |
|
| 2585 | + 'publicuri' => $query->createNamedParameter($publicUri) |
|
| 2586 | + ]); |
|
| 2587 | + $query->executeStatement(); |
|
| 2588 | + |
|
| 2589 | + $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri)); |
|
| 2590 | + return $publicUri; |
|
| 2591 | + } |
|
| 2592 | + $query->delete('dav_shares') |
|
| 2593 | + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) |
|
| 2594 | + ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))); |
|
| 2595 | + $query->executeStatement(); |
|
| 2596 | + |
|
| 2597 | + $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData)); |
|
| 2598 | + return null; |
|
| 2599 | + } |
|
| 2600 | + |
|
| 2601 | + /** |
|
| 2602 | + * @param \OCA\DAV\CalDAV\Calendar $calendar |
|
| 2603 | + * @return mixed |
|
| 2604 | + */ |
|
| 2605 | + public function getPublishStatus($calendar) { |
|
| 2606 | + $query = $this->db->getQueryBuilder(); |
|
| 2607 | + $result = $query->select('publicuri') |
|
| 2608 | + ->from('dav_shares') |
|
| 2609 | + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($calendar->getResourceId()))) |
|
| 2610 | + ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))) |
|
| 2611 | + ->executeQuery(); |
|
| 2612 | + |
|
| 2613 | + $row = $result->fetch(); |
|
| 2614 | + $result->closeCursor(); |
|
| 2615 | + return $row ? reset($row) : false; |
|
| 2616 | + } |
|
| 2617 | + |
|
| 2618 | + /** |
|
| 2619 | + * @param int $resourceId |
|
| 2620 | + * @param array $acl |
|
| 2621 | + * @return array |
|
| 2622 | + */ |
|
| 2623 | + public function applyShareAcl($resourceId, $acl) { |
|
| 2624 | + return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl); |
|
| 2625 | + } |
|
| 2626 | + |
|
| 2627 | + |
|
| 2628 | + |
|
| 2629 | + /** |
|
| 2630 | + * update properties table |
|
| 2631 | + * |
|
| 2632 | + * @param int $calendarId |
|
| 2633 | + * @param string $objectUri |
|
| 2634 | + * @param string $calendarData |
|
| 2635 | + * @param int $calendarType |
|
| 2636 | + */ |
|
| 2637 | + public function updateProperties($calendarId, $objectUri, $calendarData, $calendarType = self::CALENDAR_TYPE_CALENDAR) { |
|
| 2638 | + $objectId = $this->getCalendarObjectId($calendarId, $objectUri, $calendarType); |
|
| 2639 | + |
|
| 2640 | + try { |
|
| 2641 | + $vCalendar = $this->readCalendarData($calendarData); |
|
| 2642 | + } catch (\Exception $ex) { |
|
| 2643 | + return; |
|
| 2644 | + } |
|
| 2645 | + |
|
| 2646 | + $this->purgeProperties($calendarId, $objectId); |
|
| 2647 | + |
|
| 2648 | + $query = $this->db->getQueryBuilder(); |
|
| 2649 | + $query->insert($this->dbObjectPropertiesTable) |
|
| 2650 | + ->values( |
|
| 2651 | + [ |
|
| 2652 | + 'calendarid' => $query->createNamedParameter($calendarId), |
|
| 2653 | + 'calendartype' => $query->createNamedParameter($calendarType), |
|
| 2654 | + 'objectid' => $query->createNamedParameter($objectId), |
|
| 2655 | + 'name' => $query->createParameter('name'), |
|
| 2656 | + 'parameter' => $query->createParameter('parameter'), |
|
| 2657 | + 'value' => $query->createParameter('value'), |
|
| 2658 | + ] |
|
| 2659 | + ); |
|
| 2660 | + |
|
| 2661 | + $indexComponents = ['VEVENT', 'VJOURNAL', 'VTODO']; |
|
| 2662 | + foreach ($vCalendar->getComponents() as $component) { |
|
| 2663 | + if (!in_array($component->name, $indexComponents)) { |
|
| 2664 | + continue; |
|
| 2665 | + } |
|
| 2666 | + |
|
| 2667 | + foreach ($component->children() as $property) { |
|
| 2668 | + if (in_array($property->name, self::$indexProperties)) { |
|
| 2669 | + $value = $property->getValue(); |
|
| 2670 | + // is this a shitty db? |
|
| 2671 | + if (!$this->db->supports4ByteText()) { |
|
| 2672 | + $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); |
|
| 2673 | + } |
|
| 2674 | + $value = mb_strcut($value, 0, 254); |
|
| 2675 | + |
|
| 2676 | + $query->setParameter('name', $property->name); |
|
| 2677 | + $query->setParameter('parameter', null); |
|
| 2678 | + $query->setParameter('value', $value); |
|
| 2679 | + $query->executeStatement(); |
|
| 2680 | + } |
|
| 2681 | + |
|
| 2682 | + if (array_key_exists($property->name, self::$indexParameters)) { |
|
| 2683 | + $parameters = $property->parameters(); |
|
| 2684 | + $indexedParametersForProperty = self::$indexParameters[$property->name]; |
|
| 2685 | + |
|
| 2686 | + foreach ($parameters as $key => $value) { |
|
| 2687 | + if (in_array($key, $indexedParametersForProperty)) { |
|
| 2688 | + // is this a shitty db? |
|
| 2689 | + if ($this->db->supports4ByteText()) { |
|
| 2690 | + $value = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $value); |
|
| 2691 | + } |
|
| 2692 | + |
|
| 2693 | + $query->setParameter('name', $property->name); |
|
| 2694 | + $query->setParameter('parameter', mb_strcut($key, 0, 254)); |
|
| 2695 | + $query->setParameter('value', mb_strcut($value, 0, 254)); |
|
| 2696 | + $query->executeStatement(); |
|
| 2697 | + } |
|
| 2698 | + } |
|
| 2699 | + } |
|
| 2700 | + } |
|
| 2701 | + } |
|
| 2702 | + } |
|
| 2703 | + |
|
| 2704 | + /** |
|
| 2705 | + * deletes all birthday calendars |
|
| 2706 | + */ |
|
| 2707 | + public function deleteAllBirthdayCalendars() { |
|
| 2708 | + $query = $this->db->getQueryBuilder(); |
|
| 2709 | + $result = $query->select(['id'])->from('calendars') |
|
| 2710 | + ->where($query->expr()->eq('uri', $query->createNamedParameter(BirthdayService::BIRTHDAY_CALENDAR_URI))) |
|
| 2711 | + ->executeQuery(); |
|
| 2712 | + |
|
| 2713 | + $ids = $result->fetchAll(); |
|
| 2714 | + foreach ($ids as $id) { |
|
| 2715 | + $this->deleteCalendar($id['id']); |
|
| 2716 | + } |
|
| 2717 | + } |
|
| 2718 | + |
|
| 2719 | + /** |
|
| 2720 | + * @param $subscriptionId |
|
| 2721 | + */ |
|
| 2722 | + public function purgeAllCachedEventsForSubscription($subscriptionId) { |
|
| 2723 | + $query = $this->db->getQueryBuilder(); |
|
| 2724 | + $query->select('uri') |
|
| 2725 | + ->from('calendarobjects') |
|
| 2726 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2727 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))); |
|
| 2728 | + $stmt = $query->executeQuery(); |
|
| 2729 | + |
|
| 2730 | + $uris = []; |
|
| 2731 | + foreach ($stmt->fetchAll() as $row) { |
|
| 2732 | + $uris[] = $row['uri']; |
|
| 2733 | + } |
|
| 2734 | + $stmt->closeCursor(); |
|
| 2735 | + |
|
| 2736 | + $query = $this->db->getQueryBuilder(); |
|
| 2737 | + $query->delete('calendarobjects') |
|
| 2738 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2739 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2740 | + ->executeStatement(); |
|
| 2741 | + |
|
| 2742 | + $query->delete('calendarchanges') |
|
| 2743 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2744 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2745 | + ->executeStatement(); |
|
| 2746 | + |
|
| 2747 | + $query->delete($this->dbObjectPropertiesTable) |
|
| 2748 | + ->where($query->expr()->eq('calendarid', $query->createNamedParameter($subscriptionId))) |
|
| 2749 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter(self::CALENDAR_TYPE_SUBSCRIPTION))) |
|
| 2750 | + ->executeStatement(); |
|
| 2751 | + |
|
| 2752 | + foreach ($uris as $uri) { |
|
| 2753 | + $this->addChange($subscriptionId, $uri, 3, self::CALENDAR_TYPE_SUBSCRIPTION); |
|
| 2754 | + } |
|
| 2755 | + } |
|
| 2756 | + |
|
| 2757 | + /** |
|
| 2758 | + * Move a calendar from one user to another |
|
| 2759 | + * |
|
| 2760 | + * @param string $uriName |
|
| 2761 | + * @param string $uriOrigin |
|
| 2762 | + * @param string $uriDestination |
|
| 2763 | + * @param string $newUriName (optional) the new uriName |
|
| 2764 | + */ |
|
| 2765 | + public function moveCalendar($uriName, $uriOrigin, $uriDestination, $newUriName = null) { |
|
| 2766 | + $query = $this->db->getQueryBuilder(); |
|
| 2767 | + $query->update('calendars') |
|
| 2768 | + ->set('principaluri', $query->createNamedParameter($uriDestination)) |
|
| 2769 | + ->set('uri', $query->createNamedParameter($newUriName ?: $uriName)) |
|
| 2770 | + ->where($query->expr()->eq('principaluri', $query->createNamedParameter($uriOrigin))) |
|
| 2771 | + ->andWhere($query->expr()->eq('uri', $query->createNamedParameter($uriName))) |
|
| 2772 | + ->executeStatement(); |
|
| 2773 | + } |
|
| 2774 | + |
|
| 2775 | + /** |
|
| 2776 | + * read VCalendar data into a VCalendar object |
|
| 2777 | + * |
|
| 2778 | + * @param string $objectData |
|
| 2779 | + * @return VCalendar |
|
| 2780 | + */ |
|
| 2781 | + protected function readCalendarData($objectData) { |
|
| 2782 | + return Reader::read($objectData); |
|
| 2783 | + } |
|
| 2784 | + |
|
| 2785 | + /** |
|
| 2786 | + * delete all properties from a given calendar object |
|
| 2787 | + * |
|
| 2788 | + * @param int $calendarId |
|
| 2789 | + * @param int $objectId |
|
| 2790 | + */ |
|
| 2791 | + protected function purgeProperties($calendarId, $objectId) { |
|
| 2792 | + $query = $this->db->getQueryBuilder(); |
|
| 2793 | + $query->delete($this->dbObjectPropertiesTable) |
|
| 2794 | + ->where($query->expr()->eq('objectid', $query->createNamedParameter($objectId))) |
|
| 2795 | + ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))); |
|
| 2796 | + $query->executeStatement(); |
|
| 2797 | + } |
|
| 2798 | + |
|
| 2799 | + /** |
|
| 2800 | + * get ID from a given calendar object |
|
| 2801 | + * |
|
| 2802 | + * @param int $calendarId |
|
| 2803 | + * @param string $uri |
|
| 2804 | + * @param int $calendarType |
|
| 2805 | + * @return int |
|
| 2806 | + */ |
|
| 2807 | + protected function getCalendarObjectId($calendarId, $uri, $calendarType):int { |
|
| 2808 | + $query = $this->db->getQueryBuilder(); |
|
| 2809 | + $query->select('id') |
|
| 2810 | + ->from('calendarobjects') |
|
| 2811 | + ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) |
|
| 2812 | + ->andWhere($query->expr()->eq('calendarid', $query->createNamedParameter($calendarId))) |
|
| 2813 | + ->andWhere($query->expr()->eq('calendartype', $query->createNamedParameter($calendarType))); |
|
| 2814 | + |
|
| 2815 | + $result = $query->executeQuery(); |
|
| 2816 | + $objectIds = $result->fetch(); |
|
| 2817 | + $result->closeCursor(); |
|
| 2818 | + |
|
| 2819 | + if (!isset($objectIds['id'])) { |
|
| 2820 | + throw new \InvalidArgumentException('Calendarobject does not exists: ' . $uri); |
|
| 2821 | + } |
|
| 2822 | + |
|
| 2823 | + return (int)$objectIds['id']; |
|
| 2824 | + } |
|
| 2825 | + |
|
| 2826 | + /** |
|
| 2827 | + * return legacy endpoint principal name to new principal name |
|
| 2828 | + * |
|
| 2829 | + * @param $principalUri |
|
| 2830 | + * @param $toV2 |
|
| 2831 | + * @return string |
|
| 2832 | + */ |
|
| 2833 | + private function convertPrincipal($principalUri, $toV2) { |
|
| 2834 | + if ($this->principalBackend->getPrincipalPrefix() === 'principals') { |
|
| 2835 | + [, $name] = Uri\split($principalUri); |
|
| 2836 | + if ($toV2 === true) { |
|
| 2837 | + return "principals/users/$name"; |
|
| 2838 | + } |
|
| 2839 | + return "principals/$name"; |
|
| 2840 | + } |
|
| 2841 | + return $principalUri; |
|
| 2842 | + } |
|
| 2843 | + |
|
| 2844 | + /** |
|
| 2845 | + * adds information about an owner to the calendar data |
|
| 2846 | + * |
|
| 2847 | + * @param $calendarInfo |
|
| 2848 | + */ |
|
| 2849 | + private function addOwnerPrincipal(&$calendarInfo) { |
|
| 2850 | + $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'; |
|
| 2851 | + $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'; |
|
| 2852 | + if (isset($calendarInfo[$ownerPrincipalKey])) { |
|
| 2853 | + $uri = $calendarInfo[$ownerPrincipalKey]; |
|
| 2854 | + } else { |
|
| 2855 | + $uri = $calendarInfo['principaluri']; |
|
| 2856 | + } |
|
| 2857 | + |
|
| 2858 | + $principalInformation = $this->principalBackend->getPrincipalByPath($uri); |
|
| 2859 | + if (isset($principalInformation['{DAV:}displayname'])) { |
|
| 2860 | + $calendarInfo[$displaynameKey] = $principalInformation['{DAV:}displayname']; |
|
| 2861 | + } |
|
| 2862 | + } |
|
| 2863 | 2863 | } |
@@ -64,1067 +64,1067 @@ |
||
| 64 | 64 | |
| 65 | 65 | class Trashbin { |
| 66 | 66 | |
| 67 | - // unit: percentage; 50% of available disk space/quota |
|
| 68 | - public const DEFAULTMAXSIZE = 50; |
|
| 69 | - |
|
| 70 | - /** |
|
| 71 | - * Whether versions have already be rescanned during this PHP request |
|
| 72 | - * |
|
| 73 | - * @var bool |
|
| 74 | - */ |
|
| 75 | - private static $scannedVersions = false; |
|
| 76 | - |
|
| 77 | - /** |
|
| 78 | - * Ensure we don't need to scan the file during the move to trash |
|
| 79 | - * by triggering the scan in the pre-hook |
|
| 80 | - * |
|
| 81 | - * @param array $params |
|
| 82 | - */ |
|
| 83 | - public static function ensureFileScannedHook($params) { |
|
| 84 | - try { |
|
| 85 | - self::getUidAndFilename($params['path']); |
|
| 86 | - } catch (NotFoundException $e) { |
|
| 87 | - // nothing to scan for non existing files |
|
| 88 | - } |
|
| 89 | - } |
|
| 90 | - |
|
| 91 | - /** |
|
| 92 | - * get the UID of the owner of the file and the path to the file relative to |
|
| 93 | - * owners files folder |
|
| 94 | - * |
|
| 95 | - * @param string $filename |
|
| 96 | - * @return array |
|
| 97 | - * @throws \OC\User\NoUserException |
|
| 98 | - */ |
|
| 99 | - public static function getUidAndFilename($filename) { |
|
| 100 | - $uid = Filesystem::getOwner($filename); |
|
| 101 | - $userManager = \OC::$server->getUserManager(); |
|
| 102 | - // if the user with the UID doesn't exists, e.g. because the UID points |
|
| 103 | - // to a remote user with a federated cloud ID we use the current logged-in |
|
| 104 | - // user. We need a valid local user to move the file to the right trash bin |
|
| 105 | - if (!$userManager->userExists($uid)) { |
|
| 106 | - $uid = User::getUser(); |
|
| 107 | - } |
|
| 108 | - if (!$uid) { |
|
| 109 | - // no owner, usually because of share link from ext storage |
|
| 110 | - return [null, null]; |
|
| 111 | - } |
|
| 112 | - Filesystem::initMountPoints($uid); |
|
| 113 | - if ($uid !== User::getUser()) { |
|
| 114 | - $info = Filesystem::getFileInfo($filename); |
|
| 115 | - $ownerView = new View('/' . $uid . '/files'); |
|
| 116 | - try { |
|
| 117 | - $filename = $ownerView->getPath($info['fileid']); |
|
| 118 | - } catch (NotFoundException $e) { |
|
| 119 | - $filename = null; |
|
| 120 | - } |
|
| 121 | - } |
|
| 122 | - return [$uid, $filename]; |
|
| 123 | - } |
|
| 124 | - |
|
| 125 | - /** |
|
| 126 | - * get original location of files for user |
|
| 127 | - * |
|
| 128 | - * @param string $user |
|
| 129 | - * @return array (filename => array (timestamp => original location)) |
|
| 130 | - */ |
|
| 131 | - public static function getLocations($user) { |
|
| 132 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 133 | - $query->select('id', 'timestamp', 'location') |
|
| 134 | - ->from('files_trash') |
|
| 135 | - ->where($query->expr()->eq('user', $query->createNamedParameter($user))); |
|
| 136 | - $result = $query->executeQuery(); |
|
| 137 | - $array = []; |
|
| 138 | - while ($row = $result->fetch()) { |
|
| 139 | - if (isset($array[$row['id']])) { |
|
| 140 | - $array[$row['id']][$row['timestamp']] = $row['location']; |
|
| 141 | - } else { |
|
| 142 | - $array[$row['id']] = [$row['timestamp'] => $row['location']]; |
|
| 143 | - } |
|
| 144 | - } |
|
| 145 | - $result->closeCursor(); |
|
| 146 | - return $array; |
|
| 147 | - } |
|
| 148 | - |
|
| 149 | - /** |
|
| 150 | - * get original location of file |
|
| 151 | - * |
|
| 152 | - * @param string $user |
|
| 153 | - * @param string $filename |
|
| 154 | - * @param string $timestamp |
|
| 155 | - * @return string original location |
|
| 156 | - */ |
|
| 157 | - public static function getLocation($user, $filename, $timestamp) { |
|
| 158 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 159 | - $query->select('location') |
|
| 160 | - ->from('files_trash') |
|
| 161 | - ->where($query->expr()->eq('user', $query->createNamedParameter($user))) |
|
| 162 | - ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) |
|
| 163 | - ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); |
|
| 164 | - |
|
| 165 | - $result = $query->executeQuery(); |
|
| 166 | - $row = $result->fetch(); |
|
| 167 | - $result->closeCursor(); |
|
| 168 | - |
|
| 169 | - if (isset($row['location'])) { |
|
| 170 | - return $row['location']; |
|
| 171 | - } else { |
|
| 172 | - return false; |
|
| 173 | - } |
|
| 174 | - } |
|
| 175 | - |
|
| 176 | - private static function setUpTrash($user) { |
|
| 177 | - $view = new View('/' . $user); |
|
| 178 | - if (!$view->is_dir('files_trashbin')) { |
|
| 179 | - $view->mkdir('files_trashbin'); |
|
| 180 | - } |
|
| 181 | - if (!$view->is_dir('files_trashbin/files')) { |
|
| 182 | - $view->mkdir('files_trashbin/files'); |
|
| 183 | - } |
|
| 184 | - if (!$view->is_dir('files_trashbin/versions')) { |
|
| 185 | - $view->mkdir('files_trashbin/versions'); |
|
| 186 | - } |
|
| 187 | - if (!$view->is_dir('files_trashbin/keys')) { |
|
| 188 | - $view->mkdir('files_trashbin/keys'); |
|
| 189 | - } |
|
| 190 | - } |
|
| 191 | - |
|
| 192 | - |
|
| 193 | - /** |
|
| 194 | - * copy file to owners trash |
|
| 195 | - * |
|
| 196 | - * @param string $sourcePath |
|
| 197 | - * @param string $owner |
|
| 198 | - * @param string $targetPath |
|
| 199 | - * @param $user |
|
| 200 | - * @param integer $timestamp |
|
| 201 | - */ |
|
| 202 | - private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) { |
|
| 203 | - self::setUpTrash($owner); |
|
| 204 | - |
|
| 205 | - $targetFilename = basename($targetPath); |
|
| 206 | - $targetLocation = dirname($targetPath); |
|
| 207 | - |
|
| 208 | - $sourceFilename = basename($sourcePath); |
|
| 209 | - |
|
| 210 | - $view = new View('/'); |
|
| 211 | - |
|
| 212 | - $target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp; |
|
| 213 | - $source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp; |
|
| 214 | - $free = $view->free_space($target); |
|
| 215 | - $isUnknownOrUnlimitedFreeSpace = $free < 0; |
|
| 216 | - $isEnoughFreeSpaceLeft = $view->filesize($source) < $free; |
|
| 217 | - if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) { |
|
| 218 | - self::copy_recursive($source, $target, $view); |
|
| 219 | - } |
|
| 220 | - |
|
| 221 | - |
|
| 222 | - if ($view->file_exists($target)) { |
|
| 223 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 224 | - $query->insert('files_trash') |
|
| 225 | - ->setValue('id', $query->createNamedParameter($targetFilename)) |
|
| 226 | - ->setValue('timestamp', $query->createNamedParameter($timestamp)) |
|
| 227 | - ->setValue('location', $query->createNamedParameter($targetLocation)) |
|
| 228 | - ->setValue('user', $query->createNamedParameter($user)); |
|
| 229 | - $result = $query->executeStatement(); |
|
| 230 | - if (!$result) { |
|
| 231 | - \OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']); |
|
| 232 | - } |
|
| 233 | - } |
|
| 234 | - } |
|
| 235 | - |
|
| 236 | - |
|
| 237 | - /** |
|
| 238 | - * move file to the trash bin |
|
| 239 | - * |
|
| 240 | - * @param string $file_path path to the deleted file/directory relative to the files root directory |
|
| 241 | - * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder) |
|
| 242 | - * |
|
| 243 | - * @return bool |
|
| 244 | - */ |
|
| 245 | - public static function move2trash($file_path, $ownerOnly = false) { |
|
| 246 | - // get the user for which the filesystem is setup |
|
| 247 | - $root = Filesystem::getRoot(); |
|
| 248 | - [, $user] = explode('/', $root); |
|
| 249 | - [$owner, $ownerPath] = self::getUidAndFilename($file_path); |
|
| 250 | - |
|
| 251 | - // if no owner found (ex: ext storage + share link), will use the current user's trashbin then |
|
| 252 | - if (is_null($owner)) { |
|
| 253 | - $owner = $user; |
|
| 254 | - $ownerPath = $file_path; |
|
| 255 | - } |
|
| 256 | - |
|
| 257 | - $ownerView = new View('/' . $owner); |
|
| 258 | - // file has been deleted in between |
|
| 259 | - if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) { |
|
| 260 | - return true; |
|
| 261 | - } |
|
| 262 | - |
|
| 263 | - self::setUpTrash($user); |
|
| 264 | - if ($owner !== $user) { |
|
| 265 | - // also setup for owner |
|
| 266 | - self::setUpTrash($owner); |
|
| 267 | - } |
|
| 268 | - |
|
| 269 | - $path_parts = pathinfo($ownerPath); |
|
| 270 | - |
|
| 271 | - $filename = $path_parts['basename']; |
|
| 272 | - $location = $path_parts['dirname']; |
|
| 273 | - /** @var ITimeFactory $timeFactory */ |
|
| 274 | - $timeFactory = \OC::$server->query(ITimeFactory::class); |
|
| 275 | - $timestamp = $timeFactory->getTime(); |
|
| 276 | - |
|
| 277 | - $lockingProvider = \OC::$server->getLockingProvider(); |
|
| 278 | - |
|
| 279 | - // disable proxy to prevent recursive calls |
|
| 280 | - $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp; |
|
| 281 | - $gotLock = false; |
|
| 282 | - |
|
| 283 | - while (!$gotLock) { |
|
| 284 | - try { |
|
| 285 | - /** @var \OC\Files\Storage\Storage $trashStorage */ |
|
| 286 | - [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath); |
|
| 287 | - |
|
| 288 | - $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); |
|
| 289 | - $gotLock = true; |
|
| 290 | - } catch (LockedException $e) { |
|
| 291 | - // a file with the same name is being deleted concurrently |
|
| 292 | - // nudge the timestamp a bit to resolve the conflict |
|
| 293 | - |
|
| 294 | - $timestamp = $timestamp + 1; |
|
| 295 | - |
|
| 296 | - $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp; |
|
| 297 | - } |
|
| 298 | - } |
|
| 299 | - |
|
| 300 | - /** @var \OC\Files\Storage\Storage $sourceStorage */ |
|
| 301 | - [$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath); |
|
| 302 | - |
|
| 303 | - |
|
| 304 | - if ($trashStorage->file_exists($trashInternalPath)) { |
|
| 305 | - $trashStorage->unlink($trashInternalPath); |
|
| 306 | - } |
|
| 307 | - |
|
| 308 | - $config = \OC::$server->getConfig(); |
|
| 309 | - $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1'); |
|
| 310 | - $userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1'); |
|
| 311 | - $configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize; |
|
| 312 | - if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) { |
|
| 313 | - return false; |
|
| 314 | - } |
|
| 315 | - |
|
| 316 | - $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath); |
|
| 317 | - |
|
| 318 | - try { |
|
| 319 | - $moveSuccessful = true; |
|
| 320 | - |
|
| 321 | - // when moving within the same object store, the cache update done above is enough to move the file |
|
| 322 | - if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) { |
|
| 323 | - $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath); |
|
| 324 | - } |
|
| 325 | - } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) { |
|
| 326 | - $moveSuccessful = false; |
|
| 327 | - if ($trashStorage->file_exists($trashInternalPath)) { |
|
| 328 | - $trashStorage->unlink($trashInternalPath); |
|
| 329 | - } |
|
| 330 | - \OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']); |
|
| 331 | - } |
|
| 332 | - |
|
| 333 | - if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort |
|
| 334 | - if ($sourceStorage->is_dir($sourceInternalPath)) { |
|
| 335 | - $sourceStorage->rmdir($sourceInternalPath); |
|
| 336 | - } else { |
|
| 337 | - $sourceStorage->unlink($sourceInternalPath); |
|
| 338 | - } |
|
| 339 | - |
|
| 340 | - if ($sourceStorage->file_exists($sourceInternalPath)) { |
|
| 341 | - // undo the cache move |
|
| 342 | - $sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath); |
|
| 343 | - } else { |
|
| 344 | - $trashStorage->getUpdater()->remove($trashInternalPath); |
|
| 345 | - } |
|
| 346 | - return false; |
|
| 347 | - } |
|
| 348 | - |
|
| 349 | - if ($moveSuccessful) { |
|
| 350 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 351 | - $query->insert('files_trash') |
|
| 352 | - ->setValue('id', $query->createNamedParameter($filename)) |
|
| 353 | - ->setValue('timestamp', $query->createNamedParameter($timestamp)) |
|
| 354 | - ->setValue('location', $query->createNamedParameter($location)) |
|
| 355 | - ->setValue('user', $query->createNamedParameter($owner)); |
|
| 356 | - $result = $query->executeStatement(); |
|
| 357 | - if (!$result) { |
|
| 358 | - \OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']); |
|
| 359 | - } |
|
| 360 | - \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path), |
|
| 361 | - 'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]); |
|
| 362 | - |
|
| 363 | - self::retainVersions($filename, $owner, $ownerPath, $timestamp); |
|
| 364 | - |
|
| 365 | - // if owner !== user we need to also add a copy to the users trash |
|
| 366 | - if ($user !== $owner && $ownerOnly === false) { |
|
| 367 | - self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp); |
|
| 368 | - } |
|
| 369 | - } |
|
| 370 | - |
|
| 371 | - $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); |
|
| 372 | - |
|
| 373 | - self::scheduleExpire($user); |
|
| 374 | - |
|
| 375 | - // if owner !== user we also need to update the owners trash size |
|
| 376 | - if ($owner !== $user) { |
|
| 377 | - self::scheduleExpire($owner); |
|
| 378 | - } |
|
| 379 | - |
|
| 380 | - return $moveSuccessful; |
|
| 381 | - } |
|
| 382 | - |
|
| 383 | - /** |
|
| 384 | - * Move file versions to trash so that they can be restored later |
|
| 385 | - * |
|
| 386 | - * @param string $filename of deleted file |
|
| 387 | - * @param string $owner owner user id |
|
| 388 | - * @param string $ownerPath path relative to the owner's home storage |
|
| 389 | - * @param integer $timestamp when the file was deleted |
|
| 390 | - */ |
|
| 391 | - private static function retainVersions($filename, $owner, $ownerPath, $timestamp) { |
|
| 392 | - if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) { |
|
| 393 | - $user = User::getUser(); |
|
| 394 | - $rootView = new View('/'); |
|
| 395 | - |
|
| 396 | - if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) { |
|
| 397 | - if ($owner !== $user) { |
|
| 398 | - self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView); |
|
| 399 | - } |
|
| 400 | - self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp); |
|
| 401 | - } elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) { |
|
| 402 | - foreach ($versions as $v) { |
|
| 403 | - if ($owner !== $user) { |
|
| 404 | - self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp); |
|
| 405 | - } |
|
| 406 | - self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp); |
|
| 407 | - } |
|
| 408 | - } |
|
| 409 | - } |
|
| 410 | - } |
|
| 411 | - |
|
| 412 | - /** |
|
| 413 | - * Move a file or folder on storage level |
|
| 414 | - * |
|
| 415 | - * @param View $view |
|
| 416 | - * @param string $source |
|
| 417 | - * @param string $target |
|
| 418 | - * @return bool |
|
| 419 | - */ |
|
| 420 | - private static function move(View $view, $source, $target) { |
|
| 421 | - /** @var \OC\Files\Storage\Storage $sourceStorage */ |
|
| 422 | - [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source); |
|
| 423 | - /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
| 424 | - [$targetStorage, $targetInternalPath] = $view->resolvePath($target); |
|
| 425 | - /** @var \OC\Files\Storage\Storage $ownerTrashStorage */ |
|
| 426 | - |
|
| 427 | - $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 428 | - if ($result) { |
|
| 429 | - $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 430 | - } |
|
| 431 | - return $result; |
|
| 432 | - } |
|
| 433 | - |
|
| 434 | - /** |
|
| 435 | - * Copy a file or folder on storage level |
|
| 436 | - * |
|
| 437 | - * @param View $view |
|
| 438 | - * @param string $source |
|
| 439 | - * @param string $target |
|
| 440 | - * @return bool |
|
| 441 | - */ |
|
| 442 | - private static function copy(View $view, $source, $target) { |
|
| 443 | - /** @var \OC\Files\Storage\Storage $sourceStorage */ |
|
| 444 | - [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source); |
|
| 445 | - /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
| 446 | - [$targetStorage, $targetInternalPath] = $view->resolvePath($target); |
|
| 447 | - /** @var \OC\Files\Storage\Storage $ownerTrashStorage */ |
|
| 448 | - |
|
| 449 | - $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 450 | - if ($result) { |
|
| 451 | - $targetStorage->getUpdater()->update($targetInternalPath); |
|
| 452 | - } |
|
| 453 | - return $result; |
|
| 454 | - } |
|
| 455 | - |
|
| 456 | - /** |
|
| 457 | - * Restore a file or folder from trash bin |
|
| 458 | - * |
|
| 459 | - * @param string $file path to the deleted file/folder relative to "files_trashbin/files/", |
|
| 460 | - * including the timestamp suffix ".d12345678" |
|
| 461 | - * @param string $filename name of the file/folder |
|
| 462 | - * @param int $timestamp time when the file/folder was deleted |
|
| 463 | - * |
|
| 464 | - * @return bool true on success, false otherwise |
|
| 465 | - */ |
|
| 466 | - public static function restore($file, $filename, $timestamp) { |
|
| 467 | - $user = User::getUser(); |
|
| 468 | - $view = new View('/' . $user); |
|
| 469 | - |
|
| 470 | - $location = ''; |
|
| 471 | - if ($timestamp) { |
|
| 472 | - $location = self::getLocation($user, $filename, $timestamp); |
|
| 473 | - if ($location === false) { |
|
| 474 | - \OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']); |
|
| 475 | - } else { |
|
| 476 | - // if location no longer exists, restore file in the root directory |
|
| 477 | - if ($location !== '/' && |
|
| 478 | - (!$view->is_dir('files/' . $location) || |
|
| 479 | - !$view->isCreatable('files/' . $location)) |
|
| 480 | - ) { |
|
| 481 | - $location = ''; |
|
| 482 | - } |
|
| 483 | - } |
|
| 484 | - } |
|
| 485 | - |
|
| 486 | - // we need a extension in case a file/dir with the same name already exists |
|
| 487 | - $uniqueFilename = self::getUniqueFilename($location, $filename, $view); |
|
| 488 | - |
|
| 489 | - $source = Filesystem::normalizePath('files_trashbin/files/' . $file); |
|
| 490 | - $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename); |
|
| 491 | - if (!$view->file_exists($source)) { |
|
| 492 | - return false; |
|
| 493 | - } |
|
| 494 | - $mtime = $view->filemtime($source); |
|
| 495 | - |
|
| 496 | - // restore file |
|
| 497 | - if (!$view->isCreatable(dirname($target))) { |
|
| 498 | - throw new NotPermittedException("Can't restore trash item because the target folder is not writable"); |
|
| 499 | - } |
|
| 500 | - $restoreResult = $view->rename($source, $target); |
|
| 501 | - |
|
| 502 | - // handle the restore result |
|
| 503 | - if ($restoreResult) { |
|
| 504 | - $fakeRoot = $view->getRoot(); |
|
| 505 | - $view->chroot('/' . $user . '/files'); |
|
| 506 | - $view->touch('/' . $location . '/' . $uniqueFilename, $mtime); |
|
| 507 | - $view->chroot($fakeRoot); |
|
| 508 | - \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename), |
|
| 509 | - 'trashPath' => Filesystem::normalizePath($file)]); |
|
| 510 | - |
|
| 511 | - self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp); |
|
| 512 | - |
|
| 513 | - if ($timestamp) { |
|
| 514 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 515 | - $query->delete('files_trash') |
|
| 516 | - ->where($query->expr()->eq('user', $query->createNamedParameter($user))) |
|
| 517 | - ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) |
|
| 518 | - ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); |
|
| 519 | - $query->executeStatement(); |
|
| 520 | - } |
|
| 521 | - |
|
| 522 | - return true; |
|
| 523 | - } |
|
| 524 | - |
|
| 525 | - return false; |
|
| 526 | - } |
|
| 527 | - |
|
| 528 | - /** |
|
| 529 | - * restore versions from trash bin |
|
| 530 | - * |
|
| 531 | - * @param View $view file view |
|
| 532 | - * @param string $file complete path to file |
|
| 533 | - * @param string $filename name of file once it was deleted |
|
| 534 | - * @param string $uniqueFilename new file name to restore the file without overwriting existing files |
|
| 535 | - * @param string $location location if file |
|
| 536 | - * @param int $timestamp deletion time |
|
| 537 | - * @return false|null |
|
| 538 | - */ |
|
| 539 | - private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) { |
|
| 540 | - if (\OCP\App::isEnabled('files_versions')) { |
|
| 541 | - $user = User::getUser(); |
|
| 542 | - $rootView = new View('/'); |
|
| 543 | - |
|
| 544 | - $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename); |
|
| 545 | - |
|
| 546 | - [$owner, $ownerPath] = self::getUidAndFilename($target); |
|
| 547 | - |
|
| 548 | - // file has been deleted in between |
|
| 549 | - if (empty($ownerPath)) { |
|
| 550 | - return false; |
|
| 551 | - } |
|
| 552 | - |
|
| 553 | - if ($timestamp) { |
|
| 554 | - $versionedFile = $filename; |
|
| 555 | - } else { |
|
| 556 | - $versionedFile = $file; |
|
| 557 | - } |
|
| 558 | - |
|
| 559 | - if ($view->is_dir('/files_trashbin/versions/' . $file)) { |
|
| 560 | - $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath)); |
|
| 561 | - } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) { |
|
| 562 | - foreach ($versions as $v) { |
|
| 563 | - if ($timestamp) { |
|
| 564 | - $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v); |
|
| 565 | - } else { |
|
| 566 | - $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v); |
|
| 567 | - } |
|
| 568 | - } |
|
| 569 | - } |
|
| 570 | - } |
|
| 571 | - } |
|
| 572 | - |
|
| 573 | - /** |
|
| 574 | - * delete all files from the trash |
|
| 575 | - */ |
|
| 576 | - public static function deleteAll() { |
|
| 577 | - $user = User::getUser(); |
|
| 578 | - $userRoot = \OC::$server->getUserFolder($user)->getParent(); |
|
| 579 | - $view = new View('/' . $user); |
|
| 580 | - $fileInfos = $view->getDirectoryContent('files_trashbin/files'); |
|
| 581 | - |
|
| 582 | - try { |
|
| 583 | - $trash = $userRoot->get('files_trashbin'); |
|
| 584 | - } catch (NotFoundException $e) { |
|
| 585 | - return false; |
|
| 586 | - } |
|
| 587 | - |
|
| 588 | - // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore) |
|
| 589 | - $filePaths = []; |
|
| 590 | - foreach ($fileInfos as $fileInfo) { |
|
| 591 | - $filePaths[] = $view->getRelativePath($fileInfo->getPath()); |
|
| 592 | - } |
|
| 593 | - unset($fileInfos); // save memory |
|
| 594 | - |
|
| 595 | - // Bulk PreDelete-Hook |
|
| 596 | - \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]); |
|
| 597 | - |
|
| 598 | - // Single-File Hooks |
|
| 599 | - foreach ($filePaths as $path) { |
|
| 600 | - self::emitTrashbinPreDelete($path); |
|
| 601 | - } |
|
| 602 | - |
|
| 603 | - // actual file deletion |
|
| 604 | - $trash->delete(); |
|
| 605 | - |
|
| 606 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 607 | - $query->delete('files_trash') |
|
| 608 | - ->where($query->expr()->eq('user', $query->createNamedParameter($user))); |
|
| 609 | - $query->executeStatement(); |
|
| 610 | - |
|
| 611 | - // Bulk PostDelete-Hook |
|
| 612 | - \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]); |
|
| 613 | - |
|
| 614 | - // Single-File Hooks |
|
| 615 | - foreach ($filePaths as $path) { |
|
| 616 | - self::emitTrashbinPostDelete($path); |
|
| 617 | - } |
|
| 618 | - |
|
| 619 | - $trash = $userRoot->newFolder('files_trashbin'); |
|
| 620 | - $trash->newFolder('files'); |
|
| 621 | - |
|
| 622 | - return true; |
|
| 623 | - } |
|
| 624 | - |
|
| 625 | - /** |
|
| 626 | - * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted |
|
| 627 | - * |
|
| 628 | - * @param string $path |
|
| 629 | - */ |
|
| 630 | - protected static function emitTrashbinPreDelete($path) { |
|
| 631 | - \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]); |
|
| 632 | - } |
|
| 633 | - |
|
| 634 | - /** |
|
| 635 | - * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted |
|
| 636 | - * |
|
| 637 | - * @param string $path |
|
| 638 | - */ |
|
| 639 | - protected static function emitTrashbinPostDelete($path) { |
|
| 640 | - \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]); |
|
| 641 | - } |
|
| 642 | - |
|
| 643 | - /** |
|
| 644 | - * delete file from trash bin permanently |
|
| 645 | - * |
|
| 646 | - * @param string $filename path to the file |
|
| 647 | - * @param string $user |
|
| 648 | - * @param int $timestamp of deletion time |
|
| 649 | - * |
|
| 650 | - * @return int size of deleted files |
|
| 651 | - */ |
|
| 652 | - public static function delete($filename, $user, $timestamp = null) { |
|
| 653 | - $userRoot = \OC::$server->getUserFolder($user)->getParent(); |
|
| 654 | - $view = new View('/' . $user); |
|
| 655 | - $size = 0; |
|
| 656 | - |
|
| 657 | - if ($timestamp) { |
|
| 658 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 659 | - $query->delete('files_trash') |
|
| 660 | - ->where($query->expr()->eq('user', $query->createNamedParameter($user))) |
|
| 661 | - ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) |
|
| 662 | - ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); |
|
| 663 | - $query->executeStatement(); |
|
| 664 | - |
|
| 665 | - $file = $filename . '.d' . $timestamp; |
|
| 666 | - } else { |
|
| 667 | - $file = $filename; |
|
| 668 | - } |
|
| 669 | - |
|
| 670 | - $size += self::deleteVersions($view, $file, $filename, $timestamp, $user); |
|
| 671 | - |
|
| 672 | - try { |
|
| 673 | - $node = $userRoot->get('/files_trashbin/files/' . $file); |
|
| 674 | - } catch (NotFoundException $e) { |
|
| 675 | - return $size; |
|
| 676 | - } |
|
| 677 | - |
|
| 678 | - if ($node instanceof Folder) { |
|
| 679 | - $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file)); |
|
| 680 | - } elseif ($node instanceof File) { |
|
| 681 | - $size += $view->filesize('/files_trashbin/files/' . $file); |
|
| 682 | - } |
|
| 683 | - |
|
| 684 | - self::emitTrashbinPreDelete('/files_trashbin/files/' . $file); |
|
| 685 | - $node->delete(); |
|
| 686 | - self::emitTrashbinPostDelete('/files_trashbin/files/' . $file); |
|
| 687 | - |
|
| 688 | - return $size; |
|
| 689 | - } |
|
| 690 | - |
|
| 691 | - /** |
|
| 692 | - * @param View $view |
|
| 693 | - * @param string $file |
|
| 694 | - * @param string $filename |
|
| 695 | - * @param integer|null $timestamp |
|
| 696 | - * @param string $user |
|
| 697 | - * @return int |
|
| 698 | - */ |
|
| 699 | - private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) { |
|
| 700 | - $size = 0; |
|
| 701 | - if (\OCP\App::isEnabled('files_versions')) { |
|
| 702 | - if ($view->is_dir('files_trashbin/versions/' . $file)) { |
|
| 703 | - $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file)); |
|
| 704 | - $view->unlink('files_trashbin/versions/' . $file); |
|
| 705 | - } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) { |
|
| 706 | - foreach ($versions as $v) { |
|
| 707 | - if ($timestamp) { |
|
| 708 | - $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp); |
|
| 709 | - $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp); |
|
| 710 | - } else { |
|
| 711 | - $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v); |
|
| 712 | - $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v); |
|
| 713 | - } |
|
| 714 | - } |
|
| 715 | - } |
|
| 716 | - } |
|
| 717 | - return $size; |
|
| 718 | - } |
|
| 719 | - |
|
| 720 | - /** |
|
| 721 | - * check to see whether a file exists in trashbin |
|
| 722 | - * |
|
| 723 | - * @param string $filename path to the file |
|
| 724 | - * @param int $timestamp of deletion time |
|
| 725 | - * @return bool true if file exists, otherwise false |
|
| 726 | - */ |
|
| 727 | - public static function file_exists($filename, $timestamp = null) { |
|
| 728 | - $user = User::getUser(); |
|
| 729 | - $view = new View('/' . $user); |
|
| 730 | - |
|
| 731 | - if ($timestamp) { |
|
| 732 | - $filename = $filename . '.d' . $timestamp; |
|
| 733 | - } |
|
| 734 | - |
|
| 735 | - $target = Filesystem::normalizePath('files_trashbin/files/' . $filename); |
|
| 736 | - return $view->file_exists($target); |
|
| 737 | - } |
|
| 738 | - |
|
| 739 | - /** |
|
| 740 | - * deletes used space for trash bin in db if user was deleted |
|
| 741 | - * |
|
| 742 | - * @param string $uid id of deleted user |
|
| 743 | - * @return bool result of db delete operation |
|
| 744 | - */ |
|
| 745 | - public static function deleteUser($uid) { |
|
| 746 | - $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 747 | - $query->delete('files_trash') |
|
| 748 | - ->where($query->expr()->eq('user', $query->createNamedParameter($uid))); |
|
| 749 | - return (bool) $query->executeStatement(); |
|
| 750 | - } |
|
| 751 | - |
|
| 752 | - /** |
|
| 753 | - * calculate remaining free space for trash bin |
|
| 754 | - * |
|
| 755 | - * @param integer $trashbinSize current size of the trash bin |
|
| 756 | - * @param string $user |
|
| 757 | - * @return int available free space for trash bin |
|
| 758 | - */ |
|
| 759 | - private static function calculateFreeSpace($trashbinSize, $user) { |
|
| 760 | - $config = \OC::$server->getConfig(); |
|
| 761 | - $userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1'); |
|
| 762 | - if ($userTrashbinSize > -1) { |
|
| 763 | - return $userTrashbinSize - $trashbinSize; |
|
| 764 | - } |
|
| 765 | - $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1'); |
|
| 766 | - if ($systemTrashbinSize > -1) { |
|
| 767 | - return $systemTrashbinSize - $trashbinSize; |
|
| 768 | - } |
|
| 769 | - |
|
| 770 | - $softQuota = true; |
|
| 771 | - $userObject = \OC::$server->getUserManager()->get($user); |
|
| 772 | - if (is_null($userObject)) { |
|
| 773 | - return 0; |
|
| 774 | - } |
|
| 775 | - $quota = $userObject->getQuota(); |
|
| 776 | - if ($quota === null || $quota === 'none') { |
|
| 777 | - $quota = Filesystem::free_space('/'); |
|
| 778 | - $softQuota = false; |
|
| 779 | - // inf or unknown free space |
|
| 780 | - if ($quota < 0) { |
|
| 781 | - $quota = PHP_INT_MAX; |
|
| 782 | - } |
|
| 783 | - } else { |
|
| 784 | - $quota = \OCP\Util::computerFileSize($quota); |
|
| 785 | - } |
|
| 786 | - |
|
| 787 | - // calculate available space for trash bin |
|
| 788 | - // subtract size of files and current trash bin size from quota |
|
| 789 | - if ($softQuota) { |
|
| 790 | - $userFolder = \OC::$server->getUserFolder($user); |
|
| 791 | - if (is_null($userFolder)) { |
|
| 792 | - return 0; |
|
| 793 | - } |
|
| 794 | - $free = $quota - $userFolder->getSize(false); // remaining free space for user |
|
| 795 | - if ($free > 0) { |
|
| 796 | - $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions |
|
| 797 | - } else { |
|
| 798 | - $availableSpace = $free - $trashbinSize; |
|
| 799 | - } |
|
| 800 | - } else { |
|
| 801 | - $availableSpace = $quota; |
|
| 802 | - } |
|
| 803 | - |
|
| 804 | - return $availableSpace; |
|
| 805 | - } |
|
| 806 | - |
|
| 807 | - /** |
|
| 808 | - * resize trash bin if necessary after a new file was added to Nextcloud |
|
| 809 | - * |
|
| 810 | - * @param string $user user id |
|
| 811 | - */ |
|
| 812 | - public static function resizeTrash($user) { |
|
| 813 | - $size = self::getTrashbinSize($user); |
|
| 814 | - |
|
| 815 | - $freeSpace = self::calculateFreeSpace($size, $user); |
|
| 816 | - |
|
| 817 | - if ($freeSpace < 0) { |
|
| 818 | - self::scheduleExpire($user); |
|
| 819 | - } |
|
| 820 | - } |
|
| 821 | - |
|
| 822 | - /** |
|
| 823 | - * clean up the trash bin |
|
| 824 | - * |
|
| 825 | - * @param string $user |
|
| 826 | - */ |
|
| 827 | - public static function expire($user) { |
|
| 828 | - $trashBinSize = self::getTrashbinSize($user); |
|
| 829 | - $availableSpace = self::calculateFreeSpace($trashBinSize, $user); |
|
| 830 | - |
|
| 831 | - $dirContent = Helper::getTrashFiles('/', $user, 'mtime'); |
|
| 832 | - |
|
| 833 | - // delete all files older then $retention_obligation |
|
| 834 | - [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user); |
|
| 835 | - |
|
| 836 | - $availableSpace += $delSize; |
|
| 837 | - |
|
| 838 | - // delete files from trash until we meet the trash bin size limit again |
|
| 839 | - self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace); |
|
| 840 | - } |
|
| 841 | - |
|
| 842 | - /** |
|
| 843 | - * @param string $user |
|
| 844 | - */ |
|
| 845 | - private static function scheduleExpire($user) { |
|
| 846 | - // let the admin disable auto expire |
|
| 847 | - /** @var Application $application */ |
|
| 848 | - $application = \OC::$server->query(Application::class); |
|
| 849 | - $expiration = $application->getContainer()->query('Expiration'); |
|
| 850 | - if ($expiration->isEnabled()) { |
|
| 851 | - \OC::$server->getCommandBus()->push(new Expire($user)); |
|
| 852 | - } |
|
| 853 | - } |
|
| 854 | - |
|
| 855 | - /** |
|
| 856 | - * if the size limit for the trash bin is reached, we delete the oldest |
|
| 857 | - * files in the trash bin until we meet the limit again |
|
| 858 | - * |
|
| 859 | - * @param array $files |
|
| 860 | - * @param string $user |
|
| 861 | - * @param int $availableSpace available disc space |
|
| 862 | - * @return int size of deleted files |
|
| 863 | - */ |
|
| 864 | - protected static function deleteFiles($files, $user, $availableSpace) { |
|
| 865 | - /** @var Application $application */ |
|
| 866 | - $application = \OC::$server->query(Application::class); |
|
| 867 | - $expiration = $application->getContainer()->query('Expiration'); |
|
| 868 | - $size = 0; |
|
| 869 | - |
|
| 870 | - if ($availableSpace < 0) { |
|
| 871 | - foreach ($files as $file) { |
|
| 872 | - if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) { |
|
| 873 | - $tmp = self::delete($file['name'], $user, $file['mtime']); |
|
| 874 | - \OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']); |
|
| 875 | - $availableSpace += $tmp; |
|
| 876 | - $size += $tmp; |
|
| 877 | - } else { |
|
| 878 | - break; |
|
| 879 | - } |
|
| 880 | - } |
|
| 881 | - } |
|
| 882 | - return $size; |
|
| 883 | - } |
|
| 884 | - |
|
| 885 | - /** |
|
| 886 | - * delete files older then max storage time |
|
| 887 | - * |
|
| 888 | - * @param array $files list of files sorted by mtime |
|
| 889 | - * @param string $user |
|
| 890 | - * @return integer[] size of deleted files and number of deleted files |
|
| 891 | - */ |
|
| 892 | - public static function deleteExpiredFiles($files, $user) { |
|
| 893 | - /** @var Expiration $expiration */ |
|
| 894 | - $expiration = \OC::$server->query(Expiration::class); |
|
| 895 | - $size = 0; |
|
| 896 | - $count = 0; |
|
| 897 | - foreach ($files as $file) { |
|
| 898 | - $timestamp = $file['mtime']; |
|
| 899 | - $filename = $file['name']; |
|
| 900 | - if ($expiration->isExpired($timestamp)) { |
|
| 901 | - try { |
|
| 902 | - $size += self::delete($filename, $user, $timestamp); |
|
| 903 | - $count++; |
|
| 904 | - } catch (\OCP\Files\NotPermittedException $e) { |
|
| 905 | - \OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']); |
|
| 906 | - } |
|
| 907 | - \OC::$server->getLogger()->info( |
|
| 908 | - 'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.', |
|
| 909 | - ['app' => 'files_trashbin'] |
|
| 910 | - ); |
|
| 911 | - } else { |
|
| 912 | - break; |
|
| 913 | - } |
|
| 914 | - } |
|
| 915 | - |
|
| 916 | - return [$size, $count]; |
|
| 917 | - } |
|
| 918 | - |
|
| 919 | - /** |
|
| 920 | - * recursive copy to copy a whole directory |
|
| 921 | - * |
|
| 922 | - * @param string $source source path, relative to the users files directory |
|
| 923 | - * @param string $destination destination path relative to the users root directoy |
|
| 924 | - * @param View $view file view for the users root directory |
|
| 925 | - * @return int |
|
| 926 | - * @throws Exceptions\CopyRecursiveException |
|
| 927 | - */ |
|
| 928 | - private static function copy_recursive($source, $destination, View $view) { |
|
| 929 | - $size = 0; |
|
| 930 | - if ($view->is_dir($source)) { |
|
| 931 | - $view->mkdir($destination); |
|
| 932 | - $view->touch($destination, $view->filemtime($source)); |
|
| 933 | - foreach ($view->getDirectoryContent($source) as $i) { |
|
| 934 | - $pathDir = $source . '/' . $i['name']; |
|
| 935 | - if ($view->is_dir($pathDir)) { |
|
| 936 | - $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view); |
|
| 937 | - } else { |
|
| 938 | - $size += $view->filesize($pathDir); |
|
| 939 | - $result = $view->copy($pathDir, $destination . '/' . $i['name']); |
|
| 940 | - if (!$result) { |
|
| 941 | - throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException(); |
|
| 942 | - } |
|
| 943 | - $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir)); |
|
| 944 | - } |
|
| 945 | - } |
|
| 946 | - } else { |
|
| 947 | - $size += $view->filesize($source); |
|
| 948 | - $result = $view->copy($source, $destination); |
|
| 949 | - if (!$result) { |
|
| 950 | - throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException(); |
|
| 951 | - } |
|
| 952 | - $view->touch($destination, $view->filemtime($source)); |
|
| 953 | - } |
|
| 954 | - return $size; |
|
| 955 | - } |
|
| 956 | - |
|
| 957 | - /** |
|
| 958 | - * find all versions which belong to the file we want to restore |
|
| 959 | - * |
|
| 960 | - * @param string $filename name of the file which should be restored |
|
| 961 | - * @param int $timestamp timestamp when the file was deleted |
|
| 962 | - * @return array |
|
| 963 | - */ |
|
| 964 | - private static function getVersionsFromTrash($filename, $timestamp, $user) { |
|
| 965 | - $view = new View('/' . $user . '/files_trashbin/versions'); |
|
| 966 | - $versions = []; |
|
| 967 | - |
|
| 968 | - /** @var \OC\Files\Storage\Storage $storage */ |
|
| 969 | - [$storage,] = $view->resolvePath('/'); |
|
| 970 | - |
|
| 971 | - //force rescan of versions, local storage may not have updated the cache |
|
| 972 | - if (!self::$scannedVersions) { |
|
| 973 | - $storage->getScanner()->scan('files_trashbin/versions'); |
|
| 974 | - self::$scannedVersions = true; |
|
| 975 | - } |
|
| 976 | - |
|
| 977 | - $pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename)); |
|
| 978 | - if ($timestamp) { |
|
| 979 | - // fetch for old versions |
|
| 980 | - $escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp); |
|
| 981 | - $pattern .= '.v%.d' . $escapedTimestamp; |
|
| 982 | - $offset = -strlen($escapedTimestamp) - 2; |
|
| 983 | - } else { |
|
| 984 | - $pattern .= '.v%'; |
|
| 985 | - } |
|
| 986 | - |
|
| 987 | - // Manually fetch all versions from the file cache to be able to filter them by their parent |
|
| 988 | - $cache = $storage->getCache(''); |
|
| 989 | - $query = new CacheQueryBuilder( |
|
| 990 | - \OC::$server->getDatabaseConnection(), |
|
| 991 | - \OC::$server->getSystemConfig(), |
|
| 992 | - \OC::$server->getLogger(), |
|
| 993 | - $cache |
|
| 994 | - ); |
|
| 995 | - $normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/'); |
|
| 996 | - $parentId = $cache->getId($normalizedParentPath); |
|
| 997 | - if ($parentId === -1) { |
|
| 998 | - return []; |
|
| 999 | - } |
|
| 1000 | - |
|
| 1001 | - $query->selectFileCache() |
|
| 1002 | - ->whereStorageId() |
|
| 1003 | - ->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId))) |
|
| 1004 | - ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern))); |
|
| 1005 | - |
|
| 1006 | - $result = $query->executeQuery(); |
|
| 1007 | - $entries = $result->fetchAll(); |
|
| 1008 | - $result->closeCursor(); |
|
| 1009 | - |
|
| 1010 | - /** @var CacheEntry[] $matches */ |
|
| 1011 | - $matches = array_map(function (array $data) { |
|
| 1012 | - return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader()); |
|
| 1013 | - }, $entries); |
|
| 1014 | - |
|
| 1015 | - foreach ($matches as $ma) { |
|
| 1016 | - if ($timestamp) { |
|
| 1017 | - $parts = explode('.v', substr($ma['path'], 0, $offset)); |
|
| 1018 | - $versions[] = end($parts); |
|
| 1019 | - } else { |
|
| 1020 | - $parts = explode('.v', $ma['path']); |
|
| 1021 | - $versions[] = end($parts); |
|
| 1022 | - } |
|
| 1023 | - } |
|
| 1024 | - |
|
| 1025 | - return $versions; |
|
| 1026 | - } |
|
| 1027 | - |
|
| 1028 | - /** |
|
| 1029 | - * find unique extension for restored file if a file with the same name already exists |
|
| 1030 | - * |
|
| 1031 | - * @param string $location where the file should be restored |
|
| 1032 | - * @param string $filename name of the file |
|
| 1033 | - * @param View $view filesystem view relative to users root directory |
|
| 1034 | - * @return string with unique extension |
|
| 1035 | - */ |
|
| 1036 | - private static function getUniqueFilename($location, $filename, View $view) { |
|
| 1037 | - $ext = pathinfo($filename, PATHINFO_EXTENSION); |
|
| 1038 | - $name = pathinfo($filename, PATHINFO_FILENAME); |
|
| 1039 | - $l = \OC::$server->getL10N('files_trashbin'); |
|
| 1040 | - |
|
| 1041 | - $location = '/' . trim($location, '/'); |
|
| 1042 | - |
|
| 1043 | - // if extension is not empty we set a dot in front of it |
|
| 1044 | - if ($ext !== '') { |
|
| 1045 | - $ext = '.' . $ext; |
|
| 1046 | - } |
|
| 1047 | - |
|
| 1048 | - if ($view->file_exists('files' . $location . '/' . $filename)) { |
|
| 1049 | - $i = 2; |
|
| 1050 | - $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext; |
|
| 1051 | - while ($view->file_exists('files' . $location . '/' . $uniqueName)) { |
|
| 1052 | - $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext; |
|
| 1053 | - $i++; |
|
| 1054 | - } |
|
| 1055 | - |
|
| 1056 | - return $uniqueName; |
|
| 1057 | - } |
|
| 1058 | - |
|
| 1059 | - return $filename; |
|
| 1060 | - } |
|
| 1061 | - |
|
| 1062 | - /** |
|
| 1063 | - * get the size from a given root folder |
|
| 1064 | - * |
|
| 1065 | - * @param View $view file view on the root folder |
|
| 1066 | - * @return integer size of the folder |
|
| 1067 | - */ |
|
| 1068 | - private static function calculateSize($view) { |
|
| 1069 | - $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath(''); |
|
| 1070 | - if (!file_exists($root)) { |
|
| 1071 | - return 0; |
|
| 1072 | - } |
|
| 1073 | - $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST); |
|
| 1074 | - $size = 0; |
|
| 1075 | - |
|
| 1076 | - /** |
|
| 1077 | - * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach |
|
| 1078 | - * This bug is fixed in PHP 5.5.9 or before |
|
| 1079 | - * See #8376 |
|
| 1080 | - */ |
|
| 1081 | - $iterator->rewind(); |
|
| 1082 | - while ($iterator->valid()) { |
|
| 1083 | - $path = $iterator->current(); |
|
| 1084 | - $relpath = substr($path, strlen($root) - 1); |
|
| 1085 | - if (!$view->is_dir($relpath)) { |
|
| 1086 | - $size += $view->filesize($relpath); |
|
| 1087 | - } |
|
| 1088 | - $iterator->next(); |
|
| 1089 | - } |
|
| 1090 | - return $size; |
|
| 1091 | - } |
|
| 1092 | - |
|
| 1093 | - /** |
|
| 1094 | - * get current size of trash bin from a given user |
|
| 1095 | - * |
|
| 1096 | - * @param string $user user who owns the trash bin |
|
| 1097 | - * @return integer trash bin size |
|
| 1098 | - */ |
|
| 1099 | - private static function getTrashbinSize($user) { |
|
| 1100 | - $view = new View('/' . $user); |
|
| 1101 | - $fileInfo = $view->getFileInfo('/files_trashbin'); |
|
| 1102 | - return isset($fileInfo['size']) ? $fileInfo['size'] : 0; |
|
| 1103 | - } |
|
| 1104 | - |
|
| 1105 | - /** |
|
| 1106 | - * check if trash bin is empty for a given user |
|
| 1107 | - * |
|
| 1108 | - * @param string $user |
|
| 1109 | - * @return bool |
|
| 1110 | - */ |
|
| 1111 | - public static function isEmpty($user) { |
|
| 1112 | - $view = new View('/' . $user . '/files_trashbin'); |
|
| 1113 | - if ($view->is_dir('/files') && $dh = $view->opendir('/files')) { |
|
| 1114 | - while ($file = readdir($dh)) { |
|
| 1115 | - if (!Filesystem::isIgnoredDir($file)) { |
|
| 1116 | - return false; |
|
| 1117 | - } |
|
| 1118 | - } |
|
| 1119 | - } |
|
| 1120 | - return true; |
|
| 1121 | - } |
|
| 1122 | - |
|
| 1123 | - /** |
|
| 1124 | - * @param $path |
|
| 1125 | - * @return string |
|
| 1126 | - */ |
|
| 1127 | - public static function preview_icon($path) { |
|
| 1128 | - return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]); |
|
| 1129 | - } |
|
| 67 | + // unit: percentage; 50% of available disk space/quota |
|
| 68 | + public const DEFAULTMAXSIZE = 50; |
|
| 69 | + |
|
| 70 | + /** |
|
| 71 | + * Whether versions have already be rescanned during this PHP request |
|
| 72 | + * |
|
| 73 | + * @var bool |
|
| 74 | + */ |
|
| 75 | + private static $scannedVersions = false; |
|
| 76 | + |
|
| 77 | + /** |
|
| 78 | + * Ensure we don't need to scan the file during the move to trash |
|
| 79 | + * by triggering the scan in the pre-hook |
|
| 80 | + * |
|
| 81 | + * @param array $params |
|
| 82 | + */ |
|
| 83 | + public static function ensureFileScannedHook($params) { |
|
| 84 | + try { |
|
| 85 | + self::getUidAndFilename($params['path']); |
|
| 86 | + } catch (NotFoundException $e) { |
|
| 87 | + // nothing to scan for non existing files |
|
| 88 | + } |
|
| 89 | + } |
|
| 90 | + |
|
| 91 | + /** |
|
| 92 | + * get the UID of the owner of the file and the path to the file relative to |
|
| 93 | + * owners files folder |
|
| 94 | + * |
|
| 95 | + * @param string $filename |
|
| 96 | + * @return array |
|
| 97 | + * @throws \OC\User\NoUserException |
|
| 98 | + */ |
|
| 99 | + public static function getUidAndFilename($filename) { |
|
| 100 | + $uid = Filesystem::getOwner($filename); |
|
| 101 | + $userManager = \OC::$server->getUserManager(); |
|
| 102 | + // if the user with the UID doesn't exists, e.g. because the UID points |
|
| 103 | + // to a remote user with a federated cloud ID we use the current logged-in |
|
| 104 | + // user. We need a valid local user to move the file to the right trash bin |
|
| 105 | + if (!$userManager->userExists($uid)) { |
|
| 106 | + $uid = User::getUser(); |
|
| 107 | + } |
|
| 108 | + if (!$uid) { |
|
| 109 | + // no owner, usually because of share link from ext storage |
|
| 110 | + return [null, null]; |
|
| 111 | + } |
|
| 112 | + Filesystem::initMountPoints($uid); |
|
| 113 | + if ($uid !== User::getUser()) { |
|
| 114 | + $info = Filesystem::getFileInfo($filename); |
|
| 115 | + $ownerView = new View('/' . $uid . '/files'); |
|
| 116 | + try { |
|
| 117 | + $filename = $ownerView->getPath($info['fileid']); |
|
| 118 | + } catch (NotFoundException $e) { |
|
| 119 | + $filename = null; |
|
| 120 | + } |
|
| 121 | + } |
|
| 122 | + return [$uid, $filename]; |
|
| 123 | + } |
|
| 124 | + |
|
| 125 | + /** |
|
| 126 | + * get original location of files for user |
|
| 127 | + * |
|
| 128 | + * @param string $user |
|
| 129 | + * @return array (filename => array (timestamp => original location)) |
|
| 130 | + */ |
|
| 131 | + public static function getLocations($user) { |
|
| 132 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 133 | + $query->select('id', 'timestamp', 'location') |
|
| 134 | + ->from('files_trash') |
|
| 135 | + ->where($query->expr()->eq('user', $query->createNamedParameter($user))); |
|
| 136 | + $result = $query->executeQuery(); |
|
| 137 | + $array = []; |
|
| 138 | + while ($row = $result->fetch()) { |
|
| 139 | + if (isset($array[$row['id']])) { |
|
| 140 | + $array[$row['id']][$row['timestamp']] = $row['location']; |
|
| 141 | + } else { |
|
| 142 | + $array[$row['id']] = [$row['timestamp'] => $row['location']]; |
|
| 143 | + } |
|
| 144 | + } |
|
| 145 | + $result->closeCursor(); |
|
| 146 | + return $array; |
|
| 147 | + } |
|
| 148 | + |
|
| 149 | + /** |
|
| 150 | + * get original location of file |
|
| 151 | + * |
|
| 152 | + * @param string $user |
|
| 153 | + * @param string $filename |
|
| 154 | + * @param string $timestamp |
|
| 155 | + * @return string original location |
|
| 156 | + */ |
|
| 157 | + public static function getLocation($user, $filename, $timestamp) { |
|
| 158 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 159 | + $query->select('location') |
|
| 160 | + ->from('files_trash') |
|
| 161 | + ->where($query->expr()->eq('user', $query->createNamedParameter($user))) |
|
| 162 | + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) |
|
| 163 | + ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); |
|
| 164 | + |
|
| 165 | + $result = $query->executeQuery(); |
|
| 166 | + $row = $result->fetch(); |
|
| 167 | + $result->closeCursor(); |
|
| 168 | + |
|
| 169 | + if (isset($row['location'])) { |
|
| 170 | + return $row['location']; |
|
| 171 | + } else { |
|
| 172 | + return false; |
|
| 173 | + } |
|
| 174 | + } |
|
| 175 | + |
|
| 176 | + private static function setUpTrash($user) { |
|
| 177 | + $view = new View('/' . $user); |
|
| 178 | + if (!$view->is_dir('files_trashbin')) { |
|
| 179 | + $view->mkdir('files_trashbin'); |
|
| 180 | + } |
|
| 181 | + if (!$view->is_dir('files_trashbin/files')) { |
|
| 182 | + $view->mkdir('files_trashbin/files'); |
|
| 183 | + } |
|
| 184 | + if (!$view->is_dir('files_trashbin/versions')) { |
|
| 185 | + $view->mkdir('files_trashbin/versions'); |
|
| 186 | + } |
|
| 187 | + if (!$view->is_dir('files_trashbin/keys')) { |
|
| 188 | + $view->mkdir('files_trashbin/keys'); |
|
| 189 | + } |
|
| 190 | + } |
|
| 191 | + |
|
| 192 | + |
|
| 193 | + /** |
|
| 194 | + * copy file to owners trash |
|
| 195 | + * |
|
| 196 | + * @param string $sourcePath |
|
| 197 | + * @param string $owner |
|
| 198 | + * @param string $targetPath |
|
| 199 | + * @param $user |
|
| 200 | + * @param integer $timestamp |
|
| 201 | + */ |
|
| 202 | + private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) { |
|
| 203 | + self::setUpTrash($owner); |
|
| 204 | + |
|
| 205 | + $targetFilename = basename($targetPath); |
|
| 206 | + $targetLocation = dirname($targetPath); |
|
| 207 | + |
|
| 208 | + $sourceFilename = basename($sourcePath); |
|
| 209 | + |
|
| 210 | + $view = new View('/'); |
|
| 211 | + |
|
| 212 | + $target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp; |
|
| 213 | + $source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp; |
|
| 214 | + $free = $view->free_space($target); |
|
| 215 | + $isUnknownOrUnlimitedFreeSpace = $free < 0; |
|
| 216 | + $isEnoughFreeSpaceLeft = $view->filesize($source) < $free; |
|
| 217 | + if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) { |
|
| 218 | + self::copy_recursive($source, $target, $view); |
|
| 219 | + } |
|
| 220 | + |
|
| 221 | + |
|
| 222 | + if ($view->file_exists($target)) { |
|
| 223 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 224 | + $query->insert('files_trash') |
|
| 225 | + ->setValue('id', $query->createNamedParameter($targetFilename)) |
|
| 226 | + ->setValue('timestamp', $query->createNamedParameter($timestamp)) |
|
| 227 | + ->setValue('location', $query->createNamedParameter($targetLocation)) |
|
| 228 | + ->setValue('user', $query->createNamedParameter($user)); |
|
| 229 | + $result = $query->executeStatement(); |
|
| 230 | + if (!$result) { |
|
| 231 | + \OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']); |
|
| 232 | + } |
|
| 233 | + } |
|
| 234 | + } |
|
| 235 | + |
|
| 236 | + |
|
| 237 | + /** |
|
| 238 | + * move file to the trash bin |
|
| 239 | + * |
|
| 240 | + * @param string $file_path path to the deleted file/directory relative to the files root directory |
|
| 241 | + * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder) |
|
| 242 | + * |
|
| 243 | + * @return bool |
|
| 244 | + */ |
|
| 245 | + public static function move2trash($file_path, $ownerOnly = false) { |
|
| 246 | + // get the user for which the filesystem is setup |
|
| 247 | + $root = Filesystem::getRoot(); |
|
| 248 | + [, $user] = explode('/', $root); |
|
| 249 | + [$owner, $ownerPath] = self::getUidAndFilename($file_path); |
|
| 250 | + |
|
| 251 | + // if no owner found (ex: ext storage + share link), will use the current user's trashbin then |
|
| 252 | + if (is_null($owner)) { |
|
| 253 | + $owner = $user; |
|
| 254 | + $ownerPath = $file_path; |
|
| 255 | + } |
|
| 256 | + |
|
| 257 | + $ownerView = new View('/' . $owner); |
|
| 258 | + // file has been deleted in between |
|
| 259 | + if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) { |
|
| 260 | + return true; |
|
| 261 | + } |
|
| 262 | + |
|
| 263 | + self::setUpTrash($user); |
|
| 264 | + if ($owner !== $user) { |
|
| 265 | + // also setup for owner |
|
| 266 | + self::setUpTrash($owner); |
|
| 267 | + } |
|
| 268 | + |
|
| 269 | + $path_parts = pathinfo($ownerPath); |
|
| 270 | + |
|
| 271 | + $filename = $path_parts['basename']; |
|
| 272 | + $location = $path_parts['dirname']; |
|
| 273 | + /** @var ITimeFactory $timeFactory */ |
|
| 274 | + $timeFactory = \OC::$server->query(ITimeFactory::class); |
|
| 275 | + $timestamp = $timeFactory->getTime(); |
|
| 276 | + |
|
| 277 | + $lockingProvider = \OC::$server->getLockingProvider(); |
|
| 278 | + |
|
| 279 | + // disable proxy to prevent recursive calls |
|
| 280 | + $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp; |
|
| 281 | + $gotLock = false; |
|
| 282 | + |
|
| 283 | + while (!$gotLock) { |
|
| 284 | + try { |
|
| 285 | + /** @var \OC\Files\Storage\Storage $trashStorage */ |
|
| 286 | + [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath); |
|
| 287 | + |
|
| 288 | + $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); |
|
| 289 | + $gotLock = true; |
|
| 290 | + } catch (LockedException $e) { |
|
| 291 | + // a file with the same name is being deleted concurrently |
|
| 292 | + // nudge the timestamp a bit to resolve the conflict |
|
| 293 | + |
|
| 294 | + $timestamp = $timestamp + 1; |
|
| 295 | + |
|
| 296 | + $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp; |
|
| 297 | + } |
|
| 298 | + } |
|
| 299 | + |
|
| 300 | + /** @var \OC\Files\Storage\Storage $sourceStorage */ |
|
| 301 | + [$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath); |
|
| 302 | + |
|
| 303 | + |
|
| 304 | + if ($trashStorage->file_exists($trashInternalPath)) { |
|
| 305 | + $trashStorage->unlink($trashInternalPath); |
|
| 306 | + } |
|
| 307 | + |
|
| 308 | + $config = \OC::$server->getConfig(); |
|
| 309 | + $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1'); |
|
| 310 | + $userTrashbinSize = (int)$config->getUserValue($owner, 'files_trashbin', 'trashbin_size', '-1'); |
|
| 311 | + $configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize; |
|
| 312 | + if ($configuredTrashbinSize >= 0 && $sourceStorage->filesize($sourceInternalPath) >= $configuredTrashbinSize) { |
|
| 313 | + return false; |
|
| 314 | + } |
|
| 315 | + |
|
| 316 | + $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath); |
|
| 317 | + |
|
| 318 | + try { |
|
| 319 | + $moveSuccessful = true; |
|
| 320 | + |
|
| 321 | + // when moving within the same object store, the cache update done above is enough to move the file |
|
| 322 | + if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) { |
|
| 323 | + $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath); |
|
| 324 | + } |
|
| 325 | + } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) { |
|
| 326 | + $moveSuccessful = false; |
|
| 327 | + if ($trashStorage->file_exists($trashInternalPath)) { |
|
| 328 | + $trashStorage->unlink($trashInternalPath); |
|
| 329 | + } |
|
| 330 | + \OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']); |
|
| 331 | + } |
|
| 332 | + |
|
| 333 | + if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort |
|
| 334 | + if ($sourceStorage->is_dir($sourceInternalPath)) { |
|
| 335 | + $sourceStorage->rmdir($sourceInternalPath); |
|
| 336 | + } else { |
|
| 337 | + $sourceStorage->unlink($sourceInternalPath); |
|
| 338 | + } |
|
| 339 | + |
|
| 340 | + if ($sourceStorage->file_exists($sourceInternalPath)) { |
|
| 341 | + // undo the cache move |
|
| 342 | + $sourceStorage->getUpdater()->renameFromStorage($trashStorage, $trashInternalPath, $sourceInternalPath); |
|
| 343 | + } else { |
|
| 344 | + $trashStorage->getUpdater()->remove($trashInternalPath); |
|
| 345 | + } |
|
| 346 | + return false; |
|
| 347 | + } |
|
| 348 | + |
|
| 349 | + if ($moveSuccessful) { |
|
| 350 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 351 | + $query->insert('files_trash') |
|
| 352 | + ->setValue('id', $query->createNamedParameter($filename)) |
|
| 353 | + ->setValue('timestamp', $query->createNamedParameter($timestamp)) |
|
| 354 | + ->setValue('location', $query->createNamedParameter($location)) |
|
| 355 | + ->setValue('user', $query->createNamedParameter($owner)); |
|
| 356 | + $result = $query->executeStatement(); |
|
| 357 | + if (!$result) { |
|
| 358 | + \OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']); |
|
| 359 | + } |
|
| 360 | + \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path), |
|
| 361 | + 'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]); |
|
| 362 | + |
|
| 363 | + self::retainVersions($filename, $owner, $ownerPath, $timestamp); |
|
| 364 | + |
|
| 365 | + // if owner !== user we need to also add a copy to the users trash |
|
| 366 | + if ($user !== $owner && $ownerOnly === false) { |
|
| 367 | + self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp); |
|
| 368 | + } |
|
| 369 | + } |
|
| 370 | + |
|
| 371 | + $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider); |
|
| 372 | + |
|
| 373 | + self::scheduleExpire($user); |
|
| 374 | + |
|
| 375 | + // if owner !== user we also need to update the owners trash size |
|
| 376 | + if ($owner !== $user) { |
|
| 377 | + self::scheduleExpire($owner); |
|
| 378 | + } |
|
| 379 | + |
|
| 380 | + return $moveSuccessful; |
|
| 381 | + } |
|
| 382 | + |
|
| 383 | + /** |
|
| 384 | + * Move file versions to trash so that they can be restored later |
|
| 385 | + * |
|
| 386 | + * @param string $filename of deleted file |
|
| 387 | + * @param string $owner owner user id |
|
| 388 | + * @param string $ownerPath path relative to the owner's home storage |
|
| 389 | + * @param integer $timestamp when the file was deleted |
|
| 390 | + */ |
|
| 391 | + private static function retainVersions($filename, $owner, $ownerPath, $timestamp) { |
|
| 392 | + if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) { |
|
| 393 | + $user = User::getUser(); |
|
| 394 | + $rootView = new View('/'); |
|
| 395 | + |
|
| 396 | + if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) { |
|
| 397 | + if ($owner !== $user) { |
|
| 398 | + self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView); |
|
| 399 | + } |
|
| 400 | + self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp); |
|
| 401 | + } elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) { |
|
| 402 | + foreach ($versions as $v) { |
|
| 403 | + if ($owner !== $user) { |
|
| 404 | + self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp); |
|
| 405 | + } |
|
| 406 | + self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp); |
|
| 407 | + } |
|
| 408 | + } |
|
| 409 | + } |
|
| 410 | + } |
|
| 411 | + |
|
| 412 | + /** |
|
| 413 | + * Move a file or folder on storage level |
|
| 414 | + * |
|
| 415 | + * @param View $view |
|
| 416 | + * @param string $source |
|
| 417 | + * @param string $target |
|
| 418 | + * @return bool |
|
| 419 | + */ |
|
| 420 | + private static function move(View $view, $source, $target) { |
|
| 421 | + /** @var \OC\Files\Storage\Storage $sourceStorage */ |
|
| 422 | + [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source); |
|
| 423 | + /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
| 424 | + [$targetStorage, $targetInternalPath] = $view->resolvePath($target); |
|
| 425 | + /** @var \OC\Files\Storage\Storage $ownerTrashStorage */ |
|
| 426 | + |
|
| 427 | + $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 428 | + if ($result) { |
|
| 429 | + $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 430 | + } |
|
| 431 | + return $result; |
|
| 432 | + } |
|
| 433 | + |
|
| 434 | + /** |
|
| 435 | + * Copy a file or folder on storage level |
|
| 436 | + * |
|
| 437 | + * @param View $view |
|
| 438 | + * @param string $source |
|
| 439 | + * @param string $target |
|
| 440 | + * @return bool |
|
| 441 | + */ |
|
| 442 | + private static function copy(View $view, $source, $target) { |
|
| 443 | + /** @var \OC\Files\Storage\Storage $sourceStorage */ |
|
| 444 | + [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source); |
|
| 445 | + /** @var \OC\Files\Storage\Storage $targetStorage */ |
|
| 446 | + [$targetStorage, $targetInternalPath] = $view->resolvePath($target); |
|
| 447 | + /** @var \OC\Files\Storage\Storage $ownerTrashStorage */ |
|
| 448 | + |
|
| 449 | + $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); |
|
| 450 | + if ($result) { |
|
| 451 | + $targetStorage->getUpdater()->update($targetInternalPath); |
|
| 452 | + } |
|
| 453 | + return $result; |
|
| 454 | + } |
|
| 455 | + |
|
| 456 | + /** |
|
| 457 | + * Restore a file or folder from trash bin |
|
| 458 | + * |
|
| 459 | + * @param string $file path to the deleted file/folder relative to "files_trashbin/files/", |
|
| 460 | + * including the timestamp suffix ".d12345678" |
|
| 461 | + * @param string $filename name of the file/folder |
|
| 462 | + * @param int $timestamp time when the file/folder was deleted |
|
| 463 | + * |
|
| 464 | + * @return bool true on success, false otherwise |
|
| 465 | + */ |
|
| 466 | + public static function restore($file, $filename, $timestamp) { |
|
| 467 | + $user = User::getUser(); |
|
| 468 | + $view = new View('/' . $user); |
|
| 469 | + |
|
| 470 | + $location = ''; |
|
| 471 | + if ($timestamp) { |
|
| 472 | + $location = self::getLocation($user, $filename, $timestamp); |
|
| 473 | + if ($location === false) { |
|
| 474 | + \OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']); |
|
| 475 | + } else { |
|
| 476 | + // if location no longer exists, restore file in the root directory |
|
| 477 | + if ($location !== '/' && |
|
| 478 | + (!$view->is_dir('files/' . $location) || |
|
| 479 | + !$view->isCreatable('files/' . $location)) |
|
| 480 | + ) { |
|
| 481 | + $location = ''; |
|
| 482 | + } |
|
| 483 | + } |
|
| 484 | + } |
|
| 485 | + |
|
| 486 | + // we need a extension in case a file/dir with the same name already exists |
|
| 487 | + $uniqueFilename = self::getUniqueFilename($location, $filename, $view); |
|
| 488 | + |
|
| 489 | + $source = Filesystem::normalizePath('files_trashbin/files/' . $file); |
|
| 490 | + $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename); |
|
| 491 | + if (!$view->file_exists($source)) { |
|
| 492 | + return false; |
|
| 493 | + } |
|
| 494 | + $mtime = $view->filemtime($source); |
|
| 495 | + |
|
| 496 | + // restore file |
|
| 497 | + if (!$view->isCreatable(dirname($target))) { |
|
| 498 | + throw new NotPermittedException("Can't restore trash item because the target folder is not writable"); |
|
| 499 | + } |
|
| 500 | + $restoreResult = $view->rename($source, $target); |
|
| 501 | + |
|
| 502 | + // handle the restore result |
|
| 503 | + if ($restoreResult) { |
|
| 504 | + $fakeRoot = $view->getRoot(); |
|
| 505 | + $view->chroot('/' . $user . '/files'); |
|
| 506 | + $view->touch('/' . $location . '/' . $uniqueFilename, $mtime); |
|
| 507 | + $view->chroot($fakeRoot); |
|
| 508 | + \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename), |
|
| 509 | + 'trashPath' => Filesystem::normalizePath($file)]); |
|
| 510 | + |
|
| 511 | + self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp); |
|
| 512 | + |
|
| 513 | + if ($timestamp) { |
|
| 514 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 515 | + $query->delete('files_trash') |
|
| 516 | + ->where($query->expr()->eq('user', $query->createNamedParameter($user))) |
|
| 517 | + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) |
|
| 518 | + ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); |
|
| 519 | + $query->executeStatement(); |
|
| 520 | + } |
|
| 521 | + |
|
| 522 | + return true; |
|
| 523 | + } |
|
| 524 | + |
|
| 525 | + return false; |
|
| 526 | + } |
|
| 527 | + |
|
| 528 | + /** |
|
| 529 | + * restore versions from trash bin |
|
| 530 | + * |
|
| 531 | + * @param View $view file view |
|
| 532 | + * @param string $file complete path to file |
|
| 533 | + * @param string $filename name of file once it was deleted |
|
| 534 | + * @param string $uniqueFilename new file name to restore the file without overwriting existing files |
|
| 535 | + * @param string $location location if file |
|
| 536 | + * @param int $timestamp deletion time |
|
| 537 | + * @return false|null |
|
| 538 | + */ |
|
| 539 | + private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) { |
|
| 540 | + if (\OCP\App::isEnabled('files_versions')) { |
|
| 541 | + $user = User::getUser(); |
|
| 542 | + $rootView = new View('/'); |
|
| 543 | + |
|
| 544 | + $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename); |
|
| 545 | + |
|
| 546 | + [$owner, $ownerPath] = self::getUidAndFilename($target); |
|
| 547 | + |
|
| 548 | + // file has been deleted in between |
|
| 549 | + if (empty($ownerPath)) { |
|
| 550 | + return false; |
|
| 551 | + } |
|
| 552 | + |
|
| 553 | + if ($timestamp) { |
|
| 554 | + $versionedFile = $filename; |
|
| 555 | + } else { |
|
| 556 | + $versionedFile = $file; |
|
| 557 | + } |
|
| 558 | + |
|
| 559 | + if ($view->is_dir('/files_trashbin/versions/' . $file)) { |
|
| 560 | + $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath)); |
|
| 561 | + } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) { |
|
| 562 | + foreach ($versions as $v) { |
|
| 563 | + if ($timestamp) { |
|
| 564 | + $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v); |
|
| 565 | + } else { |
|
| 566 | + $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v); |
|
| 567 | + } |
|
| 568 | + } |
|
| 569 | + } |
|
| 570 | + } |
|
| 571 | + } |
|
| 572 | + |
|
| 573 | + /** |
|
| 574 | + * delete all files from the trash |
|
| 575 | + */ |
|
| 576 | + public static function deleteAll() { |
|
| 577 | + $user = User::getUser(); |
|
| 578 | + $userRoot = \OC::$server->getUserFolder($user)->getParent(); |
|
| 579 | + $view = new View('/' . $user); |
|
| 580 | + $fileInfos = $view->getDirectoryContent('files_trashbin/files'); |
|
| 581 | + |
|
| 582 | + try { |
|
| 583 | + $trash = $userRoot->get('files_trashbin'); |
|
| 584 | + } catch (NotFoundException $e) { |
|
| 585 | + return false; |
|
| 586 | + } |
|
| 587 | + |
|
| 588 | + // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore) |
|
| 589 | + $filePaths = []; |
|
| 590 | + foreach ($fileInfos as $fileInfo) { |
|
| 591 | + $filePaths[] = $view->getRelativePath($fileInfo->getPath()); |
|
| 592 | + } |
|
| 593 | + unset($fileInfos); // save memory |
|
| 594 | + |
|
| 595 | + // Bulk PreDelete-Hook |
|
| 596 | + \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]); |
|
| 597 | + |
|
| 598 | + // Single-File Hooks |
|
| 599 | + foreach ($filePaths as $path) { |
|
| 600 | + self::emitTrashbinPreDelete($path); |
|
| 601 | + } |
|
| 602 | + |
|
| 603 | + // actual file deletion |
|
| 604 | + $trash->delete(); |
|
| 605 | + |
|
| 606 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 607 | + $query->delete('files_trash') |
|
| 608 | + ->where($query->expr()->eq('user', $query->createNamedParameter($user))); |
|
| 609 | + $query->executeStatement(); |
|
| 610 | + |
|
| 611 | + // Bulk PostDelete-Hook |
|
| 612 | + \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]); |
|
| 613 | + |
|
| 614 | + // Single-File Hooks |
|
| 615 | + foreach ($filePaths as $path) { |
|
| 616 | + self::emitTrashbinPostDelete($path); |
|
| 617 | + } |
|
| 618 | + |
|
| 619 | + $trash = $userRoot->newFolder('files_trashbin'); |
|
| 620 | + $trash->newFolder('files'); |
|
| 621 | + |
|
| 622 | + return true; |
|
| 623 | + } |
|
| 624 | + |
|
| 625 | + /** |
|
| 626 | + * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted |
|
| 627 | + * |
|
| 628 | + * @param string $path |
|
| 629 | + */ |
|
| 630 | + protected static function emitTrashbinPreDelete($path) { |
|
| 631 | + \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]); |
|
| 632 | + } |
|
| 633 | + |
|
| 634 | + /** |
|
| 635 | + * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted |
|
| 636 | + * |
|
| 637 | + * @param string $path |
|
| 638 | + */ |
|
| 639 | + protected static function emitTrashbinPostDelete($path) { |
|
| 640 | + \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]); |
|
| 641 | + } |
|
| 642 | + |
|
| 643 | + /** |
|
| 644 | + * delete file from trash bin permanently |
|
| 645 | + * |
|
| 646 | + * @param string $filename path to the file |
|
| 647 | + * @param string $user |
|
| 648 | + * @param int $timestamp of deletion time |
|
| 649 | + * |
|
| 650 | + * @return int size of deleted files |
|
| 651 | + */ |
|
| 652 | + public static function delete($filename, $user, $timestamp = null) { |
|
| 653 | + $userRoot = \OC::$server->getUserFolder($user)->getParent(); |
|
| 654 | + $view = new View('/' . $user); |
|
| 655 | + $size = 0; |
|
| 656 | + |
|
| 657 | + if ($timestamp) { |
|
| 658 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 659 | + $query->delete('files_trash') |
|
| 660 | + ->where($query->expr()->eq('user', $query->createNamedParameter($user))) |
|
| 661 | + ->andWhere($query->expr()->eq('id', $query->createNamedParameter($filename))) |
|
| 662 | + ->andWhere($query->expr()->eq('timestamp', $query->createNamedParameter($timestamp))); |
|
| 663 | + $query->executeStatement(); |
|
| 664 | + |
|
| 665 | + $file = $filename . '.d' . $timestamp; |
|
| 666 | + } else { |
|
| 667 | + $file = $filename; |
|
| 668 | + } |
|
| 669 | + |
|
| 670 | + $size += self::deleteVersions($view, $file, $filename, $timestamp, $user); |
|
| 671 | + |
|
| 672 | + try { |
|
| 673 | + $node = $userRoot->get('/files_trashbin/files/' . $file); |
|
| 674 | + } catch (NotFoundException $e) { |
|
| 675 | + return $size; |
|
| 676 | + } |
|
| 677 | + |
|
| 678 | + if ($node instanceof Folder) { |
|
| 679 | + $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file)); |
|
| 680 | + } elseif ($node instanceof File) { |
|
| 681 | + $size += $view->filesize('/files_trashbin/files/' . $file); |
|
| 682 | + } |
|
| 683 | + |
|
| 684 | + self::emitTrashbinPreDelete('/files_trashbin/files/' . $file); |
|
| 685 | + $node->delete(); |
|
| 686 | + self::emitTrashbinPostDelete('/files_trashbin/files/' . $file); |
|
| 687 | + |
|
| 688 | + return $size; |
|
| 689 | + } |
|
| 690 | + |
|
| 691 | + /** |
|
| 692 | + * @param View $view |
|
| 693 | + * @param string $file |
|
| 694 | + * @param string $filename |
|
| 695 | + * @param integer|null $timestamp |
|
| 696 | + * @param string $user |
|
| 697 | + * @return int |
|
| 698 | + */ |
|
| 699 | + private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) { |
|
| 700 | + $size = 0; |
|
| 701 | + if (\OCP\App::isEnabled('files_versions')) { |
|
| 702 | + if ($view->is_dir('files_trashbin/versions/' . $file)) { |
|
| 703 | + $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file)); |
|
| 704 | + $view->unlink('files_trashbin/versions/' . $file); |
|
| 705 | + } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) { |
|
| 706 | + foreach ($versions as $v) { |
|
| 707 | + if ($timestamp) { |
|
| 708 | + $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp); |
|
| 709 | + $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp); |
|
| 710 | + } else { |
|
| 711 | + $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v); |
|
| 712 | + $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v); |
|
| 713 | + } |
|
| 714 | + } |
|
| 715 | + } |
|
| 716 | + } |
|
| 717 | + return $size; |
|
| 718 | + } |
|
| 719 | + |
|
| 720 | + /** |
|
| 721 | + * check to see whether a file exists in trashbin |
|
| 722 | + * |
|
| 723 | + * @param string $filename path to the file |
|
| 724 | + * @param int $timestamp of deletion time |
|
| 725 | + * @return bool true if file exists, otherwise false |
|
| 726 | + */ |
|
| 727 | + public static function file_exists($filename, $timestamp = null) { |
|
| 728 | + $user = User::getUser(); |
|
| 729 | + $view = new View('/' . $user); |
|
| 730 | + |
|
| 731 | + if ($timestamp) { |
|
| 732 | + $filename = $filename . '.d' . $timestamp; |
|
| 733 | + } |
|
| 734 | + |
|
| 735 | + $target = Filesystem::normalizePath('files_trashbin/files/' . $filename); |
|
| 736 | + return $view->file_exists($target); |
|
| 737 | + } |
|
| 738 | + |
|
| 739 | + /** |
|
| 740 | + * deletes used space for trash bin in db if user was deleted |
|
| 741 | + * |
|
| 742 | + * @param string $uid id of deleted user |
|
| 743 | + * @return bool result of db delete operation |
|
| 744 | + */ |
|
| 745 | + public static function deleteUser($uid) { |
|
| 746 | + $query = \OC::$server->getDatabaseConnection()->getQueryBuilder(); |
|
| 747 | + $query->delete('files_trash') |
|
| 748 | + ->where($query->expr()->eq('user', $query->createNamedParameter($uid))); |
|
| 749 | + return (bool) $query->executeStatement(); |
|
| 750 | + } |
|
| 751 | + |
|
| 752 | + /** |
|
| 753 | + * calculate remaining free space for trash bin |
|
| 754 | + * |
|
| 755 | + * @param integer $trashbinSize current size of the trash bin |
|
| 756 | + * @param string $user |
|
| 757 | + * @return int available free space for trash bin |
|
| 758 | + */ |
|
| 759 | + private static function calculateFreeSpace($trashbinSize, $user) { |
|
| 760 | + $config = \OC::$server->getConfig(); |
|
| 761 | + $userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1'); |
|
| 762 | + if ($userTrashbinSize > -1) { |
|
| 763 | + return $userTrashbinSize - $trashbinSize; |
|
| 764 | + } |
|
| 765 | + $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1'); |
|
| 766 | + if ($systemTrashbinSize > -1) { |
|
| 767 | + return $systemTrashbinSize - $trashbinSize; |
|
| 768 | + } |
|
| 769 | + |
|
| 770 | + $softQuota = true; |
|
| 771 | + $userObject = \OC::$server->getUserManager()->get($user); |
|
| 772 | + if (is_null($userObject)) { |
|
| 773 | + return 0; |
|
| 774 | + } |
|
| 775 | + $quota = $userObject->getQuota(); |
|
| 776 | + if ($quota === null || $quota === 'none') { |
|
| 777 | + $quota = Filesystem::free_space('/'); |
|
| 778 | + $softQuota = false; |
|
| 779 | + // inf or unknown free space |
|
| 780 | + if ($quota < 0) { |
|
| 781 | + $quota = PHP_INT_MAX; |
|
| 782 | + } |
|
| 783 | + } else { |
|
| 784 | + $quota = \OCP\Util::computerFileSize($quota); |
|
| 785 | + } |
|
| 786 | + |
|
| 787 | + // calculate available space for trash bin |
|
| 788 | + // subtract size of files and current trash bin size from quota |
|
| 789 | + if ($softQuota) { |
|
| 790 | + $userFolder = \OC::$server->getUserFolder($user); |
|
| 791 | + if (is_null($userFolder)) { |
|
| 792 | + return 0; |
|
| 793 | + } |
|
| 794 | + $free = $quota - $userFolder->getSize(false); // remaining free space for user |
|
| 795 | + if ($free > 0) { |
|
| 796 | + $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions |
|
| 797 | + } else { |
|
| 798 | + $availableSpace = $free - $trashbinSize; |
|
| 799 | + } |
|
| 800 | + } else { |
|
| 801 | + $availableSpace = $quota; |
|
| 802 | + } |
|
| 803 | + |
|
| 804 | + return $availableSpace; |
|
| 805 | + } |
|
| 806 | + |
|
| 807 | + /** |
|
| 808 | + * resize trash bin if necessary after a new file was added to Nextcloud |
|
| 809 | + * |
|
| 810 | + * @param string $user user id |
|
| 811 | + */ |
|
| 812 | + public static function resizeTrash($user) { |
|
| 813 | + $size = self::getTrashbinSize($user); |
|
| 814 | + |
|
| 815 | + $freeSpace = self::calculateFreeSpace($size, $user); |
|
| 816 | + |
|
| 817 | + if ($freeSpace < 0) { |
|
| 818 | + self::scheduleExpire($user); |
|
| 819 | + } |
|
| 820 | + } |
|
| 821 | + |
|
| 822 | + /** |
|
| 823 | + * clean up the trash bin |
|
| 824 | + * |
|
| 825 | + * @param string $user |
|
| 826 | + */ |
|
| 827 | + public static function expire($user) { |
|
| 828 | + $trashBinSize = self::getTrashbinSize($user); |
|
| 829 | + $availableSpace = self::calculateFreeSpace($trashBinSize, $user); |
|
| 830 | + |
|
| 831 | + $dirContent = Helper::getTrashFiles('/', $user, 'mtime'); |
|
| 832 | + |
|
| 833 | + // delete all files older then $retention_obligation |
|
| 834 | + [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user); |
|
| 835 | + |
|
| 836 | + $availableSpace += $delSize; |
|
| 837 | + |
|
| 838 | + // delete files from trash until we meet the trash bin size limit again |
|
| 839 | + self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace); |
|
| 840 | + } |
|
| 841 | + |
|
| 842 | + /** |
|
| 843 | + * @param string $user |
|
| 844 | + */ |
|
| 845 | + private static function scheduleExpire($user) { |
|
| 846 | + // let the admin disable auto expire |
|
| 847 | + /** @var Application $application */ |
|
| 848 | + $application = \OC::$server->query(Application::class); |
|
| 849 | + $expiration = $application->getContainer()->query('Expiration'); |
|
| 850 | + if ($expiration->isEnabled()) { |
|
| 851 | + \OC::$server->getCommandBus()->push(new Expire($user)); |
|
| 852 | + } |
|
| 853 | + } |
|
| 854 | + |
|
| 855 | + /** |
|
| 856 | + * if the size limit for the trash bin is reached, we delete the oldest |
|
| 857 | + * files in the trash bin until we meet the limit again |
|
| 858 | + * |
|
| 859 | + * @param array $files |
|
| 860 | + * @param string $user |
|
| 861 | + * @param int $availableSpace available disc space |
|
| 862 | + * @return int size of deleted files |
|
| 863 | + */ |
|
| 864 | + protected static function deleteFiles($files, $user, $availableSpace) { |
|
| 865 | + /** @var Application $application */ |
|
| 866 | + $application = \OC::$server->query(Application::class); |
|
| 867 | + $expiration = $application->getContainer()->query('Expiration'); |
|
| 868 | + $size = 0; |
|
| 869 | + |
|
| 870 | + if ($availableSpace < 0) { |
|
| 871 | + foreach ($files as $file) { |
|
| 872 | + if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) { |
|
| 873 | + $tmp = self::delete($file['name'], $user, $file['mtime']); |
|
| 874 | + \OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']); |
|
| 875 | + $availableSpace += $tmp; |
|
| 876 | + $size += $tmp; |
|
| 877 | + } else { |
|
| 878 | + break; |
|
| 879 | + } |
|
| 880 | + } |
|
| 881 | + } |
|
| 882 | + return $size; |
|
| 883 | + } |
|
| 884 | + |
|
| 885 | + /** |
|
| 886 | + * delete files older then max storage time |
|
| 887 | + * |
|
| 888 | + * @param array $files list of files sorted by mtime |
|
| 889 | + * @param string $user |
|
| 890 | + * @return integer[] size of deleted files and number of deleted files |
|
| 891 | + */ |
|
| 892 | + public static function deleteExpiredFiles($files, $user) { |
|
| 893 | + /** @var Expiration $expiration */ |
|
| 894 | + $expiration = \OC::$server->query(Expiration::class); |
|
| 895 | + $size = 0; |
|
| 896 | + $count = 0; |
|
| 897 | + foreach ($files as $file) { |
|
| 898 | + $timestamp = $file['mtime']; |
|
| 899 | + $filename = $file['name']; |
|
| 900 | + if ($expiration->isExpired($timestamp)) { |
|
| 901 | + try { |
|
| 902 | + $size += self::delete($filename, $user, $timestamp); |
|
| 903 | + $count++; |
|
| 904 | + } catch (\OCP\Files\NotPermittedException $e) { |
|
| 905 | + \OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']); |
|
| 906 | + } |
|
| 907 | + \OC::$server->getLogger()->info( |
|
| 908 | + 'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.', |
|
| 909 | + ['app' => 'files_trashbin'] |
|
| 910 | + ); |
|
| 911 | + } else { |
|
| 912 | + break; |
|
| 913 | + } |
|
| 914 | + } |
|
| 915 | + |
|
| 916 | + return [$size, $count]; |
|
| 917 | + } |
|
| 918 | + |
|
| 919 | + /** |
|
| 920 | + * recursive copy to copy a whole directory |
|
| 921 | + * |
|
| 922 | + * @param string $source source path, relative to the users files directory |
|
| 923 | + * @param string $destination destination path relative to the users root directoy |
|
| 924 | + * @param View $view file view for the users root directory |
|
| 925 | + * @return int |
|
| 926 | + * @throws Exceptions\CopyRecursiveException |
|
| 927 | + */ |
|
| 928 | + private static function copy_recursive($source, $destination, View $view) { |
|
| 929 | + $size = 0; |
|
| 930 | + if ($view->is_dir($source)) { |
|
| 931 | + $view->mkdir($destination); |
|
| 932 | + $view->touch($destination, $view->filemtime($source)); |
|
| 933 | + foreach ($view->getDirectoryContent($source) as $i) { |
|
| 934 | + $pathDir = $source . '/' . $i['name']; |
|
| 935 | + if ($view->is_dir($pathDir)) { |
|
| 936 | + $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view); |
|
| 937 | + } else { |
|
| 938 | + $size += $view->filesize($pathDir); |
|
| 939 | + $result = $view->copy($pathDir, $destination . '/' . $i['name']); |
|
| 940 | + if (!$result) { |
|
| 941 | + throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException(); |
|
| 942 | + } |
|
| 943 | + $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir)); |
|
| 944 | + } |
|
| 945 | + } |
|
| 946 | + } else { |
|
| 947 | + $size += $view->filesize($source); |
|
| 948 | + $result = $view->copy($source, $destination); |
|
| 949 | + if (!$result) { |
|
| 950 | + throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException(); |
|
| 951 | + } |
|
| 952 | + $view->touch($destination, $view->filemtime($source)); |
|
| 953 | + } |
|
| 954 | + return $size; |
|
| 955 | + } |
|
| 956 | + |
|
| 957 | + /** |
|
| 958 | + * find all versions which belong to the file we want to restore |
|
| 959 | + * |
|
| 960 | + * @param string $filename name of the file which should be restored |
|
| 961 | + * @param int $timestamp timestamp when the file was deleted |
|
| 962 | + * @return array |
|
| 963 | + */ |
|
| 964 | + private static function getVersionsFromTrash($filename, $timestamp, $user) { |
|
| 965 | + $view = new View('/' . $user . '/files_trashbin/versions'); |
|
| 966 | + $versions = []; |
|
| 967 | + |
|
| 968 | + /** @var \OC\Files\Storage\Storage $storage */ |
|
| 969 | + [$storage,] = $view->resolvePath('/'); |
|
| 970 | + |
|
| 971 | + //force rescan of versions, local storage may not have updated the cache |
|
| 972 | + if (!self::$scannedVersions) { |
|
| 973 | + $storage->getScanner()->scan('files_trashbin/versions'); |
|
| 974 | + self::$scannedVersions = true; |
|
| 975 | + } |
|
| 976 | + |
|
| 977 | + $pattern = \OC::$server->getDatabaseConnection()->escapeLikeParameter(basename($filename)); |
|
| 978 | + if ($timestamp) { |
|
| 979 | + // fetch for old versions |
|
| 980 | + $escapedTimestamp = \OC::$server->getDatabaseConnection()->escapeLikeParameter($timestamp); |
|
| 981 | + $pattern .= '.v%.d' . $escapedTimestamp; |
|
| 982 | + $offset = -strlen($escapedTimestamp) - 2; |
|
| 983 | + } else { |
|
| 984 | + $pattern .= '.v%'; |
|
| 985 | + } |
|
| 986 | + |
|
| 987 | + // Manually fetch all versions from the file cache to be able to filter them by their parent |
|
| 988 | + $cache = $storage->getCache(''); |
|
| 989 | + $query = new CacheQueryBuilder( |
|
| 990 | + \OC::$server->getDatabaseConnection(), |
|
| 991 | + \OC::$server->getSystemConfig(), |
|
| 992 | + \OC::$server->getLogger(), |
|
| 993 | + $cache |
|
| 994 | + ); |
|
| 995 | + $normalizedParentPath = ltrim(Filesystem::normalizePath(dirname('files_trashbin/versions/'. $filename)), '/'); |
|
| 996 | + $parentId = $cache->getId($normalizedParentPath); |
|
| 997 | + if ($parentId === -1) { |
|
| 998 | + return []; |
|
| 999 | + } |
|
| 1000 | + |
|
| 1001 | + $query->selectFileCache() |
|
| 1002 | + ->whereStorageId() |
|
| 1003 | + ->andWhere($query->expr()->eq('parent', $query->createNamedParameter($parentId))) |
|
| 1004 | + ->andWhere($query->expr()->iLike('name', $query->createNamedParameter($pattern))); |
|
| 1005 | + |
|
| 1006 | + $result = $query->executeQuery(); |
|
| 1007 | + $entries = $result->fetchAll(); |
|
| 1008 | + $result->closeCursor(); |
|
| 1009 | + |
|
| 1010 | + /** @var CacheEntry[] $matches */ |
|
| 1011 | + $matches = array_map(function (array $data) { |
|
| 1012 | + return Cache::cacheEntryFromData($data, \OC::$server->getMimeTypeLoader()); |
|
| 1013 | + }, $entries); |
|
| 1014 | + |
|
| 1015 | + foreach ($matches as $ma) { |
|
| 1016 | + if ($timestamp) { |
|
| 1017 | + $parts = explode('.v', substr($ma['path'], 0, $offset)); |
|
| 1018 | + $versions[] = end($parts); |
|
| 1019 | + } else { |
|
| 1020 | + $parts = explode('.v', $ma['path']); |
|
| 1021 | + $versions[] = end($parts); |
|
| 1022 | + } |
|
| 1023 | + } |
|
| 1024 | + |
|
| 1025 | + return $versions; |
|
| 1026 | + } |
|
| 1027 | + |
|
| 1028 | + /** |
|
| 1029 | + * find unique extension for restored file if a file with the same name already exists |
|
| 1030 | + * |
|
| 1031 | + * @param string $location where the file should be restored |
|
| 1032 | + * @param string $filename name of the file |
|
| 1033 | + * @param View $view filesystem view relative to users root directory |
|
| 1034 | + * @return string with unique extension |
|
| 1035 | + */ |
|
| 1036 | + private static function getUniqueFilename($location, $filename, View $view) { |
|
| 1037 | + $ext = pathinfo($filename, PATHINFO_EXTENSION); |
|
| 1038 | + $name = pathinfo($filename, PATHINFO_FILENAME); |
|
| 1039 | + $l = \OC::$server->getL10N('files_trashbin'); |
|
| 1040 | + |
|
| 1041 | + $location = '/' . trim($location, '/'); |
|
| 1042 | + |
|
| 1043 | + // if extension is not empty we set a dot in front of it |
|
| 1044 | + if ($ext !== '') { |
|
| 1045 | + $ext = '.' . $ext; |
|
| 1046 | + } |
|
| 1047 | + |
|
| 1048 | + if ($view->file_exists('files' . $location . '/' . $filename)) { |
|
| 1049 | + $i = 2; |
|
| 1050 | + $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext; |
|
| 1051 | + while ($view->file_exists('files' . $location . '/' . $uniqueName)) { |
|
| 1052 | + $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext; |
|
| 1053 | + $i++; |
|
| 1054 | + } |
|
| 1055 | + |
|
| 1056 | + return $uniqueName; |
|
| 1057 | + } |
|
| 1058 | + |
|
| 1059 | + return $filename; |
|
| 1060 | + } |
|
| 1061 | + |
|
| 1062 | + /** |
|
| 1063 | + * get the size from a given root folder |
|
| 1064 | + * |
|
| 1065 | + * @param View $view file view on the root folder |
|
| 1066 | + * @return integer size of the folder |
|
| 1067 | + */ |
|
| 1068 | + private static function calculateSize($view) { |
|
| 1069 | + $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath(''); |
|
| 1070 | + if (!file_exists($root)) { |
|
| 1071 | + return 0; |
|
| 1072 | + } |
|
| 1073 | + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST); |
|
| 1074 | + $size = 0; |
|
| 1075 | + |
|
| 1076 | + /** |
|
| 1077 | + * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach |
|
| 1078 | + * This bug is fixed in PHP 5.5.9 or before |
|
| 1079 | + * See #8376 |
|
| 1080 | + */ |
|
| 1081 | + $iterator->rewind(); |
|
| 1082 | + while ($iterator->valid()) { |
|
| 1083 | + $path = $iterator->current(); |
|
| 1084 | + $relpath = substr($path, strlen($root) - 1); |
|
| 1085 | + if (!$view->is_dir($relpath)) { |
|
| 1086 | + $size += $view->filesize($relpath); |
|
| 1087 | + } |
|
| 1088 | + $iterator->next(); |
|
| 1089 | + } |
|
| 1090 | + return $size; |
|
| 1091 | + } |
|
| 1092 | + |
|
| 1093 | + /** |
|
| 1094 | + * get current size of trash bin from a given user |
|
| 1095 | + * |
|
| 1096 | + * @param string $user user who owns the trash bin |
|
| 1097 | + * @return integer trash bin size |
|
| 1098 | + */ |
|
| 1099 | + private static function getTrashbinSize($user) { |
|
| 1100 | + $view = new View('/' . $user); |
|
| 1101 | + $fileInfo = $view->getFileInfo('/files_trashbin'); |
|
| 1102 | + return isset($fileInfo['size']) ? $fileInfo['size'] : 0; |
|
| 1103 | + } |
|
| 1104 | + |
|
| 1105 | + /** |
|
| 1106 | + * check if trash bin is empty for a given user |
|
| 1107 | + * |
|
| 1108 | + * @param string $user |
|
| 1109 | + * @return bool |
|
| 1110 | + */ |
|
| 1111 | + public static function isEmpty($user) { |
|
| 1112 | + $view = new View('/' . $user . '/files_trashbin'); |
|
| 1113 | + if ($view->is_dir('/files') && $dh = $view->opendir('/files')) { |
|
| 1114 | + while ($file = readdir($dh)) { |
|
| 1115 | + if (!Filesystem::isIgnoredDir($file)) { |
|
| 1116 | + return false; |
|
| 1117 | + } |
|
| 1118 | + } |
|
| 1119 | + } |
|
| 1120 | + return true; |
|
| 1121 | + } |
|
| 1122 | + |
|
| 1123 | + /** |
|
| 1124 | + * @param $path |
|
| 1125 | + * @return string |
|
| 1126 | + */ |
|
| 1127 | + public static function preview_icon($path) { |
|
| 1128 | + return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]); |
|
| 1129 | + } |
|
| 1130 | 1130 | } |
@@ -50,404 +50,404 @@ |
||
| 50 | 50 | |
| 51 | 51 | class RequestHandlerController extends OCSController { |
| 52 | 52 | |
| 53 | - /** @var FederatedShareProvider */ |
|
| 54 | - private $federatedShareProvider; |
|
| 55 | - |
|
| 56 | - /** @var IDBConnection */ |
|
| 57 | - private $connection; |
|
| 58 | - |
|
| 59 | - /** @var Share\IManager */ |
|
| 60 | - private $shareManager; |
|
| 61 | - |
|
| 62 | - /** @var Notifications */ |
|
| 63 | - private $notifications; |
|
| 64 | - |
|
| 65 | - /** @var AddressHandler */ |
|
| 66 | - private $addressHandler; |
|
| 67 | - |
|
| 68 | - /** @var IUserManager */ |
|
| 69 | - private $userManager; |
|
| 70 | - |
|
| 71 | - /** @var string */ |
|
| 72 | - private $shareTable = 'share'; |
|
| 73 | - |
|
| 74 | - /** @var ICloudIdManager */ |
|
| 75 | - private $cloudIdManager; |
|
| 76 | - |
|
| 77 | - /** @var LoggerInterface */ |
|
| 78 | - private $logger; |
|
| 79 | - |
|
| 80 | - /** @var ICloudFederationFactory */ |
|
| 81 | - private $cloudFederationFactory; |
|
| 82 | - |
|
| 83 | - /** @var ICloudFederationProviderManager */ |
|
| 84 | - private $cloudFederationProviderManager; |
|
| 85 | - |
|
| 86 | - public function __construct(string $appName, |
|
| 87 | - IRequest $request, |
|
| 88 | - FederatedShareProvider $federatedShareProvider, |
|
| 89 | - IDBConnection $connection, |
|
| 90 | - Share\IManager $shareManager, |
|
| 91 | - Notifications $notifications, |
|
| 92 | - AddressHandler $addressHandler, |
|
| 93 | - IUserManager $userManager, |
|
| 94 | - ICloudIdManager $cloudIdManager, |
|
| 95 | - LoggerInterface $logger, |
|
| 96 | - ICloudFederationFactory $cloudFederationFactory, |
|
| 97 | - ICloudFederationProviderManager $cloudFederationProviderManager |
|
| 98 | - ) { |
|
| 99 | - parent::__construct($appName, $request); |
|
| 100 | - |
|
| 101 | - $this->federatedShareProvider = $federatedShareProvider; |
|
| 102 | - $this->connection = $connection; |
|
| 103 | - $this->shareManager = $shareManager; |
|
| 104 | - $this->notifications = $notifications; |
|
| 105 | - $this->addressHandler = $addressHandler; |
|
| 106 | - $this->userManager = $userManager; |
|
| 107 | - $this->cloudIdManager = $cloudIdManager; |
|
| 108 | - $this->logger = $logger; |
|
| 109 | - $this->cloudFederationFactory = $cloudFederationFactory; |
|
| 110 | - $this->cloudFederationProviderManager = $cloudFederationProviderManager; |
|
| 111 | - } |
|
| 112 | - |
|
| 113 | - /** |
|
| 114 | - * @NoCSRFRequired |
|
| 115 | - * @PublicPage |
|
| 116 | - * |
|
| 117 | - * create a new share |
|
| 118 | - * |
|
| 119 | - * @return Http\DataResponse |
|
| 120 | - * @throws OCSException |
|
| 121 | - */ |
|
| 122 | - public function createShare() { |
|
| 123 | - $remote = isset($_POST['remote']) ? $_POST['remote'] : null; |
|
| 124 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 125 | - $name = isset($_POST['name']) ? $_POST['name'] : null; |
|
| 126 | - $owner = isset($_POST['owner']) ? $_POST['owner'] : null; |
|
| 127 | - $sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null; |
|
| 128 | - $shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null; |
|
| 129 | - $remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null; |
|
| 130 | - $sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null; |
|
| 131 | - $ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null; |
|
| 132 | - |
|
| 133 | - if ($ownerFederatedId === null) { |
|
| 134 | - $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId(); |
|
| 135 | - } |
|
| 136 | - // if the owner of the share and the initiator are the same user |
|
| 137 | - // we also complete the federated share ID for the initiator |
|
| 138 | - if ($sharedByFederatedId === null && $owner === $sharedBy) { |
|
| 139 | - $sharedByFederatedId = $ownerFederatedId; |
|
| 140 | - } |
|
| 141 | - |
|
| 142 | - $share = $this->cloudFederationFactory->getCloudFederationShare( |
|
| 143 | - $shareWith, |
|
| 144 | - $name, |
|
| 145 | - '', |
|
| 146 | - $remoteId, |
|
| 147 | - $ownerFederatedId, |
|
| 148 | - $owner, |
|
| 149 | - $sharedByFederatedId, |
|
| 150 | - $sharedBy, |
|
| 151 | - $token, |
|
| 152 | - 'user', |
|
| 153 | - 'file' |
|
| 154 | - ); |
|
| 155 | - |
|
| 156 | - try { |
|
| 157 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 158 | - $provider->shareReceived($share); |
|
| 159 | - } catch (ProviderDoesNotExistsException $e) { |
|
| 160 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 161 | - } catch (ProviderCouldNotAddShareException $e) { |
|
| 162 | - throw new OCSException($e->getMessage(), 400); |
|
| 163 | - } catch (\Exception $e) { |
|
| 164 | - throw new OCSException('internal server error, was not able to add share from ' . $remote, 500); |
|
| 165 | - } |
|
| 166 | - |
|
| 167 | - return new Http\DataResponse(); |
|
| 168 | - } |
|
| 169 | - |
|
| 170 | - /** |
|
| 171 | - * @NoCSRFRequired |
|
| 172 | - * @PublicPage |
|
| 173 | - * |
|
| 174 | - * create re-share on behalf of another user |
|
| 175 | - * |
|
| 176 | - * @param int $id |
|
| 177 | - * @return Http\DataResponse |
|
| 178 | - * @throws OCSBadRequestException |
|
| 179 | - * @throws OCSException |
|
| 180 | - * @throws OCSForbiddenException |
|
| 181 | - */ |
|
| 182 | - public function reShare($id) { |
|
| 183 | - $token = $this->request->getParam('token', null); |
|
| 184 | - $shareWith = $this->request->getParam('shareWith', null); |
|
| 185 | - $permission = (int)$this->request->getParam('permission', null); |
|
| 186 | - $remoteId = (int)$this->request->getParam('remoteId', null); |
|
| 187 | - |
|
| 188 | - if ($id === null || |
|
| 189 | - $token === null || |
|
| 190 | - $shareWith === null || |
|
| 191 | - $permission === null || |
|
| 192 | - $remoteId === null |
|
| 193 | - ) { |
|
| 194 | - throw new OCSBadRequestException(); |
|
| 195 | - } |
|
| 196 | - |
|
| 197 | - $notification = [ |
|
| 198 | - 'sharedSecret' => $token, |
|
| 199 | - 'shareWith' => $shareWith, |
|
| 200 | - 'senderId' => $remoteId, |
|
| 201 | - 'message' => 'Recipient of a share ask the owner to reshare the file' |
|
| 202 | - ]; |
|
| 203 | - |
|
| 204 | - try { |
|
| 205 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 206 | - [$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification); |
|
| 207 | - return new Http\DataResponse([ |
|
| 208 | - 'token' => $newToken, |
|
| 209 | - 'remoteId' => $localId |
|
| 210 | - ]); |
|
| 211 | - } catch (ProviderDoesNotExistsException $e) { |
|
| 212 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 213 | - } catch (ShareNotFound $e) { |
|
| 214 | - $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); |
|
| 215 | - } catch (\Exception $e) { |
|
| 216 | - $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); |
|
| 217 | - } |
|
| 218 | - |
|
| 219 | - throw new OCSBadRequestException(); |
|
| 220 | - } |
|
| 221 | - |
|
| 222 | - |
|
| 223 | - /** |
|
| 224 | - * @NoCSRFRequired |
|
| 225 | - * @PublicPage |
|
| 226 | - * |
|
| 227 | - * accept server-to-server share |
|
| 228 | - * |
|
| 229 | - * @param int $id |
|
| 230 | - * @return Http\DataResponse |
|
| 231 | - * @throws OCSException |
|
| 232 | - * @throws ShareNotFound |
|
| 233 | - * @throws \OC\HintException |
|
| 234 | - */ |
|
| 235 | - public function acceptShare($id) { |
|
| 236 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 237 | - |
|
| 238 | - $notification = [ |
|
| 239 | - 'sharedSecret' => $token, |
|
| 240 | - 'message' => 'Recipient accept the share' |
|
| 241 | - ]; |
|
| 242 | - |
|
| 243 | - try { |
|
| 244 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 245 | - $provider->notificationReceived('SHARE_ACCEPTED', $id, $notification); |
|
| 246 | - } catch (ProviderDoesNotExistsException $e) { |
|
| 247 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 248 | - } catch (ShareNotFound $e) { |
|
| 249 | - $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); |
|
| 250 | - } catch (\Exception $e) { |
|
| 251 | - $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); |
|
| 252 | - } |
|
| 253 | - |
|
| 254 | - return new Http\DataResponse(); |
|
| 255 | - } |
|
| 256 | - |
|
| 257 | - /** |
|
| 258 | - * @NoCSRFRequired |
|
| 259 | - * @PublicPage |
|
| 260 | - * |
|
| 261 | - * decline server-to-server share |
|
| 262 | - * |
|
| 263 | - * @param int $id |
|
| 264 | - * @return Http\DataResponse |
|
| 265 | - * @throws OCSException |
|
| 266 | - */ |
|
| 267 | - public function declineShare($id) { |
|
| 268 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 269 | - |
|
| 270 | - $notification = [ |
|
| 271 | - 'sharedSecret' => $token, |
|
| 272 | - 'message' => 'Recipient declined the share' |
|
| 273 | - ]; |
|
| 274 | - |
|
| 275 | - try { |
|
| 276 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 277 | - $provider->notificationReceived('SHARE_DECLINED', $id, $notification); |
|
| 278 | - } catch (ProviderDoesNotExistsException $e) { |
|
| 279 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 280 | - } catch (ShareNotFound $e) { |
|
| 281 | - $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); |
|
| 282 | - } catch (\Exception $e) { |
|
| 283 | - $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); |
|
| 284 | - } |
|
| 285 | - |
|
| 286 | - return new Http\DataResponse(); |
|
| 287 | - } |
|
| 288 | - |
|
| 289 | - /** |
|
| 290 | - * @NoCSRFRequired |
|
| 291 | - * @PublicPage |
|
| 292 | - * |
|
| 293 | - * remove server-to-server share if it was unshared by the owner |
|
| 294 | - * |
|
| 295 | - * @param int $id |
|
| 296 | - * @return Http\DataResponse |
|
| 297 | - * @throws OCSException |
|
| 298 | - */ |
|
| 299 | - public function unshare($id) { |
|
| 300 | - if (!$this->isS2SEnabled()) { |
|
| 301 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 302 | - } |
|
| 303 | - |
|
| 304 | - $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 305 | - |
|
| 306 | - try { |
|
| 307 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 308 | - $notification = ['sharedSecret' => $token]; |
|
| 309 | - $provider->notificationReceived('SHARE_UNSHARED', $id, $notification); |
|
| 310 | - } catch (\Exception $e) { |
|
| 311 | - $this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]); |
|
| 312 | - } |
|
| 313 | - |
|
| 314 | - return new Http\DataResponse(); |
|
| 315 | - } |
|
| 316 | - |
|
| 317 | - private function cleanupRemote($remote) { |
|
| 318 | - $remote = substr($remote, strpos($remote, '://') + 3); |
|
| 319 | - |
|
| 320 | - return rtrim($remote, '/'); |
|
| 321 | - } |
|
| 322 | - |
|
| 323 | - |
|
| 324 | - /** |
|
| 325 | - * @NoCSRFRequired |
|
| 326 | - * @PublicPage |
|
| 327 | - * |
|
| 328 | - * federated share was revoked, either by the owner or the re-sharer |
|
| 329 | - * |
|
| 330 | - * @param int $id |
|
| 331 | - * @return Http\DataResponse |
|
| 332 | - * @throws OCSBadRequestException |
|
| 333 | - */ |
|
| 334 | - public function revoke($id) { |
|
| 335 | - $token = $this->request->getParam('token'); |
|
| 336 | - |
|
| 337 | - try { |
|
| 338 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 339 | - $notification = ['sharedSecret' => $token]; |
|
| 340 | - $provider->notificationReceived('RESHARE_UNDO', $id, $notification); |
|
| 341 | - return new Http\DataResponse(); |
|
| 342 | - } catch (\Exception $e) { |
|
| 343 | - throw new OCSBadRequestException(); |
|
| 344 | - } |
|
| 345 | - } |
|
| 346 | - |
|
| 347 | - /** |
|
| 348 | - * check if server-to-server sharing is enabled |
|
| 349 | - * |
|
| 350 | - * @param bool $incoming |
|
| 351 | - * @return bool |
|
| 352 | - */ |
|
| 353 | - private function isS2SEnabled($incoming = false) { |
|
| 354 | - $result = \OCP\App::isEnabled('files_sharing'); |
|
| 355 | - |
|
| 356 | - if ($incoming) { |
|
| 357 | - $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled(); |
|
| 358 | - } else { |
|
| 359 | - $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); |
|
| 360 | - } |
|
| 361 | - |
|
| 362 | - return $result; |
|
| 363 | - } |
|
| 364 | - |
|
| 365 | - /** |
|
| 366 | - * @NoCSRFRequired |
|
| 367 | - * @PublicPage |
|
| 368 | - * |
|
| 369 | - * update share information to keep federated re-shares in sync |
|
| 370 | - * |
|
| 371 | - * @param int $id |
|
| 372 | - * @return Http\DataResponse |
|
| 373 | - * @throws OCSBadRequestException |
|
| 374 | - */ |
|
| 375 | - public function updatePermissions($id) { |
|
| 376 | - $token = $this->request->getParam('token', null); |
|
| 377 | - $ncPermissions = $this->request->getParam('permissions', null); |
|
| 378 | - |
|
| 379 | - try { |
|
| 380 | - $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 381 | - $ocmPermissions = $this->ncPermissions2ocmPermissions((int)$ncPermissions); |
|
| 382 | - $notification = ['sharedSecret' => $token, 'permission' => $ocmPermissions]; |
|
| 383 | - $provider->notificationReceived('RESHARE_CHANGE_PERMISSION', $id, $notification); |
|
| 384 | - } catch (\Exception $e) { |
|
| 385 | - $this->logger->debug($e->getMessage(), ['exception' => $e]); |
|
| 386 | - throw new OCSBadRequestException(); |
|
| 387 | - } |
|
| 388 | - |
|
| 389 | - return new Http\DataResponse(); |
|
| 390 | - } |
|
| 391 | - |
|
| 392 | - /** |
|
| 393 | - * translate Nextcloud permissions to OCM Permissions |
|
| 394 | - * |
|
| 395 | - * @param $ncPermissions |
|
| 396 | - * @return array |
|
| 397 | - */ |
|
| 398 | - protected function ncPermissions2ocmPermissions($ncPermissions) { |
|
| 399 | - $ocmPermissions = []; |
|
| 400 | - |
|
| 401 | - if ($ncPermissions & Constants::PERMISSION_SHARE) { |
|
| 402 | - $ocmPermissions[] = 'share'; |
|
| 403 | - } |
|
| 404 | - |
|
| 405 | - if ($ncPermissions & Constants::PERMISSION_READ) { |
|
| 406 | - $ocmPermissions[] = 'read'; |
|
| 407 | - } |
|
| 408 | - |
|
| 409 | - if (($ncPermissions & Constants::PERMISSION_CREATE) || |
|
| 410 | - ($ncPermissions & Constants::PERMISSION_UPDATE)) { |
|
| 411 | - $ocmPermissions[] = 'write'; |
|
| 412 | - } |
|
| 413 | - |
|
| 414 | - return $ocmPermissions; |
|
| 415 | - } |
|
| 416 | - |
|
| 417 | - /** |
|
| 418 | - * @NoCSRFRequired |
|
| 419 | - * @PublicPage |
|
| 420 | - * |
|
| 421 | - * change the owner of a server-to-server share |
|
| 422 | - * |
|
| 423 | - * @param int $id |
|
| 424 | - * @return Http\DataResponse |
|
| 425 | - * @throws \InvalidArgumentException |
|
| 426 | - * @throws OCSException |
|
| 427 | - */ |
|
| 428 | - public function move($id) { |
|
| 429 | - if (!$this->isS2SEnabled()) { |
|
| 430 | - throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 431 | - } |
|
| 432 | - |
|
| 433 | - $token = $this->request->getParam('token'); |
|
| 434 | - $remote = $this->request->getParam('remote'); |
|
| 435 | - $newRemoteId = $this->request->getParam('remote_id', $id); |
|
| 436 | - $cloudId = $this->cloudIdManager->resolveCloudId($remote); |
|
| 437 | - |
|
| 438 | - $qb = $this->connection->getQueryBuilder(); |
|
| 439 | - $query = $qb->update('share_external') |
|
| 440 | - ->set('remote', $qb->createNamedParameter($cloudId->getRemote())) |
|
| 441 | - ->set('owner', $qb->createNamedParameter($cloudId->getUser())) |
|
| 442 | - ->set('remote_id', $qb->createNamedParameter($newRemoteId)) |
|
| 443 | - ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id))) |
|
| 444 | - ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token))); |
|
| 445 | - $affected = $query->executeStatement(); |
|
| 446 | - |
|
| 447 | - if ($affected > 0) { |
|
| 448 | - return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]); |
|
| 449 | - } else { |
|
| 450 | - throw new OCSBadRequestException('Share not found or token invalid'); |
|
| 451 | - } |
|
| 452 | - } |
|
| 53 | + /** @var FederatedShareProvider */ |
|
| 54 | + private $federatedShareProvider; |
|
| 55 | + |
|
| 56 | + /** @var IDBConnection */ |
|
| 57 | + private $connection; |
|
| 58 | + |
|
| 59 | + /** @var Share\IManager */ |
|
| 60 | + private $shareManager; |
|
| 61 | + |
|
| 62 | + /** @var Notifications */ |
|
| 63 | + private $notifications; |
|
| 64 | + |
|
| 65 | + /** @var AddressHandler */ |
|
| 66 | + private $addressHandler; |
|
| 67 | + |
|
| 68 | + /** @var IUserManager */ |
|
| 69 | + private $userManager; |
|
| 70 | + |
|
| 71 | + /** @var string */ |
|
| 72 | + private $shareTable = 'share'; |
|
| 73 | + |
|
| 74 | + /** @var ICloudIdManager */ |
|
| 75 | + private $cloudIdManager; |
|
| 76 | + |
|
| 77 | + /** @var LoggerInterface */ |
|
| 78 | + private $logger; |
|
| 79 | + |
|
| 80 | + /** @var ICloudFederationFactory */ |
|
| 81 | + private $cloudFederationFactory; |
|
| 82 | + |
|
| 83 | + /** @var ICloudFederationProviderManager */ |
|
| 84 | + private $cloudFederationProviderManager; |
|
| 85 | + |
|
| 86 | + public function __construct(string $appName, |
|
| 87 | + IRequest $request, |
|
| 88 | + FederatedShareProvider $federatedShareProvider, |
|
| 89 | + IDBConnection $connection, |
|
| 90 | + Share\IManager $shareManager, |
|
| 91 | + Notifications $notifications, |
|
| 92 | + AddressHandler $addressHandler, |
|
| 93 | + IUserManager $userManager, |
|
| 94 | + ICloudIdManager $cloudIdManager, |
|
| 95 | + LoggerInterface $logger, |
|
| 96 | + ICloudFederationFactory $cloudFederationFactory, |
|
| 97 | + ICloudFederationProviderManager $cloudFederationProviderManager |
|
| 98 | + ) { |
|
| 99 | + parent::__construct($appName, $request); |
|
| 100 | + |
|
| 101 | + $this->federatedShareProvider = $federatedShareProvider; |
|
| 102 | + $this->connection = $connection; |
|
| 103 | + $this->shareManager = $shareManager; |
|
| 104 | + $this->notifications = $notifications; |
|
| 105 | + $this->addressHandler = $addressHandler; |
|
| 106 | + $this->userManager = $userManager; |
|
| 107 | + $this->cloudIdManager = $cloudIdManager; |
|
| 108 | + $this->logger = $logger; |
|
| 109 | + $this->cloudFederationFactory = $cloudFederationFactory; |
|
| 110 | + $this->cloudFederationProviderManager = $cloudFederationProviderManager; |
|
| 111 | + } |
|
| 112 | + |
|
| 113 | + /** |
|
| 114 | + * @NoCSRFRequired |
|
| 115 | + * @PublicPage |
|
| 116 | + * |
|
| 117 | + * create a new share |
|
| 118 | + * |
|
| 119 | + * @return Http\DataResponse |
|
| 120 | + * @throws OCSException |
|
| 121 | + */ |
|
| 122 | + public function createShare() { |
|
| 123 | + $remote = isset($_POST['remote']) ? $_POST['remote'] : null; |
|
| 124 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 125 | + $name = isset($_POST['name']) ? $_POST['name'] : null; |
|
| 126 | + $owner = isset($_POST['owner']) ? $_POST['owner'] : null; |
|
| 127 | + $sharedBy = isset($_POST['sharedBy']) ? $_POST['sharedBy'] : null; |
|
| 128 | + $shareWith = isset($_POST['shareWith']) ? $_POST['shareWith'] : null; |
|
| 129 | + $remoteId = isset($_POST['remoteId']) ? (int)$_POST['remoteId'] : null; |
|
| 130 | + $sharedByFederatedId = isset($_POST['sharedByFederatedId']) ? $_POST['sharedByFederatedId'] : null; |
|
| 131 | + $ownerFederatedId = isset($_POST['ownerFederatedId']) ? $_POST['ownerFederatedId'] : null; |
|
| 132 | + |
|
| 133 | + if ($ownerFederatedId === null) { |
|
| 134 | + $ownerFederatedId = $this->cloudIdManager->getCloudId($owner, $this->cleanupRemote($remote))->getId(); |
|
| 135 | + } |
|
| 136 | + // if the owner of the share and the initiator are the same user |
|
| 137 | + // we also complete the federated share ID for the initiator |
|
| 138 | + if ($sharedByFederatedId === null && $owner === $sharedBy) { |
|
| 139 | + $sharedByFederatedId = $ownerFederatedId; |
|
| 140 | + } |
|
| 141 | + |
|
| 142 | + $share = $this->cloudFederationFactory->getCloudFederationShare( |
|
| 143 | + $shareWith, |
|
| 144 | + $name, |
|
| 145 | + '', |
|
| 146 | + $remoteId, |
|
| 147 | + $ownerFederatedId, |
|
| 148 | + $owner, |
|
| 149 | + $sharedByFederatedId, |
|
| 150 | + $sharedBy, |
|
| 151 | + $token, |
|
| 152 | + 'user', |
|
| 153 | + 'file' |
|
| 154 | + ); |
|
| 155 | + |
|
| 156 | + try { |
|
| 157 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 158 | + $provider->shareReceived($share); |
|
| 159 | + } catch (ProviderDoesNotExistsException $e) { |
|
| 160 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 161 | + } catch (ProviderCouldNotAddShareException $e) { |
|
| 162 | + throw new OCSException($e->getMessage(), 400); |
|
| 163 | + } catch (\Exception $e) { |
|
| 164 | + throw new OCSException('internal server error, was not able to add share from ' . $remote, 500); |
|
| 165 | + } |
|
| 166 | + |
|
| 167 | + return new Http\DataResponse(); |
|
| 168 | + } |
|
| 169 | + |
|
| 170 | + /** |
|
| 171 | + * @NoCSRFRequired |
|
| 172 | + * @PublicPage |
|
| 173 | + * |
|
| 174 | + * create re-share on behalf of another user |
|
| 175 | + * |
|
| 176 | + * @param int $id |
|
| 177 | + * @return Http\DataResponse |
|
| 178 | + * @throws OCSBadRequestException |
|
| 179 | + * @throws OCSException |
|
| 180 | + * @throws OCSForbiddenException |
|
| 181 | + */ |
|
| 182 | + public function reShare($id) { |
|
| 183 | + $token = $this->request->getParam('token', null); |
|
| 184 | + $shareWith = $this->request->getParam('shareWith', null); |
|
| 185 | + $permission = (int)$this->request->getParam('permission', null); |
|
| 186 | + $remoteId = (int)$this->request->getParam('remoteId', null); |
|
| 187 | + |
|
| 188 | + if ($id === null || |
|
| 189 | + $token === null || |
|
| 190 | + $shareWith === null || |
|
| 191 | + $permission === null || |
|
| 192 | + $remoteId === null |
|
| 193 | + ) { |
|
| 194 | + throw new OCSBadRequestException(); |
|
| 195 | + } |
|
| 196 | + |
|
| 197 | + $notification = [ |
|
| 198 | + 'sharedSecret' => $token, |
|
| 199 | + 'shareWith' => $shareWith, |
|
| 200 | + 'senderId' => $remoteId, |
|
| 201 | + 'message' => 'Recipient of a share ask the owner to reshare the file' |
|
| 202 | + ]; |
|
| 203 | + |
|
| 204 | + try { |
|
| 205 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 206 | + [$newToken, $localId] = $provider->notificationReceived('REQUEST_RESHARE', $id, $notification); |
|
| 207 | + return new Http\DataResponse([ |
|
| 208 | + 'token' => $newToken, |
|
| 209 | + 'remoteId' => $localId |
|
| 210 | + ]); |
|
| 211 | + } catch (ProviderDoesNotExistsException $e) { |
|
| 212 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 213 | + } catch (ShareNotFound $e) { |
|
| 214 | + $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); |
|
| 215 | + } catch (\Exception $e) { |
|
| 216 | + $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + throw new OCSBadRequestException(); |
|
| 220 | + } |
|
| 221 | + |
|
| 222 | + |
|
| 223 | + /** |
|
| 224 | + * @NoCSRFRequired |
|
| 225 | + * @PublicPage |
|
| 226 | + * |
|
| 227 | + * accept server-to-server share |
|
| 228 | + * |
|
| 229 | + * @param int $id |
|
| 230 | + * @return Http\DataResponse |
|
| 231 | + * @throws OCSException |
|
| 232 | + * @throws ShareNotFound |
|
| 233 | + * @throws \OC\HintException |
|
| 234 | + */ |
|
| 235 | + public function acceptShare($id) { |
|
| 236 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 237 | + |
|
| 238 | + $notification = [ |
|
| 239 | + 'sharedSecret' => $token, |
|
| 240 | + 'message' => 'Recipient accept the share' |
|
| 241 | + ]; |
|
| 242 | + |
|
| 243 | + try { |
|
| 244 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 245 | + $provider->notificationReceived('SHARE_ACCEPTED', $id, $notification); |
|
| 246 | + } catch (ProviderDoesNotExistsException $e) { |
|
| 247 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 248 | + } catch (ShareNotFound $e) { |
|
| 249 | + $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); |
|
| 250 | + } catch (\Exception $e) { |
|
| 251 | + $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); |
|
| 252 | + } |
|
| 253 | + |
|
| 254 | + return new Http\DataResponse(); |
|
| 255 | + } |
|
| 256 | + |
|
| 257 | + /** |
|
| 258 | + * @NoCSRFRequired |
|
| 259 | + * @PublicPage |
|
| 260 | + * |
|
| 261 | + * decline server-to-server share |
|
| 262 | + * |
|
| 263 | + * @param int $id |
|
| 264 | + * @return Http\DataResponse |
|
| 265 | + * @throws OCSException |
|
| 266 | + */ |
|
| 267 | + public function declineShare($id) { |
|
| 268 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 269 | + |
|
| 270 | + $notification = [ |
|
| 271 | + 'sharedSecret' => $token, |
|
| 272 | + 'message' => 'Recipient declined the share' |
|
| 273 | + ]; |
|
| 274 | + |
|
| 275 | + try { |
|
| 276 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 277 | + $provider->notificationReceived('SHARE_DECLINED', $id, $notification); |
|
| 278 | + } catch (ProviderDoesNotExistsException $e) { |
|
| 279 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 280 | + } catch (ShareNotFound $e) { |
|
| 281 | + $this->logger->debug('Share not found: ' . $e->getMessage(), ['exception' => $e]); |
|
| 282 | + } catch (\Exception $e) { |
|
| 283 | + $this->logger->debug('internal server error, can not process notification: ' . $e->getMessage(), ['exception' => $e]); |
|
| 284 | + } |
|
| 285 | + |
|
| 286 | + return new Http\DataResponse(); |
|
| 287 | + } |
|
| 288 | + |
|
| 289 | + /** |
|
| 290 | + * @NoCSRFRequired |
|
| 291 | + * @PublicPage |
|
| 292 | + * |
|
| 293 | + * remove server-to-server share if it was unshared by the owner |
|
| 294 | + * |
|
| 295 | + * @param int $id |
|
| 296 | + * @return Http\DataResponse |
|
| 297 | + * @throws OCSException |
|
| 298 | + */ |
|
| 299 | + public function unshare($id) { |
|
| 300 | + if (!$this->isS2SEnabled()) { |
|
| 301 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 302 | + } |
|
| 303 | + |
|
| 304 | + $token = isset($_POST['token']) ? $_POST['token'] : null; |
|
| 305 | + |
|
| 306 | + try { |
|
| 307 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 308 | + $notification = ['sharedSecret' => $token]; |
|
| 309 | + $provider->notificationReceived('SHARE_UNSHARED', $id, $notification); |
|
| 310 | + } catch (\Exception $e) { |
|
| 311 | + $this->logger->debug('processing unshare notification failed: ' . $e->getMessage(), ['exception' => $e]); |
|
| 312 | + } |
|
| 313 | + |
|
| 314 | + return new Http\DataResponse(); |
|
| 315 | + } |
|
| 316 | + |
|
| 317 | + private function cleanupRemote($remote) { |
|
| 318 | + $remote = substr($remote, strpos($remote, '://') + 3); |
|
| 319 | + |
|
| 320 | + return rtrim($remote, '/'); |
|
| 321 | + } |
|
| 322 | + |
|
| 323 | + |
|
| 324 | + /** |
|
| 325 | + * @NoCSRFRequired |
|
| 326 | + * @PublicPage |
|
| 327 | + * |
|
| 328 | + * federated share was revoked, either by the owner or the re-sharer |
|
| 329 | + * |
|
| 330 | + * @param int $id |
|
| 331 | + * @return Http\DataResponse |
|
| 332 | + * @throws OCSBadRequestException |
|
| 333 | + */ |
|
| 334 | + public function revoke($id) { |
|
| 335 | + $token = $this->request->getParam('token'); |
|
| 336 | + |
|
| 337 | + try { |
|
| 338 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 339 | + $notification = ['sharedSecret' => $token]; |
|
| 340 | + $provider->notificationReceived('RESHARE_UNDO', $id, $notification); |
|
| 341 | + return new Http\DataResponse(); |
|
| 342 | + } catch (\Exception $e) { |
|
| 343 | + throw new OCSBadRequestException(); |
|
| 344 | + } |
|
| 345 | + } |
|
| 346 | + |
|
| 347 | + /** |
|
| 348 | + * check if server-to-server sharing is enabled |
|
| 349 | + * |
|
| 350 | + * @param bool $incoming |
|
| 351 | + * @return bool |
|
| 352 | + */ |
|
| 353 | + private function isS2SEnabled($incoming = false) { |
|
| 354 | + $result = \OCP\App::isEnabled('files_sharing'); |
|
| 355 | + |
|
| 356 | + if ($incoming) { |
|
| 357 | + $result = $result && $this->federatedShareProvider->isIncomingServer2serverShareEnabled(); |
|
| 358 | + } else { |
|
| 359 | + $result = $result && $this->federatedShareProvider->isOutgoingServer2serverShareEnabled(); |
|
| 360 | + } |
|
| 361 | + |
|
| 362 | + return $result; |
|
| 363 | + } |
|
| 364 | + |
|
| 365 | + /** |
|
| 366 | + * @NoCSRFRequired |
|
| 367 | + * @PublicPage |
|
| 368 | + * |
|
| 369 | + * update share information to keep federated re-shares in sync |
|
| 370 | + * |
|
| 371 | + * @param int $id |
|
| 372 | + * @return Http\DataResponse |
|
| 373 | + * @throws OCSBadRequestException |
|
| 374 | + */ |
|
| 375 | + public function updatePermissions($id) { |
|
| 376 | + $token = $this->request->getParam('token', null); |
|
| 377 | + $ncPermissions = $this->request->getParam('permissions', null); |
|
| 378 | + |
|
| 379 | + try { |
|
| 380 | + $provider = $this->cloudFederationProviderManager->getCloudFederationProvider('file'); |
|
| 381 | + $ocmPermissions = $this->ncPermissions2ocmPermissions((int)$ncPermissions); |
|
| 382 | + $notification = ['sharedSecret' => $token, 'permission' => $ocmPermissions]; |
|
| 383 | + $provider->notificationReceived('RESHARE_CHANGE_PERMISSION', $id, $notification); |
|
| 384 | + } catch (\Exception $e) { |
|
| 385 | + $this->logger->debug($e->getMessage(), ['exception' => $e]); |
|
| 386 | + throw new OCSBadRequestException(); |
|
| 387 | + } |
|
| 388 | + |
|
| 389 | + return new Http\DataResponse(); |
|
| 390 | + } |
|
| 391 | + |
|
| 392 | + /** |
|
| 393 | + * translate Nextcloud permissions to OCM Permissions |
|
| 394 | + * |
|
| 395 | + * @param $ncPermissions |
|
| 396 | + * @return array |
|
| 397 | + */ |
|
| 398 | + protected function ncPermissions2ocmPermissions($ncPermissions) { |
|
| 399 | + $ocmPermissions = []; |
|
| 400 | + |
|
| 401 | + if ($ncPermissions & Constants::PERMISSION_SHARE) { |
|
| 402 | + $ocmPermissions[] = 'share'; |
|
| 403 | + } |
|
| 404 | + |
|
| 405 | + if ($ncPermissions & Constants::PERMISSION_READ) { |
|
| 406 | + $ocmPermissions[] = 'read'; |
|
| 407 | + } |
|
| 408 | + |
|
| 409 | + if (($ncPermissions & Constants::PERMISSION_CREATE) || |
|
| 410 | + ($ncPermissions & Constants::PERMISSION_UPDATE)) { |
|
| 411 | + $ocmPermissions[] = 'write'; |
|
| 412 | + } |
|
| 413 | + |
|
| 414 | + return $ocmPermissions; |
|
| 415 | + } |
|
| 416 | + |
|
| 417 | + /** |
|
| 418 | + * @NoCSRFRequired |
|
| 419 | + * @PublicPage |
|
| 420 | + * |
|
| 421 | + * change the owner of a server-to-server share |
|
| 422 | + * |
|
| 423 | + * @param int $id |
|
| 424 | + * @return Http\DataResponse |
|
| 425 | + * @throws \InvalidArgumentException |
|
| 426 | + * @throws OCSException |
|
| 427 | + */ |
|
| 428 | + public function move($id) { |
|
| 429 | + if (!$this->isS2SEnabled()) { |
|
| 430 | + throw new OCSException('Server does not support federated cloud sharing', 503); |
|
| 431 | + } |
|
| 432 | + |
|
| 433 | + $token = $this->request->getParam('token'); |
|
| 434 | + $remote = $this->request->getParam('remote'); |
|
| 435 | + $newRemoteId = $this->request->getParam('remote_id', $id); |
|
| 436 | + $cloudId = $this->cloudIdManager->resolveCloudId($remote); |
|
| 437 | + |
|
| 438 | + $qb = $this->connection->getQueryBuilder(); |
|
| 439 | + $query = $qb->update('share_external') |
|
| 440 | + ->set('remote', $qb->createNamedParameter($cloudId->getRemote())) |
|
| 441 | + ->set('owner', $qb->createNamedParameter($cloudId->getUser())) |
|
| 442 | + ->set('remote_id', $qb->createNamedParameter($newRemoteId)) |
|
| 443 | + ->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($id))) |
|
| 444 | + ->andWhere($qb->expr()->eq('share_token', $qb->createNamedParameter($token))); |
|
| 445 | + $affected = $query->executeStatement(); |
|
| 446 | + |
|
| 447 | + if ($affected > 0) { |
|
| 448 | + return new Http\DataResponse(['remote' => $cloudId->getRemote(), 'owner' => $cloudId->getUser()]); |
|
| 449 | + } else { |
|
| 450 | + throw new OCSBadRequestException('Share not found or token invalid'); |
|
| 451 | + } |
|
| 452 | + } |
|
| 453 | 453 | } |
@@ -72,1129 +72,1129 @@ |
||
| 72 | 72 | */ |
| 73 | 73 | class ShareByMailProvider implements IShareProvider { |
| 74 | 74 | |
| 75 | - /** @var IDBConnection */ |
|
| 76 | - private $dbConnection; |
|
| 77 | - |
|
| 78 | - /** @var ILogger */ |
|
| 79 | - private $logger; |
|
| 80 | - |
|
| 81 | - /** @var ISecureRandom */ |
|
| 82 | - private $secureRandom; |
|
| 83 | - |
|
| 84 | - /** @var IUserManager */ |
|
| 85 | - private $userManager; |
|
| 86 | - |
|
| 87 | - /** @var IRootFolder */ |
|
| 88 | - private $rootFolder; |
|
| 89 | - |
|
| 90 | - /** @var IL10N */ |
|
| 91 | - private $l; |
|
| 92 | - |
|
| 93 | - /** @var IMailer */ |
|
| 94 | - private $mailer; |
|
| 95 | - |
|
| 96 | - /** @var IURLGenerator */ |
|
| 97 | - private $urlGenerator; |
|
| 98 | - |
|
| 99 | - /** @var IManager */ |
|
| 100 | - private $activityManager; |
|
| 101 | - |
|
| 102 | - /** @var SettingsManager */ |
|
| 103 | - private $settingsManager; |
|
| 104 | - |
|
| 105 | - /** @var Defaults */ |
|
| 106 | - private $defaults; |
|
| 107 | - |
|
| 108 | - /** @var IHasher */ |
|
| 109 | - private $hasher; |
|
| 110 | - |
|
| 111 | - /** @var IEventDispatcher */ |
|
| 112 | - private $eventDispatcher; |
|
| 113 | - |
|
| 114 | - /** @var IShareManager */ |
|
| 115 | - private $shareManager; |
|
| 116 | - |
|
| 117 | - /** |
|
| 118 | - * Return the identifier of this provider. |
|
| 119 | - * |
|
| 120 | - * @return string Containing only [a-zA-Z0-9] |
|
| 121 | - */ |
|
| 122 | - public function identifier() { |
|
| 123 | - return 'ocMailShare'; |
|
| 124 | - } |
|
| 125 | - |
|
| 126 | - public function __construct(IDBConnection $connection, |
|
| 127 | - ISecureRandom $secureRandom, |
|
| 128 | - IUserManager $userManager, |
|
| 129 | - IRootFolder $rootFolder, |
|
| 130 | - IL10N $l, |
|
| 131 | - ILogger $logger, |
|
| 132 | - IMailer $mailer, |
|
| 133 | - IURLGenerator $urlGenerator, |
|
| 134 | - IManager $activityManager, |
|
| 135 | - SettingsManager $settingsManager, |
|
| 136 | - Defaults $defaults, |
|
| 137 | - IHasher $hasher, |
|
| 138 | - IEventDispatcher $eventDispatcher, |
|
| 139 | - IShareManager $shareManager) { |
|
| 140 | - $this->dbConnection = $connection; |
|
| 141 | - $this->secureRandom = $secureRandom; |
|
| 142 | - $this->userManager = $userManager; |
|
| 143 | - $this->rootFolder = $rootFolder; |
|
| 144 | - $this->l = $l; |
|
| 145 | - $this->logger = $logger; |
|
| 146 | - $this->mailer = $mailer; |
|
| 147 | - $this->urlGenerator = $urlGenerator; |
|
| 148 | - $this->activityManager = $activityManager; |
|
| 149 | - $this->settingsManager = $settingsManager; |
|
| 150 | - $this->defaults = $defaults; |
|
| 151 | - $this->hasher = $hasher; |
|
| 152 | - $this->eventDispatcher = $eventDispatcher; |
|
| 153 | - $this->shareManager = $shareManager; |
|
| 154 | - } |
|
| 155 | - |
|
| 156 | - /** |
|
| 157 | - * Share a path |
|
| 158 | - * |
|
| 159 | - * @param IShare $share |
|
| 160 | - * @return IShare The share object |
|
| 161 | - * @throws ShareNotFound |
|
| 162 | - * @throws \Exception |
|
| 163 | - */ |
|
| 164 | - public function create(IShare $share) { |
|
| 165 | - $shareWith = $share->getSharedWith(); |
|
| 166 | - /* |
|
| 75 | + /** @var IDBConnection */ |
|
| 76 | + private $dbConnection; |
|
| 77 | + |
|
| 78 | + /** @var ILogger */ |
|
| 79 | + private $logger; |
|
| 80 | + |
|
| 81 | + /** @var ISecureRandom */ |
|
| 82 | + private $secureRandom; |
|
| 83 | + |
|
| 84 | + /** @var IUserManager */ |
|
| 85 | + private $userManager; |
|
| 86 | + |
|
| 87 | + /** @var IRootFolder */ |
|
| 88 | + private $rootFolder; |
|
| 89 | + |
|
| 90 | + /** @var IL10N */ |
|
| 91 | + private $l; |
|
| 92 | + |
|
| 93 | + /** @var IMailer */ |
|
| 94 | + private $mailer; |
|
| 95 | + |
|
| 96 | + /** @var IURLGenerator */ |
|
| 97 | + private $urlGenerator; |
|
| 98 | + |
|
| 99 | + /** @var IManager */ |
|
| 100 | + private $activityManager; |
|
| 101 | + |
|
| 102 | + /** @var SettingsManager */ |
|
| 103 | + private $settingsManager; |
|
| 104 | + |
|
| 105 | + /** @var Defaults */ |
|
| 106 | + private $defaults; |
|
| 107 | + |
|
| 108 | + /** @var IHasher */ |
|
| 109 | + private $hasher; |
|
| 110 | + |
|
| 111 | + /** @var IEventDispatcher */ |
|
| 112 | + private $eventDispatcher; |
|
| 113 | + |
|
| 114 | + /** @var IShareManager */ |
|
| 115 | + private $shareManager; |
|
| 116 | + |
|
| 117 | + /** |
|
| 118 | + * Return the identifier of this provider. |
|
| 119 | + * |
|
| 120 | + * @return string Containing only [a-zA-Z0-9] |
|
| 121 | + */ |
|
| 122 | + public function identifier() { |
|
| 123 | + return 'ocMailShare'; |
|
| 124 | + } |
|
| 125 | + |
|
| 126 | + public function __construct(IDBConnection $connection, |
|
| 127 | + ISecureRandom $secureRandom, |
|
| 128 | + IUserManager $userManager, |
|
| 129 | + IRootFolder $rootFolder, |
|
| 130 | + IL10N $l, |
|
| 131 | + ILogger $logger, |
|
| 132 | + IMailer $mailer, |
|
| 133 | + IURLGenerator $urlGenerator, |
|
| 134 | + IManager $activityManager, |
|
| 135 | + SettingsManager $settingsManager, |
|
| 136 | + Defaults $defaults, |
|
| 137 | + IHasher $hasher, |
|
| 138 | + IEventDispatcher $eventDispatcher, |
|
| 139 | + IShareManager $shareManager) { |
|
| 140 | + $this->dbConnection = $connection; |
|
| 141 | + $this->secureRandom = $secureRandom; |
|
| 142 | + $this->userManager = $userManager; |
|
| 143 | + $this->rootFolder = $rootFolder; |
|
| 144 | + $this->l = $l; |
|
| 145 | + $this->logger = $logger; |
|
| 146 | + $this->mailer = $mailer; |
|
| 147 | + $this->urlGenerator = $urlGenerator; |
|
| 148 | + $this->activityManager = $activityManager; |
|
| 149 | + $this->settingsManager = $settingsManager; |
|
| 150 | + $this->defaults = $defaults; |
|
| 151 | + $this->hasher = $hasher; |
|
| 152 | + $this->eventDispatcher = $eventDispatcher; |
|
| 153 | + $this->shareManager = $shareManager; |
|
| 154 | + } |
|
| 155 | + |
|
| 156 | + /** |
|
| 157 | + * Share a path |
|
| 158 | + * |
|
| 159 | + * @param IShare $share |
|
| 160 | + * @return IShare The share object |
|
| 161 | + * @throws ShareNotFound |
|
| 162 | + * @throws \Exception |
|
| 163 | + */ |
|
| 164 | + public function create(IShare $share) { |
|
| 165 | + $shareWith = $share->getSharedWith(); |
|
| 166 | + /* |
|
| 167 | 167 | * Check if file is not already shared with the remote user |
| 168 | 168 | */ |
| 169 | - $alreadyShared = $this->getSharedWith($shareWith, IShare::TYPE_EMAIL, $share->getNode(), 1, 0); |
|
| 170 | - if (!empty($alreadyShared)) { |
|
| 171 | - $message = 'Sharing %1$s failed, because this item is already shared with user %2$s'; |
|
| 172 | - $message_t = $this->l->t('Sharing %1$s failed, because this item is already shared with user %2$s', [$share->getNode()->getName(), $shareWith]); |
|
| 173 | - $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); |
|
| 174 | - throw new \Exception($message_t); |
|
| 175 | - } |
|
| 176 | - |
|
| 177 | - // if the admin enforces a password for all mail shares we create a |
|
| 178 | - // random password and send it to the recipient |
|
| 179 | - $password = $share->getPassword() ?: ''; |
|
| 180 | - $passwordEnforced = $this->shareManager->shareApiLinkEnforcePassword(); |
|
| 181 | - if ($passwordEnforced && empty($password)) { |
|
| 182 | - $password = $this->autoGeneratePassword($share); |
|
| 183 | - } |
|
| 184 | - |
|
| 185 | - if (!empty($password)) { |
|
| 186 | - $share->setPassword($this->hasher->hash($password)); |
|
| 187 | - } |
|
| 188 | - |
|
| 189 | - $shareId = $this->createMailShare($share); |
|
| 190 | - $send = $this->sendPassword($share, $password); |
|
| 191 | - if ($passwordEnforced && $send === false) { |
|
| 192 | - $this->sendPasswordToOwner($share, $password); |
|
| 193 | - } |
|
| 194 | - |
|
| 195 | - $this->createShareActivity($share); |
|
| 196 | - $data = $this->getRawShare($shareId); |
|
| 197 | - |
|
| 198 | - return $this->createShareObject($data); |
|
| 199 | - } |
|
| 200 | - |
|
| 201 | - /** |
|
| 202 | - * auto generate password in case of password enforcement on mail shares |
|
| 203 | - * |
|
| 204 | - * @param IShare $share |
|
| 205 | - * @return string |
|
| 206 | - * @throws \Exception |
|
| 207 | - */ |
|
| 208 | - protected function autoGeneratePassword($share) { |
|
| 209 | - $initiatorUser = $this->userManager->get($share->getSharedBy()); |
|
| 210 | - $initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 211 | - $allowPasswordByMail = $this->settingsManager->sendPasswordByMail(); |
|
| 212 | - |
|
| 213 | - if ($initiatorEMailAddress === null && !$allowPasswordByMail) { |
|
| 214 | - throw new \Exception( |
|
| 215 | - $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 216 | - ); |
|
| 217 | - } |
|
| 218 | - |
|
| 219 | - $passwordEvent = new GenerateSecurePasswordEvent(); |
|
| 220 | - $this->eventDispatcher->dispatchTyped($passwordEvent); |
|
| 221 | - |
|
| 222 | - $password = $passwordEvent->getPassword(); |
|
| 223 | - if ($password === null) { |
|
| 224 | - $password = $this->secureRandom->generate(8, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); |
|
| 225 | - } |
|
| 226 | - |
|
| 227 | - return $password; |
|
| 228 | - } |
|
| 229 | - |
|
| 230 | - /** |
|
| 231 | - * create activity if a file/folder was shared by mail |
|
| 232 | - * |
|
| 233 | - * @param IShare $share |
|
| 234 | - * @param string $type |
|
| 235 | - */ |
|
| 236 | - protected function createShareActivity(IShare $share, string $type = 'share') { |
|
| 237 | - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 238 | - |
|
| 239 | - $this->publishActivity( |
|
| 240 | - $type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_SELF : Activity::SUBJECT_UNSHARED_EMAIL_SELF, |
|
| 241 | - [$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()], |
|
| 242 | - $share->getSharedBy(), |
|
| 243 | - $share->getNode()->getId(), |
|
| 244 | - (string) $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 245 | - ); |
|
| 246 | - |
|
| 247 | - if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 248 | - $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); |
|
| 249 | - $fileId = $share->getNode()->getId(); |
|
| 250 | - $nodes = $ownerFolder->getById($fileId); |
|
| 251 | - $ownerPath = $nodes[0]->getPath(); |
|
| 252 | - $this->publishActivity( |
|
| 253 | - $type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_BY : Activity::SUBJECT_UNSHARED_EMAIL_BY, |
|
| 254 | - [$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()], |
|
| 255 | - $share->getShareOwner(), |
|
| 256 | - $fileId, |
|
| 257 | - (string) $ownerFolder->getRelativePath($ownerPath) |
|
| 258 | - ); |
|
| 259 | - } |
|
| 260 | - } |
|
| 261 | - |
|
| 262 | - /** |
|
| 263 | - * create activity if a file/folder was shared by mail |
|
| 264 | - * |
|
| 265 | - * @param IShare $share |
|
| 266 | - * @param string $sharedWith |
|
| 267 | - * @param bool $sendToSelf |
|
| 268 | - */ |
|
| 269 | - protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) { |
|
| 270 | - $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 271 | - |
|
| 272 | - if ($sendToSelf) { |
|
| 273 | - $this->publishActivity( |
|
| 274 | - Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF, |
|
| 275 | - [$userFolder->getRelativePath($share->getNode()->getPath())], |
|
| 276 | - $share->getSharedBy(), |
|
| 277 | - $share->getNode()->getId(), |
|
| 278 | - (string) $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 279 | - ); |
|
| 280 | - } else { |
|
| 281 | - $this->publishActivity( |
|
| 282 | - Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND, |
|
| 283 | - [$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith], |
|
| 284 | - $share->getSharedBy(), |
|
| 285 | - $share->getNode()->getId(), |
|
| 286 | - (string) $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 287 | - ); |
|
| 288 | - } |
|
| 289 | - } |
|
| 290 | - |
|
| 291 | - |
|
| 292 | - /** |
|
| 293 | - * publish activity if a file/folder was shared by mail |
|
| 294 | - * |
|
| 295 | - * @param string $subject |
|
| 296 | - * @param array $parameters |
|
| 297 | - * @param string $affectedUser |
|
| 298 | - * @param int $fileId |
|
| 299 | - * @param string $filePath |
|
| 300 | - */ |
|
| 301 | - protected function publishActivity(string $subject, array $parameters, string $affectedUser, int $fileId, string $filePath) { |
|
| 302 | - $event = $this->activityManager->generateEvent(); |
|
| 303 | - $event->setApp('sharebymail') |
|
| 304 | - ->setType('shared') |
|
| 305 | - ->setSubject($subject, $parameters) |
|
| 306 | - ->setAffectedUser($affectedUser) |
|
| 307 | - ->setObject('files', $fileId, $filePath); |
|
| 308 | - $this->activityManager->publish($event); |
|
| 309 | - } |
|
| 310 | - |
|
| 311 | - /** |
|
| 312 | - * @param IShare $share |
|
| 313 | - * @return int |
|
| 314 | - * @throws \Exception |
|
| 315 | - */ |
|
| 316 | - protected function createMailShare(IShare $share) { |
|
| 317 | - $share->setToken($this->generateToken()); |
|
| 318 | - $shareId = $this->addShareToDB( |
|
| 319 | - $share->getNodeId(), |
|
| 320 | - $share->getNodeType(), |
|
| 321 | - $share->getSharedWith(), |
|
| 322 | - $share->getSharedBy(), |
|
| 323 | - $share->getShareOwner(), |
|
| 324 | - $share->getPermissions(), |
|
| 325 | - $share->getToken(), |
|
| 326 | - $share->getPassword(), |
|
| 327 | - $share->getSendPasswordByTalk(), |
|
| 328 | - $share->getHideDownload(), |
|
| 329 | - $share->getLabel(), |
|
| 330 | - $share->getExpirationDate() |
|
| 331 | - ); |
|
| 332 | - |
|
| 333 | - try { |
|
| 334 | - $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', |
|
| 335 | - ['token' => $share->getToken()]); |
|
| 336 | - $this->sendMailNotification( |
|
| 337 | - $share->getNode()->getName(), |
|
| 338 | - $link, |
|
| 339 | - $share->getSharedBy(), |
|
| 340 | - $share->getSharedWith(), |
|
| 341 | - $share->getExpirationDate() |
|
| 342 | - ); |
|
| 343 | - } catch (HintException $hintException) { |
|
| 344 | - $this->logger->logException($hintException, [ |
|
| 345 | - 'message' => 'Failed to send share by mail.', |
|
| 346 | - 'level' => ILogger::ERROR, |
|
| 347 | - 'app' => 'sharebymail', |
|
| 348 | - ]); |
|
| 349 | - $this->removeShareFromTable($shareId); |
|
| 350 | - throw $hintException; |
|
| 351 | - } catch (\Exception $e) { |
|
| 352 | - $this->logger->logException($e, [ |
|
| 353 | - 'message' => 'Failed to send share by mail.', |
|
| 354 | - 'level' => ILogger::ERROR, |
|
| 355 | - 'app' => 'sharebymail', |
|
| 356 | - ]); |
|
| 357 | - $this->removeShareFromTable($shareId); |
|
| 358 | - throw new HintException('Failed to send share by mail', |
|
| 359 | - $this->l->t('Failed to send share by email')); |
|
| 360 | - } |
|
| 361 | - |
|
| 362 | - return $shareId; |
|
| 363 | - } |
|
| 364 | - |
|
| 365 | - /** |
|
| 366 | - * @param string $filename |
|
| 367 | - * @param string $link |
|
| 368 | - * @param string $initiator |
|
| 369 | - * @param string $shareWith |
|
| 370 | - * @param \DateTime|null $expiration |
|
| 371 | - * @throws \Exception If mail couldn't be sent |
|
| 372 | - */ |
|
| 373 | - protected function sendMailNotification($filename, |
|
| 374 | - $link, |
|
| 375 | - $initiator, |
|
| 376 | - $shareWith, |
|
| 377 | - \DateTime $expiration = null) { |
|
| 378 | - $initiatorUser = $this->userManager->get($initiator); |
|
| 379 | - $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 380 | - $message = $this->mailer->createMessage(); |
|
| 381 | - |
|
| 382 | - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [ |
|
| 383 | - 'filename' => $filename, |
|
| 384 | - 'link' => $link, |
|
| 385 | - 'initiator' => $initiatorDisplayName, |
|
| 386 | - 'expiration' => $expiration, |
|
| 387 | - 'shareWith' => $shareWith, |
|
| 388 | - ]); |
|
| 389 | - |
|
| 390 | - $emailTemplate->setSubject($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename])); |
|
| 391 | - $emailTemplate->addHeader(); |
|
| 392 | - $emailTemplate->addHeading($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]), false); |
|
| 393 | - $text = $this->l->t('%1$s shared »%2$s« with you.', [$initiatorDisplayName, $filename]); |
|
| 394 | - |
|
| 395 | - $emailTemplate->addBodyText( |
|
| 396 | - htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')), |
|
| 397 | - $text |
|
| 398 | - ); |
|
| 399 | - $emailTemplate->addBodyButton( |
|
| 400 | - $this->l->t('Open »%s«', [$filename]), |
|
| 401 | - $link |
|
| 402 | - ); |
|
| 403 | - |
|
| 404 | - $message->setTo([$shareWith]); |
|
| 405 | - |
|
| 406 | - // The "From" contains the sharers name |
|
| 407 | - $instanceName = $this->defaults->getName(); |
|
| 408 | - $senderName = $instanceName; |
|
| 409 | - if ($this->settingsManager->replyToInitiator()) { |
|
| 410 | - $senderName = $this->l->t( |
|
| 411 | - '%1$s via %2$s', |
|
| 412 | - [ |
|
| 413 | - $initiatorDisplayName, |
|
| 414 | - $instanceName |
|
| 415 | - ] |
|
| 416 | - ); |
|
| 417 | - } |
|
| 418 | - $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 419 | - |
|
| 420 | - // The "Reply-To" is set to the sharer if an mail address is configured |
|
| 421 | - // also the default footer contains a "Do not reply" which needs to be adjusted. |
|
| 422 | - $initiatorEmail = $initiatorUser->getEMailAddress(); |
|
| 423 | - if ($this->settingsManager->replyToInitiator() && $initiatorEmail !== null) { |
|
| 424 | - $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]); |
|
| 425 | - $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : '')); |
|
| 426 | - } else { |
|
| 427 | - $emailTemplate->addFooter(); |
|
| 428 | - } |
|
| 429 | - |
|
| 430 | - $message->useTemplate($emailTemplate); |
|
| 431 | - $this->mailer->send($message); |
|
| 432 | - } |
|
| 433 | - |
|
| 434 | - /** |
|
| 435 | - * send password to recipient of a mail share |
|
| 436 | - * |
|
| 437 | - * @param IShare $share |
|
| 438 | - * @param string $password |
|
| 439 | - * @return bool |
|
| 440 | - */ |
|
| 441 | - protected function sendPassword(IShare $share, $password) { |
|
| 442 | - $filename = $share->getNode()->getName(); |
|
| 443 | - $initiator = $share->getSharedBy(); |
|
| 444 | - $shareWith = $share->getSharedWith(); |
|
| 445 | - |
|
| 446 | - if ($password === '' || $this->settingsManager->sendPasswordByMail() === false || $share->getSendPasswordByTalk()) { |
|
| 447 | - return false; |
|
| 448 | - } |
|
| 449 | - |
|
| 450 | - $initiatorUser = $this->userManager->get($initiator); |
|
| 451 | - $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 452 | - $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 453 | - |
|
| 454 | - $plainBodyPart = $this->l->t("%1\$s shared »%2\$s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]); |
|
| 455 | - $htmlBodyPart = $this->l->t('%1$s shared »%2$s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]); |
|
| 456 | - |
|
| 457 | - $message = $this->mailer->createMessage(); |
|
| 458 | - |
|
| 459 | - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [ |
|
| 460 | - 'filename' => $filename, |
|
| 461 | - 'password' => $password, |
|
| 462 | - 'initiator' => $initiatorDisplayName, |
|
| 463 | - 'initiatorEmail' => $initiatorEmailAddress, |
|
| 464 | - 'shareWith' => $shareWith, |
|
| 465 | - ]); |
|
| 466 | - |
|
| 467 | - $emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared to you by %2$s', [$filename, $initiatorDisplayName])); |
|
| 468 | - $emailTemplate->addHeader(); |
|
| 469 | - $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 470 | - $emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart); |
|
| 471 | - $emailTemplate->addBodyText($this->l->t('It is protected with the following password:')); |
|
| 472 | - $emailTemplate->addBodyText($password); |
|
| 473 | - |
|
| 474 | - // The "From" contains the sharers name |
|
| 475 | - $instanceName = $this->defaults->getName(); |
|
| 476 | - $senderName = $instanceName; |
|
| 477 | - if ($this->settingsManager->replyToInitiator()) { |
|
| 478 | - $senderName = $this->l->t( |
|
| 479 | - '%1$s via %2$s', |
|
| 480 | - [ |
|
| 481 | - $initiatorDisplayName, |
|
| 482 | - $instanceName |
|
| 483 | - ] |
|
| 484 | - ); |
|
| 485 | - } |
|
| 486 | - $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 487 | - if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) { |
|
| 488 | - $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); |
|
| 489 | - $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); |
|
| 490 | - } else { |
|
| 491 | - $emailTemplate->addFooter(); |
|
| 492 | - } |
|
| 493 | - |
|
| 494 | - $message->setTo([$shareWith]); |
|
| 495 | - $message->useTemplate($emailTemplate); |
|
| 496 | - $this->mailer->send($message); |
|
| 497 | - |
|
| 498 | - $this->createPasswordSendActivity($share, $shareWith, false); |
|
| 499 | - |
|
| 500 | - return true; |
|
| 501 | - } |
|
| 502 | - |
|
| 503 | - protected function sendNote(IShare $share) { |
|
| 504 | - $recipient = $share->getSharedWith(); |
|
| 505 | - |
|
| 506 | - |
|
| 507 | - $filename = $share->getNode()->getName(); |
|
| 508 | - $initiator = $share->getSharedBy(); |
|
| 509 | - $note = $share->getNote(); |
|
| 510 | - |
|
| 511 | - $initiatorUser = $this->userManager->get($initiator); |
|
| 512 | - $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 513 | - $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 514 | - |
|
| 515 | - $plainHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add:', [$initiatorDisplayName, $filename]); |
|
| 516 | - $htmlHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add', [$initiatorDisplayName, $filename]); |
|
| 517 | - |
|
| 518 | - $message = $this->mailer->createMessage(); |
|
| 519 | - |
|
| 520 | - $emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote'); |
|
| 521 | - |
|
| 522 | - $emailTemplate->setSubject($this->l->t('»%s« added a note to a file shared with you', [$initiatorDisplayName])); |
|
| 523 | - $emailTemplate->addHeader(); |
|
| 524 | - $emailTemplate->addHeading(htmlspecialchars($htmlHeading), $plainHeading); |
|
| 525 | - $emailTemplate->addBodyText(htmlspecialchars($note), $note); |
|
| 526 | - |
|
| 527 | - $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', |
|
| 528 | - ['token' => $share->getToken()]); |
|
| 529 | - $emailTemplate->addBodyButton( |
|
| 530 | - $this->l->t('Open »%s«', [$filename]), |
|
| 531 | - $link |
|
| 532 | - ); |
|
| 533 | - |
|
| 534 | - // The "From" contains the sharers name |
|
| 535 | - $instanceName = $this->defaults->getName(); |
|
| 536 | - $senderName = $instanceName; |
|
| 537 | - if ($this->settingsManager->replyToInitiator()) { |
|
| 538 | - $senderName = $this->l->t( |
|
| 539 | - '%1$s via %2$s', |
|
| 540 | - [ |
|
| 541 | - $initiatorDisplayName, |
|
| 542 | - $instanceName |
|
| 543 | - ] |
|
| 544 | - ); |
|
| 545 | - } |
|
| 546 | - $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 547 | - if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) { |
|
| 548 | - $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); |
|
| 549 | - $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); |
|
| 550 | - } else { |
|
| 551 | - $emailTemplate->addFooter(); |
|
| 552 | - } |
|
| 553 | - |
|
| 554 | - $message->setTo([$recipient]); |
|
| 555 | - $message->useTemplate($emailTemplate); |
|
| 556 | - $this->mailer->send($message); |
|
| 557 | - } |
|
| 558 | - |
|
| 559 | - /** |
|
| 560 | - * send auto generated password to the owner. This happens if the admin enforces |
|
| 561 | - * a password for mail shares and forbid to send the password by mail to the recipient |
|
| 562 | - * |
|
| 563 | - * @param IShare $share |
|
| 564 | - * @param string $password |
|
| 565 | - * @return bool |
|
| 566 | - * @throws \Exception |
|
| 567 | - */ |
|
| 568 | - protected function sendPasswordToOwner(IShare $share, $password) { |
|
| 569 | - $filename = $share->getNode()->getName(); |
|
| 570 | - $initiator = $this->userManager->get($share->getSharedBy()); |
|
| 571 | - $initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null; |
|
| 572 | - $initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy(); |
|
| 573 | - $shareWith = $share->getSharedWith(); |
|
| 574 | - |
|
| 575 | - if ($initiatorEMailAddress === null) { |
|
| 576 | - throw new \Exception( |
|
| 577 | - $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 578 | - ); |
|
| 579 | - } |
|
| 580 | - |
|
| 581 | - $bodyPart = $this->l->t('You just shared »%1$s« with %2$s. The share was already sent to the recipient. Due to the security policies defined by the administrator of %3$s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.', [$filename, $shareWith, $this->defaults->getName()]); |
|
| 582 | - |
|
| 583 | - $message = $this->mailer->createMessage(); |
|
| 584 | - $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [ |
|
| 585 | - 'filename' => $filename, |
|
| 586 | - 'password' => $password, |
|
| 587 | - 'initiator' => $initiatorDisplayName, |
|
| 588 | - 'initiatorEmail' => $initiatorEMailAddress, |
|
| 589 | - 'shareWith' => $shareWith, |
|
| 590 | - ]); |
|
| 591 | - |
|
| 592 | - $emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared by you with %2$s', [$filename, $shareWith])); |
|
| 593 | - $emailTemplate->addHeader(); |
|
| 594 | - $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 595 | - $emailTemplate->addBodyText($bodyPart); |
|
| 596 | - $emailTemplate->addBodyText($this->l->t('This is the password:')); |
|
| 597 | - $emailTemplate->addBodyText($password); |
|
| 598 | - $emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.')); |
|
| 599 | - $emailTemplate->addFooter(); |
|
| 600 | - |
|
| 601 | - $instanceName = $this->defaults->getName(); |
|
| 602 | - $senderName = $this->l->t( |
|
| 603 | - '%1$s via %2$s', |
|
| 604 | - [ |
|
| 605 | - $initiatorDisplayName, |
|
| 606 | - $instanceName |
|
| 607 | - ] |
|
| 608 | - ); |
|
| 609 | - $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 610 | - $message->setTo([$initiatorEMailAddress => $initiatorDisplayName]); |
|
| 611 | - $message->useTemplate($emailTemplate); |
|
| 612 | - $this->mailer->send($message); |
|
| 613 | - |
|
| 614 | - $this->createPasswordSendActivity($share, $shareWith, true); |
|
| 615 | - |
|
| 616 | - return true; |
|
| 617 | - } |
|
| 618 | - |
|
| 619 | - /** |
|
| 620 | - * generate share token |
|
| 621 | - * |
|
| 622 | - * @return string |
|
| 623 | - */ |
|
| 624 | - protected function generateToken($size = 15) { |
|
| 625 | - $token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE); |
|
| 626 | - return $token; |
|
| 627 | - } |
|
| 628 | - |
|
| 629 | - /** |
|
| 630 | - * Get all children of this share |
|
| 631 | - * |
|
| 632 | - * @param IShare $parent |
|
| 633 | - * @return IShare[] |
|
| 634 | - */ |
|
| 635 | - public function getChildren(IShare $parent) { |
|
| 636 | - $children = []; |
|
| 637 | - |
|
| 638 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 639 | - $qb->select('*') |
|
| 640 | - ->from('share') |
|
| 641 | - ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) |
|
| 642 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 643 | - ->orderBy('id'); |
|
| 644 | - |
|
| 645 | - $cursor = $qb->executeQuery(); |
|
| 646 | - while ($data = $cursor->fetch()) { |
|
| 647 | - $children[] = $this->createShareObject($data); |
|
| 648 | - } |
|
| 649 | - $cursor->closeCursor(); |
|
| 650 | - |
|
| 651 | - return $children; |
|
| 652 | - } |
|
| 653 | - |
|
| 654 | - /** |
|
| 655 | - * add share to the database and return the ID |
|
| 656 | - * |
|
| 657 | - * @param int $itemSource |
|
| 658 | - * @param string $itemType |
|
| 659 | - * @param string $shareWith |
|
| 660 | - * @param string $sharedBy |
|
| 661 | - * @param string $uidOwner |
|
| 662 | - * @param int $permissions |
|
| 663 | - * @param string $token |
|
| 664 | - * @param string $password |
|
| 665 | - * @param bool $sendPasswordByTalk |
|
| 666 | - * @param bool $hideDownload |
|
| 667 | - * @param string $label |
|
| 668 | - * @param \DateTime|null $expirationTime |
|
| 669 | - * @return int |
|
| 670 | - */ |
|
| 671 | - protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password, $sendPasswordByTalk, $hideDownload, $label, $expirationTime) { |
|
| 672 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 673 | - $qb->insert('share') |
|
| 674 | - ->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)) |
|
| 675 | - ->setValue('item_type', $qb->createNamedParameter($itemType)) |
|
| 676 | - ->setValue('item_source', $qb->createNamedParameter($itemSource)) |
|
| 677 | - ->setValue('file_source', $qb->createNamedParameter($itemSource)) |
|
| 678 | - ->setValue('share_with', $qb->createNamedParameter($shareWith)) |
|
| 679 | - ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) |
|
| 680 | - ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) |
|
| 681 | - ->setValue('permissions', $qb->createNamedParameter($permissions)) |
|
| 682 | - ->setValue('token', $qb->createNamedParameter($token)) |
|
| 683 | - ->setValue('password', $qb->createNamedParameter($password)) |
|
| 684 | - ->setValue('password_by_talk', $qb->createNamedParameter($sendPasswordByTalk, IQueryBuilder::PARAM_BOOL)) |
|
| 685 | - ->setValue('stime', $qb->createNamedParameter(time())) |
|
| 686 | - ->setValue('hide_download', $qb->createNamedParameter((int)$hideDownload, IQueryBuilder::PARAM_INT)) |
|
| 687 | - ->setValue('label', $qb->createNamedParameter($label)); |
|
| 688 | - |
|
| 689 | - if ($expirationTime !== null) { |
|
| 690 | - $qb->setValue('expiration', $qb->createNamedParameter($expirationTime, IQueryBuilder::PARAM_DATE)); |
|
| 691 | - } |
|
| 692 | - |
|
| 693 | - /* |
|
| 169 | + $alreadyShared = $this->getSharedWith($shareWith, IShare::TYPE_EMAIL, $share->getNode(), 1, 0); |
|
| 170 | + if (!empty($alreadyShared)) { |
|
| 171 | + $message = 'Sharing %1$s failed, because this item is already shared with user %2$s'; |
|
| 172 | + $message_t = $this->l->t('Sharing %1$s failed, because this item is already shared with user %2$s', [$share->getNode()->getName(), $shareWith]); |
|
| 173 | + $this->logger->debug(sprintf($message, $share->getNode()->getName(), $shareWith), ['app' => 'Federated File Sharing']); |
|
| 174 | + throw new \Exception($message_t); |
|
| 175 | + } |
|
| 176 | + |
|
| 177 | + // if the admin enforces a password for all mail shares we create a |
|
| 178 | + // random password and send it to the recipient |
|
| 179 | + $password = $share->getPassword() ?: ''; |
|
| 180 | + $passwordEnforced = $this->shareManager->shareApiLinkEnforcePassword(); |
|
| 181 | + if ($passwordEnforced && empty($password)) { |
|
| 182 | + $password = $this->autoGeneratePassword($share); |
|
| 183 | + } |
|
| 184 | + |
|
| 185 | + if (!empty($password)) { |
|
| 186 | + $share->setPassword($this->hasher->hash($password)); |
|
| 187 | + } |
|
| 188 | + |
|
| 189 | + $shareId = $this->createMailShare($share); |
|
| 190 | + $send = $this->sendPassword($share, $password); |
|
| 191 | + if ($passwordEnforced && $send === false) { |
|
| 192 | + $this->sendPasswordToOwner($share, $password); |
|
| 193 | + } |
|
| 194 | + |
|
| 195 | + $this->createShareActivity($share); |
|
| 196 | + $data = $this->getRawShare($shareId); |
|
| 197 | + |
|
| 198 | + return $this->createShareObject($data); |
|
| 199 | + } |
|
| 200 | + |
|
| 201 | + /** |
|
| 202 | + * auto generate password in case of password enforcement on mail shares |
|
| 203 | + * |
|
| 204 | + * @param IShare $share |
|
| 205 | + * @return string |
|
| 206 | + * @throws \Exception |
|
| 207 | + */ |
|
| 208 | + protected function autoGeneratePassword($share) { |
|
| 209 | + $initiatorUser = $this->userManager->get($share->getSharedBy()); |
|
| 210 | + $initiatorEMailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 211 | + $allowPasswordByMail = $this->settingsManager->sendPasswordByMail(); |
|
| 212 | + |
|
| 213 | + if ($initiatorEMailAddress === null && !$allowPasswordByMail) { |
|
| 214 | + throw new \Exception( |
|
| 215 | + $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 216 | + ); |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + $passwordEvent = new GenerateSecurePasswordEvent(); |
|
| 220 | + $this->eventDispatcher->dispatchTyped($passwordEvent); |
|
| 221 | + |
|
| 222 | + $password = $passwordEvent->getPassword(); |
|
| 223 | + if ($password === null) { |
|
| 224 | + $password = $this->secureRandom->generate(8, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_DIGITS); |
|
| 225 | + } |
|
| 226 | + |
|
| 227 | + return $password; |
|
| 228 | + } |
|
| 229 | + |
|
| 230 | + /** |
|
| 231 | + * create activity if a file/folder was shared by mail |
|
| 232 | + * |
|
| 233 | + * @param IShare $share |
|
| 234 | + * @param string $type |
|
| 235 | + */ |
|
| 236 | + protected function createShareActivity(IShare $share, string $type = 'share') { |
|
| 237 | + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 238 | + |
|
| 239 | + $this->publishActivity( |
|
| 240 | + $type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_SELF : Activity::SUBJECT_UNSHARED_EMAIL_SELF, |
|
| 241 | + [$userFolder->getRelativePath($share->getNode()->getPath()), $share->getSharedWith()], |
|
| 242 | + $share->getSharedBy(), |
|
| 243 | + $share->getNode()->getId(), |
|
| 244 | + (string) $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 245 | + ); |
|
| 246 | + |
|
| 247 | + if ($share->getShareOwner() !== $share->getSharedBy()) { |
|
| 248 | + $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner()); |
|
| 249 | + $fileId = $share->getNode()->getId(); |
|
| 250 | + $nodes = $ownerFolder->getById($fileId); |
|
| 251 | + $ownerPath = $nodes[0]->getPath(); |
|
| 252 | + $this->publishActivity( |
|
| 253 | + $type === 'share' ? Activity::SUBJECT_SHARED_EMAIL_BY : Activity::SUBJECT_UNSHARED_EMAIL_BY, |
|
| 254 | + [$ownerFolder->getRelativePath($ownerPath), $share->getSharedWith(), $share->getSharedBy()], |
|
| 255 | + $share->getShareOwner(), |
|
| 256 | + $fileId, |
|
| 257 | + (string) $ownerFolder->getRelativePath($ownerPath) |
|
| 258 | + ); |
|
| 259 | + } |
|
| 260 | + } |
|
| 261 | + |
|
| 262 | + /** |
|
| 263 | + * create activity if a file/folder was shared by mail |
|
| 264 | + * |
|
| 265 | + * @param IShare $share |
|
| 266 | + * @param string $sharedWith |
|
| 267 | + * @param bool $sendToSelf |
|
| 268 | + */ |
|
| 269 | + protected function createPasswordSendActivity(IShare $share, $sharedWith, $sendToSelf) { |
|
| 270 | + $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy()); |
|
| 271 | + |
|
| 272 | + if ($sendToSelf) { |
|
| 273 | + $this->publishActivity( |
|
| 274 | + Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND_SELF, |
|
| 275 | + [$userFolder->getRelativePath($share->getNode()->getPath())], |
|
| 276 | + $share->getSharedBy(), |
|
| 277 | + $share->getNode()->getId(), |
|
| 278 | + (string) $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 279 | + ); |
|
| 280 | + } else { |
|
| 281 | + $this->publishActivity( |
|
| 282 | + Activity::SUBJECT_SHARED_EMAIL_PASSWORD_SEND, |
|
| 283 | + [$userFolder->getRelativePath($share->getNode()->getPath()), $sharedWith], |
|
| 284 | + $share->getSharedBy(), |
|
| 285 | + $share->getNode()->getId(), |
|
| 286 | + (string) $userFolder->getRelativePath($share->getNode()->getPath()) |
|
| 287 | + ); |
|
| 288 | + } |
|
| 289 | + } |
|
| 290 | + |
|
| 291 | + |
|
| 292 | + /** |
|
| 293 | + * publish activity if a file/folder was shared by mail |
|
| 294 | + * |
|
| 295 | + * @param string $subject |
|
| 296 | + * @param array $parameters |
|
| 297 | + * @param string $affectedUser |
|
| 298 | + * @param int $fileId |
|
| 299 | + * @param string $filePath |
|
| 300 | + */ |
|
| 301 | + protected function publishActivity(string $subject, array $parameters, string $affectedUser, int $fileId, string $filePath) { |
|
| 302 | + $event = $this->activityManager->generateEvent(); |
|
| 303 | + $event->setApp('sharebymail') |
|
| 304 | + ->setType('shared') |
|
| 305 | + ->setSubject($subject, $parameters) |
|
| 306 | + ->setAffectedUser($affectedUser) |
|
| 307 | + ->setObject('files', $fileId, $filePath); |
|
| 308 | + $this->activityManager->publish($event); |
|
| 309 | + } |
|
| 310 | + |
|
| 311 | + /** |
|
| 312 | + * @param IShare $share |
|
| 313 | + * @return int |
|
| 314 | + * @throws \Exception |
|
| 315 | + */ |
|
| 316 | + protected function createMailShare(IShare $share) { |
|
| 317 | + $share->setToken($this->generateToken()); |
|
| 318 | + $shareId = $this->addShareToDB( |
|
| 319 | + $share->getNodeId(), |
|
| 320 | + $share->getNodeType(), |
|
| 321 | + $share->getSharedWith(), |
|
| 322 | + $share->getSharedBy(), |
|
| 323 | + $share->getShareOwner(), |
|
| 324 | + $share->getPermissions(), |
|
| 325 | + $share->getToken(), |
|
| 326 | + $share->getPassword(), |
|
| 327 | + $share->getSendPasswordByTalk(), |
|
| 328 | + $share->getHideDownload(), |
|
| 329 | + $share->getLabel(), |
|
| 330 | + $share->getExpirationDate() |
|
| 331 | + ); |
|
| 332 | + |
|
| 333 | + try { |
|
| 334 | + $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', |
|
| 335 | + ['token' => $share->getToken()]); |
|
| 336 | + $this->sendMailNotification( |
|
| 337 | + $share->getNode()->getName(), |
|
| 338 | + $link, |
|
| 339 | + $share->getSharedBy(), |
|
| 340 | + $share->getSharedWith(), |
|
| 341 | + $share->getExpirationDate() |
|
| 342 | + ); |
|
| 343 | + } catch (HintException $hintException) { |
|
| 344 | + $this->logger->logException($hintException, [ |
|
| 345 | + 'message' => 'Failed to send share by mail.', |
|
| 346 | + 'level' => ILogger::ERROR, |
|
| 347 | + 'app' => 'sharebymail', |
|
| 348 | + ]); |
|
| 349 | + $this->removeShareFromTable($shareId); |
|
| 350 | + throw $hintException; |
|
| 351 | + } catch (\Exception $e) { |
|
| 352 | + $this->logger->logException($e, [ |
|
| 353 | + 'message' => 'Failed to send share by mail.', |
|
| 354 | + 'level' => ILogger::ERROR, |
|
| 355 | + 'app' => 'sharebymail', |
|
| 356 | + ]); |
|
| 357 | + $this->removeShareFromTable($shareId); |
|
| 358 | + throw new HintException('Failed to send share by mail', |
|
| 359 | + $this->l->t('Failed to send share by email')); |
|
| 360 | + } |
|
| 361 | + |
|
| 362 | + return $shareId; |
|
| 363 | + } |
|
| 364 | + |
|
| 365 | + /** |
|
| 366 | + * @param string $filename |
|
| 367 | + * @param string $link |
|
| 368 | + * @param string $initiator |
|
| 369 | + * @param string $shareWith |
|
| 370 | + * @param \DateTime|null $expiration |
|
| 371 | + * @throws \Exception If mail couldn't be sent |
|
| 372 | + */ |
|
| 373 | + protected function sendMailNotification($filename, |
|
| 374 | + $link, |
|
| 375 | + $initiator, |
|
| 376 | + $shareWith, |
|
| 377 | + \DateTime $expiration = null) { |
|
| 378 | + $initiatorUser = $this->userManager->get($initiator); |
|
| 379 | + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 380 | + $message = $this->mailer->createMessage(); |
|
| 381 | + |
|
| 382 | + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientNotification', [ |
|
| 383 | + 'filename' => $filename, |
|
| 384 | + 'link' => $link, |
|
| 385 | + 'initiator' => $initiatorDisplayName, |
|
| 386 | + 'expiration' => $expiration, |
|
| 387 | + 'shareWith' => $shareWith, |
|
| 388 | + ]); |
|
| 389 | + |
|
| 390 | + $emailTemplate->setSubject($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename])); |
|
| 391 | + $emailTemplate->addHeader(); |
|
| 392 | + $emailTemplate->addHeading($this->l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]), false); |
|
| 393 | + $text = $this->l->t('%1$s shared »%2$s« with you.', [$initiatorDisplayName, $filename]); |
|
| 394 | + |
|
| 395 | + $emailTemplate->addBodyText( |
|
| 396 | + htmlspecialchars($text . ' ' . $this->l->t('Click the button below to open it.')), |
|
| 397 | + $text |
|
| 398 | + ); |
|
| 399 | + $emailTemplate->addBodyButton( |
|
| 400 | + $this->l->t('Open »%s«', [$filename]), |
|
| 401 | + $link |
|
| 402 | + ); |
|
| 403 | + |
|
| 404 | + $message->setTo([$shareWith]); |
|
| 405 | + |
|
| 406 | + // The "From" contains the sharers name |
|
| 407 | + $instanceName = $this->defaults->getName(); |
|
| 408 | + $senderName = $instanceName; |
|
| 409 | + if ($this->settingsManager->replyToInitiator()) { |
|
| 410 | + $senderName = $this->l->t( |
|
| 411 | + '%1$s via %2$s', |
|
| 412 | + [ |
|
| 413 | + $initiatorDisplayName, |
|
| 414 | + $instanceName |
|
| 415 | + ] |
|
| 416 | + ); |
|
| 417 | + } |
|
| 418 | + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 419 | + |
|
| 420 | + // The "Reply-To" is set to the sharer if an mail address is configured |
|
| 421 | + // also the default footer contains a "Do not reply" which needs to be adjusted. |
|
| 422 | + $initiatorEmail = $initiatorUser->getEMailAddress(); |
|
| 423 | + if ($this->settingsManager->replyToInitiator() && $initiatorEmail !== null) { |
|
| 424 | + $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]); |
|
| 425 | + $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan() !== '' ? ' - ' . $this->defaults->getSlogan() : '')); |
|
| 426 | + } else { |
|
| 427 | + $emailTemplate->addFooter(); |
|
| 428 | + } |
|
| 429 | + |
|
| 430 | + $message->useTemplate($emailTemplate); |
|
| 431 | + $this->mailer->send($message); |
|
| 432 | + } |
|
| 433 | + |
|
| 434 | + /** |
|
| 435 | + * send password to recipient of a mail share |
|
| 436 | + * |
|
| 437 | + * @param IShare $share |
|
| 438 | + * @param string $password |
|
| 439 | + * @return bool |
|
| 440 | + */ |
|
| 441 | + protected function sendPassword(IShare $share, $password) { |
|
| 442 | + $filename = $share->getNode()->getName(); |
|
| 443 | + $initiator = $share->getSharedBy(); |
|
| 444 | + $shareWith = $share->getSharedWith(); |
|
| 445 | + |
|
| 446 | + if ($password === '' || $this->settingsManager->sendPasswordByMail() === false || $share->getSendPasswordByTalk()) { |
|
| 447 | + return false; |
|
| 448 | + } |
|
| 449 | + |
|
| 450 | + $initiatorUser = $this->userManager->get($initiator); |
|
| 451 | + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 452 | + $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 453 | + |
|
| 454 | + $plainBodyPart = $this->l->t("%1\$s shared »%2\$s« with you.\nYou should have already received a separate mail with a link to access it.\n", [$initiatorDisplayName, $filename]); |
|
| 455 | + $htmlBodyPart = $this->l->t('%1$s shared »%2$s« with you. You should have already received a separate mail with a link to access it.', [$initiatorDisplayName, $filename]); |
|
| 456 | + |
|
| 457 | + $message = $this->mailer->createMessage(); |
|
| 458 | + |
|
| 459 | + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.RecipientPasswordNotification', [ |
|
| 460 | + 'filename' => $filename, |
|
| 461 | + 'password' => $password, |
|
| 462 | + 'initiator' => $initiatorDisplayName, |
|
| 463 | + 'initiatorEmail' => $initiatorEmailAddress, |
|
| 464 | + 'shareWith' => $shareWith, |
|
| 465 | + ]); |
|
| 466 | + |
|
| 467 | + $emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared to you by %2$s', [$filename, $initiatorDisplayName])); |
|
| 468 | + $emailTemplate->addHeader(); |
|
| 469 | + $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 470 | + $emailTemplate->addBodyText(htmlspecialchars($htmlBodyPart), $plainBodyPart); |
|
| 471 | + $emailTemplate->addBodyText($this->l->t('It is protected with the following password:')); |
|
| 472 | + $emailTemplate->addBodyText($password); |
|
| 473 | + |
|
| 474 | + // The "From" contains the sharers name |
|
| 475 | + $instanceName = $this->defaults->getName(); |
|
| 476 | + $senderName = $instanceName; |
|
| 477 | + if ($this->settingsManager->replyToInitiator()) { |
|
| 478 | + $senderName = $this->l->t( |
|
| 479 | + '%1$s via %2$s', |
|
| 480 | + [ |
|
| 481 | + $initiatorDisplayName, |
|
| 482 | + $instanceName |
|
| 483 | + ] |
|
| 484 | + ); |
|
| 485 | + } |
|
| 486 | + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 487 | + if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) { |
|
| 488 | + $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); |
|
| 489 | + $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); |
|
| 490 | + } else { |
|
| 491 | + $emailTemplate->addFooter(); |
|
| 492 | + } |
|
| 493 | + |
|
| 494 | + $message->setTo([$shareWith]); |
|
| 495 | + $message->useTemplate($emailTemplate); |
|
| 496 | + $this->mailer->send($message); |
|
| 497 | + |
|
| 498 | + $this->createPasswordSendActivity($share, $shareWith, false); |
|
| 499 | + |
|
| 500 | + return true; |
|
| 501 | + } |
|
| 502 | + |
|
| 503 | + protected function sendNote(IShare $share) { |
|
| 504 | + $recipient = $share->getSharedWith(); |
|
| 505 | + |
|
| 506 | + |
|
| 507 | + $filename = $share->getNode()->getName(); |
|
| 508 | + $initiator = $share->getSharedBy(); |
|
| 509 | + $note = $share->getNote(); |
|
| 510 | + |
|
| 511 | + $initiatorUser = $this->userManager->get($initiator); |
|
| 512 | + $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator; |
|
| 513 | + $initiatorEmailAddress = ($initiatorUser instanceof IUser) ? $initiatorUser->getEMailAddress() : null; |
|
| 514 | + |
|
| 515 | + $plainHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add:', [$initiatorDisplayName, $filename]); |
|
| 516 | + $htmlHeading = $this->l->t('%1$s shared »%2$s« with you and wants to add', [$initiatorDisplayName, $filename]); |
|
| 517 | + |
|
| 518 | + $message = $this->mailer->createMessage(); |
|
| 519 | + |
|
| 520 | + $emailTemplate = $this->mailer->createEMailTemplate('shareByMail.sendNote'); |
|
| 521 | + |
|
| 522 | + $emailTemplate->setSubject($this->l->t('»%s« added a note to a file shared with you', [$initiatorDisplayName])); |
|
| 523 | + $emailTemplate->addHeader(); |
|
| 524 | + $emailTemplate->addHeading(htmlspecialchars($htmlHeading), $plainHeading); |
|
| 525 | + $emailTemplate->addBodyText(htmlspecialchars($note), $note); |
|
| 526 | + |
|
| 527 | + $link = $this->urlGenerator->linkToRouteAbsolute('files_sharing.sharecontroller.showShare', |
|
| 528 | + ['token' => $share->getToken()]); |
|
| 529 | + $emailTemplate->addBodyButton( |
|
| 530 | + $this->l->t('Open »%s«', [$filename]), |
|
| 531 | + $link |
|
| 532 | + ); |
|
| 533 | + |
|
| 534 | + // The "From" contains the sharers name |
|
| 535 | + $instanceName = $this->defaults->getName(); |
|
| 536 | + $senderName = $instanceName; |
|
| 537 | + if ($this->settingsManager->replyToInitiator()) { |
|
| 538 | + $senderName = $this->l->t( |
|
| 539 | + '%1$s via %2$s', |
|
| 540 | + [ |
|
| 541 | + $initiatorDisplayName, |
|
| 542 | + $instanceName |
|
| 543 | + ] |
|
| 544 | + ); |
|
| 545 | + } |
|
| 546 | + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 547 | + if ($this->settingsManager->replyToInitiator() && $initiatorEmailAddress !== null) { |
|
| 548 | + $message->setReplyTo([$initiatorEmailAddress => $initiatorDisplayName]); |
|
| 549 | + $emailTemplate->addFooter($instanceName . ' - ' . $this->defaults->getSlogan()); |
|
| 550 | + } else { |
|
| 551 | + $emailTemplate->addFooter(); |
|
| 552 | + } |
|
| 553 | + |
|
| 554 | + $message->setTo([$recipient]); |
|
| 555 | + $message->useTemplate($emailTemplate); |
|
| 556 | + $this->mailer->send($message); |
|
| 557 | + } |
|
| 558 | + |
|
| 559 | + /** |
|
| 560 | + * send auto generated password to the owner. This happens if the admin enforces |
|
| 561 | + * a password for mail shares and forbid to send the password by mail to the recipient |
|
| 562 | + * |
|
| 563 | + * @param IShare $share |
|
| 564 | + * @param string $password |
|
| 565 | + * @return bool |
|
| 566 | + * @throws \Exception |
|
| 567 | + */ |
|
| 568 | + protected function sendPasswordToOwner(IShare $share, $password) { |
|
| 569 | + $filename = $share->getNode()->getName(); |
|
| 570 | + $initiator = $this->userManager->get($share->getSharedBy()); |
|
| 571 | + $initiatorEMailAddress = ($initiator instanceof IUser) ? $initiator->getEMailAddress() : null; |
|
| 572 | + $initiatorDisplayName = ($initiator instanceof IUser) ? $initiator->getDisplayName() : $share->getSharedBy(); |
|
| 573 | + $shareWith = $share->getSharedWith(); |
|
| 574 | + |
|
| 575 | + if ($initiatorEMailAddress === null) { |
|
| 576 | + throw new \Exception( |
|
| 577 | + $this->l->t("We can't send you the auto-generated password. Please set a valid email address in your personal settings and try again.") |
|
| 578 | + ); |
|
| 579 | + } |
|
| 580 | + |
|
| 581 | + $bodyPart = $this->l->t('You just shared »%1$s« with %2$s. The share was already sent to the recipient. Due to the security policies defined by the administrator of %3$s each share needs to be protected by password and it is not allowed to send the password directly to the recipient. Therefore you need to forward the password manually to the recipient.', [$filename, $shareWith, $this->defaults->getName()]); |
|
| 582 | + |
|
| 583 | + $message = $this->mailer->createMessage(); |
|
| 584 | + $emailTemplate = $this->mailer->createEMailTemplate('sharebymail.OwnerPasswordNotification', [ |
|
| 585 | + 'filename' => $filename, |
|
| 586 | + 'password' => $password, |
|
| 587 | + 'initiator' => $initiatorDisplayName, |
|
| 588 | + 'initiatorEmail' => $initiatorEMailAddress, |
|
| 589 | + 'shareWith' => $shareWith, |
|
| 590 | + ]); |
|
| 591 | + |
|
| 592 | + $emailTemplate->setSubject($this->l->t('Password to access »%1$s« shared by you with %2$s', [$filename, $shareWith])); |
|
| 593 | + $emailTemplate->addHeader(); |
|
| 594 | + $emailTemplate->addHeading($this->l->t('Password to access »%s«', [$filename]), false); |
|
| 595 | + $emailTemplate->addBodyText($bodyPart); |
|
| 596 | + $emailTemplate->addBodyText($this->l->t('This is the password:')); |
|
| 597 | + $emailTemplate->addBodyText($password); |
|
| 598 | + $emailTemplate->addBodyText($this->l->t('You can choose a different password at any time in the share dialog.')); |
|
| 599 | + $emailTemplate->addFooter(); |
|
| 600 | + |
|
| 601 | + $instanceName = $this->defaults->getName(); |
|
| 602 | + $senderName = $this->l->t( |
|
| 603 | + '%1$s via %2$s', |
|
| 604 | + [ |
|
| 605 | + $initiatorDisplayName, |
|
| 606 | + $instanceName |
|
| 607 | + ] |
|
| 608 | + ); |
|
| 609 | + $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]); |
|
| 610 | + $message->setTo([$initiatorEMailAddress => $initiatorDisplayName]); |
|
| 611 | + $message->useTemplate($emailTemplate); |
|
| 612 | + $this->mailer->send($message); |
|
| 613 | + |
|
| 614 | + $this->createPasswordSendActivity($share, $shareWith, true); |
|
| 615 | + |
|
| 616 | + return true; |
|
| 617 | + } |
|
| 618 | + |
|
| 619 | + /** |
|
| 620 | + * generate share token |
|
| 621 | + * |
|
| 622 | + * @return string |
|
| 623 | + */ |
|
| 624 | + protected function generateToken($size = 15) { |
|
| 625 | + $token = $this->secureRandom->generate($size, ISecureRandom::CHAR_HUMAN_READABLE); |
|
| 626 | + return $token; |
|
| 627 | + } |
|
| 628 | + |
|
| 629 | + /** |
|
| 630 | + * Get all children of this share |
|
| 631 | + * |
|
| 632 | + * @param IShare $parent |
|
| 633 | + * @return IShare[] |
|
| 634 | + */ |
|
| 635 | + public function getChildren(IShare $parent) { |
|
| 636 | + $children = []; |
|
| 637 | + |
|
| 638 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 639 | + $qb->select('*') |
|
| 640 | + ->from('share') |
|
| 641 | + ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) |
|
| 642 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 643 | + ->orderBy('id'); |
|
| 644 | + |
|
| 645 | + $cursor = $qb->executeQuery(); |
|
| 646 | + while ($data = $cursor->fetch()) { |
|
| 647 | + $children[] = $this->createShareObject($data); |
|
| 648 | + } |
|
| 649 | + $cursor->closeCursor(); |
|
| 650 | + |
|
| 651 | + return $children; |
|
| 652 | + } |
|
| 653 | + |
|
| 654 | + /** |
|
| 655 | + * add share to the database and return the ID |
|
| 656 | + * |
|
| 657 | + * @param int $itemSource |
|
| 658 | + * @param string $itemType |
|
| 659 | + * @param string $shareWith |
|
| 660 | + * @param string $sharedBy |
|
| 661 | + * @param string $uidOwner |
|
| 662 | + * @param int $permissions |
|
| 663 | + * @param string $token |
|
| 664 | + * @param string $password |
|
| 665 | + * @param bool $sendPasswordByTalk |
|
| 666 | + * @param bool $hideDownload |
|
| 667 | + * @param string $label |
|
| 668 | + * @param \DateTime|null $expirationTime |
|
| 669 | + * @return int |
|
| 670 | + */ |
|
| 671 | + protected function addShareToDB($itemSource, $itemType, $shareWith, $sharedBy, $uidOwner, $permissions, $token, $password, $sendPasswordByTalk, $hideDownload, $label, $expirationTime) { |
|
| 672 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 673 | + $qb->insert('share') |
|
| 674 | + ->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)) |
|
| 675 | + ->setValue('item_type', $qb->createNamedParameter($itemType)) |
|
| 676 | + ->setValue('item_source', $qb->createNamedParameter($itemSource)) |
|
| 677 | + ->setValue('file_source', $qb->createNamedParameter($itemSource)) |
|
| 678 | + ->setValue('share_with', $qb->createNamedParameter($shareWith)) |
|
| 679 | + ->setValue('uid_owner', $qb->createNamedParameter($uidOwner)) |
|
| 680 | + ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) |
|
| 681 | + ->setValue('permissions', $qb->createNamedParameter($permissions)) |
|
| 682 | + ->setValue('token', $qb->createNamedParameter($token)) |
|
| 683 | + ->setValue('password', $qb->createNamedParameter($password)) |
|
| 684 | + ->setValue('password_by_talk', $qb->createNamedParameter($sendPasswordByTalk, IQueryBuilder::PARAM_BOOL)) |
|
| 685 | + ->setValue('stime', $qb->createNamedParameter(time())) |
|
| 686 | + ->setValue('hide_download', $qb->createNamedParameter((int)$hideDownload, IQueryBuilder::PARAM_INT)) |
|
| 687 | + ->setValue('label', $qb->createNamedParameter($label)); |
|
| 688 | + |
|
| 689 | + if ($expirationTime !== null) { |
|
| 690 | + $qb->setValue('expiration', $qb->createNamedParameter($expirationTime, IQueryBuilder::PARAM_DATE)); |
|
| 691 | + } |
|
| 692 | + |
|
| 693 | + /* |
|
| 694 | 694 | * Added to fix https://github.com/owncloud/core/issues/22215 |
| 695 | 695 | * Can be removed once we get rid of ajax/share.php |
| 696 | 696 | */ |
| 697 | - $qb->setValue('file_target', $qb->createNamedParameter('')); |
|
| 698 | - |
|
| 699 | - $qb->executeStatement(); |
|
| 700 | - return $qb->getLastInsertId(); |
|
| 701 | - } |
|
| 702 | - |
|
| 703 | - /** |
|
| 704 | - * Update a share |
|
| 705 | - * |
|
| 706 | - * @param IShare $share |
|
| 707 | - * @param string|null $plainTextPassword |
|
| 708 | - * @return IShare The share object |
|
| 709 | - */ |
|
| 710 | - public function update(IShare $share, $plainTextPassword = null) { |
|
| 711 | - $originalShare = $this->getShareById($share->getId()); |
|
| 712 | - |
|
| 713 | - // a real password was given |
|
| 714 | - $validPassword = $plainTextPassword !== null && $plainTextPassword !== ''; |
|
| 715 | - |
|
| 716 | - if ($validPassword && ($originalShare->getPassword() !== $share->getPassword() || |
|
| 717 | - ($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()))) { |
|
| 718 | - $this->sendPassword($share, $plainTextPassword); |
|
| 719 | - } |
|
| 720 | - /* |
|
| 697 | + $qb->setValue('file_target', $qb->createNamedParameter('')); |
|
| 698 | + |
|
| 699 | + $qb->executeStatement(); |
|
| 700 | + return $qb->getLastInsertId(); |
|
| 701 | + } |
|
| 702 | + |
|
| 703 | + /** |
|
| 704 | + * Update a share |
|
| 705 | + * |
|
| 706 | + * @param IShare $share |
|
| 707 | + * @param string|null $plainTextPassword |
|
| 708 | + * @return IShare The share object |
|
| 709 | + */ |
|
| 710 | + public function update(IShare $share, $plainTextPassword = null) { |
|
| 711 | + $originalShare = $this->getShareById($share->getId()); |
|
| 712 | + |
|
| 713 | + // a real password was given |
|
| 714 | + $validPassword = $plainTextPassword !== null && $plainTextPassword !== ''; |
|
| 715 | + |
|
| 716 | + if ($validPassword && ($originalShare->getPassword() !== $share->getPassword() || |
|
| 717 | + ($originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()))) { |
|
| 718 | + $this->sendPassword($share, $plainTextPassword); |
|
| 719 | + } |
|
| 720 | + /* |
|
| 721 | 721 | * We allow updating the permissions and password of mail shares |
| 722 | 722 | */ |
| 723 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 724 | - $qb->update('share') |
|
| 725 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) |
|
| 726 | - ->set('permissions', $qb->createNamedParameter($share->getPermissions())) |
|
| 727 | - ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) |
|
| 728 | - ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) |
|
| 729 | - ->set('password', $qb->createNamedParameter($share->getPassword())) |
|
| 730 | - ->set('label', $qb->createNamedParameter($share->getLabel())) |
|
| 731 | - ->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL)) |
|
| 732 | - ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) |
|
| 733 | - ->set('note', $qb->createNamedParameter($share->getNote())) |
|
| 734 | - ->set('hide_download', $qb->createNamedParameter((int)$share->getHideDownload(), IQueryBuilder::PARAM_INT)) |
|
| 735 | - ->executeStatement(); |
|
| 736 | - |
|
| 737 | - if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') { |
|
| 738 | - $this->sendNote($share); |
|
| 739 | - } |
|
| 740 | - |
|
| 741 | - return $share; |
|
| 742 | - } |
|
| 743 | - |
|
| 744 | - /** |
|
| 745 | - * @inheritdoc |
|
| 746 | - */ |
|
| 747 | - public function move(IShare $share, $recipient) { |
|
| 748 | - /** |
|
| 749 | - * nothing to do here, mail shares are only outgoing shares |
|
| 750 | - */ |
|
| 751 | - return $share; |
|
| 752 | - } |
|
| 753 | - |
|
| 754 | - /** |
|
| 755 | - * Delete a share (owner unShares the file) |
|
| 756 | - * |
|
| 757 | - * @param IShare $share |
|
| 758 | - */ |
|
| 759 | - public function delete(IShare $share) { |
|
| 760 | - try { |
|
| 761 | - $this->createShareActivity($share, 'unshare'); |
|
| 762 | - } catch (\Exception $e) { |
|
| 763 | - } |
|
| 764 | - |
|
| 765 | - $this->removeShareFromTable($share->getId()); |
|
| 766 | - } |
|
| 767 | - |
|
| 768 | - /** |
|
| 769 | - * @inheritdoc |
|
| 770 | - */ |
|
| 771 | - public function deleteFromSelf(IShare $share, $recipient) { |
|
| 772 | - // nothing to do here, mail shares are only outgoing shares |
|
| 773 | - } |
|
| 774 | - |
|
| 775 | - public function restore(IShare $share, string $recipient): IShare { |
|
| 776 | - throw new GenericShareException('not implemented'); |
|
| 777 | - } |
|
| 778 | - |
|
| 779 | - /** |
|
| 780 | - * @inheritdoc |
|
| 781 | - */ |
|
| 782 | - public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { |
|
| 783 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 784 | - $qb->select('*') |
|
| 785 | - ->from('share'); |
|
| 786 | - |
|
| 787 | - $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); |
|
| 788 | - |
|
| 789 | - /** |
|
| 790 | - * Reshares for this user are shares where they are the owner. |
|
| 791 | - */ |
|
| 792 | - if ($reshares === false) { |
|
| 793 | - //Special case for old shares created via the web UI |
|
| 794 | - $or1 = $qb->expr()->andX( |
|
| 795 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 796 | - $qb->expr()->isNull('uid_initiator') |
|
| 797 | - ); |
|
| 798 | - |
|
| 799 | - $qb->andWhere( |
|
| 800 | - $qb->expr()->orX( |
|
| 801 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), |
|
| 802 | - $or1 |
|
| 803 | - ) |
|
| 804 | - ); |
|
| 805 | - } else { |
|
| 806 | - $qb->andWhere( |
|
| 807 | - $qb->expr()->orX( |
|
| 808 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 809 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 810 | - ) |
|
| 811 | - ); |
|
| 812 | - } |
|
| 813 | - |
|
| 814 | - if ($node !== null) { |
|
| 815 | - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 816 | - } |
|
| 817 | - |
|
| 818 | - if ($limit !== -1) { |
|
| 819 | - $qb->setMaxResults($limit); |
|
| 820 | - } |
|
| 821 | - |
|
| 822 | - $qb->setFirstResult($offset); |
|
| 823 | - $qb->orderBy('id'); |
|
| 824 | - |
|
| 825 | - $cursor = $qb->executeQuery(); |
|
| 826 | - $shares = []; |
|
| 827 | - while ($data = $cursor->fetch()) { |
|
| 828 | - $shares[] = $this->createShareObject($data); |
|
| 829 | - } |
|
| 830 | - $cursor->closeCursor(); |
|
| 831 | - |
|
| 832 | - return $shares; |
|
| 833 | - } |
|
| 834 | - |
|
| 835 | - /** |
|
| 836 | - * @inheritdoc |
|
| 837 | - */ |
|
| 838 | - public function getShareById($id, $recipientId = null) { |
|
| 839 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 840 | - |
|
| 841 | - $qb->select('*') |
|
| 842 | - ->from('share') |
|
| 843 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) |
|
| 844 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); |
|
| 845 | - |
|
| 846 | - $cursor = $qb->executeQuery(); |
|
| 847 | - $data = $cursor->fetch(); |
|
| 848 | - $cursor->closeCursor(); |
|
| 849 | - |
|
| 850 | - if ($data === false) { |
|
| 851 | - throw new ShareNotFound(); |
|
| 852 | - } |
|
| 853 | - |
|
| 854 | - try { |
|
| 855 | - $share = $this->createShareObject($data); |
|
| 856 | - } catch (InvalidShare $e) { |
|
| 857 | - throw new ShareNotFound(); |
|
| 858 | - } |
|
| 859 | - |
|
| 860 | - return $share; |
|
| 861 | - } |
|
| 862 | - |
|
| 863 | - /** |
|
| 864 | - * Get shares for a given path |
|
| 865 | - * |
|
| 866 | - * @param \OCP\Files\Node $path |
|
| 867 | - * @return IShare[] |
|
| 868 | - */ |
|
| 869 | - public function getSharesByPath(Node $path) { |
|
| 870 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 871 | - |
|
| 872 | - $cursor = $qb->select('*') |
|
| 873 | - ->from('share') |
|
| 874 | - ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) |
|
| 875 | - ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 876 | - ->executeQuery(); |
|
| 877 | - |
|
| 878 | - $shares = []; |
|
| 879 | - while ($data = $cursor->fetch()) { |
|
| 880 | - $shares[] = $this->createShareObject($data); |
|
| 881 | - } |
|
| 882 | - $cursor->closeCursor(); |
|
| 883 | - |
|
| 884 | - return $shares; |
|
| 885 | - } |
|
| 886 | - |
|
| 887 | - /** |
|
| 888 | - * @inheritdoc |
|
| 889 | - */ |
|
| 890 | - public function getSharedWith($userId, $shareType, $node, $limit, $offset) { |
|
| 891 | - /** @var IShare[] $shares */ |
|
| 892 | - $shares = []; |
|
| 893 | - |
|
| 894 | - //Get shares directly with this user |
|
| 895 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 896 | - $qb->select('*') |
|
| 897 | - ->from('share'); |
|
| 898 | - |
|
| 899 | - // Order by id |
|
| 900 | - $qb->orderBy('id'); |
|
| 901 | - |
|
| 902 | - // Set limit and offset |
|
| 903 | - if ($limit !== -1) { |
|
| 904 | - $qb->setMaxResults($limit); |
|
| 905 | - } |
|
| 906 | - $qb->setFirstResult($offset); |
|
| 907 | - |
|
| 908 | - $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); |
|
| 909 | - $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); |
|
| 910 | - |
|
| 911 | - // Filter by node if provided |
|
| 912 | - if ($node !== null) { |
|
| 913 | - $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 914 | - } |
|
| 915 | - |
|
| 916 | - $cursor = $qb->executeQuery(); |
|
| 917 | - |
|
| 918 | - while ($data = $cursor->fetch()) { |
|
| 919 | - $shares[] = $this->createShareObject($data); |
|
| 920 | - } |
|
| 921 | - $cursor->closeCursor(); |
|
| 922 | - |
|
| 923 | - |
|
| 924 | - return $shares; |
|
| 925 | - } |
|
| 926 | - |
|
| 927 | - /** |
|
| 928 | - * Get a share by token |
|
| 929 | - * |
|
| 930 | - * @param string $token |
|
| 931 | - * @return IShare |
|
| 932 | - * @throws ShareNotFound |
|
| 933 | - */ |
|
| 934 | - public function getShareByToken($token) { |
|
| 935 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 936 | - |
|
| 937 | - $cursor = $qb->select('*') |
|
| 938 | - ->from('share') |
|
| 939 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 940 | - ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) |
|
| 941 | - ->executeQuery(); |
|
| 942 | - |
|
| 943 | - $data = $cursor->fetch(); |
|
| 944 | - |
|
| 945 | - if ($data === false) { |
|
| 946 | - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 947 | - } |
|
| 948 | - |
|
| 949 | - try { |
|
| 950 | - $share = $this->createShareObject($data); |
|
| 951 | - } catch (InvalidShare $e) { |
|
| 952 | - throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 953 | - } |
|
| 954 | - |
|
| 955 | - return $share; |
|
| 956 | - } |
|
| 957 | - |
|
| 958 | - /** |
|
| 959 | - * remove share from table |
|
| 960 | - * |
|
| 961 | - * @param string $shareId |
|
| 962 | - */ |
|
| 963 | - protected function removeShareFromTable($shareId) { |
|
| 964 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 965 | - $qb->delete('share') |
|
| 966 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); |
|
| 967 | - $qb->executeStatement(); |
|
| 968 | - } |
|
| 969 | - |
|
| 970 | - /** |
|
| 971 | - * Create a share object from an database row |
|
| 972 | - * |
|
| 973 | - * @param array $data |
|
| 974 | - * @return IShare |
|
| 975 | - * @throws InvalidShare |
|
| 976 | - * @throws ShareNotFound |
|
| 977 | - */ |
|
| 978 | - protected function createShareObject($data) { |
|
| 979 | - $share = new Share($this->rootFolder, $this->userManager); |
|
| 980 | - $share->setId((int)$data['id']) |
|
| 981 | - ->setShareType((int)$data['share_type']) |
|
| 982 | - ->setPermissions((int)$data['permissions']) |
|
| 983 | - ->setTarget($data['file_target']) |
|
| 984 | - ->setMailSend((bool)$data['mail_send']) |
|
| 985 | - ->setNote($data['note']) |
|
| 986 | - ->setToken($data['token']); |
|
| 987 | - |
|
| 988 | - $shareTime = new \DateTime(); |
|
| 989 | - $shareTime->setTimestamp((int)$data['stime']); |
|
| 990 | - $share->setShareTime($shareTime); |
|
| 991 | - $share->setSharedWith($data['share_with']); |
|
| 992 | - $share->setPassword($data['password']); |
|
| 993 | - $share->setLabel($data['label']); |
|
| 994 | - $share->setSendPasswordByTalk((bool)$data['password_by_talk']); |
|
| 995 | - $share->setHideDownload((bool)$data['hide_download']); |
|
| 996 | - |
|
| 997 | - if ($data['uid_initiator'] !== null) { |
|
| 998 | - $share->setShareOwner($data['uid_owner']); |
|
| 999 | - $share->setSharedBy($data['uid_initiator']); |
|
| 1000 | - } else { |
|
| 1001 | - //OLD SHARE |
|
| 1002 | - $share->setSharedBy($data['uid_owner']); |
|
| 1003 | - $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); |
|
| 1004 | - |
|
| 1005 | - $owner = $path->getOwner(); |
|
| 1006 | - $share->setShareOwner($owner->getUID()); |
|
| 1007 | - } |
|
| 1008 | - |
|
| 1009 | - if ($data['expiration'] !== null) { |
|
| 1010 | - $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']); |
|
| 1011 | - if ($expiration !== false) { |
|
| 1012 | - $share->setExpirationDate($expiration); |
|
| 1013 | - } |
|
| 1014 | - } |
|
| 1015 | - |
|
| 1016 | - $share->setNodeId((int)$data['file_source']); |
|
| 1017 | - $share->setNodeType($data['item_type']); |
|
| 1018 | - |
|
| 1019 | - $share->setProviderId($this->identifier()); |
|
| 1020 | - |
|
| 1021 | - return $share; |
|
| 1022 | - } |
|
| 1023 | - |
|
| 1024 | - /** |
|
| 1025 | - * Get the node with file $id for $user |
|
| 1026 | - * |
|
| 1027 | - * @param string $userId |
|
| 1028 | - * @param int $id |
|
| 1029 | - * @return \OCP\Files\File|\OCP\Files\Folder |
|
| 1030 | - * @throws InvalidShare |
|
| 1031 | - */ |
|
| 1032 | - private function getNode($userId, $id) { |
|
| 1033 | - try { |
|
| 1034 | - $userFolder = $this->rootFolder->getUserFolder($userId); |
|
| 1035 | - } catch (NoUserException $e) { |
|
| 1036 | - throw new InvalidShare(); |
|
| 1037 | - } |
|
| 1038 | - |
|
| 1039 | - $nodes = $userFolder->getById($id); |
|
| 1040 | - |
|
| 1041 | - if (empty($nodes)) { |
|
| 1042 | - throw new InvalidShare(); |
|
| 1043 | - } |
|
| 1044 | - |
|
| 1045 | - return $nodes[0]; |
|
| 1046 | - } |
|
| 1047 | - |
|
| 1048 | - /** |
|
| 1049 | - * A user is deleted from the system |
|
| 1050 | - * So clean up the relevant shares. |
|
| 1051 | - * |
|
| 1052 | - * @param string $uid |
|
| 1053 | - * @param int $shareType |
|
| 1054 | - */ |
|
| 1055 | - public function userDeleted($uid, $shareType) { |
|
| 1056 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1057 | - |
|
| 1058 | - $qb->delete('share') |
|
| 1059 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 1060 | - ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) |
|
| 1061 | - ->executeStatement(); |
|
| 1062 | - } |
|
| 1063 | - |
|
| 1064 | - /** |
|
| 1065 | - * This provider does not support group shares |
|
| 1066 | - * |
|
| 1067 | - * @param string $gid |
|
| 1068 | - */ |
|
| 1069 | - public function groupDeleted($gid) { |
|
| 1070 | - } |
|
| 1071 | - |
|
| 1072 | - /** |
|
| 1073 | - * This provider does not support group shares |
|
| 1074 | - * |
|
| 1075 | - * @param string $uid |
|
| 1076 | - * @param string $gid |
|
| 1077 | - */ |
|
| 1078 | - public function userDeletedFromGroup($uid, $gid) { |
|
| 1079 | - } |
|
| 1080 | - |
|
| 1081 | - /** |
|
| 1082 | - * get database row of a give share |
|
| 1083 | - * |
|
| 1084 | - * @param $id |
|
| 1085 | - * @return array |
|
| 1086 | - * @throws ShareNotFound |
|
| 1087 | - */ |
|
| 1088 | - protected function getRawShare($id) { |
|
| 1089 | - |
|
| 1090 | - // Now fetch the inserted share and create a complete share object |
|
| 1091 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1092 | - $qb->select('*') |
|
| 1093 | - ->from('share') |
|
| 1094 | - ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); |
|
| 1095 | - |
|
| 1096 | - $cursor = $qb->executeQuery(); |
|
| 1097 | - $data = $cursor->fetch(); |
|
| 1098 | - $cursor->closeCursor(); |
|
| 1099 | - |
|
| 1100 | - if ($data === false) { |
|
| 1101 | - throw new ShareNotFound; |
|
| 1102 | - } |
|
| 1103 | - |
|
| 1104 | - return $data; |
|
| 1105 | - } |
|
| 1106 | - |
|
| 1107 | - public function getSharesInFolder($userId, Folder $node, $reshares) { |
|
| 1108 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1109 | - $qb->select('*') |
|
| 1110 | - ->from('share', 's') |
|
| 1111 | - ->andWhere($qb->expr()->orX( |
|
| 1112 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1113 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1114 | - )) |
|
| 1115 | - ->andWhere( |
|
| 1116 | - $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)) |
|
| 1117 | - ); |
|
| 1118 | - |
|
| 1119 | - /** |
|
| 1120 | - * Reshares for this user are shares where they are the owner. |
|
| 1121 | - */ |
|
| 1122 | - if ($reshares === false) { |
|
| 1123 | - $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); |
|
| 1124 | - } else { |
|
| 1125 | - $qb->andWhere( |
|
| 1126 | - $qb->expr()->orX( |
|
| 1127 | - $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 1128 | - $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 1129 | - ) |
|
| 1130 | - ); |
|
| 1131 | - } |
|
| 1132 | - |
|
| 1133 | - $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); |
|
| 1134 | - $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); |
|
| 1135 | - |
|
| 1136 | - $qb->orderBy('id'); |
|
| 1137 | - |
|
| 1138 | - $cursor = $qb->executeQuery(); |
|
| 1139 | - $shares = []; |
|
| 1140 | - while ($data = $cursor->fetch()) { |
|
| 1141 | - $shares[$data['fileid']][] = $this->createShareObject($data); |
|
| 1142 | - } |
|
| 1143 | - $cursor->closeCursor(); |
|
| 1144 | - |
|
| 1145 | - return $shares; |
|
| 1146 | - } |
|
| 1147 | - |
|
| 1148 | - /** |
|
| 1149 | - * @inheritdoc |
|
| 1150 | - */ |
|
| 1151 | - public function getAccessList($nodes, $currentAccess) { |
|
| 1152 | - $ids = []; |
|
| 1153 | - foreach ($nodes as $node) { |
|
| 1154 | - $ids[] = $node->getId(); |
|
| 1155 | - } |
|
| 1156 | - |
|
| 1157 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1158 | - $qb->select('share_with') |
|
| 1159 | - ->from('share') |
|
| 1160 | - ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 1161 | - ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) |
|
| 1162 | - ->andWhere($qb->expr()->orX( |
|
| 1163 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1164 | - $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1165 | - )) |
|
| 1166 | - ->setMaxResults(1); |
|
| 1167 | - $cursor = $qb->executeQuery(); |
|
| 1168 | - |
|
| 1169 | - $mail = $cursor->fetch() !== false; |
|
| 1170 | - $cursor->closeCursor(); |
|
| 1171 | - |
|
| 1172 | - return ['public' => $mail]; |
|
| 1173 | - } |
|
| 1174 | - |
|
| 1175 | - public function getAllShares(): iterable { |
|
| 1176 | - $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1177 | - |
|
| 1178 | - $qb->select('*') |
|
| 1179 | - ->from('share') |
|
| 1180 | - ->where( |
|
| 1181 | - $qb->expr()->orX( |
|
| 1182 | - $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share\IShare::TYPE_EMAIL)) |
|
| 1183 | - ) |
|
| 1184 | - ); |
|
| 1185 | - |
|
| 1186 | - $cursor = $qb->executeQuery(); |
|
| 1187 | - while ($data = $cursor->fetch()) { |
|
| 1188 | - try { |
|
| 1189 | - $share = $this->createShareObject($data); |
|
| 1190 | - } catch (InvalidShare $e) { |
|
| 1191 | - continue; |
|
| 1192 | - } catch (ShareNotFound $e) { |
|
| 1193 | - continue; |
|
| 1194 | - } |
|
| 1195 | - |
|
| 1196 | - yield $share; |
|
| 1197 | - } |
|
| 1198 | - $cursor->closeCursor(); |
|
| 1199 | - } |
|
| 723 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 724 | + $qb->update('share') |
|
| 725 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) |
|
| 726 | + ->set('permissions', $qb->createNamedParameter($share->getPermissions())) |
|
| 727 | + ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) |
|
| 728 | + ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) |
|
| 729 | + ->set('password', $qb->createNamedParameter($share->getPassword())) |
|
| 730 | + ->set('label', $qb->createNamedParameter($share->getLabel())) |
|
| 731 | + ->set('password_by_talk', $qb->createNamedParameter($share->getSendPasswordByTalk(), IQueryBuilder::PARAM_BOOL)) |
|
| 732 | + ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) |
|
| 733 | + ->set('note', $qb->createNamedParameter($share->getNote())) |
|
| 734 | + ->set('hide_download', $qb->createNamedParameter((int)$share->getHideDownload(), IQueryBuilder::PARAM_INT)) |
|
| 735 | + ->executeStatement(); |
|
| 736 | + |
|
| 737 | + if ($originalShare->getNote() !== $share->getNote() && $share->getNote() !== '') { |
|
| 738 | + $this->sendNote($share); |
|
| 739 | + } |
|
| 740 | + |
|
| 741 | + return $share; |
|
| 742 | + } |
|
| 743 | + |
|
| 744 | + /** |
|
| 745 | + * @inheritdoc |
|
| 746 | + */ |
|
| 747 | + public function move(IShare $share, $recipient) { |
|
| 748 | + /** |
|
| 749 | + * nothing to do here, mail shares are only outgoing shares |
|
| 750 | + */ |
|
| 751 | + return $share; |
|
| 752 | + } |
|
| 753 | + |
|
| 754 | + /** |
|
| 755 | + * Delete a share (owner unShares the file) |
|
| 756 | + * |
|
| 757 | + * @param IShare $share |
|
| 758 | + */ |
|
| 759 | + public function delete(IShare $share) { |
|
| 760 | + try { |
|
| 761 | + $this->createShareActivity($share, 'unshare'); |
|
| 762 | + } catch (\Exception $e) { |
|
| 763 | + } |
|
| 764 | + |
|
| 765 | + $this->removeShareFromTable($share->getId()); |
|
| 766 | + } |
|
| 767 | + |
|
| 768 | + /** |
|
| 769 | + * @inheritdoc |
|
| 770 | + */ |
|
| 771 | + public function deleteFromSelf(IShare $share, $recipient) { |
|
| 772 | + // nothing to do here, mail shares are only outgoing shares |
|
| 773 | + } |
|
| 774 | + |
|
| 775 | + public function restore(IShare $share, string $recipient): IShare { |
|
| 776 | + throw new GenericShareException('not implemented'); |
|
| 777 | + } |
|
| 778 | + |
|
| 779 | + /** |
|
| 780 | + * @inheritdoc |
|
| 781 | + */ |
|
| 782 | + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { |
|
| 783 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 784 | + $qb->select('*') |
|
| 785 | + ->from('share'); |
|
| 786 | + |
|
| 787 | + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); |
|
| 788 | + |
|
| 789 | + /** |
|
| 790 | + * Reshares for this user are shares where they are the owner. |
|
| 791 | + */ |
|
| 792 | + if ($reshares === false) { |
|
| 793 | + //Special case for old shares created via the web UI |
|
| 794 | + $or1 = $qb->expr()->andX( |
|
| 795 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 796 | + $qb->expr()->isNull('uid_initiator') |
|
| 797 | + ); |
|
| 798 | + |
|
| 799 | + $qb->andWhere( |
|
| 800 | + $qb->expr()->orX( |
|
| 801 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)), |
|
| 802 | + $or1 |
|
| 803 | + ) |
|
| 804 | + ); |
|
| 805 | + } else { |
|
| 806 | + $qb->andWhere( |
|
| 807 | + $qb->expr()->orX( |
|
| 808 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 809 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 810 | + ) |
|
| 811 | + ); |
|
| 812 | + } |
|
| 813 | + |
|
| 814 | + if ($node !== null) { |
|
| 815 | + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 816 | + } |
|
| 817 | + |
|
| 818 | + if ($limit !== -1) { |
|
| 819 | + $qb->setMaxResults($limit); |
|
| 820 | + } |
|
| 821 | + |
|
| 822 | + $qb->setFirstResult($offset); |
|
| 823 | + $qb->orderBy('id'); |
|
| 824 | + |
|
| 825 | + $cursor = $qb->executeQuery(); |
|
| 826 | + $shares = []; |
|
| 827 | + while ($data = $cursor->fetch()) { |
|
| 828 | + $shares[] = $this->createShareObject($data); |
|
| 829 | + } |
|
| 830 | + $cursor->closeCursor(); |
|
| 831 | + |
|
| 832 | + return $shares; |
|
| 833 | + } |
|
| 834 | + |
|
| 835 | + /** |
|
| 836 | + * @inheritdoc |
|
| 837 | + */ |
|
| 838 | + public function getShareById($id, $recipientId = null) { |
|
| 839 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 840 | + |
|
| 841 | + $qb->select('*') |
|
| 842 | + ->from('share') |
|
| 843 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) |
|
| 844 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); |
|
| 845 | + |
|
| 846 | + $cursor = $qb->executeQuery(); |
|
| 847 | + $data = $cursor->fetch(); |
|
| 848 | + $cursor->closeCursor(); |
|
| 849 | + |
|
| 850 | + if ($data === false) { |
|
| 851 | + throw new ShareNotFound(); |
|
| 852 | + } |
|
| 853 | + |
|
| 854 | + try { |
|
| 855 | + $share = $this->createShareObject($data); |
|
| 856 | + } catch (InvalidShare $e) { |
|
| 857 | + throw new ShareNotFound(); |
|
| 858 | + } |
|
| 859 | + |
|
| 860 | + return $share; |
|
| 861 | + } |
|
| 862 | + |
|
| 863 | + /** |
|
| 864 | + * Get shares for a given path |
|
| 865 | + * |
|
| 866 | + * @param \OCP\Files\Node $path |
|
| 867 | + * @return IShare[] |
|
| 868 | + */ |
|
| 869 | + public function getSharesByPath(Node $path) { |
|
| 870 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 871 | + |
|
| 872 | + $cursor = $qb->select('*') |
|
| 873 | + ->from('share') |
|
| 874 | + ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) |
|
| 875 | + ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 876 | + ->executeQuery(); |
|
| 877 | + |
|
| 878 | + $shares = []; |
|
| 879 | + while ($data = $cursor->fetch()) { |
|
| 880 | + $shares[] = $this->createShareObject($data); |
|
| 881 | + } |
|
| 882 | + $cursor->closeCursor(); |
|
| 883 | + |
|
| 884 | + return $shares; |
|
| 885 | + } |
|
| 886 | + |
|
| 887 | + /** |
|
| 888 | + * @inheritdoc |
|
| 889 | + */ |
|
| 890 | + public function getSharedWith($userId, $shareType, $node, $limit, $offset) { |
|
| 891 | + /** @var IShare[] $shares */ |
|
| 892 | + $shares = []; |
|
| 893 | + |
|
| 894 | + //Get shares directly with this user |
|
| 895 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 896 | + $qb->select('*') |
|
| 897 | + ->from('share'); |
|
| 898 | + |
|
| 899 | + // Order by id |
|
| 900 | + $qb->orderBy('id'); |
|
| 901 | + |
|
| 902 | + // Set limit and offset |
|
| 903 | + if ($limit !== -1) { |
|
| 904 | + $qb->setMaxResults($limit); |
|
| 905 | + } |
|
| 906 | + $qb->setFirstResult($offset); |
|
| 907 | + |
|
| 908 | + $qb->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))); |
|
| 909 | + $qb->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))); |
|
| 910 | + |
|
| 911 | + // Filter by node if provided |
|
| 912 | + if ($node !== null) { |
|
| 913 | + $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); |
|
| 914 | + } |
|
| 915 | + |
|
| 916 | + $cursor = $qb->executeQuery(); |
|
| 917 | + |
|
| 918 | + while ($data = $cursor->fetch()) { |
|
| 919 | + $shares[] = $this->createShareObject($data); |
|
| 920 | + } |
|
| 921 | + $cursor->closeCursor(); |
|
| 922 | + |
|
| 923 | + |
|
| 924 | + return $shares; |
|
| 925 | + } |
|
| 926 | + |
|
| 927 | + /** |
|
| 928 | + * Get a share by token |
|
| 929 | + * |
|
| 930 | + * @param string $token |
|
| 931 | + * @return IShare |
|
| 932 | + * @throws ShareNotFound |
|
| 933 | + */ |
|
| 934 | + public function getShareByToken($token) { |
|
| 935 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 936 | + |
|
| 937 | + $cursor = $qb->select('*') |
|
| 938 | + ->from('share') |
|
| 939 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 940 | + ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) |
|
| 941 | + ->executeQuery(); |
|
| 942 | + |
|
| 943 | + $data = $cursor->fetch(); |
|
| 944 | + |
|
| 945 | + if ($data === false) { |
|
| 946 | + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 947 | + } |
|
| 948 | + |
|
| 949 | + try { |
|
| 950 | + $share = $this->createShareObject($data); |
|
| 951 | + } catch (InvalidShare $e) { |
|
| 952 | + throw new ShareNotFound('Share not found', $this->l->t('Could not find share')); |
|
| 953 | + } |
|
| 954 | + |
|
| 955 | + return $share; |
|
| 956 | + } |
|
| 957 | + |
|
| 958 | + /** |
|
| 959 | + * remove share from table |
|
| 960 | + * |
|
| 961 | + * @param string $shareId |
|
| 962 | + */ |
|
| 963 | + protected function removeShareFromTable($shareId) { |
|
| 964 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 965 | + $qb->delete('share') |
|
| 966 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($shareId))); |
|
| 967 | + $qb->executeStatement(); |
|
| 968 | + } |
|
| 969 | + |
|
| 970 | + /** |
|
| 971 | + * Create a share object from an database row |
|
| 972 | + * |
|
| 973 | + * @param array $data |
|
| 974 | + * @return IShare |
|
| 975 | + * @throws InvalidShare |
|
| 976 | + * @throws ShareNotFound |
|
| 977 | + */ |
|
| 978 | + protected function createShareObject($data) { |
|
| 979 | + $share = new Share($this->rootFolder, $this->userManager); |
|
| 980 | + $share->setId((int)$data['id']) |
|
| 981 | + ->setShareType((int)$data['share_type']) |
|
| 982 | + ->setPermissions((int)$data['permissions']) |
|
| 983 | + ->setTarget($data['file_target']) |
|
| 984 | + ->setMailSend((bool)$data['mail_send']) |
|
| 985 | + ->setNote($data['note']) |
|
| 986 | + ->setToken($data['token']); |
|
| 987 | + |
|
| 988 | + $shareTime = new \DateTime(); |
|
| 989 | + $shareTime->setTimestamp((int)$data['stime']); |
|
| 990 | + $share->setShareTime($shareTime); |
|
| 991 | + $share->setSharedWith($data['share_with']); |
|
| 992 | + $share->setPassword($data['password']); |
|
| 993 | + $share->setLabel($data['label']); |
|
| 994 | + $share->setSendPasswordByTalk((bool)$data['password_by_talk']); |
|
| 995 | + $share->setHideDownload((bool)$data['hide_download']); |
|
| 996 | + |
|
| 997 | + if ($data['uid_initiator'] !== null) { |
|
| 998 | + $share->setShareOwner($data['uid_owner']); |
|
| 999 | + $share->setSharedBy($data['uid_initiator']); |
|
| 1000 | + } else { |
|
| 1001 | + //OLD SHARE |
|
| 1002 | + $share->setSharedBy($data['uid_owner']); |
|
| 1003 | + $path = $this->getNode($share->getSharedBy(), (int)$data['file_source']); |
|
| 1004 | + |
|
| 1005 | + $owner = $path->getOwner(); |
|
| 1006 | + $share->setShareOwner($owner->getUID()); |
|
| 1007 | + } |
|
| 1008 | + |
|
| 1009 | + if ($data['expiration'] !== null) { |
|
| 1010 | + $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']); |
|
| 1011 | + if ($expiration !== false) { |
|
| 1012 | + $share->setExpirationDate($expiration); |
|
| 1013 | + } |
|
| 1014 | + } |
|
| 1015 | + |
|
| 1016 | + $share->setNodeId((int)$data['file_source']); |
|
| 1017 | + $share->setNodeType($data['item_type']); |
|
| 1018 | + |
|
| 1019 | + $share->setProviderId($this->identifier()); |
|
| 1020 | + |
|
| 1021 | + return $share; |
|
| 1022 | + } |
|
| 1023 | + |
|
| 1024 | + /** |
|
| 1025 | + * Get the node with file $id for $user |
|
| 1026 | + * |
|
| 1027 | + * @param string $userId |
|
| 1028 | + * @param int $id |
|
| 1029 | + * @return \OCP\Files\File|\OCP\Files\Folder |
|
| 1030 | + * @throws InvalidShare |
|
| 1031 | + */ |
|
| 1032 | + private function getNode($userId, $id) { |
|
| 1033 | + try { |
|
| 1034 | + $userFolder = $this->rootFolder->getUserFolder($userId); |
|
| 1035 | + } catch (NoUserException $e) { |
|
| 1036 | + throw new InvalidShare(); |
|
| 1037 | + } |
|
| 1038 | + |
|
| 1039 | + $nodes = $userFolder->getById($id); |
|
| 1040 | + |
|
| 1041 | + if (empty($nodes)) { |
|
| 1042 | + throw new InvalidShare(); |
|
| 1043 | + } |
|
| 1044 | + |
|
| 1045 | + return $nodes[0]; |
|
| 1046 | + } |
|
| 1047 | + |
|
| 1048 | + /** |
|
| 1049 | + * A user is deleted from the system |
|
| 1050 | + * So clean up the relevant shares. |
|
| 1051 | + * |
|
| 1052 | + * @param string $uid |
|
| 1053 | + * @param int $shareType |
|
| 1054 | + */ |
|
| 1055 | + public function userDeleted($uid, $shareType) { |
|
| 1056 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1057 | + |
|
| 1058 | + $qb->delete('share') |
|
| 1059 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 1060 | + ->andWhere($qb->expr()->eq('uid_owner', $qb->createNamedParameter($uid))) |
|
| 1061 | + ->executeStatement(); |
|
| 1062 | + } |
|
| 1063 | + |
|
| 1064 | + /** |
|
| 1065 | + * This provider does not support group shares |
|
| 1066 | + * |
|
| 1067 | + * @param string $gid |
|
| 1068 | + */ |
|
| 1069 | + public function groupDeleted($gid) { |
|
| 1070 | + } |
|
| 1071 | + |
|
| 1072 | + /** |
|
| 1073 | + * This provider does not support group shares |
|
| 1074 | + * |
|
| 1075 | + * @param string $uid |
|
| 1076 | + * @param string $gid |
|
| 1077 | + */ |
|
| 1078 | + public function userDeletedFromGroup($uid, $gid) { |
|
| 1079 | + } |
|
| 1080 | + |
|
| 1081 | + /** |
|
| 1082 | + * get database row of a give share |
|
| 1083 | + * |
|
| 1084 | + * @param $id |
|
| 1085 | + * @return array |
|
| 1086 | + * @throws ShareNotFound |
|
| 1087 | + */ |
|
| 1088 | + protected function getRawShare($id) { |
|
| 1089 | + |
|
| 1090 | + // Now fetch the inserted share and create a complete share object |
|
| 1091 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1092 | + $qb->select('*') |
|
| 1093 | + ->from('share') |
|
| 1094 | + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); |
|
| 1095 | + |
|
| 1096 | + $cursor = $qb->executeQuery(); |
|
| 1097 | + $data = $cursor->fetch(); |
|
| 1098 | + $cursor->closeCursor(); |
|
| 1099 | + |
|
| 1100 | + if ($data === false) { |
|
| 1101 | + throw new ShareNotFound; |
|
| 1102 | + } |
|
| 1103 | + |
|
| 1104 | + return $data; |
|
| 1105 | + } |
|
| 1106 | + |
|
| 1107 | + public function getSharesInFolder($userId, Folder $node, $reshares) { |
|
| 1108 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1109 | + $qb->select('*') |
|
| 1110 | + ->from('share', 's') |
|
| 1111 | + ->andWhere($qb->expr()->orX( |
|
| 1112 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1113 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1114 | + )) |
|
| 1115 | + ->andWhere( |
|
| 1116 | + $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL)) |
|
| 1117 | + ); |
|
| 1118 | + |
|
| 1119 | + /** |
|
| 1120 | + * Reshares for this user are shares where they are the owner. |
|
| 1121 | + */ |
|
| 1122 | + if ($reshares === false) { |
|
| 1123 | + $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); |
|
| 1124 | + } else { |
|
| 1125 | + $qb->andWhere( |
|
| 1126 | + $qb->expr()->orX( |
|
| 1127 | + $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), |
|
| 1128 | + $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) |
|
| 1129 | + ) |
|
| 1130 | + ); |
|
| 1131 | + } |
|
| 1132 | + |
|
| 1133 | + $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); |
|
| 1134 | + $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); |
|
| 1135 | + |
|
| 1136 | + $qb->orderBy('id'); |
|
| 1137 | + |
|
| 1138 | + $cursor = $qb->executeQuery(); |
|
| 1139 | + $shares = []; |
|
| 1140 | + while ($data = $cursor->fetch()) { |
|
| 1141 | + $shares[$data['fileid']][] = $this->createShareObject($data); |
|
| 1142 | + } |
|
| 1143 | + $cursor->closeCursor(); |
|
| 1144 | + |
|
| 1145 | + return $shares; |
|
| 1146 | + } |
|
| 1147 | + |
|
| 1148 | + /** |
|
| 1149 | + * @inheritdoc |
|
| 1150 | + */ |
|
| 1151 | + public function getAccessList($nodes, $currentAccess) { |
|
| 1152 | + $ids = []; |
|
| 1153 | + foreach ($nodes as $node) { |
|
| 1154 | + $ids[] = $node->getId(); |
|
| 1155 | + } |
|
| 1156 | + |
|
| 1157 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1158 | + $qb->select('share_with') |
|
| 1159 | + ->from('share') |
|
| 1160 | + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_EMAIL))) |
|
| 1161 | + ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) |
|
| 1162 | + ->andWhere($qb->expr()->orX( |
|
| 1163 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), |
|
| 1164 | + $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) |
|
| 1165 | + )) |
|
| 1166 | + ->setMaxResults(1); |
|
| 1167 | + $cursor = $qb->executeQuery(); |
|
| 1168 | + |
|
| 1169 | + $mail = $cursor->fetch() !== false; |
|
| 1170 | + $cursor->closeCursor(); |
|
| 1171 | + |
|
| 1172 | + return ['public' => $mail]; |
|
| 1173 | + } |
|
| 1174 | + |
|
| 1175 | + public function getAllShares(): iterable { |
|
| 1176 | + $qb = $this->dbConnection->getQueryBuilder(); |
|
| 1177 | + |
|
| 1178 | + $qb->select('*') |
|
| 1179 | + ->from('share') |
|
| 1180 | + ->where( |
|
| 1181 | + $qb->expr()->orX( |
|
| 1182 | + $qb->expr()->eq('share_type', $qb->createNamedParameter(\OCP\Share\IShare::TYPE_EMAIL)) |
|
| 1183 | + ) |
|
| 1184 | + ); |
|
| 1185 | + |
|
| 1186 | + $cursor = $qb->executeQuery(); |
|
| 1187 | + while ($data = $cursor->fetch()) { |
|
| 1188 | + try { |
|
| 1189 | + $share = $this->createShareObject($data); |
|
| 1190 | + } catch (InvalidShare $e) { |
|
| 1191 | + continue; |
|
| 1192 | + } catch (ShareNotFound $e) { |
|
| 1193 | + continue; |
|
| 1194 | + } |
|
| 1195 | + |
|
| 1196 | + yield $share; |
|
| 1197 | + } |
|
| 1198 | + $cursor->closeCursor(); |
|
| 1199 | + } |
|
| 1200 | 1200 | } |
@@ -44,308 +44,308 @@ |
||
| 44 | 44 | */ |
| 45 | 45 | abstract class QBMapper { |
| 46 | 46 | |
| 47 | - /** @var string */ |
|
| 48 | - protected $tableName; |
|
| 49 | - |
|
| 50 | - /** @var string|class-string<T> */ |
|
| 51 | - protected $entityClass; |
|
| 52 | - |
|
| 53 | - /** @var IDBConnection */ |
|
| 54 | - protected $db; |
|
| 55 | - |
|
| 56 | - /** |
|
| 57 | - * @param IDBConnection $db Instance of the Db abstraction layer |
|
| 58 | - * @param string $tableName the name of the table. set this to allow entity |
|
| 59 | - * @param string|null $entityClass the name of the entity that the sql should be |
|
| 60 | - * @psalm-param class-string<T>|null $entityClass the name of the entity that the sql should be |
|
| 61 | - * mapped to queries without using sql |
|
| 62 | - * @since 14.0.0 |
|
| 63 | - */ |
|
| 64 | - public function __construct(IDBConnection $db, string $tableName, string $entityClass = null) { |
|
| 65 | - $this->db = $db; |
|
| 66 | - $this->tableName = $tableName; |
|
| 67 | - |
|
| 68 | - // if not given set the entity name to the class without the mapper part |
|
| 69 | - // cache it here for later use since reflection is slow |
|
| 70 | - if ($entityClass === null) { |
|
| 71 | - $this->entityClass = str_replace('Mapper', '', \get_class($this)); |
|
| 72 | - } else { |
|
| 73 | - $this->entityClass = $entityClass; |
|
| 74 | - } |
|
| 75 | - } |
|
| 76 | - |
|
| 77 | - |
|
| 78 | - /** |
|
| 79 | - * @return string the table name |
|
| 80 | - * @since 14.0.0 |
|
| 81 | - */ |
|
| 82 | - public function getTableName(): string { |
|
| 83 | - return $this->tableName; |
|
| 84 | - } |
|
| 85 | - |
|
| 86 | - |
|
| 87 | - /** |
|
| 88 | - * Deletes an entity from the table |
|
| 89 | - * @param Entity $entity the entity that should be deleted |
|
| 90 | - * @psalm-param T $entity the entity that should be deleted |
|
| 91 | - * @return Entity the deleted entity |
|
| 92 | - * @psalm-return T the deleted entity |
|
| 93 | - * @since 14.0.0 |
|
| 94 | - */ |
|
| 95 | - public function delete(Entity $entity): Entity { |
|
| 96 | - $qb = $this->db->getQueryBuilder(); |
|
| 97 | - |
|
| 98 | - $idType = $this->getParameterTypeForProperty($entity, 'id'); |
|
| 99 | - |
|
| 100 | - $qb->delete($this->tableName) |
|
| 101 | - ->where( |
|
| 102 | - $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType)) |
|
| 103 | - ); |
|
| 104 | - $qb->executeStatement(); |
|
| 105 | - return $entity; |
|
| 106 | - } |
|
| 107 | - |
|
| 108 | - |
|
| 109 | - /** |
|
| 110 | - * Creates a new entry in the db from an entity |
|
| 111 | - * @param Entity $entity the entity that should be created |
|
| 112 | - * @psalm-param T $entity the entity that should be created |
|
| 113 | - * @return Entity the saved entity with the set id |
|
| 114 | - * @psalm-return T the saved entity with the set id |
|
| 115 | - * @since 14.0.0 |
|
| 116 | - */ |
|
| 117 | - public function insert(Entity $entity): Entity { |
|
| 118 | - // get updated fields to save, fields have to be set using a setter to |
|
| 119 | - // be saved |
|
| 120 | - $properties = $entity->getUpdatedFields(); |
|
| 121 | - |
|
| 122 | - $qb = $this->db->getQueryBuilder(); |
|
| 123 | - $qb->insert($this->tableName); |
|
| 124 | - |
|
| 125 | - // build the fields |
|
| 126 | - foreach ($properties as $property => $updated) { |
|
| 127 | - $column = $entity->propertyToColumn($property); |
|
| 128 | - $getter = 'get' . ucfirst($property); |
|
| 129 | - $value = $entity->$getter(); |
|
| 130 | - |
|
| 131 | - $type = $this->getParameterTypeForProperty($entity, $property); |
|
| 132 | - $qb->setValue($column, $qb->createNamedParameter($value, $type)); |
|
| 133 | - } |
|
| 134 | - |
|
| 135 | - $qb->executeStatement(); |
|
| 136 | - |
|
| 137 | - if ($entity->id === null) { |
|
| 138 | - // When autoincrement is used id is always an int |
|
| 139 | - $entity->setId($qb->getLastInsertId()); |
|
| 140 | - } |
|
| 141 | - |
|
| 142 | - return $entity; |
|
| 143 | - } |
|
| 144 | - |
|
| 145 | - /** |
|
| 146 | - * Tries to creates a new entry in the db from an entity and |
|
| 147 | - * updates an existing entry if duplicate keys are detected |
|
| 148 | - * by the database |
|
| 149 | - * |
|
| 150 | - * @param Entity $entity the entity that should be created/updated |
|
| 151 | - * @psalm-param T $entity the entity that should be created/updated |
|
| 152 | - * @return Entity the saved entity with the (new) id |
|
| 153 | - * @psalm-return T the saved entity with the (new) id |
|
| 154 | - * @throws \InvalidArgumentException if entity has no id |
|
| 155 | - * @since 15.0.0 |
|
| 156 | - */ |
|
| 157 | - public function insertOrUpdate(Entity $entity): Entity { |
|
| 158 | - try { |
|
| 159 | - return $this->insert($entity); |
|
| 160 | - } catch (Exception $ex) { |
|
| 161 | - if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { |
|
| 162 | - return $this->update($entity); |
|
| 163 | - } |
|
| 164 | - throw $ex; |
|
| 165 | - } |
|
| 166 | - } |
|
| 167 | - |
|
| 168 | - /** |
|
| 169 | - * Updates an entry in the db from an entity |
|
| 170 | - * @throws \InvalidArgumentException if entity has no id |
|
| 171 | - * @param Entity $entity the entity that should be created |
|
| 172 | - * @psalm-param T $entity the entity that should be created |
|
| 173 | - * @return Entity the saved entity with the set id |
|
| 174 | - * @psalm-return T the saved entity with the set id |
|
| 175 | - * @since 14.0.0 |
|
| 176 | - */ |
|
| 177 | - public function update(Entity $entity): Entity { |
|
| 178 | - // if entity wasn't changed it makes no sense to run a db query |
|
| 179 | - $properties = $entity->getUpdatedFields(); |
|
| 180 | - if (\count($properties) === 0) { |
|
| 181 | - return $entity; |
|
| 182 | - } |
|
| 183 | - |
|
| 184 | - // entity needs an id |
|
| 185 | - $id = $entity->getId(); |
|
| 186 | - if ($id === null) { |
|
| 187 | - throw new \InvalidArgumentException( |
|
| 188 | - 'Entity which should be updated has no id'); |
|
| 189 | - } |
|
| 190 | - |
|
| 191 | - // get updated fields to save, fields have to be set using a setter to |
|
| 192 | - // be saved |
|
| 193 | - // do not update the id field |
|
| 194 | - unset($properties['id']); |
|
| 195 | - |
|
| 196 | - $qb = $this->db->getQueryBuilder(); |
|
| 197 | - $qb->update($this->tableName); |
|
| 198 | - |
|
| 199 | - // build the fields |
|
| 200 | - foreach ($properties as $property => $updated) { |
|
| 201 | - $column = $entity->propertyToColumn($property); |
|
| 202 | - $getter = 'get' . ucfirst($property); |
|
| 203 | - $value = $entity->$getter(); |
|
| 204 | - |
|
| 205 | - $type = $this->getParameterTypeForProperty($entity, $property); |
|
| 206 | - $qb->set($column, $qb->createNamedParameter($value, $type)); |
|
| 207 | - } |
|
| 208 | - |
|
| 209 | - $idType = $this->getParameterTypeForProperty($entity, 'id'); |
|
| 210 | - |
|
| 211 | - $qb->where( |
|
| 212 | - $qb->expr()->eq('id', $qb->createNamedParameter($id, $idType)) |
|
| 213 | - ); |
|
| 214 | - $qb->executeStatement(); |
|
| 215 | - |
|
| 216 | - return $entity; |
|
| 217 | - } |
|
| 218 | - |
|
| 219 | - /** |
|
| 220 | - * Returns the type parameter for the QueryBuilder for a specific property |
|
| 221 | - * of the $entity |
|
| 222 | - * |
|
| 223 | - * @param Entity $entity The entity to get the types from |
|
| 224 | - * @psalm-param T $entity |
|
| 225 | - * @param string $property The property of $entity to get the type for |
|
| 226 | - * @return int |
|
| 227 | - * @since 16.0.0 |
|
| 228 | - */ |
|
| 229 | - protected function getParameterTypeForProperty(Entity $entity, string $property): int { |
|
| 230 | - $types = $entity->getFieldTypes(); |
|
| 231 | - |
|
| 232 | - if (!isset($types[ $property ])) { |
|
| 233 | - return IQueryBuilder::PARAM_STR; |
|
| 234 | - } |
|
| 235 | - |
|
| 236 | - switch ($types[ $property ]) { |
|
| 237 | - case 'int': |
|
| 238 | - case 'integer': |
|
| 239 | - return IQueryBuilder::PARAM_INT; |
|
| 240 | - case 'string': |
|
| 241 | - return IQueryBuilder::PARAM_STR; |
|
| 242 | - case 'bool': |
|
| 243 | - case 'boolean': |
|
| 244 | - return IQueryBuilder::PARAM_BOOL; |
|
| 245 | - case 'blob': |
|
| 246 | - return IQueryBuilder::PARAM_LOB; |
|
| 247 | - } |
|
| 248 | - |
|
| 249 | - return IQueryBuilder::PARAM_STR; |
|
| 250 | - } |
|
| 251 | - |
|
| 252 | - /** |
|
| 253 | - * Returns an db result and throws exceptions when there are more or less |
|
| 254 | - * results |
|
| 255 | - * |
|
| 256 | - * @see findEntity |
|
| 257 | - * |
|
| 258 | - * @param IQueryBuilder $query |
|
| 259 | - * @throws DoesNotExistException if the item does not exist |
|
| 260 | - * @throws MultipleObjectsReturnedException if more than one item exist |
|
| 261 | - * @return array the result as row |
|
| 262 | - * @since 14.0.0 |
|
| 263 | - */ |
|
| 264 | - protected function findOneQuery(IQueryBuilder $query): array { |
|
| 265 | - $result = $query->executeQuery(); |
|
| 266 | - |
|
| 267 | - $row = $result->fetch(); |
|
| 268 | - if ($row === false) { |
|
| 269 | - $result->closeCursor(); |
|
| 270 | - $msg = $this->buildDebugMessage( |
|
| 271 | - 'Did expect one result but found none when executing', $query |
|
| 272 | - ); |
|
| 273 | - throw new DoesNotExistException($msg); |
|
| 274 | - } |
|
| 275 | - |
|
| 276 | - $row2 = $result->fetch(); |
|
| 277 | - $result->closeCursor(); |
|
| 278 | - if ($row2 !== false) { |
|
| 279 | - $msg = $this->buildDebugMessage( |
|
| 280 | - 'Did not expect more than one result when executing', $query |
|
| 281 | - ); |
|
| 282 | - throw new MultipleObjectsReturnedException($msg); |
|
| 283 | - } |
|
| 284 | - |
|
| 285 | - return $row; |
|
| 286 | - } |
|
| 287 | - |
|
| 288 | - /** |
|
| 289 | - * @param string $msg |
|
| 290 | - * @param IQueryBuilder $sql |
|
| 291 | - * @return string |
|
| 292 | - * @since 14.0.0 |
|
| 293 | - */ |
|
| 294 | - private function buildDebugMessage(string $msg, IQueryBuilder $sql): string { |
|
| 295 | - return $msg . |
|
| 296 | - ': query "' . $sql->getSQL() . '"; '; |
|
| 297 | - } |
|
| 298 | - |
|
| 299 | - |
|
| 300 | - /** |
|
| 301 | - * Creates an entity from a row. Automatically determines the entity class |
|
| 302 | - * from the current mapper name (MyEntityMapper -> MyEntity) |
|
| 303 | - * |
|
| 304 | - * @param array $row the row which should be converted to an entity |
|
| 305 | - * @return Entity the entity |
|
| 306 | - * @psalm-return T the entity |
|
| 307 | - * @since 14.0.0 |
|
| 308 | - */ |
|
| 309 | - protected function mapRowToEntity(array $row): Entity { |
|
| 310 | - return \call_user_func($this->entityClass .'::fromRow', $row); |
|
| 311 | - } |
|
| 312 | - |
|
| 313 | - |
|
| 314 | - /** |
|
| 315 | - * Runs a sql query and returns an array of entities |
|
| 316 | - * |
|
| 317 | - * @param IQueryBuilder $query |
|
| 318 | - * @return Entity[] all fetched entities |
|
| 319 | - * @psalm-return T[] all fetched entities |
|
| 320 | - * @since 14.0.0 |
|
| 321 | - */ |
|
| 322 | - protected function findEntities(IQueryBuilder $query): array { |
|
| 323 | - $result = $query->executeQuery(); |
|
| 324 | - |
|
| 325 | - $entities = []; |
|
| 326 | - |
|
| 327 | - while ($row = $result->fetch()) { |
|
| 328 | - $entities[] = $this->mapRowToEntity($row); |
|
| 329 | - } |
|
| 330 | - |
|
| 331 | - $result->closeCursor(); |
|
| 332 | - |
|
| 333 | - return $entities; |
|
| 334 | - } |
|
| 335 | - |
|
| 336 | - |
|
| 337 | - /** |
|
| 338 | - * Returns an db result and throws exceptions when there are more or less |
|
| 339 | - * results |
|
| 340 | - * |
|
| 341 | - * @param IQueryBuilder $query |
|
| 342 | - * @throws DoesNotExistException if the item does not exist |
|
| 343 | - * @throws MultipleObjectsReturnedException if more than one item exist |
|
| 344 | - * @return Entity the entity |
|
| 345 | - * @psalm-return T the entity |
|
| 346 | - * @since 14.0.0 |
|
| 347 | - */ |
|
| 348 | - protected function findEntity(IQueryBuilder $query): Entity { |
|
| 349 | - return $this->mapRowToEntity($this->findOneQuery($query)); |
|
| 350 | - } |
|
| 47 | + /** @var string */ |
|
| 48 | + protected $tableName; |
|
| 49 | + |
|
| 50 | + /** @var string|class-string<T> */ |
|
| 51 | + protected $entityClass; |
|
| 52 | + |
|
| 53 | + /** @var IDBConnection */ |
|
| 54 | + protected $db; |
|
| 55 | + |
|
| 56 | + /** |
|
| 57 | + * @param IDBConnection $db Instance of the Db abstraction layer |
|
| 58 | + * @param string $tableName the name of the table. set this to allow entity |
|
| 59 | + * @param string|null $entityClass the name of the entity that the sql should be |
|
| 60 | + * @psalm-param class-string<T>|null $entityClass the name of the entity that the sql should be |
|
| 61 | + * mapped to queries without using sql |
|
| 62 | + * @since 14.0.0 |
|
| 63 | + */ |
|
| 64 | + public function __construct(IDBConnection $db, string $tableName, string $entityClass = null) { |
|
| 65 | + $this->db = $db; |
|
| 66 | + $this->tableName = $tableName; |
|
| 67 | + |
|
| 68 | + // if not given set the entity name to the class without the mapper part |
|
| 69 | + // cache it here for later use since reflection is slow |
|
| 70 | + if ($entityClass === null) { |
|
| 71 | + $this->entityClass = str_replace('Mapper', '', \get_class($this)); |
|
| 72 | + } else { |
|
| 73 | + $this->entityClass = $entityClass; |
|
| 74 | + } |
|
| 75 | + } |
|
| 76 | + |
|
| 77 | + |
|
| 78 | + /** |
|
| 79 | + * @return string the table name |
|
| 80 | + * @since 14.0.0 |
|
| 81 | + */ |
|
| 82 | + public function getTableName(): string { |
|
| 83 | + return $this->tableName; |
|
| 84 | + } |
|
| 85 | + |
|
| 86 | + |
|
| 87 | + /** |
|
| 88 | + * Deletes an entity from the table |
|
| 89 | + * @param Entity $entity the entity that should be deleted |
|
| 90 | + * @psalm-param T $entity the entity that should be deleted |
|
| 91 | + * @return Entity the deleted entity |
|
| 92 | + * @psalm-return T the deleted entity |
|
| 93 | + * @since 14.0.0 |
|
| 94 | + */ |
|
| 95 | + public function delete(Entity $entity): Entity { |
|
| 96 | + $qb = $this->db->getQueryBuilder(); |
|
| 97 | + |
|
| 98 | + $idType = $this->getParameterTypeForProperty($entity, 'id'); |
|
| 99 | + |
|
| 100 | + $qb->delete($this->tableName) |
|
| 101 | + ->where( |
|
| 102 | + $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType)) |
|
| 103 | + ); |
|
| 104 | + $qb->executeStatement(); |
|
| 105 | + return $entity; |
|
| 106 | + } |
|
| 107 | + |
|
| 108 | + |
|
| 109 | + /** |
|
| 110 | + * Creates a new entry in the db from an entity |
|
| 111 | + * @param Entity $entity the entity that should be created |
|
| 112 | + * @psalm-param T $entity the entity that should be created |
|
| 113 | + * @return Entity the saved entity with the set id |
|
| 114 | + * @psalm-return T the saved entity with the set id |
|
| 115 | + * @since 14.0.0 |
|
| 116 | + */ |
|
| 117 | + public function insert(Entity $entity): Entity { |
|
| 118 | + // get updated fields to save, fields have to be set using a setter to |
|
| 119 | + // be saved |
|
| 120 | + $properties = $entity->getUpdatedFields(); |
|
| 121 | + |
|
| 122 | + $qb = $this->db->getQueryBuilder(); |
|
| 123 | + $qb->insert($this->tableName); |
|
| 124 | + |
|
| 125 | + // build the fields |
|
| 126 | + foreach ($properties as $property => $updated) { |
|
| 127 | + $column = $entity->propertyToColumn($property); |
|
| 128 | + $getter = 'get' . ucfirst($property); |
|
| 129 | + $value = $entity->$getter(); |
|
| 130 | + |
|
| 131 | + $type = $this->getParameterTypeForProperty($entity, $property); |
|
| 132 | + $qb->setValue($column, $qb->createNamedParameter($value, $type)); |
|
| 133 | + } |
|
| 134 | + |
|
| 135 | + $qb->executeStatement(); |
|
| 136 | + |
|
| 137 | + if ($entity->id === null) { |
|
| 138 | + // When autoincrement is used id is always an int |
|
| 139 | + $entity->setId($qb->getLastInsertId()); |
|
| 140 | + } |
|
| 141 | + |
|
| 142 | + return $entity; |
|
| 143 | + } |
|
| 144 | + |
|
| 145 | + /** |
|
| 146 | + * Tries to creates a new entry in the db from an entity and |
|
| 147 | + * updates an existing entry if duplicate keys are detected |
|
| 148 | + * by the database |
|
| 149 | + * |
|
| 150 | + * @param Entity $entity the entity that should be created/updated |
|
| 151 | + * @psalm-param T $entity the entity that should be created/updated |
|
| 152 | + * @return Entity the saved entity with the (new) id |
|
| 153 | + * @psalm-return T the saved entity with the (new) id |
|
| 154 | + * @throws \InvalidArgumentException if entity has no id |
|
| 155 | + * @since 15.0.0 |
|
| 156 | + */ |
|
| 157 | + public function insertOrUpdate(Entity $entity): Entity { |
|
| 158 | + try { |
|
| 159 | + return $this->insert($entity); |
|
| 160 | + } catch (Exception $ex) { |
|
| 161 | + if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { |
|
| 162 | + return $this->update($entity); |
|
| 163 | + } |
|
| 164 | + throw $ex; |
|
| 165 | + } |
|
| 166 | + } |
|
| 167 | + |
|
| 168 | + /** |
|
| 169 | + * Updates an entry in the db from an entity |
|
| 170 | + * @throws \InvalidArgumentException if entity has no id |
|
| 171 | + * @param Entity $entity the entity that should be created |
|
| 172 | + * @psalm-param T $entity the entity that should be created |
|
| 173 | + * @return Entity the saved entity with the set id |
|
| 174 | + * @psalm-return T the saved entity with the set id |
|
| 175 | + * @since 14.0.0 |
|
| 176 | + */ |
|
| 177 | + public function update(Entity $entity): Entity { |
|
| 178 | + // if entity wasn't changed it makes no sense to run a db query |
|
| 179 | + $properties = $entity->getUpdatedFields(); |
|
| 180 | + if (\count($properties) === 0) { |
|
| 181 | + return $entity; |
|
| 182 | + } |
|
| 183 | + |
|
| 184 | + // entity needs an id |
|
| 185 | + $id = $entity->getId(); |
|
| 186 | + if ($id === null) { |
|
| 187 | + throw new \InvalidArgumentException( |
|
| 188 | + 'Entity which should be updated has no id'); |
|
| 189 | + } |
|
| 190 | + |
|
| 191 | + // get updated fields to save, fields have to be set using a setter to |
|
| 192 | + // be saved |
|
| 193 | + // do not update the id field |
|
| 194 | + unset($properties['id']); |
|
| 195 | + |
|
| 196 | + $qb = $this->db->getQueryBuilder(); |
|
| 197 | + $qb->update($this->tableName); |
|
| 198 | + |
|
| 199 | + // build the fields |
|
| 200 | + foreach ($properties as $property => $updated) { |
|
| 201 | + $column = $entity->propertyToColumn($property); |
|
| 202 | + $getter = 'get' . ucfirst($property); |
|
| 203 | + $value = $entity->$getter(); |
|
| 204 | + |
|
| 205 | + $type = $this->getParameterTypeForProperty($entity, $property); |
|
| 206 | + $qb->set($column, $qb->createNamedParameter($value, $type)); |
|
| 207 | + } |
|
| 208 | + |
|
| 209 | + $idType = $this->getParameterTypeForProperty($entity, 'id'); |
|
| 210 | + |
|
| 211 | + $qb->where( |
|
| 212 | + $qb->expr()->eq('id', $qb->createNamedParameter($id, $idType)) |
|
| 213 | + ); |
|
| 214 | + $qb->executeStatement(); |
|
| 215 | + |
|
| 216 | + return $entity; |
|
| 217 | + } |
|
| 218 | + |
|
| 219 | + /** |
|
| 220 | + * Returns the type parameter for the QueryBuilder for a specific property |
|
| 221 | + * of the $entity |
|
| 222 | + * |
|
| 223 | + * @param Entity $entity The entity to get the types from |
|
| 224 | + * @psalm-param T $entity |
|
| 225 | + * @param string $property The property of $entity to get the type for |
|
| 226 | + * @return int |
|
| 227 | + * @since 16.0.0 |
|
| 228 | + */ |
|
| 229 | + protected function getParameterTypeForProperty(Entity $entity, string $property): int { |
|
| 230 | + $types = $entity->getFieldTypes(); |
|
| 231 | + |
|
| 232 | + if (!isset($types[ $property ])) { |
|
| 233 | + return IQueryBuilder::PARAM_STR; |
|
| 234 | + } |
|
| 235 | + |
|
| 236 | + switch ($types[ $property ]) { |
|
| 237 | + case 'int': |
|
| 238 | + case 'integer': |
|
| 239 | + return IQueryBuilder::PARAM_INT; |
|
| 240 | + case 'string': |
|
| 241 | + return IQueryBuilder::PARAM_STR; |
|
| 242 | + case 'bool': |
|
| 243 | + case 'boolean': |
|
| 244 | + return IQueryBuilder::PARAM_BOOL; |
|
| 245 | + case 'blob': |
|
| 246 | + return IQueryBuilder::PARAM_LOB; |
|
| 247 | + } |
|
| 248 | + |
|
| 249 | + return IQueryBuilder::PARAM_STR; |
|
| 250 | + } |
|
| 251 | + |
|
| 252 | + /** |
|
| 253 | + * Returns an db result and throws exceptions when there are more or less |
|
| 254 | + * results |
|
| 255 | + * |
|
| 256 | + * @see findEntity |
|
| 257 | + * |
|
| 258 | + * @param IQueryBuilder $query |
|
| 259 | + * @throws DoesNotExistException if the item does not exist |
|
| 260 | + * @throws MultipleObjectsReturnedException if more than one item exist |
|
| 261 | + * @return array the result as row |
|
| 262 | + * @since 14.0.0 |
|
| 263 | + */ |
|
| 264 | + protected function findOneQuery(IQueryBuilder $query): array { |
|
| 265 | + $result = $query->executeQuery(); |
|
| 266 | + |
|
| 267 | + $row = $result->fetch(); |
|
| 268 | + if ($row === false) { |
|
| 269 | + $result->closeCursor(); |
|
| 270 | + $msg = $this->buildDebugMessage( |
|
| 271 | + 'Did expect one result but found none when executing', $query |
|
| 272 | + ); |
|
| 273 | + throw new DoesNotExistException($msg); |
|
| 274 | + } |
|
| 275 | + |
|
| 276 | + $row2 = $result->fetch(); |
|
| 277 | + $result->closeCursor(); |
|
| 278 | + if ($row2 !== false) { |
|
| 279 | + $msg = $this->buildDebugMessage( |
|
| 280 | + 'Did not expect more than one result when executing', $query |
|
| 281 | + ); |
|
| 282 | + throw new MultipleObjectsReturnedException($msg); |
|
| 283 | + } |
|
| 284 | + |
|
| 285 | + return $row; |
|
| 286 | + } |
|
| 287 | + |
|
| 288 | + /** |
|
| 289 | + * @param string $msg |
|
| 290 | + * @param IQueryBuilder $sql |
|
| 291 | + * @return string |
|
| 292 | + * @since 14.0.0 |
|
| 293 | + */ |
|
| 294 | + private function buildDebugMessage(string $msg, IQueryBuilder $sql): string { |
|
| 295 | + return $msg . |
|
| 296 | + ': query "' . $sql->getSQL() . '"; '; |
|
| 297 | + } |
|
| 298 | + |
|
| 299 | + |
|
| 300 | + /** |
|
| 301 | + * Creates an entity from a row. Automatically determines the entity class |
|
| 302 | + * from the current mapper name (MyEntityMapper -> MyEntity) |
|
| 303 | + * |
|
| 304 | + * @param array $row the row which should be converted to an entity |
|
| 305 | + * @return Entity the entity |
|
| 306 | + * @psalm-return T the entity |
|
| 307 | + * @since 14.0.0 |
|
| 308 | + */ |
|
| 309 | + protected function mapRowToEntity(array $row): Entity { |
|
| 310 | + return \call_user_func($this->entityClass .'::fromRow', $row); |
|
| 311 | + } |
|
| 312 | + |
|
| 313 | + |
|
| 314 | + /** |
|
| 315 | + * Runs a sql query and returns an array of entities |
|
| 316 | + * |
|
| 317 | + * @param IQueryBuilder $query |
|
| 318 | + * @return Entity[] all fetched entities |
|
| 319 | + * @psalm-return T[] all fetched entities |
|
| 320 | + * @since 14.0.0 |
|
| 321 | + */ |
|
| 322 | + protected function findEntities(IQueryBuilder $query): array { |
|
| 323 | + $result = $query->executeQuery(); |
|
| 324 | + |
|
| 325 | + $entities = []; |
|
| 326 | + |
|
| 327 | + while ($row = $result->fetch()) { |
|
| 328 | + $entities[] = $this->mapRowToEntity($row); |
|
| 329 | + } |
|
| 330 | + |
|
| 331 | + $result->closeCursor(); |
|
| 332 | + |
|
| 333 | + return $entities; |
|
| 334 | + } |
|
| 335 | + |
|
| 336 | + |
|
| 337 | + /** |
|
| 338 | + * Returns an db result and throws exceptions when there are more or less |
|
| 339 | + * results |
|
| 340 | + * |
|
| 341 | + * @param IQueryBuilder $query |
|
| 342 | + * @throws DoesNotExistException if the item does not exist |
|
| 343 | + * @throws MultipleObjectsReturnedException if more than one item exist |
|
| 344 | + * @return Entity the entity |
|
| 345 | + * @psalm-return T the entity |
|
| 346 | + * @since 14.0.0 |
|
| 347 | + */ |
|
| 348 | + protected function findEntity(IQueryBuilder $query): Entity { |
|
| 349 | + return $this->mapRowToEntity($this->findOneQuery($query)); |
|
| 350 | + } |
|
| 351 | 351 | } |
@@ -40,974 +40,974 @@ |
||
| 40 | 40 | */ |
| 41 | 41 | interface IQueryBuilder { |
| 42 | 42 | |
| 43 | - /** |
|
| 44 | - * @since 9.0.0 |
|
| 45 | - */ |
|
| 46 | - public const PARAM_NULL = \PDO::PARAM_NULL; |
|
| 47 | - /** |
|
| 48 | - * @since 9.0.0 |
|
| 49 | - */ |
|
| 50 | - public const PARAM_BOOL = \PDO::PARAM_BOOL; |
|
| 51 | - /** |
|
| 52 | - * @since 9.0.0 |
|
| 53 | - */ |
|
| 54 | - public const PARAM_INT = \PDO::PARAM_INT; |
|
| 55 | - /** |
|
| 56 | - * @since 9.0.0 |
|
| 57 | - */ |
|
| 58 | - public const PARAM_STR = \PDO::PARAM_STR; |
|
| 59 | - /** |
|
| 60 | - * @since 9.0.0 |
|
| 61 | - */ |
|
| 62 | - public const PARAM_LOB = \PDO::PARAM_LOB; |
|
| 63 | - /** |
|
| 64 | - * @since 9.0.0 |
|
| 65 | - */ |
|
| 66 | - public const PARAM_DATE = 'datetime'; |
|
| 67 | - |
|
| 68 | - /** |
|
| 69 | - * @since 9.0.0 |
|
| 70 | - */ |
|
| 71 | - public const PARAM_INT_ARRAY = Connection::PARAM_INT_ARRAY; |
|
| 72 | - /** |
|
| 73 | - * @since 9.0.0 |
|
| 74 | - */ |
|
| 75 | - public const PARAM_STR_ARRAY = Connection::PARAM_STR_ARRAY; |
|
| 76 | - |
|
| 77 | - |
|
| 78 | - /** |
|
| 79 | - * Enable/disable automatic prefixing of table names with the oc_ prefix |
|
| 80 | - * |
|
| 81 | - * @param bool $enabled If set to true table names will be prefixed with the |
|
| 82 | - * owncloud database prefix automatically. |
|
| 83 | - * @since 8.2.0 |
|
| 84 | - */ |
|
| 85 | - public function automaticTablePrefix($enabled); |
|
| 86 | - |
|
| 87 | - /** |
|
| 88 | - * Gets an ExpressionBuilder used for object-oriented construction of query expressions. |
|
| 89 | - * This producer method is intended for convenient inline usage. Example: |
|
| 90 | - * |
|
| 91 | - * <code> |
|
| 92 | - * $qb = $conn->getQueryBuilder() |
|
| 93 | - * ->select('u') |
|
| 94 | - * ->from('users', 'u') |
|
| 95 | - * ->where($qb->expr()->eq('u.id', 1)); |
|
| 96 | - * </code> |
|
| 97 | - * |
|
| 98 | - * For more complex expression construction, consider storing the expression |
|
| 99 | - * builder object in a local variable. |
|
| 100 | - * |
|
| 101 | - * @return \OCP\DB\QueryBuilder\IExpressionBuilder |
|
| 102 | - * @since 8.2.0 |
|
| 103 | - */ |
|
| 104 | - public function expr(); |
|
| 105 | - |
|
| 106 | - /** |
|
| 107 | - * Gets an FunctionBuilder used for object-oriented construction of query functions. |
|
| 108 | - * This producer method is intended for convenient inline usage. Example: |
|
| 109 | - * |
|
| 110 | - * <code> |
|
| 111 | - * $qb = $conn->getQueryBuilder() |
|
| 112 | - * ->select('u') |
|
| 113 | - * ->from('users', 'u') |
|
| 114 | - * ->where($qb->fun()->md5('u.id')); |
|
| 115 | - * </code> |
|
| 116 | - * |
|
| 117 | - * For more complex function construction, consider storing the function |
|
| 118 | - * builder object in a local variable. |
|
| 119 | - * |
|
| 120 | - * @return \OCP\DB\QueryBuilder\IFunctionBuilder |
|
| 121 | - * @since 12.0.0 |
|
| 122 | - */ |
|
| 123 | - public function func(); |
|
| 124 | - |
|
| 125 | - /** |
|
| 126 | - * Gets the type of the currently built query. |
|
| 127 | - * |
|
| 128 | - * @return integer |
|
| 129 | - * @since 8.2.0 |
|
| 130 | - */ |
|
| 131 | - public function getType(); |
|
| 132 | - |
|
| 133 | - /** |
|
| 134 | - * Gets the associated DBAL Connection for this query builder. |
|
| 135 | - * |
|
| 136 | - * @return \OCP\IDBConnection |
|
| 137 | - * @since 8.2.0 |
|
| 138 | - */ |
|
| 139 | - public function getConnection(); |
|
| 140 | - |
|
| 141 | - /** |
|
| 142 | - * Gets the state of this query builder instance. |
|
| 143 | - * |
|
| 144 | - * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. |
|
| 145 | - * @since 8.2.0 |
|
| 146 | - */ |
|
| 147 | - public function getState(); |
|
| 148 | - |
|
| 149 | - /** |
|
| 150 | - * Executes this query using the bound parameters and their types. |
|
| 151 | - * |
|
| 152 | - * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeStatement} |
|
| 153 | - * for insert, update and delete statements. |
|
| 154 | - * |
|
| 155 | - * Warning: until Nextcloud 20, this method could return a \Doctrine\DBAL\Driver\Statement but since |
|
| 156 | - * that interface changed in a breaking way the adapter \OCP\DB\QueryBuilder\IStatement is returned |
|
| 157 | - * to bridge old code to the new API |
|
| 158 | - * |
|
| 159 | - * @return IResult|int |
|
| 160 | - * @throws Exception since 21.0.0 |
|
| 161 | - * @since 8.2.0 |
|
| 162 | - * @deprecated 22.0.0 Use executeQuery or executeUpdate |
|
| 163 | - */ |
|
| 164 | - public function execute(); |
|
| 165 | - |
|
| 166 | - /** |
|
| 167 | - * Execute for select statements |
|
| 168 | - * |
|
| 169 | - * @return IResult |
|
| 170 | - * @since 22.0.0 |
|
| 171 | - * |
|
| 172 | - * @throws Exception |
|
| 173 | - * @throws \RuntimeException in case of usage with non select query |
|
| 174 | - */ |
|
| 175 | - public function executeQuery(): IResult; |
|
| 176 | - |
|
| 177 | - /** |
|
| 178 | - * Execute insert, update and delete statements |
|
| 179 | - * |
|
| 180 | - * @return int the number of affected rows |
|
| 181 | - * @since 22.0.0 |
|
| 182 | - * |
|
| 183 | - * @throws Exception |
|
| 184 | - * @throws \RuntimeException in case of usage with select query |
|
| 185 | - */ |
|
| 186 | - public function executeStatement(): int; |
|
| 187 | - |
|
| 188 | - /** |
|
| 189 | - * Gets the complete SQL string formed by the current specifications of this QueryBuilder. |
|
| 190 | - * |
|
| 191 | - * <code> |
|
| 192 | - * $qb = $conn->getQueryBuilder() |
|
| 193 | - * ->select('u') |
|
| 194 | - * ->from('User', 'u') |
|
| 195 | - * echo $qb->getSQL(); // SELECT u FROM User u |
|
| 196 | - * </code> |
|
| 197 | - * |
|
| 198 | - * @return string The SQL query string. |
|
| 199 | - * @since 8.2.0 |
|
| 200 | - */ |
|
| 201 | - public function getSQL(); |
|
| 202 | - |
|
| 203 | - /** |
|
| 204 | - * Sets a query parameter for the query being constructed. |
|
| 205 | - * |
|
| 206 | - * <code> |
|
| 207 | - * $qb = $conn->getQueryBuilder() |
|
| 208 | - * ->select('u') |
|
| 209 | - * ->from('users', 'u') |
|
| 210 | - * ->where('u.id = :user_id') |
|
| 211 | - * ->setParameter(':user_id', 1); |
|
| 212 | - * </code> |
|
| 213 | - * |
|
| 214 | - * @param string|integer $key The parameter position or name. |
|
| 215 | - * @param mixed $value The parameter value. |
|
| 216 | - * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants. |
|
| 217 | - * |
|
| 218 | - * @return $this This QueryBuilder instance. |
|
| 219 | - * @since 8.2.0 |
|
| 220 | - */ |
|
| 221 | - public function setParameter($key, $value, $type = null); |
|
| 222 | - |
|
| 223 | - /** |
|
| 224 | - * Sets a collection of query parameters for the query being constructed. |
|
| 225 | - * |
|
| 226 | - * <code> |
|
| 227 | - * $qb = $conn->getQueryBuilder() |
|
| 228 | - * ->select('u') |
|
| 229 | - * ->from('users', 'u') |
|
| 230 | - * ->where('u.id = :user_id1 OR u.id = :user_id2') |
|
| 231 | - * ->setParameters(array( |
|
| 232 | - * ':user_id1' => 1, |
|
| 233 | - * ':user_id2' => 2 |
|
| 234 | - * )); |
|
| 235 | - * </code> |
|
| 236 | - * |
|
| 237 | - * @param array $params The query parameters to set. |
|
| 238 | - * @param array $types The query parameters types to set. |
|
| 239 | - * |
|
| 240 | - * @return $this This QueryBuilder instance. |
|
| 241 | - * @since 8.2.0 |
|
| 242 | - */ |
|
| 243 | - public function setParameters(array $params, array $types = []); |
|
| 244 | - |
|
| 245 | - /** |
|
| 246 | - * Gets all defined query parameters for the query being constructed indexed by parameter index or name. |
|
| 247 | - * |
|
| 248 | - * @return array The currently defined query parameters indexed by parameter index or name. |
|
| 249 | - * @since 8.2.0 |
|
| 250 | - */ |
|
| 251 | - public function getParameters(); |
|
| 252 | - |
|
| 253 | - /** |
|
| 254 | - * Gets a (previously set) query parameter of the query being constructed. |
|
| 255 | - * |
|
| 256 | - * @param mixed $key The key (index or name) of the bound parameter. |
|
| 257 | - * |
|
| 258 | - * @return mixed The value of the bound parameter. |
|
| 259 | - * @since 8.2.0 |
|
| 260 | - */ |
|
| 261 | - public function getParameter($key); |
|
| 262 | - |
|
| 263 | - /** |
|
| 264 | - * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. |
|
| 265 | - * |
|
| 266 | - * @return array The currently defined query parameter types indexed by parameter index or name. |
|
| 267 | - * @since 8.2.0 |
|
| 268 | - */ |
|
| 269 | - public function getParameterTypes(); |
|
| 270 | - |
|
| 271 | - /** |
|
| 272 | - * Gets a (previously set) query parameter type of the query being constructed. |
|
| 273 | - * |
|
| 274 | - * @param mixed $key The key (index or name) of the bound parameter type. |
|
| 275 | - * |
|
| 276 | - * @return mixed The value of the bound parameter type. |
|
| 277 | - * @since 8.2.0 |
|
| 278 | - */ |
|
| 279 | - public function getParameterType($key); |
|
| 280 | - |
|
| 281 | - /** |
|
| 282 | - * Sets the position of the first result to retrieve (the "offset"). |
|
| 283 | - * |
|
| 284 | - * @param integer $firstResult The first result to return. |
|
| 285 | - * |
|
| 286 | - * @return $this This QueryBuilder instance. |
|
| 287 | - * @since 8.2.0 |
|
| 288 | - */ |
|
| 289 | - public function setFirstResult($firstResult); |
|
| 290 | - |
|
| 291 | - /** |
|
| 292 | - * Gets the position of the first result the query object was set to retrieve (the "offset"). |
|
| 293 | - * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. |
|
| 294 | - * |
|
| 295 | - * @return integer The position of the first result. |
|
| 296 | - * @since 8.2.0 |
|
| 297 | - */ |
|
| 298 | - public function getFirstResult(); |
|
| 299 | - |
|
| 300 | - /** |
|
| 301 | - * Sets the maximum number of results to retrieve (the "limit"). |
|
| 302 | - * |
|
| 303 | - * @param integer $maxResults The maximum number of results to retrieve. |
|
| 304 | - * |
|
| 305 | - * @return $this This QueryBuilder instance. |
|
| 306 | - * @since 8.2.0 |
|
| 307 | - */ |
|
| 308 | - public function setMaxResults($maxResults); |
|
| 309 | - |
|
| 310 | - /** |
|
| 311 | - * Gets the maximum number of results the query object was set to retrieve (the "limit"). |
|
| 312 | - * Returns NULL if {@link setMaxResults} was not applied to this query builder. |
|
| 313 | - * |
|
| 314 | - * @return int|null The maximum number of results. |
|
| 315 | - * @since 8.2.0 |
|
| 316 | - */ |
|
| 317 | - public function getMaxResults(); |
|
| 318 | - |
|
| 319 | - /** |
|
| 320 | - * Specifies an item that is to be returned in the query result. |
|
| 321 | - * Replaces any previously specified selections, if any. |
|
| 322 | - * |
|
| 323 | - * <code> |
|
| 324 | - * $qb = $conn->getQueryBuilder() |
|
| 325 | - * ->select('u.id', 'p.id') |
|
| 326 | - * ->from('users', 'u') |
|
| 327 | - * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 328 | - * </code> |
|
| 329 | - * |
|
| 330 | - * @param mixed ...$selects The selection expressions. |
|
| 331 | - * |
|
| 332 | - * @return $this This QueryBuilder instance. |
|
| 333 | - * @since 8.2.0 |
|
| 334 | - * |
|
| 335 | - * @psalm-taint-sink sql $selects |
|
| 336 | - */ |
|
| 337 | - public function select(...$selects); |
|
| 338 | - |
|
| 339 | - /** |
|
| 340 | - * Specifies an item that is to be returned with a different name in the query result. |
|
| 341 | - * |
|
| 342 | - * <code> |
|
| 343 | - * $qb = $conn->getQueryBuilder() |
|
| 344 | - * ->selectAlias('u.id', 'user_id') |
|
| 345 | - * ->from('users', 'u') |
|
| 346 | - * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 347 | - * </code> |
|
| 348 | - * |
|
| 349 | - * @param mixed $select The selection expressions. |
|
| 350 | - * @param string $alias The column alias used in the constructed query. |
|
| 351 | - * |
|
| 352 | - * @return $this This QueryBuilder instance. |
|
| 353 | - * @since 8.2.1 |
|
| 354 | - * |
|
| 355 | - * @psalm-taint-sink sql $select |
|
| 356 | - * @psalm-taint-sink sql $alias |
|
| 357 | - */ |
|
| 358 | - public function selectAlias($select, $alias); |
|
| 359 | - |
|
| 360 | - /** |
|
| 361 | - * Specifies an item that is to be returned uniquely in the query result. |
|
| 362 | - * |
|
| 363 | - * <code> |
|
| 364 | - * $qb = $conn->getQueryBuilder() |
|
| 365 | - * ->selectDistinct('type') |
|
| 366 | - * ->from('users'); |
|
| 367 | - * </code> |
|
| 368 | - * |
|
| 369 | - * @param mixed $select The selection expressions. |
|
| 370 | - * |
|
| 371 | - * @return $this This QueryBuilder instance. |
|
| 372 | - * @since 9.0.0 |
|
| 373 | - * |
|
| 374 | - * @psalm-taint-sink sql $select |
|
| 375 | - */ |
|
| 376 | - public function selectDistinct($select); |
|
| 377 | - |
|
| 378 | - /** |
|
| 379 | - * Adds an item that is to be returned in the query result. |
|
| 380 | - * |
|
| 381 | - * <code> |
|
| 382 | - * $qb = $conn->getQueryBuilder() |
|
| 383 | - * ->select('u.id') |
|
| 384 | - * ->addSelect('p.id') |
|
| 385 | - * ->from('users', 'u') |
|
| 386 | - * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); |
|
| 387 | - * </code> |
|
| 388 | - * |
|
| 389 | - * @param mixed ...$select The selection expression. |
|
| 390 | - * |
|
| 391 | - * @return $this This QueryBuilder instance. |
|
| 392 | - * @since 8.2.0 |
|
| 393 | - * |
|
| 394 | - * @psalm-taint-sink sql $select |
|
| 395 | - */ |
|
| 396 | - public function addSelect(...$select); |
|
| 397 | - |
|
| 398 | - /** |
|
| 399 | - * Turns the query being built into a bulk delete query that ranges over |
|
| 400 | - * a certain table. |
|
| 401 | - * |
|
| 402 | - * <code> |
|
| 403 | - * $qb = $conn->getQueryBuilder() |
|
| 404 | - * ->delete('users', 'u') |
|
| 405 | - * ->where('u.id = :user_id'); |
|
| 406 | - * ->setParameter(':user_id', 1); |
|
| 407 | - * </code> |
|
| 408 | - * |
|
| 409 | - * @param string $delete The table whose rows are subject to the deletion. |
|
| 410 | - * @param string $alias The table alias used in the constructed query. |
|
| 411 | - * |
|
| 412 | - * @return $this This QueryBuilder instance. |
|
| 413 | - * @since 8.2.0 |
|
| 414 | - * |
|
| 415 | - * @psalm-taint-sink sql $delete |
|
| 416 | - */ |
|
| 417 | - public function delete($delete = null, $alias = null); |
|
| 418 | - |
|
| 419 | - /** |
|
| 420 | - * Turns the query being built into a bulk update query that ranges over |
|
| 421 | - * a certain table |
|
| 422 | - * |
|
| 423 | - * <code> |
|
| 424 | - * $qb = $conn->getQueryBuilder() |
|
| 425 | - * ->update('users', 'u') |
|
| 426 | - * ->set('u.password', md5('password')) |
|
| 427 | - * ->where('u.id = ?'); |
|
| 428 | - * </code> |
|
| 429 | - * |
|
| 430 | - * @param string $update The table whose rows are subject to the update. |
|
| 431 | - * @param string $alias The table alias used in the constructed query. |
|
| 432 | - * |
|
| 433 | - * @return $this This QueryBuilder instance. |
|
| 434 | - * @since 8.2.0 |
|
| 435 | - * |
|
| 436 | - * @psalm-taint-sink sql $update |
|
| 437 | - */ |
|
| 438 | - public function update($update = null, $alias = null); |
|
| 439 | - |
|
| 440 | - /** |
|
| 441 | - * Turns the query being built into an insert query that inserts into |
|
| 442 | - * a certain table |
|
| 443 | - * |
|
| 444 | - * <code> |
|
| 445 | - * $qb = $conn->getQueryBuilder() |
|
| 446 | - * ->insert('users') |
|
| 447 | - * ->values( |
|
| 448 | - * array( |
|
| 449 | - * 'name' => '?', |
|
| 450 | - * 'password' => '?' |
|
| 451 | - * ) |
|
| 452 | - * ); |
|
| 453 | - * </code> |
|
| 454 | - * |
|
| 455 | - * @param string $insert The table into which the rows should be inserted. |
|
| 456 | - * |
|
| 457 | - * @return $this This QueryBuilder instance. |
|
| 458 | - * @since 8.2.0 |
|
| 459 | - * |
|
| 460 | - * @psalm-taint-sink sql $insert |
|
| 461 | - */ |
|
| 462 | - public function insert($insert = null); |
|
| 463 | - |
|
| 464 | - /** |
|
| 465 | - * Creates and adds a query root corresponding to the table identified by the |
|
| 466 | - * given alias, forming a cartesian product with any existing query roots. |
|
| 467 | - * |
|
| 468 | - * <code> |
|
| 469 | - * $qb = $conn->getQueryBuilder() |
|
| 470 | - * ->select('u.id') |
|
| 471 | - * ->from('users', 'u') |
|
| 472 | - * </code> |
|
| 473 | - * |
|
| 474 | - * @param string $from The table. |
|
| 475 | - * @param string|null $alias The alias of the table. |
|
| 476 | - * |
|
| 477 | - * @return $this This QueryBuilder instance. |
|
| 478 | - * @since 8.2.0 |
|
| 479 | - * |
|
| 480 | - * @psalm-taint-sink sql $from |
|
| 481 | - */ |
|
| 482 | - public function from($from, $alias = null); |
|
| 483 | - |
|
| 484 | - /** |
|
| 485 | - * Creates and adds a join to the query. |
|
| 486 | - * |
|
| 487 | - * <code> |
|
| 488 | - * $qb = $conn->getQueryBuilder() |
|
| 489 | - * ->select('u.name') |
|
| 490 | - * ->from('users', 'u') |
|
| 491 | - * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 492 | - * </code> |
|
| 493 | - * |
|
| 494 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 495 | - * @param string $join The table name to join. |
|
| 496 | - * @param string $alias The alias of the join table. |
|
| 497 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 498 | - * |
|
| 499 | - * @return $this This QueryBuilder instance. |
|
| 500 | - * @since 8.2.0 |
|
| 501 | - * |
|
| 502 | - * @psalm-taint-sink sql $fromAlias |
|
| 503 | - * @psalm-taint-sink sql $join |
|
| 504 | - * @psalm-taint-sink sql $alias |
|
| 505 | - * @psalm-taint-sink sql $condition |
|
| 506 | - */ |
|
| 507 | - public function join($fromAlias, $join, $alias, $condition = null); |
|
| 508 | - |
|
| 509 | - /** |
|
| 510 | - * Creates and adds a join to the query. |
|
| 511 | - * |
|
| 512 | - * <code> |
|
| 513 | - * $qb = $conn->getQueryBuilder() |
|
| 514 | - * ->select('u.name') |
|
| 515 | - * ->from('users', 'u') |
|
| 516 | - * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 517 | - * </code> |
|
| 518 | - * |
|
| 519 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 520 | - * @param string $join The table name to join. |
|
| 521 | - * @param string $alias The alias of the join table. |
|
| 522 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 523 | - * |
|
| 524 | - * @return $this This QueryBuilder instance. |
|
| 525 | - * @since 8.2.0 |
|
| 526 | - * |
|
| 527 | - * @psalm-taint-sink sql $fromAlias |
|
| 528 | - * @psalm-taint-sink sql $join |
|
| 529 | - * @psalm-taint-sink sql $alias |
|
| 530 | - * @psalm-taint-sink sql $condition |
|
| 531 | - */ |
|
| 532 | - public function innerJoin($fromAlias, $join, $alias, $condition = null); |
|
| 533 | - |
|
| 534 | - /** |
|
| 535 | - * Creates and adds a left join to the query. |
|
| 536 | - * |
|
| 537 | - * <code> |
|
| 538 | - * $qb = $conn->getQueryBuilder() |
|
| 539 | - * ->select('u.name') |
|
| 540 | - * ->from('users', 'u') |
|
| 541 | - * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 542 | - * </code> |
|
| 543 | - * |
|
| 544 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 545 | - * @param string $join The table name to join. |
|
| 546 | - * @param string $alias The alias of the join table. |
|
| 547 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 548 | - * |
|
| 549 | - * @return $this This QueryBuilder instance. |
|
| 550 | - * @since 8.2.0 |
|
| 551 | - * |
|
| 552 | - * @psalm-taint-sink sql $fromAlias |
|
| 553 | - * @psalm-taint-sink sql $join |
|
| 554 | - * @psalm-taint-sink sql $alias |
|
| 555 | - * @psalm-taint-sink sql $condition |
|
| 556 | - */ |
|
| 557 | - public function leftJoin($fromAlias, $join, $alias, $condition = null); |
|
| 558 | - |
|
| 559 | - /** |
|
| 560 | - * Creates and adds a right join to the query. |
|
| 561 | - * |
|
| 562 | - * <code> |
|
| 563 | - * $qb = $conn->getQueryBuilder() |
|
| 564 | - * ->select('u.name') |
|
| 565 | - * ->from('users', 'u') |
|
| 566 | - * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 567 | - * </code> |
|
| 568 | - * |
|
| 569 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 570 | - * @param string $join The table name to join. |
|
| 571 | - * @param string $alias The alias of the join table. |
|
| 572 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 573 | - * |
|
| 574 | - * @return $this This QueryBuilder instance. |
|
| 575 | - * @since 8.2.0 |
|
| 576 | - * |
|
| 577 | - * @psalm-taint-sink sql $fromAlias |
|
| 578 | - * @psalm-taint-sink sql $join |
|
| 579 | - * @psalm-taint-sink sql $alias |
|
| 580 | - * @psalm-taint-sink sql $condition |
|
| 581 | - */ |
|
| 582 | - public function rightJoin($fromAlias, $join, $alias, $condition = null); |
|
| 583 | - |
|
| 584 | - /** |
|
| 585 | - * Sets a new value for a column in a bulk update query. |
|
| 586 | - * |
|
| 587 | - * <code> |
|
| 588 | - * $qb = $conn->getQueryBuilder() |
|
| 589 | - * ->update('users', 'u') |
|
| 590 | - * ->set('u.password', md5('password')) |
|
| 591 | - * ->where('u.id = ?'); |
|
| 592 | - * </code> |
|
| 593 | - * |
|
| 594 | - * @param string $key The column to set. |
|
| 595 | - * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc. |
|
| 596 | - * |
|
| 597 | - * @return $this This QueryBuilder instance. |
|
| 598 | - * @since 8.2.0 |
|
| 599 | - * |
|
| 600 | - * @psalm-taint-sink sql $key |
|
| 601 | - * @psalm-taint-sink sql $value |
|
| 602 | - */ |
|
| 603 | - public function set($key, $value); |
|
| 604 | - |
|
| 605 | - /** |
|
| 606 | - * Specifies one or more restrictions to the query result. |
|
| 607 | - * Replaces any previously specified restrictions, if any. |
|
| 608 | - * |
|
| 609 | - * <code> |
|
| 610 | - * $qb = $conn->getQueryBuilder() |
|
| 611 | - * ->select('u.name') |
|
| 612 | - * ->from('users', 'u') |
|
| 613 | - * ->where('u.id = ?'); |
|
| 614 | - * |
|
| 615 | - * // You can optionally programatically build and/or expressions |
|
| 616 | - * $qb = $conn->getQueryBuilder(); |
|
| 617 | - * |
|
| 618 | - * $or = $qb->expr()->orx(); |
|
| 619 | - * $or->add($qb->expr()->eq('u.id', 1)); |
|
| 620 | - * $or->add($qb->expr()->eq('u.id', 2)); |
|
| 621 | - * |
|
| 622 | - * $qb->update('users', 'u') |
|
| 623 | - * ->set('u.password', md5('password')) |
|
| 624 | - * ->where($or); |
|
| 625 | - * </code> |
|
| 626 | - * |
|
| 627 | - * @param mixed $predicates The restriction predicates. |
|
| 628 | - * |
|
| 629 | - * @return $this This QueryBuilder instance. |
|
| 630 | - * @since 8.2.0 |
|
| 631 | - * |
|
| 632 | - * @psalm-taint-sink sql $predicates |
|
| 633 | - */ |
|
| 634 | - public function where(...$predicates); |
|
| 635 | - |
|
| 636 | - /** |
|
| 637 | - * Adds one or more restrictions to the query results, forming a logical |
|
| 638 | - * conjunction with any previously specified restrictions. |
|
| 639 | - * |
|
| 640 | - * <code> |
|
| 641 | - * $qb = $conn->getQueryBuilder() |
|
| 642 | - * ->select('u') |
|
| 643 | - * ->from('users', 'u') |
|
| 644 | - * ->where('u.username LIKE ?') |
|
| 645 | - * ->andWhere('u.is_active = 1'); |
|
| 646 | - * </code> |
|
| 647 | - * |
|
| 648 | - * @param mixed ...$where The query restrictions. |
|
| 649 | - * |
|
| 650 | - * @return $this This QueryBuilder instance. |
|
| 651 | - * |
|
| 652 | - * @see where() |
|
| 653 | - * @since 8.2.0 |
|
| 654 | - * |
|
| 655 | - * @psalm-taint-sink sql $where |
|
| 656 | - */ |
|
| 657 | - public function andWhere(...$where); |
|
| 658 | - |
|
| 659 | - /** |
|
| 660 | - * Adds one or more restrictions to the query results, forming a logical |
|
| 661 | - * disjunction with any previously specified restrictions. |
|
| 662 | - * |
|
| 663 | - * <code> |
|
| 664 | - * $qb = $conn->getQueryBuilder() |
|
| 665 | - * ->select('u.name') |
|
| 666 | - * ->from('users', 'u') |
|
| 667 | - * ->where('u.id = 1') |
|
| 668 | - * ->orWhere('u.id = 2'); |
|
| 669 | - * </code> |
|
| 670 | - * |
|
| 671 | - * @param mixed ...$where The WHERE statement. |
|
| 672 | - * |
|
| 673 | - * @return $this This QueryBuilder instance. |
|
| 674 | - * |
|
| 675 | - * @see where() |
|
| 676 | - * @since 8.2.0 |
|
| 677 | - * |
|
| 678 | - * @psalm-taint-sink sql $where |
|
| 679 | - */ |
|
| 680 | - public function orWhere(...$where); |
|
| 681 | - |
|
| 682 | - /** |
|
| 683 | - * Specifies a grouping over the results of the query. |
|
| 684 | - * Replaces any previously specified groupings, if any. |
|
| 685 | - * |
|
| 686 | - * <code> |
|
| 687 | - * $qb = $conn->getQueryBuilder() |
|
| 688 | - * ->select('u.name') |
|
| 689 | - * ->from('users', 'u') |
|
| 690 | - * ->groupBy('u.id'); |
|
| 691 | - * </code> |
|
| 692 | - * |
|
| 693 | - * @param mixed ...$groupBys The grouping expression. |
|
| 694 | - * |
|
| 695 | - * @return $this This QueryBuilder instance. |
|
| 696 | - * @since 8.2.0 |
|
| 697 | - * |
|
| 698 | - * @psalm-taint-sink sql $groupBys |
|
| 699 | - */ |
|
| 700 | - public function groupBy(...$groupBys); |
|
| 701 | - |
|
| 702 | - /** |
|
| 703 | - * Adds a grouping expression to the query. |
|
| 704 | - * |
|
| 705 | - * <code> |
|
| 706 | - * $qb = $conn->getQueryBuilder() |
|
| 707 | - * ->select('u.name') |
|
| 708 | - * ->from('users', 'u') |
|
| 709 | - * ->groupBy('u.lastLogin'); |
|
| 710 | - * ->addGroupBy('u.createdAt') |
|
| 711 | - * </code> |
|
| 712 | - * |
|
| 713 | - * @param mixed ...$groupBy The grouping expression. |
|
| 714 | - * |
|
| 715 | - * @return $this This QueryBuilder instance. |
|
| 716 | - * @since 8.2.0 |
|
| 717 | - * |
|
| 718 | - * @psalm-taint-sink sql $groupby |
|
| 719 | - */ |
|
| 720 | - public function addGroupBy(...$groupBy); |
|
| 721 | - |
|
| 722 | - /** |
|
| 723 | - * Sets a value for a column in an insert query. |
|
| 724 | - * |
|
| 725 | - * <code> |
|
| 726 | - * $qb = $conn->getQueryBuilder() |
|
| 727 | - * ->insert('users') |
|
| 728 | - * ->values( |
|
| 729 | - * array( |
|
| 730 | - * 'name' => '?' |
|
| 731 | - * ) |
|
| 732 | - * ) |
|
| 733 | - * ->setValue('password', '?'); |
|
| 734 | - * </code> |
|
| 735 | - * |
|
| 736 | - * @param string $column The column into which the value should be inserted. |
|
| 737 | - * @param IParameter|string $value The value that should be inserted into the column. |
|
| 738 | - * |
|
| 739 | - * @return $this This QueryBuilder instance. |
|
| 740 | - * @since 8.2.0 |
|
| 741 | - * |
|
| 742 | - * @psalm-taint-sink sql $column |
|
| 743 | - * @psalm-taint-sink sql $value |
|
| 744 | - */ |
|
| 745 | - public function setValue($column, $value); |
|
| 746 | - |
|
| 747 | - /** |
|
| 748 | - * Specifies values for an insert query indexed by column names. |
|
| 749 | - * Replaces any previous values, if any. |
|
| 750 | - * |
|
| 751 | - * <code> |
|
| 752 | - * $qb = $conn->getQueryBuilder() |
|
| 753 | - * ->insert('users') |
|
| 754 | - * ->values( |
|
| 755 | - * array( |
|
| 756 | - * 'name' => '?', |
|
| 757 | - * 'password' => '?' |
|
| 758 | - * ) |
|
| 759 | - * ); |
|
| 760 | - * </code> |
|
| 761 | - * |
|
| 762 | - * @param array $values The values to specify for the insert query indexed by column names. |
|
| 763 | - * |
|
| 764 | - * @return $this This QueryBuilder instance. |
|
| 765 | - * @since 8.2.0 |
|
| 766 | - * |
|
| 767 | - * @psalm-taint-sink sql $values |
|
| 768 | - */ |
|
| 769 | - public function values(array $values); |
|
| 770 | - |
|
| 771 | - /** |
|
| 772 | - * Specifies a restriction over the groups of the query. |
|
| 773 | - * Replaces any previous having restrictions, if any. |
|
| 774 | - * |
|
| 775 | - * @param mixed ...$having The restriction over the groups. |
|
| 776 | - * |
|
| 777 | - * @return $this This QueryBuilder instance. |
|
| 778 | - * @since 8.2.0 |
|
| 779 | - * |
|
| 780 | - * @psalm-taint-sink sql $having |
|
| 781 | - */ |
|
| 782 | - public function having(...$having); |
|
| 783 | - |
|
| 784 | - /** |
|
| 785 | - * Adds a restriction over the groups of the query, forming a logical |
|
| 786 | - * conjunction with any existing having restrictions. |
|
| 787 | - * |
|
| 788 | - * @param mixed ...$having The restriction to append. |
|
| 789 | - * |
|
| 790 | - * @return $this This QueryBuilder instance. |
|
| 791 | - * @since 8.2.0 |
|
| 792 | - * |
|
| 793 | - * @psalm-taint-sink sql $andHaving |
|
| 794 | - */ |
|
| 795 | - public function andHaving(...$having); |
|
| 796 | - |
|
| 797 | - /** |
|
| 798 | - * Adds a restriction over the groups of the query, forming a logical |
|
| 799 | - * disjunction with any existing having restrictions. |
|
| 800 | - * |
|
| 801 | - * @param mixed ...$having The restriction to add. |
|
| 802 | - * |
|
| 803 | - * @return $this This QueryBuilder instance. |
|
| 804 | - * @since 8.2.0 |
|
| 805 | - * |
|
| 806 | - * @psalm-taint-sink sql $having |
|
| 807 | - */ |
|
| 808 | - public function orHaving(...$having); |
|
| 809 | - |
|
| 810 | - /** |
|
| 811 | - * Specifies an ordering for the query results. |
|
| 812 | - * Replaces any previously specified orderings, if any. |
|
| 813 | - * |
|
| 814 | - * @param string $sort The ordering expression. |
|
| 815 | - * @param string $order The ordering direction. |
|
| 816 | - * |
|
| 817 | - * @return $this This QueryBuilder instance. |
|
| 818 | - * @since 8.2.0 |
|
| 819 | - * |
|
| 820 | - * @psalm-taint-sink sql $sort |
|
| 821 | - * @psalm-taint-sink sql $order |
|
| 822 | - */ |
|
| 823 | - public function orderBy($sort, $order = null); |
|
| 824 | - |
|
| 825 | - /** |
|
| 826 | - * Adds an ordering to the query results. |
|
| 827 | - * |
|
| 828 | - * @param string $sort The ordering expression. |
|
| 829 | - * @param string $order The ordering direction. |
|
| 830 | - * |
|
| 831 | - * @return $this This QueryBuilder instance. |
|
| 832 | - * @since 8.2.0 |
|
| 833 | - * |
|
| 834 | - * @psalm-taint-sink sql $sort |
|
| 835 | - * @psalm-taint-sink sql $order |
|
| 836 | - */ |
|
| 837 | - public function addOrderBy($sort, $order = null); |
|
| 838 | - |
|
| 839 | - /** |
|
| 840 | - * Gets a query part by its name. |
|
| 841 | - * |
|
| 842 | - * @param string $queryPartName |
|
| 843 | - * |
|
| 844 | - * @return mixed |
|
| 845 | - * @since 8.2.0 |
|
| 846 | - */ |
|
| 847 | - public function getQueryPart($queryPartName); |
|
| 848 | - |
|
| 849 | - /** |
|
| 850 | - * Gets all query parts. |
|
| 851 | - * |
|
| 852 | - * @return array |
|
| 853 | - * @since 8.2.0 |
|
| 854 | - */ |
|
| 855 | - public function getQueryParts(); |
|
| 856 | - |
|
| 857 | - /** |
|
| 858 | - * Resets SQL parts. |
|
| 859 | - * |
|
| 860 | - * @param array|null $queryPartNames |
|
| 861 | - * |
|
| 862 | - * @return $this This QueryBuilder instance. |
|
| 863 | - * @since 8.2.0 |
|
| 864 | - */ |
|
| 865 | - public function resetQueryParts($queryPartNames = null); |
|
| 866 | - |
|
| 867 | - /** |
|
| 868 | - * Resets a single SQL part. |
|
| 869 | - * |
|
| 870 | - * @param string $queryPartName |
|
| 871 | - * |
|
| 872 | - * @return $this This QueryBuilder instance. |
|
| 873 | - * @since 8.2.0 |
|
| 874 | - */ |
|
| 875 | - public function resetQueryPart($queryPartName); |
|
| 876 | - |
|
| 877 | - /** |
|
| 878 | - * Creates a new named parameter and bind the value $value to it. |
|
| 879 | - * |
|
| 880 | - * This method provides a shortcut for PDOStatement::bindValue |
|
| 881 | - * when using prepared statements. |
|
| 882 | - * |
|
| 883 | - * The parameter $value specifies the value that you want to bind. If |
|
| 884 | - * $placeholder is not provided bindValue() will automatically create a |
|
| 885 | - * placeholder for you. An automatic placeholder will be of the name |
|
| 886 | - * ':dcValue1', ':dcValue2' etc. |
|
| 887 | - * |
|
| 888 | - * For more information see {@link https://www.php.net/pdostatement-bindparam} |
|
| 889 | - * |
|
| 890 | - * Example: |
|
| 891 | - * <code> |
|
| 892 | - * $value = 2; |
|
| 893 | - * $q->eq( 'id', $q->bindValue( $value ) ); |
|
| 894 | - * $stmt = $q->executeQuery(); // executed with 'id = 2' |
|
| 895 | - * </code> |
|
| 896 | - * |
|
| 897 | - * @license New BSD License |
|
| 898 | - * @link http://www.zetacomponents.org |
|
| 899 | - * |
|
| 900 | - * @param mixed $value |
|
| 901 | - * @param mixed $type |
|
| 902 | - * @param string $placeHolder The name to bind with. The string must start with a colon ':'. |
|
| 903 | - * |
|
| 904 | - * @return IParameter |
|
| 905 | - * @since 8.2.0 |
|
| 906 | - * |
|
| 907 | - * @psalm-taint-escape sql |
|
| 908 | - */ |
|
| 909 | - public function createNamedParameter($value, $type = self::PARAM_STR, $placeHolder = null); |
|
| 910 | - |
|
| 911 | - /** |
|
| 912 | - * Creates a new positional parameter and bind the given value to it. |
|
| 913 | - * |
|
| 914 | - * Attention: If you are using positional parameters with the query builder you have |
|
| 915 | - * to be very careful to bind all parameters in the order they appear in the SQL |
|
| 916 | - * statement , otherwise they get bound in the wrong order which can lead to serious |
|
| 917 | - * bugs in your code. |
|
| 918 | - * |
|
| 919 | - * Example: |
|
| 920 | - * <code> |
|
| 921 | - * $qb = $conn->getQueryBuilder(); |
|
| 922 | - * $qb->select('u.*') |
|
| 923 | - * ->from('users', 'u') |
|
| 924 | - * ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR)) |
|
| 925 | - * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR)) |
|
| 926 | - * </code> |
|
| 927 | - * |
|
| 928 | - * @param mixed $value |
|
| 929 | - * @param integer $type |
|
| 930 | - * |
|
| 931 | - * @return IParameter |
|
| 932 | - * @since 8.2.0 |
|
| 933 | - * |
|
| 934 | - * @psalm-taint-escape sql |
|
| 935 | - */ |
|
| 936 | - public function createPositionalParameter($value, $type = self::PARAM_STR); |
|
| 937 | - |
|
| 938 | - /** |
|
| 939 | - * Creates a new parameter |
|
| 940 | - * |
|
| 941 | - * Example: |
|
| 942 | - * <code> |
|
| 943 | - * $qb = $conn->getQueryBuilder(); |
|
| 944 | - * $qb->select('u.*') |
|
| 945 | - * ->from('users', 'u') |
|
| 946 | - * ->where('u.username = ' . $qb->createParameter('name')) |
|
| 947 | - * ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR)) |
|
| 948 | - * </code> |
|
| 949 | - * |
|
| 950 | - * @param string $name |
|
| 951 | - * |
|
| 952 | - * @return IParameter |
|
| 953 | - * @since 8.2.0 |
|
| 954 | - * |
|
| 955 | - * @psalm-taint-escape sql |
|
| 956 | - */ |
|
| 957 | - public function createParameter($name); |
|
| 958 | - |
|
| 959 | - /** |
|
| 960 | - * Creates a new function |
|
| 961 | - * |
|
| 962 | - * Attention: Column names inside the call have to be quoted before hand |
|
| 963 | - * |
|
| 964 | - * Example: |
|
| 965 | - * <code> |
|
| 966 | - * $qb = $conn->getQueryBuilder(); |
|
| 967 | - * $qb->select($qb->createFunction('COUNT(*)')) |
|
| 968 | - * ->from('users', 'u') |
|
| 969 | - * echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u |
|
| 970 | - * </code> |
|
| 971 | - * <code> |
|
| 972 | - * $qb = $conn->getQueryBuilder(); |
|
| 973 | - * $qb->select($qb->createFunction('COUNT(`column`)')) |
|
| 974 | - * ->from('users', 'u') |
|
| 975 | - * echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u |
|
| 976 | - * </code> |
|
| 977 | - * |
|
| 978 | - * @param string $call |
|
| 979 | - * |
|
| 980 | - * @return IQueryFunction |
|
| 981 | - * @since 8.2.0 |
|
| 982 | - * |
|
| 983 | - * @psalm-taint-sink sql |
|
| 984 | - */ |
|
| 985 | - public function createFunction($call); |
|
| 986 | - |
|
| 987 | - /** |
|
| 988 | - * Used to get the id of the last inserted element |
|
| 989 | - * @return int |
|
| 990 | - * @throws \BadMethodCallException When being called before an insert query has been run. |
|
| 991 | - * @since 9.0.0 |
|
| 992 | - */ |
|
| 993 | - public function getLastInsertId(): int; |
|
| 994 | - |
|
| 995 | - /** |
|
| 996 | - * Returns the table name quoted and with database prefix as needed by the implementation |
|
| 997 | - * |
|
| 998 | - * @param string $table |
|
| 999 | - * @return string |
|
| 1000 | - * @since 9.0.0 |
|
| 1001 | - */ |
|
| 1002 | - public function getTableName($table); |
|
| 1003 | - |
|
| 1004 | - /** |
|
| 1005 | - * Returns the column name quoted and with table alias prefix as needed by the implementation |
|
| 1006 | - * |
|
| 1007 | - * @param string $column |
|
| 1008 | - * @param string $tableAlias |
|
| 1009 | - * @return string |
|
| 1010 | - * @since 9.0.0 |
|
| 1011 | - */ |
|
| 1012 | - public function getColumnName($column, $tableAlias = ''); |
|
| 43 | + /** |
|
| 44 | + * @since 9.0.0 |
|
| 45 | + */ |
|
| 46 | + public const PARAM_NULL = \PDO::PARAM_NULL; |
|
| 47 | + /** |
|
| 48 | + * @since 9.0.0 |
|
| 49 | + */ |
|
| 50 | + public const PARAM_BOOL = \PDO::PARAM_BOOL; |
|
| 51 | + /** |
|
| 52 | + * @since 9.0.0 |
|
| 53 | + */ |
|
| 54 | + public const PARAM_INT = \PDO::PARAM_INT; |
|
| 55 | + /** |
|
| 56 | + * @since 9.0.0 |
|
| 57 | + */ |
|
| 58 | + public const PARAM_STR = \PDO::PARAM_STR; |
|
| 59 | + /** |
|
| 60 | + * @since 9.0.0 |
|
| 61 | + */ |
|
| 62 | + public const PARAM_LOB = \PDO::PARAM_LOB; |
|
| 63 | + /** |
|
| 64 | + * @since 9.0.0 |
|
| 65 | + */ |
|
| 66 | + public const PARAM_DATE = 'datetime'; |
|
| 67 | + |
|
| 68 | + /** |
|
| 69 | + * @since 9.0.0 |
|
| 70 | + */ |
|
| 71 | + public const PARAM_INT_ARRAY = Connection::PARAM_INT_ARRAY; |
|
| 72 | + /** |
|
| 73 | + * @since 9.0.0 |
|
| 74 | + */ |
|
| 75 | + public const PARAM_STR_ARRAY = Connection::PARAM_STR_ARRAY; |
|
| 76 | + |
|
| 77 | + |
|
| 78 | + /** |
|
| 79 | + * Enable/disable automatic prefixing of table names with the oc_ prefix |
|
| 80 | + * |
|
| 81 | + * @param bool $enabled If set to true table names will be prefixed with the |
|
| 82 | + * owncloud database prefix automatically. |
|
| 83 | + * @since 8.2.0 |
|
| 84 | + */ |
|
| 85 | + public function automaticTablePrefix($enabled); |
|
| 86 | + |
|
| 87 | + /** |
|
| 88 | + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. |
|
| 89 | + * This producer method is intended for convenient inline usage. Example: |
|
| 90 | + * |
|
| 91 | + * <code> |
|
| 92 | + * $qb = $conn->getQueryBuilder() |
|
| 93 | + * ->select('u') |
|
| 94 | + * ->from('users', 'u') |
|
| 95 | + * ->where($qb->expr()->eq('u.id', 1)); |
|
| 96 | + * </code> |
|
| 97 | + * |
|
| 98 | + * For more complex expression construction, consider storing the expression |
|
| 99 | + * builder object in a local variable. |
|
| 100 | + * |
|
| 101 | + * @return \OCP\DB\QueryBuilder\IExpressionBuilder |
|
| 102 | + * @since 8.2.0 |
|
| 103 | + */ |
|
| 104 | + public function expr(); |
|
| 105 | + |
|
| 106 | + /** |
|
| 107 | + * Gets an FunctionBuilder used for object-oriented construction of query functions. |
|
| 108 | + * This producer method is intended for convenient inline usage. Example: |
|
| 109 | + * |
|
| 110 | + * <code> |
|
| 111 | + * $qb = $conn->getQueryBuilder() |
|
| 112 | + * ->select('u') |
|
| 113 | + * ->from('users', 'u') |
|
| 114 | + * ->where($qb->fun()->md5('u.id')); |
|
| 115 | + * </code> |
|
| 116 | + * |
|
| 117 | + * For more complex function construction, consider storing the function |
|
| 118 | + * builder object in a local variable. |
|
| 119 | + * |
|
| 120 | + * @return \OCP\DB\QueryBuilder\IFunctionBuilder |
|
| 121 | + * @since 12.0.0 |
|
| 122 | + */ |
|
| 123 | + public function func(); |
|
| 124 | + |
|
| 125 | + /** |
|
| 126 | + * Gets the type of the currently built query. |
|
| 127 | + * |
|
| 128 | + * @return integer |
|
| 129 | + * @since 8.2.0 |
|
| 130 | + */ |
|
| 131 | + public function getType(); |
|
| 132 | + |
|
| 133 | + /** |
|
| 134 | + * Gets the associated DBAL Connection for this query builder. |
|
| 135 | + * |
|
| 136 | + * @return \OCP\IDBConnection |
|
| 137 | + * @since 8.2.0 |
|
| 138 | + */ |
|
| 139 | + public function getConnection(); |
|
| 140 | + |
|
| 141 | + /** |
|
| 142 | + * Gets the state of this query builder instance. |
|
| 143 | + * |
|
| 144 | + * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. |
|
| 145 | + * @since 8.2.0 |
|
| 146 | + */ |
|
| 147 | + public function getState(); |
|
| 148 | + |
|
| 149 | + /** |
|
| 150 | + * Executes this query using the bound parameters and their types. |
|
| 151 | + * |
|
| 152 | + * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeStatement} |
|
| 153 | + * for insert, update and delete statements. |
|
| 154 | + * |
|
| 155 | + * Warning: until Nextcloud 20, this method could return a \Doctrine\DBAL\Driver\Statement but since |
|
| 156 | + * that interface changed in a breaking way the adapter \OCP\DB\QueryBuilder\IStatement is returned |
|
| 157 | + * to bridge old code to the new API |
|
| 158 | + * |
|
| 159 | + * @return IResult|int |
|
| 160 | + * @throws Exception since 21.0.0 |
|
| 161 | + * @since 8.2.0 |
|
| 162 | + * @deprecated 22.0.0 Use executeQuery or executeUpdate |
|
| 163 | + */ |
|
| 164 | + public function execute(); |
|
| 165 | + |
|
| 166 | + /** |
|
| 167 | + * Execute for select statements |
|
| 168 | + * |
|
| 169 | + * @return IResult |
|
| 170 | + * @since 22.0.0 |
|
| 171 | + * |
|
| 172 | + * @throws Exception |
|
| 173 | + * @throws \RuntimeException in case of usage with non select query |
|
| 174 | + */ |
|
| 175 | + public function executeQuery(): IResult; |
|
| 176 | + |
|
| 177 | + /** |
|
| 178 | + * Execute insert, update and delete statements |
|
| 179 | + * |
|
| 180 | + * @return int the number of affected rows |
|
| 181 | + * @since 22.0.0 |
|
| 182 | + * |
|
| 183 | + * @throws Exception |
|
| 184 | + * @throws \RuntimeException in case of usage with select query |
|
| 185 | + */ |
|
| 186 | + public function executeStatement(): int; |
|
| 187 | + |
|
| 188 | + /** |
|
| 189 | + * Gets the complete SQL string formed by the current specifications of this QueryBuilder. |
|
| 190 | + * |
|
| 191 | + * <code> |
|
| 192 | + * $qb = $conn->getQueryBuilder() |
|
| 193 | + * ->select('u') |
|
| 194 | + * ->from('User', 'u') |
|
| 195 | + * echo $qb->getSQL(); // SELECT u FROM User u |
|
| 196 | + * </code> |
|
| 197 | + * |
|
| 198 | + * @return string The SQL query string. |
|
| 199 | + * @since 8.2.0 |
|
| 200 | + */ |
|
| 201 | + public function getSQL(); |
|
| 202 | + |
|
| 203 | + /** |
|
| 204 | + * Sets a query parameter for the query being constructed. |
|
| 205 | + * |
|
| 206 | + * <code> |
|
| 207 | + * $qb = $conn->getQueryBuilder() |
|
| 208 | + * ->select('u') |
|
| 209 | + * ->from('users', 'u') |
|
| 210 | + * ->where('u.id = :user_id') |
|
| 211 | + * ->setParameter(':user_id', 1); |
|
| 212 | + * </code> |
|
| 213 | + * |
|
| 214 | + * @param string|integer $key The parameter position or name. |
|
| 215 | + * @param mixed $value The parameter value. |
|
| 216 | + * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants. |
|
| 217 | + * |
|
| 218 | + * @return $this This QueryBuilder instance. |
|
| 219 | + * @since 8.2.0 |
|
| 220 | + */ |
|
| 221 | + public function setParameter($key, $value, $type = null); |
|
| 222 | + |
|
| 223 | + /** |
|
| 224 | + * Sets a collection of query parameters for the query being constructed. |
|
| 225 | + * |
|
| 226 | + * <code> |
|
| 227 | + * $qb = $conn->getQueryBuilder() |
|
| 228 | + * ->select('u') |
|
| 229 | + * ->from('users', 'u') |
|
| 230 | + * ->where('u.id = :user_id1 OR u.id = :user_id2') |
|
| 231 | + * ->setParameters(array( |
|
| 232 | + * ':user_id1' => 1, |
|
| 233 | + * ':user_id2' => 2 |
|
| 234 | + * )); |
|
| 235 | + * </code> |
|
| 236 | + * |
|
| 237 | + * @param array $params The query parameters to set. |
|
| 238 | + * @param array $types The query parameters types to set. |
|
| 239 | + * |
|
| 240 | + * @return $this This QueryBuilder instance. |
|
| 241 | + * @since 8.2.0 |
|
| 242 | + */ |
|
| 243 | + public function setParameters(array $params, array $types = []); |
|
| 244 | + |
|
| 245 | + /** |
|
| 246 | + * Gets all defined query parameters for the query being constructed indexed by parameter index or name. |
|
| 247 | + * |
|
| 248 | + * @return array The currently defined query parameters indexed by parameter index or name. |
|
| 249 | + * @since 8.2.0 |
|
| 250 | + */ |
|
| 251 | + public function getParameters(); |
|
| 252 | + |
|
| 253 | + /** |
|
| 254 | + * Gets a (previously set) query parameter of the query being constructed. |
|
| 255 | + * |
|
| 256 | + * @param mixed $key The key (index or name) of the bound parameter. |
|
| 257 | + * |
|
| 258 | + * @return mixed The value of the bound parameter. |
|
| 259 | + * @since 8.2.0 |
|
| 260 | + */ |
|
| 261 | + public function getParameter($key); |
|
| 262 | + |
|
| 263 | + /** |
|
| 264 | + * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. |
|
| 265 | + * |
|
| 266 | + * @return array The currently defined query parameter types indexed by parameter index or name. |
|
| 267 | + * @since 8.2.0 |
|
| 268 | + */ |
|
| 269 | + public function getParameterTypes(); |
|
| 270 | + |
|
| 271 | + /** |
|
| 272 | + * Gets a (previously set) query parameter type of the query being constructed. |
|
| 273 | + * |
|
| 274 | + * @param mixed $key The key (index or name) of the bound parameter type. |
|
| 275 | + * |
|
| 276 | + * @return mixed The value of the bound parameter type. |
|
| 277 | + * @since 8.2.0 |
|
| 278 | + */ |
|
| 279 | + public function getParameterType($key); |
|
| 280 | + |
|
| 281 | + /** |
|
| 282 | + * Sets the position of the first result to retrieve (the "offset"). |
|
| 283 | + * |
|
| 284 | + * @param integer $firstResult The first result to return. |
|
| 285 | + * |
|
| 286 | + * @return $this This QueryBuilder instance. |
|
| 287 | + * @since 8.2.0 |
|
| 288 | + */ |
|
| 289 | + public function setFirstResult($firstResult); |
|
| 290 | + |
|
| 291 | + /** |
|
| 292 | + * Gets the position of the first result the query object was set to retrieve (the "offset"). |
|
| 293 | + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. |
|
| 294 | + * |
|
| 295 | + * @return integer The position of the first result. |
|
| 296 | + * @since 8.2.0 |
|
| 297 | + */ |
|
| 298 | + public function getFirstResult(); |
|
| 299 | + |
|
| 300 | + /** |
|
| 301 | + * Sets the maximum number of results to retrieve (the "limit"). |
|
| 302 | + * |
|
| 303 | + * @param integer $maxResults The maximum number of results to retrieve. |
|
| 304 | + * |
|
| 305 | + * @return $this This QueryBuilder instance. |
|
| 306 | + * @since 8.2.0 |
|
| 307 | + */ |
|
| 308 | + public function setMaxResults($maxResults); |
|
| 309 | + |
|
| 310 | + /** |
|
| 311 | + * Gets the maximum number of results the query object was set to retrieve (the "limit"). |
|
| 312 | + * Returns NULL if {@link setMaxResults} was not applied to this query builder. |
|
| 313 | + * |
|
| 314 | + * @return int|null The maximum number of results. |
|
| 315 | + * @since 8.2.0 |
|
| 316 | + */ |
|
| 317 | + public function getMaxResults(); |
|
| 318 | + |
|
| 319 | + /** |
|
| 320 | + * Specifies an item that is to be returned in the query result. |
|
| 321 | + * Replaces any previously specified selections, if any. |
|
| 322 | + * |
|
| 323 | + * <code> |
|
| 324 | + * $qb = $conn->getQueryBuilder() |
|
| 325 | + * ->select('u.id', 'p.id') |
|
| 326 | + * ->from('users', 'u') |
|
| 327 | + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 328 | + * </code> |
|
| 329 | + * |
|
| 330 | + * @param mixed ...$selects The selection expressions. |
|
| 331 | + * |
|
| 332 | + * @return $this This QueryBuilder instance. |
|
| 333 | + * @since 8.2.0 |
|
| 334 | + * |
|
| 335 | + * @psalm-taint-sink sql $selects |
|
| 336 | + */ |
|
| 337 | + public function select(...$selects); |
|
| 338 | + |
|
| 339 | + /** |
|
| 340 | + * Specifies an item that is to be returned with a different name in the query result. |
|
| 341 | + * |
|
| 342 | + * <code> |
|
| 343 | + * $qb = $conn->getQueryBuilder() |
|
| 344 | + * ->selectAlias('u.id', 'user_id') |
|
| 345 | + * ->from('users', 'u') |
|
| 346 | + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 347 | + * </code> |
|
| 348 | + * |
|
| 349 | + * @param mixed $select The selection expressions. |
|
| 350 | + * @param string $alias The column alias used in the constructed query. |
|
| 351 | + * |
|
| 352 | + * @return $this This QueryBuilder instance. |
|
| 353 | + * @since 8.2.1 |
|
| 354 | + * |
|
| 355 | + * @psalm-taint-sink sql $select |
|
| 356 | + * @psalm-taint-sink sql $alias |
|
| 357 | + */ |
|
| 358 | + public function selectAlias($select, $alias); |
|
| 359 | + |
|
| 360 | + /** |
|
| 361 | + * Specifies an item that is to be returned uniquely in the query result. |
|
| 362 | + * |
|
| 363 | + * <code> |
|
| 364 | + * $qb = $conn->getQueryBuilder() |
|
| 365 | + * ->selectDistinct('type') |
|
| 366 | + * ->from('users'); |
|
| 367 | + * </code> |
|
| 368 | + * |
|
| 369 | + * @param mixed $select The selection expressions. |
|
| 370 | + * |
|
| 371 | + * @return $this This QueryBuilder instance. |
|
| 372 | + * @since 9.0.0 |
|
| 373 | + * |
|
| 374 | + * @psalm-taint-sink sql $select |
|
| 375 | + */ |
|
| 376 | + public function selectDistinct($select); |
|
| 377 | + |
|
| 378 | + /** |
|
| 379 | + * Adds an item that is to be returned in the query result. |
|
| 380 | + * |
|
| 381 | + * <code> |
|
| 382 | + * $qb = $conn->getQueryBuilder() |
|
| 383 | + * ->select('u.id') |
|
| 384 | + * ->addSelect('p.id') |
|
| 385 | + * ->from('users', 'u') |
|
| 386 | + * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); |
|
| 387 | + * </code> |
|
| 388 | + * |
|
| 389 | + * @param mixed ...$select The selection expression. |
|
| 390 | + * |
|
| 391 | + * @return $this This QueryBuilder instance. |
|
| 392 | + * @since 8.2.0 |
|
| 393 | + * |
|
| 394 | + * @psalm-taint-sink sql $select |
|
| 395 | + */ |
|
| 396 | + public function addSelect(...$select); |
|
| 397 | + |
|
| 398 | + /** |
|
| 399 | + * Turns the query being built into a bulk delete query that ranges over |
|
| 400 | + * a certain table. |
|
| 401 | + * |
|
| 402 | + * <code> |
|
| 403 | + * $qb = $conn->getQueryBuilder() |
|
| 404 | + * ->delete('users', 'u') |
|
| 405 | + * ->where('u.id = :user_id'); |
|
| 406 | + * ->setParameter(':user_id', 1); |
|
| 407 | + * </code> |
|
| 408 | + * |
|
| 409 | + * @param string $delete The table whose rows are subject to the deletion. |
|
| 410 | + * @param string $alias The table alias used in the constructed query. |
|
| 411 | + * |
|
| 412 | + * @return $this This QueryBuilder instance. |
|
| 413 | + * @since 8.2.0 |
|
| 414 | + * |
|
| 415 | + * @psalm-taint-sink sql $delete |
|
| 416 | + */ |
|
| 417 | + public function delete($delete = null, $alias = null); |
|
| 418 | + |
|
| 419 | + /** |
|
| 420 | + * Turns the query being built into a bulk update query that ranges over |
|
| 421 | + * a certain table |
|
| 422 | + * |
|
| 423 | + * <code> |
|
| 424 | + * $qb = $conn->getQueryBuilder() |
|
| 425 | + * ->update('users', 'u') |
|
| 426 | + * ->set('u.password', md5('password')) |
|
| 427 | + * ->where('u.id = ?'); |
|
| 428 | + * </code> |
|
| 429 | + * |
|
| 430 | + * @param string $update The table whose rows are subject to the update. |
|
| 431 | + * @param string $alias The table alias used in the constructed query. |
|
| 432 | + * |
|
| 433 | + * @return $this This QueryBuilder instance. |
|
| 434 | + * @since 8.2.0 |
|
| 435 | + * |
|
| 436 | + * @psalm-taint-sink sql $update |
|
| 437 | + */ |
|
| 438 | + public function update($update = null, $alias = null); |
|
| 439 | + |
|
| 440 | + /** |
|
| 441 | + * Turns the query being built into an insert query that inserts into |
|
| 442 | + * a certain table |
|
| 443 | + * |
|
| 444 | + * <code> |
|
| 445 | + * $qb = $conn->getQueryBuilder() |
|
| 446 | + * ->insert('users') |
|
| 447 | + * ->values( |
|
| 448 | + * array( |
|
| 449 | + * 'name' => '?', |
|
| 450 | + * 'password' => '?' |
|
| 451 | + * ) |
|
| 452 | + * ); |
|
| 453 | + * </code> |
|
| 454 | + * |
|
| 455 | + * @param string $insert The table into which the rows should be inserted. |
|
| 456 | + * |
|
| 457 | + * @return $this This QueryBuilder instance. |
|
| 458 | + * @since 8.2.0 |
|
| 459 | + * |
|
| 460 | + * @psalm-taint-sink sql $insert |
|
| 461 | + */ |
|
| 462 | + public function insert($insert = null); |
|
| 463 | + |
|
| 464 | + /** |
|
| 465 | + * Creates and adds a query root corresponding to the table identified by the |
|
| 466 | + * given alias, forming a cartesian product with any existing query roots. |
|
| 467 | + * |
|
| 468 | + * <code> |
|
| 469 | + * $qb = $conn->getQueryBuilder() |
|
| 470 | + * ->select('u.id') |
|
| 471 | + * ->from('users', 'u') |
|
| 472 | + * </code> |
|
| 473 | + * |
|
| 474 | + * @param string $from The table. |
|
| 475 | + * @param string|null $alias The alias of the table. |
|
| 476 | + * |
|
| 477 | + * @return $this This QueryBuilder instance. |
|
| 478 | + * @since 8.2.0 |
|
| 479 | + * |
|
| 480 | + * @psalm-taint-sink sql $from |
|
| 481 | + */ |
|
| 482 | + public function from($from, $alias = null); |
|
| 483 | + |
|
| 484 | + /** |
|
| 485 | + * Creates and adds a join to the query. |
|
| 486 | + * |
|
| 487 | + * <code> |
|
| 488 | + * $qb = $conn->getQueryBuilder() |
|
| 489 | + * ->select('u.name') |
|
| 490 | + * ->from('users', 'u') |
|
| 491 | + * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 492 | + * </code> |
|
| 493 | + * |
|
| 494 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 495 | + * @param string $join The table name to join. |
|
| 496 | + * @param string $alias The alias of the join table. |
|
| 497 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 498 | + * |
|
| 499 | + * @return $this This QueryBuilder instance. |
|
| 500 | + * @since 8.2.0 |
|
| 501 | + * |
|
| 502 | + * @psalm-taint-sink sql $fromAlias |
|
| 503 | + * @psalm-taint-sink sql $join |
|
| 504 | + * @psalm-taint-sink sql $alias |
|
| 505 | + * @psalm-taint-sink sql $condition |
|
| 506 | + */ |
|
| 507 | + public function join($fromAlias, $join, $alias, $condition = null); |
|
| 508 | + |
|
| 509 | + /** |
|
| 510 | + * Creates and adds a join to the query. |
|
| 511 | + * |
|
| 512 | + * <code> |
|
| 513 | + * $qb = $conn->getQueryBuilder() |
|
| 514 | + * ->select('u.name') |
|
| 515 | + * ->from('users', 'u') |
|
| 516 | + * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 517 | + * </code> |
|
| 518 | + * |
|
| 519 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 520 | + * @param string $join The table name to join. |
|
| 521 | + * @param string $alias The alias of the join table. |
|
| 522 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 523 | + * |
|
| 524 | + * @return $this This QueryBuilder instance. |
|
| 525 | + * @since 8.2.0 |
|
| 526 | + * |
|
| 527 | + * @psalm-taint-sink sql $fromAlias |
|
| 528 | + * @psalm-taint-sink sql $join |
|
| 529 | + * @psalm-taint-sink sql $alias |
|
| 530 | + * @psalm-taint-sink sql $condition |
|
| 531 | + */ |
|
| 532 | + public function innerJoin($fromAlias, $join, $alias, $condition = null); |
|
| 533 | + |
|
| 534 | + /** |
|
| 535 | + * Creates and adds a left join to the query. |
|
| 536 | + * |
|
| 537 | + * <code> |
|
| 538 | + * $qb = $conn->getQueryBuilder() |
|
| 539 | + * ->select('u.name') |
|
| 540 | + * ->from('users', 'u') |
|
| 541 | + * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 542 | + * </code> |
|
| 543 | + * |
|
| 544 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 545 | + * @param string $join The table name to join. |
|
| 546 | + * @param string $alias The alias of the join table. |
|
| 547 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 548 | + * |
|
| 549 | + * @return $this This QueryBuilder instance. |
|
| 550 | + * @since 8.2.0 |
|
| 551 | + * |
|
| 552 | + * @psalm-taint-sink sql $fromAlias |
|
| 553 | + * @psalm-taint-sink sql $join |
|
| 554 | + * @psalm-taint-sink sql $alias |
|
| 555 | + * @psalm-taint-sink sql $condition |
|
| 556 | + */ |
|
| 557 | + public function leftJoin($fromAlias, $join, $alias, $condition = null); |
|
| 558 | + |
|
| 559 | + /** |
|
| 560 | + * Creates and adds a right join to the query. |
|
| 561 | + * |
|
| 562 | + * <code> |
|
| 563 | + * $qb = $conn->getQueryBuilder() |
|
| 564 | + * ->select('u.name') |
|
| 565 | + * ->from('users', 'u') |
|
| 566 | + * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 567 | + * </code> |
|
| 568 | + * |
|
| 569 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 570 | + * @param string $join The table name to join. |
|
| 571 | + * @param string $alias The alias of the join table. |
|
| 572 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 573 | + * |
|
| 574 | + * @return $this This QueryBuilder instance. |
|
| 575 | + * @since 8.2.0 |
|
| 576 | + * |
|
| 577 | + * @psalm-taint-sink sql $fromAlias |
|
| 578 | + * @psalm-taint-sink sql $join |
|
| 579 | + * @psalm-taint-sink sql $alias |
|
| 580 | + * @psalm-taint-sink sql $condition |
|
| 581 | + */ |
|
| 582 | + public function rightJoin($fromAlias, $join, $alias, $condition = null); |
|
| 583 | + |
|
| 584 | + /** |
|
| 585 | + * Sets a new value for a column in a bulk update query. |
|
| 586 | + * |
|
| 587 | + * <code> |
|
| 588 | + * $qb = $conn->getQueryBuilder() |
|
| 589 | + * ->update('users', 'u') |
|
| 590 | + * ->set('u.password', md5('password')) |
|
| 591 | + * ->where('u.id = ?'); |
|
| 592 | + * </code> |
|
| 593 | + * |
|
| 594 | + * @param string $key The column to set. |
|
| 595 | + * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc. |
|
| 596 | + * |
|
| 597 | + * @return $this This QueryBuilder instance. |
|
| 598 | + * @since 8.2.0 |
|
| 599 | + * |
|
| 600 | + * @psalm-taint-sink sql $key |
|
| 601 | + * @psalm-taint-sink sql $value |
|
| 602 | + */ |
|
| 603 | + public function set($key, $value); |
|
| 604 | + |
|
| 605 | + /** |
|
| 606 | + * Specifies one or more restrictions to the query result. |
|
| 607 | + * Replaces any previously specified restrictions, if any. |
|
| 608 | + * |
|
| 609 | + * <code> |
|
| 610 | + * $qb = $conn->getQueryBuilder() |
|
| 611 | + * ->select('u.name') |
|
| 612 | + * ->from('users', 'u') |
|
| 613 | + * ->where('u.id = ?'); |
|
| 614 | + * |
|
| 615 | + * // You can optionally programatically build and/or expressions |
|
| 616 | + * $qb = $conn->getQueryBuilder(); |
|
| 617 | + * |
|
| 618 | + * $or = $qb->expr()->orx(); |
|
| 619 | + * $or->add($qb->expr()->eq('u.id', 1)); |
|
| 620 | + * $or->add($qb->expr()->eq('u.id', 2)); |
|
| 621 | + * |
|
| 622 | + * $qb->update('users', 'u') |
|
| 623 | + * ->set('u.password', md5('password')) |
|
| 624 | + * ->where($or); |
|
| 625 | + * </code> |
|
| 626 | + * |
|
| 627 | + * @param mixed $predicates The restriction predicates. |
|
| 628 | + * |
|
| 629 | + * @return $this This QueryBuilder instance. |
|
| 630 | + * @since 8.2.0 |
|
| 631 | + * |
|
| 632 | + * @psalm-taint-sink sql $predicates |
|
| 633 | + */ |
|
| 634 | + public function where(...$predicates); |
|
| 635 | + |
|
| 636 | + /** |
|
| 637 | + * Adds one or more restrictions to the query results, forming a logical |
|
| 638 | + * conjunction with any previously specified restrictions. |
|
| 639 | + * |
|
| 640 | + * <code> |
|
| 641 | + * $qb = $conn->getQueryBuilder() |
|
| 642 | + * ->select('u') |
|
| 643 | + * ->from('users', 'u') |
|
| 644 | + * ->where('u.username LIKE ?') |
|
| 645 | + * ->andWhere('u.is_active = 1'); |
|
| 646 | + * </code> |
|
| 647 | + * |
|
| 648 | + * @param mixed ...$where The query restrictions. |
|
| 649 | + * |
|
| 650 | + * @return $this This QueryBuilder instance. |
|
| 651 | + * |
|
| 652 | + * @see where() |
|
| 653 | + * @since 8.2.0 |
|
| 654 | + * |
|
| 655 | + * @psalm-taint-sink sql $where |
|
| 656 | + */ |
|
| 657 | + public function andWhere(...$where); |
|
| 658 | + |
|
| 659 | + /** |
|
| 660 | + * Adds one or more restrictions to the query results, forming a logical |
|
| 661 | + * disjunction with any previously specified restrictions. |
|
| 662 | + * |
|
| 663 | + * <code> |
|
| 664 | + * $qb = $conn->getQueryBuilder() |
|
| 665 | + * ->select('u.name') |
|
| 666 | + * ->from('users', 'u') |
|
| 667 | + * ->where('u.id = 1') |
|
| 668 | + * ->orWhere('u.id = 2'); |
|
| 669 | + * </code> |
|
| 670 | + * |
|
| 671 | + * @param mixed ...$where The WHERE statement. |
|
| 672 | + * |
|
| 673 | + * @return $this This QueryBuilder instance. |
|
| 674 | + * |
|
| 675 | + * @see where() |
|
| 676 | + * @since 8.2.0 |
|
| 677 | + * |
|
| 678 | + * @psalm-taint-sink sql $where |
|
| 679 | + */ |
|
| 680 | + public function orWhere(...$where); |
|
| 681 | + |
|
| 682 | + /** |
|
| 683 | + * Specifies a grouping over the results of the query. |
|
| 684 | + * Replaces any previously specified groupings, if any. |
|
| 685 | + * |
|
| 686 | + * <code> |
|
| 687 | + * $qb = $conn->getQueryBuilder() |
|
| 688 | + * ->select('u.name') |
|
| 689 | + * ->from('users', 'u') |
|
| 690 | + * ->groupBy('u.id'); |
|
| 691 | + * </code> |
|
| 692 | + * |
|
| 693 | + * @param mixed ...$groupBys The grouping expression. |
|
| 694 | + * |
|
| 695 | + * @return $this This QueryBuilder instance. |
|
| 696 | + * @since 8.2.0 |
|
| 697 | + * |
|
| 698 | + * @psalm-taint-sink sql $groupBys |
|
| 699 | + */ |
|
| 700 | + public function groupBy(...$groupBys); |
|
| 701 | + |
|
| 702 | + /** |
|
| 703 | + * Adds a grouping expression to the query. |
|
| 704 | + * |
|
| 705 | + * <code> |
|
| 706 | + * $qb = $conn->getQueryBuilder() |
|
| 707 | + * ->select('u.name') |
|
| 708 | + * ->from('users', 'u') |
|
| 709 | + * ->groupBy('u.lastLogin'); |
|
| 710 | + * ->addGroupBy('u.createdAt') |
|
| 711 | + * </code> |
|
| 712 | + * |
|
| 713 | + * @param mixed ...$groupBy The grouping expression. |
|
| 714 | + * |
|
| 715 | + * @return $this This QueryBuilder instance. |
|
| 716 | + * @since 8.2.0 |
|
| 717 | + * |
|
| 718 | + * @psalm-taint-sink sql $groupby |
|
| 719 | + */ |
|
| 720 | + public function addGroupBy(...$groupBy); |
|
| 721 | + |
|
| 722 | + /** |
|
| 723 | + * Sets a value for a column in an insert query. |
|
| 724 | + * |
|
| 725 | + * <code> |
|
| 726 | + * $qb = $conn->getQueryBuilder() |
|
| 727 | + * ->insert('users') |
|
| 728 | + * ->values( |
|
| 729 | + * array( |
|
| 730 | + * 'name' => '?' |
|
| 731 | + * ) |
|
| 732 | + * ) |
|
| 733 | + * ->setValue('password', '?'); |
|
| 734 | + * </code> |
|
| 735 | + * |
|
| 736 | + * @param string $column The column into which the value should be inserted. |
|
| 737 | + * @param IParameter|string $value The value that should be inserted into the column. |
|
| 738 | + * |
|
| 739 | + * @return $this This QueryBuilder instance. |
|
| 740 | + * @since 8.2.0 |
|
| 741 | + * |
|
| 742 | + * @psalm-taint-sink sql $column |
|
| 743 | + * @psalm-taint-sink sql $value |
|
| 744 | + */ |
|
| 745 | + public function setValue($column, $value); |
|
| 746 | + |
|
| 747 | + /** |
|
| 748 | + * Specifies values for an insert query indexed by column names. |
|
| 749 | + * Replaces any previous values, if any. |
|
| 750 | + * |
|
| 751 | + * <code> |
|
| 752 | + * $qb = $conn->getQueryBuilder() |
|
| 753 | + * ->insert('users') |
|
| 754 | + * ->values( |
|
| 755 | + * array( |
|
| 756 | + * 'name' => '?', |
|
| 757 | + * 'password' => '?' |
|
| 758 | + * ) |
|
| 759 | + * ); |
|
| 760 | + * </code> |
|
| 761 | + * |
|
| 762 | + * @param array $values The values to specify for the insert query indexed by column names. |
|
| 763 | + * |
|
| 764 | + * @return $this This QueryBuilder instance. |
|
| 765 | + * @since 8.2.0 |
|
| 766 | + * |
|
| 767 | + * @psalm-taint-sink sql $values |
|
| 768 | + */ |
|
| 769 | + public function values(array $values); |
|
| 770 | + |
|
| 771 | + /** |
|
| 772 | + * Specifies a restriction over the groups of the query. |
|
| 773 | + * Replaces any previous having restrictions, if any. |
|
| 774 | + * |
|
| 775 | + * @param mixed ...$having The restriction over the groups. |
|
| 776 | + * |
|
| 777 | + * @return $this This QueryBuilder instance. |
|
| 778 | + * @since 8.2.0 |
|
| 779 | + * |
|
| 780 | + * @psalm-taint-sink sql $having |
|
| 781 | + */ |
|
| 782 | + public function having(...$having); |
|
| 783 | + |
|
| 784 | + /** |
|
| 785 | + * Adds a restriction over the groups of the query, forming a logical |
|
| 786 | + * conjunction with any existing having restrictions. |
|
| 787 | + * |
|
| 788 | + * @param mixed ...$having The restriction to append. |
|
| 789 | + * |
|
| 790 | + * @return $this This QueryBuilder instance. |
|
| 791 | + * @since 8.2.0 |
|
| 792 | + * |
|
| 793 | + * @psalm-taint-sink sql $andHaving |
|
| 794 | + */ |
|
| 795 | + public function andHaving(...$having); |
|
| 796 | + |
|
| 797 | + /** |
|
| 798 | + * Adds a restriction over the groups of the query, forming a logical |
|
| 799 | + * disjunction with any existing having restrictions. |
|
| 800 | + * |
|
| 801 | + * @param mixed ...$having The restriction to add. |
|
| 802 | + * |
|
| 803 | + * @return $this This QueryBuilder instance. |
|
| 804 | + * @since 8.2.0 |
|
| 805 | + * |
|
| 806 | + * @psalm-taint-sink sql $having |
|
| 807 | + */ |
|
| 808 | + public function orHaving(...$having); |
|
| 809 | + |
|
| 810 | + /** |
|
| 811 | + * Specifies an ordering for the query results. |
|
| 812 | + * Replaces any previously specified orderings, if any. |
|
| 813 | + * |
|
| 814 | + * @param string $sort The ordering expression. |
|
| 815 | + * @param string $order The ordering direction. |
|
| 816 | + * |
|
| 817 | + * @return $this This QueryBuilder instance. |
|
| 818 | + * @since 8.2.0 |
|
| 819 | + * |
|
| 820 | + * @psalm-taint-sink sql $sort |
|
| 821 | + * @psalm-taint-sink sql $order |
|
| 822 | + */ |
|
| 823 | + public function orderBy($sort, $order = null); |
|
| 824 | + |
|
| 825 | + /** |
|
| 826 | + * Adds an ordering to the query results. |
|
| 827 | + * |
|
| 828 | + * @param string $sort The ordering expression. |
|
| 829 | + * @param string $order The ordering direction. |
|
| 830 | + * |
|
| 831 | + * @return $this This QueryBuilder instance. |
|
| 832 | + * @since 8.2.0 |
|
| 833 | + * |
|
| 834 | + * @psalm-taint-sink sql $sort |
|
| 835 | + * @psalm-taint-sink sql $order |
|
| 836 | + */ |
|
| 837 | + public function addOrderBy($sort, $order = null); |
|
| 838 | + |
|
| 839 | + /** |
|
| 840 | + * Gets a query part by its name. |
|
| 841 | + * |
|
| 842 | + * @param string $queryPartName |
|
| 843 | + * |
|
| 844 | + * @return mixed |
|
| 845 | + * @since 8.2.0 |
|
| 846 | + */ |
|
| 847 | + public function getQueryPart($queryPartName); |
|
| 848 | + |
|
| 849 | + /** |
|
| 850 | + * Gets all query parts. |
|
| 851 | + * |
|
| 852 | + * @return array |
|
| 853 | + * @since 8.2.0 |
|
| 854 | + */ |
|
| 855 | + public function getQueryParts(); |
|
| 856 | + |
|
| 857 | + /** |
|
| 858 | + * Resets SQL parts. |
|
| 859 | + * |
|
| 860 | + * @param array|null $queryPartNames |
|
| 861 | + * |
|
| 862 | + * @return $this This QueryBuilder instance. |
|
| 863 | + * @since 8.2.0 |
|
| 864 | + */ |
|
| 865 | + public function resetQueryParts($queryPartNames = null); |
|
| 866 | + |
|
| 867 | + /** |
|
| 868 | + * Resets a single SQL part. |
|
| 869 | + * |
|
| 870 | + * @param string $queryPartName |
|
| 871 | + * |
|
| 872 | + * @return $this This QueryBuilder instance. |
|
| 873 | + * @since 8.2.0 |
|
| 874 | + */ |
|
| 875 | + public function resetQueryPart($queryPartName); |
|
| 876 | + |
|
| 877 | + /** |
|
| 878 | + * Creates a new named parameter and bind the value $value to it. |
|
| 879 | + * |
|
| 880 | + * This method provides a shortcut for PDOStatement::bindValue |
|
| 881 | + * when using prepared statements. |
|
| 882 | + * |
|
| 883 | + * The parameter $value specifies the value that you want to bind. If |
|
| 884 | + * $placeholder is not provided bindValue() will automatically create a |
|
| 885 | + * placeholder for you. An automatic placeholder will be of the name |
|
| 886 | + * ':dcValue1', ':dcValue2' etc. |
|
| 887 | + * |
|
| 888 | + * For more information see {@link https://www.php.net/pdostatement-bindparam} |
|
| 889 | + * |
|
| 890 | + * Example: |
|
| 891 | + * <code> |
|
| 892 | + * $value = 2; |
|
| 893 | + * $q->eq( 'id', $q->bindValue( $value ) ); |
|
| 894 | + * $stmt = $q->executeQuery(); // executed with 'id = 2' |
|
| 895 | + * </code> |
|
| 896 | + * |
|
| 897 | + * @license New BSD License |
|
| 898 | + * @link http://www.zetacomponents.org |
|
| 899 | + * |
|
| 900 | + * @param mixed $value |
|
| 901 | + * @param mixed $type |
|
| 902 | + * @param string $placeHolder The name to bind with. The string must start with a colon ':'. |
|
| 903 | + * |
|
| 904 | + * @return IParameter |
|
| 905 | + * @since 8.2.0 |
|
| 906 | + * |
|
| 907 | + * @psalm-taint-escape sql |
|
| 908 | + */ |
|
| 909 | + public function createNamedParameter($value, $type = self::PARAM_STR, $placeHolder = null); |
|
| 910 | + |
|
| 911 | + /** |
|
| 912 | + * Creates a new positional parameter and bind the given value to it. |
|
| 913 | + * |
|
| 914 | + * Attention: If you are using positional parameters with the query builder you have |
|
| 915 | + * to be very careful to bind all parameters in the order they appear in the SQL |
|
| 916 | + * statement , otherwise they get bound in the wrong order which can lead to serious |
|
| 917 | + * bugs in your code. |
|
| 918 | + * |
|
| 919 | + * Example: |
|
| 920 | + * <code> |
|
| 921 | + * $qb = $conn->getQueryBuilder(); |
|
| 922 | + * $qb->select('u.*') |
|
| 923 | + * ->from('users', 'u') |
|
| 924 | + * ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR)) |
|
| 925 | + * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR)) |
|
| 926 | + * </code> |
|
| 927 | + * |
|
| 928 | + * @param mixed $value |
|
| 929 | + * @param integer $type |
|
| 930 | + * |
|
| 931 | + * @return IParameter |
|
| 932 | + * @since 8.2.0 |
|
| 933 | + * |
|
| 934 | + * @psalm-taint-escape sql |
|
| 935 | + */ |
|
| 936 | + public function createPositionalParameter($value, $type = self::PARAM_STR); |
|
| 937 | + |
|
| 938 | + /** |
|
| 939 | + * Creates a new parameter |
|
| 940 | + * |
|
| 941 | + * Example: |
|
| 942 | + * <code> |
|
| 943 | + * $qb = $conn->getQueryBuilder(); |
|
| 944 | + * $qb->select('u.*') |
|
| 945 | + * ->from('users', 'u') |
|
| 946 | + * ->where('u.username = ' . $qb->createParameter('name')) |
|
| 947 | + * ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR)) |
|
| 948 | + * </code> |
|
| 949 | + * |
|
| 950 | + * @param string $name |
|
| 951 | + * |
|
| 952 | + * @return IParameter |
|
| 953 | + * @since 8.2.0 |
|
| 954 | + * |
|
| 955 | + * @psalm-taint-escape sql |
|
| 956 | + */ |
|
| 957 | + public function createParameter($name); |
|
| 958 | + |
|
| 959 | + /** |
|
| 960 | + * Creates a new function |
|
| 961 | + * |
|
| 962 | + * Attention: Column names inside the call have to be quoted before hand |
|
| 963 | + * |
|
| 964 | + * Example: |
|
| 965 | + * <code> |
|
| 966 | + * $qb = $conn->getQueryBuilder(); |
|
| 967 | + * $qb->select($qb->createFunction('COUNT(*)')) |
|
| 968 | + * ->from('users', 'u') |
|
| 969 | + * echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u |
|
| 970 | + * </code> |
|
| 971 | + * <code> |
|
| 972 | + * $qb = $conn->getQueryBuilder(); |
|
| 973 | + * $qb->select($qb->createFunction('COUNT(`column`)')) |
|
| 974 | + * ->from('users', 'u') |
|
| 975 | + * echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u |
|
| 976 | + * </code> |
|
| 977 | + * |
|
| 978 | + * @param string $call |
|
| 979 | + * |
|
| 980 | + * @return IQueryFunction |
|
| 981 | + * @since 8.2.0 |
|
| 982 | + * |
|
| 983 | + * @psalm-taint-sink sql |
|
| 984 | + */ |
|
| 985 | + public function createFunction($call); |
|
| 986 | + |
|
| 987 | + /** |
|
| 988 | + * Used to get the id of the last inserted element |
|
| 989 | + * @return int |
|
| 990 | + * @throws \BadMethodCallException When being called before an insert query has been run. |
|
| 991 | + * @since 9.0.0 |
|
| 992 | + */ |
|
| 993 | + public function getLastInsertId(): int; |
|
| 994 | + |
|
| 995 | + /** |
|
| 996 | + * Returns the table name quoted and with database prefix as needed by the implementation |
|
| 997 | + * |
|
| 998 | + * @param string $table |
|
| 999 | + * @return string |
|
| 1000 | + * @since 9.0.0 |
|
| 1001 | + */ |
|
| 1002 | + public function getTableName($table); |
|
| 1003 | + |
|
| 1004 | + /** |
|
| 1005 | + * Returns the column name quoted and with table alias prefix as needed by the implementation |
|
| 1006 | + * |
|
| 1007 | + * @param string $column |
|
| 1008 | + * @param string $tableAlias |
|
| 1009 | + * @return string |
|
| 1010 | + * @since 9.0.0 |
|
| 1011 | + */ |
|
| 1012 | + public function getColumnName($column, $tableAlias = ''); |
|
| 1013 | 1013 | } |
@@ -57,1300 +57,1300 @@ |
||
| 57 | 57 | |
| 58 | 58 | class QueryBuilder implements IQueryBuilder { |
| 59 | 59 | |
| 60 | - /** @var ConnectionAdapter */ |
|
| 61 | - private $connection; |
|
| 62 | - |
|
| 63 | - /** @var SystemConfig */ |
|
| 64 | - private $systemConfig; |
|
| 65 | - |
|
| 66 | - /** @var ILogger */ |
|
| 67 | - private $logger; |
|
| 68 | - |
|
| 69 | - /** @var \Doctrine\DBAL\Query\QueryBuilder */ |
|
| 70 | - private $queryBuilder; |
|
| 71 | - |
|
| 72 | - /** @var QuoteHelper */ |
|
| 73 | - private $helper; |
|
| 74 | - |
|
| 75 | - /** @var bool */ |
|
| 76 | - private $automaticTablePrefix = true; |
|
| 77 | - |
|
| 78 | - /** @var string */ |
|
| 79 | - protected $lastInsertedTable; |
|
| 80 | - |
|
| 81 | - /** |
|
| 82 | - * Initializes a new QueryBuilder. |
|
| 83 | - * |
|
| 84 | - * @param ConnectionAdapter $connection |
|
| 85 | - * @param SystemConfig $systemConfig |
|
| 86 | - * @param ILogger $logger |
|
| 87 | - */ |
|
| 88 | - public function __construct(ConnectionAdapter $connection, SystemConfig $systemConfig, ILogger $logger) { |
|
| 89 | - $this->connection = $connection; |
|
| 90 | - $this->systemConfig = $systemConfig; |
|
| 91 | - $this->logger = $logger; |
|
| 92 | - $this->queryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection->getInner()); |
|
| 93 | - $this->helper = new QuoteHelper(); |
|
| 94 | - } |
|
| 95 | - |
|
| 96 | - /** |
|
| 97 | - * Enable/disable automatic prefixing of table names with the oc_ prefix |
|
| 98 | - * |
|
| 99 | - * @param bool $enabled If set to true table names will be prefixed with the |
|
| 100 | - * owncloud database prefix automatically. |
|
| 101 | - * @since 8.2.0 |
|
| 102 | - */ |
|
| 103 | - public function automaticTablePrefix($enabled) { |
|
| 104 | - $this->automaticTablePrefix = (bool) $enabled; |
|
| 105 | - } |
|
| 106 | - |
|
| 107 | - /** |
|
| 108 | - * Gets an ExpressionBuilder used for object-oriented construction of query expressions. |
|
| 109 | - * This producer method is intended for convenient inline usage. Example: |
|
| 110 | - * |
|
| 111 | - * <code> |
|
| 112 | - * $qb = $conn->getQueryBuilder() |
|
| 113 | - * ->select('u') |
|
| 114 | - * ->from('users', 'u') |
|
| 115 | - * ->where($qb->expr()->eq('u.id', 1)); |
|
| 116 | - * </code> |
|
| 117 | - * |
|
| 118 | - * For more complex expression construction, consider storing the expression |
|
| 119 | - * builder object in a local variable. |
|
| 120 | - * |
|
| 121 | - * @return \OCP\DB\QueryBuilder\IExpressionBuilder |
|
| 122 | - */ |
|
| 123 | - public function expr() { |
|
| 124 | - if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { |
|
| 125 | - return new OCIExpressionBuilder($this->connection, $this); |
|
| 126 | - } |
|
| 127 | - if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) { |
|
| 128 | - return new PgSqlExpressionBuilder($this->connection, $this); |
|
| 129 | - } |
|
| 130 | - if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) { |
|
| 131 | - return new MySqlExpressionBuilder($this->connection, $this); |
|
| 132 | - } |
|
| 133 | - if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { |
|
| 134 | - return new SqliteExpressionBuilder($this->connection, $this); |
|
| 135 | - } |
|
| 136 | - |
|
| 137 | - return new ExpressionBuilder($this->connection, $this); |
|
| 138 | - } |
|
| 139 | - |
|
| 140 | - /** |
|
| 141 | - * Gets an FunctionBuilder used for object-oriented construction of query functions. |
|
| 142 | - * This producer method is intended for convenient inline usage. Example: |
|
| 143 | - * |
|
| 144 | - * <code> |
|
| 145 | - * $qb = $conn->getQueryBuilder() |
|
| 146 | - * ->select('u') |
|
| 147 | - * ->from('users', 'u') |
|
| 148 | - * ->where($qb->fun()->md5('u.id')); |
|
| 149 | - * </code> |
|
| 150 | - * |
|
| 151 | - * For more complex function construction, consider storing the function |
|
| 152 | - * builder object in a local variable. |
|
| 153 | - * |
|
| 154 | - * @return \OCP\DB\QueryBuilder\IFunctionBuilder |
|
| 155 | - */ |
|
| 156 | - public function func() { |
|
| 157 | - if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { |
|
| 158 | - return new OCIFunctionBuilder($this->helper); |
|
| 159 | - } |
|
| 160 | - if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { |
|
| 161 | - return new SqliteFunctionBuilder($this->helper); |
|
| 162 | - } |
|
| 163 | - if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) { |
|
| 164 | - return new PgSqlFunctionBuilder($this->helper); |
|
| 165 | - } |
|
| 166 | - |
|
| 167 | - return new FunctionBuilder($this->helper); |
|
| 168 | - } |
|
| 169 | - |
|
| 170 | - /** |
|
| 171 | - * Gets the type of the currently built query. |
|
| 172 | - * |
|
| 173 | - * @return integer |
|
| 174 | - */ |
|
| 175 | - public function getType() { |
|
| 176 | - return $this->queryBuilder->getType(); |
|
| 177 | - } |
|
| 178 | - |
|
| 179 | - /** |
|
| 180 | - * Gets the associated DBAL Connection for this query builder. |
|
| 181 | - * |
|
| 182 | - * @return \OCP\IDBConnection |
|
| 183 | - */ |
|
| 184 | - public function getConnection() { |
|
| 185 | - return $this->connection; |
|
| 186 | - } |
|
| 187 | - |
|
| 188 | - /** |
|
| 189 | - * Gets the state of this query builder instance. |
|
| 190 | - * |
|
| 191 | - * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. |
|
| 192 | - */ |
|
| 193 | - public function getState() { |
|
| 194 | - return $this->queryBuilder->getState(); |
|
| 195 | - } |
|
| 196 | - |
|
| 197 | - /** |
|
| 198 | - * Executes this query using the bound parameters and their types. |
|
| 199 | - * |
|
| 200 | - * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate} |
|
| 201 | - * for insert, update and delete statements. |
|
| 202 | - * |
|
| 203 | - * @return IResult|int |
|
| 204 | - */ |
|
| 205 | - public function execute() { |
|
| 206 | - if ($this->systemConfig->getValue('log_query', false)) { |
|
| 207 | - try { |
|
| 208 | - $params = []; |
|
| 209 | - foreach ($this->getParameters() as $placeholder => $value) { |
|
| 210 | - if ($value instanceof \DateTime) { |
|
| 211 | - $params[] = $placeholder . ' => DateTime:\'' . $value->format('c') . '\''; |
|
| 212 | - } elseif (is_array($value)) { |
|
| 213 | - $params[] = $placeholder . ' => (\'' . implode('\', \'', $value) . '\')'; |
|
| 214 | - } else { |
|
| 215 | - $params[] = $placeholder . ' => \'' . $value . '\''; |
|
| 216 | - } |
|
| 217 | - } |
|
| 218 | - if (empty($params)) { |
|
| 219 | - $this->logger->debug('DB QueryBuilder: \'{query}\'', [ |
|
| 220 | - 'query' => $this->getSQL(), |
|
| 221 | - 'app' => 'core', |
|
| 222 | - ]); |
|
| 223 | - } else { |
|
| 224 | - $this->logger->debug('DB QueryBuilder: \'{query}\' with parameters: {params}', [ |
|
| 225 | - 'query' => $this->getSQL(), |
|
| 226 | - 'params' => implode(', ', $params), |
|
| 227 | - 'app' => 'core', |
|
| 228 | - ]); |
|
| 229 | - } |
|
| 230 | - } catch (\Error $e) { |
|
| 231 | - // likely an error during conversion of $value to string |
|
| 232 | - $this->logger->debug('DB QueryBuilder: error trying to log SQL query'); |
|
| 233 | - $this->logger->logException($e); |
|
| 234 | - } |
|
| 235 | - } |
|
| 236 | - |
|
| 237 | - if (!empty($this->getQueryPart('select'))) { |
|
| 238 | - $select = $this->getQueryPart('select'); |
|
| 239 | - $hasSelectAll = array_filter($select, static function ($s) { |
|
| 240 | - return $s === '*'; |
|
| 241 | - }); |
|
| 242 | - $hasSelectSpecific = array_filter($select, static function ($s) { |
|
| 243 | - return $s !== '*'; |
|
| 244 | - }); |
|
| 245 | - |
|
| 246 | - if (empty($hasSelectAll) === empty($hasSelectSpecific)) { |
|
| 247 | - $exception = new QueryException('Query is selecting * and specific values in the same query. This is not supported in Oracle.'); |
|
| 248 | - $this->logger->logException($exception, [ |
|
| 249 | - 'message' => 'Query is selecting * and specific values in the same query. This is not supported in Oracle.', |
|
| 250 | - 'query' => $this->getSQL(), |
|
| 251 | - 'level' => ILogger::ERROR, |
|
| 252 | - 'app' => 'core', |
|
| 253 | - ]); |
|
| 254 | - } |
|
| 255 | - } |
|
| 256 | - |
|
| 257 | - $numberOfParameters = 0; |
|
| 258 | - $hasTooLargeArrayParameter = false; |
|
| 259 | - foreach ($this->getParameters() as $parameter) { |
|
| 260 | - if (is_array($parameter)) { |
|
| 261 | - $count = count($parameter); |
|
| 262 | - $numberOfParameters += $count; |
|
| 263 | - $hasTooLargeArrayParameter = $hasTooLargeArrayParameter || ($count > 1000); |
|
| 264 | - } |
|
| 265 | - } |
|
| 266 | - |
|
| 267 | - if ($hasTooLargeArrayParameter) { |
|
| 268 | - $exception = new QueryException('More than 1000 expressions in a list are not allowed on Oracle.'); |
|
| 269 | - $this->logger->logException($exception, [ |
|
| 270 | - 'message' => 'More than 1000 expressions in a list are not allowed on Oracle.', |
|
| 271 | - 'query' => $this->getSQL(), |
|
| 272 | - 'level' => ILogger::ERROR, |
|
| 273 | - 'app' => 'core', |
|
| 274 | - ]); |
|
| 275 | - } |
|
| 276 | - |
|
| 277 | - if ($numberOfParameters > 65535) { |
|
| 278 | - $exception = new QueryException('The number of parameters must not exceed 65535. Restriction by PostgreSQL.'); |
|
| 279 | - $this->logger->logException($exception, [ |
|
| 280 | - 'message' => 'The number of parameters must not exceed 65535. Restriction by PostgreSQL.', |
|
| 281 | - 'query' => $this->getSQL(), |
|
| 282 | - 'level' => ILogger::ERROR, |
|
| 283 | - 'app' => 'core', |
|
| 284 | - ]); |
|
| 285 | - } |
|
| 286 | - |
|
| 287 | - $result = $this->queryBuilder->execute(); |
|
| 288 | - if (is_int($result)) { |
|
| 289 | - return $result; |
|
| 290 | - } |
|
| 291 | - return new ResultAdapter($result); |
|
| 292 | - } |
|
| 293 | - |
|
| 294 | - public function executeQuery(): IResult { |
|
| 295 | - if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) { |
|
| 296 | - throw new \RuntimeException('Invalid query type, expected SELECT query'); |
|
| 297 | - } |
|
| 298 | - |
|
| 299 | - try { |
|
| 300 | - $result = $this->execute(); |
|
| 301 | - } catch (\Doctrine\DBAL\Exception $e) { |
|
| 302 | - throw \OC\DB\Exceptions\DbalException::wrap($e); |
|
| 303 | - } |
|
| 304 | - |
|
| 305 | - if ($result instanceof IResult) { |
|
| 306 | - return $result; |
|
| 307 | - } |
|
| 308 | - |
|
| 309 | - throw new \RuntimeException('Invalid return type for query'); |
|
| 310 | - } |
|
| 311 | - |
|
| 312 | - /** |
|
| 313 | - * Monkey-patched compatibility layer for apps that were adapted for Nextcloud 22 before |
|
| 314 | - * the first beta, where executeStatement was named executeUpdate. |
|
| 315 | - * |
|
| 316 | - * Static analysis should catch those misuses, but until then let's try to keep things |
|
| 317 | - * running. |
|
| 318 | - * |
|
| 319 | - * @internal |
|
| 320 | - * @deprecated |
|
| 321 | - * @todo drop ASAP |
|
| 322 | - */ |
|
| 323 | - public function executeUpdate(): int { |
|
| 324 | - return $this->executeStatement(); |
|
| 325 | - } |
|
| 326 | - |
|
| 327 | - public function executeStatement(): int { |
|
| 328 | - if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) { |
|
| 329 | - throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement'); |
|
| 330 | - } |
|
| 331 | - |
|
| 332 | - try { |
|
| 333 | - $result = $this->execute(); |
|
| 334 | - } catch (\Doctrine\DBAL\Exception $e) { |
|
| 335 | - throw \OC\DB\Exceptions\DbalException::wrap($e); |
|
| 336 | - } |
|
| 337 | - |
|
| 338 | - if (!is_int($result)) { |
|
| 339 | - throw new \RuntimeException('Invalid return type for statement'); |
|
| 340 | - } |
|
| 341 | - |
|
| 342 | - return $result; |
|
| 343 | - } |
|
| 344 | - |
|
| 345 | - |
|
| 346 | - /** |
|
| 347 | - * Gets the complete SQL string formed by the current specifications of this QueryBuilder. |
|
| 348 | - * |
|
| 349 | - * <code> |
|
| 350 | - * $qb = $conn->getQueryBuilder() |
|
| 351 | - * ->select('u') |
|
| 352 | - * ->from('User', 'u') |
|
| 353 | - * echo $qb->getSQL(); // SELECT u FROM User u |
|
| 354 | - * </code> |
|
| 355 | - * |
|
| 356 | - * @return string The SQL query string. |
|
| 357 | - */ |
|
| 358 | - public function getSQL() { |
|
| 359 | - return $this->queryBuilder->getSQL(); |
|
| 360 | - } |
|
| 361 | - |
|
| 362 | - /** |
|
| 363 | - * Sets a query parameter for the query being constructed. |
|
| 364 | - * |
|
| 365 | - * <code> |
|
| 366 | - * $qb = $conn->getQueryBuilder() |
|
| 367 | - * ->select('u') |
|
| 368 | - * ->from('users', 'u') |
|
| 369 | - * ->where('u.id = :user_id') |
|
| 370 | - * ->setParameter(':user_id', 1); |
|
| 371 | - * </code> |
|
| 372 | - * |
|
| 373 | - * @param string|integer $key The parameter position or name. |
|
| 374 | - * @param mixed $value The parameter value. |
|
| 375 | - * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants. |
|
| 376 | - * |
|
| 377 | - * @return $this This QueryBuilder instance. |
|
| 378 | - */ |
|
| 379 | - public function setParameter($key, $value, $type = null) { |
|
| 380 | - $this->queryBuilder->setParameter($key, $value, $type); |
|
| 381 | - |
|
| 382 | - return $this; |
|
| 383 | - } |
|
| 384 | - |
|
| 385 | - /** |
|
| 386 | - * Sets a collection of query parameters for the query being constructed. |
|
| 387 | - * |
|
| 388 | - * <code> |
|
| 389 | - * $qb = $conn->getQueryBuilder() |
|
| 390 | - * ->select('u') |
|
| 391 | - * ->from('users', 'u') |
|
| 392 | - * ->where('u.id = :user_id1 OR u.id = :user_id2') |
|
| 393 | - * ->setParameters(array( |
|
| 394 | - * ':user_id1' => 1, |
|
| 395 | - * ':user_id2' => 2 |
|
| 396 | - * )); |
|
| 397 | - * </code> |
|
| 398 | - * |
|
| 399 | - * @param array $params The query parameters to set. |
|
| 400 | - * @param array $types The query parameters types to set. |
|
| 401 | - * |
|
| 402 | - * @return $this This QueryBuilder instance. |
|
| 403 | - */ |
|
| 404 | - public function setParameters(array $params, array $types = []) { |
|
| 405 | - $this->queryBuilder->setParameters($params, $types); |
|
| 406 | - |
|
| 407 | - return $this; |
|
| 408 | - } |
|
| 409 | - |
|
| 410 | - /** |
|
| 411 | - * Gets all defined query parameters for the query being constructed indexed by parameter index or name. |
|
| 412 | - * |
|
| 413 | - * @return array The currently defined query parameters indexed by parameter index or name. |
|
| 414 | - */ |
|
| 415 | - public function getParameters() { |
|
| 416 | - return $this->queryBuilder->getParameters(); |
|
| 417 | - } |
|
| 418 | - |
|
| 419 | - /** |
|
| 420 | - * Gets a (previously set) query parameter of the query being constructed. |
|
| 421 | - * |
|
| 422 | - * @param mixed $key The key (index or name) of the bound parameter. |
|
| 423 | - * |
|
| 424 | - * @return mixed The value of the bound parameter. |
|
| 425 | - */ |
|
| 426 | - public function getParameter($key) { |
|
| 427 | - return $this->queryBuilder->getParameter($key); |
|
| 428 | - } |
|
| 429 | - |
|
| 430 | - /** |
|
| 431 | - * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. |
|
| 432 | - * |
|
| 433 | - * @return array The currently defined query parameter types indexed by parameter index or name. |
|
| 434 | - */ |
|
| 435 | - public function getParameterTypes() { |
|
| 436 | - return $this->queryBuilder->getParameterTypes(); |
|
| 437 | - } |
|
| 438 | - |
|
| 439 | - /** |
|
| 440 | - * Gets a (previously set) query parameter type of the query being constructed. |
|
| 441 | - * |
|
| 442 | - * @param mixed $key The key (index or name) of the bound parameter type. |
|
| 443 | - * |
|
| 444 | - * @return mixed The value of the bound parameter type. |
|
| 445 | - */ |
|
| 446 | - public function getParameterType($key) { |
|
| 447 | - return $this->queryBuilder->getParameterType($key); |
|
| 448 | - } |
|
| 449 | - |
|
| 450 | - /** |
|
| 451 | - * Sets the position of the first result to retrieve (the "offset"). |
|
| 452 | - * |
|
| 453 | - * @param integer $firstResult The first result to return. |
|
| 454 | - * |
|
| 455 | - * @return $this This QueryBuilder instance. |
|
| 456 | - */ |
|
| 457 | - public function setFirstResult($firstResult) { |
|
| 458 | - $this->queryBuilder->setFirstResult($firstResult); |
|
| 459 | - |
|
| 460 | - return $this; |
|
| 461 | - } |
|
| 462 | - |
|
| 463 | - /** |
|
| 464 | - * Gets the position of the first result the query object was set to retrieve (the "offset"). |
|
| 465 | - * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. |
|
| 466 | - * |
|
| 467 | - * @return integer The position of the first result. |
|
| 468 | - */ |
|
| 469 | - public function getFirstResult() { |
|
| 470 | - return $this->queryBuilder->getFirstResult(); |
|
| 471 | - } |
|
| 472 | - |
|
| 473 | - /** |
|
| 474 | - * Sets the maximum number of results to retrieve (the "limit"). |
|
| 475 | - * |
|
| 476 | - * NOTE: Setting max results to "0" will cause mixed behaviour. While most |
|
| 477 | - * of the databases will just return an empty result set, Oracle will return |
|
| 478 | - * all entries. |
|
| 479 | - * |
|
| 480 | - * @param integer $maxResults The maximum number of results to retrieve. |
|
| 481 | - * |
|
| 482 | - * @return $this This QueryBuilder instance. |
|
| 483 | - */ |
|
| 484 | - public function setMaxResults($maxResults) { |
|
| 485 | - $this->queryBuilder->setMaxResults($maxResults); |
|
| 486 | - |
|
| 487 | - return $this; |
|
| 488 | - } |
|
| 489 | - |
|
| 490 | - /** |
|
| 491 | - * Gets the maximum number of results the query object was set to retrieve (the "limit"). |
|
| 492 | - * Returns NULL if {@link setMaxResults} was not applied to this query builder. |
|
| 493 | - * |
|
| 494 | - * @return int|null The maximum number of results. |
|
| 495 | - */ |
|
| 496 | - public function getMaxResults() { |
|
| 497 | - return $this->queryBuilder->getMaxResults(); |
|
| 498 | - } |
|
| 499 | - |
|
| 500 | - /** |
|
| 501 | - * Specifies an item that is to be returned in the query result. |
|
| 502 | - * Replaces any previously specified selections, if any. |
|
| 503 | - * |
|
| 504 | - * <code> |
|
| 505 | - * $qb = $conn->getQueryBuilder() |
|
| 506 | - * ->select('u.id', 'p.id') |
|
| 507 | - * ->from('users', 'u') |
|
| 508 | - * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 509 | - * </code> |
|
| 510 | - * |
|
| 511 | - * @param mixed ...$selects The selection expressions. |
|
| 512 | - * |
|
| 513 | - * '@return $this This QueryBuilder instance. |
|
| 514 | - */ |
|
| 515 | - public function select(...$selects) { |
|
| 516 | - if (count($selects) === 1 && is_array($selects[0])) { |
|
| 517 | - $selects = $selects[0]; |
|
| 518 | - } |
|
| 519 | - |
|
| 520 | - $this->queryBuilder->select( |
|
| 521 | - $this->helper->quoteColumnNames($selects) |
|
| 522 | - ); |
|
| 523 | - |
|
| 524 | - return $this; |
|
| 525 | - } |
|
| 526 | - |
|
| 527 | - /** |
|
| 528 | - * Specifies an item that is to be returned with a different name in the query result. |
|
| 529 | - * |
|
| 530 | - * <code> |
|
| 531 | - * $qb = $conn->getQueryBuilder() |
|
| 532 | - * ->selectAlias('u.id', 'user_id') |
|
| 533 | - * ->from('users', 'u') |
|
| 534 | - * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 535 | - * </code> |
|
| 536 | - * |
|
| 537 | - * @param mixed $select The selection expressions. |
|
| 538 | - * @param string $alias The column alias used in the constructed query. |
|
| 539 | - * |
|
| 540 | - * @return $this This QueryBuilder instance. |
|
| 541 | - */ |
|
| 542 | - public function selectAlias($select, $alias) { |
|
| 543 | - $this->queryBuilder->addSelect( |
|
| 544 | - $this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias) |
|
| 545 | - ); |
|
| 546 | - |
|
| 547 | - return $this; |
|
| 548 | - } |
|
| 549 | - |
|
| 550 | - /** |
|
| 551 | - * Specifies an item that is to be returned uniquely in the query result. |
|
| 552 | - * |
|
| 553 | - * <code> |
|
| 554 | - * $qb = $conn->getQueryBuilder() |
|
| 555 | - * ->selectDistinct('type') |
|
| 556 | - * ->from('users'); |
|
| 557 | - * </code> |
|
| 558 | - * |
|
| 559 | - * @param mixed $select The selection expressions. |
|
| 560 | - * |
|
| 561 | - * @return $this This QueryBuilder instance. |
|
| 562 | - */ |
|
| 563 | - public function selectDistinct($select) { |
|
| 564 | - if (!is_array($select)) { |
|
| 565 | - $select = [$select]; |
|
| 566 | - } |
|
| 567 | - |
|
| 568 | - $quotedSelect = $this->helper->quoteColumnNames($select); |
|
| 569 | - |
|
| 570 | - $this->queryBuilder->addSelect( |
|
| 571 | - 'DISTINCT ' . implode(', ', $quotedSelect) |
|
| 572 | - ); |
|
| 573 | - |
|
| 574 | - return $this; |
|
| 575 | - } |
|
| 576 | - |
|
| 577 | - /** |
|
| 578 | - * Adds an item that is to be returned in the query result. |
|
| 579 | - * |
|
| 580 | - * <code> |
|
| 581 | - * $qb = $conn->getQueryBuilder() |
|
| 582 | - * ->select('u.id') |
|
| 583 | - * ->addSelect('p.id') |
|
| 584 | - * ->from('users', 'u') |
|
| 585 | - * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); |
|
| 586 | - * </code> |
|
| 587 | - * |
|
| 588 | - * @param mixed ...$selects The selection expression. |
|
| 589 | - * |
|
| 590 | - * @return $this This QueryBuilder instance. |
|
| 591 | - */ |
|
| 592 | - public function addSelect(...$selects) { |
|
| 593 | - if (count($selects) === 1 && is_array($selects[0])) { |
|
| 594 | - $selects = $selects[0]; |
|
| 595 | - } |
|
| 596 | - |
|
| 597 | - $this->queryBuilder->addSelect( |
|
| 598 | - $this->helper->quoteColumnNames($selects) |
|
| 599 | - ); |
|
| 600 | - |
|
| 601 | - return $this; |
|
| 602 | - } |
|
| 603 | - |
|
| 604 | - /** |
|
| 605 | - * Turns the query being built into a bulk delete query that ranges over |
|
| 606 | - * a certain table. |
|
| 607 | - * |
|
| 608 | - * <code> |
|
| 609 | - * $qb = $conn->getQueryBuilder() |
|
| 610 | - * ->delete('users', 'u') |
|
| 611 | - * ->where('u.id = :user_id'); |
|
| 612 | - * ->setParameter(':user_id', 1); |
|
| 613 | - * </code> |
|
| 614 | - * |
|
| 615 | - * @param string $delete The table whose rows are subject to the deletion. |
|
| 616 | - * @param string $alias The table alias used in the constructed query. |
|
| 617 | - * |
|
| 618 | - * @return $this This QueryBuilder instance. |
|
| 619 | - */ |
|
| 620 | - public function delete($delete = null, $alias = null) { |
|
| 621 | - $this->queryBuilder->delete( |
|
| 622 | - $this->getTableName($delete), |
|
| 623 | - $alias |
|
| 624 | - ); |
|
| 625 | - |
|
| 626 | - return $this; |
|
| 627 | - } |
|
| 628 | - |
|
| 629 | - /** |
|
| 630 | - * Turns the query being built into a bulk update query that ranges over |
|
| 631 | - * a certain table |
|
| 632 | - * |
|
| 633 | - * <code> |
|
| 634 | - * $qb = $conn->getQueryBuilder() |
|
| 635 | - * ->update('users', 'u') |
|
| 636 | - * ->set('u.password', md5('password')) |
|
| 637 | - * ->where('u.id = ?'); |
|
| 638 | - * </code> |
|
| 639 | - * |
|
| 640 | - * @param string $update The table whose rows are subject to the update. |
|
| 641 | - * @param string $alias The table alias used in the constructed query. |
|
| 642 | - * |
|
| 643 | - * @return $this This QueryBuilder instance. |
|
| 644 | - */ |
|
| 645 | - public function update($update = null, $alias = null) { |
|
| 646 | - $this->queryBuilder->update( |
|
| 647 | - $this->getTableName($update), |
|
| 648 | - $alias |
|
| 649 | - ); |
|
| 650 | - |
|
| 651 | - return $this; |
|
| 652 | - } |
|
| 653 | - |
|
| 654 | - /** |
|
| 655 | - * Turns the query being built into an insert query that inserts into |
|
| 656 | - * a certain table |
|
| 657 | - * |
|
| 658 | - * <code> |
|
| 659 | - * $qb = $conn->getQueryBuilder() |
|
| 660 | - * ->insert('users') |
|
| 661 | - * ->values( |
|
| 662 | - * array( |
|
| 663 | - * 'name' => '?', |
|
| 664 | - * 'password' => '?' |
|
| 665 | - * ) |
|
| 666 | - * ); |
|
| 667 | - * </code> |
|
| 668 | - * |
|
| 669 | - * @param string $insert The table into which the rows should be inserted. |
|
| 670 | - * |
|
| 671 | - * @return $this This QueryBuilder instance. |
|
| 672 | - */ |
|
| 673 | - public function insert($insert = null) { |
|
| 674 | - $this->queryBuilder->insert( |
|
| 675 | - $this->getTableName($insert) |
|
| 676 | - ); |
|
| 677 | - |
|
| 678 | - $this->lastInsertedTable = $insert; |
|
| 679 | - |
|
| 680 | - return $this; |
|
| 681 | - } |
|
| 682 | - |
|
| 683 | - /** |
|
| 684 | - * Creates and adds a query root corresponding to the table identified by the |
|
| 685 | - * given alias, forming a cartesian product with any existing query roots. |
|
| 686 | - * |
|
| 687 | - * <code> |
|
| 688 | - * $qb = $conn->getQueryBuilder() |
|
| 689 | - * ->select('u.id') |
|
| 690 | - * ->from('users', 'u') |
|
| 691 | - * </code> |
|
| 692 | - * |
|
| 693 | - * @param string $from The table. |
|
| 694 | - * @param string|null $alias The alias of the table. |
|
| 695 | - * |
|
| 696 | - * @return $this This QueryBuilder instance. |
|
| 697 | - */ |
|
| 698 | - public function from($from, $alias = null) { |
|
| 699 | - $this->queryBuilder->from( |
|
| 700 | - $this->getTableName($from), |
|
| 701 | - $this->quoteAlias($alias) |
|
| 702 | - ); |
|
| 703 | - |
|
| 704 | - return $this; |
|
| 705 | - } |
|
| 706 | - |
|
| 707 | - /** |
|
| 708 | - * Creates and adds a join to the query. |
|
| 709 | - * |
|
| 710 | - * <code> |
|
| 711 | - * $qb = $conn->getQueryBuilder() |
|
| 712 | - * ->select('u.name') |
|
| 713 | - * ->from('users', 'u') |
|
| 714 | - * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 715 | - * </code> |
|
| 716 | - * |
|
| 717 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 718 | - * @param string $join The table name to join. |
|
| 719 | - * @param string $alias The alias of the join table. |
|
| 720 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 721 | - * |
|
| 722 | - * @return $this This QueryBuilder instance. |
|
| 723 | - */ |
|
| 724 | - public function join($fromAlias, $join, $alias, $condition = null) { |
|
| 725 | - $this->queryBuilder->join( |
|
| 726 | - $this->quoteAlias($fromAlias), |
|
| 727 | - $this->getTableName($join), |
|
| 728 | - $this->quoteAlias($alias), |
|
| 729 | - $condition |
|
| 730 | - ); |
|
| 731 | - |
|
| 732 | - return $this; |
|
| 733 | - } |
|
| 734 | - |
|
| 735 | - /** |
|
| 736 | - * Creates and adds a join to the query. |
|
| 737 | - * |
|
| 738 | - * <code> |
|
| 739 | - * $qb = $conn->getQueryBuilder() |
|
| 740 | - * ->select('u.name') |
|
| 741 | - * ->from('users', 'u') |
|
| 742 | - * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 743 | - * </code> |
|
| 744 | - * |
|
| 745 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 746 | - * @param string $join The table name to join. |
|
| 747 | - * @param string $alias The alias of the join table. |
|
| 748 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 749 | - * |
|
| 750 | - * @return $this This QueryBuilder instance. |
|
| 751 | - */ |
|
| 752 | - public function innerJoin($fromAlias, $join, $alias, $condition = null) { |
|
| 753 | - $this->queryBuilder->innerJoin( |
|
| 754 | - $this->quoteAlias($fromAlias), |
|
| 755 | - $this->getTableName($join), |
|
| 756 | - $this->quoteAlias($alias), |
|
| 757 | - $condition |
|
| 758 | - ); |
|
| 759 | - |
|
| 760 | - return $this; |
|
| 761 | - } |
|
| 762 | - |
|
| 763 | - /** |
|
| 764 | - * Creates and adds a left join to the query. |
|
| 765 | - * |
|
| 766 | - * <code> |
|
| 767 | - * $qb = $conn->getQueryBuilder() |
|
| 768 | - * ->select('u.name') |
|
| 769 | - * ->from('users', 'u') |
|
| 770 | - * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 771 | - * </code> |
|
| 772 | - * |
|
| 773 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 774 | - * @param string $join The table name to join. |
|
| 775 | - * @param string $alias The alias of the join table. |
|
| 776 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 777 | - * |
|
| 778 | - * @return $this This QueryBuilder instance. |
|
| 779 | - */ |
|
| 780 | - public function leftJoin($fromAlias, $join, $alias, $condition = null) { |
|
| 781 | - $this->queryBuilder->leftJoin( |
|
| 782 | - $this->quoteAlias($fromAlias), |
|
| 783 | - $this->getTableName($join), |
|
| 784 | - $this->quoteAlias($alias), |
|
| 785 | - $condition |
|
| 786 | - ); |
|
| 787 | - |
|
| 788 | - return $this; |
|
| 789 | - } |
|
| 790 | - |
|
| 791 | - /** |
|
| 792 | - * Creates and adds a right join to the query. |
|
| 793 | - * |
|
| 794 | - * <code> |
|
| 795 | - * $qb = $conn->getQueryBuilder() |
|
| 796 | - * ->select('u.name') |
|
| 797 | - * ->from('users', 'u') |
|
| 798 | - * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 799 | - * </code> |
|
| 800 | - * |
|
| 801 | - * @param string $fromAlias The alias that points to a from clause. |
|
| 802 | - * @param string $join The table name to join. |
|
| 803 | - * @param string $alias The alias of the join table. |
|
| 804 | - * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 805 | - * |
|
| 806 | - * @return $this This QueryBuilder instance. |
|
| 807 | - */ |
|
| 808 | - public function rightJoin($fromAlias, $join, $alias, $condition = null) { |
|
| 809 | - $this->queryBuilder->rightJoin( |
|
| 810 | - $this->quoteAlias($fromAlias), |
|
| 811 | - $this->getTableName($join), |
|
| 812 | - $this->quoteAlias($alias), |
|
| 813 | - $condition |
|
| 814 | - ); |
|
| 815 | - |
|
| 816 | - return $this; |
|
| 817 | - } |
|
| 818 | - |
|
| 819 | - /** |
|
| 820 | - * Sets a new value for a column in a bulk update query. |
|
| 821 | - * |
|
| 822 | - * <code> |
|
| 823 | - * $qb = $conn->getQueryBuilder() |
|
| 824 | - * ->update('users', 'u') |
|
| 825 | - * ->set('u.password', md5('password')) |
|
| 826 | - * ->where('u.id = ?'); |
|
| 827 | - * </code> |
|
| 828 | - * |
|
| 829 | - * @param string $key The column to set. |
|
| 830 | - * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc. |
|
| 831 | - * |
|
| 832 | - * @return $this This QueryBuilder instance. |
|
| 833 | - */ |
|
| 834 | - public function set($key, $value) { |
|
| 835 | - $this->queryBuilder->set( |
|
| 836 | - $this->helper->quoteColumnName($key), |
|
| 837 | - $this->helper->quoteColumnName($value) |
|
| 838 | - ); |
|
| 839 | - |
|
| 840 | - return $this; |
|
| 841 | - } |
|
| 842 | - |
|
| 843 | - /** |
|
| 844 | - * Specifies one or more restrictions to the query result. |
|
| 845 | - * Replaces any previously specified restrictions, if any. |
|
| 846 | - * |
|
| 847 | - * <code> |
|
| 848 | - * $qb = $conn->getQueryBuilder() |
|
| 849 | - * ->select('u.name') |
|
| 850 | - * ->from('users', 'u') |
|
| 851 | - * ->where('u.id = ?'); |
|
| 852 | - * |
|
| 853 | - * // You can optionally programatically build and/or expressions |
|
| 854 | - * $qb = $conn->getQueryBuilder(); |
|
| 855 | - * |
|
| 856 | - * $or = $qb->expr()->orx(); |
|
| 857 | - * $or->add($qb->expr()->eq('u.id', 1)); |
|
| 858 | - * $or->add($qb->expr()->eq('u.id', 2)); |
|
| 859 | - * |
|
| 860 | - * $qb->update('users', 'u') |
|
| 861 | - * ->set('u.password', md5('password')) |
|
| 862 | - * ->where($or); |
|
| 863 | - * </code> |
|
| 864 | - * |
|
| 865 | - * @param mixed ...$predicates The restriction predicates. |
|
| 866 | - * |
|
| 867 | - * @return $this This QueryBuilder instance. |
|
| 868 | - */ |
|
| 869 | - public function where(...$predicates) { |
|
| 870 | - call_user_func_array( |
|
| 871 | - [$this->queryBuilder, 'where'], |
|
| 872 | - $predicates |
|
| 873 | - ); |
|
| 874 | - |
|
| 875 | - return $this; |
|
| 876 | - } |
|
| 877 | - |
|
| 878 | - /** |
|
| 879 | - * Adds one or more restrictions to the query results, forming a logical |
|
| 880 | - * conjunction with any previously specified restrictions. |
|
| 881 | - * |
|
| 882 | - * <code> |
|
| 883 | - * $qb = $conn->getQueryBuilder() |
|
| 884 | - * ->select('u') |
|
| 885 | - * ->from('users', 'u') |
|
| 886 | - * ->where('u.username LIKE ?') |
|
| 887 | - * ->andWhere('u.is_active = 1'); |
|
| 888 | - * </code> |
|
| 889 | - * |
|
| 890 | - * @param mixed ...$where The query restrictions. |
|
| 891 | - * |
|
| 892 | - * @return $this This QueryBuilder instance. |
|
| 893 | - * |
|
| 894 | - * @see where() |
|
| 895 | - */ |
|
| 896 | - public function andWhere(...$where) { |
|
| 897 | - call_user_func_array( |
|
| 898 | - [$this->queryBuilder, 'andWhere'], |
|
| 899 | - $where |
|
| 900 | - ); |
|
| 901 | - |
|
| 902 | - return $this; |
|
| 903 | - } |
|
| 904 | - |
|
| 905 | - /** |
|
| 906 | - * Adds one or more restrictions to the query results, forming a logical |
|
| 907 | - * disjunction with any previously specified restrictions. |
|
| 908 | - * |
|
| 909 | - * <code> |
|
| 910 | - * $qb = $conn->getQueryBuilder() |
|
| 911 | - * ->select('u.name') |
|
| 912 | - * ->from('users', 'u') |
|
| 913 | - * ->where('u.id = 1') |
|
| 914 | - * ->orWhere('u.id = 2'); |
|
| 915 | - * </code> |
|
| 916 | - * |
|
| 917 | - * @param mixed ...$where The WHERE statement. |
|
| 918 | - * |
|
| 919 | - * @return $this This QueryBuilder instance. |
|
| 920 | - * |
|
| 921 | - * @see where() |
|
| 922 | - */ |
|
| 923 | - public function orWhere(...$where) { |
|
| 924 | - call_user_func_array( |
|
| 925 | - [$this->queryBuilder, 'orWhere'], |
|
| 926 | - $where |
|
| 927 | - ); |
|
| 928 | - |
|
| 929 | - return $this; |
|
| 930 | - } |
|
| 931 | - |
|
| 932 | - /** |
|
| 933 | - * Specifies a grouping over the results of the query. |
|
| 934 | - * Replaces any previously specified groupings, if any. |
|
| 935 | - * |
|
| 936 | - * <code> |
|
| 937 | - * $qb = $conn->getQueryBuilder() |
|
| 938 | - * ->select('u.name') |
|
| 939 | - * ->from('users', 'u') |
|
| 940 | - * ->groupBy('u.id'); |
|
| 941 | - * </code> |
|
| 942 | - * |
|
| 943 | - * @param mixed ...$groupBys The grouping expression. |
|
| 944 | - * |
|
| 945 | - * @return $this This QueryBuilder instance. |
|
| 946 | - */ |
|
| 947 | - public function groupBy(...$groupBys) { |
|
| 948 | - if (count($groupBys) === 1 && is_array($groupBys[0])) { |
|
| 949 | - $groupBys = $groupBys[0]; |
|
| 950 | - } |
|
| 951 | - |
|
| 952 | - call_user_func_array( |
|
| 953 | - [$this->queryBuilder, 'groupBy'], |
|
| 954 | - $this->helper->quoteColumnNames($groupBys) |
|
| 955 | - ); |
|
| 956 | - |
|
| 957 | - return $this; |
|
| 958 | - } |
|
| 959 | - |
|
| 960 | - /** |
|
| 961 | - * Adds a grouping expression to the query. |
|
| 962 | - * |
|
| 963 | - * <code> |
|
| 964 | - * $qb = $conn->getQueryBuilder() |
|
| 965 | - * ->select('u.name') |
|
| 966 | - * ->from('users', 'u') |
|
| 967 | - * ->groupBy('u.lastLogin'); |
|
| 968 | - * ->addGroupBy('u.createdAt') |
|
| 969 | - * </code> |
|
| 970 | - * |
|
| 971 | - * @param mixed ...$groupBy The grouping expression. |
|
| 972 | - * |
|
| 973 | - * @return $this This QueryBuilder instance. |
|
| 974 | - */ |
|
| 975 | - public function addGroupBy(...$groupBys) { |
|
| 976 | - if (count($groupBys) === 1 && is_array($groupBys[0])) { |
|
| 977 | - $$groupBys = $groupBys[0]; |
|
| 978 | - } |
|
| 979 | - |
|
| 980 | - call_user_func_array( |
|
| 981 | - [$this->queryBuilder, 'addGroupBy'], |
|
| 982 | - $this->helper->quoteColumnNames($groupBys) |
|
| 983 | - ); |
|
| 984 | - |
|
| 985 | - return $this; |
|
| 986 | - } |
|
| 987 | - |
|
| 988 | - /** |
|
| 989 | - * Sets a value for a column in an insert query. |
|
| 990 | - * |
|
| 991 | - * <code> |
|
| 992 | - * $qb = $conn->getQueryBuilder() |
|
| 993 | - * ->insert('users') |
|
| 994 | - * ->values( |
|
| 995 | - * array( |
|
| 996 | - * 'name' => '?' |
|
| 997 | - * ) |
|
| 998 | - * ) |
|
| 999 | - * ->setValue('password', '?'); |
|
| 1000 | - * </code> |
|
| 1001 | - * |
|
| 1002 | - * @param string $column The column into which the value should be inserted. |
|
| 1003 | - * @param IParameter|string $value The value that should be inserted into the column. |
|
| 1004 | - * |
|
| 1005 | - * @return $this This QueryBuilder instance. |
|
| 1006 | - */ |
|
| 1007 | - public function setValue($column, $value) { |
|
| 1008 | - $this->queryBuilder->setValue( |
|
| 1009 | - $this->helper->quoteColumnName($column), |
|
| 1010 | - (string) $value |
|
| 1011 | - ); |
|
| 1012 | - |
|
| 1013 | - return $this; |
|
| 1014 | - } |
|
| 1015 | - |
|
| 1016 | - /** |
|
| 1017 | - * Specifies values for an insert query indexed by column names. |
|
| 1018 | - * Replaces any previous values, if any. |
|
| 1019 | - * |
|
| 1020 | - * <code> |
|
| 1021 | - * $qb = $conn->getQueryBuilder() |
|
| 1022 | - * ->insert('users') |
|
| 1023 | - * ->values( |
|
| 1024 | - * array( |
|
| 1025 | - * 'name' => '?', |
|
| 1026 | - * 'password' => '?' |
|
| 1027 | - * ) |
|
| 1028 | - * ); |
|
| 1029 | - * </code> |
|
| 1030 | - * |
|
| 1031 | - * @param array $values The values to specify for the insert query indexed by column names. |
|
| 1032 | - * |
|
| 1033 | - * @return $this This QueryBuilder instance. |
|
| 1034 | - */ |
|
| 1035 | - public function values(array $values) { |
|
| 1036 | - $quotedValues = []; |
|
| 1037 | - foreach ($values as $key => $value) { |
|
| 1038 | - $quotedValues[$this->helper->quoteColumnName($key)] = $value; |
|
| 1039 | - } |
|
| 1040 | - |
|
| 1041 | - $this->queryBuilder->values($quotedValues); |
|
| 1042 | - |
|
| 1043 | - return $this; |
|
| 1044 | - } |
|
| 1045 | - |
|
| 1046 | - /** |
|
| 1047 | - * Specifies a restriction over the groups of the query. |
|
| 1048 | - * Replaces any previous having restrictions, if any. |
|
| 1049 | - * |
|
| 1050 | - * @param mixed ...$having The restriction over the groups. |
|
| 1051 | - * |
|
| 1052 | - * @return $this This QueryBuilder instance. |
|
| 1053 | - */ |
|
| 1054 | - public function having(...$having) { |
|
| 1055 | - call_user_func_array( |
|
| 1056 | - [$this->queryBuilder, 'having'], |
|
| 1057 | - $having |
|
| 1058 | - ); |
|
| 1059 | - |
|
| 1060 | - return $this; |
|
| 1061 | - } |
|
| 1062 | - |
|
| 1063 | - /** |
|
| 1064 | - * Adds a restriction over the groups of the query, forming a logical |
|
| 1065 | - * conjunction with any existing having restrictions. |
|
| 1066 | - * |
|
| 1067 | - * @param mixed ...$having The restriction to append. |
|
| 1068 | - * |
|
| 1069 | - * @return $this This QueryBuilder instance. |
|
| 1070 | - */ |
|
| 1071 | - public function andHaving(...$having) { |
|
| 1072 | - call_user_func_array( |
|
| 1073 | - [$this->queryBuilder, 'andHaving'], |
|
| 1074 | - $having |
|
| 1075 | - ); |
|
| 1076 | - |
|
| 1077 | - return $this; |
|
| 1078 | - } |
|
| 1079 | - |
|
| 1080 | - /** |
|
| 1081 | - * Adds a restriction over the groups of the query, forming a logical |
|
| 1082 | - * disjunction with any existing having restrictions. |
|
| 1083 | - * |
|
| 1084 | - * @param mixed ...$having The restriction to add. |
|
| 1085 | - * |
|
| 1086 | - * @return $this This QueryBuilder instance. |
|
| 1087 | - */ |
|
| 1088 | - public function orHaving(...$having) { |
|
| 1089 | - call_user_func_array( |
|
| 1090 | - [$this->queryBuilder, 'orHaving'], |
|
| 1091 | - $having |
|
| 1092 | - ); |
|
| 1093 | - |
|
| 1094 | - return $this; |
|
| 1095 | - } |
|
| 1096 | - |
|
| 1097 | - /** |
|
| 1098 | - * Specifies an ordering for the query results. |
|
| 1099 | - * Replaces any previously specified orderings, if any. |
|
| 1100 | - * |
|
| 1101 | - * @param string $sort The ordering expression. |
|
| 1102 | - * @param string $order The ordering direction. |
|
| 1103 | - * |
|
| 1104 | - * @return $this This QueryBuilder instance. |
|
| 1105 | - */ |
|
| 1106 | - public function orderBy($sort, $order = null) { |
|
| 1107 | - $this->queryBuilder->orderBy( |
|
| 1108 | - $this->helper->quoteColumnName($sort), |
|
| 1109 | - $order |
|
| 1110 | - ); |
|
| 1111 | - |
|
| 1112 | - return $this; |
|
| 1113 | - } |
|
| 1114 | - |
|
| 1115 | - /** |
|
| 1116 | - * Adds an ordering to the query results. |
|
| 1117 | - * |
|
| 1118 | - * @param string $sort The ordering expression. |
|
| 1119 | - * @param string $order The ordering direction. |
|
| 1120 | - * |
|
| 1121 | - * @return $this This QueryBuilder instance. |
|
| 1122 | - */ |
|
| 1123 | - public function addOrderBy($sort, $order = null) { |
|
| 1124 | - $this->queryBuilder->addOrderBy( |
|
| 1125 | - $this->helper->quoteColumnName($sort), |
|
| 1126 | - $order |
|
| 1127 | - ); |
|
| 1128 | - |
|
| 1129 | - return $this; |
|
| 1130 | - } |
|
| 1131 | - |
|
| 1132 | - /** |
|
| 1133 | - * Gets a query part by its name. |
|
| 1134 | - * |
|
| 1135 | - * @param string $queryPartName |
|
| 1136 | - * |
|
| 1137 | - * @return mixed |
|
| 1138 | - */ |
|
| 1139 | - public function getQueryPart($queryPartName) { |
|
| 1140 | - return $this->queryBuilder->getQueryPart($queryPartName); |
|
| 1141 | - } |
|
| 1142 | - |
|
| 1143 | - /** |
|
| 1144 | - * Gets all query parts. |
|
| 1145 | - * |
|
| 1146 | - * @return array |
|
| 1147 | - */ |
|
| 1148 | - public function getQueryParts() { |
|
| 1149 | - return $this->queryBuilder->getQueryParts(); |
|
| 1150 | - } |
|
| 1151 | - |
|
| 1152 | - /** |
|
| 1153 | - * Resets SQL parts. |
|
| 1154 | - * |
|
| 1155 | - * @param array|null $queryPartNames |
|
| 1156 | - * |
|
| 1157 | - * @return $this This QueryBuilder instance. |
|
| 1158 | - */ |
|
| 1159 | - public function resetQueryParts($queryPartNames = null) { |
|
| 1160 | - $this->queryBuilder->resetQueryParts($queryPartNames); |
|
| 1161 | - |
|
| 1162 | - return $this; |
|
| 1163 | - } |
|
| 1164 | - |
|
| 1165 | - /** |
|
| 1166 | - * Resets a single SQL part. |
|
| 1167 | - * |
|
| 1168 | - * @param string $queryPartName |
|
| 1169 | - * |
|
| 1170 | - * @return $this This QueryBuilder instance. |
|
| 1171 | - */ |
|
| 1172 | - public function resetQueryPart($queryPartName) { |
|
| 1173 | - $this->queryBuilder->resetQueryPart($queryPartName); |
|
| 1174 | - |
|
| 1175 | - return $this; |
|
| 1176 | - } |
|
| 1177 | - |
|
| 1178 | - /** |
|
| 1179 | - * Creates a new named parameter and bind the value $value to it. |
|
| 1180 | - * |
|
| 1181 | - * This method provides a shortcut for PDOStatement::bindValue |
|
| 1182 | - * when using prepared statements. |
|
| 1183 | - * |
|
| 1184 | - * The parameter $value specifies the value that you want to bind. If |
|
| 1185 | - * $placeholder is not provided bindValue() will automatically create a |
|
| 1186 | - * placeholder for you. An automatic placeholder will be of the name |
|
| 1187 | - * ':dcValue1', ':dcValue2' etc. |
|
| 1188 | - * |
|
| 1189 | - * For more information see {@link https://www.php.net/pdostatement-bindparam} |
|
| 1190 | - * |
|
| 1191 | - * Example: |
|
| 1192 | - * <code> |
|
| 1193 | - * $value = 2; |
|
| 1194 | - * $q->eq( 'id', $q->bindValue( $value ) ); |
|
| 1195 | - * $stmt = $q->executeQuery(); // executed with 'id = 2' |
|
| 1196 | - * </code> |
|
| 1197 | - * |
|
| 1198 | - * @license New BSD License |
|
| 1199 | - * @link http://www.zetacomponents.org |
|
| 1200 | - * |
|
| 1201 | - * @param mixed $value |
|
| 1202 | - * @param mixed $type |
|
| 1203 | - * @param string $placeHolder The name to bind with. The string must start with a colon ':'. |
|
| 1204 | - * |
|
| 1205 | - * @return IParameter the placeholder name used. |
|
| 1206 | - */ |
|
| 1207 | - public function createNamedParameter($value, $type = IQueryBuilder::PARAM_STR, $placeHolder = null) { |
|
| 1208 | - return new Parameter($this->queryBuilder->createNamedParameter($value, $type, $placeHolder)); |
|
| 1209 | - } |
|
| 1210 | - |
|
| 1211 | - /** |
|
| 1212 | - * Creates a new positional parameter and bind the given value to it. |
|
| 1213 | - * |
|
| 1214 | - * Attention: If you are using positional parameters with the query builder you have |
|
| 1215 | - * to be very careful to bind all parameters in the order they appear in the SQL |
|
| 1216 | - * statement , otherwise they get bound in the wrong order which can lead to serious |
|
| 1217 | - * bugs in your code. |
|
| 1218 | - * |
|
| 1219 | - * Example: |
|
| 1220 | - * <code> |
|
| 1221 | - * $qb = $conn->getQueryBuilder(); |
|
| 1222 | - * $qb->select('u.*') |
|
| 1223 | - * ->from('users', 'u') |
|
| 1224 | - * ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR)) |
|
| 1225 | - * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR)) |
|
| 1226 | - * </code> |
|
| 1227 | - * |
|
| 1228 | - * @param mixed $value |
|
| 1229 | - * @param integer $type |
|
| 1230 | - * |
|
| 1231 | - * @return IParameter |
|
| 1232 | - */ |
|
| 1233 | - public function createPositionalParameter($value, $type = IQueryBuilder::PARAM_STR) { |
|
| 1234 | - return new Parameter($this->queryBuilder->createPositionalParameter($value, $type)); |
|
| 1235 | - } |
|
| 1236 | - |
|
| 1237 | - /** |
|
| 1238 | - * Creates a new parameter |
|
| 1239 | - * |
|
| 1240 | - * Example: |
|
| 1241 | - * <code> |
|
| 1242 | - * $qb = $conn->getQueryBuilder(); |
|
| 1243 | - * $qb->select('u.*') |
|
| 1244 | - * ->from('users', 'u') |
|
| 1245 | - * ->where('u.username = ' . $qb->createParameter('name')) |
|
| 1246 | - * ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR)) |
|
| 1247 | - * </code> |
|
| 1248 | - * |
|
| 1249 | - * @param string $name |
|
| 1250 | - * |
|
| 1251 | - * @return IParameter |
|
| 1252 | - */ |
|
| 1253 | - public function createParameter($name) { |
|
| 1254 | - return new Parameter(':' . $name); |
|
| 1255 | - } |
|
| 1256 | - |
|
| 1257 | - /** |
|
| 1258 | - * Creates a new function |
|
| 1259 | - * |
|
| 1260 | - * Attention: Column names inside the call have to be quoted before hand |
|
| 1261 | - * |
|
| 1262 | - * Example: |
|
| 1263 | - * <code> |
|
| 1264 | - * $qb = $conn->getQueryBuilder(); |
|
| 1265 | - * $qb->select($qb->createFunction('COUNT(*)')) |
|
| 1266 | - * ->from('users', 'u') |
|
| 1267 | - * echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u |
|
| 1268 | - * </code> |
|
| 1269 | - * <code> |
|
| 1270 | - * $qb = $conn->getQueryBuilder(); |
|
| 1271 | - * $qb->select($qb->createFunction('COUNT(`column`)')) |
|
| 1272 | - * ->from('users', 'u') |
|
| 1273 | - * echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u |
|
| 1274 | - * </code> |
|
| 1275 | - * |
|
| 1276 | - * @param string $call |
|
| 1277 | - * |
|
| 1278 | - * @return IQueryFunction |
|
| 1279 | - */ |
|
| 1280 | - public function createFunction($call) { |
|
| 1281 | - return new QueryFunction($call); |
|
| 1282 | - } |
|
| 1283 | - |
|
| 1284 | - /** |
|
| 1285 | - * Used to get the id of the last inserted element |
|
| 1286 | - * @return int |
|
| 1287 | - * @throws \BadMethodCallException When being called before an insert query has been run. |
|
| 1288 | - */ |
|
| 1289 | - public function getLastInsertId(): int { |
|
| 1290 | - if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT && $this->lastInsertedTable) { |
|
| 1291 | - // lastInsertId() needs the prefix but no quotes |
|
| 1292 | - $table = $this->prefixTableName($this->lastInsertedTable); |
|
| 1293 | - return $this->connection->lastInsertId($table); |
|
| 1294 | - } |
|
| 1295 | - |
|
| 1296 | - throw new \BadMethodCallException('Invalid call to getLastInsertId without using insert() before.'); |
|
| 1297 | - } |
|
| 1298 | - |
|
| 1299 | - /** |
|
| 1300 | - * Returns the table name quoted and with database prefix as needed by the implementation |
|
| 1301 | - * |
|
| 1302 | - * @param string $table |
|
| 1303 | - * @return string |
|
| 1304 | - */ |
|
| 1305 | - public function getTableName($table) { |
|
| 1306 | - if ($table instanceof IQueryFunction) { |
|
| 1307 | - return (string) $table; |
|
| 1308 | - } |
|
| 1309 | - |
|
| 1310 | - $table = $this->prefixTableName($table); |
|
| 1311 | - return $this->helper->quoteColumnName($table); |
|
| 1312 | - } |
|
| 1313 | - |
|
| 1314 | - /** |
|
| 1315 | - * Returns the table name with database prefix as needed by the implementation |
|
| 1316 | - * |
|
| 1317 | - * @param string $table |
|
| 1318 | - * @return string |
|
| 1319 | - */ |
|
| 1320 | - protected function prefixTableName($table) { |
|
| 1321 | - if ($this->automaticTablePrefix === false || strpos($table, '*PREFIX*') === 0) { |
|
| 1322 | - return $table; |
|
| 1323 | - } |
|
| 1324 | - |
|
| 1325 | - return '*PREFIX*' . $table; |
|
| 1326 | - } |
|
| 1327 | - |
|
| 1328 | - /** |
|
| 1329 | - * Returns the column name quoted and with table alias prefix as needed by the implementation |
|
| 1330 | - * |
|
| 1331 | - * @param string $column |
|
| 1332 | - * @param string $tableAlias |
|
| 1333 | - * @return string |
|
| 1334 | - */ |
|
| 1335 | - public function getColumnName($column, $tableAlias = '') { |
|
| 1336 | - if ($tableAlias !== '') { |
|
| 1337 | - $tableAlias .= '.'; |
|
| 1338 | - } |
|
| 1339 | - |
|
| 1340 | - return $this->helper->quoteColumnName($tableAlias . $column); |
|
| 1341 | - } |
|
| 1342 | - |
|
| 1343 | - /** |
|
| 1344 | - * Returns the column name quoted and with table alias prefix as needed by the implementation |
|
| 1345 | - * |
|
| 1346 | - * @param string $alias |
|
| 1347 | - * @return string |
|
| 1348 | - */ |
|
| 1349 | - public function quoteAlias($alias) { |
|
| 1350 | - if ($alias === '' || $alias === null) { |
|
| 1351 | - return $alias; |
|
| 1352 | - } |
|
| 1353 | - |
|
| 1354 | - return $this->helper->quoteColumnName($alias); |
|
| 1355 | - } |
|
| 60 | + /** @var ConnectionAdapter */ |
|
| 61 | + private $connection; |
|
| 62 | + |
|
| 63 | + /** @var SystemConfig */ |
|
| 64 | + private $systemConfig; |
|
| 65 | + |
|
| 66 | + /** @var ILogger */ |
|
| 67 | + private $logger; |
|
| 68 | + |
|
| 69 | + /** @var \Doctrine\DBAL\Query\QueryBuilder */ |
|
| 70 | + private $queryBuilder; |
|
| 71 | + |
|
| 72 | + /** @var QuoteHelper */ |
|
| 73 | + private $helper; |
|
| 74 | + |
|
| 75 | + /** @var bool */ |
|
| 76 | + private $automaticTablePrefix = true; |
|
| 77 | + |
|
| 78 | + /** @var string */ |
|
| 79 | + protected $lastInsertedTable; |
|
| 80 | + |
|
| 81 | + /** |
|
| 82 | + * Initializes a new QueryBuilder. |
|
| 83 | + * |
|
| 84 | + * @param ConnectionAdapter $connection |
|
| 85 | + * @param SystemConfig $systemConfig |
|
| 86 | + * @param ILogger $logger |
|
| 87 | + */ |
|
| 88 | + public function __construct(ConnectionAdapter $connection, SystemConfig $systemConfig, ILogger $logger) { |
|
| 89 | + $this->connection = $connection; |
|
| 90 | + $this->systemConfig = $systemConfig; |
|
| 91 | + $this->logger = $logger; |
|
| 92 | + $this->queryBuilder = new \Doctrine\DBAL\Query\QueryBuilder($this->connection->getInner()); |
|
| 93 | + $this->helper = new QuoteHelper(); |
|
| 94 | + } |
|
| 95 | + |
|
| 96 | + /** |
|
| 97 | + * Enable/disable automatic prefixing of table names with the oc_ prefix |
|
| 98 | + * |
|
| 99 | + * @param bool $enabled If set to true table names will be prefixed with the |
|
| 100 | + * owncloud database prefix automatically. |
|
| 101 | + * @since 8.2.0 |
|
| 102 | + */ |
|
| 103 | + public function automaticTablePrefix($enabled) { |
|
| 104 | + $this->automaticTablePrefix = (bool) $enabled; |
|
| 105 | + } |
|
| 106 | + |
|
| 107 | + /** |
|
| 108 | + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. |
|
| 109 | + * This producer method is intended for convenient inline usage. Example: |
|
| 110 | + * |
|
| 111 | + * <code> |
|
| 112 | + * $qb = $conn->getQueryBuilder() |
|
| 113 | + * ->select('u') |
|
| 114 | + * ->from('users', 'u') |
|
| 115 | + * ->where($qb->expr()->eq('u.id', 1)); |
|
| 116 | + * </code> |
|
| 117 | + * |
|
| 118 | + * For more complex expression construction, consider storing the expression |
|
| 119 | + * builder object in a local variable. |
|
| 120 | + * |
|
| 121 | + * @return \OCP\DB\QueryBuilder\IExpressionBuilder |
|
| 122 | + */ |
|
| 123 | + public function expr() { |
|
| 124 | + if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { |
|
| 125 | + return new OCIExpressionBuilder($this->connection, $this); |
|
| 126 | + } |
|
| 127 | + if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) { |
|
| 128 | + return new PgSqlExpressionBuilder($this->connection, $this); |
|
| 129 | + } |
|
| 130 | + if ($this->connection->getDatabasePlatform() instanceof MySQLPlatform) { |
|
| 131 | + return new MySqlExpressionBuilder($this->connection, $this); |
|
| 132 | + } |
|
| 133 | + if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { |
|
| 134 | + return new SqliteExpressionBuilder($this->connection, $this); |
|
| 135 | + } |
|
| 136 | + |
|
| 137 | + return new ExpressionBuilder($this->connection, $this); |
|
| 138 | + } |
|
| 139 | + |
|
| 140 | + /** |
|
| 141 | + * Gets an FunctionBuilder used for object-oriented construction of query functions. |
|
| 142 | + * This producer method is intended for convenient inline usage. Example: |
|
| 143 | + * |
|
| 144 | + * <code> |
|
| 145 | + * $qb = $conn->getQueryBuilder() |
|
| 146 | + * ->select('u') |
|
| 147 | + * ->from('users', 'u') |
|
| 148 | + * ->where($qb->fun()->md5('u.id')); |
|
| 149 | + * </code> |
|
| 150 | + * |
|
| 151 | + * For more complex function construction, consider storing the function |
|
| 152 | + * builder object in a local variable. |
|
| 153 | + * |
|
| 154 | + * @return \OCP\DB\QueryBuilder\IFunctionBuilder |
|
| 155 | + */ |
|
| 156 | + public function func() { |
|
| 157 | + if ($this->connection->getDatabasePlatform() instanceof OraclePlatform) { |
|
| 158 | + return new OCIFunctionBuilder($this->helper); |
|
| 159 | + } |
|
| 160 | + if ($this->connection->getDatabasePlatform() instanceof SqlitePlatform) { |
|
| 161 | + return new SqliteFunctionBuilder($this->helper); |
|
| 162 | + } |
|
| 163 | + if ($this->connection->getDatabasePlatform() instanceof PostgreSQL94Platform) { |
|
| 164 | + return new PgSqlFunctionBuilder($this->helper); |
|
| 165 | + } |
|
| 166 | + |
|
| 167 | + return new FunctionBuilder($this->helper); |
|
| 168 | + } |
|
| 169 | + |
|
| 170 | + /** |
|
| 171 | + * Gets the type of the currently built query. |
|
| 172 | + * |
|
| 173 | + * @return integer |
|
| 174 | + */ |
|
| 175 | + public function getType() { |
|
| 176 | + return $this->queryBuilder->getType(); |
|
| 177 | + } |
|
| 178 | + |
|
| 179 | + /** |
|
| 180 | + * Gets the associated DBAL Connection for this query builder. |
|
| 181 | + * |
|
| 182 | + * @return \OCP\IDBConnection |
|
| 183 | + */ |
|
| 184 | + public function getConnection() { |
|
| 185 | + return $this->connection; |
|
| 186 | + } |
|
| 187 | + |
|
| 188 | + /** |
|
| 189 | + * Gets the state of this query builder instance. |
|
| 190 | + * |
|
| 191 | + * @return integer Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. |
|
| 192 | + */ |
|
| 193 | + public function getState() { |
|
| 194 | + return $this->queryBuilder->getState(); |
|
| 195 | + } |
|
| 196 | + |
|
| 197 | + /** |
|
| 198 | + * Executes this query using the bound parameters and their types. |
|
| 199 | + * |
|
| 200 | + * Uses {@see Connection::executeQuery} for select statements and {@see Connection::executeUpdate} |
|
| 201 | + * for insert, update and delete statements. |
|
| 202 | + * |
|
| 203 | + * @return IResult|int |
|
| 204 | + */ |
|
| 205 | + public function execute() { |
|
| 206 | + if ($this->systemConfig->getValue('log_query', false)) { |
|
| 207 | + try { |
|
| 208 | + $params = []; |
|
| 209 | + foreach ($this->getParameters() as $placeholder => $value) { |
|
| 210 | + if ($value instanceof \DateTime) { |
|
| 211 | + $params[] = $placeholder . ' => DateTime:\'' . $value->format('c') . '\''; |
|
| 212 | + } elseif (is_array($value)) { |
|
| 213 | + $params[] = $placeholder . ' => (\'' . implode('\', \'', $value) . '\')'; |
|
| 214 | + } else { |
|
| 215 | + $params[] = $placeholder . ' => \'' . $value . '\''; |
|
| 216 | + } |
|
| 217 | + } |
|
| 218 | + if (empty($params)) { |
|
| 219 | + $this->logger->debug('DB QueryBuilder: \'{query}\'', [ |
|
| 220 | + 'query' => $this->getSQL(), |
|
| 221 | + 'app' => 'core', |
|
| 222 | + ]); |
|
| 223 | + } else { |
|
| 224 | + $this->logger->debug('DB QueryBuilder: \'{query}\' with parameters: {params}', [ |
|
| 225 | + 'query' => $this->getSQL(), |
|
| 226 | + 'params' => implode(', ', $params), |
|
| 227 | + 'app' => 'core', |
|
| 228 | + ]); |
|
| 229 | + } |
|
| 230 | + } catch (\Error $e) { |
|
| 231 | + // likely an error during conversion of $value to string |
|
| 232 | + $this->logger->debug('DB QueryBuilder: error trying to log SQL query'); |
|
| 233 | + $this->logger->logException($e); |
|
| 234 | + } |
|
| 235 | + } |
|
| 236 | + |
|
| 237 | + if (!empty($this->getQueryPart('select'))) { |
|
| 238 | + $select = $this->getQueryPart('select'); |
|
| 239 | + $hasSelectAll = array_filter($select, static function ($s) { |
|
| 240 | + return $s === '*'; |
|
| 241 | + }); |
|
| 242 | + $hasSelectSpecific = array_filter($select, static function ($s) { |
|
| 243 | + return $s !== '*'; |
|
| 244 | + }); |
|
| 245 | + |
|
| 246 | + if (empty($hasSelectAll) === empty($hasSelectSpecific)) { |
|
| 247 | + $exception = new QueryException('Query is selecting * and specific values in the same query. This is not supported in Oracle.'); |
|
| 248 | + $this->logger->logException($exception, [ |
|
| 249 | + 'message' => 'Query is selecting * and specific values in the same query. This is not supported in Oracle.', |
|
| 250 | + 'query' => $this->getSQL(), |
|
| 251 | + 'level' => ILogger::ERROR, |
|
| 252 | + 'app' => 'core', |
|
| 253 | + ]); |
|
| 254 | + } |
|
| 255 | + } |
|
| 256 | + |
|
| 257 | + $numberOfParameters = 0; |
|
| 258 | + $hasTooLargeArrayParameter = false; |
|
| 259 | + foreach ($this->getParameters() as $parameter) { |
|
| 260 | + if (is_array($parameter)) { |
|
| 261 | + $count = count($parameter); |
|
| 262 | + $numberOfParameters += $count; |
|
| 263 | + $hasTooLargeArrayParameter = $hasTooLargeArrayParameter || ($count > 1000); |
|
| 264 | + } |
|
| 265 | + } |
|
| 266 | + |
|
| 267 | + if ($hasTooLargeArrayParameter) { |
|
| 268 | + $exception = new QueryException('More than 1000 expressions in a list are not allowed on Oracle.'); |
|
| 269 | + $this->logger->logException($exception, [ |
|
| 270 | + 'message' => 'More than 1000 expressions in a list are not allowed on Oracle.', |
|
| 271 | + 'query' => $this->getSQL(), |
|
| 272 | + 'level' => ILogger::ERROR, |
|
| 273 | + 'app' => 'core', |
|
| 274 | + ]); |
|
| 275 | + } |
|
| 276 | + |
|
| 277 | + if ($numberOfParameters > 65535) { |
|
| 278 | + $exception = new QueryException('The number of parameters must not exceed 65535. Restriction by PostgreSQL.'); |
|
| 279 | + $this->logger->logException($exception, [ |
|
| 280 | + 'message' => 'The number of parameters must not exceed 65535. Restriction by PostgreSQL.', |
|
| 281 | + 'query' => $this->getSQL(), |
|
| 282 | + 'level' => ILogger::ERROR, |
|
| 283 | + 'app' => 'core', |
|
| 284 | + ]); |
|
| 285 | + } |
|
| 286 | + |
|
| 287 | + $result = $this->queryBuilder->execute(); |
|
| 288 | + if (is_int($result)) { |
|
| 289 | + return $result; |
|
| 290 | + } |
|
| 291 | + return new ResultAdapter($result); |
|
| 292 | + } |
|
| 293 | + |
|
| 294 | + public function executeQuery(): IResult { |
|
| 295 | + if ($this->getType() !== \Doctrine\DBAL\Query\QueryBuilder::SELECT) { |
|
| 296 | + throw new \RuntimeException('Invalid query type, expected SELECT query'); |
|
| 297 | + } |
|
| 298 | + |
|
| 299 | + try { |
|
| 300 | + $result = $this->execute(); |
|
| 301 | + } catch (\Doctrine\DBAL\Exception $e) { |
|
| 302 | + throw \OC\DB\Exceptions\DbalException::wrap($e); |
|
| 303 | + } |
|
| 304 | + |
|
| 305 | + if ($result instanceof IResult) { |
|
| 306 | + return $result; |
|
| 307 | + } |
|
| 308 | + |
|
| 309 | + throw new \RuntimeException('Invalid return type for query'); |
|
| 310 | + } |
|
| 311 | + |
|
| 312 | + /** |
|
| 313 | + * Monkey-patched compatibility layer for apps that were adapted for Nextcloud 22 before |
|
| 314 | + * the first beta, where executeStatement was named executeUpdate. |
|
| 315 | + * |
|
| 316 | + * Static analysis should catch those misuses, but until then let's try to keep things |
|
| 317 | + * running. |
|
| 318 | + * |
|
| 319 | + * @internal |
|
| 320 | + * @deprecated |
|
| 321 | + * @todo drop ASAP |
|
| 322 | + */ |
|
| 323 | + public function executeUpdate(): int { |
|
| 324 | + return $this->executeStatement(); |
|
| 325 | + } |
|
| 326 | + |
|
| 327 | + public function executeStatement(): int { |
|
| 328 | + if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::SELECT) { |
|
| 329 | + throw new \RuntimeException('Invalid query type, expected INSERT, DELETE or UPDATE statement'); |
|
| 330 | + } |
|
| 331 | + |
|
| 332 | + try { |
|
| 333 | + $result = $this->execute(); |
|
| 334 | + } catch (\Doctrine\DBAL\Exception $e) { |
|
| 335 | + throw \OC\DB\Exceptions\DbalException::wrap($e); |
|
| 336 | + } |
|
| 337 | + |
|
| 338 | + if (!is_int($result)) { |
|
| 339 | + throw new \RuntimeException('Invalid return type for statement'); |
|
| 340 | + } |
|
| 341 | + |
|
| 342 | + return $result; |
|
| 343 | + } |
|
| 344 | + |
|
| 345 | + |
|
| 346 | + /** |
|
| 347 | + * Gets the complete SQL string formed by the current specifications of this QueryBuilder. |
|
| 348 | + * |
|
| 349 | + * <code> |
|
| 350 | + * $qb = $conn->getQueryBuilder() |
|
| 351 | + * ->select('u') |
|
| 352 | + * ->from('User', 'u') |
|
| 353 | + * echo $qb->getSQL(); // SELECT u FROM User u |
|
| 354 | + * </code> |
|
| 355 | + * |
|
| 356 | + * @return string The SQL query string. |
|
| 357 | + */ |
|
| 358 | + public function getSQL() { |
|
| 359 | + return $this->queryBuilder->getSQL(); |
|
| 360 | + } |
|
| 361 | + |
|
| 362 | + /** |
|
| 363 | + * Sets a query parameter for the query being constructed. |
|
| 364 | + * |
|
| 365 | + * <code> |
|
| 366 | + * $qb = $conn->getQueryBuilder() |
|
| 367 | + * ->select('u') |
|
| 368 | + * ->from('users', 'u') |
|
| 369 | + * ->where('u.id = :user_id') |
|
| 370 | + * ->setParameter(':user_id', 1); |
|
| 371 | + * </code> |
|
| 372 | + * |
|
| 373 | + * @param string|integer $key The parameter position or name. |
|
| 374 | + * @param mixed $value The parameter value. |
|
| 375 | + * @param string|null|int $type One of the IQueryBuilder::PARAM_* constants. |
|
| 376 | + * |
|
| 377 | + * @return $this This QueryBuilder instance. |
|
| 378 | + */ |
|
| 379 | + public function setParameter($key, $value, $type = null) { |
|
| 380 | + $this->queryBuilder->setParameter($key, $value, $type); |
|
| 381 | + |
|
| 382 | + return $this; |
|
| 383 | + } |
|
| 384 | + |
|
| 385 | + /** |
|
| 386 | + * Sets a collection of query parameters for the query being constructed. |
|
| 387 | + * |
|
| 388 | + * <code> |
|
| 389 | + * $qb = $conn->getQueryBuilder() |
|
| 390 | + * ->select('u') |
|
| 391 | + * ->from('users', 'u') |
|
| 392 | + * ->where('u.id = :user_id1 OR u.id = :user_id2') |
|
| 393 | + * ->setParameters(array( |
|
| 394 | + * ':user_id1' => 1, |
|
| 395 | + * ':user_id2' => 2 |
|
| 396 | + * )); |
|
| 397 | + * </code> |
|
| 398 | + * |
|
| 399 | + * @param array $params The query parameters to set. |
|
| 400 | + * @param array $types The query parameters types to set. |
|
| 401 | + * |
|
| 402 | + * @return $this This QueryBuilder instance. |
|
| 403 | + */ |
|
| 404 | + public function setParameters(array $params, array $types = []) { |
|
| 405 | + $this->queryBuilder->setParameters($params, $types); |
|
| 406 | + |
|
| 407 | + return $this; |
|
| 408 | + } |
|
| 409 | + |
|
| 410 | + /** |
|
| 411 | + * Gets all defined query parameters for the query being constructed indexed by parameter index or name. |
|
| 412 | + * |
|
| 413 | + * @return array The currently defined query parameters indexed by parameter index or name. |
|
| 414 | + */ |
|
| 415 | + public function getParameters() { |
|
| 416 | + return $this->queryBuilder->getParameters(); |
|
| 417 | + } |
|
| 418 | + |
|
| 419 | + /** |
|
| 420 | + * Gets a (previously set) query parameter of the query being constructed. |
|
| 421 | + * |
|
| 422 | + * @param mixed $key The key (index or name) of the bound parameter. |
|
| 423 | + * |
|
| 424 | + * @return mixed The value of the bound parameter. |
|
| 425 | + */ |
|
| 426 | + public function getParameter($key) { |
|
| 427 | + return $this->queryBuilder->getParameter($key); |
|
| 428 | + } |
|
| 429 | + |
|
| 430 | + /** |
|
| 431 | + * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. |
|
| 432 | + * |
|
| 433 | + * @return array The currently defined query parameter types indexed by parameter index or name. |
|
| 434 | + */ |
|
| 435 | + public function getParameterTypes() { |
|
| 436 | + return $this->queryBuilder->getParameterTypes(); |
|
| 437 | + } |
|
| 438 | + |
|
| 439 | + /** |
|
| 440 | + * Gets a (previously set) query parameter type of the query being constructed. |
|
| 441 | + * |
|
| 442 | + * @param mixed $key The key (index or name) of the bound parameter type. |
|
| 443 | + * |
|
| 444 | + * @return mixed The value of the bound parameter type. |
|
| 445 | + */ |
|
| 446 | + public function getParameterType($key) { |
|
| 447 | + return $this->queryBuilder->getParameterType($key); |
|
| 448 | + } |
|
| 449 | + |
|
| 450 | + /** |
|
| 451 | + * Sets the position of the first result to retrieve (the "offset"). |
|
| 452 | + * |
|
| 453 | + * @param integer $firstResult The first result to return. |
|
| 454 | + * |
|
| 455 | + * @return $this This QueryBuilder instance. |
|
| 456 | + */ |
|
| 457 | + public function setFirstResult($firstResult) { |
|
| 458 | + $this->queryBuilder->setFirstResult($firstResult); |
|
| 459 | + |
|
| 460 | + return $this; |
|
| 461 | + } |
|
| 462 | + |
|
| 463 | + /** |
|
| 464 | + * Gets the position of the first result the query object was set to retrieve (the "offset"). |
|
| 465 | + * Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder. |
|
| 466 | + * |
|
| 467 | + * @return integer The position of the first result. |
|
| 468 | + */ |
|
| 469 | + public function getFirstResult() { |
|
| 470 | + return $this->queryBuilder->getFirstResult(); |
|
| 471 | + } |
|
| 472 | + |
|
| 473 | + /** |
|
| 474 | + * Sets the maximum number of results to retrieve (the "limit"). |
|
| 475 | + * |
|
| 476 | + * NOTE: Setting max results to "0" will cause mixed behaviour. While most |
|
| 477 | + * of the databases will just return an empty result set, Oracle will return |
|
| 478 | + * all entries. |
|
| 479 | + * |
|
| 480 | + * @param integer $maxResults The maximum number of results to retrieve. |
|
| 481 | + * |
|
| 482 | + * @return $this This QueryBuilder instance. |
|
| 483 | + */ |
|
| 484 | + public function setMaxResults($maxResults) { |
|
| 485 | + $this->queryBuilder->setMaxResults($maxResults); |
|
| 486 | + |
|
| 487 | + return $this; |
|
| 488 | + } |
|
| 489 | + |
|
| 490 | + /** |
|
| 491 | + * Gets the maximum number of results the query object was set to retrieve (the "limit"). |
|
| 492 | + * Returns NULL if {@link setMaxResults} was not applied to this query builder. |
|
| 493 | + * |
|
| 494 | + * @return int|null The maximum number of results. |
|
| 495 | + */ |
|
| 496 | + public function getMaxResults() { |
|
| 497 | + return $this->queryBuilder->getMaxResults(); |
|
| 498 | + } |
|
| 499 | + |
|
| 500 | + /** |
|
| 501 | + * Specifies an item that is to be returned in the query result. |
|
| 502 | + * Replaces any previously specified selections, if any. |
|
| 503 | + * |
|
| 504 | + * <code> |
|
| 505 | + * $qb = $conn->getQueryBuilder() |
|
| 506 | + * ->select('u.id', 'p.id') |
|
| 507 | + * ->from('users', 'u') |
|
| 508 | + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 509 | + * </code> |
|
| 510 | + * |
|
| 511 | + * @param mixed ...$selects The selection expressions. |
|
| 512 | + * |
|
| 513 | + * '@return $this This QueryBuilder instance. |
|
| 514 | + */ |
|
| 515 | + public function select(...$selects) { |
|
| 516 | + if (count($selects) === 1 && is_array($selects[0])) { |
|
| 517 | + $selects = $selects[0]; |
|
| 518 | + } |
|
| 519 | + |
|
| 520 | + $this->queryBuilder->select( |
|
| 521 | + $this->helper->quoteColumnNames($selects) |
|
| 522 | + ); |
|
| 523 | + |
|
| 524 | + return $this; |
|
| 525 | + } |
|
| 526 | + |
|
| 527 | + /** |
|
| 528 | + * Specifies an item that is to be returned with a different name in the query result. |
|
| 529 | + * |
|
| 530 | + * <code> |
|
| 531 | + * $qb = $conn->getQueryBuilder() |
|
| 532 | + * ->selectAlias('u.id', 'user_id') |
|
| 533 | + * ->from('users', 'u') |
|
| 534 | + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); |
|
| 535 | + * </code> |
|
| 536 | + * |
|
| 537 | + * @param mixed $select The selection expressions. |
|
| 538 | + * @param string $alias The column alias used in the constructed query. |
|
| 539 | + * |
|
| 540 | + * @return $this This QueryBuilder instance. |
|
| 541 | + */ |
|
| 542 | + public function selectAlias($select, $alias) { |
|
| 543 | + $this->queryBuilder->addSelect( |
|
| 544 | + $this->helper->quoteColumnName($select) . ' AS ' . $this->helper->quoteColumnName($alias) |
|
| 545 | + ); |
|
| 546 | + |
|
| 547 | + return $this; |
|
| 548 | + } |
|
| 549 | + |
|
| 550 | + /** |
|
| 551 | + * Specifies an item that is to be returned uniquely in the query result. |
|
| 552 | + * |
|
| 553 | + * <code> |
|
| 554 | + * $qb = $conn->getQueryBuilder() |
|
| 555 | + * ->selectDistinct('type') |
|
| 556 | + * ->from('users'); |
|
| 557 | + * </code> |
|
| 558 | + * |
|
| 559 | + * @param mixed $select The selection expressions. |
|
| 560 | + * |
|
| 561 | + * @return $this This QueryBuilder instance. |
|
| 562 | + */ |
|
| 563 | + public function selectDistinct($select) { |
|
| 564 | + if (!is_array($select)) { |
|
| 565 | + $select = [$select]; |
|
| 566 | + } |
|
| 567 | + |
|
| 568 | + $quotedSelect = $this->helper->quoteColumnNames($select); |
|
| 569 | + |
|
| 570 | + $this->queryBuilder->addSelect( |
|
| 571 | + 'DISTINCT ' . implode(', ', $quotedSelect) |
|
| 572 | + ); |
|
| 573 | + |
|
| 574 | + return $this; |
|
| 575 | + } |
|
| 576 | + |
|
| 577 | + /** |
|
| 578 | + * Adds an item that is to be returned in the query result. |
|
| 579 | + * |
|
| 580 | + * <code> |
|
| 581 | + * $qb = $conn->getQueryBuilder() |
|
| 582 | + * ->select('u.id') |
|
| 583 | + * ->addSelect('p.id') |
|
| 584 | + * ->from('users', 'u') |
|
| 585 | + * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); |
|
| 586 | + * </code> |
|
| 587 | + * |
|
| 588 | + * @param mixed ...$selects The selection expression. |
|
| 589 | + * |
|
| 590 | + * @return $this This QueryBuilder instance. |
|
| 591 | + */ |
|
| 592 | + public function addSelect(...$selects) { |
|
| 593 | + if (count($selects) === 1 && is_array($selects[0])) { |
|
| 594 | + $selects = $selects[0]; |
|
| 595 | + } |
|
| 596 | + |
|
| 597 | + $this->queryBuilder->addSelect( |
|
| 598 | + $this->helper->quoteColumnNames($selects) |
|
| 599 | + ); |
|
| 600 | + |
|
| 601 | + return $this; |
|
| 602 | + } |
|
| 603 | + |
|
| 604 | + /** |
|
| 605 | + * Turns the query being built into a bulk delete query that ranges over |
|
| 606 | + * a certain table. |
|
| 607 | + * |
|
| 608 | + * <code> |
|
| 609 | + * $qb = $conn->getQueryBuilder() |
|
| 610 | + * ->delete('users', 'u') |
|
| 611 | + * ->where('u.id = :user_id'); |
|
| 612 | + * ->setParameter(':user_id', 1); |
|
| 613 | + * </code> |
|
| 614 | + * |
|
| 615 | + * @param string $delete The table whose rows are subject to the deletion. |
|
| 616 | + * @param string $alias The table alias used in the constructed query. |
|
| 617 | + * |
|
| 618 | + * @return $this This QueryBuilder instance. |
|
| 619 | + */ |
|
| 620 | + public function delete($delete = null, $alias = null) { |
|
| 621 | + $this->queryBuilder->delete( |
|
| 622 | + $this->getTableName($delete), |
|
| 623 | + $alias |
|
| 624 | + ); |
|
| 625 | + |
|
| 626 | + return $this; |
|
| 627 | + } |
|
| 628 | + |
|
| 629 | + /** |
|
| 630 | + * Turns the query being built into a bulk update query that ranges over |
|
| 631 | + * a certain table |
|
| 632 | + * |
|
| 633 | + * <code> |
|
| 634 | + * $qb = $conn->getQueryBuilder() |
|
| 635 | + * ->update('users', 'u') |
|
| 636 | + * ->set('u.password', md5('password')) |
|
| 637 | + * ->where('u.id = ?'); |
|
| 638 | + * </code> |
|
| 639 | + * |
|
| 640 | + * @param string $update The table whose rows are subject to the update. |
|
| 641 | + * @param string $alias The table alias used in the constructed query. |
|
| 642 | + * |
|
| 643 | + * @return $this This QueryBuilder instance. |
|
| 644 | + */ |
|
| 645 | + public function update($update = null, $alias = null) { |
|
| 646 | + $this->queryBuilder->update( |
|
| 647 | + $this->getTableName($update), |
|
| 648 | + $alias |
|
| 649 | + ); |
|
| 650 | + |
|
| 651 | + return $this; |
|
| 652 | + } |
|
| 653 | + |
|
| 654 | + /** |
|
| 655 | + * Turns the query being built into an insert query that inserts into |
|
| 656 | + * a certain table |
|
| 657 | + * |
|
| 658 | + * <code> |
|
| 659 | + * $qb = $conn->getQueryBuilder() |
|
| 660 | + * ->insert('users') |
|
| 661 | + * ->values( |
|
| 662 | + * array( |
|
| 663 | + * 'name' => '?', |
|
| 664 | + * 'password' => '?' |
|
| 665 | + * ) |
|
| 666 | + * ); |
|
| 667 | + * </code> |
|
| 668 | + * |
|
| 669 | + * @param string $insert The table into which the rows should be inserted. |
|
| 670 | + * |
|
| 671 | + * @return $this This QueryBuilder instance. |
|
| 672 | + */ |
|
| 673 | + public function insert($insert = null) { |
|
| 674 | + $this->queryBuilder->insert( |
|
| 675 | + $this->getTableName($insert) |
|
| 676 | + ); |
|
| 677 | + |
|
| 678 | + $this->lastInsertedTable = $insert; |
|
| 679 | + |
|
| 680 | + return $this; |
|
| 681 | + } |
|
| 682 | + |
|
| 683 | + /** |
|
| 684 | + * Creates and adds a query root corresponding to the table identified by the |
|
| 685 | + * given alias, forming a cartesian product with any existing query roots. |
|
| 686 | + * |
|
| 687 | + * <code> |
|
| 688 | + * $qb = $conn->getQueryBuilder() |
|
| 689 | + * ->select('u.id') |
|
| 690 | + * ->from('users', 'u') |
|
| 691 | + * </code> |
|
| 692 | + * |
|
| 693 | + * @param string $from The table. |
|
| 694 | + * @param string|null $alias The alias of the table. |
|
| 695 | + * |
|
| 696 | + * @return $this This QueryBuilder instance. |
|
| 697 | + */ |
|
| 698 | + public function from($from, $alias = null) { |
|
| 699 | + $this->queryBuilder->from( |
|
| 700 | + $this->getTableName($from), |
|
| 701 | + $this->quoteAlias($alias) |
|
| 702 | + ); |
|
| 703 | + |
|
| 704 | + return $this; |
|
| 705 | + } |
|
| 706 | + |
|
| 707 | + /** |
|
| 708 | + * Creates and adds a join to the query. |
|
| 709 | + * |
|
| 710 | + * <code> |
|
| 711 | + * $qb = $conn->getQueryBuilder() |
|
| 712 | + * ->select('u.name') |
|
| 713 | + * ->from('users', 'u') |
|
| 714 | + * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 715 | + * </code> |
|
| 716 | + * |
|
| 717 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 718 | + * @param string $join The table name to join. |
|
| 719 | + * @param string $alias The alias of the join table. |
|
| 720 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 721 | + * |
|
| 722 | + * @return $this This QueryBuilder instance. |
|
| 723 | + */ |
|
| 724 | + public function join($fromAlias, $join, $alias, $condition = null) { |
|
| 725 | + $this->queryBuilder->join( |
|
| 726 | + $this->quoteAlias($fromAlias), |
|
| 727 | + $this->getTableName($join), |
|
| 728 | + $this->quoteAlias($alias), |
|
| 729 | + $condition |
|
| 730 | + ); |
|
| 731 | + |
|
| 732 | + return $this; |
|
| 733 | + } |
|
| 734 | + |
|
| 735 | + /** |
|
| 736 | + * Creates and adds a join to the query. |
|
| 737 | + * |
|
| 738 | + * <code> |
|
| 739 | + * $qb = $conn->getQueryBuilder() |
|
| 740 | + * ->select('u.name') |
|
| 741 | + * ->from('users', 'u') |
|
| 742 | + * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 743 | + * </code> |
|
| 744 | + * |
|
| 745 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 746 | + * @param string $join The table name to join. |
|
| 747 | + * @param string $alias The alias of the join table. |
|
| 748 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 749 | + * |
|
| 750 | + * @return $this This QueryBuilder instance. |
|
| 751 | + */ |
|
| 752 | + public function innerJoin($fromAlias, $join, $alias, $condition = null) { |
|
| 753 | + $this->queryBuilder->innerJoin( |
|
| 754 | + $this->quoteAlias($fromAlias), |
|
| 755 | + $this->getTableName($join), |
|
| 756 | + $this->quoteAlias($alias), |
|
| 757 | + $condition |
|
| 758 | + ); |
|
| 759 | + |
|
| 760 | + return $this; |
|
| 761 | + } |
|
| 762 | + |
|
| 763 | + /** |
|
| 764 | + * Creates and adds a left join to the query. |
|
| 765 | + * |
|
| 766 | + * <code> |
|
| 767 | + * $qb = $conn->getQueryBuilder() |
|
| 768 | + * ->select('u.name') |
|
| 769 | + * ->from('users', 'u') |
|
| 770 | + * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 771 | + * </code> |
|
| 772 | + * |
|
| 773 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 774 | + * @param string $join The table name to join. |
|
| 775 | + * @param string $alias The alias of the join table. |
|
| 776 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 777 | + * |
|
| 778 | + * @return $this This QueryBuilder instance. |
|
| 779 | + */ |
|
| 780 | + public function leftJoin($fromAlias, $join, $alias, $condition = null) { |
|
| 781 | + $this->queryBuilder->leftJoin( |
|
| 782 | + $this->quoteAlias($fromAlias), |
|
| 783 | + $this->getTableName($join), |
|
| 784 | + $this->quoteAlias($alias), |
|
| 785 | + $condition |
|
| 786 | + ); |
|
| 787 | + |
|
| 788 | + return $this; |
|
| 789 | + } |
|
| 790 | + |
|
| 791 | + /** |
|
| 792 | + * Creates and adds a right join to the query. |
|
| 793 | + * |
|
| 794 | + * <code> |
|
| 795 | + * $qb = $conn->getQueryBuilder() |
|
| 796 | + * ->select('u.name') |
|
| 797 | + * ->from('users', 'u') |
|
| 798 | + * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); |
|
| 799 | + * </code> |
|
| 800 | + * |
|
| 801 | + * @param string $fromAlias The alias that points to a from clause. |
|
| 802 | + * @param string $join The table name to join. |
|
| 803 | + * @param string $alias The alias of the join table. |
|
| 804 | + * @param string|ICompositeExpression|null $condition The condition for the join. |
|
| 805 | + * |
|
| 806 | + * @return $this This QueryBuilder instance. |
|
| 807 | + */ |
|
| 808 | + public function rightJoin($fromAlias, $join, $alias, $condition = null) { |
|
| 809 | + $this->queryBuilder->rightJoin( |
|
| 810 | + $this->quoteAlias($fromAlias), |
|
| 811 | + $this->getTableName($join), |
|
| 812 | + $this->quoteAlias($alias), |
|
| 813 | + $condition |
|
| 814 | + ); |
|
| 815 | + |
|
| 816 | + return $this; |
|
| 817 | + } |
|
| 818 | + |
|
| 819 | + /** |
|
| 820 | + * Sets a new value for a column in a bulk update query. |
|
| 821 | + * |
|
| 822 | + * <code> |
|
| 823 | + * $qb = $conn->getQueryBuilder() |
|
| 824 | + * ->update('users', 'u') |
|
| 825 | + * ->set('u.password', md5('password')) |
|
| 826 | + * ->where('u.id = ?'); |
|
| 827 | + * </code> |
|
| 828 | + * |
|
| 829 | + * @param string $key The column to set. |
|
| 830 | + * @param ILiteral|IParameter|IQueryFunction|string $value The value, expression, placeholder, etc. |
|
| 831 | + * |
|
| 832 | + * @return $this This QueryBuilder instance. |
|
| 833 | + */ |
|
| 834 | + public function set($key, $value) { |
|
| 835 | + $this->queryBuilder->set( |
|
| 836 | + $this->helper->quoteColumnName($key), |
|
| 837 | + $this->helper->quoteColumnName($value) |
|
| 838 | + ); |
|
| 839 | + |
|
| 840 | + return $this; |
|
| 841 | + } |
|
| 842 | + |
|
| 843 | + /** |
|
| 844 | + * Specifies one or more restrictions to the query result. |
|
| 845 | + * Replaces any previously specified restrictions, if any. |
|
| 846 | + * |
|
| 847 | + * <code> |
|
| 848 | + * $qb = $conn->getQueryBuilder() |
|
| 849 | + * ->select('u.name') |
|
| 850 | + * ->from('users', 'u') |
|
| 851 | + * ->where('u.id = ?'); |
|
| 852 | + * |
|
| 853 | + * // You can optionally programatically build and/or expressions |
|
| 854 | + * $qb = $conn->getQueryBuilder(); |
|
| 855 | + * |
|
| 856 | + * $or = $qb->expr()->orx(); |
|
| 857 | + * $or->add($qb->expr()->eq('u.id', 1)); |
|
| 858 | + * $or->add($qb->expr()->eq('u.id', 2)); |
|
| 859 | + * |
|
| 860 | + * $qb->update('users', 'u') |
|
| 861 | + * ->set('u.password', md5('password')) |
|
| 862 | + * ->where($or); |
|
| 863 | + * </code> |
|
| 864 | + * |
|
| 865 | + * @param mixed ...$predicates The restriction predicates. |
|
| 866 | + * |
|
| 867 | + * @return $this This QueryBuilder instance. |
|
| 868 | + */ |
|
| 869 | + public function where(...$predicates) { |
|
| 870 | + call_user_func_array( |
|
| 871 | + [$this->queryBuilder, 'where'], |
|
| 872 | + $predicates |
|
| 873 | + ); |
|
| 874 | + |
|
| 875 | + return $this; |
|
| 876 | + } |
|
| 877 | + |
|
| 878 | + /** |
|
| 879 | + * Adds one or more restrictions to the query results, forming a logical |
|
| 880 | + * conjunction with any previously specified restrictions. |
|
| 881 | + * |
|
| 882 | + * <code> |
|
| 883 | + * $qb = $conn->getQueryBuilder() |
|
| 884 | + * ->select('u') |
|
| 885 | + * ->from('users', 'u') |
|
| 886 | + * ->where('u.username LIKE ?') |
|
| 887 | + * ->andWhere('u.is_active = 1'); |
|
| 888 | + * </code> |
|
| 889 | + * |
|
| 890 | + * @param mixed ...$where The query restrictions. |
|
| 891 | + * |
|
| 892 | + * @return $this This QueryBuilder instance. |
|
| 893 | + * |
|
| 894 | + * @see where() |
|
| 895 | + */ |
|
| 896 | + public function andWhere(...$where) { |
|
| 897 | + call_user_func_array( |
|
| 898 | + [$this->queryBuilder, 'andWhere'], |
|
| 899 | + $where |
|
| 900 | + ); |
|
| 901 | + |
|
| 902 | + return $this; |
|
| 903 | + } |
|
| 904 | + |
|
| 905 | + /** |
|
| 906 | + * Adds one or more restrictions to the query results, forming a logical |
|
| 907 | + * disjunction with any previously specified restrictions. |
|
| 908 | + * |
|
| 909 | + * <code> |
|
| 910 | + * $qb = $conn->getQueryBuilder() |
|
| 911 | + * ->select('u.name') |
|
| 912 | + * ->from('users', 'u') |
|
| 913 | + * ->where('u.id = 1') |
|
| 914 | + * ->orWhere('u.id = 2'); |
|
| 915 | + * </code> |
|
| 916 | + * |
|
| 917 | + * @param mixed ...$where The WHERE statement. |
|
| 918 | + * |
|
| 919 | + * @return $this This QueryBuilder instance. |
|
| 920 | + * |
|
| 921 | + * @see where() |
|
| 922 | + */ |
|
| 923 | + public function orWhere(...$where) { |
|
| 924 | + call_user_func_array( |
|
| 925 | + [$this->queryBuilder, 'orWhere'], |
|
| 926 | + $where |
|
| 927 | + ); |
|
| 928 | + |
|
| 929 | + return $this; |
|
| 930 | + } |
|
| 931 | + |
|
| 932 | + /** |
|
| 933 | + * Specifies a grouping over the results of the query. |
|
| 934 | + * Replaces any previously specified groupings, if any. |
|
| 935 | + * |
|
| 936 | + * <code> |
|
| 937 | + * $qb = $conn->getQueryBuilder() |
|
| 938 | + * ->select('u.name') |
|
| 939 | + * ->from('users', 'u') |
|
| 940 | + * ->groupBy('u.id'); |
|
| 941 | + * </code> |
|
| 942 | + * |
|
| 943 | + * @param mixed ...$groupBys The grouping expression. |
|
| 944 | + * |
|
| 945 | + * @return $this This QueryBuilder instance. |
|
| 946 | + */ |
|
| 947 | + public function groupBy(...$groupBys) { |
|
| 948 | + if (count($groupBys) === 1 && is_array($groupBys[0])) { |
|
| 949 | + $groupBys = $groupBys[0]; |
|
| 950 | + } |
|
| 951 | + |
|
| 952 | + call_user_func_array( |
|
| 953 | + [$this->queryBuilder, 'groupBy'], |
|
| 954 | + $this->helper->quoteColumnNames($groupBys) |
|
| 955 | + ); |
|
| 956 | + |
|
| 957 | + return $this; |
|
| 958 | + } |
|
| 959 | + |
|
| 960 | + /** |
|
| 961 | + * Adds a grouping expression to the query. |
|
| 962 | + * |
|
| 963 | + * <code> |
|
| 964 | + * $qb = $conn->getQueryBuilder() |
|
| 965 | + * ->select('u.name') |
|
| 966 | + * ->from('users', 'u') |
|
| 967 | + * ->groupBy('u.lastLogin'); |
|
| 968 | + * ->addGroupBy('u.createdAt') |
|
| 969 | + * </code> |
|
| 970 | + * |
|
| 971 | + * @param mixed ...$groupBy The grouping expression. |
|
| 972 | + * |
|
| 973 | + * @return $this This QueryBuilder instance. |
|
| 974 | + */ |
|
| 975 | + public function addGroupBy(...$groupBys) { |
|
| 976 | + if (count($groupBys) === 1 && is_array($groupBys[0])) { |
|
| 977 | + $$groupBys = $groupBys[0]; |
|
| 978 | + } |
|
| 979 | + |
|
| 980 | + call_user_func_array( |
|
| 981 | + [$this->queryBuilder, 'addGroupBy'], |
|
| 982 | + $this->helper->quoteColumnNames($groupBys) |
|
| 983 | + ); |
|
| 984 | + |
|
| 985 | + return $this; |
|
| 986 | + } |
|
| 987 | + |
|
| 988 | + /** |
|
| 989 | + * Sets a value for a column in an insert query. |
|
| 990 | + * |
|
| 991 | + * <code> |
|
| 992 | + * $qb = $conn->getQueryBuilder() |
|
| 993 | + * ->insert('users') |
|
| 994 | + * ->values( |
|
| 995 | + * array( |
|
| 996 | + * 'name' => '?' |
|
| 997 | + * ) |
|
| 998 | + * ) |
|
| 999 | + * ->setValue('password', '?'); |
|
| 1000 | + * </code> |
|
| 1001 | + * |
|
| 1002 | + * @param string $column The column into which the value should be inserted. |
|
| 1003 | + * @param IParameter|string $value The value that should be inserted into the column. |
|
| 1004 | + * |
|
| 1005 | + * @return $this This QueryBuilder instance. |
|
| 1006 | + */ |
|
| 1007 | + public function setValue($column, $value) { |
|
| 1008 | + $this->queryBuilder->setValue( |
|
| 1009 | + $this->helper->quoteColumnName($column), |
|
| 1010 | + (string) $value |
|
| 1011 | + ); |
|
| 1012 | + |
|
| 1013 | + return $this; |
|
| 1014 | + } |
|
| 1015 | + |
|
| 1016 | + /** |
|
| 1017 | + * Specifies values for an insert query indexed by column names. |
|
| 1018 | + * Replaces any previous values, if any. |
|
| 1019 | + * |
|
| 1020 | + * <code> |
|
| 1021 | + * $qb = $conn->getQueryBuilder() |
|
| 1022 | + * ->insert('users') |
|
| 1023 | + * ->values( |
|
| 1024 | + * array( |
|
| 1025 | + * 'name' => '?', |
|
| 1026 | + * 'password' => '?' |
|
| 1027 | + * ) |
|
| 1028 | + * ); |
|
| 1029 | + * </code> |
|
| 1030 | + * |
|
| 1031 | + * @param array $values The values to specify for the insert query indexed by column names. |
|
| 1032 | + * |
|
| 1033 | + * @return $this This QueryBuilder instance. |
|
| 1034 | + */ |
|
| 1035 | + public function values(array $values) { |
|
| 1036 | + $quotedValues = []; |
|
| 1037 | + foreach ($values as $key => $value) { |
|
| 1038 | + $quotedValues[$this->helper->quoteColumnName($key)] = $value; |
|
| 1039 | + } |
|
| 1040 | + |
|
| 1041 | + $this->queryBuilder->values($quotedValues); |
|
| 1042 | + |
|
| 1043 | + return $this; |
|
| 1044 | + } |
|
| 1045 | + |
|
| 1046 | + /** |
|
| 1047 | + * Specifies a restriction over the groups of the query. |
|
| 1048 | + * Replaces any previous having restrictions, if any. |
|
| 1049 | + * |
|
| 1050 | + * @param mixed ...$having The restriction over the groups. |
|
| 1051 | + * |
|
| 1052 | + * @return $this This QueryBuilder instance. |
|
| 1053 | + */ |
|
| 1054 | + public function having(...$having) { |
|
| 1055 | + call_user_func_array( |
|
| 1056 | + [$this->queryBuilder, 'having'], |
|
| 1057 | + $having |
|
| 1058 | + ); |
|
| 1059 | + |
|
| 1060 | + return $this; |
|
| 1061 | + } |
|
| 1062 | + |
|
| 1063 | + /** |
|
| 1064 | + * Adds a restriction over the groups of the query, forming a logical |
|
| 1065 | + * conjunction with any existing having restrictions. |
|
| 1066 | + * |
|
| 1067 | + * @param mixed ...$having The restriction to append. |
|
| 1068 | + * |
|
| 1069 | + * @return $this This QueryBuilder instance. |
|
| 1070 | + */ |
|
| 1071 | + public function andHaving(...$having) { |
|
| 1072 | + call_user_func_array( |
|
| 1073 | + [$this->queryBuilder, 'andHaving'], |
|
| 1074 | + $having |
|
| 1075 | + ); |
|
| 1076 | + |
|
| 1077 | + return $this; |
|
| 1078 | + } |
|
| 1079 | + |
|
| 1080 | + /** |
|
| 1081 | + * Adds a restriction over the groups of the query, forming a logical |
|
| 1082 | + * disjunction with any existing having restrictions. |
|
| 1083 | + * |
|
| 1084 | + * @param mixed ...$having The restriction to add. |
|
| 1085 | + * |
|
| 1086 | + * @return $this This QueryBuilder instance. |
|
| 1087 | + */ |
|
| 1088 | + public function orHaving(...$having) { |
|
| 1089 | + call_user_func_array( |
|
| 1090 | + [$this->queryBuilder, 'orHaving'], |
|
| 1091 | + $having |
|
| 1092 | + ); |
|
| 1093 | + |
|
| 1094 | + return $this; |
|
| 1095 | + } |
|
| 1096 | + |
|
| 1097 | + /** |
|
| 1098 | + * Specifies an ordering for the query results. |
|
| 1099 | + * Replaces any previously specified orderings, if any. |
|
| 1100 | + * |
|
| 1101 | + * @param string $sort The ordering expression. |
|
| 1102 | + * @param string $order The ordering direction. |
|
| 1103 | + * |
|
| 1104 | + * @return $this This QueryBuilder instance. |
|
| 1105 | + */ |
|
| 1106 | + public function orderBy($sort, $order = null) { |
|
| 1107 | + $this->queryBuilder->orderBy( |
|
| 1108 | + $this->helper->quoteColumnName($sort), |
|
| 1109 | + $order |
|
| 1110 | + ); |
|
| 1111 | + |
|
| 1112 | + return $this; |
|
| 1113 | + } |
|
| 1114 | + |
|
| 1115 | + /** |
|
| 1116 | + * Adds an ordering to the query results. |
|
| 1117 | + * |
|
| 1118 | + * @param string $sort The ordering expression. |
|
| 1119 | + * @param string $order The ordering direction. |
|
| 1120 | + * |
|
| 1121 | + * @return $this This QueryBuilder instance. |
|
| 1122 | + */ |
|
| 1123 | + public function addOrderBy($sort, $order = null) { |
|
| 1124 | + $this->queryBuilder->addOrderBy( |
|
| 1125 | + $this->helper->quoteColumnName($sort), |
|
| 1126 | + $order |
|
| 1127 | + ); |
|
| 1128 | + |
|
| 1129 | + return $this; |
|
| 1130 | + } |
|
| 1131 | + |
|
| 1132 | + /** |
|
| 1133 | + * Gets a query part by its name. |
|
| 1134 | + * |
|
| 1135 | + * @param string $queryPartName |
|
| 1136 | + * |
|
| 1137 | + * @return mixed |
|
| 1138 | + */ |
|
| 1139 | + public function getQueryPart($queryPartName) { |
|
| 1140 | + return $this->queryBuilder->getQueryPart($queryPartName); |
|
| 1141 | + } |
|
| 1142 | + |
|
| 1143 | + /** |
|
| 1144 | + * Gets all query parts. |
|
| 1145 | + * |
|
| 1146 | + * @return array |
|
| 1147 | + */ |
|
| 1148 | + public function getQueryParts() { |
|
| 1149 | + return $this->queryBuilder->getQueryParts(); |
|
| 1150 | + } |
|
| 1151 | + |
|
| 1152 | + /** |
|
| 1153 | + * Resets SQL parts. |
|
| 1154 | + * |
|
| 1155 | + * @param array|null $queryPartNames |
|
| 1156 | + * |
|
| 1157 | + * @return $this This QueryBuilder instance. |
|
| 1158 | + */ |
|
| 1159 | + public function resetQueryParts($queryPartNames = null) { |
|
| 1160 | + $this->queryBuilder->resetQueryParts($queryPartNames); |
|
| 1161 | + |
|
| 1162 | + return $this; |
|
| 1163 | + } |
|
| 1164 | + |
|
| 1165 | + /** |
|
| 1166 | + * Resets a single SQL part. |
|
| 1167 | + * |
|
| 1168 | + * @param string $queryPartName |
|
| 1169 | + * |
|
| 1170 | + * @return $this This QueryBuilder instance. |
|
| 1171 | + */ |
|
| 1172 | + public function resetQueryPart($queryPartName) { |
|
| 1173 | + $this->queryBuilder->resetQueryPart($queryPartName); |
|
| 1174 | + |
|
| 1175 | + return $this; |
|
| 1176 | + } |
|
| 1177 | + |
|
| 1178 | + /** |
|
| 1179 | + * Creates a new named parameter and bind the value $value to it. |
|
| 1180 | + * |
|
| 1181 | + * This method provides a shortcut for PDOStatement::bindValue |
|
| 1182 | + * when using prepared statements. |
|
| 1183 | + * |
|
| 1184 | + * The parameter $value specifies the value that you want to bind. If |
|
| 1185 | + * $placeholder is not provided bindValue() will automatically create a |
|
| 1186 | + * placeholder for you. An automatic placeholder will be of the name |
|
| 1187 | + * ':dcValue1', ':dcValue2' etc. |
|
| 1188 | + * |
|
| 1189 | + * For more information see {@link https://www.php.net/pdostatement-bindparam} |
|
| 1190 | + * |
|
| 1191 | + * Example: |
|
| 1192 | + * <code> |
|
| 1193 | + * $value = 2; |
|
| 1194 | + * $q->eq( 'id', $q->bindValue( $value ) ); |
|
| 1195 | + * $stmt = $q->executeQuery(); // executed with 'id = 2' |
|
| 1196 | + * </code> |
|
| 1197 | + * |
|
| 1198 | + * @license New BSD License |
|
| 1199 | + * @link http://www.zetacomponents.org |
|
| 1200 | + * |
|
| 1201 | + * @param mixed $value |
|
| 1202 | + * @param mixed $type |
|
| 1203 | + * @param string $placeHolder The name to bind with. The string must start with a colon ':'. |
|
| 1204 | + * |
|
| 1205 | + * @return IParameter the placeholder name used. |
|
| 1206 | + */ |
|
| 1207 | + public function createNamedParameter($value, $type = IQueryBuilder::PARAM_STR, $placeHolder = null) { |
|
| 1208 | + return new Parameter($this->queryBuilder->createNamedParameter($value, $type, $placeHolder)); |
|
| 1209 | + } |
|
| 1210 | + |
|
| 1211 | + /** |
|
| 1212 | + * Creates a new positional parameter and bind the given value to it. |
|
| 1213 | + * |
|
| 1214 | + * Attention: If you are using positional parameters with the query builder you have |
|
| 1215 | + * to be very careful to bind all parameters in the order they appear in the SQL |
|
| 1216 | + * statement , otherwise they get bound in the wrong order which can lead to serious |
|
| 1217 | + * bugs in your code. |
|
| 1218 | + * |
|
| 1219 | + * Example: |
|
| 1220 | + * <code> |
|
| 1221 | + * $qb = $conn->getQueryBuilder(); |
|
| 1222 | + * $qb->select('u.*') |
|
| 1223 | + * ->from('users', 'u') |
|
| 1224 | + * ->where('u.username = ' . $qb->createPositionalParameter('Foo', IQueryBuilder::PARAM_STR)) |
|
| 1225 | + * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', IQueryBuilder::PARAM_STR)) |
|
| 1226 | + * </code> |
|
| 1227 | + * |
|
| 1228 | + * @param mixed $value |
|
| 1229 | + * @param integer $type |
|
| 1230 | + * |
|
| 1231 | + * @return IParameter |
|
| 1232 | + */ |
|
| 1233 | + public function createPositionalParameter($value, $type = IQueryBuilder::PARAM_STR) { |
|
| 1234 | + return new Parameter($this->queryBuilder->createPositionalParameter($value, $type)); |
|
| 1235 | + } |
|
| 1236 | + |
|
| 1237 | + /** |
|
| 1238 | + * Creates a new parameter |
|
| 1239 | + * |
|
| 1240 | + * Example: |
|
| 1241 | + * <code> |
|
| 1242 | + * $qb = $conn->getQueryBuilder(); |
|
| 1243 | + * $qb->select('u.*') |
|
| 1244 | + * ->from('users', 'u') |
|
| 1245 | + * ->where('u.username = ' . $qb->createParameter('name')) |
|
| 1246 | + * ->setParameter('name', 'Bar', IQueryBuilder::PARAM_STR)) |
|
| 1247 | + * </code> |
|
| 1248 | + * |
|
| 1249 | + * @param string $name |
|
| 1250 | + * |
|
| 1251 | + * @return IParameter |
|
| 1252 | + */ |
|
| 1253 | + public function createParameter($name) { |
|
| 1254 | + return new Parameter(':' . $name); |
|
| 1255 | + } |
|
| 1256 | + |
|
| 1257 | + /** |
|
| 1258 | + * Creates a new function |
|
| 1259 | + * |
|
| 1260 | + * Attention: Column names inside the call have to be quoted before hand |
|
| 1261 | + * |
|
| 1262 | + * Example: |
|
| 1263 | + * <code> |
|
| 1264 | + * $qb = $conn->getQueryBuilder(); |
|
| 1265 | + * $qb->select($qb->createFunction('COUNT(*)')) |
|
| 1266 | + * ->from('users', 'u') |
|
| 1267 | + * echo $qb->getSQL(); // SELECT COUNT(*) FROM `users` u |
|
| 1268 | + * </code> |
|
| 1269 | + * <code> |
|
| 1270 | + * $qb = $conn->getQueryBuilder(); |
|
| 1271 | + * $qb->select($qb->createFunction('COUNT(`column`)')) |
|
| 1272 | + * ->from('users', 'u') |
|
| 1273 | + * echo $qb->getSQL(); // SELECT COUNT(`column`) FROM `users` u |
|
| 1274 | + * </code> |
|
| 1275 | + * |
|
| 1276 | + * @param string $call |
|
| 1277 | + * |
|
| 1278 | + * @return IQueryFunction |
|
| 1279 | + */ |
|
| 1280 | + public function createFunction($call) { |
|
| 1281 | + return new QueryFunction($call); |
|
| 1282 | + } |
|
| 1283 | + |
|
| 1284 | + /** |
|
| 1285 | + * Used to get the id of the last inserted element |
|
| 1286 | + * @return int |
|
| 1287 | + * @throws \BadMethodCallException When being called before an insert query has been run. |
|
| 1288 | + */ |
|
| 1289 | + public function getLastInsertId(): int { |
|
| 1290 | + if ($this->getType() === \Doctrine\DBAL\Query\QueryBuilder::INSERT && $this->lastInsertedTable) { |
|
| 1291 | + // lastInsertId() needs the prefix but no quotes |
|
| 1292 | + $table = $this->prefixTableName($this->lastInsertedTable); |
|
| 1293 | + return $this->connection->lastInsertId($table); |
|
| 1294 | + } |
|
| 1295 | + |
|
| 1296 | + throw new \BadMethodCallException('Invalid call to getLastInsertId without using insert() before.'); |
|
| 1297 | + } |
|
| 1298 | + |
|
| 1299 | + /** |
|
| 1300 | + * Returns the table name quoted and with database prefix as needed by the implementation |
|
| 1301 | + * |
|
| 1302 | + * @param string $table |
|
| 1303 | + * @return string |
|
| 1304 | + */ |
|
| 1305 | + public function getTableName($table) { |
|
| 1306 | + if ($table instanceof IQueryFunction) { |
|
| 1307 | + return (string) $table; |
|
| 1308 | + } |
|
| 1309 | + |
|
| 1310 | + $table = $this->prefixTableName($table); |
|
| 1311 | + return $this->helper->quoteColumnName($table); |
|
| 1312 | + } |
|
| 1313 | + |
|
| 1314 | + /** |
|
| 1315 | + * Returns the table name with database prefix as needed by the implementation |
|
| 1316 | + * |
|
| 1317 | + * @param string $table |
|
| 1318 | + * @return string |
|
| 1319 | + */ |
|
| 1320 | + protected function prefixTableName($table) { |
|
| 1321 | + if ($this->automaticTablePrefix === false || strpos($table, '*PREFIX*') === 0) { |
|
| 1322 | + return $table; |
|
| 1323 | + } |
|
| 1324 | + |
|
| 1325 | + return '*PREFIX*' . $table; |
|
| 1326 | + } |
|
| 1327 | + |
|
| 1328 | + /** |
|
| 1329 | + * Returns the column name quoted and with table alias prefix as needed by the implementation |
|
| 1330 | + * |
|
| 1331 | + * @param string $column |
|
| 1332 | + * @param string $tableAlias |
|
| 1333 | + * @return string |
|
| 1334 | + */ |
|
| 1335 | + public function getColumnName($column, $tableAlias = '') { |
|
| 1336 | + if ($tableAlias !== '') { |
|
| 1337 | + $tableAlias .= '.'; |
|
| 1338 | + } |
|
| 1339 | + |
|
| 1340 | + return $this->helper->quoteColumnName($tableAlias . $column); |
|
| 1341 | + } |
|
| 1342 | + |
|
| 1343 | + /** |
|
| 1344 | + * Returns the column name quoted and with table alias prefix as needed by the implementation |
|
| 1345 | + * |
|
| 1346 | + * @param string $alias |
|
| 1347 | + * @return string |
|
| 1348 | + */ |
|
| 1349 | + public function quoteAlias($alias) { |
|
| 1350 | + if ($alias === '' || $alias === null) { |
|
| 1351 | + return $alias; |
|
| 1352 | + } |
|
| 1353 | + |
|
| 1354 | + return $this->helper->quoteColumnName($alias); |
|
| 1355 | + } |
|
| 1356 | 1356 | } |