1<?php 2/** 3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12 13namespace ComboStrap; 14 15use dokuwiki\Extension\SyntaxPlugin; 16use syntax_plugin_combo_cell; 17 18/** 19 * An helper to create manipulate component and html attributes 20 * 21 * You can: 22 * * declare component attribute after parsing 23 * * declare Html attribute during parsing 24 * * output the final HTML attributes at the end of the process with the function {@link TagAttributes::toHTMLAttributeString()} 25 * 26 * Component attributes have precedence on HTML attributes. 27 * 28 * @package ComboStrap 29 */ 30class TagAttributes 31{ 32 /** 33 * @var string the alt attribute value (known as the title for dokuwiki) 34 */ 35 const TITLE_KEY = 'title'; 36 37 38 const TYPE_KEY = "type"; 39 const ID_KEY = "id"; 40 41 /** 42 * The logical attributes that: 43 * * are not becoming HTML attributes 44 * * are never deleted 45 * (ie internal reserved words) 46 */ 47 const RESERVED_ATTRIBUTES = [ 48 self::SCRIPT_KEY, // no script attribute for security reason 49 TagAttributes::TYPE_KEY, // type is the component class 50 MediaLink::LINKING_KEY, // internal to image 51 CacheMedia::CACHE_KEY, // internal also 52 \syntax_plugin_combo_webcode::RENDERING_MODE_ATTRIBUTE, 53 syntax_plugin_combo_cell::VERTICAL_ATTRIBUTE, 54 self::OPEN_TAG, 55 self::HTML_BEFORE, 56 self::HTML_AFTER 57 ]; 58 59 /** 60 * The inline element 61 * We could pass the plugin object into tag attribute in place of the logical tag 62 * and check if the {@link SyntaxPlugin::getPType()} is normal 63 */ 64 const INLINE_LOGICAL_ELEMENTS = [ 65 SvgImageLink::CANONICAL, 66 RasterImageLink::CANONICAL, 67 \syntax_plugin_combo_link::TAG, // link button for instance 68 \syntax_plugin_combo_button::TAG, 69 \syntax_plugin_combo_heading::TAG 70 ]; 71 const SCRIPT_KEY = "script"; 72 const TRANSFORM = "transform"; 73 74 const CANONICAL = "tag"; 75 const DISPLAY = "display"; 76 const CLASS_KEY = "class"; 77 const WIKI_ID = "wiki-id"; 78 79 /** 80 * The open tag attributes 81 * permit to not close the tag in {@link TagAttributes::toHtmlEnterTag()} 82 * 83 * It's used for instance by the {@link \syntax_plugin_combo_tooltip} 84 * to advertise that it will add attribute and close it 85 */ 86 const OPEN_TAG = "open-tag"; 87 88 /** 89 * If an attribute has this value, 90 * it will not be added to the output (ie {@link TagAttributes::toHtmlEnterTag()}) 91 * Child element can unset attribute this way 92 * in order to write their own 93 * 94 * This is used by the {@link \syntax_plugin_combo_tooltip} 95 * to advertise that the title attribute should not be set 96 */ 97 const UN_SET = "unset"; 98 99 /** 100 * When wrapping an element 101 * A tag may get HTML before and after 102 * Uses for instance to wrap a svg in span 103 * when adding a {@link \syntax_plugin_combo_tooltip} 104 */ 105 const HTML_BEFORE = "htmlBefore"; 106 const HTML_AFTER = "htmlAfter"; 107 108 /** 109 * A global static counter 110 * to {@link TagAttributes::generateAndSetId()} 111 */ 112 private static $counter = 0; 113 114 115 /** 116 * @var array attribute that were set on a component 117 */ 118 private $componentAttributesCaseInsensitive; 119 120 /** 121 * @var array the style declaration array 122 */ 123 private $styleDeclaration = array(); 124 125 /** 126 * @var bool - set when the transformation from component attribute to html attribute 127 * was done to avoid circular problem 128 */ 129 private $componentToHtmlAttributeProcessingWasDone = false; 130 131 /** 132 * @var array - html attributes set in the code. This is needed to make a difference 133 * on attribute name that are the same such as the component attribute `width` that is 134 * transformed as a style `max-width` but exists also as attribute of an image for instance 135 */ 136 private $htmlAttributes = array(); 137 138 /** 139 * @var array - the final html array 140 */ 141 private $finalHtmlArray = array(); 142 143 /** 144 * @var string the functional tag to which the attributes applies 145 * It's not an HTML tag (a div can have a flex display or a block and they don't carry this information) 146 * The tag gives also context for the attributes (ie an div has no natural width while an img has) 147 */ 148 private $logicalTag; 149 150 /** 151 * An html that should be added after the enter tag 152 * (used for instance to add metadata such as backgrounds, illustration image for cards ,... 153 * @var string 154 */ 155 private $htmlAfterEnterTag; 156 157 /** 158 * Use to make the difference between 159 * an HTTP call for a media (ie SVG) vs an HTTP call for a page (HTML) 160 */ 161 const TEXT_HTML_MIME = "text/html"; 162 private $mime = TagAttributes::TEXT_HTML_MIME; 163 164 165 /** 166 * ComponentAttributes constructor. 167 * Use the static create function to instantiate this object 168 * @param $tag - tag (the tag gives context for the attributes 169 * * an div has no natural width while an img has 170 * * this is not always the component name / syntax name (for instance the {@link \syntax_plugin_combo_codemarkdown} is another syntax 171 * for a {@link \syntax_plugin_combo_code} and have therefore the same logical name) 172 * @param array $componentAttributes 173 */ 174 private function __construct($componentAttributes = array(), $tag = null) 175 { 176 $this->logicalTag = $tag; 177 $this->componentAttributesCaseInsensitive = new ArrayCaseInsensitive($componentAttributes); 178 179 /** 180 * Delete null values 181 * Empty string, 0 may exist 182 */ 183 foreach ($componentAttributes as $key => $value) { 184 if (is_null($value)) { 185 unset($this->componentAttributesCaseInsensitive[$key]); 186 } 187 } 188 189 } 190 191 /** 192 * @param $match - the {@link SyntaxPlugin::handle()} match 193 * @param array $defaultAttributes 194 * @return TagAttributes 195 */ 196 public static function createFromTagMatch($match, $defaultAttributes = []) 197 { 198 $inlineHtmlAttributes = PluginUtility::getTagAttributes($match); 199 $tag = PluginUtility::getTag($match); 200 $mergedAttributes = PluginUtility::mergeAttributes($inlineHtmlAttributes, $defaultAttributes); 201 return self::createFromCallStackArray($mergedAttributes, $tag); 202 } 203 204 205 public static function createEmpty($logicalTag = "") 206 { 207 if ($logicalTag !== "") { 208 return new TagAttributes([], $logicalTag); 209 } else { 210 return new TagAttributes(); 211 } 212 } 213 214 /** 215 * @param array $renderArray - an array of key value pair 216 * @param string $logicalTag - the logical tag for which this attribute will apply 217 * @return TagAttributes 218 */ 219 public static function createFromCallStackArray($renderArray, $logicalTag = null) 220 { 221 if (!is_array($renderArray)) { 222 LogUtility::msg("The renderArray variable passed is not an array ($renderArray)", LogUtility::LVL_MSG_ERROR); 223 $renderArray = TagAttributes::createEmpty($logicalTag); 224 } 225 return new TagAttributes($renderArray, $logicalTag); 226 } 227 228 229 /** 230 * For CSS a unit is mandatory (not for HTML or SVG attributes) 231 * @param $value 232 * @return string return a CSS property with pixel as unit if the unit is not specified 233 */ 234 public static function toQualifiedCssValue($value) 235 { 236 /** 237 * A length value may be also `fit-content` 238 * we just check that if there is only number, 239 * we add the pixel 240 * Same as {@link is_numeric()} ? 241 */ 242 if (is_numeric($value)) { 243 return $value . "px"; 244 } else { 245 return $value; 246 } 247 248 } 249 250 /** 251 * Function used to normalize the attribute name to the combostrap attribute name 252 * @param $name 253 * @return mixed|string 254 */ 255 public static function AttributeNameFromDokuwikiToCombo($name) 256 { 257 switch ($name) { 258 case "w": 259 return Dimension::WIDTH_KEY; 260 case "h": 261 return Dimension::HEIGHT_KEY; 262 default: 263 return $name; 264 } 265 } 266 267 public function addClassName($className) 268 { 269 270 $this->addComponentAttributeValue(self::CLASS_KEY, $className); 271 return $this; 272 273 } 274 275 public function getClass() 276 { 277 return $this->getValue(self::CLASS_KEY); 278 } 279 280 public function getStyle() 281 { 282 if (sizeof($this->styleDeclaration) != 0) { 283 return PluginUtility::array2InlineStyle($this->styleDeclaration); 284 } else { 285 /** 286 * null is needed to see if the attribute was set or not 287 * because an attribute may have the empty string 288 * Example: the wiki id of the root namespace 289 */ 290 return null; 291 } 292 293 } 294 295 /** 296 * Add an attribute with its value if the value is not empty 297 * @param $attributeName 298 * @param $attributeValue 299 */ 300 public function addComponentAttributeValue($attributeName, $attributeValue) 301 { 302 303 if (empty($attributeValue) && !is_bool($attributeValue)) { 304 LogUtility::msg("The value of the attribute ($attributeName) is empty. Use the nonEmpty function instead", LogUtility::LVL_MSG_WARNING, "support"); 305 } 306 307 $attLower = strtolower($attributeName); 308 if ($this->hasComponentAttribute($attLower)) { 309 $actual = $this->componentAttributesCaseInsensitive[$attLower]; 310 } 311 312 /** 313 * Type of data: list (class) or atomic (id) 314 */ 315 if ($attributeName === "class") { 316 if (!is_string($attributeValue)) { 317 LogUtility::msg("The value ($attributeValue) for the `class` attribute is not a string", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 318 } 319 /** 320 * It may be in the form "value1 value2" 321 */ 322 $newValues = StringUtility::explodeAndTrim($attributeValue, " "); 323 if (!empty($actual)) { 324 $actualValues = StringUtility::explodeAndTrim($actual, " "); 325 } else { 326 $actualValues = []; 327 } 328 $newValues = PluginUtility::mergeAttributes($newValues, $actualValues); 329 $this->componentAttributesCaseInsensitive[$attLower] = implode(" ", $newValues); 330 } else { 331 if (!empty($actual)) { 332 LogUtility::msg("The attribute ($attLower) stores an unique value and has already a value ($actual). to set another value ($attributeValue), use the `set` operation instead", LogUtility::LVL_MSG_ERROR, self::CANONICAL); 333 } 334 $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue; 335 } 336 337 338 } 339 340 public function setComponentAttributeValue($attributeName, $attributeValue) 341 { 342 $attLower = strtolower($attributeName); 343 $actualValue = $this->getValue($attributeName); 344 if ($actualValue === null || $actualValue !== TagAttributes::UN_SET) { 345 $this->componentAttributesCaseInsensitive[$attLower] = $attributeValue; 346 } 347 } 348 349 public function addComponentAttributeValueIfNotEmpty($attributeName, $attributeValue) 350 { 351 if (!empty($attributeValue)) { 352 $this->addComponentAttributeValue($attributeName, $attributeValue); 353 } 354 } 355 356 public function hasComponentAttribute($attributeName) 357 { 358 $isset = isset($this->componentAttributesCaseInsensitive[$attributeName]); 359 if ($isset === false) { 360 /** 361 * Edge effect 362 * if this is a boolean value and the first value, it may be stored in the type 363 */ 364 if (isset($this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY])) { 365 if ($attributeName == $this->componentAttributesCaseInsensitive[TagAttributes::TYPE_KEY]) { 366 return true; 367 } 368 } 369 } 370 return $isset; 371 } 372 373 /** 374 * To an HTML array in the form 375 * class => 'value1 value2', 376 * att => 'value1 value 2' 377 * For historic reason, data passed between the handle and the render 378 * can still be in this format 379 */ 380 public function toHtmlArray() 381 { 382 if (!$this->componentToHtmlAttributeProcessingWasDone) { 383 384 $this->componentToHtmlAttributeProcessingWasDone = true; 385 386 /** 387 * Following the rule 2 to encode the unknown value 388 * We encode the component attribute (ie not the HTML attribute because 389 * they may have already encoded value) 390 * https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#rule-2-attribute-encode-before-inserting-untrusted-data-into-html-common-attributes 391 */ 392 393 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 394 $this->escapeComponentAttribute($originalArray); 395 396 397 /** 398 * Width and height 399 */ 400 Dimension::processWidthAndHeight($this); 401 402 /** 403 * Process animation (onHover, onView) 404 */ 405 Hover::processOnHover($this); 406 Animation::processOnView($this); 407 408 409 /** 410 * Position and Stickiness 411 */ 412 Position::processStickiness($this); 413 Position::processPosition($this); 414 415 /** 416 * Block processing 417 * 418 * Float, align, spacing 419 */ 420 FloatAttribute::processFloat($this); 421 Align::processAlignAttributes($this); 422 Spacing::processSpacingAttributes($this); 423 Opacity::processOpacityAttribute($this); 424 Background::processBackgroundAttributes($this); 425 Shadow::process($this); 426 427 /** 428 * Process text attributes 429 */ 430 LineSpacing::processLineSpacingAttributes($this); 431 TextAlign::processTextAlign($this); 432 Boldness::processBoldnessAttribute($this); 433 FontSize::processFontSizeAttribute($this); 434 TextColor::processTextColorAttribute($this); 435 Underline::processUnderlineAttribute($this); 436 437 /** 438 * Process the style attributes if any 439 */ 440 PluginUtility::processStyle($this); 441 Toggle::processToggle($this); 442 443 444 /** 445 * Skin Attribute 446 */ 447 Skin::processSkinAttribute($this); 448 449 /** 450 * Lang 451 */ 452 Lang::processLangAttribute($this); 453 454 /** 455 * Transform 456 */ 457 if ($this->hasComponentAttribute(self::TRANSFORM)) { 458 $transformValue = $this->getValueAndRemove(self::TRANSFORM); 459 $this->addStyleDeclaration("transform", $transformValue); 460 } 461 462 /** 463 * Add the type class used for CSS styling 464 */ 465 StyleUtility::addStylingClass($this); 466 467 /** 468 * Add the style has html attribute 469 * before processing 470 */ 471 $this->addHtmlAttributeValueIfNotEmpty("style", $this->getStyle()); 472 473 /** 474 * Create a non-sorted temporary html attributes array 475 */ 476 $tempHtmlArray = $this->htmlAttributes; 477 478 /** 479 * copy the unknown component attributes 480 */ 481 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 482 foreach ($originalArray as $key => $value) { 483 484 // Null Value, not needed 485 if (is_null($value)) { 486 continue; 487 } 488 489 // No overwrite 490 if (isset($tempHtmlArray[$key])) { 491 continue; 492 } 493 494 // Reserved attribute 495 if (!in_array($key, self::RESERVED_ATTRIBUTES)) { 496 $tempHtmlArray[$key] = $value; 497 } 498 499 } 500 501 502 /** 503 * Sort by attribute 504 * https://datacadamia.com/web/html/attribute#order 505 */ 506 $sortedArray = array(); 507 $once = "once"; 508 $multiple = "multiple"; 509 $orderPatterns = [ 510 "class" => $once, 511 "id" => $once, 512 "name" => $once, 513 "data-.*" => $multiple, 514 "src.*" => $multiple, 515 "for" => $once, 516 "type" => $once, 517 "href" => $once, 518 "value" => $once, 519 "title" => $once, 520 "alt" => $once, 521 "role" => $once, 522 "aria-*" => $multiple]; 523 foreach ($orderPatterns as $pattern => $type) { 524 foreach ($tempHtmlArray as $name => $value) { 525 if (empty($value)) { 526 break; 527 } 528 $searchPattern = "^$pattern$"; 529 if (preg_match("/$searchPattern/", $name)) { 530 $sortedArray[$name] = $value; 531 unset($tempHtmlArray[$name]); 532 if ($type == $once) { 533 break; 534 } 535 } 536 } 537 } 538 foreach ($tempHtmlArray as $name => $value) { 539 540 if (!is_null($value)) { 541 /** 542 * 543 * Don't add a filter on the empty values 544 * 545 * The value of an HTML attribute may be empty 546 * Example the wiki id of the root namespace 547 * 548 * By default, {@link TagAttributes::addHtmlAttributeValue()} 549 * will not accept any value, it must be implicitly said with the 550 * {@link TagAttributes::addHtmlAttributeValue()} 551 * 552 */ 553 $sortedArray[$name] = $value; 554 } 555 556 } 557 $this->finalHtmlArray = $sortedArray; 558 559 } 560 561 562 return $this->finalHtmlArray; 563 564 } 565 566 /** 567 * HTML attribute are attributes 568 * that are not transformed to HTML 569 * (We make a difference between a high level attribute 570 * that we have in the written document set on a component 571 * @param $key 572 * @param $value 573 * @return TagAttributes 574 */ 575 public function addHtmlAttributeValue($key, $value) 576 { 577 if (blank($value)) { 578 LogUtility::msg("The value of the HTML attribute is blank for the key ($key) - Tag ($this->logicalTag). Use the empty function if the value can be empty", LogUtility::LVL_MSG_ERROR); 579 } 580 /** 581 * We encode all HTML attribute 582 * because `Unescaped '<' not allowed in attributes values` 583 * 584 * except for url that have another encoding 585 * (ie only the query parameters value should be encoded) 586 */ 587 $urlEncoding = ["href", "src", "data-src", "data-srcset"]; 588 if (!in_array($key, $urlEncoding)) { 589 /** 590 * htmlencode the value `true` as `1`, 591 * We transform it first as string, then 592 */ 593 $value = PluginUtility::htmlEncode(StringUtility::toString($value)); 594 } 595 $this->htmlAttributes[$key] = $value; 596 return $this; 597 } 598 599 600 public function addHtmlAttributeValueIfNotEmpty($key, $value) 601 { 602 if (!empty($value)) { 603 $this->addHtmlAttributeValue($key, $value); 604 } 605 } 606 607 /** 608 * @param $attributeName 609 * @param null $default 610 * @return string|array|null a HTML value in the form 'value1 value2...' 611 */ 612 public function getValue($attributeName, $default = null) 613 { 614 $attributeName = strtolower($attributeName); 615 if ($this->hasComponentAttribute($attributeName)) { 616 return $this->componentAttributesCaseInsensitive[$attributeName]; 617 } else { 618 return $default; 619 } 620 } 621 622 /** 623 * @return array - the storage format returned from the {@link SyntaxPlugin::handle()} method 624 */ 625 public function toInternalArray() 626 { 627 return $this->componentAttributesCaseInsensitive; 628 } 629 630 /** 631 * Get the value and remove it from the attributes 632 * @param $attributeName 633 * @param $default 634 * @return string|array|null 635 */ 636 public function getValueAndRemove($attributeName, $default = null) 637 { 638 $attributeName = strtolower($attributeName); 639 $value = $default; 640 if ($this->hasComponentAttribute($attributeName)) { 641 $value = $this->getValue($attributeName); 642 643 if (!in_array($attributeName, self::RESERVED_ATTRIBUTES)) { 644 /** 645 * Don't remove for instance the `type` 646 * because it may be used elsewhere 647 */ 648 unset($this->componentAttributesCaseInsensitive[$attributeName]); 649 } else { 650 LogUtility::msg("Internal: The attribute $attributeName is a reserved word and cannot be removed. Use the get function instead", LogUtility::LVL_MSG_WARNING, "support"); 651 } 652 653 } 654 return $value; 655 } 656 657 658 /** 659 * @return array - an array of key string and value of the component attributes 660 * This array is saved on the disk 661 */ 662 public function toCallStackArray() 663 { 664 $array = array(); 665 $originalArray = $this->componentAttributesCaseInsensitive->getOriginalArray(); 666 foreach ($originalArray as $key => $value) { 667 /** 668 * Only null value are not passed 669 * width can be zero, wiki-id can be the empty string (ie root namespace) 670 */ 671 if (!is_null($value)) { 672 $array[$key] = StringUtility::toString($value); 673 } 674 } 675 $style = $this->getStyle(); 676 if ($style != null) { 677 $array["style"] = $style; 678 } 679 return $array; 680 } 681 682 public 683 function getComponentAttributeValue($attributeName, $default = null) 684 { 685 $lowerAttribute = strtolower($attributeName); 686 $value = $default; 687 if ($this->hasComponentAttribute($lowerAttribute)) { 688 $value = $this->getValue($lowerAttribute); 689 } 690 return $value; 691 } 692 693 public 694 function addStyleDeclaration($property, $value) 695 { 696 ArrayUtility::addIfNotSet($this->styleDeclaration, $property, $value); 697 } 698 699 700 public 701 function hasStyleDeclaration($styleDeclaration) 702 { 703 return isset($this->styleDeclaration[$styleDeclaration]); 704 } 705 706 public 707 function getAndRemoveStyleDeclaration($styleDeclaration) 708 { 709 $styleValue = $this->styleDeclaration[$styleDeclaration]; 710 unset($this->styleDeclaration[$styleDeclaration]); 711 return $styleValue; 712 } 713 714 715 public 716 function toHTMLAttributeString() 717 { 718 719 $tagAttributeString = ""; 720 721 $htmlArray = $this->toHtmlArray(); 722 foreach ($htmlArray as $name => $value) { 723 724 /** 725 * Empty value are authorized 726 * null are just not set 727 */ 728 if (!is_null($value)) { 729 730 /** 731 * Unset attribute should not be added 732 */ 733 if ($value === TagAttributes::UN_SET) { 734 continue; 735 } 736 737 /** 738 * The condition is important 739 * because we may pass the javascript character `\n` in a `srcdoc` for javascript 740 * and the {@link StringUtility::toString()} will transform it as `\\n` 741 * making it unusable 742 */ 743 if (!is_string($value)) { 744 $stringValue = StringUtility::toString($value); 745 } else { 746 $stringValue = $value; 747 } 748 749 750 $tagAttributeString .= $name . '="' . $stringValue . '" '; 751 } 752 753 } 754 return trim($tagAttributeString); 755 756 757 } 758 759 public 760 function getComponentAttributes() 761 { 762 return $this->toCallStackArray(); 763 } 764 765 public 766 function removeComponentAttributeIfPresent($attributeName) 767 { 768 if ($this->hasComponentAttribute($attributeName)) { 769 unset($this->componentAttributesCaseInsensitive[$attributeName]); 770 } 771 772 } 773 774 public 775 function toHtmlEnterTag($htmlTag) 776 { 777 778 $enterTag = "<" . $htmlTag; 779 $attributeString = $this->toHTMLAttributeString(); 780 if (!empty($attributeString)) { 781 $enterTag .= " " . $attributeString; 782 } 783 /** 784 * Is it an open tag ? 785 */ 786 if (!$this->getValue(self::OPEN_TAG, false)) { 787 788 $enterTag .= ">"; 789 790 /** 791 * Do we have html after the tag is closed 792 */ 793 if (!empty($this->htmlAfterEnterTag)) { 794 $enterTag .= DOKU_LF . $this->htmlAfterEnterTag; 795 } 796 797 } 798 799 800 return $enterTag; 801 802 } 803 804 public 805 function getLogicalTag() 806 { 807 return $this->logicalTag; 808 } 809 810 public 811 function setLogicalTag($tag) 812 { 813 $this->logicalTag = $tag; 814 } 815 816 public 817 function removeComponentAttribute($attribute) 818 { 819 $lowerAtt = strtolower($attribute); 820 if (isset($this->componentAttributesCaseInsensitive[$lowerAtt])) { 821 $value = $this->componentAttributesCaseInsensitive[$lowerAtt]; 822 unset($this->componentAttributesCaseInsensitive[$lowerAtt]); 823 return $value; 824 } else { 825 /** 826 * Edge case, this is the first boolean attribute 827 * and may has been categorized as the type 828 */ 829 if (!$this->getType() == $lowerAtt) { 830 LogUtility::msg("Internal Error: The component attribute ($attribute) is not present. Use the ifPresent function, if you don't want this message", LogUtility::LVL_MSG_ERROR); 831 } 832 833 } 834 835 } 836 837 /** 838 * @param $html - an html that should be closed and added after the enter tag 839 */ 840 public 841 function addHtmlAfterEnterTag($html) 842 { 843 $this->htmlAfterEnterTag = $html . $this->htmlAfterEnterTag; 844 } 845 846 /** 847 * The mime of the HTTP request 848 * This is not the good place but yeah, 849 * this class has become the context class 850 * 851 * Mime make the difference for a svg to know if it's required as external resource (ie SVG) 852 * or as included in HTML page 853 * @param $mime 854 */ 855 public 856 function setMime($mime) 857 { 858 $this->mime = $mime; 859 } 860 861 /** 862 * @return string - the mime of the request 863 */ 864 public 865 function getMime() 866 { 867 return $this->mime; 868 } 869 870 public 871 function getType() 872 { 873 return $this->getValue(self::TYPE_KEY); 874 } 875 876 /** 877 * @param $attributeName 878 * @return ConditionalValue 879 */ 880 public 881 function getConditionalValueAndRemove($attributeName) 882 { 883 $value = $this->getConditionalValueAndRemove($attributeName); 884 return new ConditionalValue($value); 885 886 } 887 888 /** 889 * @param $attributeName 890 * @return false|string[] - an array of values 891 */ 892 public 893 function getValuesAndRemove($attributeName) 894 { 895 896 /** 897 * Trim 898 */ 899 $trim = trim($this->getValueAndRemove($attributeName)); 900 901 /** 902 * Replace all suite of space that have more than 2 characters 903 */ 904 $value = preg_replace("/\s{2,}/", " ", $trim); 905 return explode(" ", $value); 906 907 } 908 909 public 910 function setType($type) 911 { 912 $this->setComponentAttributeValue(TagAttributes::TYPE_KEY, $type); 913 } 914 915 /** 916 * Merging will add the values, no replace or overwrite 917 * @param $callStackArray 918 */ 919 public 920 function mergeWithCallStackArray($callStackArray) 921 { 922 foreach ($callStackArray as $key => $value) { 923 if ($this->hasComponentAttribute($key)) { 924 $this->addComponentAttributeValue($key, $value); 925 } else { 926 $this->setComponentAttributeValue($key, $value); 927 } 928 } 929 930 } 931 932 /** 933 * @param $string 934 */ 935 public 936 function removeAttributeIfPresent($string) 937 { 938 $this->removeComponentAttributeIfPresent($string); 939 $this->removeHTMLAttributeIfPresent($string); 940 941 } 942 943 private 944 function removeHTMLAttributeIfPresent($string) 945 { 946 $lowerAtt = strtolower($string); 947 if (isset($this->htmlAttributes[$lowerAtt])) { 948 unset($this->htmlAttributes[$lowerAtt]); 949 } 950 } 951 952 public 953 function getValueAndRemoveIfPresent($attribute, $default = null) 954 { 955 $value = $this->getValue($attribute, $default); 956 $this->removeAttributeIfPresent($attribute); 957 return $value; 958 } 959 960 public 961 function generateAndSetId() 962 { 963 self::$counter += 1; 964 $id = self::$counter; 965 $logicalTag = $this->getLogicalTag(); 966 if (!empty($logicalTag)) { 967 $id = $this->logicalTag . $id; 968 } 969 $this->setComponentAttributeValue("id", $id); 970 return $id; 971 } 972 973 /** 974 * 975 * @param $markiTag 976 * @return string - the marki tag made of logical attribute 977 * There is no processing to transform it to an HTML tag 978 */ 979 public 980 function toMarkiEnterTag($markiTag) 981 { 982 $enterTag = "<" . $markiTag; 983 984 $attributeString = ""; 985 foreach ($this->getComponentAttributes() as $key => $value) { 986 $attributeString .= "$key=\"$value\" "; 987 } 988 $attributeString = trim($attributeString); 989 990 if (!empty($attributeString)) { 991 $enterTag .= " " . $attributeString; 992 } 993 $enterTag .= ">"; 994 return $enterTag; 995 996 } 997 998 /** 999 * @param string $key add an html attribute with the empty string 1000 */ 1001 public 1002 function addEmptyHtmlAttributeValue($key) 1003 { 1004 1005 $this->htmlAttributes[$key] = ''; 1006 return $this; 1007 1008 } 1009 1010 public 1011 function addEmptyComponentAttributeValue($attribute) 1012 { 1013 $this->componentAttributesCaseInsensitive[$attribute] = ""; 1014 } 1015 1016 /** 1017 * @param $attribute 1018 * @param null $default 1019 * @return mixed 1020 */ 1021 public 1022 function getBooleanValueAndRemove($attribute, $default = null) 1023 { 1024 $value = $this->getValueAndRemove($attribute); 1025 if ($value == null) { 1026 return $default; 1027 } else { 1028 return filter_var($value, FILTER_VALIDATE_BOOLEAN); 1029 } 1030 } 1031 1032 public 1033 function hasAttribute($attribute) 1034 { 1035 $hasAttribute = $this->hasComponentAttribute($attribute); 1036 if ($hasAttribute === true) { 1037 return true; 1038 } else { 1039 return $this->hasHtmlAttribute($attribute); 1040 } 1041 } 1042 1043 private 1044 function hasHtmlAttribute($attribute) 1045 { 1046 return isset($this->htmlAttributes[$attribute]); 1047 } 1048 1049 /** 1050 * Component attribute are entered by the user and should be encoded 1051 * @param array $arrayToEscape 1052 * @param null $subKey 1053 */ 1054 private 1055 function escapeComponentAttribute(array $arrayToEscape, $subKey = null) 1056 { 1057 1058 foreach ($arrayToEscape as $name => $value) { 1059 1060 $encodedName = PluginUtility::htmlEncode($name); 1061 1062 /** 1063 * Boolean does not need to be encoded 1064 */ 1065 if (is_bool($value)) { 1066 if ($subKey == null) { 1067 $this->componentAttributesCaseInsensitive[$encodedName] = $value; 1068 } else { 1069 $this->componentAttributesCaseInsensitive[$subKey][$encodedName] = $value; 1070 } 1071 continue; 1072 } 1073 1074 if (is_array($value)) { 1075 $this->escapeComponentAttribute($value, $encodedName); 1076 } else { 1077 1078 $value = PluginUtility::htmlEncode($value); 1079 if ($subKey == null) { 1080 $this->componentAttributesCaseInsensitive[$encodedName] = $value; 1081 } else { 1082 $this->componentAttributesCaseInsensitive[$subKey][$encodedName] = $value; 1083 } 1084 } 1085 } 1086 } 1087 1088 1089} 1090