Folder.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. <?php
  2. /**
  3. * Convenience class for handling directories.
  4. *
  5. * PHP 5
  6. *
  7. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  8. * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  9. *
  10. * Licensed under The MIT License
  11. * Redistributions of files must retain the above copyright notice.
  12. *
  13. * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  14. * @link http://cakephp.org CakePHP(tm) Project
  15. * @package Cake.Utility
  16. * @since CakePHP(tm) v 0.2.9
  17. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  18. */
  19. /**
  20. * Folder structure browser, lists folders and files.
  21. * Provides an Object interface for Common directory related tasks.
  22. *
  23. * @package Cake.Utility
  24. */
  25. class Folder {
  26. /**
  27. * Path to Folder.
  28. *
  29. * @var string
  30. */
  31. public $path = null;
  32. /**
  33. * Sortedness. Whether or not list results
  34. * should be sorted by name.
  35. *
  36. * @var boolean
  37. */
  38. public $sort = false;
  39. /**
  40. * Mode to be used on create. Does nothing on windows platforms.
  41. *
  42. * @var integer
  43. */
  44. public $mode = 0755;
  45. /**
  46. * Holds messages from last method.
  47. *
  48. * @var array
  49. */
  50. protected $_messages = array();
  51. /**
  52. * Holds errors from last method.
  53. *
  54. * @var array
  55. */
  56. protected $_errors = array();
  57. /**
  58. * Holds array of complete directory paths.
  59. *
  60. * @var array
  61. */
  62. protected $_directories;
  63. /**
  64. * Holds array of complete file paths.
  65. *
  66. * @var array
  67. */
  68. protected $_files;
  69. /**
  70. * Constructor.
  71. *
  72. * @param string $path Path to folder
  73. * @param boolean $create Create folder if not found
  74. * @param mixed $mode Mode (CHMOD) to apply to created folder, false to ignore
  75. */
  76. public function __construct($path = false, $create = false, $mode = false) {
  77. if (empty($path)) {
  78. $path = TMP;
  79. }
  80. if ($mode) {
  81. $this->mode = $mode;
  82. }
  83. if (!file_exists($path) && $create === true) {
  84. $this->create($path, $this->mode);
  85. }
  86. if (!Folder::isAbsolute($path)) {
  87. $path = realpath($path);
  88. }
  89. if (!empty($path)) {
  90. $this->cd($path);
  91. }
  92. }
  93. /**
  94. * Return current path.
  95. *
  96. * @return string Current path
  97. */
  98. public function pwd() {
  99. return $this->path;
  100. }
  101. /**
  102. * Change directory to $path.
  103. *
  104. * @param string $path Path to the directory to change to
  105. * @return string The new path. Returns false on failure
  106. */
  107. public function cd($path) {
  108. $path = $this->realpath($path);
  109. if (is_dir($path)) {
  110. return $this->path = $path;
  111. }
  112. return false;
  113. }
  114. /**
  115. * Returns an array of the contents of the current directory.
  116. * The returned array holds two arrays: One of directories and one of files.
  117. *
  118. * @param boolean $sort Whether you want the results sorted, set this and the sort property
  119. * to false to get unsorted results.
  120. * @param mixed $exceptions Either an array or boolean true will not grab dot files
  121. * @param boolean $fullPath True returns the full path
  122. * @return mixed Contents of current directory as an array, an empty array on failure
  123. */
  124. public function read($sort = true, $exceptions = false, $fullPath = false) {
  125. $dirs = $files = array();
  126. if (!$this->pwd()) {
  127. return array($dirs, $files);
  128. }
  129. if (is_array($exceptions)) {
  130. $exceptions = array_flip($exceptions);
  131. }
  132. $skipHidden = isset($exceptions['.']) || $exceptions === true;
  133. try {
  134. $iterator = new DirectoryIterator($this->path);
  135. } catch (UnexpectedValueException $e) {
  136. return array($dirs, $files);
  137. }
  138. foreach ($iterator as $item) {
  139. if ($item->isDot()) {
  140. continue;
  141. }
  142. $name = $item->getFileName();
  143. if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) {
  144. continue;
  145. }
  146. if ($fullPath) {
  147. $name = $item->getPathName();
  148. }
  149. if ($item->isDir()) {
  150. $dirs[] = $name;
  151. } else {
  152. $files[] = $name;
  153. }
  154. }
  155. if ($sort || $this->sort) {
  156. sort($dirs);
  157. sort($files);
  158. }
  159. return array($dirs, $files);
  160. }
  161. /**
  162. * Returns an array of all matching files in current directory.
  163. *
  164. * @param string $regexpPattern Preg_match pattern (Defaults to: .*)
  165. * @param boolean $sort Whether results should be sorted.
  166. * @return array Files that match given pattern
  167. */
  168. public function find($regexpPattern = '.*', $sort = false) {
  169. list($dirs, $files) = $this->read($sort);
  170. return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files)); ;
  171. }
  172. /**
  173. * Returns an array of all matching files in and below current directory.
  174. *
  175. * @param string $pattern Preg_match pattern (Defaults to: .*)
  176. * @param boolean $sort Whether results should be sorted.
  177. * @return array Files matching $pattern
  178. */
  179. public function findRecursive($pattern = '.*', $sort = false) {
  180. if (!$this->pwd()) {
  181. return array();
  182. }
  183. $startsOn = $this->path;
  184. $out = $this->_findRecursive($pattern, $sort);
  185. $this->cd($startsOn);
  186. return $out;
  187. }
  188. /**
  189. * Private helper function for findRecursive.
  190. *
  191. * @param string $pattern Pattern to match against
  192. * @param boolean $sort Whether results should be sorted.
  193. * @return array Files matching pattern
  194. */
  195. protected function _findRecursive($pattern, $sort = false) {
  196. list($dirs, $files) = $this->read($sort);
  197. $found = array();
  198. foreach ($files as $file) {
  199. if (preg_match('/^' . $pattern . '$/i', $file)) {
  200. $found[] = Folder::addPathElement($this->path, $file);
  201. }
  202. }
  203. $start = $this->path;
  204. foreach ($dirs as $dir) {
  205. $this->cd(Folder::addPathElement($start, $dir));
  206. $found = array_merge($found, $this->findRecursive($pattern, $sort));
  207. }
  208. return $found;
  209. }
  210. /**
  211. * Returns true if given $path is a Windows path.
  212. *
  213. * @param string $path Path to check
  214. * @return boolean true if windows path, false otherwise
  215. */
  216. public static function isWindowsPath($path) {
  217. return (preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) == '\\\\');
  218. }
  219. /**
  220. * Returns true if given $path is an absolute path.
  221. *
  222. * @param string $path Path to check
  223. * @return boolean true if path is absolute.
  224. */
  225. public static function isAbsolute($path) {
  226. return !empty($path) && ($path[0] === '/' || preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) == '\\\\');
  227. }
  228. /**
  229. * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
  230. *
  231. * @param string $path Path to check
  232. * @return string Set of slashes ("\\" or "/")
  233. */
  234. public static function normalizePath($path) {
  235. return Folder::correctSlashFor($path);
  236. }
  237. /**
  238. * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.)
  239. *
  240. * @param string $path Path to check
  241. * @return string Set of slashes ("\\" or "/")
  242. */
  243. public static function correctSlashFor($path) {
  244. return (Folder::isWindowsPath($path)) ? '\\' : '/';
  245. }
  246. /**
  247. * Returns $path with added terminating slash (corrected for Windows or other OS).
  248. *
  249. * @param string $path Path to check
  250. * @return string Path with ending slash
  251. */
  252. public static function slashTerm($path) {
  253. if (Folder::isSlashTerm($path)) {
  254. return $path;
  255. }
  256. return $path . Folder::correctSlashFor($path);
  257. }
  258. /**
  259. * Returns $path with $element added, with correct slash in-between.
  260. *
  261. * @param string $path Path
  262. * @param string $element Element to and at end of path
  263. * @return string Combined path
  264. */
  265. public static function addPathElement($path, $element) {
  266. return rtrim($path, DS) . DS . $element;
  267. }
  268. /**
  269. * Returns true if the File is in a given CakePath.
  270. *
  271. * @param string $path The path to check.
  272. * @return boolean
  273. */
  274. public function inCakePath($path = '') {
  275. $dir = substr(Folder::slashTerm(ROOT), 0, -1);
  276. $newdir = $dir . $path;
  277. return $this->inPath($newdir);
  278. }
  279. /**
  280. * Returns true if the File is in given path.
  281. *
  282. * @param string $path The path to check that the current pwd() resides with in.
  283. * @param boolean $reverse
  284. * @return boolean
  285. */
  286. public function inPath($path = '', $reverse = false) {
  287. $dir = Folder::slashTerm($path);
  288. $current = Folder::slashTerm($this->pwd());
  289. if (!$reverse) {
  290. $return = preg_match('/^(.*)' . preg_quote($dir, '/') . '(.*)/', $current);
  291. } else {
  292. $return = preg_match('/^(.*)' . preg_quote($current, '/') . '(.*)/', $dir);
  293. }
  294. return (bool)$return;
  295. }
  296. /**
  297. * Change the mode on a directory structure recursively. This includes changing the mode on files as well.
  298. *
  299. * @param string $path The path to chmod
  300. * @param integer $mode octal value 0755
  301. * @param boolean $recursive chmod recursively, set to false to only change the current directory.
  302. * @param array $exceptions array of files, directories to skip
  303. * @return boolean Returns TRUE on success, FALSE on failure
  304. */
  305. public function chmod($path, $mode = false, $recursive = true, $exceptions = array()) {
  306. if (!$mode) {
  307. $mode = $this->mode;
  308. }
  309. if ($recursive === false && is_dir($path)) {
  310. if (@chmod($path, intval($mode, 8))) {
  311. $this->_messages[] = __d('cake_dev', '%s changed to %s', $path, $mode);
  312. return true;
  313. }
  314. $this->_errors[] = __d('cake_dev', '%s NOT changed to %s', $path, $mode);
  315. return false;
  316. }
  317. if (is_dir($path)) {
  318. $paths = $this->tree($path);
  319. foreach ($paths as $type) {
  320. foreach ($type as $key => $fullpath) {
  321. $check = explode(DS, $fullpath);
  322. $count = count($check);
  323. if (in_array($check[$count - 1], $exceptions)) {
  324. continue;
  325. }
  326. if (@chmod($fullpath, intval($mode, 8))) {
  327. $this->_messages[] = __d('cake_dev', '%s changed to %s', $fullpath, $mode);
  328. } else {
  329. $this->_errors[] = __d('cake_dev', '%s NOT changed to %s', $fullpath, $mode);
  330. }
  331. }
  332. }
  333. if (empty($this->_errors)) {
  334. return true;
  335. }
  336. }
  337. return false;
  338. }
  339. /**
  340. * Returns an array of nested directories and files in each directory
  341. *
  342. * @param string $path the directory path to build the tree from
  343. * @param mixed $exceptions Array of files to exclude, defaults to excluding hidden files.
  344. * @param string $type either file or dir. null returns both files and directories
  345. * @return mixed array of nested directories and files in each directory
  346. */
  347. public function tree($path = null, $exceptions = true, $type = null) {
  348. if ($path == null) {
  349. $path = $this->path;
  350. }
  351. $files = array();
  352. $directories = array($path);
  353. $skipHidden = false;
  354. if ($exceptions === false) {
  355. $skipHidden = true;
  356. }
  357. if (is_array($exceptions)) {
  358. $exceptions = array_flip($exceptions);
  359. }
  360. try {
  361. $directory = new RecursiveDirectoryIterator($path);
  362. $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
  363. } catch (UnexpectedValueException $e) {
  364. if ($type === null) {
  365. return array(array(), array());
  366. }
  367. return array();
  368. }
  369. foreach ($iterator as $item) {
  370. $name = $item->getFileName();
  371. if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) {
  372. continue;
  373. }
  374. if ($item->isFile()) {
  375. $files[] = $item->getPathName();
  376. } else if ($item->isDir() && !in_array($name, array('.', '..'))) {
  377. $directories[] = $item->getPathName();
  378. }
  379. }
  380. if ($type === null) {
  381. return array($directories, $files);
  382. }
  383. if ($type === 'dir') {
  384. return $directories;
  385. }
  386. return $files;
  387. }
  388. /**
  389. * Private method to list directories and files in each directory
  390. *
  391. * @param string $path The Path to read.
  392. * @param mixed $exceptions Array of files to exclude from the read that will be performed.
  393. * @return void
  394. */
  395. protected function _tree($path, $exceptions) {
  396. $this->path = $path;
  397. list($dirs, $files) = $this->read(false, $exceptions, true);
  398. $this->_directories = array_merge($this->_directories, $dirs);
  399. $this->_files = array_merge($this->_files, $files);
  400. }
  401. /**
  402. * Create a directory structure recursively. Can be used to create
  403. * deep path structures like `/foo/bar/baz/shoe/horn`
  404. *
  405. * @param string $pathname The directory structure to create
  406. * @param integer $mode octal value 0755
  407. * @return boolean Returns TRUE on success, FALSE on failure
  408. */
  409. public function create($pathname, $mode = false) {
  410. if (is_dir($pathname) || empty($pathname)) {
  411. return true;
  412. }
  413. if (!$mode) {
  414. $mode = $this->mode;
  415. }
  416. if (is_file($pathname)) {
  417. $this->_errors[] = __d('cake_dev', '%s is a file', $pathname);
  418. return false;
  419. }
  420. $pathname = rtrim($pathname, DS);
  421. $nextPathname = substr($pathname, 0, strrpos($pathname, DS));
  422. if ($this->create($nextPathname, $mode)) {
  423. if (!file_exists($pathname)) {
  424. $old = umask(0);
  425. if (mkdir($pathname, $mode)) {
  426. umask($old);
  427. $this->_messages[] = __d('cake_dev', '%s created', $pathname);
  428. return true;
  429. } else {
  430. umask($old);
  431. $this->_errors[] = __d('cake_dev', '%s NOT created', $pathname);
  432. return false;
  433. }
  434. }
  435. }
  436. return false;
  437. }
  438. /**
  439. * Returns the size in bytes of this Folder and its contents.
  440. *
  441. * @return integer size in bytes of current folder
  442. */
  443. public function dirsize() {
  444. $size = 0;
  445. $directory = Folder::slashTerm($this->path);
  446. $stack = array($directory);
  447. $count = count($stack);
  448. for ($i = 0, $j = $count; $i < $j; ++$i) {
  449. if (is_file($stack[$i])) {
  450. $size += filesize($stack[$i]);
  451. } elseif (is_dir($stack[$i])) {
  452. $dir = dir($stack[$i]);
  453. if ($dir) {
  454. while (false !== ($entry = $dir->read())) {
  455. if ($entry === '.' || $entry === '..') {
  456. continue;
  457. }
  458. $add = $stack[$i] . $entry;
  459. if (is_dir($stack[$i] . $entry)) {
  460. $add = Folder::slashTerm($add);
  461. }
  462. $stack[] = $add;
  463. }
  464. $dir->close();
  465. }
  466. }
  467. $j = count($stack);
  468. }
  469. return $size;
  470. }
  471. /**
  472. * Recursively Remove directories if the system allows.
  473. *
  474. * @param string $path Path of directory to delete
  475. * @return boolean Success
  476. */
  477. public function delete($path = null) {
  478. if (!$path) {
  479. $path = $this->pwd();
  480. }
  481. if (!$path) {
  482. return null;
  483. }
  484. $path = Folder::slashTerm($path);
  485. if (is_dir($path) === true) {
  486. $normalFiles = glob($path . '*');
  487. $hiddenFiles = glob($path . '\.?*');
  488. $normalFiles = $normalFiles ? $normalFiles : array();
  489. $hiddenFiles = $hiddenFiles ? $hiddenFiles : array();
  490. $files = array_merge($normalFiles, $hiddenFiles);
  491. if (is_array($files)) {
  492. foreach ($files as $file) {
  493. if (preg_match('/(\.|\.\.)$/', $file)) {
  494. continue;
  495. }
  496. if (is_file($file) === true) {
  497. if (@unlink($file)) {
  498. $this->_messages[] = __d('cake_dev', '%s removed', $file);
  499. } else {
  500. $this->_errors[] = __d('cake_dev', '%s NOT removed', $file);
  501. }
  502. } elseif (is_dir($file) === true && $this->delete($file) === false) {
  503. return false;
  504. }
  505. }
  506. }
  507. $path = substr($path, 0, strlen($path) - 1);
  508. if (rmdir($path) === false) {
  509. $this->_errors[] = __d('cake_dev', '%s NOT removed', $path);
  510. return false;
  511. } else {
  512. $this->_messages[] = __d('cake_dev', '%s removed', $path);
  513. }
  514. }
  515. return true;
  516. }
  517. /**
  518. * Recursive directory copy.
  519. *
  520. * ### Options
  521. *
  522. * - `to` The directory to copy to.
  523. * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
  524. * - `mode` The mode to copy the files/directories with.
  525. * - `skip` Files/directories to skip.
  526. *
  527. * @param mixed $options Either an array of options (see above) or a string of the destination directory.
  528. * @return boolean Success
  529. */
  530. public function copy($options = array()) {
  531. if (!$this->pwd()) {
  532. return false;
  533. }
  534. $to = null;
  535. if (is_string($options)) {
  536. $to = $options;
  537. $options = array();
  538. }
  539. $options = array_merge(array('to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => array()), $options);
  540. $fromDir = $options['from'];
  541. $toDir = $options['to'];
  542. $mode = $options['mode'];
  543. if (!$this->cd($fromDir)) {
  544. $this->_errors[] = __d('cake_dev', '%s not found', $fromDir);
  545. return false;
  546. }
  547. if (!is_dir($toDir)) {
  548. $this->create($toDir, $mode);
  549. }
  550. if (!is_writable($toDir)) {
  551. $this->_errors[] = __d('cake_dev', '%s not writable', $toDir);
  552. return false;
  553. }
  554. $exceptions = array_merge(array('.', '..', '.svn'), $options['skip']);
  555. if ($handle = @opendir($fromDir)) {
  556. while (false !== ($item = readdir($handle))) {
  557. if (!in_array($item, $exceptions)) {
  558. $from = Folder::addPathElement($fromDir, $item);
  559. $to = Folder::addPathElement($toDir, $item);
  560. if (is_file($from)) {
  561. if (copy($from, $to)) {
  562. chmod($to, intval($mode, 8));
  563. touch($to, filemtime($from));
  564. $this->_messages[] = __d('cake_dev', '%s copied to %s', $from, $to);
  565. } else {
  566. $this->_errors[] = __d('cake_dev', '%s NOT copied to %s', $from, $to);
  567. }
  568. }
  569. if (is_dir($from) && !file_exists($to)) {
  570. $old = umask(0);
  571. if (mkdir($to, $mode)) {
  572. umask($old);
  573. $old = umask(0);
  574. chmod($to, $mode);
  575. umask($old);
  576. $this->_messages[] = __d('cake_dev', '%s created', $to);
  577. $options = array_merge($options, array('to'=> $to, 'from'=> $from));
  578. $this->copy($options);
  579. } else {
  580. $this->_errors[] = __d('cake_dev', '%s not created', $to);
  581. }
  582. }
  583. }
  584. }
  585. closedir($handle);
  586. } else {
  587. return false;
  588. }
  589. if (!empty($this->_errors)) {
  590. return false;
  591. }
  592. return true;
  593. }
  594. /**
  595. * Recursive directory move.
  596. *
  597. * ### Options
  598. *
  599. * - `to` The directory to copy to.
  600. * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd().
  601. * - `chmod` The mode to copy the files/directories with.
  602. * - `skip` Files/directories to skip.
  603. *
  604. * @param array $options (to, from, chmod, skip)
  605. * @return boolean Success
  606. */
  607. public function move($options) {
  608. $to = null;
  609. if (is_string($options)) {
  610. $to = $options;
  611. $options = (array)$options;
  612. }
  613. $options = array_merge(
  614. array('to' => $to, 'from' => $this->path, 'mode' => $this->mode, 'skip' => array()),
  615. $options
  616. );
  617. if ($this->copy($options)) {
  618. if ($this->delete($options['from'])) {
  619. return (bool)$this->cd($options['to']);
  620. }
  621. }
  622. return false;
  623. }
  624. /**
  625. * get messages from latest method
  626. *
  627. * @return array
  628. */
  629. public function messages() {
  630. return $this->_messages;
  631. }
  632. /**
  633. * get error from latest method
  634. *
  635. * @return array
  636. */
  637. public function errors() {
  638. return $this->_errors;
  639. }
  640. /**
  641. * Get the real path (taking ".." and such into account)
  642. *
  643. * @param string $path Path to resolve
  644. * @return string The resolved path
  645. */
  646. public function realpath($path) {
  647. $path = str_replace('/', DS, trim($path));
  648. if (strpos($path, '..') === false) {
  649. if (!Folder::isAbsolute($path)) {
  650. $path = Folder::addPathElement($this->path, $path);
  651. }
  652. return $path;
  653. }
  654. $parts = explode(DS, $path);
  655. $newparts = array();
  656. $newpath = '';
  657. if ($path[0] === DS) {
  658. $newpath = DS;
  659. }
  660. while (($part = array_shift($parts)) !== NULL) {
  661. if ($part === '.' || $part === '') {
  662. continue;
  663. }
  664. if ($part === '..') {
  665. if (!empty($newparts)) {
  666. array_pop($newparts);
  667. continue;
  668. } else {
  669. return false;
  670. }
  671. }
  672. $newparts[] = $part;
  673. }
  674. $newpath .= implode(DS, $newparts);
  675. return Folder::slashTerm($newpath);
  676. }
  677. /**
  678. * Returns true if given $path ends in a slash (i.e. is slash-terminated).
  679. *
  680. * @param string $path Path to check
  681. * @return boolean true if path ends with slash, false otherwise
  682. */
  683. public static function isSlashTerm($path) {
  684. $lastChar = $path[strlen($path) - 1];
  685. return $lastChar === '/' || $lastChar === '\\';
  686. }
  687. }