KXStudio Website https://kx.studio/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

752 lines
25KB

  1. <?php
  2. /**
  3. * Horde YAML package
  4. *
  5. * This package is heavily inspired by the Spyc PHP YAML
  6. * implementation (http://spyc.sourceforge.net/), and portions are
  7. * copyright 2005-2006 Chris Wanstrath.
  8. *
  9. * @author Chris Wanstrath (chris@ozmm.org)
  10. * @author Chuck Hagenbuch (chuck@horde.org)
  11. * @author Mike Naberezny (mike@maintainable.com)
  12. * @license http://opensource.org/licenses/bsd-license.php BSD
  13. * @category Horde
  14. * @package Horde_Yaml
  15. */
  16. /**
  17. * Parse YAML strings into PHP data structures
  18. *
  19. * @category Horde
  20. * @package Horde_Yaml
  21. */
  22. class Horde_Yaml_Loader
  23. {
  24. /**
  25. * List of nodes with references
  26. * @var array
  27. */
  28. protected $_haveRefs = array();
  29. /**
  30. * All nodes
  31. * @var array
  32. */
  33. protected $_allNodes = array();
  34. /**
  35. * Array of node parents
  36. * @var array
  37. */
  38. protected $_allParent = array();
  39. /**
  40. * Last indent level
  41. * @var integer
  42. */
  43. protected $_lastIndent = 0;
  44. /**
  45. * Last node id
  46. * @var integer
  47. */
  48. protected $_lastNode = null;
  49. /**
  50. * Is the parser inside a block?
  51. * @var boolean
  52. */
  53. protected $_inBlock = false;
  54. /**
  55. * @var boolean
  56. */
  57. protected $_isInline = false;
  58. /**
  59. * Next node id to use
  60. * @var integer
  61. */
  62. protected $_nodeId = 1;
  63. /**
  64. * Last line number parsed.
  65. * @var integer
  66. */
  67. protected $_lineNumber = 0;
  68. /**
  69. * Create a new YAML parser.
  70. */
  71. public function __construct()
  72. {
  73. $base = new Horde_Yaml_Node($this->_nodeId++);
  74. $base->indent = 0;
  75. $this->_lastNode = $base->id;
  76. }
  77. /**
  78. * Return the PHP built from all YAML parsed so far.
  79. *
  80. * @return array PHP version of parsed YAML
  81. */
  82. public function toArray()
  83. {
  84. // Here we travel through node-space and pick out references
  85. // (& and *).
  86. $this->_linkReferences();
  87. // Build the PHP array out of node-space.
  88. return $this->_buildArray();
  89. }
  90. /**
  91. * Parse a line of a YAML file.
  92. *
  93. * @param string $line The line of YAML to parse.
  94. * @return Horde_Yaml_Node YAML Node
  95. */
  96. public function parse($line)
  97. {
  98. // Keep track of how many lines we've parsed for friendlier
  99. // error messages.
  100. ++$this->_lineNumber;
  101. $trimmed = trim($line);
  102. // If the line starts with a tab (instead of a space), throw a fit.
  103. if (preg_match('/^ *(\t) *[^\t ]/', $line)) {
  104. $msg = "Line {$this->_lineNumber} indent contains a tab. "
  105. . 'YAML only allows spaces for indentation.';
  106. throw new Horde_Yaml_Exception($msg);
  107. }
  108. if (!$this->_inBlock && empty($trimmed)) {
  109. return;
  110. } elseif ($this->_inBlock && empty($trimmed)) {
  111. $last =& $this->_allNodes[$this->_lastNode];
  112. $last->data[key($last->data)] .= "\n";
  113. } elseif ($trimmed[0] != '#' && substr($trimmed, 0, 3) != '---') {
  114. // Create a new node and get its indent
  115. $node = new Horde_Yaml_Node($this->_nodeId++);
  116. $node->indent = $this->_getIndent($line);
  117. // Check where the node lies in the hierarchy
  118. if ($this->_lastIndent == $node->indent) {
  119. // If we're in a block, add the text to the parent's data
  120. if ($this->_inBlock) {
  121. $parent =& $this->_allNodes[$this->_lastNode];
  122. $parent->data[key($parent->data)] .= trim($line) . $this->_blockEnd;
  123. } else {
  124. // The current node's parent is the same as the previous node's
  125. if (isset($this->_allNodes[$this->_lastNode])) {
  126. $node->parent = $this->_allNodes[$this->_lastNode]->parent;
  127. }
  128. }
  129. } elseif ($this->_lastIndent < $node->indent) {
  130. if ($this->_inBlock) {
  131. $parent =& $this->_allNodes[$this->_lastNode];
  132. $parent->data[key($parent->data)] .= trim($line) . $this->_blockEnd;
  133. } elseif (!$this->_inBlock) {
  134. // The current node's parent is the previous node
  135. $node->parent = $this->_lastNode;
  136. // If the value of the last node's data was > or |
  137. // we need to start blocking i.e. taking in all
  138. // lines as a text value until we drop our indent.
  139. $parent =& $this->_allNodes[$node->parent];
  140. $this->_allNodes[$node->parent]->children = true;
  141. if (is_array($parent->data)) {
  142. if (isset($parent->data[key($parent->data)])) {
  143. $chk = $parent->data[key($parent->data)];
  144. if ($chk === '>') {
  145. $this->_inBlock = true;
  146. $this->_blockEnd = '';
  147. $parent->data[key($parent->data)] =
  148. str_replace('>', '', $parent->data[key($parent->data)]);
  149. $parent->data[key($parent->data)] .= trim($line) . ' ';
  150. $this->_allNodes[$node->parent]->children = false;
  151. $this->_lastIndent = $node->indent;
  152. } elseif ($chk === '|') {
  153. $this->_inBlock = true;
  154. $this->_blockEnd = "\n";
  155. $parent->data[key($parent->data)] =
  156. str_replace('|', '', $parent->data[key($parent->data)]);
  157. $parent->data[key($parent->data)] .= trim($line) . "\n";
  158. $this->_allNodes[$node->parent]->children = false;
  159. $this->_lastIndent = $node->indent;
  160. }
  161. }
  162. }
  163. }
  164. } elseif ($this->_lastIndent > $node->indent) {
  165. // Any block we had going is dead now
  166. if ($this->_inBlock) {
  167. $this->_inBlock = false;
  168. if ($this->_blockEnd == "\n") {
  169. $last =& $this->_allNodes[$this->_lastNode];
  170. $last->data[key($last->data)] =
  171. trim($last->data[key($last->data)]);
  172. }
  173. }
  174. // We don't know the parent of the node so we have to
  175. // find it
  176. foreach ($this->_indentSort[$node->indent] as $n) {
  177. if ($n->indent == $node->indent) {
  178. $node->parent = $n->parent;
  179. }
  180. }
  181. }
  182. if (!$this->_inBlock) {
  183. // Set these properties with information from our
  184. // current node
  185. $this->_lastIndent = $node->indent;
  186. // Set the last node
  187. $this->_lastNode = $node->id;
  188. // Parse the YAML line and return its data
  189. $node->data = $this->_parseLine($line);
  190. // Add the node to the master list
  191. $this->_allNodes[$node->id] = $node;
  192. // Add a reference to the parent list
  193. $this->_allParent[intval($node->parent)][] = $node->id;
  194. // Add a reference to the node in an indent array
  195. $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id];
  196. // Add a reference to the node in a References array
  197. // if this node has a YAML reference in it.
  198. $is_array = is_array($node->data);
  199. $key = key($node->data);
  200. $isset = isset($node->data[$key]);
  201. if ($isset) {
  202. $nodeval = $node->data[$key];
  203. }
  204. if (($is_array && $isset && !is_array($nodeval) && !is_object($nodeval))
  205. && (strlen($nodeval) && (false))) { # $nodeval[0] == '&' || $nodeval[0] == '*') && $nodeval[1] != ' ')) {
  206. $this->_haveRefs[] =& $this->_allNodes[$node->id];
  207. } elseif ($is_array && $isset && is_array($nodeval)) {
  208. // Incomplete reference making code. Needs to be
  209. // cleaned up.
  210. foreach ($node->data[$key] as $d) {
  211. if (!is_array($d) && strlen($d) && (false)) { # ($d[0] == '&' || $d[0] == '*') && $d[1] != ' ')) {
  212. $this->_haveRefs[] =& $this->_allNodes[$node->id];
  213. }
  214. }
  215. }
  216. }
  217. }
  218. }
  219. /**
  220. * Finds and returns the indentation of a YAML line
  221. *
  222. * @param string $line A line from the YAML file
  223. * @return int Indentation level
  224. */
  225. protected function _getIndent($line)
  226. {
  227. if (preg_match('/^\s+/', $line, $match)) {
  228. return strlen($match[0]);
  229. } else {
  230. return 0;
  231. }
  232. }
  233. /**
  234. * Parses YAML code and returns an array for a node
  235. *
  236. * @param string $line A line from the YAML file
  237. * @return array
  238. */
  239. protected function _parseLine($line)
  240. {
  241. $array = array();
  242. $line = trim($line);
  243. if (preg_match('/^-(.*):$/', $line)) {
  244. // It's a mapped sequence
  245. $key = trim(substr(substr($line, 1), 0, -1));
  246. $array[$key] = '';
  247. } elseif ($line[0] == '-' && substr($line, 0, 3) != '---') {
  248. // It's a list item but not a new stream
  249. if (strlen($line) > 1) {
  250. // Set the type of the value. Int, string, etc
  251. $array[] = $this->_toType(trim(substr($line, 1)));
  252. } else {
  253. $array[] = array();
  254. }
  255. } elseif (preg_match('/^(.+):/', $line, $key)) {
  256. // It's a key/value pair most likely
  257. // If the key is in double quotes pull it out
  258. if (preg_match('/^(["\'](.*)["\'](\s)*:)/', $line, $matches)) {
  259. $value = trim(str_replace($matches[1], '', $line));
  260. $key = $matches[2];
  261. } else {
  262. // Do some guesswork as to the key and the value
  263. $explode = explode(':', $line);
  264. $key = trim(array_shift($explode));
  265. $value = trim(implode(':', $explode));
  266. }
  267. // Set the type of the value. Int, string, etc
  268. $value = $this->_toType($value);
  269. if (empty($key)) {
  270. $array[] = $value;
  271. } else {
  272. $array[$key] = $value;
  273. }
  274. }
  275. return $array;
  276. }
  277. /**
  278. * Finds the type of the passed value, returns the value as the new type.
  279. *
  280. * @param string $value
  281. * @return mixed
  282. */
  283. protected function _toType($value)
  284. {
  285. // Check for PHP specials
  286. self::_unserialize($value);
  287. if (!is_scalar($value)) {
  288. return $value;
  289. }
  290. // Used in a lot of cases.
  291. $lower_value = strtolower($value);
  292. if (preg_match('/^("(.*)"|\'(.*)\')/', $value, $matches)) {
  293. $value = (string)str_replace(array('\'\'', '\\\''), "'", end($matches));
  294. $value = str_replace('\\"', '"', $value);
  295. } elseif (preg_match('/^\\[(\s*)\\]$/', $value)) {
  296. // empty inline mapping
  297. $value = array();
  298. } elseif (preg_match('/^\\[(.+)\\]$/', $value, $matches)) {
  299. // Inline Sequence
  300. // Take out strings sequences and mappings
  301. $explode = $this->_inlineEscape($matches[1]);
  302. // Propogate value array
  303. $value = array();
  304. foreach ($explode as $v) {
  305. $value[] = $this->_toType($v);
  306. }
  307. } elseif (preg_match('/^\\{(\s*)\\}$/', $value)) {
  308. // empty inline mapping
  309. $value = array();
  310. } elseif (strpos($value, ': ') !== false && !preg_match('/^{(.+)/', $value)) {
  311. // inline mapping
  312. $array = explode(': ', $value);
  313. $key = trim($array[0]);
  314. array_shift($array);
  315. $value = trim(implode(': ', $array));
  316. $value = $this->_toType($value);
  317. $value = array($key => $value);
  318. } elseif (preg_match("/{(.+)}$/", $value, $matches)) {
  319. // Inline Mapping
  320. // Take out strings sequences and mappings
  321. $explode = $this->_inlineEscape($matches[1]);
  322. // Propogate value array
  323. $array = array();
  324. foreach ($explode as $v) {
  325. $array = $array + $this->_toType($v);
  326. }
  327. $value = $array;
  328. } elseif ($lower_value == 'null' || $value == '' || $value == '~') {
  329. $value = null;
  330. } elseif ($lower_value == '.nan') {
  331. $value = NAN;
  332. } elseif ($lower_value == '.inf') {
  333. $value = INF;
  334. } elseif ($lower_value == '-.inf') {
  335. $value = -INF;
  336. } elseif (is_numeric($value) and !substr_count($value, ".")) {
  337. $value = (int)$value;
  338. } elseif (in_array($lower_value,
  339. array('true', 'on', '+', 'yes', 'y'))) {
  340. $value = true;
  341. } elseif (in_array($lower_value,
  342. array('false', 'off', '-', 'no', 'n'))) {
  343. $value = false;
  344. } elseif (is_numeric($value)) {
  345. $value = (float)$value;
  346. } else {
  347. // Just a normal string, right?
  348. if (($pos = strpos($value, '#')) !== false) {
  349. $value = substr($value, 0, $pos);
  350. }
  351. $value = trim($value);
  352. }
  353. return $value;
  354. }
  355. /**
  356. * Handle PHP serialized data.
  357. *
  358. * @param string &$data Data to check for serialized PHP types.
  359. */
  360. protected function _unserialize(&$data)
  361. {
  362. if (substr($data, 0, 5) != '!php/') {
  363. return;
  364. }
  365. $first_space = strpos($data, ' ');
  366. $type = substr($data, 5, $first_space - 5);
  367. $class = null;
  368. if (strpos($type, '::') !== false) {
  369. list($type, $class) = explode('::', $type);
  370. if (!in_array($class, Horde_Yaml::$allowedClasses)) {
  371. throw new Horde_Yaml_Exception("$class is not in the list of allowed classes");
  372. }
  373. }
  374. switch ($type) {
  375. case 'object':
  376. if (!class_exists($class)) {
  377. throw new Horde_Yaml_Exception("$class is not defined");
  378. }
  379. $reflector = new ReflectionClass($class);
  380. if (!$reflector->implementsInterface('Serializable')) {
  381. throw new Horde_Yaml_Exception("$class does not implement Serializable");
  382. }
  383. $class_data = substr($data, $first_space + 1);
  384. $serialized = 'C:' . strlen($class) . ':"' . $class . '":' . strlen($class_data) . ':{' . $class_data . '}';
  385. $data = unserialize($serialized);
  386. break;
  387. case 'array':
  388. case 'hash':
  389. $array_data = substr($data, $first_space + 1);
  390. $array_data = Horde_Yaml::load('a: ' . $array_data);
  391. if (is_null($class)) {
  392. $data = $array_data['a'];
  393. } else {
  394. if (!class_exists($class)) {
  395. throw new Horde_Yaml_Exception("$class is not defined");
  396. }
  397. $array = new $class;
  398. if (!$array instanceof ArrayAccess) {
  399. throw new Horde_Yaml_Exception("$class does not implement ArrayAccess");
  400. }
  401. foreach ($array_data['a'] as $key => $val) {
  402. $array[$key] = $val;
  403. }
  404. $data = $array;
  405. }
  406. break;
  407. }
  408. }
  409. /**
  410. * Used in inlines to check for more inlines or quoted strings
  411. *
  412. * @todo There should be a cleaner way to do this. While
  413. * pure sequences seem to be nesting just fine,
  414. * pure mappings and mappings with sequences inside
  415. * can't go very deep. This needs to be fixed.
  416. *
  417. * @param string $inline Inline data
  418. * @return array
  419. */
  420. protected function _inlineEscape($inline)
  421. {
  422. $saved_strings = array();
  423. // Check for strings
  424. $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/';
  425. if (preg_match_all($regex, $inline, $strings)) {
  426. $saved_strings = $strings[0];
  427. $inline = preg_replace($regex, 'YAMLString', $inline);
  428. }
  429. // Check for sequences
  430. if (preg_match_all('/\[(.+)\]/U', $inline, $seqs)) {
  431. $inline = preg_replace('/\[(.+)\]/U', 'YAMLSeq', $inline);
  432. $seqs = $seqs[0];
  433. }
  434. // Check for mappings
  435. if (preg_match_all('/{(.+)}/U', $inline, $maps)) {
  436. $inline = preg_replace('/{(.+)}/U', 'YAMLMap', $inline);
  437. $maps = $maps[0];
  438. }
  439. $explode = explode(', ', $inline);
  440. // Re-add the sequences
  441. if (!empty($seqs)) {
  442. $i = 0;
  443. foreach ($explode as $key => $value) {
  444. if (strpos($value, 'YAMLSeq') !== false) {
  445. $explode[$key] = str_replace('YAMLSeq', $seqs[$i], $value);
  446. ++$i;
  447. }
  448. }
  449. }
  450. // Re-add the mappings
  451. if (!empty($maps)) {
  452. $i = 0;
  453. foreach ($explode as $key => $value) {
  454. if (strpos($value, 'YAMLMap') !== false) {
  455. $explode[$key] = str_replace('YAMLMap', $maps[$i], $value);
  456. ++$i;
  457. }
  458. }
  459. }
  460. // Re-add the strings
  461. if (!empty($saved_strings)) {
  462. $i = 0;
  463. foreach ($explode as $key => $value) {
  464. while (strpos($value, 'YAMLString') !== false) {
  465. $explode[$key] = preg_replace('/YAMLString/', $saved_strings[$i], $value, 1);
  466. ++$i;
  467. $value = $explode[$key];
  468. }
  469. }
  470. }
  471. return $explode;
  472. }
  473. /**
  474. * Builds the PHP array from all the YAML nodes we've gathered
  475. *
  476. * @return array
  477. */
  478. protected function _buildArray()
  479. {
  480. $trunk = array();
  481. if (!isset($this->_indentSort[0])) {
  482. return $trunk;
  483. }
  484. foreach ($this->_indentSort[0] as $n) {
  485. if (empty($n->parent)) {
  486. $this->_nodeArrayizeData($n);
  487. // Check for references and copy the needed data to complete them.
  488. $this->_makeReferences($n);
  489. // Merge our data with the big array we're building
  490. $trunk = $this->_array_kmerge($trunk, $n->data);
  491. }
  492. }
  493. return $trunk;
  494. }
  495. /**
  496. * Traverses node-space and sets references (& and *) accordingly
  497. *
  498. * @return bool
  499. */
  500. protected function _linkReferences()
  501. {
  502. if (is_array($this->_haveRefs)) {
  503. foreach ($this->_haveRefs as $node) {
  504. if (!empty($node->data)) {
  505. $key = key($node->data);
  506. // If it's an array, don't check.
  507. if (is_array($node->data[$key])) {
  508. foreach ($node->data[$key] as $k => $v) {
  509. $this->_linkRef($node, $key, $k, $v);
  510. }
  511. } else {
  512. $this->_linkRef($node, $key);
  513. }
  514. }
  515. }
  516. }
  517. return true;
  518. }
  519. /**
  520. * Helper for _linkReferences()
  521. *
  522. * @param Horde_Yaml_Node $n Node
  523. * @param string $k Key
  524. * @param mixed $v Value
  525. * @return void
  526. */
  527. function _linkRef(&$n, $key, $k = null, $v = null)
  528. {
  529. if (empty($k) && empty($v)) {
  530. // Look for &refs
  531. if (preg_match('/^&([^ ]+)/', $n->data[$key], $matches)) {
  532. // Flag the node so we know it's a reference
  533. $this->_allNodes[$n->id]->ref = substr($matches[0], 1);
  534. $this->_allNodes[$n->id]->data[$key] =
  535. substr($n->data[$key], strlen($matches[0]) + 1);
  536. // Look for *refs
  537. } elseif (preg_match('/^\*([^ ]+)/', $n->data[$key], $matches)) {
  538. $ref = substr($matches[0], 1);
  539. // Flag the node as having a reference
  540. $this->_allNodes[$n->id]->refKey = $ref;
  541. }
  542. } elseif (!empty($k) && !empty($v)) {
  543. if (preg_match('/^&([^ ]+)/', $v, $matches)) {
  544. // Flag the node so we know it's a reference
  545. $this->_allNodes[$n->id]->ref = substr($matches[0], 1);
  546. $this->_allNodes[$n->id]->data[$key][$k] =
  547. substr($v, strlen($matches[0]) + 1);
  548. // Look for *refs
  549. } elseif (preg_match('/^\*([^ ]+)/', $v, $matches)) {
  550. $ref = substr($matches[0], 1);
  551. // Flag the node as having a reference
  552. $this->_allNodes[$n->id]->refKey = $ref;
  553. }
  554. }
  555. }
  556. /**
  557. * Finds the children of a node and aids in the building of the PHP array
  558. *
  559. * @param int $nid The id of the node whose children we're gathering
  560. * @return array
  561. */
  562. protected function _gatherChildren($nid)
  563. {
  564. $return = array();
  565. $node =& $this->_allNodes[$nid];
  566. if (is_array ($this->_allParent[$node->id])) {
  567. foreach ($this->_allParent[$node->id] as $nodeZ) {
  568. $z =& $this->_allNodes[$nodeZ];
  569. // We found a child
  570. $this->_nodeArrayizeData($z);
  571. // Check for references
  572. $this->_makeReferences($z);
  573. // Merge with the big array we're returning, the big
  574. // array being all the data of the children of our
  575. // parent node
  576. $return = $this->_array_kmerge($return, $z->data);
  577. }
  578. }
  579. return $return;
  580. }
  581. /**
  582. * Turns a node's data and its children's data into a PHP array
  583. *
  584. * @param array $node The node which you want to arrayize
  585. * @return boolean
  586. */
  587. protected function _nodeArrayizeData(&$node)
  588. {
  589. if ($node->children == true) {
  590. if (is_array($node->data)) {
  591. // This node has children, so we need to find them
  592. $children = $this->_gatherChildren($node->id);
  593. // We've gathered all our children's data and are ready to use it
  594. $key = key($node->data);
  595. $key = empty($key) ? 0 : $key;
  596. // If it's an array, add to it of course
  597. if (isset($node->data[$key])) {
  598. if (is_array($node->data[$key])) {
  599. $node->data[$key] = $this->_array_kmerge($node->data[$key], $children);
  600. } else {
  601. $node->data[$key] = $children;
  602. }
  603. } else {
  604. $node->data[$key] = $children;
  605. }
  606. } else {
  607. // Same as above, find the children of this node
  608. $children = $this->_gatherChildren($node->id);
  609. $node->data = array();
  610. $node->data[] = $children;
  611. }
  612. } else {
  613. // The node is a single string. See if we need to unserialize it.
  614. if (is_array($node->data)) {
  615. $key = key($node->data);
  616. $key = empty($key) ? 0 : $key;
  617. if (!isset($node->data[$key]) || is_array($node->data[$key]) || is_object($node->data[$key])) {
  618. return true;
  619. }
  620. self::_unserialize($node->data[$key]);
  621. } elseif (is_string($node->data)) {
  622. self::_unserialize($node->data);
  623. }
  624. }
  625. // We edited $node by reference, so just return true
  626. return true;
  627. }
  628. /**
  629. * Traverses node-space and copies references to / from this object.
  630. *
  631. * @param Horde_Yaml_Node $z A node whose references we wish to make real
  632. * @return bool
  633. */
  634. protected function _makeReferences(&$z)
  635. {
  636. // It is a reference
  637. if (isset($z->ref)) {
  638. $key = key($z->data);
  639. // Copy the data to this object for easy retrieval later
  640. $this->ref[$z->ref] =& $z->data[$key];
  641. // It has a reference
  642. } elseif (isset($z->refKey)) {
  643. if (isset($this->ref[$z->refKey])) {
  644. $key = key($z->data);
  645. // Copy the data from this object to make the node a real reference
  646. $z->data[$key] =& $this->ref[$z->refKey];
  647. }
  648. }
  649. return true;
  650. }
  651. /**
  652. * Merges two arrays, maintaining numeric keys. If two numeric
  653. * keys clash, the second one will be appended to the resulting
  654. * array. If string keys clash, the last one wins.
  655. *
  656. * @param array $arr1
  657. * @param array $arr2
  658. * @return array
  659. */
  660. protected function _array_kmerge($arr1, $arr2)
  661. {
  662. while (list($key, $val) = each($arr2)) {
  663. if (isset($arr1[$key]) && is_int($key)) {
  664. $arr1[] = $val;
  665. } else {
  666. $arr1[$key] = $val;
  667. }
  668. }
  669. return $arr1;
  670. }
  671. }