CustomPropertiesBackend.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. * @copyright Copyright (c) 2017, Georg Ehrke <oc.list@georgehrke.com>
  5. *
  6. * @author Georg Ehrke <oc.list@georgehrke.com>
  7. * @author Robin Appelman <robin@icewind.nl>
  8. * @author Thomas Müller <thomas.mueller@tmit.eu>
  9. * @author Richard Steinmetz <richard@steinmetz.cloud>
  10. *
  11. * @license AGPL-3.0
  12. *
  13. * This code is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU Affero General Public License, version 3,
  15. * as published by the Free Software Foundation.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU Affero General Public License for more details.
  21. *
  22. * You should have received a copy of the GNU Affero General Public License, version 3,
  23. * along with this program. If not, see <http://www.gnu.org/licenses/>
  24. *
  25. */
  26. namespace OCA\DAV\DAV;
  27. use Exception;
  28. use OCA\DAV\CalDAV\Calendar;
  29. use OCA\DAV\CalDAV\DefaultCalendarValidator;
  30. use OCA\DAV\Connector\Sabre\Directory;
  31. use OCA\DAV\Connector\Sabre\FilesPlugin;
  32. use OCP\DB\QueryBuilder\IQueryBuilder;
  33. use OCP\IDBConnection;
  34. use OCP\IUser;
  35. use Sabre\DAV\Exception as DavException;
  36. use Sabre\DAV\PropertyStorage\Backend\BackendInterface;
  37. use Sabre\DAV\PropFind;
  38. use Sabre\DAV\PropPatch;
  39. use Sabre\DAV\Server;
  40. use Sabre\DAV\Tree;
  41. use Sabre\DAV\Xml\Property\Complex;
  42. use Sabre\DAV\Xml\Property\Href;
  43. use Sabre\DAV\Xml\Property\LocalHref;
  44. use Sabre\Xml\ParseException;
  45. use Sabre\Xml\Service as XmlService;
  46. use function array_intersect;
  47. class CustomPropertiesBackend implements BackendInterface {
  48. /** @var string */
  49. private const TABLE_NAME = 'properties';
  50. /**
  51. * Value is stored as string.
  52. */
  53. public const PROPERTY_TYPE_STRING = 1;
  54. /**
  55. * Value is stored as XML fragment.
  56. */
  57. public const PROPERTY_TYPE_XML = 2;
  58. /**
  59. * Value is stored as a property object.
  60. */
  61. public const PROPERTY_TYPE_OBJECT = 3;
  62. /**
  63. * Value is stored as a {DAV:}href string.
  64. */
  65. public const PROPERTY_TYPE_HREF = 4;
  66. /**
  67. * Ignored properties
  68. *
  69. * @var string[]
  70. */
  71. private const IGNORED_PROPERTIES = [
  72. '{DAV:}getcontentlength',
  73. '{DAV:}getcontenttype',
  74. '{DAV:}getetag',
  75. '{DAV:}quota-used-bytes',
  76. '{DAV:}quota-available-bytes',
  77. '{http://owncloud.org/ns}permissions',
  78. '{http://owncloud.org/ns}downloadURL',
  79. '{http://owncloud.org/ns}dDC',
  80. '{http://owncloud.org/ns}size',
  81. '{http://nextcloud.org/ns}is-encrypted',
  82. // Currently, returning null from any propfind handler would still trigger the backend,
  83. // so we add all known Nextcloud custom properties in here to avoid that
  84. // text app
  85. '{http://nextcloud.org/ns}rich-workspace',
  86. '{http://nextcloud.org/ns}rich-workspace-file',
  87. // groupfolders
  88. '{http://nextcloud.org/ns}acl-enabled',
  89. '{http://nextcloud.org/ns}acl-can-manage',
  90. '{http://nextcloud.org/ns}acl-list',
  91. '{http://nextcloud.org/ns}inherited-acl-list',
  92. '{http://nextcloud.org/ns}group-folder-id',
  93. // files_lock
  94. '{http://nextcloud.org/ns}lock',
  95. '{http://nextcloud.org/ns}lock-owner-type',
  96. '{http://nextcloud.org/ns}lock-owner',
  97. '{http://nextcloud.org/ns}lock-owner-displayname',
  98. '{http://nextcloud.org/ns}lock-owner-editor',
  99. '{http://nextcloud.org/ns}lock-time',
  100. '{http://nextcloud.org/ns}lock-timeout',
  101. '{http://nextcloud.org/ns}lock-token',
  102. ];
  103. /**
  104. * Properties set by one user, readable by all others
  105. *
  106. * @var string[]
  107. */
  108. private const PUBLISHED_READ_ONLY_PROPERTIES = [
  109. '{urn:ietf:params:xml:ns:caldav}calendar-availability',
  110. '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
  111. ];
  112. /**
  113. * Map of custom XML elements to parse when trying to deserialize an instance of
  114. * \Sabre\DAV\Xml\Property\Complex to find a more specialized PROPERTY_TYPE_*
  115. */
  116. private const COMPLEX_XML_ELEMENT_MAP = [
  117. '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => Href::class,
  118. ];
  119. /**
  120. * @var Tree
  121. */
  122. private $tree;
  123. /**
  124. * @var IDBConnection
  125. */
  126. private $connection;
  127. /**
  128. * @var IUser
  129. */
  130. private $user;
  131. /**
  132. * Properties cache
  133. *
  134. * @var array
  135. */
  136. private $userCache = [];
  137. private Server $server;
  138. private XmlService $xmlService;
  139. private DefaultCalendarValidator $defaultCalendarValidator;
  140. /**
  141. * @param Tree $tree node tree
  142. * @param IDBConnection $connection database connection
  143. * @param IUser $user owner of the tree and properties
  144. */
  145. public function __construct(
  146. Server $server,
  147. Tree $tree,
  148. IDBConnection $connection,
  149. IUser $user,
  150. DefaultCalendarValidator $defaultCalendarValidator,
  151. ) {
  152. $this->server = $server;
  153. $this->tree = $tree;
  154. $this->connection = $connection;
  155. $this->user = $user;
  156. $this->xmlService = new XmlService();
  157. $this->xmlService->elementMap = array_merge(
  158. $this->xmlService->elementMap,
  159. self::COMPLEX_XML_ELEMENT_MAP,
  160. );
  161. $this->defaultCalendarValidator = $defaultCalendarValidator;
  162. }
  163. /**
  164. * Fetches properties for a path.
  165. *
  166. * @param string $path
  167. * @param PropFind $propFind
  168. * @return void
  169. */
  170. public function propFind($path, PropFind $propFind) {
  171. $requestedProps = $propFind->get404Properties();
  172. // these might appear
  173. $requestedProps = array_diff(
  174. $requestedProps,
  175. self::IGNORED_PROPERTIES,
  176. );
  177. $requestedProps = array_filter(
  178. $requestedProps,
  179. fn ($prop) => !str_starts_with($prop, FilesPlugin::FILE_METADATA_PREFIX),
  180. );
  181. // substr of calendars/ => path is inside the CalDAV component
  182. // two '/' => this a calendar (no calendar-home nor calendar object)
  183. if (str_starts_with($path, 'calendars/') && substr_count($path, '/') === 2) {
  184. $allRequestedProps = $propFind->getRequestedProperties();
  185. $customPropertiesForShares = [
  186. '{DAV:}displayname',
  187. '{urn:ietf:params:xml:ns:caldav}calendar-description',
  188. '{urn:ietf:params:xml:ns:caldav}calendar-timezone',
  189. '{http://apple.com/ns/ical/}calendar-order',
  190. '{http://apple.com/ns/ical/}calendar-color',
  191. '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp',
  192. ];
  193. foreach ($customPropertiesForShares as $customPropertyForShares) {
  194. if (in_array($customPropertyForShares, $allRequestedProps)) {
  195. $requestedProps[] = $customPropertyForShares;
  196. }
  197. }
  198. }
  199. // substr of addressbooks/ => path is inside the CardDAV component
  200. // three '/' => this a addressbook (no addressbook-home nor contact object)
  201. if (str_starts_with($path, 'addressbooks/') && substr_count($path, '/') === 3) {
  202. $allRequestedProps = $propFind->getRequestedProperties();
  203. $customPropertiesForShares = [
  204. '{DAV:}displayname',
  205. ];
  206. foreach ($customPropertiesForShares as $customPropertyForShares) {
  207. if (in_array($customPropertyForShares, $allRequestedProps, true)) {
  208. $requestedProps[] = $customPropertyForShares;
  209. }
  210. }
  211. }
  212. // substr of principals/users/ => path is a user principal
  213. // two '/' => this a principal collection (and not some child object)
  214. if (str_starts_with($path, 'principals/users/') && substr_count($path, '/') === 2) {
  215. $allRequestedProps = $propFind->getRequestedProperties();
  216. $customProperties = [
  217. '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
  218. ];
  219. foreach ($customProperties as $customProperty) {
  220. if (in_array($customProperty, $allRequestedProps, true)) {
  221. $requestedProps[] = $customProperty;
  222. }
  223. }
  224. }
  225. if (empty($requestedProps)) {
  226. return;
  227. }
  228. $node = $this->tree->getNodeForPath($path);
  229. if ($node instanceof Directory && $propFind->getDepth() !== 0) {
  230. $this->cacheDirectory($path, $node);
  231. }
  232. // First fetch the published properties (set by another user), then get the ones set by
  233. // the current user. If both are set then the latter as priority.
  234. foreach ($this->getPublishedProperties($path, $requestedProps) as $propName => $propValue) {
  235. try {
  236. $this->validateProperty($path, $propName, $propValue);
  237. } catch (DavException $e) {
  238. continue;
  239. }
  240. $propFind->set($propName, $propValue);
  241. }
  242. foreach ($this->getUserProperties($path, $requestedProps) as $propName => $propValue) {
  243. try {
  244. $this->validateProperty($path, $propName, $propValue);
  245. } catch (DavException $e) {
  246. continue;
  247. }
  248. $propFind->set($propName, $propValue);
  249. }
  250. }
  251. /**
  252. * Updates properties for a path
  253. *
  254. * @param string $path
  255. * @param PropPatch $propPatch
  256. *
  257. * @return void
  258. */
  259. public function propPatch($path, PropPatch $propPatch) {
  260. $propPatch->handleRemaining(function ($changedProps) use ($path) {
  261. return $this->updateProperties($path, $changedProps);
  262. });
  263. }
  264. /**
  265. * This method is called after a node is deleted.
  266. *
  267. * @param string $path path of node for which to delete properties
  268. */
  269. public function delete($path) {
  270. $statement = $this->connection->prepare(
  271. 'DELETE FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?'
  272. );
  273. $statement->execute([$this->user->getUID(), $this->formatPath($path)]);
  274. $statement->closeCursor();
  275. unset($this->userCache[$path]);
  276. }
  277. /**
  278. * This method is called after a successful MOVE
  279. *
  280. * @param string $source
  281. * @param string $destination
  282. *
  283. * @return void
  284. */
  285. public function move($source, $destination) {
  286. $statement = $this->connection->prepare(
  287. 'UPDATE `*PREFIX*properties` SET `propertypath` = ?' .
  288. ' WHERE `userid` = ? AND `propertypath` = ?'
  289. );
  290. $statement->execute([$this->formatPath($destination), $this->user->getUID(), $this->formatPath($source)]);
  291. $statement->closeCursor();
  292. }
  293. /**
  294. * Validate the value of a property. Will throw if a value is invalid.
  295. *
  296. * @throws DavException The value of the property is invalid
  297. */
  298. private function validateProperty(string $path, string $propName, mixed $propValue): void {
  299. switch ($propName) {
  300. case '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL':
  301. /** @var Href $propValue */
  302. $href = $propValue->getHref();
  303. if ($href === null) {
  304. throw new DavException('Href is empty');
  305. }
  306. // $path is the principal here as this prop is only set on principals
  307. $node = $this->tree->getNodeForPath($href);
  308. if (!($node instanceof Calendar) || $node->getOwner() !== $path) {
  309. throw new DavException('No such calendar');
  310. }
  311. $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node);
  312. break;
  313. }
  314. }
  315. /**
  316. * @param string $path
  317. * @param string[] $requestedProperties
  318. *
  319. * @return array
  320. */
  321. private function getPublishedProperties(string $path, array $requestedProperties): array {
  322. $allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties);
  323. if (empty($allowedProps)) {
  324. return [];
  325. }
  326. $qb = $this->connection->getQueryBuilder();
  327. $qb->select('*')
  328. ->from(self::TABLE_NAME)
  329. ->where($qb->expr()->eq('propertypath', $qb->createNamedParameter($path)));
  330. $result = $qb->executeQuery();
  331. $props = [];
  332. while ($row = $result->fetch()) {
  333. $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
  334. }
  335. $result->closeCursor();
  336. return $props;
  337. }
  338. /**
  339. * prefetch all user properties in a directory
  340. */
  341. private function cacheDirectory(string $path, Directory $node): void {
  342. $prefix = ltrim($path . '/', '/');
  343. $query = $this->connection->getQueryBuilder();
  344. $query->select('name', 'propertypath', 'propertyname', 'propertyvalue', 'valuetype')
  345. ->from('filecache', 'f')
  346. ->leftJoin('f', 'properties', 'p', $query->expr()->andX(
  347. $query->expr()->eq('propertypath', $query->func()->concat(
  348. $query->createNamedParameter($prefix),
  349. 'name'
  350. )),
  351. $query->expr()->eq('userid', $query->createNamedParameter($this->user->getUID()))
  352. ))
  353. ->where($query->expr()->eq('parent', $query->createNamedParameter($node->getInternalFileId(), IQueryBuilder::PARAM_INT)));
  354. $result = $query->executeQuery();
  355. $propsByPath = [];
  356. while ($row = $result->fetch()) {
  357. $childPath = $prefix . $row['name'];
  358. if (!isset($propsByPath[$childPath])) {
  359. $propsByPath[$childPath] = [];
  360. }
  361. if (isset($row['propertyname'])) {
  362. $propsByPath[$childPath][$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
  363. }
  364. }
  365. $this->userCache = array_merge($this->userCache, $propsByPath);
  366. }
  367. /**
  368. * Returns a list of properties for the given path and current user
  369. *
  370. * @param string $path
  371. * @param array $requestedProperties requested properties or empty array for "all"
  372. * @return array
  373. * @note The properties list is a list of propertynames the client
  374. * requested, encoded as xmlnamespace#tagName, for example:
  375. * http://www.example.org/namespace#author If the array is empty, all
  376. * properties should be returned
  377. */
  378. private function getUserProperties(string $path, array $requestedProperties) {
  379. if (isset($this->userCache[$path])) {
  380. return $this->userCache[$path];
  381. }
  382. // TODO: chunking if more than 1000 properties
  383. $sql = 'SELECT * FROM `*PREFIX*properties` WHERE `userid` = ? AND `propertypath` = ?';
  384. $whereValues = [$this->user->getUID(), $this->formatPath($path)];
  385. $whereTypes = [null, null];
  386. if (!empty($requestedProperties)) {
  387. // request only a subset
  388. $sql .= ' AND `propertyname` in (?)';
  389. $whereValues[] = $requestedProperties;
  390. $whereTypes[] = \Doctrine\DBAL\Connection::PARAM_STR_ARRAY;
  391. }
  392. $result = $this->connection->executeQuery(
  393. $sql,
  394. $whereValues,
  395. $whereTypes
  396. );
  397. $props = [];
  398. while ($row = $result->fetch()) {
  399. $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']);
  400. }
  401. $result->closeCursor();
  402. $this->userCache[$path] = $props;
  403. return $props;
  404. }
  405. /**
  406. * @throws Exception
  407. */
  408. private function updateProperties(string $path, array $properties): bool {
  409. // TODO: use "insert or update" strategy ?
  410. $existing = $this->getUserProperties($path, []);
  411. try {
  412. $this->connection->beginTransaction();
  413. foreach ($properties as $propertyName => $propertyValue) {
  414. // common parameters for all queries
  415. $dbParameters = [
  416. 'userid' => $this->user->getUID(),
  417. 'propertyPath' => $this->formatPath($path),
  418. 'propertyName' => $propertyName,
  419. ];
  420. // If it was null, we need to delete the property
  421. if (is_null($propertyValue)) {
  422. if (array_key_exists($propertyName, $existing)) {
  423. $deleteQuery = $deleteQuery ?? $this->createDeleteQuery();
  424. $deleteQuery
  425. ->setParameters($dbParameters)
  426. ->executeStatement();
  427. }
  428. } else {
  429. [$value, $valueType] = $this->encodeValueForDatabase(
  430. $path,
  431. $propertyName,
  432. $propertyValue,
  433. );
  434. $dbParameters['propertyValue'] = $value;
  435. $dbParameters['valueType'] = $valueType;
  436. if (!array_key_exists($propertyName, $existing)) {
  437. $insertQuery = $insertQuery ?? $this->createInsertQuery();
  438. $insertQuery
  439. ->setParameters($dbParameters)
  440. ->executeStatement();
  441. } else {
  442. $updateQuery = $updateQuery ?? $this->createUpdateQuery();
  443. $updateQuery
  444. ->setParameters($dbParameters)
  445. ->executeStatement();
  446. }
  447. }
  448. }
  449. $this->connection->commit();
  450. unset($this->userCache[$path]);
  451. } catch (Exception $e) {
  452. $this->connection->rollBack();
  453. throw $e;
  454. }
  455. return true;
  456. }
  457. /**
  458. * long paths are hashed to ensure they fit in the database
  459. *
  460. * @param string $path
  461. * @return string
  462. */
  463. private function formatPath(string $path): string {
  464. if (strlen($path) > 250) {
  465. return sha1($path);
  466. }
  467. return $path;
  468. }
  469. /**
  470. * @throws ParseException If parsing a \Sabre\DAV\Xml\Property\Complex value fails
  471. * @throws DavException If the property value is invalid
  472. */
  473. private function encodeValueForDatabase(string $path, string $name, mixed $value): array {
  474. // Try to parse a more specialized property type first
  475. if ($value instanceof Complex) {
  476. $xml = $this->xmlService->write($name, [$value], $this->server->getBaseUri());
  477. $value = $this->xmlService->parse($xml, $this->server->getBaseUri()) ?? $value;
  478. }
  479. if ($name === '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL') {
  480. $value = $this->encodeDefaultCalendarUrl($value);
  481. }
  482. try {
  483. $this->validateProperty($path, $name, $value);
  484. } catch (DavException $e) {
  485. throw new DavException(
  486. "Property \"$name\" has an invalid value: " . $e->getMessage(),
  487. 0,
  488. $e,
  489. );
  490. }
  491. if (is_scalar($value)) {
  492. $valueType = self::PROPERTY_TYPE_STRING;
  493. } elseif ($value instanceof Complex) {
  494. $valueType = self::PROPERTY_TYPE_XML;
  495. $value = $value->getXml();
  496. } elseif ($value instanceof Href) {
  497. $valueType = self::PROPERTY_TYPE_HREF;
  498. $value = $value->getHref();
  499. } else {
  500. $valueType = self::PROPERTY_TYPE_OBJECT;
  501. $value = serialize($value);
  502. }
  503. return [$value, $valueType];
  504. }
  505. /**
  506. * @return mixed|Complex|string
  507. */
  508. private function decodeValueFromDatabase(string $value, int $valueType) {
  509. switch ($valueType) {
  510. case self::PROPERTY_TYPE_XML:
  511. return new Complex($value);
  512. case self::PROPERTY_TYPE_HREF:
  513. return new Href($value);
  514. case self::PROPERTY_TYPE_OBJECT:
  515. return unserialize($value);
  516. case self::PROPERTY_TYPE_STRING:
  517. default:
  518. return $value;
  519. }
  520. }
  521. private function encodeDefaultCalendarUrl(Href $value): Href {
  522. $href = $value->getHref();
  523. if ($href === null) {
  524. return $value;
  525. }
  526. if (!str_starts_with($href, '/')) {
  527. return $value;
  528. }
  529. try {
  530. // Build path relative to the dav base URI to be used later to find the node
  531. $value = new LocalHref($this->server->calculateUri($href) . '/');
  532. } catch (DavException\Forbidden) {
  533. // Not existing calendars will be handled later when the value is validated
  534. }
  535. return $value;
  536. }
  537. private function createDeleteQuery(): IQueryBuilder {
  538. $deleteQuery = $this->connection->getQueryBuilder();
  539. $deleteQuery->delete('properties')
  540. ->where($deleteQuery->expr()->eq('userid', $deleteQuery->createParameter('userid')))
  541. ->andWhere($deleteQuery->expr()->eq('propertypath', $deleteQuery->createParameter('propertyPath')))
  542. ->andWhere($deleteQuery->expr()->eq('propertyname', $deleteQuery->createParameter('propertyName')));
  543. return $deleteQuery;
  544. }
  545. private function createInsertQuery(): IQueryBuilder {
  546. $insertQuery = $this->connection->getQueryBuilder();
  547. $insertQuery->insert('properties')
  548. ->values([
  549. 'userid' => $insertQuery->createParameter('userid'),
  550. 'propertypath' => $insertQuery->createParameter('propertyPath'),
  551. 'propertyname' => $insertQuery->createParameter('propertyName'),
  552. 'propertyvalue' => $insertQuery->createParameter('propertyValue'),
  553. 'valuetype' => $insertQuery->createParameter('valueType'),
  554. ]);
  555. return $insertQuery;
  556. }
  557. private function createUpdateQuery(): IQueryBuilder {
  558. $updateQuery = $this->connection->getQueryBuilder();
  559. $updateQuery->update('properties')
  560. ->set('propertyvalue', $updateQuery->createParameter('propertyValue'))
  561. ->set('valuetype', $updateQuery->createParameter('valueType'))
  562. ->where($updateQuery->expr()->eq('userid', $updateQuery->createParameter('userid')))
  563. ->andWhere($updateQuery->expr()->eq('propertypath', $updateQuery->createParameter('propertyPath')))
  564. ->andWhere($updateQuery->expr()->eq('propertyname', $updateQuery->createParameter('propertyName')));
  565. return $updateQuery;
  566. }
  567. }