vendor/sabre/vobject/lib/Component.php line 72

Open in your IDE?
  1. <?php
  2. namespace Sabre\VObject;
  3. use Sabre\Xml;
  4. /**
  5.  * Component.
  6.  *
  7.  * A component represents a group of properties, such as VCALENDAR, VEVENT, or
  8.  * VCARD.
  9.  *
  10.  * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  11.  * @author Evert Pot (http://evertpot.com/)
  12.  * @license http://sabre.io/license/ Modified BSD License
  13.  */
  14. class Component extends Node
  15. {
  16.     /**
  17.      * Component name.
  18.      *
  19.      * This will contain a string such as VEVENT, VTODO, VCALENDAR, VCARD.
  20.      *
  21.      * @var string
  22.      */
  23.     public $name;
  24.     /**
  25.      * A list of properties and/or sub-components.
  26.      *
  27.      * @var array<string, Component|Property>
  28.      */
  29.     protected $children = [];
  30.     /**
  31.      * Creates a new component.
  32.      *
  33.      * You can specify the children either in key=>value syntax, in which case
  34.      * properties will automatically be created, or you can just pass a list of
  35.      * Component and Property object.
  36.      *
  37.      * By default, a set of sensible values will be added to the component. For
  38.      * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
  39.      * ensure that this does not happen, set $defaults to false.
  40.      *
  41.      * @param string|null $name     such as VCALENDAR, VEVENT
  42.      * @param bool        $defaults
  43.      */
  44.     public function __construct(Document $root$name, array $children = [], $defaults true)
  45.     {
  46.         $this->name = isset($name) ? strtoupper($name) : '';
  47.         $this->root $root;
  48.         if ($defaults) {
  49.             // This is a terribly convoluted way to do this, but this ensures
  50.             // that the order of properties as they are specified in both
  51.             // defaults and the childrens list, are inserted in the object in a
  52.             // natural way.
  53.             $list $this->getDefaults();
  54.             $nodes = [];
  55.             foreach ($children as $key => $value) {
  56.                 if ($value instanceof Node) {
  57.                     if (isset($list[$value->name])) {
  58.                         unset($list[$value->name]);
  59.                     }
  60.                     $nodes[] = $value;
  61.                 } else {
  62.                     $list[$key] = $value;
  63.                 }
  64.             }
  65.             foreach ($list as $key => $value) {
  66.                 $this->add($key$value);
  67.             }
  68.             foreach ($nodes as $node) {
  69.                 $this->add($node);
  70.             }
  71.         } else {
  72.             foreach ($children as $k => $child) {
  73.                 if ($child instanceof Node) {
  74.                     // Component or Property
  75.                     $this->add($child);
  76.                 } else {
  77.                     // Property key=>value
  78.                     $this->add($k$child);
  79.                 }
  80.             }
  81.         }
  82.     }
  83.     /**
  84.      * Adds a new property or component, and returns the new item.
  85.      *
  86.      * This method has 3 possible signatures:
  87.      *
  88.      * add(Component $comp) // Adds a new component
  89.      * add(Property $prop)  // Adds a new property
  90.      * add($name, $value, array $parameters = []) // Adds a new property
  91.      * add($name, array $children = []) // Adds a new component
  92.      * by name.
  93.      *
  94.      * @return Node
  95.      */
  96.     public function add()
  97.     {
  98.         $arguments func_get_args();
  99.         if ($arguments[0] instanceof Node) {
  100.             if (isset($arguments[1])) {
  101.                 throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
  102.             }
  103.             $arguments[0]->parent $this;
  104.             $newNode $arguments[0];
  105.         } elseif (is_string($arguments[0])) {
  106.             $newNode call_user_func_array([$this->root'create'], $arguments);
  107.         } else {
  108.             throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
  109.         }
  110.         $name $newNode->name;
  111.         if (isset($this->children[$name])) {
  112.             $this->children[$name][] = $newNode;
  113.         } else {
  114.             $this->children[$name] = [$newNode];
  115.         }
  116.         return $newNode;
  117.     }
  118.     /**
  119.      * This method removes a component or property from this component.
  120.      *
  121.      * You can either specify the item by name (like DTSTART), in which case
  122.      * all properties/components with that name will be removed, or you can
  123.      * pass an instance of a property or component, in which case only that
  124.      * exact item will be removed.
  125.      *
  126.      * @param string|Property|Component $item
  127.      */
  128.     public function remove($item)
  129.     {
  130.         if (is_string($item)) {
  131.             // If there's no dot in the name, it's an exact property name and
  132.             // we can just wipe out all those properties.
  133.             //
  134.             if (false === strpos($item'.')) {
  135.                 unset($this->children[strtoupper($item)]);
  136.                 return;
  137.             }
  138.             // If there was a dot, we need to ask select() to help us out and
  139.             // then we just call remove recursively.
  140.             foreach ($this->select($item) as $child) {
  141.                 $this->remove($child);
  142.             }
  143.         } else {
  144.             foreach ($this->select($item->name) as $k => $child) {
  145.                 if ($child === $item) {
  146.                     unset($this->children[$item->name][$k]);
  147.                     return;
  148.                 }
  149.             }
  150.             throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component');
  151.         }
  152.     }
  153.     /**
  154.      * Returns a flat list of all the properties and components in this
  155.      * component.
  156.      *
  157.      * @return array
  158.      */
  159.     public function children()
  160.     {
  161.         $result = [];
  162.         foreach ($this->children as $childGroup) {
  163.             $result array_merge($result$childGroup);
  164.         }
  165.         return $result;
  166.     }
  167.     /**
  168.      * This method only returns a list of sub-components. Properties are
  169.      * ignored.
  170.      *
  171.      * @return array
  172.      */
  173.     public function getComponents()
  174.     {
  175.         $result = [];
  176.         foreach ($this->children as $childGroup) {
  177.             foreach ($childGroup as $child) {
  178.                 if ($child instanceof self) {
  179.                     $result[] = $child;
  180.                 }
  181.             }
  182.         }
  183.         return $result;
  184.     }
  185.     /**
  186.      * Returns an array with elements that match the specified name.
  187.      *
  188.      * This function is also aware of MIME-Directory groups (as they appear in
  189.      * vcards). This means that if a property is grouped as "HOME.EMAIL", it
  190.      * will also be returned when searching for just "EMAIL". If you want to
  191.      * search for a property in a specific group, you can select on the entire
  192.      * string ("HOME.EMAIL"). If you want to search on a specific property that
  193.      * has not been assigned a group, specify ".EMAIL".
  194.      *
  195.      * @param string $name
  196.      *
  197.      * @return array
  198.      */
  199.     public function select($name)
  200.     {
  201.         $group null;
  202.         $name strtoupper($name);
  203.         if (false !== strpos($name'.')) {
  204.             list($group$name) = explode('.'$name2);
  205.         }
  206.         if ('' === $name) {
  207.             $name null;
  208.         }
  209.         if (!is_null($name)) {
  210.             $result = isset($this->children[$name]) ? $this->children[$name] : [];
  211.             if (is_null($group)) {
  212.                 return $result;
  213.             } else {
  214.                 // If we have a group filter as well, we need to narrow it down
  215.                 // more.
  216.                 return array_filter(
  217.                     $result,
  218.                     function ($child) use ($group) {
  219.                         return $child instanceof Property && (null !== $child->group strtoupper($child->group) : '') === $group;
  220.                     }
  221.                 );
  222.             }
  223.         }
  224.         // If we got to this point, it means there was no 'name' specified for
  225.         // searching, implying that this is a group-only search.
  226.         $result = [];
  227.         foreach ($this->children as $childGroup) {
  228.             foreach ($childGroup as $child) {
  229.                 if ($child instanceof Property && (null !== $child->group strtoupper($child->group) : '') === $group) {
  230.                     $result[] = $child;
  231.                 }
  232.             }
  233.         }
  234.         return $result;
  235.     }
  236.     /**
  237.      * Turns the object back into a serialized blob.
  238.      *
  239.      * @return string
  240.      */
  241.     public function serialize()
  242.     {
  243.         $str 'BEGIN:'.$this->name."\r\n";
  244.         /**
  245.          * Gives a component a 'score' for sorting purposes.
  246.          *
  247.          * This is solely used by the childrenSort method.
  248.          *
  249.          * A higher score means the item will be lower in the list.
  250.          * To avoid score collisions, each "score category" has a reasonable
  251.          * space to accommodate elements. The $key is added to the $score to
  252.          * preserve the original relative order of elements.
  253.          *
  254.          * @param int   $key
  255.          * @param array $array
  256.          *
  257.          * @return int
  258.          */
  259.         $sortScore = function ($key$array) {
  260.             if ($array[$key] instanceof Component) {
  261.                 // We want to encode VTIMEZONE first, this is a personal
  262.                 // preference.
  263.                 if ('VTIMEZONE' === $array[$key]->name) {
  264.                     $score 300000000;
  265.                     return $score $key;
  266.                 } else {
  267.                     $score 400000000;
  268.                     return $score $key;
  269.                 }
  270.             } else {
  271.                 // Properties get encoded first
  272.                 // VCARD version 4.0 wants the VERSION property to appear first
  273.                 if ($array[$key] instanceof Property) {
  274.                     if ('VERSION' === $array[$key]->name) {
  275.                         $score 100000000;
  276.                         return $score $key;
  277.                     } else {
  278.                         // All other properties
  279.                         $score 200000000;
  280.                         return $score $key;
  281.                     }
  282.                 }
  283.             }
  284.         };
  285.         $children $this->children();
  286.         $tmp $children;
  287.         uksort(
  288.             $children,
  289.             function ($a$b) use ($sortScore$tmp) {
  290.                 $sA $sortScore($a$tmp);
  291.                 $sB $sortScore($b$tmp);
  292.                 return $sA $sB;
  293.             }
  294.         );
  295.         foreach ($children as $child) {
  296.             $str .= $child->serialize();
  297.         }
  298.         $str .= 'END:'.$this->name."\r\n";
  299.         return $str;
  300.     }
  301.     /**
  302.      * This method returns an array, with the representation as it should be
  303.      * encoded in JSON. This is used to create jCard or jCal documents.
  304.      *
  305.      * @return array
  306.      */
  307.     #[\ReturnTypeWillChange]
  308.     public function jsonSerialize()
  309.     {
  310.         $components = [];
  311.         $properties = [];
  312.         foreach ($this->children as $childGroup) {
  313.             foreach ($childGroup as $child) {
  314.                 if ($child instanceof self) {
  315.                     $components[] = $child->jsonSerialize();
  316.                 } else {
  317.                     $properties[] = $child->jsonSerialize();
  318.                 }
  319.             }
  320.         }
  321.         return [
  322.             strtolower($this->name),
  323.             $properties,
  324.             $components,
  325.         ];
  326.     }
  327.     /**
  328.      * This method serializes the data into XML. This is used to create xCard or
  329.      * xCal documents.
  330.      *
  331.      * @param Xml\Writer $writer XML writer
  332.      */
  333.     public function xmlSerialize(Xml\Writer $writer): void
  334.     {
  335.         $components = [];
  336.         $properties = [];
  337.         foreach ($this->children as $childGroup) {
  338.             foreach ($childGroup as $child) {
  339.                 if ($child instanceof self) {
  340.                     $components[] = $child;
  341.                 } else {
  342.                     $properties[] = $child;
  343.                 }
  344.             }
  345.         }
  346.         $writer->startElement(strtolower($this->name));
  347.         if (!empty($properties)) {
  348.             $writer->startElement('properties');
  349.             foreach ($properties as $property) {
  350.                 $property->xmlSerialize($writer);
  351.             }
  352.             $writer->endElement();
  353.         }
  354.         if (!empty($components)) {
  355.             $writer->startElement('components');
  356.             foreach ($components as $component) {
  357.                 $component->xmlSerialize($writer);
  358.             }
  359.             $writer->endElement();
  360.         }
  361.         $writer->endElement();
  362.     }
  363.     /**
  364.      * This method should return a list of default property values.
  365.      *
  366.      * @return array
  367.      */
  368.     protected function getDefaults()
  369.     {
  370.         return [];
  371.     }
  372.     /* Magic property accessors {{{ */
  373.     /**
  374.      * Using 'get' you will either get a property or component.
  375.      *
  376.      * If there were no child-elements found with the specified name,
  377.      * null is returned.
  378.      *
  379.      * To use this, this may look something like this:
  380.      *
  381.      * $event = $calendar->VEVENT;
  382.      *
  383.      * @param string $name
  384.      *
  385.      * @return Property|null
  386.      */
  387.     public function __get($name)
  388.     {
  389.         if ('children' === $name) {
  390.             throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead');
  391.         }
  392.         $matches $this->select($name);
  393.         if (=== count($matches)) {
  394.             return;
  395.         } else {
  396.             $firstMatch current($matches);
  397.             /* @var $firstMatch Property */
  398.             $firstMatch->setIterator(new ElementList(array_values($matches)));
  399.             return $firstMatch;
  400.         }
  401.     }
  402.     /**
  403.      * This method checks if a sub-element with the specified name exists.
  404.      *
  405.      * @param string $name
  406.      *
  407.      * @return bool
  408.      */
  409.     public function __isset($name)
  410.     {
  411.         $matches $this->select($name);
  412.         return count($matches) > 0;
  413.     }
  414.     /**
  415.      * Using the setter method you can add properties or subcomponents.
  416.      *
  417.      * You can either pass a Component, Property
  418.      * object, or a string to automatically create a Property.
  419.      *
  420.      * If the item already exists, it will be removed. If you want to add
  421.      * a new item with the same name, always use the add() method.
  422.      *
  423.      * @param string $name
  424.      * @param mixed  $value
  425.      */
  426.     public function __set($name$value)
  427.     {
  428.         $name strtoupper($name);
  429.         $this->remove($name);
  430.         if ($value instanceof self || $value instanceof Property) {
  431.             $this->add($value);
  432.         } else {
  433.             $this->add($name$value);
  434.         }
  435.     }
  436.     /**
  437.      * Removes all properties and components within this component with the
  438.      * specified name.
  439.      *
  440.      * @param string $name
  441.      */
  442.     public function __unset($name)
  443.     {
  444.         $this->remove($name);
  445.     }
  446.     /* }}} */
  447.     /**
  448.      * This method is automatically called when the object is cloned.
  449.      * Specifically, this will ensure all child elements are also cloned.
  450.      */
  451.     public function __clone()
  452.     {
  453.         foreach ($this->children as $childName => $childGroup) {
  454.             foreach ($childGroup as $key => $child) {
  455.                 $clonedChild = clone $child;
  456.                 $clonedChild->parent $this;
  457.                 $clonedChild->root $this->root;
  458.                 $this->children[$childName][$key] = $clonedChild;
  459.             }
  460.         }
  461.     }
  462.     /**
  463.      * A simple list of validation rules.
  464.      *
  465.      * This is simply a list of properties, and how many times they either
  466.      * must or must not appear.
  467.      *
  468.      * Possible values per property:
  469.      *   * 0 - Must not appear.
  470.      *   * 1 - Must appear exactly once.
  471.      *   * + - Must appear at least once.
  472.      *   * * - Can appear any number of times.
  473.      *   * ? - May appear, but not more than once.
  474.      *
  475.      * It is also possible to specify defaults and severity levels for
  476.      * violating the rule.
  477.      *
  478.      * See the VEVENT implementation for getValidationRules for a more complex
  479.      * example.
  480.      *
  481.      * @var array
  482.      */
  483.     public function getValidationRules()
  484.     {
  485.         return [];
  486.     }
  487.     /**
  488.      * Validates the node for correctness.
  489.      *
  490.      * The following options are supported:
  491.      *   Node::REPAIR - May attempt to automatically repair the problem.
  492.      *   Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes.
  493.      *   Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes.
  494.      *
  495.      * This method returns an array with detected problems.
  496.      * Every element has the following properties:
  497.      *
  498.      *  * level - problem level.
  499.      *  * message - A human-readable string describing the issue.
  500.      *  * node - A reference to the problematic node.
  501.      *
  502.      * The level means:
  503.      *   1 - The issue was repaired (only happens if REPAIR was turned on).
  504.      *   2 - A warning.
  505.      *   3 - An error.
  506.      *
  507.      * @param int $options
  508.      *
  509.      * @return array
  510.      */
  511.     public function validate($options 0)
  512.     {
  513.         $rules $this->getValidationRules();
  514.         $defaults $this->getDefaults();
  515.         $propertyCounters = [];
  516.         $messages = [];
  517.         foreach ($this->children() as $child) {
  518.             $name strtoupper($child->name);
  519.             if (!isset($propertyCounters[$name])) {
  520.                 $propertyCounters[$name] = 1;
  521.             } else {
  522.                 ++$propertyCounters[$name];
  523.             }
  524.             $messages array_merge($messages$child->validate($options));
  525.         }
  526.         foreach ($rules as $propName => $rule) {
  527.             switch ($rule) {
  528.                 case '0':
  529.                     if (isset($propertyCounters[$propName])) {
  530.                         $messages[] = [
  531.                             'level' => 3,
  532.                             'message' => $propName.' MUST NOT appear in a '.$this->name.' component',
  533.                             'node' => $this,
  534.                         ];
  535.                     }
  536.                     break;
  537.                 case '1':
  538.                     if (!isset($propertyCounters[$propName]) || !== $propertyCounters[$propName]) {
  539.                         $repaired false;
  540.                         if ($options self::REPAIR && isset($defaults[$propName])) {
  541.                             $this->add($propName$defaults[$propName]);
  542.                             $repaired true;
  543.                         }
  544.                         $messages[] = [
  545.                             'level' => $repaired 3,
  546.                             'message' => $propName.' MUST appear exactly once in a '.$this->name.' component',
  547.                             'node' => $this,
  548.                         ];
  549.                     }
  550.                     break;
  551.                 case '+':
  552.                     if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) {
  553.                         $messages[] = [
  554.                             'level' => 3,
  555.                             'message' => $propName.' MUST appear at least once in a '.$this->name.' component',
  556.                             'node' => $this,
  557.                         ];
  558.                     }
  559.                     break;
  560.                 case '*':
  561.                     break;
  562.                 case '?':
  563.                     if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) {
  564.                         $level 3;
  565.                         // We try to repair the same property appearing multiple times with the exact same value
  566.                         // by removing the duplicates and keeping only one property
  567.                         if ($options self::REPAIR) {
  568.                             $properties array_unique($this->select($propName), SORT_REGULAR);
  569.                             if (=== count($properties)) {
  570.                                 $this->remove($propName);
  571.                                 $this->add($properties[0]);
  572.                                 $level 1;
  573.                             }
  574.                         }
  575.                         $messages[] = [
  576.                             'level' => $level,
  577.                             'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component',
  578.                             'node' => $this,
  579.                         ];
  580.                     }
  581.                     break;
  582.             }
  583.         }
  584.         return $messages;
  585.     }
  586.     /**
  587.      * Call this method on a document if you're done using it.
  588.      *
  589.      * It's intended to remove all circular references, so PHP can easily clean
  590.      * it up.
  591.      */
  592.     public function destroy()
  593.     {
  594.         parent::destroy();
  595.         foreach ($this->children as $childGroup) {
  596.             foreach ($childGroup as $child) {
  597.                 $child->destroy();
  598.             }
  599.         }
  600.         $this->children = [];
  601.     }
  602. }