1<?php 2 3namespace Sabre\VObject\Property\ICalendar; 4 5use Sabre\VObject\Property; 6use Sabre\Xml; 7 8/** 9 * Recur property. 10 * 11 * This object represents RECUR properties. 12 * These values are just used for RRULE and the now deprecated EXRULE. 13 * 14 * The RRULE property may look something like this: 15 * 16 * RRULE:FREQ=MONTHLY;BYDAY=1,2,3;BYHOUR=5. 17 * 18 * This property exposes this as a key=>value array that is accessible using 19 * getParts, and may be set using setParts. 20 * 21 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 22 * @author Evert Pot (http://evertpot.com/) 23 * @license http://sabre.io/license/ Modified BSD License 24 */ 25class Recur extends Property 26{ 27 /** 28 * Updates the current value. 29 * 30 * This may be either a single, or multiple strings in an array. 31 * 32 * @param string|array $value 33 */ 34 public function setValue($value) 35 { 36 // If we're getting the data from json, we'll be receiving an object 37 if ($value instanceof \StdClass) { 38 $value = (array) $value; 39 } 40 41 if (is_array($value)) { 42 $newVal = []; 43 foreach ($value as $k => $v) { 44 if (is_string($v)) { 45 $v = strtoupper($v); 46 47 // The value had multiple sub-values 48 if (false !== strpos($v, ',')) { 49 $v = explode(',', $v); 50 } 51 if (0 === strcmp($k, 'until')) { 52 $v = strtr($v, [':' => '', '-' => '']); 53 } 54 } elseif (is_array($v)) { 55 $v = array_map('strtoupper', $v); 56 } 57 58 $newVal[strtoupper($k)] = $v; 59 } 60 $this->value = $newVal; 61 } elseif (is_string($value)) { 62 $this->value = self::stringToArray($value); 63 } else { 64 throw new \InvalidArgumentException('You must either pass a string, or a key=>value array'); 65 } 66 } 67 68 /** 69 * Returns the current value. 70 * 71 * This method will always return a singular value. If this was a 72 * multi-value object, some decision will be made first on how to represent 73 * it as a string. 74 * 75 * To get the correct multi-value version, use getParts. 76 * 77 * @return string 78 */ 79 public function getValue() 80 { 81 $out = []; 82 foreach ($this->value as $key => $value) { 83 $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value); 84 } 85 86 return strtoupper(implode(';', $out)); 87 } 88 89 /** 90 * Sets a multi-valued property. 91 * 92 * @param array $parts 93 */ 94 public function setParts(array $parts) 95 { 96 $this->setValue($parts); 97 } 98 99 /** 100 * Returns a multi-valued property. 101 * 102 * This method always returns an array, if there was only a single value, 103 * it will still be wrapped in an array. 104 * 105 * @return array 106 */ 107 public function getParts() 108 { 109 return $this->value; 110 } 111 112 /** 113 * Sets a raw value coming from a mimedir (iCalendar/vCard) file. 114 * 115 * This has been 'unfolded', so only 1 line will be passed. Unescaping is 116 * not yet done, but parameters are not included. 117 * 118 * @param string $val 119 */ 120 public function setRawMimeDirValue($val) 121 { 122 $this->setValue($val); 123 } 124 125 /** 126 * Returns a raw mime-dir representation of the value. 127 * 128 * @return string 129 */ 130 public function getRawMimeDirValue() 131 { 132 return $this->getValue(); 133 } 134 135 /** 136 * Returns the type of value. 137 * 138 * This corresponds to the VALUE= parameter. Every property also has a 139 * 'default' valueType. 140 * 141 * @return string 142 */ 143 public function getValueType() 144 { 145 return 'RECUR'; 146 } 147 148 /** 149 * Returns the value, in the format it should be encoded for json. 150 * 151 * This method must always return an array. 152 * 153 * @return array 154 */ 155 public function getJsonValue() 156 { 157 $values = []; 158 foreach ($this->getParts() as $k => $v) { 159 if (0 === strcmp($k, 'UNTIL')) { 160 $date = new DateTime($this->root, null, $v); 161 $values[strtolower($k)] = $date->getJsonValue()[0]; 162 } elseif (0 === strcmp($k, 'COUNT')) { 163 $values[strtolower($k)] = intval($v); 164 } else { 165 $values[strtolower($k)] = $v; 166 } 167 } 168 169 return [$values]; 170 } 171 172 /** 173 * This method serializes only the value of a property. This is used to 174 * create xCard or xCal documents. 175 * 176 * @param Xml\Writer $writer XML writer 177 */ 178 protected function xmlSerializeValue(Xml\Writer $writer) 179 { 180 $valueType = strtolower($this->getValueType()); 181 182 foreach ($this->getJsonValue() as $value) { 183 $writer->writeElement($valueType, $value); 184 } 185 } 186 187 /** 188 * Parses an RRULE value string, and turns it into a struct-ish array. 189 * 190 * @param string $value 191 * 192 * @return array 193 */ 194 public static function stringToArray($value) 195 { 196 $value = strtoupper($value); 197 $newValue = []; 198 foreach (explode(';', $value) as $part) { 199 // Skipping empty parts. 200 if (empty($part)) { 201 continue; 202 } 203 list($partName, $partValue) = explode('=', $part); 204 205 // The value itself had multiple values.. 206 if (false !== strpos($partValue, ',')) { 207 $partValue = explode(',', $partValue); 208 } 209 $newValue[$partName] = $partValue; 210 } 211 212 return $newValue; 213 } 214 215 /** 216 * Validates the node for correctness. 217 * 218 * The following options are supported: 219 * Node::REPAIR - May attempt to automatically repair the problem. 220 * 221 * This method returns an array with detected problems. 222 * Every element has the following properties: 223 * 224 * * level - problem level. 225 * * message - A human-readable string describing the issue. 226 * * node - A reference to the problematic node. 227 * 228 * The level means: 229 * 1 - The issue was repaired (only happens if REPAIR was turned on) 230 * 2 - An inconsequential issue 231 * 3 - A severe issue. 232 * 233 * @param int $options 234 * 235 * @return array 236 */ 237 public function validate($options = 0) 238 { 239 $repair = ($options & self::REPAIR); 240 241 $warnings = parent::validate($options); 242 $values = $this->getParts(); 243 244 foreach ($values as $key => $value) { 245 if ('' === $value) { 246 $warnings[] = [ 247 'level' => $repair ? 1 : 3, 248 'message' => 'Invalid value for '.$key.' in '.$this->name, 249 'node' => $this, 250 ]; 251 if ($repair) { 252 unset($values[$key]); 253 } 254 } elseif ('BYMONTH' == $key) { 255 $byMonth = (array) $value; 256 foreach ($byMonth as $i => $v) { 257 if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) { 258 $warnings[] = [ 259 'level' => $repair ? 1 : 3, 260 'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!', 261 'node' => $this, 262 ]; 263 if ($repair) { 264 if (is_array($value)) { 265 unset($values[$key][$i]); 266 } else { 267 unset($values[$key]); 268 } 269 } 270 } 271 } 272 // if there is no valid entry left, remove the whole value 273 if (is_array($value) && empty($values[$key])) { 274 unset($values[$key]); 275 } 276 } elseif ('BYWEEKNO' == $key) { 277 $byWeekNo = (array) $value; 278 foreach ($byWeekNo as $i => $v) { 279 if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) { 280 $warnings[] = [ 281 'level' => $repair ? 1 : 3, 282 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', 283 'node' => $this, 284 ]; 285 if ($repair) { 286 if (is_array($value)) { 287 unset($values[$key][$i]); 288 } else { 289 unset($values[$key]); 290 } 291 } 292 } 293 } 294 // if there is no valid entry left, remove the whole value 295 if (is_array($value) && empty($values[$key])) { 296 unset($values[$key]); 297 } 298 } elseif ('BYYEARDAY' == $key) { 299 $byYearDay = (array) $value; 300 foreach ($byYearDay as $i => $v) { 301 if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) { 302 $warnings[] = [ 303 'level' => $repair ? 1 : 3, 304 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', 305 'node' => $this, 306 ]; 307 if ($repair) { 308 if (is_array($value)) { 309 unset($values[$key][$i]); 310 } else { 311 unset($values[$key]); 312 } 313 } 314 } 315 } 316 // if there is no valid entry left, remove the whole value 317 if (is_array($value) && empty($values[$key])) { 318 unset($values[$key]); 319 } 320 } 321 } 322 if (!isset($values['FREQ'])) { 323 $warnings[] = [ 324 'level' => $repair ? 1 : 3, 325 'message' => 'FREQ is required in '.$this->name, 326 'node' => $this, 327 ]; 328 if ($repair) { 329 $this->parent->remove($this); 330 } 331 } 332 if ($repair) { 333 $this->setValue($values); 334 } 335 336 return $warnings; 337 } 338} 339