99 $this->cache =
$xmlDb->cache;
101 $this->dbNs =
new XmlNs(
'db',
'http://cms.depagecms.net/ns/database');
105 $this->tablePrefix =
$xmlDb->tablePrefix;
106 $this->tableDocs =
$xmlDb->tableDocs;
107 $this->tableXml =
$xmlDb->tableXml;
126 public function getXml($addIdAttribute =
true)
130 if (is_null($rootId)) {
150 if (!$this->docInfoLoaded) {
151 $query = $this->pdo->prepare(
155 docs.rootid AS rootid,
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
165 'doc_id' => $this->docId,
168 $info = $query->fetchObject();
171 $info->lastchange = new \DateTime($info->lastchange);
172 $info->lastrelease = $this->
getHistory()->getLatestVersion()->lastsaved ??
false;
175 $this->docInfo = $info;
176 $this->docInfoLoaded =
true;
188 if (!isset($this->doctypeHandlers[$this->docId])) {
191 if (empty($className)) {
196 $className =
'\\' . ltrim($className,
'\\');
198 if (!class_exists($className)) {
201 if (!is_a($className,
Interfaces\DoctypeInterface::class,
true)) {
205 $handler =
new $className($this->xmlDb, $this);
208 if ($handler instanceof
Interfaces\DoctypeInterface) {
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",
238 'doc_id' => $this->docId,
241 return $query->fetchObject();
281 if (is_null($this->
writer)) {
284 table: $this->tableXml,
287 idAttribute: $this->idAttribute,
305 if (!empty($info->lastrelease) && $info->lastchange->getTimestamp() <= $info->lastrelease->getTimestamp()) {
321 return !empty($info->lastrelease);
333 return $node->nodeType == XML_ELEMENT_NODE
334 ? $node->getAttributeNS($this->dbNs->uri, $this->idAttribute)
348 return $node->nodeType == XML_ELEMENT_NODE
349 ? $node->getAttributeNS($this->dbNs->uri, $this->idDataAttribute)
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",
362 'doc_id' => $this->docId,
365 return ($result = $query->fetchObject()) ? $result->$attribute :
false;
406 return $this->
positions()->indexById($id);
417 $this->
positions()->rebalance($parentId, $order);
435 $fetchedIds = $this->cache->get($identifier);
437 if ($fetchedIds ===
false) {
438 $fetchedIds = $this->xmlDb->getNodeIdsByXpath($xpath, $this->docId);
440 $this->cache->set($identifier, $fetchedIds);
454 $xmlDoc = new \Depage\Xml\Document();
456 if ($xmlStr = $this->cache->get($identifier)) {
458 $xmlDoc->loadXML($xmlStr);
465 $query = $this->pdo->prepare(
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",
475 'doc_id' => $this->docId,
477 $result = $query->fetchObject();
479 $this->entities = $result->entities;
480 $this->namespaceString = $result->namespaces;
483 $this->lastchange = $result->lastchange;
484 $this->lastchangeUid = $result->lastchangeUid;
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(),
492 'doc_id' => $this->docId,
495 $row = $query->fetchObject();
498 if (!($row && $row->type ==
'ELEMENT_NODE')) {
503 }
catch (\Throwable $e) {
507 $query->closeCursor();
515 if ($dth->testDocument($xmlDoc)) {
520 if (is_object($xmlDoc) && $xmlDoc->documentElement !=
null) {
521 $this->cache->set($identifier, $xmlDoc->saveXML());
525 if (is_a($xmlDoc,
'DOMDocument') && $xmlDoc->documentElement !=
null && !$addIdAttribute) {
545 if (isset($ids[0])) {
564 return (isset($attributes[$attrName])) ? $attributes[$attrName] :
false;
577 $query = $this->pdo->prepare(
579 FROM {$this->tableXml} AS xml
580 WHERE xml.id = :node_id AND xml.type='ELEMENT_NODE' AND xml.id_doc = :doc_id",
583 'node_id' => $nodeId,
584 'doc_id' => $this->docId,
587 if ($result = $query->fetchObject()) {
601 $matches = preg_split(
'/(="|"$|" )/', (
string) $value);
602 $matches = array_chunk($matches, 2);
603 foreach ($matches as $match) {
604 $name = trim($match[0]);
606 $attributes[$name] = htmlspecialchars_decode($match[1] ??
'');
623 $xml =
"<$name {$docInfo->namespaces}";
624 foreach ($attributes as $attr => $value) {
625 $xml .=
" $attr=\"$value\"";
629 $doc = new \Depage\Xml\Document();
632 return $doc->documentElement;
655 $this->pdo->exec(
"SET FOREIGN_KEY_CHECKS=0;");
658 $query = $this->pdo->prepare(
659 "DELETE FROM {$this->tableXml}
669 $this->pdo->exec(
"SET FOREIGN_KEY_CHECKS=1;");
681 public function save(\DomDocument $xml)
690 $query = $this->pdo->prepare(
691 "UPDATE {$this->tableDocs}
742 if (is_null($plan)) {
743 $this->lastPatchStats = [
'fallback' =>
true];
745 return $this->
save($xml);
749 $query = $this->pdo->prepare(
750 "UPDATE {$this->tableDocs}
758 'doc_id' => $this->docId,
766 $this->lastPatchStats = $plan[
'stats'] + [
'fallback' =>
false];
788 $root = $xml->documentElement;
789 $rootId = is_null($root) ?
'' : $this->
getNodeId($root);
791 if (empty($rootId) || (
int) $rootId !== (
int)
$docInfo->rootid) {
817 if (is_null($this->patchPlanner)) {
818 $this->patchPlanner =
new Patch\PatchPlanner($this->pdo, $this->tableXml, $this->docId, $this->dbNs, $this->idAttribute);
821 return $this->patchPlanner->plan($nodeArray, $checkRoot);
830 if (is_null($this->patchApplier)) {
834 $this->patchApplier->apply($plan);
842 $xpath = new \DOMXPath($xml);
845 foreach ($xpath->query(
'namespace::*') as $node) {
846 if ($node->nodeName ==
'xmlns:xml') {
849 $namespaces .=
" " . $node->nodeName .
"=\"" . htmlspecialchars($node->nodeValue) .
"\"";
864 if (!$dth->isAllowedDelete($nodeId)) {
869 $success = $this->
writer()->deleteNode($nodeId);
921 if (is_null($plan)) {
922 $this->lastPatchStats = [
'fallback' =>
true];
924 return (
int) $this->
saveNode($node);
933 $this->lastPatchStats = $plan[
'stats'] + [
'fallback' =>
false];
935 return $plan[
'root_id'];
949 public function addNode(\DomNode $node, $targetId, $targetIndex = -1, $extras = [])
953 if (!$dth->isAllowedAdd($node, $targetId)) {
957 $dth->onAddNode($node, $targetId, $targetIndex, $extras);
960 fn() => $this->
saveNodeIn($node, $targetId, $targetIndex,
true)
980 $newNodes = $dth->getNewNodeFor($name);
990 foreach ($newNodes as $node) {
991 if ($node->nodeType != \XML_ELEMENT_NODE) {
995 if (!$dth->isAllowedAdd($node, $targetId)) {
1008 $targetIndex = $this->
positions()->normalizeIndex($targetId, $targetIndex);
1011 foreach ($nodes as $index => $node) {
1012 $added = $this->
addNode($node, $targetId, $targetIndex + $index, $extras);
1014 if ($added ===
false) {
1035 return $this->
inTransaction(
function () use ($node, $idToReplace) {
1039 $this->
writer()->deleteNode($idToReplace);
1042 $changedIds[] = $this->
saveNodeIn($node, $targetId, $targetIndex,
true);
1043 $changedIds[] = $targetId;
1064 if (!$dth->isAllowedMove($nodeId, $targetId)) {
1073 fn() => $this->
saveNodeIn($xmlDoc, $targetId, $targetIndex, $recursive)
1076 $dth->onCopyNode($nodeId, $copyId);
1092 public function moveNode($nodeId, $targetId, $targetIndex)
1094 return $this->
moveNodeChecked($nodeId, $targetId, fn() => $targetIndex);
1119 fn() => $this->indexToMoveTo($nodeId, $targetId)
1135 fn() => $this->indexToMoveTo($nodeId, $targetId) + 1
1150 private function indexToMoveTo($nodeId, $targetId, $offset = 0)
1156 $nodeIndex !==
false
1158 && $nodeIndex < $targetIndex
1163 return max(0, $targetIndex + $offset);
1182 $nodeId !== $targetId
1183 && $dth->isAllowedMove($nodeId, $targetId)
1186 fn() => $this->
writer()->
moveNode($nodeId, $targetId, $getIndex())
1189 $dth->onMoveNode($nodeId, $parentId);
1207 public function copyNode($nodeId, $targetId, $targetIndex)
1209 return $this->
copyNodeChecked($nodeId, $targetId, fn() => $targetIndex);
1268 if ($dth->isAllowedCopy($nodeId, $targetId)) {
1270 fn() => $this->
writer()->
copyNode($nodeId, $targetId, $getIndex())
1273 $dth->onCopyNode($nodeId, $copyId);
1290 return $this->
inTransaction(
function () use ($nodeId, $attrName, $attrValue) {
1295 isset($attributes[$attrName])
1296 && $attributes[$attrName] !== htmlspecialchars($attrValue)
1298 || !isset($attributes[$attrName])
1302 $dth->onSetAttribute($nodeId, $attrName, $attributes[$attrName] ??
null, $attrValue);
1304 $attributes[$attrName] = $attrValue;
1306 return $this->
writer()->saveAttributes($nodeId, $attributes);
1320 return $this->
inTransaction(
function () use ($nodeId, $attrName) {
1323 if (isset($attributes[$attrName])) {
1324 unset($attributes[$attrName]);
1326 return $this->
writer()->saveAttributes($nodeId, $attributes);
1338 if ($node->nodeType == XML_ELEMENT_NODE || $node->nodeType == XML_DOCUMENT_NODE) {
1339 [$xml, $node] = \Depage\Xml\Document::getDocAndNode($node);
1341 $xpath = new \DOMXPath($xml);
1342 $xpath->registerNamespace(
$dbNs->ns,
$dbNs->uri);
1343 $xpResult = $xpath->query(
".//@{$dbNs->ns}:{$attribute}", $node);
1345 foreach ($xpResult as $node) {
1346 $node->parentNode->removeAttributeNode($node);
1358 $doc = new \DOMDocument();
1359 $doc->formatOutput =
false;
1361 [$d, $node] = \Depage\Xml\Document::getDocAndNode($node);
1362 $rootNode = $doc->importNode($node,
true);
1363 $doc->appendChild($rootNode);
1371 return hash(
"sha256", $doc->saveXML());
1380 $this->freeElementIds = $this->
writer()->getFreeNodeIds($preference);
1393 $doc = $parentNode->ownerDocument ?? $parentNode;
1397 if ($row->type ==
'ELEMENT_NODE') {
1400 $node = $doc->createElementNS($ns->uri, $row->name);
1402 $node = $doc->createElement($row->name);
1404 $parentNode->appendChild($node);
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);
1412 $node->setAttributeNS($this->dbNs->uri,
"{$this->dbNs->ns}:lastchange", $this->lastchange);
1413 $node->setAttributeNS($this->dbNs->uri,
"{$this->dbNs->ns}:lastchangeUid", $this->lastchangeUid ??
'');
1417 foreach (self::parseAttributeString($row->value) as $attrName => $attrValue) {
1419 $node->setAttributeNS($ns->uri, $attrName, $attrValue);
1421 $node->setAttribute($attrName, $attrValue);
1426 $node->setAttributeNS($this->dbNs->uri,
"{$this->dbNs->ns}:{$this->idAttribute}", $row->id);
1429 } elseif ($row->type ==
'TEXT_NODE') {
1430 $node = $doc->createTextNode(strtr($row->value,
"\r",
" "));
1431 $parentNode->appendChild($node);
1433 } elseif ($row->type ==
'CDATA_SECTION_NODE') {
1436 } elseif ($row->type ==
'COMMENT_NODE') {
1437 $node = $doc->createComment($row->value);
1438 $parentNode->appendChild($node);
1440 } elseif ($row->type ==
'PI_NODE') {
1441 $node = $doc->createProcessingInstruction($row->name, $row->value);
1442 $parentNode->appendChild($node);
1444 } elseif ($row->type ==
'ENTITY_REF_NODE') {
1449 $row = $query->fetchObject();
1451 if ($lastRow->type ==
'ELEMENT_NODE') {
1452 if ($row && $lastRow->lvl < $row->lvl) {
1457 if (!$row || $lastRow->lvl > $row->lvl) {
1475 return $this->
positions()->appendIndex($targetId);
1486 $parts = explode(
":", $name, 2);
1488 if (!isset($parts[1])) {
1492 return $this->namespaces[$parts[0]];
1497 $pName =
'([a-zA-Z0-9]*)';
1499 preg_match_all(
"/xmlns:$pName=\"$pAttr\"/", $str, $nsElements, PREG_SET_ORDER);
1501 foreach ($nsElements as $nsElement) {
1510 return $this->
writer()->saveNodePrivate($node);
1521 protected function saveNodeIn($node, $targetId, $targetIndex, $incChildren)
1523 return $this->
writer()->saveNodeIn($node, $targetId, $targetIndex, $incChildren);
1537 return $this->
writer()->getNodeArrayForSaving($nodeArray, $node, $parentIndex, $pos, $stripwhitespace);
1552 protected function saveNodeToDb($node, $id, $targetId, $targetIndex, $setPosition =
false)
1554 return $this->
writer()->saveNodeToDb($node, $id, $targetId, $targetIndex, $setPosition);
1577 }
catch (\Throwable $e) {
1597 return $this->xmlDb->beginTransactionAltering();
1609 return $this->xmlDb->beginTransactionNonAltering();
1623 if ($this->subhashesDirty && $this->xmlDb->transactions == 1) {
1626 $this->subhashesDirty =
false;
1630 $altered = $this->xmlDb->endTransaction();
1652 $this->docInfoLoaded =
false;
1654 return $this->xmlDb->rollBackTransaction();
1663 $this->docInfoLoaded =
false;
1674 $this->subhashesDirty =
true;
1677 $this->docInfoLoaded =
false;
1679 $query = $this->pdo->prepare(
1680 "UPDATE {$this->tableDocs}
1682 lastchange=:timestamp,
1683 lastchange_uid=IFNULL(:user_id, lastchange_uid)
1688 if (empty($timestamp)) {
1689 $timestamp = time();
1690 } elseif (is_string($timestamp)) {
1691 $timestamp = strtotime($timestamp);
1696 } elseif (!empty($this->xmlDb->options[
'userId'])) {
1697 $userId = $this->xmlDb->options[
'userId'];
1704 'timestamp' => date(
'Y-m-d H:i:s', $timestamp),
1705 'user_id' => $userId,
1708 return ($query->execute($params)) ? $timestamp :
false;
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);
1727 $this->subhashUpdater->refresh();
1739 if (!is_null($this->docId)) {
1740 $this->cache->delete(
Helpers\CacheKey::forDocument($this->tableDocs, $this->docId));
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
saveNodePatch($node)
saves a subtree of the document reusing the parts of the tree that did not change; works like savePat...
getNodeIdsByXpath($xpath)
gets node_ids by xpath
refreshSubhashes()
recalculates the merkle style subhash of every node of the document; a subhash covers the stored type...
__construct($xmlDb, $docId)
Construct.
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...
planNodePatch($node, $checkRoot=false)
compares the subtree rooted at $node with the nodes stored under the db:id its root element carries a...
const PATCH_MAX_CHANGE_RATIO
savePatch() falls back to a full save() and saveNodePatch() to a full saveNode() when more than this ...
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
getAttributes($nodeId)
gets all attributes of a node by id
copyNodeAfter($nodeId, $targetId)
copy node after another node
getSubdocByNodeId($id, $addIdAttribute=true)
gets an xml-document-object from specific db-id
getParentIdById($id)
gets parent db-id by one of its child_nodes-id
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...
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...
saveNodeIn($node, $targetId, $targetIndex, $incChildren)
saves a node into the child list of a target node
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...
getDocInfo()
gets info about a document by doc_id
$positions
lazily built collaborators: the sparse position management, shared by the read (index),...
getNodeNameById($id)
gets node_name by node db-id
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...
positions()
the collaborator managing the sparse sibling positions of this document's nodes
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...
savePatch(\DomDocument $xml)
saves a document reusing the parts of the tree that did not change; subtrees whose merkle subhash equ...
moveNodeAfter($nodeId, $targetId)
moves node after another node
getSubdocByXpath($xpath, $addIdAttribute=true)
gets document by xpath.
getNodeDataId($node)
gets node db-dataid from db-dataid attribute
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)
static parseAttributeString($value)
parses a stored attribute string into a name => value map; used by getAttributes and by the batch val...
getXml($addIdAttribute=true)
getXml
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
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
removeAttribute($nodeId, $attrName)
removes an attribute of a node
removeIdAttr($node)
recursively remove all db-id attributes from node
applyPatch($plan)
executes the change plan of planPatch(), all nodes that were not touched by the plan keep their datab...
clearDoc()
clean all nodes inside of document
writer()
the collaborator running the write pipeline (DOM trees to rows) for this document; the transaction st...
$lastPatchStats
statistics about the changes applied by the last savePatch() or saveNodePatch() call,...
planPatch(\DomDocument $xml, $docInfo)
compares the nodes of $xml with the nodes stored in the database and collects the minimal set of chan...
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...
getNamespace($name)
parseNodename
buildNode($name, $attributes)
getNamespacesAndEntities()
gets the namespaces and the entities of the document
getLastPatchStats()
number of nodes that could be skipped/updated/deleted/inserted by the last savePatch()/saveNodePatch(...
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) ...
const POS_MIN_GAP
the child list is rebalanced once the space between two neighbours drops below this gap
const POS_GAP
distance between the sparse positions of sibling nodes
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.
A namespace prefix and the URI it is bound to.
what the write pipeline (Helpers\DocumentWriter) needs from the document it writes: the document meta...