PageRenderTime 54ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/fuel/category_tool/fuel/core/classes/file.php

https://github.com/connvoi/dev
PHP | 776 lines | 492 code | 95 blank | 189 comment | 39 complexity | f90b0f42731efd600ec814808beab3d9 MD5 | raw file
Possible License(s): MIT, BSD-3-Clause
  1. <?php
  2. /**
  3. * Part of the Fuel framework.
  4. *
  5. * @package Fuel
  6. * @version 1.0
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2012 Fuel Development Team
  10. * @link http://fuelphp.com
  11. */
  12. namespace Fuel\Core;
  13. class FileAccessException extends \FuelException {}
  14. class OutsideAreaException extends \OutOfBoundsException {}
  15. class InvalidPathException extends \FileAccessException {}
  16. // ------------------------------------------------------------------------
  17. /**
  18. * File Class
  19. *
  20. * @package Fuel
  21. * @subpackage Core
  22. * @category Core
  23. */
  24. class File
  25. {
  26. /**
  27. * @var array loaded area's
  28. */
  29. protected static $areas = array();
  30. public static function _init()
  31. {
  32. \Config::load('file', true);
  33. // make sure the configured chmod values are octal
  34. $chmod = \Config::get('file.chmod.folders', 0777);
  35. is_string($chmod) and \Config::set('file.chmod.folders', octdec($chmod));
  36. $chmod = \Config::get('file.chmod.files', 0666);
  37. is_string($chmod) and \Config::set('file.chmod.files', octdec($chmod));
  38. static::$areas[null] = \File_Area::forge(\Config::get('file.base_config', array()));
  39. foreach (\Config::get('file.areas', array()) as $name => $config)
  40. {
  41. static::$areas[$name] = \File_Area::forge($config);
  42. }
  43. }
  44. public static function forge(array $config = array())
  45. {
  46. return \File_Area::forge($config);
  47. }
  48. /**
  49. * Instance
  50. *
  51. * @param string|File_Area|null file area name, object or null for base area
  52. * @return File_Area
  53. */
  54. public static function instance($area = null)
  55. {
  56. if ($area instanceof File_Area)
  57. {
  58. return $area;
  59. }
  60. return array_key_exists($area, static::$areas) ? static::$areas[$area] : false;
  61. }
  62. /**
  63. * File & directory objects factory
  64. *
  65. * @param string path to the file or directory
  66. * @param array configuration items
  67. * @param string|File_Area|null file area name, object or null for base area
  68. * @return File_Handler_File
  69. */
  70. public static function get($path, array $config = array(), $area = null)
  71. {
  72. return static::instance($area)->get_handler($path, $config);
  73. }
  74. /**
  75. * Get the url.
  76. *
  77. * @return bool
  78. */
  79. public static function get_url($path, array $config = array(), $area = null)
  80. {
  81. return static::get($path, $config, $area)->get_url();
  82. }
  83. /**
  84. * Create an empty file
  85. *
  86. * @param string directory where to create file
  87. * @param string filename
  88. * @param string|File_Area|null file area name, object or null for base area
  89. * @return bool
  90. */
  91. public static function create($basepath, $name, $contents = null, $area = null)
  92. {
  93. $basepath = rtrim(static::instance($area)->get_path($basepath), '\\/').DS;
  94. $new_file = static::instance($area)->get_path($basepath.$name);
  95. if ( ! is_dir($basepath) or ! is_writable($basepath))
  96. {
  97. throw new \InvalidPathException('Invalid basepath, cannot create file at this location.');
  98. }
  99. elseif (file_exists($new_file))
  100. {
  101. throw new \FileAccessException('File exists already, cannot be created.');
  102. }
  103. $file = static::open_file(@fopen($new_file, 'c'), true, $area);
  104. fwrite($file, $contents);
  105. static::close_file($file, $area);
  106. return true;
  107. }
  108. /**
  109. * Create an empty directory
  110. *
  111. * @param string directory where to create new dir
  112. * @param string dirname
  113. * @param int (octal) file permissions
  114. * @param string|File_Area|null file area name, object or null for non-specific
  115. * @return bool
  116. */
  117. public static function create_dir($basepath, $name, $chmod = null, $area = null)
  118. {
  119. $basepath = rtrim(static::instance($area)->get_path($basepath), '\\/').DS;
  120. $new_dir = static::instance($area)->get_path($basepath.$name);
  121. is_null($chmod) and $chmod = \Config::get('file.chmod.folders', 0777);
  122. if ( ! is_dir($basepath) or ! is_writable($basepath))
  123. {
  124. throw new \InvalidPathException('Invalid basepath, cannot create directory at this location.');
  125. }
  126. elseif (file_exists($new_dir))
  127. {
  128. throw new \FileAccessException('Directory exists already, cannot be created.');
  129. }
  130. $recursive = (strpos($name, '/') !== false or strpos($name, '\\') !== false);
  131. return mkdir($new_dir, $chmod, $recursive);
  132. }
  133. /**
  134. * Read file
  135. *
  136. * @param string file to read
  137. * @param bool whether to use readfile() or file_get_contents()
  138. * @param string|File_Area|null file area name, object or null for base area
  139. * @return IO|string file contents
  140. */
  141. public static function read($path, $as_string = false, $area = null)
  142. {
  143. $path = static::instance($area)->get_path($path);
  144. if( ! file_exists($path) or ! is_file($path))
  145. {
  146. throw new \InvalidPathException('Cannot read file, file does not exists.');
  147. }
  148. $file = static::open_file(@fopen($path, 'r'), LOCK_SH, $area);
  149. $return = $as_string ? file_get_contents($path) : readfile($path);
  150. static::close_file($file, $area);
  151. return $return;
  152. }
  153. /**
  154. * Read directory
  155. *
  156. * @param string directory to read
  157. * @param int depth to recurse directory, 1 is only current and 0 or smaller is unlimited
  158. * @param Array|null array of partial regexes or non-array for default
  159. * @param string|File_Area|null file area name, object or null for base area
  160. * @return array directory contents in an array
  161. */
  162. public static function read_dir($path, $depth = 0, $filter = null, $area = null)
  163. {
  164. $path = rtrim(static::instance($area)->get_path($path), '\\/').DS;
  165. if ( ! is_dir($path))
  166. {
  167. throw new \InvalidPathException('Invalid path, directory cannot be read.');
  168. }
  169. if ( ! $fp = @opendir($path))
  170. {
  171. throw new \FileAccessException('Could not open directory for reading.');
  172. }
  173. // Use default when not set
  174. if ( ! is_array($filter))
  175. {
  176. $filter = array('!^\.');
  177. if ($extensions = static::instance($area)->extensions())
  178. {
  179. foreach($extensions as $ext)
  180. {
  181. $filter[] = '\.'.$ext.'$';
  182. }
  183. }
  184. }
  185. $files = array();
  186. $dirs = array();
  187. $new_depth = $depth - 1;
  188. while (false !== ($file = readdir($fp)))
  189. {
  190. // Remove '.', '..'
  191. if (in_array($file, array('.', '..')))
  192. {
  193. continue;
  194. }
  195. // use filters when given
  196. elseif ( ! empty($filter))
  197. {
  198. $continue = false; // whether or not to continue
  199. $matched = false; // whether any positive pattern matched
  200. $positive = false; // whether positive filters are present
  201. foreach($filter as $f)
  202. {
  203. $not = substr($f, 0, 1) == '!'; // whether it's a negative condition
  204. $f = $not ? substr($f, 1) : $f;
  205. // on negative condition a match leads to a continue
  206. if (($match = preg_match('/'.$f.'/uiD', $file) > 0) and $not)
  207. {
  208. $continue = true;
  209. }
  210. $positive = $positive ?: ! $not; // whether a positive condition was encountered
  211. $matched = $matched ?: ($match and ! $not); // whether one of the filters has matched
  212. }
  213. // continue when negative matched or when positive filters and nothing matched
  214. if ($continue or $positive and ! $matched)
  215. {
  216. continue;
  217. }
  218. }
  219. if (@is_dir($path.$file))
  220. {
  221. // Use recursion when depth not depleted or not limited...
  222. if ($depth < 1 or $new_depth > 0)
  223. {
  224. $dirs[$file.DS] = static::read_dir($path.$file.DS, $new_depth, $filter, $area);
  225. }
  226. // ... or set dir to false when not read
  227. else
  228. {
  229. $dirs[$file.DS] = false;
  230. }
  231. }
  232. else
  233. {
  234. $files[] = $file;
  235. }
  236. }
  237. closedir($fp);
  238. // sort dirs & files naturally and return array with dirs on top and files
  239. uksort($dirs, 'strnatcasecmp');
  240. natcasesort($files);
  241. return array_merge($dirs, $files);
  242. }
  243. /**
  244. * Update a file
  245. *
  246. * @param string directory where to write the file
  247. * @param string filename
  248. * @param string|File_Area|null file area name, object or null for base area
  249. * @return bool
  250. */
  251. public static function update($basepath, $name, $contents = null, $area = null)
  252. {
  253. $basepath = rtrim(static::instance($area)->get_path($basepath), '\\/').DS;
  254. $new_file = static::instance($area)->get_path($basepath.$name);
  255. if ( ! is_dir($basepath) or ! is_writable($basepath))
  256. {
  257. throw new \InvalidPathException('Invalid basepath, cannot update a file at this location.');
  258. }
  259. if ( ! $file = static::open_file(@fopen($new_file, 'w'), true, $area) )
  260. {
  261. throw new \FileAccessException('No write access, cannot update a file.');
  262. }
  263. fwrite($file, $contents);
  264. static::close_file($file, $area);
  265. return true;
  266. }
  267. /**
  268. * Append to a file
  269. *
  270. * @param string directory where to write the file
  271. * @param string filename
  272. * @param string|File_Area|null file area name, object or null for base area
  273. * @return bool
  274. */
  275. public static function append($basepath, $name, $contents = null, $area = null)
  276. {
  277. $basepath = rtrim(static::instance($area)->get_path($basepath), '\\/').DS;
  278. $new_file = static::instance($area)->get_path($basepath.$name);
  279. if ( ! is_dir($basepath) or ! is_writable($basepath))
  280. {
  281. throw new \InvalidPathException('Invalid basepath, cannot append to a file at this location.');
  282. }
  283. elseif ( ! file_exists($new_file))
  284. {
  285. throw new \FileAccessException('File does not exist, cannot be appended.');
  286. }
  287. $file = static::open_file(@fopen($new_file, 'a'), true, $area);
  288. fwrite($file, $contents);
  289. static::close_file($file, $area);
  290. return true;
  291. }
  292. /**
  293. * Get the octal permissions for a file or directory
  294. *
  295. * @param string path to the file or directory
  296. * @param mixed file area name, object or null for base area
  297. * $return string octal file permissions
  298. */
  299. public static function get_permissions($path, $area = null)
  300. {
  301. $path = static::instance($area)->get_path($path);
  302. if ( ! file_exists($path))
  303. {
  304. throw new \InvalidPathException('Path is not a directory or a file, cannot get permissions.');
  305. }
  306. return substr(sprintf('%o', fileperms($path)), -4);
  307. }
  308. /**
  309. * Get a file's or directory's created or modified timestamp.
  310. *
  311. * @param string path to the file or directory
  312. * @param string modified or created
  313. * @param mixed file area name, object or null for base area
  314. * @return int Unix Timestamp
  315. */
  316. public static function get_time($path, $type = 'modified', $area = null)
  317. {
  318. $path = static::instance($area)->get_path($path);
  319. if ( ! file_exists($path))
  320. {
  321. throw new \InvalidPathException('Path is not a directory or a file, cannot get creation timestamp.');
  322. }
  323. if($type === 'modified')
  324. {
  325. return filemtime($path);
  326. }
  327. elseif($type === 'created')
  328. {
  329. return filectime($path);
  330. }
  331. else
  332. {
  333. throw new \UnexpectedValueException('File::time $type must be "modified" or "created".');
  334. }
  335. }
  336. /**
  337. * Get a file's size.
  338. *
  339. * @param string path to the file or directory
  340. * @param mixed file area name, object or null for base area
  341. * @return int the file's size in bytes
  342. */
  343. public static function get_size($path, $area = null)
  344. {
  345. $path = static::instance($area)->get_path($path);
  346. if ( ! file_exists($path))
  347. {
  348. throw new \InvalidPathException('Path is not a directory or a file, cannot get size.');
  349. }
  350. return filesize($path);
  351. }
  352. /**
  353. * Rename directory or file
  354. *
  355. * @param string path to file or directory to rename
  356. * @param string new path (full path, can also cause move)
  357. * @param string|File_Area|null file area name, object or null for non-specific
  358. * @return bool
  359. */
  360. public static function rename($path, $new_path, $area = null)
  361. {
  362. $path = static::instance($area)->get_path($path);
  363. $new_path = static::instance($area)->get_path($new_path);
  364. return rename($path, $new_path);
  365. }
  366. /**
  367. * Alias for rename(), not needed but consistent with other methods
  368. */
  369. public static function rename_dir($path, $new_path, $area = null)
  370. {
  371. return static::rename($path, $new_path, $area);
  372. }
  373. /**
  374. * Copy file
  375. *
  376. * @param string path to file to copy
  377. * @param string new base directory (full path)
  378. * @param string|File_Area|null file area name, object or null for non-specific
  379. * @return bool
  380. */
  381. public static function copy($path, $new_path, $area = null)
  382. {
  383. $path = static::instance($area)->get_path($path);
  384. $new_path = static::instance($area)->get_path($new_path);
  385. if ( ! is_file($path))
  386. {
  387. throw new \InvalidPathException('Cannot copy file: given path is not a file.');
  388. }
  389. elseif (file_exists($new_path))
  390. {
  391. throw new \FileAccessException('Cannot copy file: new path already exists.');
  392. }
  393. return copy($path, $new_path);
  394. }
  395. /**
  396. * Copy directory
  397. *
  398. * @param string path to directory which contents will be copied
  399. * @param string new base directory (full path)
  400. * @param string|File_Area|null file area name, object or null for non-specific
  401. * @return bool
  402. * @throws FileAccessException when something went wrong
  403. */
  404. public static function copy_dir($path, $new_path, $area = null)
  405. {
  406. $path = rtrim(static::instance($area)->get_path($path), '\\/').DS;
  407. $new_path = rtrim(static::instance($area)->get_path($new_path), '\\/').DS;
  408. if ( ! is_dir($path))
  409. {
  410. throw new \InvalidPathException('Cannot copy directory: given path is not a directory: '.$path);
  411. }
  412. elseif ( ! file_exists($new_path))
  413. {
  414. $newpath_dirname = pathinfo($new_path, PATHINFO_DIRNAME);
  415. static::create_dir($newpath_dirname, pathinfo($new_path, PATHINFO_BASENAME), fileperms($newpath_dirname) ?: 0777, $area);
  416. }
  417. $files = static::read_dir($path, -1, array(), $area);
  418. foreach ($files as $dir => $file)
  419. {
  420. if (is_array($file))
  421. {
  422. $check = static::create_dir($new_path.DS, substr($dir, 0, -1), fileperms($path.$dir) ?: 0777, $area);
  423. $check and static::copy_dir($path.$dir.DS, $new_path.$dir, $area);
  424. }
  425. else
  426. {
  427. $check = static::copy($path.$file, $new_path.$file, $area);
  428. }
  429. // abort if something went wrong
  430. if ( ! $check)
  431. {
  432. throw new \FileAccessException('Directory copy aborted prematurely, part of the operation failed during copying: '.(is_array($file) ? $dir : $file));
  433. }
  434. }
  435. }
  436. /**
  437. * Create a new symlink
  438. *
  439. * @param string target of symlink
  440. * @param string destination of symlink
  441. * @param bool true for file, false for directory
  442. * @param string|File_Area|null file area name, object or null for base area
  443. * @return bool
  444. */
  445. public static function symlink($path, $link_path, $is_file = true, $area = null)
  446. {
  447. $path = rtrim(static::instance($area)->get_path($path), '\\/');
  448. $link_path = rtrim(static::instance($area)->get_path($link_path), '\\/');
  449. if ($is_file and ! is_file($path))
  450. {
  451. throw new \InvalidPathException('Cannot symlink: given file does not exist.');
  452. }
  453. if ( ! $is_file and ! is_dir($path))
  454. {
  455. throw new \InvalidPathException('Cannot symlink: given directory does not exist.');
  456. }
  457. elseif (file_exists($link_path))
  458. {
  459. throw new \FileAccessException('Cannot symlink: link path already exists.');
  460. }
  461. return symlink($path, $link_path);
  462. }
  463. /**
  464. * Delete file
  465. *
  466. * @param string path to file to delete
  467. * @param string|File_Area|null file area name, object or null for base area
  468. * @return bool
  469. */
  470. public static function delete($path, $area = null)
  471. {
  472. $path = rtrim(static::instance($area)->get_path($path), '\\/');
  473. if ( ! is_file($path) and ! is_link($path))
  474. {
  475. throw new \InvalidPathException('Cannot delete file: given path "'.$path.'" is not a file.');
  476. }
  477. return unlink($path);
  478. }
  479. /**
  480. * Delete directory
  481. *
  482. * @param string path to directory to delete
  483. * @param bool whether to also delete contents of subdirectories
  484. * @param bool whether to delete the parent dir itself when empty
  485. * @param string|File_Area|null file area name, object or null for base area
  486. * @return bool
  487. */
  488. public static function delete_dir($path, $recursive = true, $delete_top = true, $area = null)
  489. {
  490. $path = rtrim(static::instance($area)->get_path($path), '\\/').DS;
  491. if ( ! is_dir($path))
  492. {
  493. throw new \InvalidPathException('Cannot delete directory: given path is not a directory.');
  494. }
  495. $files = static::read_dir($path, -1, array(), $area);
  496. $not_empty = false;
  497. $check = true;
  498. foreach ($files as $dir => $file)
  499. {
  500. if (is_array($file))
  501. {
  502. if ($recursive)
  503. {
  504. $check = static::delete_dir($path.$dir, true, true, $area);
  505. }
  506. else
  507. {
  508. $not_empty = true;
  509. }
  510. }
  511. else
  512. {
  513. $check = static::delete($path.$file, $area);
  514. }
  515. // abort if something went wrong
  516. if ( ! $check)
  517. {
  518. throw new \FileAccessException('Directory deletion aborted prematurely, part of the operation failed.');
  519. }
  520. }
  521. if ( ! $not_empty and $delete_top)
  522. {
  523. return rmdir($path);
  524. }
  525. return true;
  526. }
  527. /**
  528. * Open and lock file
  529. *
  530. * @param resource|string file resource or path
  531. * @param constant either valid lock constant or true=LOCK_EX / false=LOCK_UN
  532. * @param string|File_Area|null file area name, object or null for base area
  533. */
  534. public static function open_file($path, $lock = true, $area = null)
  535. {
  536. if (is_string($path))
  537. {
  538. $path = static::instance($area)->get_path($path);
  539. $resource = fopen($path, 'r+');
  540. }
  541. else
  542. {
  543. $resource = $path;
  544. }
  545. // Make sure the parameter is a valid resource
  546. if ( ! is_resource($resource))
  547. {
  548. return false;
  549. }
  550. // If locks aren't used, don't lock
  551. if ( ! static::instance($area)->use_locks())
  552. {
  553. return $resource;
  554. }
  555. // Accept valid lock constant or set to LOCK_EX
  556. $lock = in_array($lock, array(LOCK_SH, LOCK_EX, LOCK_NB)) ? $lock : LOCK_EX;
  557. // Try to get a lock, timeout after 5 seconds
  558. $lock_mtime = microtime(true);
  559. while ( ! flock($resource, $lock))
  560. {
  561. if (microtime(true) - $lock_mtime > 5)
  562. {
  563. throw new \FileAccessException('Could not secure file lock, timed out after 5 seconds.');
  564. }
  565. }
  566. return $resource;
  567. }
  568. /**
  569. * Close file resource & unlock
  570. *
  571. * @param resource open file resource
  572. * @param string|File_Area|null file area name, object or null for base area
  573. */
  574. public static function close_file($resource, $area = null)
  575. {
  576. fclose($resource);
  577. // If locks aren't used, don't unlock
  578. if ( ! static::instance($area)->use_locks())
  579. {
  580. return;
  581. }
  582. flock($resource, LOCK_UN);
  583. }
  584. /**
  585. * Get detailed information about a file
  586. *
  587. * @param string file path
  588. * @param string|File_Area|null file area name, object or null for base area
  589. */
  590. public static function file_info($path, $area = null)
  591. {
  592. $info = array(
  593. 'original' => $path,
  594. 'realpath' => '',
  595. 'dirname' => '',
  596. 'basename' => '',
  597. 'filename' => '',
  598. 'extension' => '',
  599. 'mimetype' => '',
  600. 'charset' => '',
  601. 'size' => 0,
  602. 'permissions' => '',
  603. 'time_created' => '',
  604. 'time_modified' => '',
  605. );
  606. if ( ! $info['realpath'] = static::instance($area)->get_path($path) or ! file_exists($info['realpath']))
  607. {
  608. throw new \InvalidPathException('Filename given is not a valid file.');
  609. }
  610. $info = array_merge($info, pathinfo($info['realpath']));
  611. if ( ! $fileinfo = new \finfo(FILEINFO_MIME, \Config::get('file.magic_file', null)))
  612. {
  613. throw new \InvalidArgumentException('Can not retrieve information about this file.');
  614. }
  615. $fileinfo = explode(';', $fileinfo->file($info['realpath']));
  616. $info['mimetype'] = isset($fileinfo[0]) ? $fileinfo[0] : 'application/octet-stream';
  617. if (isset($fileinfo[1]))
  618. {
  619. $fileinfo = explode('=', $fileinfo[1]);
  620. $info['charset'] = isset($fileinfo[1]) ? $fileinfo[1] : '';
  621. }
  622. $info['size'] = static::get_size($info['realpath'], $area);
  623. $info['permissions'] = static::get_permissions($info['realpath'], $area);
  624. $info['time_created'] = static::get_time($info['realpath'], $type = 'created', $area);
  625. $info['time_modified'] = static::get_time($info['realpath'], $type = 'modified', $area);
  626. return $info;
  627. }
  628. /**
  629. * Download a file
  630. *
  631. * @param string file path
  632. * @param string|null custom name for the file to be downloaded
  633. * @param string|null custom mime type or null for file mime type
  634. * @param string|File_Area|null file area name, object or null for base area
  635. * @param bool if false, return instead of exit
  636. */
  637. public static function download($path, $name = null, $mime = null, $area = null, $exit = true)
  638. {
  639. $info = static::file_info($path, $area);
  640. empty($mime) and $mime = $info['mimetype'];
  641. empty($name) and $name = $info['basename'];
  642. if ( ! $file = static::open_file(@fopen($info['realpath'], 'rb'), LOCK_SH, $area))
  643. {
  644. throw new \FileAccessException('Filename given could not be opened for download.');
  645. }
  646. while (ob_get_level() > 0)
  647. {
  648. ob_end_clean();
  649. }
  650. ini_get('zlib.output_compression') and ini_set('zlib.output_compression', 0);
  651. ! ini_get('safe_mode') and set_time_limit(0);
  652. header('Content-Type: '.$mime);
  653. header('Content-Disposition: attachment; filename="'.$name.'"');
  654. header('Content-Description: File Transfer');
  655. header('Content-Length: '.$info['size']);
  656. header('Content-Transfer-Encoding: binary');
  657. header('Expires: 0');
  658. header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
  659. while( ! feof($file))
  660. {
  661. echo fread($file, 2048);
  662. }
  663. static::close_file($file, $area);
  664. if ($exit)
  665. {
  666. \Event::shutdown();
  667. exit;
  668. }
  669. }
  670. }