Text.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. <?php
  2. namespace Sabre\VObject\Property;
  3. use Sabre\VObject\Component;
  4. use Sabre\VObject\Document;
  5. use Sabre\VObject\Parser\MimeDir;
  6. use Sabre\VObject\Property;
  7. use Sabre\Xml;
  8. /**
  9. * Text property.
  10. *
  11. * This object represents TEXT values.
  12. *
  13. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  14. * @author Evert Pot (http://evertpot.com/)
  15. * @license http://sabre.io/license/ Modified BSD License
  16. */
  17. class Text extends Property
  18. {
  19. /**
  20. * In case this is a multi-value property. This string will be used as a
  21. * delimiter.
  22. *
  23. * @var string
  24. */
  25. public $delimiter = ',';
  26. /**
  27. * List of properties that are considered 'structured'.
  28. *
  29. * @var array
  30. */
  31. protected $structuredValues = [
  32. // vCard
  33. 'N',
  34. 'ADR',
  35. 'ORG',
  36. 'GENDER',
  37. 'CLIENTPIDMAP',
  38. // iCalendar
  39. 'REQUEST-STATUS',
  40. ];
  41. /**
  42. * Some text components have a minimum number of components.
  43. *
  44. * N must for instance be represented as 5 components, separated by ;, even
  45. * if the last few components are unused.
  46. *
  47. * @var array
  48. */
  49. protected $minimumPropertyValues = [
  50. 'N' => 5,
  51. 'ADR' => 7,
  52. ];
  53. /**
  54. * Creates the property.
  55. *
  56. * You can specify the parameters either in key=>value syntax, in which case
  57. * parameters will automatically be created, or you can just pass a list of
  58. * Parameter objects.
  59. *
  60. * @param Component $root The root document
  61. * @param string $name
  62. * @param string|array|null $value
  63. * @param array $parameters List of parameters
  64. * @param string $group The vcard property group
  65. */
  66. public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null)
  67. {
  68. // There's two types of multi-valued text properties:
  69. // 1. multivalue properties.
  70. // 2. structured value properties
  71. //
  72. // The former is always separated by a comma, the latter by semi-colon.
  73. if (in_array($name, $this->structuredValues)) {
  74. $this->delimiter = ';';
  75. }
  76. parent::__construct($root, $name, $value, $parameters, $group);
  77. }
  78. /**
  79. * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
  80. *
  81. * This has been 'unfolded', so only 1 line will be passed. Unescaping is
  82. * not yet done, but parameters are not included.
  83. *
  84. * @param string $val
  85. */
  86. public function setRawMimeDirValue($val)
  87. {
  88. $this->setValue(MimeDir::unescapeValue($val, $this->delimiter));
  89. }
  90. /**
  91. * Sets the value as a quoted-printable encoded string.
  92. *
  93. * @param string $val
  94. */
  95. public function setQuotedPrintableValue($val)
  96. {
  97. $val = quoted_printable_decode($val);
  98. // Quoted printable only appears in vCard 2.1, and the only character
  99. // that may be escaped there is ;. So we are simply splitting on just
  100. // that.
  101. //
  102. // We also don't have to unescape \\, so all we need to look for is a ;
  103. // that's not preceded with a \.
  104. $regex = '# (?<!\\\\) ; #x';
  105. $matches = preg_split($regex, $val);
  106. $this->setValue($matches);
  107. }
  108. /**
  109. * Returns a raw mime-dir representation of the value.
  110. *
  111. * @return string
  112. */
  113. public function getRawMimeDirValue()
  114. {
  115. $val = $this->getParts();
  116. if (isset($this->minimumPropertyValues[$this->name])) {
  117. $val = array_pad($val, $this->minimumPropertyValues[$this->name], '');
  118. }
  119. foreach ($val as &$item) {
  120. if (!is_array($item)) {
  121. $item = [$item];
  122. }
  123. foreach ($item as &$subItem) {
  124. if (!is_null($subItem)) {
  125. $subItem = strtr(
  126. $subItem,
  127. [
  128. '\\' => '\\\\',
  129. ';' => '\;',
  130. ',' => '\,',
  131. "\n" => '\n',
  132. "\r" => '',
  133. ]
  134. );
  135. }
  136. }
  137. $item = implode(',', $item);
  138. }
  139. return implode($this->delimiter, $val);
  140. }
  141. /**
  142. * Returns the value, in the format it should be encoded for json.
  143. *
  144. * This method must always return an array.
  145. *
  146. * @return array
  147. */
  148. public function getJsonValue()
  149. {
  150. // Structured text values should always be returned as a single
  151. // array-item. Multi-value text should be returned as multiple items in
  152. // the top-array.
  153. if (in_array($this->name, $this->structuredValues)) {
  154. return [$this->getParts()];
  155. }
  156. return $this->getParts();
  157. }
  158. /**
  159. * Returns the type of value.
  160. *
  161. * This corresponds to the VALUE= parameter. Every property also has a
  162. * 'default' valueType.
  163. *
  164. * @return string
  165. */
  166. public function getValueType()
  167. {
  168. return 'TEXT';
  169. }
  170. /**
  171. * Turns the object back into a serialized blob.
  172. *
  173. * @return string
  174. */
  175. public function serialize()
  176. {
  177. // We need to kick in a special type of encoding, if it's a 2.1 vcard.
  178. if (Document::VCARD21 !== $this->root->getDocumentType()) {
  179. return parent::serialize();
  180. }
  181. $val = $this->getParts();
  182. if (isset($this->minimumPropertyValues[$this->name])) {
  183. $val = \array_pad($val, $this->minimumPropertyValues[$this->name], '');
  184. }
  185. // Imploding multiple parts into a single value, and splitting the
  186. // values with ;.
  187. if (\count($val) > 1) {
  188. foreach ($val as $k => $v) {
  189. $val[$k] = \str_replace(';', '\;', $v);
  190. }
  191. $val = \implode(';', $val);
  192. } else {
  193. $val = $val[0];
  194. }
  195. $str = $this->name;
  196. if ($this->group) {
  197. $str = $this->group.'.'.$this->name;
  198. }
  199. foreach ($this->parameters as $param) {
  200. if ('QUOTED-PRINTABLE' === $param->getValue()) {
  201. continue;
  202. }
  203. $str .= ';'.$param->serialize();
  204. }
  205. // If the resulting value contains a \n, we must encode it as
  206. // quoted-printable.
  207. if (false !== \strpos($val, "\n")) {
  208. $str .= ';ENCODING=QUOTED-PRINTABLE:';
  209. $lastLine = $str;
  210. $out = null;
  211. // The PHP built-in quoted-printable-encode does not correctly
  212. // encode newlines for us. Specifically, the \r\n sequence must in
  213. // vcards be encoded as =0D=OA and we must insert soft-newlines
  214. // every 75 bytes.
  215. for ($ii = 0; $ii < \strlen($val); ++$ii) {
  216. $ord = \ord($val[$ii]);
  217. // These characters are encoded as themselves.
  218. if ($ord >= 32 && $ord <= 126) {
  219. $lastLine .= $val[$ii];
  220. } else {
  221. $lastLine .= '='.\strtoupper(\bin2hex($val[$ii]));
  222. }
  223. if (\strlen($lastLine) >= 75) {
  224. // Soft line break
  225. $out .= $lastLine."=\r\n ";
  226. $lastLine = null;
  227. }
  228. }
  229. if (!\is_null($lastLine)) {
  230. $out .= $lastLine."\r\n";
  231. }
  232. return $out;
  233. } else {
  234. $str .= ':'.$val;
  235. $str = \preg_replace(
  236. '/(
  237. (?:^.)? # 1 additional byte in first line because of missing single space (see next line)
  238. .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF)
  239. (?![\x80-\xbf]) # prevent splitting multibyte characters
  240. )/x',
  241. "$1\r\n ",
  242. $str
  243. );
  244. // remove single space after last CRLF
  245. return \substr($str, 0, -1);
  246. }
  247. }
  248. /**
  249. * This method serializes only the value of a property. This is used to
  250. * create xCard or xCal documents.
  251. *
  252. * @param Xml\Writer $writer XML writer
  253. */
  254. protected function xmlSerializeValue(Xml\Writer $writer)
  255. {
  256. $values = $this->getParts();
  257. $map = function ($items) use ($values, $writer) {
  258. foreach ($items as $i => $item) {
  259. $writer->writeElement(
  260. $item,
  261. !empty($values[$i]) ? $values[$i] : null
  262. );
  263. }
  264. };
  265. switch ($this->name) {
  266. // Special-casing the REQUEST-STATUS property.
  267. //
  268. // See:
  269. // http://tools.ietf.org/html/rfc6321#section-3.4.1.3
  270. case 'REQUEST-STATUS':
  271. $writer->writeElement('code', $values[0]);
  272. $writer->writeElement('description', $values[1]);
  273. if (isset($values[2])) {
  274. $writer->writeElement('data', $values[2]);
  275. }
  276. break;
  277. case 'N':
  278. $map([
  279. 'surname',
  280. 'given',
  281. 'additional',
  282. 'prefix',
  283. 'suffix',
  284. ]);
  285. break;
  286. case 'GENDER':
  287. $map([
  288. 'sex',
  289. 'text',
  290. ]);
  291. break;
  292. case 'ADR':
  293. $map([
  294. 'pobox',
  295. 'ext',
  296. 'street',
  297. 'locality',
  298. 'region',
  299. 'code',
  300. 'country',
  301. ]);
  302. break;
  303. case 'CLIENTPIDMAP':
  304. $map([
  305. 'sourceid',
  306. 'uri',
  307. ]);
  308. break;
  309. default:
  310. parent::xmlSerializeValue($writer);
  311. }
  312. }
  313. /**
  314. * Validates the node for correctness.
  315. *
  316. * The following options are supported:
  317. * - Node::REPAIR - If something is broken, and automatic repair may
  318. * be attempted.
  319. *
  320. * An array is returned with warnings.
  321. *
  322. * Every item in the array has the following properties:
  323. * * level - (number between 1 and 3 with severity information)
  324. * * message - (human readable message)
  325. * * node - (reference to the offending node)
  326. *
  327. * @param int $options
  328. *
  329. * @return array
  330. */
  331. public function validate($options = 0)
  332. {
  333. $warnings = parent::validate($options);
  334. if (isset($this->minimumPropertyValues[$this->name])) {
  335. $minimum = $this->minimumPropertyValues[$this->name];
  336. $parts = $this->getParts();
  337. if (count($parts) < $minimum) {
  338. $warnings[] = [
  339. 'level' => $options & self::REPAIR ? 1 : 3,
  340. 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.count($parts),
  341. 'node' => $this,
  342. ];
  343. if ($options & self::REPAIR) {
  344. $parts = array_pad($parts, $minimum, '');
  345. $this->setParts($parts);
  346. }
  347. }
  348. }
  349. return $warnings;
  350. }
  351. }