1*04fd306cSNickeau<?php 2*04fd306cSNickeau 3*04fd306cSNickeau 4*04fd306cSNickeaunamespace ComboStrap\Xml; 5*04fd306cSNickeau 6*04fd306cSNickeau 7*04fd306cSNickeauuse ComboStrap\ExceptionBadArgument; 8*04fd306cSNickeauuse ComboStrap\ExceptionBadSyntax; 9*04fd306cSNickeauuse ComboStrap\ExceptionCompile; 10*04fd306cSNickeauuse ComboStrap\ExceptionNotEquals; 11*04fd306cSNickeauuse ComboStrap\ExceptionRuntime; 12*04fd306cSNickeauuse ComboStrap\Html; 13*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 14*04fd306cSNickeauuse ComboStrap\Web\Url; 15*04fd306cSNickeauuse ComboStrap\Xml\XmlDocument; 16*04fd306cSNickeauuse ComboStrap\Xml\XmlElement; 17*04fd306cSNickeauuse DOMDocument; 18*04fd306cSNickeauuse DOMElement; 19*04fd306cSNickeauuse DOMNode; 20*04fd306cSNickeauuse Exception; 21*04fd306cSNickeau 22*04fd306cSNickeau/** 23*04fd306cSNickeau * 24*04fd306cSNickeau * @package ComboStrap 25*04fd306cSNickeau * Static function around the {@link XmlDocument} 26*04fd306cSNickeau * 27*04fd306cSNickeau * 28*04fd306cSNickeau */ 29*04fd306cSNickeauclass XmlSystems 30*04fd306cSNickeau{ 31*04fd306cSNickeau const OPEN = "open"; 32*04fd306cSNickeau const CLOSED = "closed"; 33*04fd306cSNickeau const NORMAL = "normal"; 34*04fd306cSNickeau 35*04fd306cSNickeau 36*04fd306cSNickeau /** 37*04fd306cSNickeau * Get a Simple XMl Element and returns it without the XML header (ie as HTML node) 38*04fd306cSNickeau * @param DOMDocument $linkDom 39*04fd306cSNickeau * @return false|string 40*04fd306cSNickeau */ 41*04fd306cSNickeau public static function asHtml($linkDom) 42*04fd306cSNickeau { 43*04fd306cSNickeau 44*04fd306cSNickeau /** 45*04fd306cSNickeau * ownerDocument returned the DOMElement 46*04fd306cSNickeau */ 47*04fd306cSNickeau return $linkDom->ownerDocument->saveXML($linkDom->ownerDocument->documentElement); 48*04fd306cSNickeau } 49*04fd306cSNickeau 50*04fd306cSNickeau /** 51*04fd306cSNickeau * Check of the text is a valid XML 52*04fd306cSNickeau * @param $text 53*04fd306cSNickeau * @return bool 54*04fd306cSNickeau */ 55*04fd306cSNickeau public static function isXml($text) 56*04fd306cSNickeau { 57*04fd306cSNickeau 58*04fd306cSNickeau $valid = true; 59*04fd306cSNickeau try { 60*04fd306cSNickeau new XmlDocument($text); 61*04fd306cSNickeau } catch (\Exception $e) { 62*04fd306cSNickeau $valid = false; 63*04fd306cSNickeau } 64*04fd306cSNickeau return $valid; 65*04fd306cSNickeau 66*04fd306cSNickeau 67*04fd306cSNickeau } 68*04fd306cSNickeau 69*04fd306cSNickeau /** 70*04fd306cSNickeau * Return a formatted HTML 71*04fd306cSNickeau * @param $text 72*04fd306cSNickeau * @return mixed 73*04fd306cSNickeau * DOMDocument supports formatted XML while SimpleXMLElement does not. 74*04fd306cSNickeau * @throws \Exception if empty 75*04fd306cSNickeau */ 76*04fd306cSNickeau public static function format($text) 77*04fd306cSNickeau { 78*04fd306cSNickeau if (empty($text)) { 79*04fd306cSNickeau throw new \Exception("The text should not be empty"); 80*04fd306cSNickeau } 81*04fd306cSNickeau $doc = new DOMDocument(); 82*04fd306cSNickeau $doc->loadXML($text); 83*04fd306cSNickeau $doc->normalize(); 84*04fd306cSNickeau $doc->formatOutput = true; 85*04fd306cSNickeau // Type doc can also be reach with $domNode->ownerDocument 86*04fd306cSNickeau return $doc->saveXML(); 87*04fd306cSNickeau 88*04fd306cSNickeau 89*04fd306cSNickeau } 90*04fd306cSNickeau 91*04fd306cSNickeau /** 92*04fd306cSNickeau * @param $text 93*04fd306cSNickeau * @return string 94*04fd306cSNickeau * @throws ExceptionBadSyntax 95*04fd306cSNickeau */ 96*04fd306cSNickeau public static function normalize($text) 97*04fd306cSNickeau { 98*04fd306cSNickeau if (empty($text)) { 99*04fd306cSNickeau throw new ExceptionBadSyntax("The text should not be empty"); 100*04fd306cSNickeau } 101*04fd306cSNickeau $xmlDoc = new XmlDocument($text, XmlDocument::XML_TYPE); 102*04fd306cSNickeau return $xmlDoc->toXmlNormalized(); 103*04fd306cSNickeau } 104*04fd306cSNickeau 105*04fd306cSNickeau /** 106*04fd306cSNickeau * note: Option for the loading of {@link XmlDocument} 107*04fd306cSNickeau * have also this option 108*04fd306cSNickeau * 109*04fd306cSNickeau * @param $text 110*04fd306cSNickeau * @return string|string[] 111*04fd306cSNickeau */ 112*04fd306cSNickeau public static function extractTextWithoutCdata($text) 113*04fd306cSNickeau { 114*04fd306cSNickeau $text = str_replace("/*<![CDATA[*/", "", $text); 115*04fd306cSNickeau $text = str_replace("/*!]]>*/", "", $text); 116*04fd306cSNickeau $text = str_replace("\/", "/", $text); 117*04fd306cSNickeau return $text; 118*04fd306cSNickeau } 119*04fd306cSNickeau 120*04fd306cSNickeau public static function preprocessText($text) 121*04fd306cSNickeau { 122*04fd306cSNickeau 123*04fd306cSNickeau } 124*04fd306cSNickeau 125*04fd306cSNickeau 126*04fd306cSNickeau /** 127*04fd306cSNickeau * @param DOMNode $leftNode 128*04fd306cSNickeau * @param DOMNode $rightNode 129*04fd306cSNickeau * Tip: To get the text of a node: 130*04fd306cSNickeau * $leftNode->ownerDocument->saveHTML($leftNode) 131*04fd306cSNickeau * @param $error 132*04fd306cSNickeau * @param string[]|null $excludedAttributes - the value of this attributes will not be checked 133*04fd306cSNickeau */ 134*04fd306cSNickeau public static function diffNode(DOMNode $leftNode, DOMNode $rightNode, &$error, array $excludedAttributes = null) 135*04fd306cSNickeau { 136*04fd306cSNickeau 137*04fd306cSNickeau if ($excludedAttributes === null) { 138*04fd306cSNickeau $excludedAttributes = []; 139*04fd306cSNickeau } 140*04fd306cSNickeau $leftNodeName = $leftNode->localName; 141*04fd306cSNickeau $rightNodeName = $rightNode->localName; 142*04fd306cSNickeau if ($leftNodeName != $rightNodeName) { 143*04fd306cSNickeau $error .= "The node (" . $rightNode->getNodePath() . ") are different (" . $leftNodeName . "," . $rightNodeName . ")\n"; 144*04fd306cSNickeau } 145*04fd306cSNickeau if ($leftNode->hasAttributes()) { 146*04fd306cSNickeau $leftAttributesLength = $leftNode->attributes->length; 147*04fd306cSNickeau $rightNodeAttributes = $rightNode->attributes; 148*04fd306cSNickeau if ($rightNodeAttributes == null) { 149*04fd306cSNickeau $error .= "The node (" . $rightNode->getNodePath() . ") have no attributes while the left node has.\n"; 150*04fd306cSNickeau } else { 151*04fd306cSNickeau 152*04fd306cSNickeau /** 153*04fd306cSNickeau * Collect the attributes by name 154*04fd306cSNickeau */ 155*04fd306cSNickeau $leftAttributes = array(); 156*04fd306cSNickeau for ($i = 0; $i < $leftAttributesLength; $i++) { 157*04fd306cSNickeau $leftAtt = $leftNode->attributes->item($i); 158*04fd306cSNickeau $leftAttributes[$leftAtt->nodeName] = $leftAtt; 159*04fd306cSNickeau } 160*04fd306cSNickeau ksort($leftAttributes); 161*04fd306cSNickeau $rightAttributes = array(); 162*04fd306cSNickeau for ($i = 0; $i < $rightNodeAttributes->length; $i++) { 163*04fd306cSNickeau $rightAtt = $rightNodeAttributes->item($i); 164*04fd306cSNickeau $rightAttributes[$rightAtt->nodeName] = $rightAtt; 165*04fd306cSNickeau } 166*04fd306cSNickeau 167*04fd306cSNickeau foreach ($leftAttributes as $leftAttName => $leftAtt) { 168*04fd306cSNickeau /** @var \DOMAttr $leftAtt */ 169*04fd306cSNickeau $rightAtt = $rightAttributes[$leftAttName]; 170*04fd306cSNickeau if ($rightAtt == null) { 171*04fd306cSNickeau if (!in_array($leftAttName, $excludedAttributes)) { 172*04fd306cSNickeau $error .= "The attribute (" . $leftAtt->getNodePath() . ") does not exist on the right side\n"; 173*04fd306cSNickeau } 174*04fd306cSNickeau continue; 175*04fd306cSNickeau } 176*04fd306cSNickeau 177*04fd306cSNickeau unset($rightAttributes[$leftAttName]); 178*04fd306cSNickeau 179*04fd306cSNickeau /** 180*04fd306cSNickeau * Value check 181*04fd306cSNickeau */ 182*04fd306cSNickeau if (in_array($leftAttName, $excludedAttributes)) { 183*04fd306cSNickeau continue; 184*04fd306cSNickeau } 185*04fd306cSNickeau $leftAttValue = $leftAtt->nodeValue; 186*04fd306cSNickeau $rightAttValue = $rightAtt->nodeValue; 187*04fd306cSNickeau if ($leftAttValue !== $rightAttValue) { 188*04fd306cSNickeau switch ($leftAtt->name) { 189*04fd306cSNickeau case "class": 190*04fd306cSNickeau $error .= Html::getDiffBetweenValuesSeparatedByBlank($leftAttValue, $rightAttValue, "left ,{$leftAtt->getNodePath()}", "right, {$leftAtt->getNodePath()}"); 191*04fd306cSNickeau break; 192*04fd306cSNickeau case "srcset": 193*04fd306cSNickeau case "data-srcset": 194*04fd306cSNickeau try { 195*04fd306cSNickeau Html::getDiffBetweenSrcSet($leftAttValue, $rightAttValue); 196*04fd306cSNickeau } catch (ExceptionBadSyntax|ExceptionNotEquals $e) { 197*04fd306cSNickeau $error .= $e->getMessage(); 198*04fd306cSNickeau } 199*04fd306cSNickeau break; 200*04fd306cSNickeau case "src": 201*04fd306cSNickeau case "data-src": 202*04fd306cSNickeau case "href": 203*04fd306cSNickeau case "action": // form 204*04fd306cSNickeau try { 205*04fd306cSNickeau $leftUrl = Url::createFromString($leftAttValue); 206*04fd306cSNickeau try { 207*04fd306cSNickeau $rightUrl = Url::createFromString($rightAttValue); 208*04fd306cSNickeau try { 209*04fd306cSNickeau $leftUrl->equals($rightUrl); 210*04fd306cSNickeau } catch (ExceptionNotEquals $e) { 211*04fd306cSNickeau $error .= "The attribute (" . $rightAtt->getNodePath() . ") has different values. Error:{$e->getMessage()}\n"; 212*04fd306cSNickeau } 213*04fd306cSNickeau } catch (ExceptionBadSyntax|ExceptionBadArgument $e) { 214*04fd306cSNickeau $error .= "The attribute (" . $leftAtt->getNodePath() . ") has different values (" . $leftAttValue . "," . $rightAttValue . ") and the right value is not an URL. Error:{$e->getMessage()}\n"; 215*04fd306cSNickeau } 216*04fd306cSNickeau } catch (ExceptionBadSyntax|ExceptionBadArgument $e) { 217*04fd306cSNickeau $error .= "The attribute (" . $leftAtt->getNodePath() . ") has different values (" . $leftAttValue . "," . $rightAttValue . ") and the left value is not an URL. Error:{$e->getMessage()}\n"; 218*04fd306cSNickeau } 219*04fd306cSNickeau break; 220*04fd306cSNickeau case "style": 221*04fd306cSNickeau try { 222*04fd306cSNickeau StyleAttribute::stringEquals($leftAttValue, $rightAttValue); 223*04fd306cSNickeau } catch (ExceptionNotEquals $e) { 224*04fd306cSNickeau $error .= "The style attribute (" . $leftAtt->getNodePath() . ") has different values (" . $leftAttValue . "," . $rightAttValue . "). Error:{$e->getMessage()}\n"; 225*04fd306cSNickeau } 226*04fd306cSNickeau break; 227*04fd306cSNickeau default: 228*04fd306cSNickeau $error .= "The attribute (" . $leftAtt->getNodePath() . ") have different values (" . $leftAttValue . "," . $rightAttValue . ")\n"; 229*04fd306cSNickeau break; 230*04fd306cSNickeau } 231*04fd306cSNickeau } 232*04fd306cSNickeau 233*04fd306cSNickeau } 234*04fd306cSNickeau 235*04fd306cSNickeau ksort($rightAttributes); 236*04fd306cSNickeau foreach ($rightAttributes as $rightAttName => $rightAtt) { 237*04fd306cSNickeau if (!in_array($rightAttName, $excludedAttributes)) { 238*04fd306cSNickeau $error .= "The attribute (" . $rightAttName . ") of the node (" . $rightAtt->getNodePath() . ") does not exist on the left side\n"; 239*04fd306cSNickeau } 240*04fd306cSNickeau } 241*04fd306cSNickeau } 242*04fd306cSNickeau } else { 243*04fd306cSNickeau if ($rightNode->hasAttributes()) { 244*04fd306cSNickeau for ($i = 0; $i < $rightNode->attributes->length; $i++) { 245*04fd306cSNickeau /** @var \DOMAttr $rightAtt */ 246*04fd306cSNickeau $rightAtt = $rightNode->attributes->item($i); 247*04fd306cSNickeau $error .= "The attribute (" . $rightAtt->getNodePath() . ") does not exist on the left side\n"; 248*04fd306cSNickeau } 249*04fd306cSNickeau } 250*04fd306cSNickeau } 251*04fd306cSNickeau if ($leftNode->nodeName == "#text") { 252*04fd306cSNickeau $leftNodeValue = trim($leftNode->nodeValue); 253*04fd306cSNickeau $rightNodeValue = trim($rightNode->nodeValue); 254*04fd306cSNickeau if ($leftNodeValue != $rightNodeValue) { 255*04fd306cSNickeau $error .= "The node (" . $rightNode->getNodePath() . ") have different values (" . $leftNodeValue . "," . $rightNodeValue . ")\n"; 256*04fd306cSNickeau } 257*04fd306cSNickeau } 258*04fd306cSNickeau 259*04fd306cSNickeau /** 260*04fd306cSNickeau * Sub 261*04fd306cSNickeau */ 262*04fd306cSNickeau if ($leftNode->hasChildNodes()) { 263*04fd306cSNickeau 264*04fd306cSNickeau $rightChildNodes = $rightNode->childNodes; 265*04fd306cSNickeau $rightChildNodesCount = $rightChildNodes->length; 266*04fd306cSNickeau if ($rightChildNodes == null || $rightChildNodesCount == 0) { 267*04fd306cSNickeau $firstNode = $leftNode->childNodes->item(0); 268*04fd306cSNickeau $firstNodeName = $firstNode->nodeName; 269*04fd306cSNickeau $firstValue = $firstNode->nodeValue; 270*04fd306cSNickeau $error .= "The left node (" . $leftNode->getNodePath() . ") have child nodes while the right has not (First Left Node: $firstNodeName, value: $firstValue) \n"; 271*04fd306cSNickeau } else { 272*04fd306cSNickeau $leftChildNodeCount = $leftNode->childNodes->length; 273*04fd306cSNickeau $leftChildIndex = 0; 274*04fd306cSNickeau $rightChildIndex = 0; 275*04fd306cSNickeau while ($leftChildIndex < $leftChildNodeCount && $rightChildIndex < $rightChildNodesCount) { 276*04fd306cSNickeau 277*04fd306cSNickeau $leftChildNode = $leftNode->childNodes->item($leftChildIndex); 278*04fd306cSNickeau if ($leftChildNode->nodeName == "#text") { 279*04fd306cSNickeau $leftChildNodeValue = trim($leftChildNode->nodeValue); 280*04fd306cSNickeau if (empty(trim($leftChildNodeValue))) { 281*04fd306cSNickeau $leftChildIndex++; 282*04fd306cSNickeau $leftChildNode = $leftNode->childNodes->item($leftChildIndex); 283*04fd306cSNickeau } 284*04fd306cSNickeau } 285*04fd306cSNickeau 286*04fd306cSNickeau $rightChildNode = $rightChildNodes->item($rightChildIndex); 287*04fd306cSNickeau if ($rightChildNode->nodeName == "#text") { 288*04fd306cSNickeau $leftChildNodeValue = trim($rightChildNode->nodeValue); 289*04fd306cSNickeau if (empty(trim($leftChildNodeValue))) { 290*04fd306cSNickeau $rightChildIndex++; 291*04fd306cSNickeau $rightChildNode = $rightChildNodes->item($rightChildIndex); 292*04fd306cSNickeau } 293*04fd306cSNickeau } 294*04fd306cSNickeau 295*04fd306cSNickeau if ($rightChildNode != null) { 296*04fd306cSNickeau if ($leftChildNode != null) { 297*04fd306cSNickeau self::diffNode($leftChildNode, $rightChildNode, $error, $excludedAttributes); 298*04fd306cSNickeau } else { 299*04fd306cSNickeau $error .= "The right node (" . $rightChildNode->getNodePath() . ") does not exist in the left document.\n"; 300*04fd306cSNickeau } 301*04fd306cSNickeau } else { 302*04fd306cSNickeau if ($leftChildNode != null) { 303*04fd306cSNickeau $error .= "The left node (" . $leftChildNode->getNodePath() . ") does not exist in the right document.\n"; 304*04fd306cSNickeau } 305*04fd306cSNickeau } 306*04fd306cSNickeau 307*04fd306cSNickeau /** 308*04fd306cSNickeau * 0 based index 309*04fd306cSNickeau */ 310*04fd306cSNickeau $leftChildIndex++; 311*04fd306cSNickeau $rightChildIndex++; 312*04fd306cSNickeau } 313*04fd306cSNickeau } 314*04fd306cSNickeau } 315*04fd306cSNickeau 316*04fd306cSNickeau } 317*04fd306cSNickeau 318*04fd306cSNickeau /** 319*04fd306cSNickeau * Return a diff 320*04fd306cSNickeau * @param string $left 321*04fd306cSNickeau * @param string $right 322*04fd306cSNickeau * @return string 323*04fd306cSNickeau * DOMDocument supports formatted XML while SimpleXMLElement does not. 324*04fd306cSNickeau * @throws ExceptionCompile 325*04fd306cSNickeau */ 326*04fd306cSNickeau public 327*04fd306cSNickeau static function diffMarkup(string $left, string $right): string 328*04fd306cSNickeau { 329*04fd306cSNickeau if (empty($right)) { 330*04fd306cSNickeau throw new \RuntimeException("The right text should not be empty"); 331*04fd306cSNickeau } 332*04fd306cSNickeau $leftDocument = new XmlDocument($left); 333*04fd306cSNickeau 334*04fd306cSNickeau if (empty($left)) { 335*04fd306cSNickeau throw new \RuntimeException("The left text should not be empty"); 336*04fd306cSNickeau } 337*04fd306cSNickeau $rightDocument = new XmlDocument($right); 338*04fd306cSNickeau 339*04fd306cSNickeau return $leftDocument->diff($rightDocument); 340*04fd306cSNickeau 341*04fd306cSNickeau } 342*04fd306cSNickeau 343*04fd306cSNickeau public static function deleteAllElementsByName(string $elementName, XmlDocument $xmlDocument) 344*04fd306cSNickeau { 345*04fd306cSNickeau $xpathQuery = "//*[local-name()='$elementName']"; 346*04fd306cSNickeau try { 347*04fd306cSNickeau $svgElement = $xmlDocument->xpath($xpathQuery); 348*04fd306cSNickeau } catch (ExceptionBadSyntax $e) { 349*04fd306cSNickeau // should not happen on prod 350*04fd306cSNickeau throw new ExceptionRuntime("xpath query error ($xpathQuery"); 351*04fd306cSNickeau } 352*04fd306cSNickeau for ($i = 0; $i < $svgElement->length; $i++) { 353*04fd306cSNickeau $nodeElement = XmlElement::create($svgElement[$i], $xmlDocument); 354*04fd306cSNickeau $nodeElement->remove(); 355*04fd306cSNickeau } 356*04fd306cSNickeau } 357*04fd306cSNickeau 358*04fd306cSNickeau 359*04fd306cSNickeau} 360