depage-xmldb v2.3.1
XML documents in a relational database
Loading...
Searching...
No Matches
Document.php
Go to the documentation of this file.
1<?php
2
12
13namespace Depage\XmlDb;
14
16{
26
34
35 protected $pdo;
36 protected $cache;
37 protected $dbNs;
38
39 protected $tablePrefix;
40 protected $tableDocs;
41 protected $tableXml;
42
43 protected $xmlDb;
44 protected $docId;
45
46 protected $idAttribute = 'id';
47 protected $idDataAttribute = 'dataid';
48
49 protected $freeElementIds = [];
50 protected $doctypeHandlers = [];
51
52 protected $entities = '';
53 protected $namespaceString = '';
54 protected $namespaces = [];
55 protected $lastchange = '';
56 protected $lastchangeUid = '';
57
64 protected $docInfo = null;
65 protected $docInfoLoaded = false;
66
72 protected $positions = null;
73 protected $subhashUpdater = null;
74 protected $patchPlanner = null;
75 protected $patchApplier = null;
76 protected $writer = null;
77
83 protected $subhashesDirty = false;
84
89 protected $lastPatchStats = null;
96 public function __construct($xmlDb, $docId)
97 {
98 $this->pdo = $xmlDb->pdo;
99 $this->cache = $xmlDb->cache;
100
101 $this->dbNs = new XmlNs('db', 'http://cms.depagecms.net/ns/database');
102
103 $this->xmlDb = $xmlDb;
104
105 $this->tablePrefix = $xmlDb->tablePrefix;
106 $this->tableDocs = $xmlDb->tableDocs;
107 $this->tableXml = $xmlDb->tableXml;
108
109 $this->docId = $docId;
110 }
111
117 public function getDocId()
118 {
119 return $this->docId;
120 }
121
126 public function getXml($addIdAttribute = true)
127 {
128 $rootId = $this->getDocInfo()->rootid;
129
130 if (is_null($rootId)) {
131 throw new Exceptions\XmlDbException('Trying to get contents of empty document.');
132 } else {
133 $xml = $this->getSubdocByNodeId($rootId, $addIdAttribute);
134 }
135
136 return $xml;
137 }
138
148 public function getDocInfo()
149 {
150 if (!$this->docInfoLoaded) {
151 $query = $this->pdo->prepare(
152 "SELECT
153 docs.id AS id,
154 docs.name AS name,
155 docs.rootid AS rootid,
156 docs.type AS type,
157 docs.ns AS namespaces,
158 docs.lastchange AS lastchange,
159 docs.lastchange_uid AS lastchangeUid
160 FROM {$this->tableDocs} AS docs
161 WHERE docs.id = :doc_id
162 LIMIT 1",
163 );
164 $query->execute([
165 'doc_id' => $this->docId,
166 ]);
167
168 $info = $query->fetchObject();
169
170 if ($info) {
171 $info->lastchange = new \DateTime($info->lastchange);
172 $info->lastrelease = $this->getHistory()->getLatestVersion()->lastsaved ?? false;
173 }
174
175 $this->docInfo = $info;
176 $this->docInfoLoaded = true;
177 }
178
179 return $this->docInfo;
180 }
181
186 public function getDoctypeHandler()
187 {
188 if (!isset($this->doctypeHandlers[$this->docId])) {
189 $className = $this->getDocInfo()->type;
190
191 if (empty($className)) {
192 $handler = new XmlDoctypes\Base($this->xmlDb, $this);
193 } else {
194 // don't instantiate handlers straight from the database
195 // before validating them
196 $className = '\\' . ltrim($className, '\\');
197
198 if (!class_exists($className)) {
199 throw new Exceptions\XmlDbException("Doctype handler class \"$className\" does not exist");
200 }
201 if (!is_a($className, Interfaces\DoctypeInterface::class, true)) {
202 throw new Exceptions\XmlDbException('Doctype handler must implement DoctypeInterface');
203 }
204
205 $handler = new $className($this->xmlDb, $this);
206 }
207
208 if ($handler instanceof Interfaces\DoctypeInterface) {
209 $this->doctypeHandlers[$this->docId] = $handler;
210 } else {
211 throw new Exceptions\XmlDbException('Doctype handler must implement DoctypeInterface');
212 }
213 }
214
215 return $this->doctypeHandlers[$this->docId];
216 }
217
220 public function getPermissions()
221 {
222 return $this->getDoctypeHandler()->getPermissions();
223 }
224
230 public function getNamespacesAndEntities()
231 {
232 $query = $this->pdo->prepare(
233 "SELECT docs.entities AS entities, docs.ns AS namespaces
234 FROM {$this->tableDocs} AS docs
235 WHERE docs.id = :doc_id",
236 );
237 $query->execute([
238 'doc_id' => $this->docId,
239 ]);
240
241 return $query->fetchObject();
242 }
243
248 public function getHistory()
249 {
250 return new DocumentHistory($this->pdo, $this->tablePrefix, $this);
251 }
252
260 protected function positions()
261 {
262 if (is_null($this->positions)) {
263 $this->positions = new Helpers\Positions($this->pdo, $this->tableXml, $this->docId, self::SAVE_BATCH_SIZE);
264 }
265
266 return $this->positions;
267 }
268
279 protected function writer()
280 {
281 if (is_null($this->writer)) {
282 $this->writer = new Helpers\DocumentWriter(
283 pdo: $this->pdo,
284 table: $this->tableXml,
285 docId: $this->docId,
286 dbNs: $this->dbNs,
287 idAttribute: $this->idAttribute,
288 positions: $this->positions(),
289 host: $this
290 );
291 }
292
293 return $this->writer;
294 }
295
301 public function isReleased()
302 {
303 $info = $this->getDocInfo();
304
305 if (!empty($info->lastrelease) && $info->lastchange->getTimestamp() <= $info->lastrelease->getTimestamp()) {
306 return true;
307 }
308
309 return false;
310 }
311
317 public function isPublished()
318 {
319 $info = $this->getDocInfo();
320
321 return !empty($info->lastrelease);
322 }
323
331 public function getNodeId($node)
332 {
333 return $node->nodeType == XML_ELEMENT_NODE
334 ? $node->getAttributeNS($this->dbNs->uri, $this->idAttribute)
335 : null;
336 }
337
346 public function getNodeDataId($node)
347 {
348 return $node->nodeType == XML_ELEMENT_NODE
349 ? $node->getAttributeNS($this->dbNs->uri, $this->idDataAttribute)
350 : null;
351 }
352
353 protected function getNodeAttributeById($id, $attribute)
354 {
355 $query = $this->pdo->prepare(
356 "SELECT xml.$attribute AS $attribute
357 FROM {$this->tableXml} AS xml
358 WHERE xml.id = :id AND xml.id_doc = :doc_id",
359 );
360 $query->execute([
361 'id' => $id,
362 'doc_id' => $this->docId,
363 ]);
364
365 return ($result = $query->fetchObject()) ? $result->$attribute : false;
366 }
367
374 public function getNodeNameById($id)
375 {
376 return $this->getNodeAttributeById($id, 'name');
377 }
378
385 public function getParentIdById($id)
386 {
387 return $this->getNodeAttributeById($id, 'id_parent');
388 }
389
404 public function getIndexById($id)
405 {
406 return $this->positions()->indexById($id);
407 }
408
415 protected function rebalancePositions($parentId, $order = null)
416 {
417 $this->positions()->rebalance($parentId, $order);
418 }
419
432 public function getNodeIdsByXpath($xpath)
433 {
434 $identifier = Helpers\CacheKey::forDocument($this->tableDocs, $this->docId) . 'xpath_' . sha1($xpath);
435 $fetchedIds = $this->cache->get($identifier);
436
437 if ($fetchedIds === false) {
438 $fetchedIds = $this->xmlDb->getNodeIdsByXpath($xpath, $this->docId);
439
440 $this->cache->set($identifier, $fetchedIds);
441 }
442 return $fetchedIds;
443 }
444
451 public function getSubdocByNodeId($id, $addIdAttribute = true)
452 {
453 $identifier = Helpers\CacheKey::forDocument($this->tableDocs, $this->docId) . "{$id}.xml";
454 $xmlDoc = new \Depage\Xml\Document();
455
456 if ($xmlStr = $this->cache->get($identifier)) {
457 // read from cache
458 $xmlDoc->loadXML($xmlStr);
459 } else {
460 // read from database
461 $this->inTransaction(function () use ($id, &$xmlDoc) {
462 $query = null;
463
464 try {
465 $query = $this->pdo->prepare(
466 "SELECT
467 docs.entities AS entities,
468 docs.ns AS namespaces,
469 docs.lastchange AS lastchange,
470 docs.lastchange_uid AS lastchangeUid
471 FROM {$this->tableDocs} AS docs
472 WHERE docs.id = :doc_id",
473 );
474 $query->execute([
475 'doc_id' => $this->docId,
476 ]);
477 $result = $query->fetchObject();
478
479 $this->entities = $result->entities;
480 $this->namespaceString = $result->namespaces;
481 $this->namespaces = $this->extractNamespaces($this->namespaceString);
482 $this->namespaces[$this->dbNs->ns] = $this->dbNs;
483 $this->lastchange = $result->lastchange;
484 $this->lastchangeUid = $result->lastchangeUid;
485
486 $query = $this->pdo->prepare(
487 Helpers\TreeSql::descendants($this->tableXml, "id = :id AND id_doc = :doc_id", self::SORTKEY_PAD) . "
488 SELECT lvl, xml.* FROM tree JOIN {$this->tableXml} AS xml ON tree.id = xml.id ORDER BY sortkey;",
489 \Depage\Db\Pdo::unbuffered(),
490 );
491 $query->execute([
492 'doc_id' => $this->docId,
493 'id' => $id,
494 ]);
495 $row = $query->fetchObject();
496
497 //get ROOT-NODE
498 if (!($row && $row->type == 'ELEMENT_NODE')) {
499 throw new Exceptions\XmlDbException('This node is no ELEMENT_NODE or node does not exist' . " {$this->docId}/{$id}");
500 }
501
502 $this->addChildnodesByQuery($query, $row, $xmlDoc);
503 } catch (\Throwable $e) {
504 // the tree query may be an unbuffered result, it has to be
505 // closed before any other statement can run on the connection
506 if (isset($query)) {
507 $query->closeCursor();
508 }
509
510 throw $e;
511 }
512 }, false);
513
514 $dth = $this->getDoctypeHandler();
515 if ($dth->testDocument($xmlDoc)) { // test whether the document was altered
516 $this->saveNode($xmlDoc);
517 }
518
519 // add xml to xml-cache
520 if (is_object($xmlDoc) && $xmlDoc->documentElement != null) {
521 $this->cache->set($identifier, $xmlDoc->saveXML());
522 }
523 }
524
525 if (is_a($xmlDoc, 'DOMDocument') && $xmlDoc->documentElement != null && !$addIdAttribute) {
526 $this->removeIdAttr($xmlDoc);
527 }
528
529 return $xmlDoc;
530 }
531
540 public function getSubdocByXpath($xpath, $addIdAttribute = true)
541 {
542 $subdoc = false;
543 $ids = $this->getNodeIdsByXpath($xpath);
544
545 if (isset($ids[0])) {
546 $subdoc = $this->getSubdocByNodeId($ids[0], $addIdAttribute);
547 }
548
549 return $subdoc;
550 }
551
560 public function getAttribute($nodeId, $attrName)
561 {
562 $attributes = $this->getAttributes($nodeId);
563
564 return (isset($attributes[$attrName])) ? $attributes[$attrName] : false;
565 }
566
573 public function getAttributes($nodeId)
574 {
575 $attributes = [];
576
577 $query = $this->pdo->prepare(
578 "SELECT xml.value
579 FROM {$this->tableXml} AS xml
580 WHERE xml.id = :node_id AND xml.type='ELEMENT_NODE' AND xml.id_doc = :doc_id",
581 );
582 $query->execute([
583 'node_id' => $nodeId,
584 'doc_id' => $this->docId,
585 ]);
586
587 if ($result = $query->fetchObject()) {
588 $attributes = self::parseAttributeString($result->value);
589 }
590
591 return $attributes;
592 }
593
597 public static function parseAttributeString($value)
598 {
599 $attributes = [];
600
601 $matches = preg_split('/(="|"$|" )/', (string) $value);
602 $matches = array_chunk($matches, 2);
603 foreach ($matches as $match) {
604 $name = trim($match[0]);
605 if ($name != '') {
606 $attributes[$name] = htmlspecialchars_decode($match[1] ?? '');
607 }
608 }
609
610 return $attributes;
611 }
612
619 public function buildNode($name, $attributes)
620 {
621 //@todo dont build node directly but get from templates according to document type
622 $docInfo = $this->getNamespacesAndEntities($this->docId);
623 $xml = "<$name {$docInfo->namespaces}";
624 foreach ($attributes as $attr => $value) {
625 $xml .= " $attr=\"$value\"";
626 }
627 $xml .= '/>';
628
629 $doc = new \Depage\Xml\Document();
630 $doc->loadXML($xml);
631
632 return $doc->documentElement;
633 }
634
639 public function removeIdAttr($node)
640 {
641 self::removeNodeAttr($node, $this->dbNs, $this->idAttribute);
642 }
643
649 public function clearDoc()
650 {
651 $info = $this->getDocInfo();
652
653 $this->inTransaction(function () use ($info) {
654 if ($info) {
655 $this->pdo->exec("SET FOREIGN_KEY_CHECKS=0;");
656
657 try {
658 $query = $this->pdo->prepare(
659 "DELETE FROM {$this->tableXml}
660 WHERE
661 id_doc = :id",
662 );
663 $query->execute([
664 'id' => $info->id,
665 ]);
666 } finally {
667 // keep foreign key checks enabled even if the
668 // delete failed, they are session scoped
669 $this->pdo->exec("SET FOREIGN_KEY_CHECKS=1;");
670 }
671 }
672 });
673
674 return $info;
675 }
676
681 public function save(\DomDocument $xml)
682 {
683 return $this->inTransaction(function () use ($xml) {
684 $docInfo = $this->clearDoc();
685
686 $namespaces = $this->getNamespaceString($xml);
687
688 // @TODO get document and entities or set html_entities as standard as long as php does not inherit the entites() function
689 $docInfo->rootid = $this->saveNodePrivate($xml);
690 $query = $this->pdo->prepare(
691 "UPDATE {$this->tableDocs}
692 SET
693 rootid = :rootid,
694 ns = :ns,
695 entities=''
696 WHERE id = :doc_id",
697 );
698
699 $query->execute([
700 'rootid' => $docInfo->rootid,
701 'ns' => $namespaces,
702 'doc_id' => $docInfo->id,
703 ]);
704
705 return $docInfo->id;
706 });
707 }
708
733 public function savePatch(\DomDocument $xml)
734 {
735 $docInfo = $this->getDocInfo();
736 $plan = null;
737
738 if ($docInfo) {
739 $plan = $this->planPatch($xml, $docInfo);
740 }
741
742 if (is_null($plan)) {
743 $this->lastPatchStats = ['fallback' => true];
744
745 return $this->save($xml);
746 }
747
748 $this->inTransaction(function () use ($plan, $xml) {
749 $query = $this->pdo->prepare(
750 "UPDATE {$this->tableDocs}
751 SET
752 ns = :ns,
753 entities = ''
754 WHERE id = :doc_id",
755 );
756 $query->execute([
757 'ns' => $this->getNamespaceString($xml),
758 'doc_id' => $this->docId,
759 ]);
760
761 $this->applyPatch($plan);
762
763 $this->updateLastChange();
764 });
765
766 $this->lastPatchStats = $plan['stats'] + ['fallback' => false];
767
768 return $docInfo->id;
769 }
770
776 public function getLastPatchStats()
777 {
779 }
780
786 protected function planPatch(\DomDocument $xml, $docInfo)
787 {
788 $root = $xml->documentElement;
789 $rootId = is_null($root) ? '' : $this->getNodeId($root);
790
791 if (empty($rootId) || (int) $rootId !== (int) $docInfo->rootid) {
792 // new document or the root node was replaced
793 return null;
794 }
795
796 return $this->planNodePatch($xml);
797 }
798
810 protected function planNodePatch($node, $checkRoot = false)
811 {
812 // run the subtree through the same pipeline save() uses, this
813 // also applies the doctype whitespace handling
814 $nodeArray = [];
815 $this->getNodeArrayForSaving($nodeArray, $node);
816
817 if (is_null($this->patchPlanner)) {
818 $this->patchPlanner = new Patch\PatchPlanner($this->pdo, $this->tableXml, $this->docId, $this->dbNs, $this->idAttribute);
819 }
820
821 return $this->patchPlanner->plan($nodeArray, $checkRoot);
822 }
823
828 protected function applyPatch($plan)
829 {
830 if (is_null($this->patchApplier)) {
831 $this->patchApplier = new Patch\PatchApplier($this->pdo, $this->tableXml, $this->docId, $this->positions(), $this->writer());
832 }
833
834 $this->patchApplier->apply($plan);
835 }
836
840 protected function getNamespaceString($xml)
841 {
842 $xpath = new \DOMXPath($xml);
843
844 $namespaces = '';
845 foreach ($xpath->query('namespace::*') as $node) {
846 if ($node->nodeName == 'xmlns:xml') {
847 continue;
848 }
849 $namespaces .= " " . $node->nodeName . "=\"" . htmlspecialchars($node->nodeValue) . "\"";
850 }
851
852 return $namespaces;
853 }
854
860 public function deleteNode($nodeId)
861 {
862 $dth = $this->getDoctypeHandler();
863
864 if (!$dth->isAllowedDelete($nodeId)) {
865 return false;
866 }
867
868 return $this->inTransaction(function () use ($nodeId) {
869 $success = $this->writer()->deleteNode($nodeId);
870
871 $this->updateLastChange();
872
873 return $success;
874 });
875 }
876
886 public function saveNode($node)
887 {
888 return $this->inTransaction(
889 fn() => $this->saveNodePrivate($node)
890 );
891 }
892
917 public function saveNodePatch($node)
918 {
919 $plan = $this->planNodePatch($node, true);
920
921 if (is_null($plan)) {
922 $this->lastPatchStats = ['fallback' => true];
923
924 return (int) $this->saveNode($node);
925 }
926
927 $this->inTransaction(function () use ($plan) {
928 $this->applyPatch($plan);
929
930 $this->updateLastChange();
931 });
932
933 $this->lastPatchStats = $plan['stats'] + ['fallback' => false];
934
935 return $plan['root_id'];
936 }
937
949 public function addNode(\DomNode $node, $targetId, $targetIndex = -1, $extras = [])
950 {
951 $dth = $this->getDoctypeHandler();
952
953 if (!$dth->isAllowedAdd($node, $targetId)) {
954 return false;
955 }
956
957 $dth->onAddNode($node, $targetId, $targetIndex, $extras);
958
959 return $this->inTransaction(
960 fn() => $this->saveNodeIn($node, $targetId, $targetIndex, true)
961 );
962 }
963
977 public function addNodeByName($name, $targetId, $targetIndex, $extras = [])
978 {
979 $dth = $this->getDoctypeHandler();
980 $newNodes = $dth->getNewNodeFor($name);
981
982 if (!$newNodes) {
983 return false;
984 }
985
986 // collect the element nodes the handler offers first: a node the
987 // doctype handler does not allow must not leave the ones added
988 // before it in the document
989 $nodes = [];
990 foreach ($newNodes as $node) {
991 if ($node->nodeType != \XML_ELEMENT_NODE) {
992 continue;
993 }
994
995 if (!$dth->isAllowedAdd($node, $targetId)) {
996 return false;
997 }
998
999 $nodes[] = $node;
1000 }
1001
1002 // the position of the first node is resolved once and the others
1003 // are added behind it; adding all of them at the same position
1004 // would add them in the reverse of the order the handler returns
1005 // them in. this is read before the adds open their own
1006 // transactions, the doctype handler hooks of addNode have to run
1007 // outside of a transaction
1008 $targetIndex = $this->positions()->normalizeIndex($targetId, $targetIndex);
1009
1010 $success = false;
1011 foreach ($nodes as $index => $node) {
1012 $added = $this->addNode($node, $targetId, $targetIndex + $index, $extras);
1013
1014 if ($added === false) {
1015 break;
1016 }
1017
1018 $success = $added;
1019 }
1020
1021 return $success;
1022 }
1023
1033 public function replaceNode($node, $idToReplace)
1034 {
1035 return $this->inTransaction(function () use ($node, $idToReplace) {
1036 $targetId = $this->getParentIdById($idToReplace);
1037 $targetIndex = $this->getIndexById($idToReplace);
1038
1039 $this->writer()->deleteNode($idToReplace);
1040
1041 $changedIds = [];
1042 $changedIds[] = $this->saveNodeIn($node, $targetId, $targetIndex, true);
1043 $changedIds[] = $targetId;
1044
1045 return $changedIds;
1046 });
1047 }
1048
1057 public function duplicateNode($nodeId, $recursive = false)
1058 {
1059 // get parent and position for new node
1060 $targetId = $this->getParentIdById($nodeId);
1061 $targetIndex = $this->getIndexById($nodeId) + 1;
1062 $dth = $this->getDoctypeHandler();
1063
1064 if (!$dth->isAllowedMove($nodeId, $targetId)) {
1065 return false;
1066 }
1067
1068 $xmlDoc = $this->getSubdocByNodeId($nodeId, false);
1069
1070 $this->clearCache();
1071
1072 $copyId = $this->inTransaction(
1073 fn() => $this->saveNodeIn($xmlDoc, $targetId, $targetIndex, $recursive)
1074 );
1075
1076 $dth->onCopyNode($nodeId, $copyId);
1077
1078 return $copyId;
1079 }
1080
1092 public function moveNode($nodeId, $targetId, $targetIndex)
1093 {
1094 return $this->moveNodeChecked($nodeId, $targetId, fn() => $targetIndex);
1095 }
1096
1102 public function moveNodeIn($nodeId, $targetId)
1103 {
1104 return $this->moveNodeChecked($nodeId, $targetId, fn() => -1);
1105 }
1106
1112 public function moveNodeBefore($nodeId, $targetId)
1113 {
1114 $targetParentId = $this->getParentIdById($targetId);
1115
1116 return $this->moveNodeChecked(
1117 $nodeId,
1118 $targetParentId,
1119 fn() => $this->indexToMoveTo($nodeId, $targetId)
1120 );
1121 }
1122
1128 public function moveNodeAfter($nodeId, $targetId)
1129 {
1130 $targetParentId = $this->getParentIdById($targetId);
1131
1132 return $this->moveNodeChecked(
1133 $nodeId,
1134 $targetParentId,
1135 fn() => $this->indexToMoveTo($nodeId, $targetId) + 1
1136 );
1137 }
1138
1150 private function indexToMoveTo($nodeId, $targetId, $offset = 0)
1151 {
1152 $targetIndex = (int) $this->getIndexById($targetId);
1153 $nodeIndex = $this->getIndexById($nodeId);
1154
1155 if (
1156 $nodeIndex !== false
1157 && $this->getParentIdById($nodeId) == $this->getParentIdById($targetId)
1158 && $nodeIndex < $targetIndex
1159 ) {
1160 $targetIndex--;
1161 }
1162
1163 return max(0, $targetIndex + $offset);
1164 }
1177 protected function moveNodeChecked($nodeId, $targetId, callable $getIndex)
1178 {
1179 $dth = $this->getDoctypeHandler();
1180
1181 if (
1182 $nodeId !== $targetId
1183 && $dth->isAllowedMove($nodeId, $targetId)
1184 ) {
1185 $parentId = $this->inTransaction(
1186 fn() => $this->writer()->moveNode($nodeId, $targetId, $getIndex())
1187 );
1188
1189 $dth->onMoveNode($nodeId, $parentId);
1190
1191 return $parentId;
1192 }
1193
1194 return false;
1195 }
1196
1207 public function copyNode($nodeId, $targetId, $targetIndex)
1208 {
1209 return $this->copyNodeChecked($nodeId, $targetId, fn() => $targetIndex);
1210 }
1211
1217 public function copyNodeIn($nodeId, $targetId)
1218 {
1219 return $this->copyNodeChecked($nodeId, $targetId, fn() => -1);
1220 }
1221
1231 public function copyNodeBefore($nodeId, $targetId)
1232 {
1233 return $this->copyNodeChecked(
1234 $nodeId,
1235 $this->getParentIdById($targetId),
1236 fn() => $this->getIndexById($targetId)
1237 );
1238 }
1239
1245 public function copyNodeAfter($nodeId, $targetId)
1246 {
1247 return $this->copyNodeChecked(
1248 $nodeId,
1249 $this->getParentIdById($targetId),
1250 fn() => $this->getIndexById($targetId) + 1
1251 );
1252 }
1253
1264 protected function copyNodeChecked($nodeId, $targetId, callable $getIndex)
1265 {
1266 $dth = $this->getDoctypeHandler();
1267
1268 if ($dth->isAllowedCopy($nodeId, $targetId)) {
1269 $copyId = $this->inTransaction(
1270 fn() => $this->writer()->copyNode($nodeId, $targetId, $getIndex())
1271 );
1272
1273 $dth->onCopyNode($nodeId, $copyId);
1274
1275 return $copyId;
1276 }
1277
1278 return false;
1279 }
1280
1288 public function setAttribute($nodeId, $attrName, $attrValue)
1289 {
1290 return $this->inTransaction(function () use ($nodeId, $attrName, $attrValue) {
1291 $attributes = $this->getAttributes($nodeId);
1292
1293 if (
1294 (
1295 isset($attributes[$attrName])
1296 && $attributes[$attrName] !== htmlspecialchars($attrValue)
1297 )
1298 || !isset($attributes[$attrName])
1299 ) {
1300 $dth = $this->getDoctypeHandler();
1301
1302 $dth->onSetAttribute($nodeId, $attrName, $attributes[$attrName] ?? null, $attrValue);
1303
1304 $attributes[$attrName] = $attrValue;
1305
1306 return $this->writer()->saveAttributes($nodeId, $attributes);
1307 }
1308
1309 return false;
1310 });
1311 }
1312
1318 public function removeAttribute($nodeId, $attrName)
1319 {
1320 return $this->inTransaction(function () use ($nodeId, $attrName) {
1321 $attributes = $this->getAttributes($nodeId);
1322
1323 if (isset($attributes[$attrName])) {
1324 unset($attributes[$attrName]);
1325
1326 return $this->writer()->saveAttributes($nodeId, $attributes);
1327 }
1328
1329 return false;
1330 });
1331 }
1332
1336 public static function removeNodeAttr($node, $dbNs, $attribute)
1337 {
1338 if ($node->nodeType == XML_ELEMENT_NODE || $node->nodeType == XML_DOCUMENT_NODE) {
1339 [$xml, $node] = \Depage\Xml\Document::getDocAndNode($node);
1340
1341 $xpath = new \DOMXPath($xml);
1342 $xpath->registerNamespace($dbNs->ns, $dbNs->uri);
1343 $xpResult = $xpath->query(".//@{$dbNs->ns}:{$attribute}", $node);
1344
1345 foreach ($xpResult as $node) {
1346 $node->parentNode->removeAttributeNode($node);
1347 }
1348 }
1349 }
1350
1356 public function hashDomNode($node)
1357 {
1358 $doc = new \DOMDocument();
1359 $doc->formatOutput = false;
1360
1361 [$d, $node] = \Depage\Xml\Document::getDocAndNode($node);
1362 $rootNode = $doc->importNode($node, true);
1363 $doc->appendChild($rootNode);
1364
1365 self::removeIdAttr($rootNode);
1366 self::removeNodeAttr($rootNode, $this->dbNs, 'lastchange');
1367 self::removeNodeAttr($rootNode, $this->dbNs, 'lastchangeUid');
1368 self::removeNodeAttr($rootNode, $this->dbNs, 'docid');
1369 self::removeNodeAttr($rootNode, $this->dbNs, 'released');
1370
1371 return hash("sha256", $doc->saveXML());
1372 }
1373
1378 protected function getFreeNodeIds($preference = [])
1379 {
1380 $this->freeElementIds = $this->writer()->getFreeNodeIds($preference);
1381 }
1382
1391 protected function addChildnodesByQuery($query, $row, $parentNode)
1392 {
1393 $doc = $parentNode->ownerDocument ?? $parentNode;
1394
1395 do {
1396 //get ELMEMENT_NODE
1397 if ($row->type == 'ELEMENT_NODE') {
1398 //create node
1399 if ($ns = $this->getNamespace($row->name)) {
1400 $node = $doc->createElementNS($ns->uri, $row->name);
1401 } else {
1402 $node = $doc->createElement($row->name);
1403 }
1404 $parentNode->appendChild($node);
1405
1406 if ($row->lvl == 0) {
1407 foreach ($this->namespaces as $n) {
1408 $node->setAttributeNS('http://www.w3.org/2000/xmlns/', "xmlns:{$n->ns}", $n->uri);
1409 }
1410
1411 //add lastchange-data
1412 $node->setAttributeNS($this->dbNs->uri, "{$this->dbNs->ns}:lastchange", $this->lastchange);
1413 $node->setAttributeNS($this->dbNs->uri, "{$this->dbNs->ns}:lastchangeUid", $this->lastchangeUid ?? '');
1414 }
1415
1416 //add attributes to node
1417 foreach (self::parseAttributeString($row->value) as $attrName => $attrValue) {
1418 if ($ns = $this->getNamespace($attrName)) {
1419 $node->setAttributeNS($ns->uri, $attrName, $attrValue);
1420 } else {
1421 $node->setAttribute($attrName, $attrValue);
1422 }
1423 }
1424
1425 //add id_attribute to node
1426 $node->setAttributeNS($this->dbNs->uri, "{$this->dbNs->ns}:{$this->idAttribute}", $row->id);
1427
1428 //get TEXT_NODES
1429 } elseif ($row->type == 'TEXT_NODE') {
1430 $node = $doc->createTextNode(strtr($row->value, "\r", " "));
1431 $parentNode->appendChild($node);
1432 //get CDATA_SECTION
1433 } elseif ($row->type == 'CDATA_SECTION_NODE') {
1434 // @todo CDATA not implemented yet
1435 //get COMMENT_NODE
1436 } elseif ($row->type == 'COMMENT_NODE') {
1437 $node = $doc->createComment($row->value);
1438 $parentNode->appendChild($node);
1439 //get PROCESSING_INSTRUCTION
1440 } elseif ($row->type == 'PI_NODE') {
1441 $node = $doc->createProcessingInstruction($row->name, $row->value);
1442 $parentNode->appendChild($node);
1443 //get ENTITY_REF Node
1444 } elseif ($row->type == 'ENTITY_REF_NODE') {
1445 // @todo ENTITY_REF_NODE not implemented yet
1446 }
1447
1448 $lastRow = $row;
1449 $row = $query->fetchObject();
1450
1451 if ($lastRow->type == 'ELEMENT_NODE') {
1452 if ($row && $lastRow->lvl < $row->lvl) {
1453 //add child_nodes
1454 $row = $this->addChildnodesByQuery($query, $row, $node);
1455 }
1456 }
1457 if (!$row || $lastRow->lvl > $row->lvl) {
1458 return $row;
1459 }
1460 } while ($row);
1461
1462 return $row;
1463 }
1464
1473 protected function getAppendIndex($targetId)
1474 {
1475 return $this->positions()->appendIndex($targetId);
1476 }
1477
1484 protected function getNamespace($name)
1485 {
1486 $parts = explode(":", $name, 2);
1487
1488 if (!isset($parts[1])) {
1489 return false;
1490 }
1491
1492 return $this->namespaces[$parts[0]];
1493 }
1494 protected function extractNamespaces($str)
1495 {
1496 $namespaces = [];
1497 $pName = '([a-zA-Z0-9]*)';
1498 $pAttr = '([^"]*)';
1499 preg_match_all("/xmlns:$pName=\"$pAttr\"/", $str, $nsElements, PREG_SET_ORDER);
1500
1501 foreach ($nsElements as $nsElement) {
1502 $namespaces[$nsElement[1]] = new XmlNs($nsElement[1], $nsElement[2]);
1503 }
1504
1505 return $namespaces;
1506 }
1507
1508 protected function saveNodePrivate($node)
1509 {
1510 return $this->writer()->saveNodePrivate($node);
1511 }
1512
1521 protected function saveNodeIn($node, $targetId, $targetIndex, $incChildren)
1522 {
1523 return $this->writer()->saveNodeIn($node, $targetId, $targetIndex, $incChildren);
1524 }
1525
1535 protected function getNodeArrayForSaving(&$nodeArray, $node, $parentIndex = null, $pos = 0, $stripwhitespace = true)
1536 {
1537 return $this->writer()->getNodeArrayForSaving($nodeArray, $node, $parentIndex, $pos, $stripwhitespace);
1538 }
1539
1552 protected function saveNodeToDb($node, $id, $targetId, $targetIndex, $setPosition = false)
1553 {
1554 return $this->writer()->saveNodeToDb($node, $id, $targetId, $targetIndex, $setPosition);
1555 }
1556
1567 protected function inTransaction(callable $work, bool $altering = true)
1568 {
1569 if ($altering) {
1570 $this->beginTransactionAltering();
1571 } else {
1573 }
1574
1575 try {
1576 $result = $work();
1577 } catch (\Throwable $e) {
1578 $this->rollBackTransaction();
1579
1580 throw $e;
1581 }
1582
1583 $this->endTransaction();
1584
1585 return $result;
1586 }
1587
1596 {
1597 return $this->xmlDb->beginTransactionAltering();
1598 }
1599
1608 {
1609 return $this->xmlDb->beginTransactionNonAltering();
1610 }
1611
1621 public function endTransaction()
1622 {
1623 if ($this->subhashesDirty && $this->xmlDb->transactions == 1) {
1624 // last open transaction level; the subhashes still have to be
1625 // written before the actual commit runs to stay atomic
1626 $this->subhashesDirty = false;
1627 $this->refreshSubhashes();
1628 }
1629
1630 $altered = $this->xmlDb->endTransaction();
1631
1632 if ($altered) {
1633 $this->clearDocInfoCache();
1634 $this->clearCache();
1635 $this->getDoctypeHandler()->onDocumentChange();
1636 }
1637
1638 return $altered;
1639 }
1640
1647 public function rollBackTransaction()
1648 {
1649 // the document may have changed before the rollback, reload the
1650 // doc info instead of handing out information about rolled back
1651 // changes
1652 $this->docInfoLoaded = false;
1653
1654 return $this->xmlDb->rollBackTransaction();
1655 }
1656
1661 public function clearDocInfoCache()
1662 {
1663 $this->docInfoLoaded = false;
1664 }
1665
1672 public function updateLastChange($timestamp = null, $uid = null)
1673 {
1674 $this->subhashesDirty = true;
1675
1676 // the document row changes, the memoized info is stale
1677 $this->docInfoLoaded = false;
1678
1679 $query = $this->pdo->prepare(
1680 "UPDATE {$this->tableDocs}
1681 SET
1682 lastchange=:timestamp,
1683 lastchange_uid=IFNULL(:user_id, lastchange_uid)
1684 WHERE
1685 id=:doc_id;",
1686 );
1687
1688 if (empty($timestamp)) {
1689 $timestamp = time();
1690 } elseif (is_string($timestamp)) {
1691 $timestamp = strtotime($timestamp);
1692 }
1693
1694 if (!empty($uid)) {
1695 $userId = $uid;
1696 } elseif (!empty($this->xmlDb->options['userId'])) {
1697 $userId = $this->xmlDb->options['userId'];
1698 } else {
1699 $userId = null;
1700 }
1701
1702 $params = [
1703 'doc_id' => $this->getDocId(),
1704 'timestamp' => date('Y-m-d H:i:s', $timestamp),
1705 'user_id' => $userId,
1706 ];
1707
1708 return ($query->execute($params)) ? $timestamp : false;
1709 }
1710
1721 protected function refreshSubhashes()
1722 {
1723 if (is_null($this->subhashUpdater)) {
1724 $this->subhashUpdater = new Helpers\SubhashUpdater($this->pdo, $this->tableXml, $this->docId, self::SORTKEY_PAD, self::SAVE_BATCH_SIZE);
1725 }
1726
1727 $this->subhashUpdater->refresh();
1728 }
1729
1737 public function clearCache()
1738 {
1739 if (!is_null($this->docId)) {
1740 $this->cache->delete(Helpers\CacheKey::forDocument($this->tableDocs, $this->docId));
1741 }
1742 }
1743}
1744
1745/* vim:set ft=php sw=4 sts=4 fdm=marker et : */
getFreeNodeIds($preference=[])
gets unused db-node-ids for saving nodes; the result is kept in free_element_ids, where the write pip...
getAttribute($nodeId, $attrName)
gets attribute of node
Definition Document.php:560
isPublished()
isPublished
Definition Document.php:317
saveNodePatch($node)
saves a subtree of the document reusing the parts of the tree that did not change; works like savePat...
Definition Document.php:917
getNodeIdsByXpath($xpath)
gets node_ids by xpath
Definition Document.php:432
refreshSubhashes()
recalculates the merkle style subhash of every node of the document; a subhash covers the stored type...
__construct($xmlDb, $docId)
Construct.
Definition Document.php:96
endTransaction()
closes the innermost open transaction level
clearDocInfoCache()
drops the memoized document info; has to be called when the document row or its history changes outsi...
copyNodeIn($nodeId, $targetId)
copy node to another node
saveNode($node)
saves the subtree of a node; the subtree is deleted and written again, the parent and the position of...
Definition Document.php:886
planNodePatch($node, $checkRoot=false)
compares the subtree rooted at $node with the nodes stored under the db:id its root element carries a...
Definition Document.php:810
const PATCH_MAX_CHANGE_RATIO
savePatch() falls back to a full save() and saveNodePatch() to a full saveNode() when more than this ...
Definition Document.php:33
setAttribute($nodeId, $attrName, $attrValue)
sets attribute of node
rollBackTransaction()
rolls back to the start of the innermost open transaction level, keeping the outer transaction levels...
duplicateNode($nodeId, $recursive=false)
duplicates node in database, and inserts it in the next position
addNode(\DomNode $node, $targetId, $targetIndex=-1, $extras=[])
adds a node to the child list of a node
Definition Document.php:949
getAttributes($nodeId)
gets all attributes of a node by id
Definition Document.php:573
copyNodeAfter($nodeId, $targetId)
copy node after another node
getSubdocByNodeId($id, $addIdAttribute=true)
gets an xml-document-object from specific db-id
Definition Document.php:451
getParentIdById($id)
gets parent db-id by one of its child_nodes-id
Definition Document.php:385
saveNodeToDb($node, $id, $targetId, $targetIndex, $setPosition=false)
saves a node to database
$subhashesDirty
set by updateLastChange, checked in endTransaction: the subhashes of the document are recalculated be...
Definition Document.php:83
updateLastChange($timestamp=null, $uid=null)
set or updates the lastchange date and uid for the current document
$docInfo
memoized result of getDocInfo(); every altering save updates the document row (and the history may ch...
Definition Document.php:64
saveNodeIn($node, $targetId, $targetIndex, $incChildren)
saves a node into the child list of a target node
getHistory()
getHistory
Definition Document.php:248
hashDomNode($node)
hashDomNode
rebalancePositions($parentId, $order=null)
renumbers the children of a parent node to have an even distance of self\POS_GAP between their positi...
Definition Document.php:415
getDocInfo()
gets info about a document by doc_id
Definition Document.php:148
$positions
lazily built collaborators: the sparse position management, shared by the read (index),...
Definition Document.php:72
save(\DomDocument $xml)
Definition Document.php:681
getNodeNameById($id)
gets node_name by node db-id
Definition Document.php:374
addChildnodesByQuery($query, $row, $parentNode)
add child nodes of a node to DOMDocument
const POS_GAP
distance between the sparse positions of sibling nodes; inserting a node between two neighbours takes...
Definition Document.php:24
getDocId()
Get Doc ID.
Definition Document.php:117
positions()
the collaborator managing the sparse sibling positions of this document's nodes
Definition Document.php:260
beginTransactionNonAltering()
opens a transaction that only reads the document
getNamespaceString($xml)
returns the namespace string that save() stores in the ns column of the document for the given xml do...
Definition Document.php:840
savePatch(\DomDocument $xml)
saves a document reusing the parts of the tree that did not change; subtrees whose merkle subhash equ...
Definition Document.php:733
moveNodeAfter($nodeId, $targetId)
moves node after another node
getSubdocByXpath($xpath, $addIdAttribute=true)
gets document by xpath.
Definition Document.php:540
getNodeDataId($node)
gets node db-dataid from db-dataid attribute
Definition Document.php:346
moveNodeBefore($nodeId, $targetId)
moves node before another node (insert before)
getAppendIndex($targetId)
gets the index a child would get when it is appended to the child list of a node, i....
getNodeAttributeById($id, $attribute)
Definition Document.php:353
isReleased()
isReleased
Definition Document.php:301
static parseAttributeString($value)
parses a stored attribute string into a name => value map; used by getAttributes and by the batch val...
Definition Document.php:597
getXml($addIdAttribute=true)
getXml
Definition Document.php:126
inTransaction(callable $work, bool $altering=true)
runs $work() inside a transaction and ends the transaction afterwards; rolls back to the transaction ...
copyNode($nodeId, $targetId, $targetIndex)
copies a node to a position in the child list of another node
addNodeByName($name, $targetId, $targetIndex, $extras=[])
creates the node(s) the doctype handler defines for a name and adds them to the child list of a node
Definition Document.php:977
moveNode($nodeId, $targetId, $targetIndex)
moves a node to a position in the child list of another node
clearCache()
clears the node-cache
moveNodeChecked($nodeId, $targetId, callable $getIndex)
moves a node if the doctype handler allows it; the four moveNode* variants differ only in how they co...
getNodeId($node)
gets node db-id from db-id attribute
Definition Document.php:331
removeAttribute($nodeId, $attrName)
removes an attribute of a node
removeIdAttr($node)
recursively remove all db-id attributes from node
Definition Document.php:639
applyPatch($plan)
executes the change plan of planPatch(), all nodes that were not touched by the plan keep their datab...
Definition Document.php:828
clearDoc()
clean all nodes inside of document
Definition Document.php:649
writer()
the collaborator running the write pipeline (DOM trees to rows) for this document; the transaction st...
Definition Document.php:279
$lastPatchStats
statistics about the changes applied by the last savePatch() or saveNodePatch() call,...
Definition Document.php:89
planPatch(\DomDocument $xml, $docInfo)
compares the nodes of $xml with the nodes stored in the database and collects the minimal set of chan...
Definition Document.php:786
static removeNodeAttr($node, $dbNs, $attribute)
recursively remove attributes from node
replaceNode($node, $idToReplace)
replaces a node in database
copyNodeChecked($nodeId, $targetId, callable $getIndex)
copies a node if the doctype handler allows it; the four copyNode* variants differ only in how they c...
moveNodeIn($nodeId, $targetId)
moves node to another node (append child)
getIndexById($id)
gets the index of a node in the child list of its parent node (number of siblings before it); this is...
Definition Document.php:404
getNamespace($name)
parseNodename
buildNode($name, $attributes)
Definition Document.php:619
getNamespacesAndEntities()
gets the namespaces and the entities of the document
Definition Document.php:230
getLastPatchStats()
number of nodes that could be skipped/updated/deleted/inserted by the last savePatch()/saveNodePatch(...
Definition Document.php:776
copyNodeBefore($nodeId, $targetId)
copy node before another node
beginTransactionAltering()
opens a transaction that changes the document
getNodeArrayForSaving(&$nodeArray, $node, $parentIndex=null, $pos=0, $stripwhitespace=true)
gets all nodes of a document in one array
static forDocument(string $tableDocs, $docId)
cache namespace of a document; all cached entries of a document (its subdocuments and xpath results) ...
Definition CacheKey.php:26
const POS_MIN_GAP
the child list is rebalanced once the space between two neighbours drops below this gap
Definition Positions.php:46
const POS_GAP
distance between the sparse positions of sibling nodes
Definition Positions.php:40
const MAX_CHANGE_RATIO
when more than this ratio of the nodes of a subtree has to be inserted or deleted,...
Default doctype handler of a document.
Definition Base.php:16
A namespace prefix and the URI it is bound to.
Definition XmlNs.php:9
what the write pipeline (Helpers\DocumentWriter) needs from the document it writes: the document meta...