filesystemview.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <?php
  2. /**
  3. * ownCloud
  4. *
  5. * @author Frank Karlitschek
  6. * @copyright 2012 Frank Karlitschek frank@owncloud.org
  7. *
  8. * This library is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  10. * License as published by the Free Software Foundation; either
  11. * version 3 of the License, or any later version.
  12. *
  13. * This library is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public
  19. * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  20. */
  21. /**
  22. * Class to provide access to ownCloud filesystem via a "view", and methods for
  23. * working with files within that view (e.g. read, write, delete, etc.). Each
  24. * view is restricted to a set of directories via a virtual root. The default view
  25. * uses the currently logged in user's data directory as root (parts of
  26. * OC_Filesystem are merely a wrapper for OC_FilesystemView).
  27. *
  28. * Apps that need to access files outside of the user data folders (to modify files
  29. * belonging to a user other than the one currently logged in, for example) should
  30. * use this class directly rather than using OC_Filesystem, or making use of PHP's
  31. * built-in file manipulation functions. This will ensure all hooks and proxies
  32. * are triggered correctly.
  33. *
  34. * Filesystem functions are not called directly; they are passed to the correct
  35. * OC_Filestorage object
  36. */
  37. class OC_FilesystemView {
  38. private $fakeRoot='';
  39. private $internal_path_cache=array();
  40. private $storage_cache=array();
  41. public function __construct($root) {
  42. $this->fakeRoot=$root;
  43. }
  44. public function getAbsolutePath($path) {
  45. if(!$path) {
  46. $path='/';
  47. }
  48. if($path[0]!=='/') {
  49. $path='/'.$path;
  50. }
  51. return $this->fakeRoot.$path;
  52. }
  53. /**
  54. * change the root to a fake toor
  55. * @param string fakeRoot
  56. * @return bool
  57. */
  58. public function chroot($fakeRoot) {
  59. if(!$fakeRoot=='') {
  60. if($fakeRoot[0]!=='/') {
  61. $fakeRoot='/'.$fakeRoot;
  62. }
  63. }
  64. $this->fakeRoot=$fakeRoot;
  65. }
  66. /**
  67. * get the fake root
  68. * @return string
  69. */
  70. public function getRoot() {
  71. return $this->fakeRoot;
  72. }
  73. /**
  74. * get the part of the path relative to the mountpoint of the storage it's stored in
  75. * @param string path
  76. * @return bool
  77. */
  78. public function getInternalPath($path) {
  79. if (!isset($this->internal_path_cache[$path])) {
  80. $this->internal_path_cache[$path] = OC_Filesystem::getInternalPath($this->getAbsolutePath($path));
  81. }
  82. return $this->internal_path_cache[$path];
  83. }
  84. /**
  85. * get path relative to the root of the view
  86. * @param string path
  87. * @return string
  88. */
  89. public function getRelativePath($path) {
  90. if($this->fakeRoot=='') {
  91. return $path;
  92. }
  93. if(strpos($path, $this->fakeRoot)!==0) {
  94. return null;
  95. }else{
  96. $path=substr($path, strlen($this->fakeRoot));
  97. if(strlen($path)===0) {
  98. return '/';
  99. }else{
  100. return $path;
  101. }
  102. }
  103. }
  104. /**
  105. * get the storage object for a path
  106. * @param string path
  107. * @return OC_Filestorage
  108. */
  109. public function getStorage($path) {
  110. if (!isset($this->storage_cache[$path])) {
  111. $this->storage_cache[$path] = OC_Filesystem::getStorage($this->getAbsolutePath($path));
  112. }
  113. return $this->storage_cache[$path];
  114. }
  115. /**
  116. * get the mountpoint of the storage object for a path
  117. ( note: because a storage is not always mounted inside the fakeroot, the returned mountpoint is relative to the absolute root of the filesystem and doesn't take the chroot into account
  118. *
  119. * @param string path
  120. * @return string
  121. */
  122. public function getMountPoint($path) {
  123. return OC_Filesystem::getMountPoint($this->getAbsolutePath($path));
  124. }
  125. /**
  126. * return the path to a local version of the file
  127. * we need this because we can't know if a file is stored local or not from outside the filestorage and for some purposes a local file is needed
  128. * @param string path
  129. * @return string
  130. */
  131. public function getLocalFile($path) {
  132. $parent=substr($path, 0, strrpos($path,'/'));
  133. if(OC_Filesystem::isValidPath($parent) and $storage=$this->getStorage($path)) {
  134. return $storage->getLocalFile($this->getInternalPath($path));
  135. }
  136. }
  137. /**
  138. * @param string path
  139. * @return string
  140. */
  141. public function getLocalFolder($path) {
  142. $parent=substr($path, 0, strrpos($path,'/'));
  143. if(OC_Filesystem::isValidPath($parent) and $storage=$this->getStorage($path)) {
  144. return $storage->getLocalFolder($this->getInternalPath($path));
  145. }
  146. }
  147. /**
  148. * the following functions operate with arguments and return values identical
  149. * to those of their PHP built-in equivalents. Mostly they are merely wrappers
  150. * for OC_Filestorage via basicOperation().
  151. */
  152. public function mkdir($path) {
  153. return $this->basicOperation('mkdir', $path, array('create', 'write'));
  154. }
  155. public function rmdir($path) {
  156. return $this->basicOperation('rmdir', $path, array('delete'));
  157. }
  158. public function opendir($path) {
  159. return $this->basicOperation('opendir', $path, array('read'));
  160. }
  161. public function readdir($handle) {
  162. $fsLocal= new OC_Filestorage_Local( array( 'datadir' => '/' ) );
  163. return $fsLocal->readdir( $handle );
  164. }
  165. public function is_dir($path) {
  166. if($path=='/') {
  167. return true;
  168. }
  169. return $this->basicOperation('is_dir', $path);
  170. }
  171. public function is_file($path) {
  172. if($path=='/') {
  173. return false;
  174. }
  175. return $this->basicOperation('is_file', $path);
  176. }
  177. public function stat($path) {
  178. return $this->basicOperation('stat', $path);
  179. }
  180. public function filetype($path) {
  181. return $this->basicOperation('filetype', $path);
  182. }
  183. public function filesize($path) {
  184. return $this->basicOperation('filesize', $path);
  185. }
  186. public function readfile($path) {
  187. @ob_end_clean();
  188. $handle=$this->fopen($path, 'rb');
  189. if ($handle) {
  190. $chunkSize = 8192;// 8 MB chunks
  191. while (!feof($handle)) {
  192. echo fread($handle, $chunkSize);
  193. flush();
  194. }
  195. $size=$this->filesize($path);
  196. return $size;
  197. }
  198. return false;
  199. }
  200. /**
  201. * @deprecated Replaced by isReadable() as part of CRUDS
  202. */
  203. public function is_readable($path) {
  204. return $this->basicOperation('isReadable',$path);
  205. }
  206. /**
  207. * @deprecated Replaced by isCreatable(), isUpdatable(), isDeletable() as part of CRUDS
  208. */
  209. public function is_writable($path) {
  210. return $this->basicOperation('isUpdatable',$path);
  211. }
  212. public function isCreatable($path) {
  213. return $this->basicOperation('isCreatable', $path);
  214. }
  215. public function isReadable($path) {
  216. return $this->basicOperation('isReadable', $path);
  217. }
  218. public function isUpdatable($path) {
  219. return $this->basicOperation('isUpdatable', $path);
  220. }
  221. public function isDeletable($path) {
  222. return $this->basicOperation('isDeletable', $path);
  223. }
  224. public function isSharable($path) {
  225. return $this->basicOperation('isSharable', $path);
  226. }
  227. public function file_exists($path) {
  228. if($path=='/') {
  229. return true;
  230. }
  231. return $this->basicOperation('file_exists', $path);
  232. }
  233. public function filectime($path) {
  234. return $this->basicOperation('filectime', $path);
  235. }
  236. public function filemtime($path) {
  237. return $this->basicOperation('filemtime', $path);
  238. }
  239. public function touch($path, $mtime=null) {
  240. return $this->basicOperation('touch', $path, array('write'), $mtime);
  241. }
  242. public function file_get_contents($path) {
  243. return $this->basicOperation('file_get_contents', $path, array('read'));
  244. }
  245. public function file_put_contents($path, $data) {
  246. if(is_resource($data)) {//not having to deal with streams in file_put_contents makes life easier
  247. $absolutePath = OC_Filesystem::normalizePath($this->getAbsolutePath($path));
  248. if (OC_FileProxy::runPreProxies('file_put_contents', $absolutePath, $data) && OC_Filesystem::isValidPath($path)) {
  249. $path = $this->getRelativePath($absolutePath);
  250. $exists = $this->file_exists($path);
  251. $run = true;
  252. if( $this->fakeRoot==OC_Filesystem::getRoot() ){
  253. if(!$exists) {
  254. OC_Hook::emit(
  255. OC_Filesystem::CLASSNAME,
  256. OC_Filesystem::signal_create,
  257. array(
  258. OC_Filesystem::signal_param_path => $path,
  259. OC_Filesystem::signal_param_run => &$run
  260. )
  261. );
  262. }
  263. OC_Hook::emit(
  264. OC_Filesystem::CLASSNAME,
  265. OC_Filesystem::signal_write,
  266. array(
  267. OC_Filesystem::signal_param_path => $path,
  268. OC_Filesystem::signal_param_run => &$run
  269. )
  270. );
  271. }
  272. if(!$run) {
  273. return false;
  274. }
  275. $target=$this->fopen($path, 'w');
  276. if($target) {
  277. $count=OC_Helper::streamCopy($data, $target);
  278. fclose($target);
  279. fclose($data);
  280. if( $this->fakeRoot==OC_Filesystem::getRoot() ){
  281. if(!$exists) {
  282. OC_Hook::emit(
  283. OC_Filesystem::CLASSNAME,
  284. OC_Filesystem::signal_post_create,
  285. array( OC_Filesystem::signal_param_path => $path)
  286. );
  287. }
  288. OC_Hook::emit(
  289. OC_Filesystem::CLASSNAME,
  290. OC_Filesystem::signal_post_write,
  291. array( OC_Filesystem::signal_param_path => $path)
  292. );
  293. }
  294. OC_FileProxy::runPostProxies('file_put_contents', $absolutePath, $count);
  295. return $count > 0;
  296. }else{
  297. return false;
  298. }
  299. }
  300. }else{
  301. return $this->basicOperation('file_put_contents', $path, array('create', 'write'), $data);
  302. }
  303. }
  304. public function unlink($path) {
  305. return $this->basicOperation('unlink', $path, array('delete'));
  306. }
  307. public function deleteAll( $directory, $empty = false ) {
  308. return $this->basicOperation( 'deleteAll', $directory, array('delete'), $empty );
  309. }
  310. public function rename($path1, $path2) {
  311. $postFix1=(substr($path1,-1,1)==='/')?'/':'';
  312. $postFix2=(substr($path2,-1,1)==='/')?'/':'';
  313. $absolutePath1 = OC_Filesystem::normalizePath($this->getAbsolutePath($path1));
  314. $absolutePath2 = OC_Filesystem::normalizePath($this->getAbsolutePath($path2));
  315. if(OC_FileProxy::runPreProxies('rename', $absolutePath1, $absolutePath2) and OC_Filesystem::isValidPath($path2)) {
  316. $path1 = $this->getRelativePath($absolutePath1);
  317. $path2 = $this->getRelativePath($absolutePath2);
  318. if($path1 == null or $path2 == null) {
  319. return false;
  320. }
  321. $run=true;
  322. if( $this->fakeRoot==OC_Filesystem::getRoot() ){
  323. OC_Hook::emit(
  324. OC_Filesystem::CLASSNAME, OC_Filesystem::signal_rename,
  325. array(
  326. OC_Filesystem::signal_param_oldpath => $path1,
  327. OC_Filesystem::signal_param_newpath => $path2,
  328. OC_Filesystem::signal_param_run => &$run
  329. )
  330. );
  331. }
  332. if($run) {
  333. $mp1 = $this->getMountPoint($path1.$postFix1);
  334. $mp2 = $this->getMountPoint($path2.$postFix2);
  335. if($mp1 == $mp2) {
  336. if($storage = $this->getStorage($path1)) {
  337. $result = $storage->rename($this->getInternalPath($path1.$postFix1), $this->getInternalPath($path2.$postFix2));
  338. }
  339. } else {
  340. $source = $this->fopen($path1.$postFix1, 'r');
  341. $target = $this->fopen($path2.$postFix2, 'w');
  342. $count = OC_Helper::streamCopy($source, $target);
  343. $storage1 = $this->getStorage($path1);
  344. $storage1->unlink($this->getInternalPath($path1.$postFix1));
  345. $result = $count>0;
  346. }
  347. if( $this->fakeRoot==OC_Filesystem::getRoot() ){
  348. OC_Hook::emit(
  349. OC_Filesystem::CLASSNAME,
  350. OC_Filesystem::signal_post_rename,
  351. array(
  352. OC_Filesystem::signal_param_oldpath => $path1,
  353. OC_Filesystem::signal_param_newpath => $path2
  354. )
  355. );
  356. }
  357. return $result;
  358. }
  359. }
  360. }
  361. public function copy($path1, $path2) {
  362. $postFix1=(substr($path1,-1,1)==='/')?'/':'';
  363. $postFix2=(substr($path2,-1,1)==='/')?'/':'';
  364. $absolutePath1 = OC_Filesystem::normalizePath($this->getAbsolutePath($path1));
  365. $absolutePath2 = OC_Filesystem::normalizePath($this->getAbsolutePath($path2));
  366. if(OC_FileProxy::runPreProxies('copy', $absolutePath1, $absolutePath2) and OC_Filesystem::isValidPath($path2)) {
  367. $path1 = $this->getRelativePath($absolutePath1);
  368. $path2 = $this->getRelativePath($absolutePath2);
  369. if($path1 == null or $path2 == null) {
  370. return false;
  371. }
  372. $run=true;
  373. if( $this->fakeRoot==OC_Filesystem::getRoot() ){
  374. OC_Hook::emit(
  375. OC_Filesystem::CLASSNAME,
  376. OC_Filesystem::signal_copy,
  377. array(
  378. OC_Filesystem::signal_param_oldpath => $path1,
  379. OC_Filesystem::signal_param_newpath=>$path2,
  380. OC_Filesystem::signal_param_run => &$run
  381. )
  382. );
  383. $exists=$this->file_exists($path2);
  384. if($run and !$exists) {
  385. OC_Hook::emit(
  386. OC_Filesystem::CLASSNAME,
  387. OC_Filesystem::signal_create,
  388. array(
  389. OC_Filesystem::signal_param_path => $path2,
  390. OC_Filesystem::signal_param_run => &$run
  391. )
  392. );
  393. }
  394. if($run) {
  395. OC_Hook::emit(
  396. OC_Filesystem::CLASSNAME,
  397. OC_Filesystem::signal_write,
  398. array(
  399. OC_Filesystem::signal_param_path => $path2,
  400. OC_Filesystem::signal_param_run => &$run
  401. )
  402. );
  403. }
  404. }
  405. if($run) {
  406. $mp1=$this->getMountPoint($path1.$postFix1);
  407. $mp2=$this->getMountPoint($path2.$postFix2);
  408. if($mp1 == $mp2) {
  409. if($storage = $this->getStorage($path1.$postFix1)) {
  410. $result=$storage->copy($this->getInternalPath($path1.$postFix1), $this->getInternalPath($path2.$postFix2));
  411. }
  412. } else {
  413. $source = $this->fopen($path1.$postFix1, 'r');
  414. $target = $this->fopen($path2.$postFix2, 'w');
  415. $result = OC_Helper::streamCopy($source, $target);
  416. }
  417. if( $this->fakeRoot==OC_Filesystem::getRoot() ){
  418. OC_Hook::emit(
  419. OC_Filesystem::CLASSNAME,
  420. OC_Filesystem::signal_post_copy,
  421. array(
  422. OC_Filesystem::signal_param_oldpath => $path1,
  423. OC_Filesystem::signal_param_newpath=>$path2
  424. )
  425. );
  426. if(!$exists) {
  427. OC_Hook::emit(
  428. OC_Filesystem::CLASSNAME,
  429. OC_Filesystem::signal_post_create,
  430. array(OC_Filesystem::signal_param_path => $path2)
  431. );
  432. }
  433. OC_Hook::emit(
  434. OC_Filesystem::CLASSNAME,
  435. OC_Filesystem::signal_post_write,
  436. array( OC_Filesystem::signal_param_path => $path2)
  437. );
  438. } else { // no real copy, file comes from somewhere else, e.g. version rollback -> just update the file cache and the webdav properties without all the other post_write actions
  439. OC_FileCache_Update::update($path2, $this->fakeRoot);
  440. OC_Filesystem::removeETagHook(array("path" => $path2), $this->fakeRoot);
  441. }
  442. return $result;
  443. }
  444. }
  445. }
  446. public function fopen($path, $mode) {
  447. $hooks=array();
  448. switch($mode) {
  449. case 'r':
  450. case 'rb':
  451. $hooks[]='read';
  452. break;
  453. case 'r+':
  454. case 'rb+':
  455. case 'w+':
  456. case 'wb+':
  457. case 'x+':
  458. case 'xb+':
  459. case 'a+':
  460. case 'ab+':
  461. $hooks[]='read';
  462. $hooks[]='write';
  463. break;
  464. case 'w':
  465. case 'wb':
  466. case 'x':
  467. case 'xb':
  468. case 'a':
  469. case 'ab':
  470. $hooks[]='write';
  471. break;
  472. default:
  473. OC_Log::write('core','invalid mode ('.$mode.') for '.$path,OC_Log::ERROR);
  474. }
  475. return $this->basicOperation('fopen', $path, $hooks, $mode);
  476. }
  477. public function toTmpFile($path) {
  478. if(OC_Filesystem::isValidPath($path)) {
  479. $source = $this->fopen($path, 'r');
  480. if($source) {
  481. $extension='';
  482. $extOffset=strpos($path, '.');
  483. if($extOffset !== false) {
  484. $extension=substr($path, strrpos($path,'.'));
  485. }
  486. $tmpFile = OC_Helper::tmpFile($extension);
  487. file_put_contents($tmpFile, $source);
  488. return $tmpFile;
  489. }
  490. }
  491. }
  492. public function fromTmpFile($tmpFile, $path) {
  493. if(OC_Filesystem::isValidPath($path)) {
  494. if(!$tmpFile) {
  495. debug_print_backtrace();
  496. }
  497. $source=fopen($tmpFile, 'r');
  498. if($source) {
  499. $this->file_put_contents($path, $source);
  500. unlink($tmpFile);
  501. return true;
  502. } else {
  503. }
  504. } else {
  505. return false;
  506. }
  507. }
  508. public function getMimeType($path) {
  509. return $this->basicOperation('getMimeType', $path);
  510. }
  511. public function hash($type, $path, $raw = false) {
  512. $postFix=(substr($path,-1,1)==='/')?'/':'';
  513. $absolutePath = OC_Filesystem::normalizePath($this->getAbsolutePath($path));
  514. if (OC_FileProxy::runPreProxies('hash', $absolutePath) && OC_Filesystem::isValidPath($path)) {
  515. $path = $this->getRelativePath($absolutePath);
  516. if ($path == null) {
  517. return false;
  518. }
  519. if (OC_Filesystem::$loaded && $this->fakeRoot == OC_Filesystem::getRoot()) {
  520. OC_Hook::emit(
  521. OC_Filesystem::CLASSNAME,
  522. OC_Filesystem::signal_read,
  523. array( OC_Filesystem::signal_param_path => $path)
  524. );
  525. }
  526. if ($storage = $this->getStorage($path.$postFix)) {
  527. $result = $storage->hash($type, $this->getInternalPath($path.$postFix), $raw);
  528. $result = OC_FileProxy::runPostProxies('hash', $absolutePath, $result);
  529. return $result;
  530. }
  531. }
  532. return null;
  533. }
  534. public function free_space($path='/') {
  535. return $this->basicOperation('free_space', $path);
  536. }
  537. /**
  538. * @brief abstraction layer for basic filesystem functions: wrapper for OC_Filestorage
  539. * @param string $operation
  540. * @param string #path
  541. * @param array (optional) hooks
  542. * @param mixed (optional) $extraParam
  543. * @return mixed
  544. *
  545. * This method takes requests for basic filesystem functions (e.g. reading & writing
  546. * files), processes hooks and proxies, sanitises paths, and finally passes them on to
  547. * OC_Filestorage for delegation to a storage backend for execution
  548. */
  549. private function basicOperation($operation, $path, $hooks=array(), $extraParam=null) {
  550. $postFix=(substr($path,-1,1)==='/')?'/':'';
  551. $absolutePath = OC_Filesystem::normalizePath($this->getAbsolutePath($path));
  552. if(OC_FileProxy::runPreProxies($operation, $absolutePath, $extraParam) and OC_Filesystem::isValidPath($path)) {
  553. $path = $this->getRelativePath($absolutePath);
  554. if($path == null) {
  555. return false;
  556. }
  557. $internalPath = $this->getInternalPath($path.$postFix);
  558. $run=$this->runHooks($hooks,$path);
  559. if($run and $storage = $this->getStorage($path.$postFix)) {
  560. if(!is_null($extraParam)) {
  561. $result = $storage->$operation($internalPath, $extraParam);
  562. } else {
  563. $result = $storage->$operation($internalPath);
  564. }
  565. $result = OC_FileProxy::runPostProxies($operation, $this->getAbsolutePath($path), $result);
  566. if(OC_Filesystem::$loaded and $this->fakeRoot==OC_Filesystem::getRoot()) {
  567. if($operation!='fopen') {//no post hooks for fopen, the file stream is still open
  568. $this->runHooks($hooks,$path,true);
  569. }
  570. }
  571. return $result;
  572. }
  573. }
  574. return null;
  575. }
  576. private function runHooks($hooks,$path,$post=false) {
  577. $prefix=($post)?'post_':'';
  578. $run=true;
  579. if(OC_Filesystem::$loaded and $this->fakeRoot==OC_Filesystem::getRoot()) {
  580. foreach($hooks as $hook) {
  581. if($hook!='read') {
  582. OC_Hook::emit(
  583. OC_Filesystem::CLASSNAME,
  584. $prefix.$hook,
  585. array(
  586. OC_Filesystem::signal_param_run => &$run,
  587. OC_Filesystem::signal_param_path => $path
  588. )
  589. );
  590. } elseif(!$post) {
  591. OC_Hook::emit(
  592. OC_Filesystem::CLASSNAME,
  593. $prefix.$hook,
  594. array(
  595. OC_Filesystem::signal_param_path => $path
  596. )
  597. );
  598. }
  599. }
  600. }
  601. return $run;
  602. }
  603. /**
  604. * check if a file or folder has been updated since $time
  605. * @param int $time
  606. * @return bool
  607. */
  608. public function hasUpdated($path, $time) {
  609. return $this->basicOperation('hasUpdated', $path, array(), $time);
  610. }
  611. }