mapper.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <?php
  2. namespace OC\Files;
  3. /**
  4. * class Mapper is responsible to translate logical paths to physical paths and reverse
  5. */
  6. class Mapper
  7. {
  8. private $unchangedPhysicalRoot;
  9. public function __construct($rootDir) {
  10. $this->unchangedPhysicalRoot = $rootDir;
  11. }
  12. /**
  13. * @param string $logicPath
  14. * @param bool $create indicates if the generated physical name shall be stored in the database or not
  15. * @return string the physical path
  16. */
  17. public function logicToPhysical($logicPath, $create) {
  18. $physicalPath = $this->resolveLogicPath($logicPath);
  19. if ($physicalPath !== null) {
  20. return $physicalPath;
  21. }
  22. return $this->create($logicPath, $create);
  23. }
  24. /**
  25. * @param string $physicalPath
  26. * @return string
  27. */
  28. public function physicalToLogic($physicalPath) {
  29. $logicPath = $this->resolvePhysicalPath($physicalPath);
  30. if ($logicPath !== null) {
  31. return $logicPath;
  32. }
  33. $this->insert($physicalPath, $physicalPath);
  34. return $physicalPath;
  35. }
  36. /**
  37. * @param string $path
  38. * @param bool $isLogicPath indicates if $path is logical or physical
  39. * @param boolean $recursive
  40. * @return void
  41. */
  42. public function removePath($path, $isLogicPath, $recursive) {
  43. if ($recursive) {
  44. $path=$path.'%';
  45. }
  46. if ($isLogicPath) {
  47. \OC_DB::executeAudited('DELETE FROM `*PREFIX*file_map` WHERE `logic_path` LIKE ?', array($path));
  48. } else {
  49. \OC_DB::executeAudited('DELETE FROM `*PREFIX*file_map` WHERE `physic_path` LIKE ?', array($path));
  50. }
  51. }
  52. /**
  53. * @param string $path1
  54. * @param string $path2
  55. * @throws \Exception
  56. */
  57. public function copy($path1, $path2)
  58. {
  59. $path1 = $this->resolveRelativePath($path1);
  60. $path2 = $this->resolveRelativePath($path2);
  61. $physicPath1 = $this->logicToPhysical($path1, true);
  62. $physicPath2 = $this->logicToPhysical($path2, true);
  63. $sql = 'SELECT * FROM `*PREFIX*file_map` WHERE `logic_path` LIKE ?';
  64. $result = \OC_DB::executeAudited($sql, array($path1.'%'));
  65. $updateQuery = \OC_DB::prepare('UPDATE `*PREFIX*file_map`'
  66. .' SET `logic_path` = ?'
  67. .' , `logic_path_hash` = ?'
  68. .' , `physic_path` = ?'
  69. .' , `physic_path_hash` = ?'
  70. .' WHERE `logic_path` = ?');
  71. while( $row = $result->fetchRow()) {
  72. $currentLogic = $row['logic_path'];
  73. $currentPhysic = $row['physic_path'];
  74. $newLogic = $path2.$this->stripRootFolder($currentLogic, $path1);
  75. $newPhysic = $physicPath2.$this->stripRootFolder($currentPhysic, $physicPath1);
  76. if ($path1 !== $currentLogic) {
  77. try {
  78. \OC_DB::executeAudited($updateQuery, array($newLogic, md5($newLogic), $newPhysic, md5($newPhysic),
  79. $currentLogic));
  80. } catch (\Exception $e) {
  81. error_log('Mapper::Copy failed '.$currentLogic.' -> '.$newLogic.'\n'.$e);
  82. throw $e;
  83. }
  84. }
  85. }
  86. }
  87. /**
  88. * @param string $path
  89. * @param string $root
  90. * @return false|string
  91. */
  92. public function stripRootFolder($path, $root) {
  93. if (strpos($path, $root) !== 0) {
  94. // throw exception ???
  95. return false;
  96. }
  97. if (strlen($path) > strlen($root)) {
  98. return substr($path, strlen($root));
  99. }
  100. return '';
  101. }
  102. /**
  103. * @param string $logicPath
  104. * @return null
  105. * @throws \OC\DatabaseException
  106. */
  107. private function resolveLogicPath($logicPath) {
  108. $logicPath = $this->resolveRelativePath($logicPath);
  109. $sql = 'SELECT * FROM `*PREFIX*file_map` WHERE `logic_path_hash` = ?';
  110. $result = \OC_DB::executeAudited($sql, array(md5($logicPath)));
  111. $result = $result->fetchRow();
  112. if ($result === false) {
  113. return null;
  114. }
  115. return $result['physic_path'];
  116. }
  117. private function resolvePhysicalPath($physicalPath) {
  118. $physicalPath = $this->resolveRelativePath($physicalPath);
  119. $sql = \OC_DB::prepare('SELECT * FROM `*PREFIX*file_map` WHERE `physic_path_hash` = ?');
  120. $result = \OC_DB::executeAudited($sql, array(md5($physicalPath)));
  121. $result = $result->fetchRow();
  122. return $result['logic_path'];
  123. }
  124. private function resolveRelativePath($path) {
  125. $explodedPath = explode('/', $path);
  126. $pathArray = array();
  127. foreach ($explodedPath as $pathElement) {
  128. if (empty($pathElement) || ($pathElement == '.')) {
  129. continue;
  130. } elseif ($pathElement == '..') {
  131. if (count($pathArray) == 0) {
  132. return false;
  133. }
  134. array_pop($pathArray);
  135. } else {
  136. array_push($pathArray, $pathElement);
  137. }
  138. }
  139. if (substr($path, 0, 1) == '/') {
  140. $path = '/';
  141. } else {
  142. $path = '';
  143. }
  144. return $path.implode('/', $pathArray);
  145. }
  146. /**
  147. * @param string $logicPath
  148. * @param bool $store
  149. * @return string
  150. */
  151. private function create($logicPath, $store) {
  152. $logicPath = $this->resolveRelativePath($logicPath);
  153. $index = 0;
  154. // create the slugified path
  155. $physicalPath = $this->slugifyPath($logicPath);
  156. // detect duplicates
  157. while ($this->resolvePhysicalPath($physicalPath) !== null) {
  158. $physicalPath = $this->slugifyPath($logicPath, $index++);
  159. }
  160. // insert the new path mapping if requested
  161. if ($store) {
  162. $this->insert($logicPath, $physicalPath);
  163. }
  164. return $physicalPath;
  165. }
  166. private function insert($logicPath, $physicalPath) {
  167. $sql = 'INSERT INTO `*PREFIX*file_map` (`logic_path`, `physic_path`, `logic_path_hash`, `physic_path_hash`)
  168. VALUES (?, ?, ?, ?)';
  169. \OC_DB::executeAudited($sql, array($logicPath, $physicalPath, md5($logicPath), md5($physicalPath)));
  170. }
  171. /**
  172. * @param string $path
  173. * @param int $index
  174. * @return string
  175. */
  176. public function slugifyPath($path, $index = null) {
  177. $path = $this->stripRootFolder($path, $this->unchangedPhysicalRoot);
  178. $pathElements = explode('/', $path);
  179. $sluggedElements = array();
  180. foreach ($pathElements as $pathElement) {
  181. // remove empty elements
  182. if (empty($pathElement)) {
  183. continue;
  184. }
  185. $sluggedElements[] = $this->slugify($pathElement);
  186. }
  187. // apply index to file name
  188. if ($index !== null) {
  189. $last = array_pop($sluggedElements);
  190. // if filename contains periods - add index number before last period
  191. if (preg_match('~\.[^\.]+$~i', $last, $extension)) {
  192. array_push($sluggedElements, substr($last, 0, -(strlen($extension[0]))) . '-' . $index . $extension[0]);
  193. } else {
  194. // if filename doesn't contain periods add index ofter the last char
  195. array_push($sluggedElements, $last . '-' . $index);
  196. }
  197. }
  198. $sluggedPath = $this->unchangedPhysicalRoot.implode('/', $sluggedElements);
  199. return $this->resolveRelativePath($sluggedPath);
  200. }
  201. /**
  202. * Modifies a string to remove all non ASCII characters and spaces.
  203. *
  204. * @param string $text
  205. * @return string
  206. */
  207. private function slugify($text) {
  208. $originalText = $text;
  209. // replace non letter or digits or dots by -
  210. $text = preg_replace('~[^\\pL\d\.]+~u', '-', $text);
  211. // trim
  212. $text = trim($text, '-');
  213. // transliterate
  214. if (function_exists('iconv')) {
  215. $text = iconv('utf-8', 'us-ascii//TRANSLIT//IGNORE', $text);
  216. }
  217. // lowercase
  218. $text = strtolower($text);
  219. // remove unwanted characters
  220. $text = preg_replace('~[^-\w\.]+~', '', $text);
  221. // trim ending dots (for security reasons and win compatibility)
  222. $text = preg_replace('~\.+$~', '', $text);
  223. if (empty($text) || \OC\Files\Filesystem::isFileBlacklisted($text)) {
  224. /**
  225. * Item slug would be empty. Previously we used uniqid() here.
  226. * However this means that the behaviour is not reproducible, so
  227. * when uploading files into a "empty" folder, the folders name is
  228. * different.
  229. *
  230. * The other case is, that the slugified name would be a blacklisted
  231. * filename. In this case we just use the same workaround by
  232. * returning the secure md5 hash of the original name.
  233. *
  234. *
  235. * If there would be a md5() hash collision, the deduplicate check
  236. * will spot this and append an index later, so this should not be
  237. * a problem.
  238. */
  239. return md5($originalText);
  240. }
  241. return $text;
  242. }
  243. }