PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fFilesystem.php

https://bitbucket.org/wbond/flourish/
PHP | 736 lines | 372 code | 99 blank | 265 comment | 31 complexity | ff3c992e383d0eab4855409e75d50cfc MD5 | raw file
  1. <?php
  2. /**
  3. * Handles filesystem-level tasks including filesystem transactions and the reference map to keep all fFile and fDirectory objects in sync
  4. *
  5. * @copyright Copyright (c) 2008-2010 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Alex Leeds [al] <alex@kingleeds.com>
  8. * @author Will Bond, iMarc LLC [wb-imarc] <will@imarc.net>
  9. * @license http://flourishlib.com/license
  10. *
  11. * @package Flourish
  12. * @link http://flourishlib.com/fFilesystem
  13. *
  14. * @version 1.0.0b16
  15. * @changes 1.0.0b16 Added a call to clearstatcache() to ::rollback() to prevent incorrect data from being returned by fFile::getMTime() and fFile::getSize() [wb, 2010-11-27]
  16. * @changes 1.0.0b15 Fixed ::translateToWebPath() to handle Windows \s [wb, 2010-04-09]
  17. * @changes 1.0.0b14 Added ::recordAppend() [wb, 2010-03-15]
  18. * @changes 1.0.0b13 Changed the way files/directories deleted in a filesystem transaction are handled, including improvements to the exception that is thrown [wb+wb-imarc, 2010-03-05]
  19. * @changes 1.0.0b12 Updated ::convertToBytes() to properly handle integers without a suffix and sizes with fractions [al+wb, 2009-11-14]
  20. * @changes 1.0.0b11 Corrected the API documentation for ::getPathInfo() [wb, 2009-09-09]
  21. * @changes 1.0.0b10 Updated ::updateExceptionMap() to not contain the Exception class parameter hint, allowing NULL to be passed [wb, 2009-08-20]
  22. * @changes 1.0.0b9 Added some performance tweaks to ::createObject() [wb, 2009-08-06]
  23. * @changes 1.0.0b8 Changed ::formatFilesize() to not use decimal places for bytes, add a space before and drop the `B` in suffixes [wb, 2009-07-12]
  24. * @changes 1.0.0b7 Fixed ::formatFilesize() to work when `$bytes` equals zero [wb, 2009-07-08]
  25. * @changes 1.0.0b6 Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
  26. * @changes 1.0.0b5 Changed ::formatFilesize() to use proper uppercase letters instead of lowercase [wb, 2009-06-02]
  27. * @changes 1.0.0b4 Added the ::createObject() method [wb, 2009-01-21]
  28. * @changes 1.0.0b3 Removed some unnecessary error suppresion operators [wb, 2008-12-11]
  29. * @changes 1.0.0b2 Fixed a bug where the filepath and exception maps weren't being updated after a rollback [wb, 2008-12-11]
  30. * @changes 1.0.0b The initial implementation [wb, 2008-03-24]
  31. */
  32. class fFilesystem
  33. {
  34. // The following constants allow for nice looking callbacks to static methods
  35. const addWebPathTranslation = 'fFilesystem::addWebPathTranslation';
  36. const begin = 'fFilesystem::begin';
  37. const commit = 'fFilesystem::commit';
  38. const convertToBytes = 'fFilesystem::convertToBytes';
  39. const createObject = 'fFilesystem::createObject';
  40. const formatFilesize = 'fFilesystem::formatFilesize';
  41. const getPathInfo = 'fFilesystem::getPathInfo';
  42. const hookDeletedMap = 'fFilesystem::hookDeletedMap';
  43. const hookFilenameMap = 'fFilesystem::hookFilenameMap';
  44. const isInsideTransaction = 'fFilesystem::isInsideTransaction';
  45. const makeUniqueName = 'fFilesystem::makeUniqueName';
  46. const recordAppend = 'fFilesystem::recordAppend';
  47. const recordCreate = 'fFilesystem::recordCreate';
  48. const recordDelete = 'fFilesystem::recordDelete';
  49. const recordDuplicate = 'fFilesystem::recordDuplicate';
  50. const recordRename = 'fFilesystem::recordRename';
  51. const recordWrite = 'fFilesystem::recordWrite';
  52. const reset = 'fFilesystem::reset';
  53. const rollback = 'fFilesystem::rollback';
  54. const translateToWebPath = 'fFilesystem::translateToWebPath';
  55. const updateDeletedMap = 'fFilesystem::updateDeletedMap';
  56. const updateFilenameMap = 'fFilesystem::updateFilenameMap';
  57. const updateFilenameMapForDirectory = 'fFilesystem::updateFilenameMapForDirectory';
  58. /**
  59. * Stores the operations to perform when a commit occurs
  60. *
  61. * @var array
  62. */
  63. static private $commit_operations = NULL;
  64. /**
  65. * Maps deletion backtraces to all instances of a file or directory, providing consistency
  66. *
  67. * @var array
  68. */
  69. static private $deleted_map = array();
  70. /**
  71. * Stores file and directory names by reference, allowing all object instances to be updated at once
  72. *
  73. * @var array
  74. */
  75. static private $filename_map = array();
  76. /**
  77. * Stores the operations to perform if a rollback occurs
  78. *
  79. * @var array
  80. */
  81. static private $rollback_operations = NULL;
  82. /**
  83. * Stores a list of search => replace strings for web path translations
  84. *
  85. * @var array
  86. */
  87. static private $web_path_translations = array();
  88. /**
  89. * Adds a directory to the web path translation list
  90. *
  91. * The web path conversion list is a list of directory paths that will be
  92. * converted (from the beginning of filesystem paths) when preparing a path
  93. * for output into HTML.
  94. *
  95. * By default the `$_SERVER['DOCUMENT_ROOT']` will be converted to a blank
  96. * string, in essence stripping it from filesystem paths.
  97. *
  98. * @param string $search_path The path to look for
  99. * @param string $replace_path The path to replace with
  100. * @return void
  101. */
  102. static public function addWebPathTranslation($search_path, $replace_path)
  103. {
  104. // Ensure we have the correct kind of slash for the OS being used
  105. $search_path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $search_path);
  106. $replace_path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $replace_path);
  107. self::$web_path_translations[$search_path] = $replace_path;
  108. }
  109. /**
  110. * Starts a filesystem pseudo-transaction, should only be called when no transaction is in progress.
  111. *
  112. * Flourish filesystem transactions are NOT full ACID-compliant
  113. * transactions, but rather more of an filesystem undo buffer which can
  114. * return the filesystem to the state when ::begin() was called. If your PHP
  115. * script dies in the middle of an operation this functionality will do
  116. * nothing for you and all operations will be retained, except for deletes
  117. * which only occur once the transaction is committed.
  118. *
  119. * @return void
  120. */
  121. static public function begin()
  122. {
  123. if (self::$commit_operations !== NULL) {
  124. throw new fProgrammerException(
  125. 'There is already a filesystem transaction in progress'
  126. );
  127. }
  128. self::$commit_operations = array();
  129. self::$rollback_operations = array();
  130. }
  131. /**
  132. * Commits a filesystem transaction, should only be called when a transaction is in progress
  133. *
  134. * @return void
  135. */
  136. static public function commit()
  137. {
  138. if (!self::isInsideTransaction()) {
  139. throw new fProgrammerException(
  140. 'There is no filesystem transaction in progress to commit'
  141. );
  142. }
  143. $commit_operations = self::$commit_operations;
  144. self::$commit_operations = NULL;
  145. self::$rollback_operations = NULL;
  146. $commit_operations = array_reverse($commit_operations);
  147. foreach ($commit_operations as $operation) {
  148. // Commit operations only include deletes, however it could be a filename or object
  149. if (isset($operation['filename'])) {
  150. unlink($operation['filename']);
  151. } else {
  152. $operation['object']->delete();
  153. }
  154. }
  155. }
  156. /**
  157. * Takes a file size including a unit of measure (i.e. kb, GB, M) and converts it to bytes
  158. *
  159. * Sizes are interpreted using base 2, not base 10. Sizes above 2GB may not
  160. * be accurately represented on 32 bit operating systems.
  161. *
  162. * @param string $size The size to convert to bytes
  163. * @return integer The number of bytes represented by the size
  164. */
  165. static public function convertToBytes($size)
  166. {
  167. if (!preg_match('#^(\d+(?:\.\d+)?)\s*(k|m|g|t)?(ilo|ega|era|iga)?( )?b?(yte(s)?)?$#D', strtolower(trim($size)), $matches)) {
  168. throw new fProgrammerException(
  169. 'The size specified, %s, does not appears to be a valid size',
  170. $size
  171. );
  172. }
  173. if (empty($matches[2])) {
  174. $matches[2] = 'b';
  175. }
  176. $size_map = array('b' => 1,
  177. 'k' => 1024,
  178. 'm' => 1048576,
  179. 'g' => 1073741824,
  180. 't' => 1099511627776);
  181. return round($matches[1] * $size_map[$matches[2]]);
  182. }
  183. /**
  184. * Takes a filesystem path and creates either an fDirectory, fFile or fImage object from it
  185. *
  186. * @throws fValidationException When no path was specified or the path specified does not exist
  187. *
  188. * @param string $path The path to the filesystem object
  189. * @return fDirectory|fFile|fImage
  190. */
  191. static public function createObject($path)
  192. {
  193. if (empty($path)) {
  194. throw new fValidationException(
  195. 'No path was specified'
  196. );
  197. }
  198. if (!is_readable($path)) {
  199. throw new fValidationException(
  200. 'The path specified, %s, does not exist or is not readable',
  201. $path
  202. );
  203. }
  204. if (is_dir($path)) {
  205. return new fDirectory($path, TRUE);
  206. }
  207. if (fImage::isImageCompatible($path)) {
  208. return new fImage($path, TRUE);
  209. }
  210. return new fFile($path, TRUE);
  211. }
  212. /**
  213. * Takes the size of a file in bytes and returns a friendly size in B/K/M/G/T
  214. *
  215. * @param integer $bytes The size of the file in bytes
  216. * @param integer $decimal_places The number of decimal places to display
  217. * @return string
  218. */
  219. static public function formatFilesize($bytes, $decimal_places=1)
  220. {
  221. if ($bytes < 0) {
  222. $bytes = 0;
  223. }
  224. $suffixes = array('B', 'K', 'M', 'G', 'T');
  225. $sizes = array(1, 1024, 1048576, 1073741824, 1099511627776);
  226. $suffix = (!$bytes) ? 0 : floor(log($bytes)/6.9314718);
  227. return number_format($bytes/$sizes[$suffix], ($suffix == 0) ? 0 : $decimal_places) . ' ' . $suffixes[$suffix];
  228. }
  229. /**
  230. * Returns info about a path including dirname, basename, extension and filename
  231. *
  232. * @param string $path The file/directory path to retrieve information about
  233. * @param string $element The piece of information to return: `'dirname'`, `'basename'`, `'extension'`, or `'filename'`
  234. * @return array The file's dirname, basename, extension and filename
  235. */
  236. static public function getPathInfo($path, $element=NULL)
  237. {
  238. $valid_elements = array('dirname', 'basename', 'extension', 'filename');
  239. if ($element !== NULL && !in_array($element, $valid_elements)) {
  240. throw new fProgrammerException(
  241. 'The element specified, %1$s, is invalid. Must be one of: %2$s.',
  242. $element,
  243. join(', ', $valid_elements)
  244. );
  245. }
  246. $path_info = pathinfo($path);
  247. if (!isset($path_info['extension'])) {
  248. $path_info['extension'] = NULL;
  249. }
  250. if (!isset($path_info['filename'])) {
  251. $path_info['filename'] = preg_replace('#\.' . preg_quote($path_info['extension'], '#') . '$#D', '', $path_info['basename']);
  252. }
  253. $path_info['dirname'] .= DIRECTORY_SEPARATOR;
  254. if ($element) {
  255. return $path_info[$element];
  256. }
  257. return $path_info;
  258. }
  259. /**
  260. * Hooks a file/directory into the deleted backtrace map entry for that filename
  261. *
  262. * Since the value is returned by reference, all objects that represent
  263. * this file/directory always see the same backtrace.
  264. *
  265. * @internal
  266. *
  267. * @param string $file The name of the file or directory
  268. * @return mixed Will return `NULL` if no match, or the backtrace array if a match occurs
  269. */
  270. static public function &hookDeletedMap($file)
  271. {
  272. if (!isset(self::$deleted_map[$file])) {
  273. self::$deleted_map[$file] = NULL;
  274. }
  275. return self::$deleted_map[$file];
  276. }
  277. /**
  278. * Hooks a file/directory name to the filename map
  279. *
  280. * Since the value is returned by reference, all objects that represent
  281. * this file/directory will always be update on a rename.
  282. *
  283. * @internal
  284. *
  285. * @param string $file The name of the file or directory
  286. * @return mixed Will return `NULL` if no match, or the exception object if a match occurs
  287. */
  288. static public function &hookFilenameMap($file)
  289. {
  290. if (!isset(self::$filename_map[$file])) {
  291. self::$filename_map[$file] = $file;
  292. }
  293. return self::$filename_map[$file];
  294. }
  295. /**
  296. * Indicates if a transaction is in progress
  297. *
  298. * @return void
  299. */
  300. static public function isInsideTransaction()
  301. {
  302. return is_array(self::$commit_operations);
  303. }
  304. /**
  305. * Changes a filename to be safe for URLs by making it all lower case and changing everything but letters, numers, - and . to _
  306. *
  307. * @param string $filename The filename to clean up
  308. * @return string The cleaned up filename
  309. */
  310. static public function makeURLSafe($filename)
  311. {
  312. $filename = strtolower(trim($filename));
  313. $filename = str_replace("'", '', $filename);
  314. return preg_replace('#[^a-z0-9\-\.]+#', '_', $filename);
  315. }
  316. /**
  317. * Returns a unique name for a file
  318. *
  319. * @param string $file The filename to check
  320. * @param string $new_extension The new extension for the filename, should not include `.`
  321. * @return string The unique file name
  322. */
  323. static public function makeUniqueName($file, $new_extension=NULL)
  324. {
  325. $info = self::getPathInfo($file);
  326. // Change the file extension
  327. if ($new_extension !== NULL) {
  328. $new_extension = ($new_extension) ? '.' . $new_extension : $new_extension;
  329. $file = $info['dirname'] . $info['filename'] . $new_extension;
  330. $info = self::getPathInfo($file);
  331. }
  332. // If there is an extension, be sure to add . before it
  333. $extension = (!empty($info['extension'])) ? '.' . $info['extension'] : '';
  334. // Remove _copy# from the filename to start
  335. $file = preg_replace('#_copy(\d+)' . preg_quote($extension, '#') . '$#D', $extension, $file);
  336. // Look for a unique name by adding _copy# to the end of the file
  337. while (file_exists($file)) {
  338. $info = self::getPathInfo($file);
  339. if (preg_match('#_copy(\d+)' . preg_quote($extension, '#') . '$#D', $file, $match)) {
  340. $file = preg_replace('#_copy(\d+)' . preg_quote($extension, '#') . '$#D', '_copy' . ($match[1]+1) . $extension, $file);
  341. } else {
  342. $file = $info['dirname'] . $info['filename'] . '_copy1' . $extension;
  343. }
  344. }
  345. return $file;
  346. }
  347. /**
  348. * Updates the deleted backtrace for a file or directory
  349. *
  350. * @internal
  351. *
  352. * @param string $file A file or directory name, directories should end in `/` or `\`
  353. * @param array $backtrace The backtrace for this file/directory
  354. * @return void
  355. */
  356. static public function updateDeletedMap($file, $backtrace)
  357. {
  358. self::$deleted_map[$file] = $backtrace;
  359. }
  360. /**
  361. * Updates the filename map, causing all objects representing a file/directory to be updated
  362. *
  363. * @internal
  364. *
  365. * @param string $existing_filename The existing filename
  366. * @param string $new_filename The new filename
  367. * @return void
  368. */
  369. static public function updateFilenameMap($existing_filename, $new_filename)
  370. {
  371. if ($existing_filename == $new_filename) {
  372. return;
  373. }
  374. self::$filename_map[$new_filename] =& self::$filename_map[$existing_filename];
  375. self::$deleted_map[$new_filename] =& self::$deleted_map[$existing_filename];
  376. unset(self::$filename_map[$existing_filename]);
  377. unset(self::$deleted_map[$existing_filename]);
  378. self::$filename_map[$new_filename] = $new_filename;
  379. }
  380. /**
  381. * Updates the filename map recursively, causing all objects representing a directory to be updated
  382. *
  383. * Also updates all files and directories in the specified directory to the new paths.
  384. *
  385. * @internal
  386. *
  387. * @param string $existing_dirname The existing directory name
  388. * @param string $new_dirname The new dirname
  389. * @return void
  390. */
  391. static public function updateFilenameMapForDirectory($existing_dirname, $new_dirname)
  392. {
  393. if ($existing_dirname == $new_dirname) {
  394. return;
  395. }
  396. // Handle the directory name
  397. self::$filename_map[$new_dirname] =& self::$filename_map[$existing_dirname];
  398. self::$deleted_map[$new_dirname] =& self::$deleted_map[$existing_dirname];
  399. unset(self::$filename_map[$existing_dirname]);
  400. unset(self::$deleted_map[$existing_dirname]);
  401. self::$filename_map[$new_dirname] = $new_dirname;
  402. // Handle all of the directories and files inside this directory
  403. foreach (self::$filename_map as $filename => $ignore) {
  404. if (preg_match('#^' . preg_quote($existing_dirname, '#') . '#', $filename)) {
  405. $new_filename = preg_replace(
  406. '#^' . preg_quote($existing_dirname, '#') . '#',
  407. strtr($new_dirname, array('\\' => '\\\\', '$' => '\\$')),
  408. $filename
  409. );
  410. self::$filename_map[$new_filename] =& self::$filename_map[$filename];
  411. self::$deleted_map[$new_filename] =& self::$deleted_map[$filename];
  412. unset(self::$filename_map[$filename]);
  413. unset(self::$deleted_map[$filename]);
  414. self::$filename_map[$new_filename] = $new_filename;
  415. }
  416. }
  417. }
  418. /**
  419. * Stores what data has been added to a file so it can be removed if there is a rollback
  420. *
  421. * @internal
  422. *
  423. * @param fFile $file The file that is being written to
  424. * @param string $data The data being appended to the file
  425. * @return void
  426. */
  427. static public function recordAppend($file, $data)
  428. {
  429. self::$rollback_operations[] = array(
  430. 'action' => 'append',
  431. 'filename' => $file->getPath(),
  432. 'length' => strlen($data)
  433. );
  434. }
  435. /**
  436. * Keeps a record of created files so they can be deleted up in case of a rollback
  437. *
  438. * @internal
  439. *
  440. * @param object $object The new file or directory to get rid of on rollback
  441. * @return void
  442. */
  443. static public function recordCreate($object)
  444. {
  445. self::$rollback_operations[] = array(
  446. 'action' => 'delete',
  447. 'object' => $object
  448. );
  449. }
  450. /**
  451. * Keeps track of file and directory names to delete when a transaction is committed
  452. *
  453. * @internal
  454. *
  455. * @param fFile|fDirectory $object The filesystem object to delete
  456. * @return void
  457. */
  458. static public function recordDelete($object)
  459. {
  460. self::$commit_operations[] = array(
  461. 'action' => 'delete',
  462. 'object' => $object
  463. );
  464. }
  465. /**
  466. * Keeps a record of duplicated files so they can be cleaned up in case of a rollback
  467. *
  468. * @internal
  469. *
  470. * @param fFile $file The duplicate file to get rid of on rollback
  471. * @return void
  472. */
  473. static public function recordDuplicate($file)
  474. {
  475. self::$rollback_operations[] = array(
  476. 'action' => 'delete',
  477. 'filename' => $file->getPath()
  478. );
  479. }
  480. /**
  481. * Keeps a temp file in place of the old filename so the file can be restored during a rollback
  482. *
  483. * @internal
  484. *
  485. * @param string $old_name The old file or directory name
  486. * @param string $new_name The new file or directory name
  487. * @return void
  488. */
  489. static public function recordRename($old_name, $new_name)
  490. {
  491. self::$rollback_operations[] = array(
  492. 'action' => 'rename',
  493. 'old_name' => $old_name,
  494. 'new_name' => $new_name
  495. );
  496. // Create the file with no content to prevent overwriting by another process
  497. file_put_contents($old_name, '');
  498. self::$commit_operations[] = array(
  499. 'action' => 'delete',
  500. 'filename' => $old_name
  501. );
  502. }
  503. /**
  504. * Keeps backup copies of files so they can be restored if there is a rollback
  505. *
  506. * @internal
  507. *
  508. * @param fFile $file The file that is being written to
  509. * @return void
  510. */
  511. static public function recordWrite($file)
  512. {
  513. self::$rollback_operations[] = array(
  514. 'action' => 'write',
  515. 'filename' => $file->getPath(),
  516. 'old_data' => file_get_contents($file->getPath())
  517. );
  518. }
  519. /**
  520. * Resets the configuration of the class
  521. *
  522. * @internal
  523. *
  524. * @return void
  525. */
  526. static public function reset()
  527. {
  528. self::rollback();
  529. self::$commit_operations = NULL;
  530. self::$deleted_map = array();
  531. self::$filename_map = array();
  532. self::$rollback_operations = NULL;
  533. self::$web_path_translations = array();
  534. }
  535. /**
  536. * Rolls back a filesystem transaction, it is safe to rollback when no transaction is in progress
  537. *
  538. * @return void
  539. */
  540. static public function rollback()
  541. {
  542. if (self::$rollback_operations === NULL) {
  543. return;
  544. }
  545. self::$rollback_operations = array_reverse(self::$rollback_operations);
  546. $clear_cache = FALSE;
  547. foreach (self::$rollback_operations as $operation) {
  548. switch($operation['action']) {
  549. case 'append':
  550. $current_length = filesize($operation['filename']);
  551. $handle = fopen($operation['filename'], 'r+');
  552. ftruncate($handle, $current_length - $operation['length']);
  553. fclose($handle);
  554. $clear_cache = TRUE;
  555. break;
  556. case 'delete':
  557. self::updateDeletedMap(
  558. $operation['filename'],
  559. debug_backtrace()
  560. );
  561. unlink($operation['filename']);
  562. fFilesystem::updateFilenameMap($operation['filename'], '*DELETED at ' . time() . ' with token ' . uniqid('', TRUE) . '* ' . $operation['filename']);
  563. break;
  564. case 'write':
  565. file_put_contents($operation['filename'], $operation['old_data']);
  566. $clear_cache = TRUE;
  567. break;
  568. case 'rename':
  569. fFilesystem::updateFilenameMap($operation['new_name'], $operation['old_name']);
  570. rename($operation['new_name'], $operation['old_name']);
  571. break;
  572. }
  573. }
  574. // All files to be deleted should have their backtraces erased
  575. foreach (self::$commit_operations as $operation) {
  576. if (isset($operation['object'])) {
  577. self::updateDeletedMap($operation['object']->getPath(), NULL);
  578. fFilesystem::updateFilenameMap($operation['object']->getPath(), preg_replace('#*DELETED at \d+ with token [\w.]+* #', '', $operation['filename']));
  579. }
  580. }
  581. self::$commit_operations = NULL;
  582. self::$rollback_operations = NULL;
  583. if ($clear_cache) {
  584. clearstatcache();
  585. }
  586. }
  587. /**
  588. * Takes a filesystem path and translates it to a web path using the rules added
  589. *
  590. * @param string $path The path to translate
  591. * @return string The filesystem path translated to a web path
  592. */
  593. static public function translateToWebPath($path)
  594. {
  595. $translations = array(realpath($_SERVER['DOCUMENT_ROOT']) => '') + self::$web_path_translations;
  596. foreach ($translations as $search => $replace) {
  597. $path = preg_replace(
  598. '#^' . preg_quote($search, '#') . '#',
  599. strtr($replace, array('\\' => '\\\\', '$' => '\\$')),
  600. $path
  601. );
  602. }
  603. return str_replace('\\', '/', $path);
  604. }
  605. /**
  606. * Forces use as a static class
  607. *
  608. * @return fFilesystem
  609. */
  610. private function __construct() { }
  611. }
  612. /**
  613. * Copyright (c) 2008-2010 Will Bond <will@flourishlib.com>, others
  614. *
  615. * Permission is hereby granted, free of charge, to any person obtaining a copy
  616. * of this software and associated documentation files (the "Software"), to deal
  617. * in the Software without restriction, including without limitation the rights
  618. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  619. * copies of the Software, and to permit persons to whom the Software is
  620. * furnished to do so, subject to the following conditions:
  621. *
  622. * The above copyright notice and this permission notice shall be included in
  623. * all copies or substantial portions of the Software.
  624. *
  625. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  626. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  627. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  628. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  629. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  630. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  631. * THE SOFTWARE.
  632. */