Detection.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Andreas Fischer <bantu@owncloud.com>
  6. * @author Hendrik Leppelsack <hendrik@leppelsack.de>
  7. * @author Jens-Christian Fischer <jens-christian.fischer@switch.ch>
  8. * @author Joas Schilling <coding@schilljs.com>
  9. * @author Lukas Reschke <lukas@statuscode.ch>
  10. * @author Magnus Walbeck <mw@mwalbeck.org>
  11. * @author Morris Jobke <hey@morrisjobke.de>
  12. * @author Robin Appelman <robin@icewind.nl>
  13. * @author Robin McCorkell <robin@mccorkell.me.uk>
  14. * @author Roeland Jago Douma <roeland@famdouma.nl>
  15. * @author Thomas Tanghus <thomas@tanghus.net>
  16. * @author Vincent Petry <pvince81@owncloud.com>
  17. *
  18. * @license AGPL-3.0
  19. *
  20. * This code is free software: you can redistribute it and/or modify
  21. * it under the terms of the GNU Affero General Public License, version 3,
  22. * as published by the Free Software Foundation.
  23. *
  24. * This program is distributed in the hope that it will be useful,
  25. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  26. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  27. * GNU Affero General Public License for more details.
  28. *
  29. * You should have received a copy of the GNU Affero General Public License, version 3,
  30. * along with this program. If not, see <http://www.gnu.org/licenses/>
  31. *
  32. */
  33. namespace OC\Files\Type;
  34. use OCP\Files\IMimeTypeDetector;
  35. use OCP\IURLGenerator;
  36. /**
  37. * Class Detection
  38. *
  39. * Mimetype detection
  40. *
  41. * @package OC\Files\Type
  42. */
  43. class Detection implements IMimeTypeDetector {
  44. protected $mimetypes = [];
  45. protected $secureMimeTypes = [];
  46. protected $mimetypeIcons = [];
  47. /** @var string[] */
  48. protected $mimeTypeAlias = [];
  49. /** @var IURLGenerator */
  50. private $urlGenerator;
  51. /** @var string */
  52. private $customConfigDir;
  53. /** @var string */
  54. private $defaultConfigDir;
  55. /**
  56. * @param IURLGenerator $urlGenerator
  57. * @param string $customConfigDir
  58. * @param string $defaultConfigDir
  59. */
  60. public function __construct(IURLGenerator $urlGenerator,
  61. $customConfigDir,
  62. $defaultConfigDir) {
  63. $this->urlGenerator = $urlGenerator;
  64. $this->customConfigDir = $customConfigDir;
  65. $this->defaultConfigDir = $defaultConfigDir;
  66. }
  67. /**
  68. * Add an extension -> mimetype mapping
  69. *
  70. * $mimetype is the assumed correct mime type
  71. * The optional $secureMimeType is an alternative to send to send
  72. * to avoid potential XSS.
  73. *
  74. * @param string $extension
  75. * @param string $mimetype
  76. * @param string|null $secureMimeType
  77. */
  78. public function registerType($extension,
  79. $mimetype,
  80. $secureMimeType = null) {
  81. $this->mimetypes[$extension] = array($mimetype, $secureMimeType);
  82. $this->secureMimeTypes[$mimetype] = $secureMimeType ?: $mimetype;
  83. }
  84. /**
  85. * Add an array of extension -> mimetype mappings
  86. *
  87. * The mimetype value is in itself an array where the first index is
  88. * the assumed correct mimetype and the second is either a secure alternative
  89. * or null if the correct is considered secure.
  90. *
  91. * @param array $types
  92. */
  93. public function registerTypeArray($types) {
  94. $this->mimetypes = array_merge($this->mimetypes, $types);
  95. // Update the alternative mimetypes to avoid having to look them up each time.
  96. foreach ($this->mimetypes as $mimeType) {
  97. $this->secureMimeTypes[$mimeType[0]] = isset($mimeType[1]) ? $mimeType[1]: $mimeType[0];
  98. }
  99. }
  100. /**
  101. * Add the mimetype aliases if they are not yet present
  102. */
  103. private function loadAliases() {
  104. if (!empty($this->mimeTypeAlias)) {
  105. return;
  106. }
  107. $this->mimeTypeAlias = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypealiases.dist.json'), true);
  108. if (file_exists($this->customConfigDir . '/mimetypealiases.json')) {
  109. $custom = json_decode(file_get_contents($this->customConfigDir . '/mimetypealiases.json'), true);
  110. $this->mimeTypeAlias = array_merge($this->mimeTypeAlias, $custom);
  111. }
  112. }
  113. /**
  114. * @return string[]
  115. */
  116. public function getAllAliases() {
  117. $this->loadAliases();
  118. return $this->mimeTypeAlias;
  119. }
  120. /**
  121. * Add mimetype mappings if they are not yet present
  122. */
  123. private function loadMappings() {
  124. if (!empty($this->mimetypes)) {
  125. return;
  126. }
  127. $mimetypeMapping = json_decode(file_get_contents($this->defaultConfigDir . '/mimetypemapping.dist.json'), true);
  128. //Check if need to load custom mappings
  129. if (file_exists($this->customConfigDir . '/mimetypemapping.json')) {
  130. $custom = json_decode(file_get_contents($this->customConfigDir . '/mimetypemapping.json'), true);
  131. $mimetypeMapping = array_merge($mimetypeMapping, $custom);
  132. }
  133. $this->registerTypeArray($mimetypeMapping);
  134. }
  135. /**
  136. * @return array
  137. */
  138. public function getAllMappings() {
  139. $this->loadMappings();
  140. return $this->mimetypes;
  141. }
  142. /**
  143. * detect mimetype only based on filename, content of file is not used
  144. *
  145. * @param string $path
  146. * @return string
  147. */
  148. public function detectPath($path) {
  149. $this->loadMappings();
  150. $fileName = basename($path);
  151. // remove leading dot on hidden files with a file extension
  152. $fileName = ltrim($fileName, '.');
  153. // note: leading dot doesn't qualify as extension
  154. if (strpos($fileName, '.') > 0) {
  155. // remove versioning extension: name.v1508946057 and transfer extension: name.ocTransferId2057600214.part
  156. $fileName = preg_replace('!((\.v\d+)|((\.ocTransferId\d+)?\.part))$!', '', $fileName);
  157. //try to guess the type by the file extension
  158. $extension = strrchr($fileName, '.');
  159. if ($extension !== false) {
  160. $extension = strtolower($extension);
  161. $extension = substr($extension, 1); //remove leading .
  162. }
  163. return (isset($this->mimetypes[$extension]) && isset($this->mimetypes[$extension][0]))
  164. ? $this->mimetypes[$extension][0]
  165. : 'application/octet-stream';
  166. } else {
  167. return 'application/octet-stream';
  168. }
  169. }
  170. /**
  171. * detect mimetype only based on the content of file
  172. * @param string $path
  173. * @return string
  174. * @since 18.0.0
  175. */
  176. public function detectContent(string $path): string {
  177. $this->loadMappings();
  178. if (@is_dir($path)) {
  179. // directories are easy
  180. return "httpd/unix-directory";
  181. }
  182. if (function_exists('finfo_open')
  183. && function_exists('finfo_file')
  184. && $finfo = finfo_open(FILEINFO_MIME)) {
  185. $info = @finfo_file($finfo, $path);
  186. finfo_close($finfo);
  187. if ($info) {
  188. $info = strtolower($info);
  189. $mimeType = strpos($info, ';') !== false ? substr($info, 0, strpos($info, ';')) : $info;
  190. $mimeType = $this->getSecureMimeType($mimeType);
  191. if ($mimeType !== 'application/octet-stream') {
  192. return $mimeType;
  193. }
  194. }
  195. }
  196. if (strpos($path, '://') !== false && strpos($path, 'file://') === 0) {
  197. // Is the file wrapped in a stream?
  198. return 'application/octet-stream';
  199. }
  200. if (function_exists('mime_content_type')) {
  201. // use mime magic extension if available
  202. $mimeType = mime_content_type($path);
  203. if ($mimeType !== false) {
  204. $mimeType = $this->getSecureMimeType($mimeType);
  205. if ($mimeType !== 'application/octet-stream') {
  206. return $mimeType;
  207. }
  208. }
  209. }
  210. if (\OC_Helper::canExecute('file')) {
  211. // it looks like we have a 'file' command,
  212. // lets see if it does have mime support
  213. $path = escapeshellarg($path);
  214. $fp = popen("test -f $path && file -b --mime-type $path", 'r');
  215. $mimeType = fgets($fp);
  216. pclose($fp);
  217. if ($mimeType !== false) {
  218. //trim the newline
  219. $mimeType = trim($mimeType);
  220. $mimeType = $this->getSecureMimeType($mimeType);
  221. if ($mimeType !== 'application/octet-stream') {
  222. return $mimeType;
  223. }
  224. }
  225. }
  226. return 'application/octet-stream';
  227. }
  228. /**
  229. * detect mimetype based on both filename and content
  230. *
  231. * @param string $path
  232. * @return string
  233. */
  234. public function detect($path) {
  235. $mimeType = $this->detectPath($path);
  236. if ($mimeType !== 'application/octet-stream') {
  237. return $mimeType;
  238. }
  239. return $this->detectContent($path);
  240. }
  241. /**
  242. * detect mimetype based on the content of a string
  243. *
  244. * @param string $data
  245. * @return string
  246. */
  247. public function detectString($data) {
  248. if (function_exists('finfo_open') and function_exists('finfo_file')) {
  249. $finfo = finfo_open(FILEINFO_MIME);
  250. $info = finfo_buffer($finfo, $data);
  251. return strpos($info, ';') !== false ? substr($info, 0, strpos($info, ';')) : $info;
  252. } else {
  253. $tmpFile = \OC::$server->getTempManager()->getTemporaryFile();
  254. $fh = fopen($tmpFile, 'wb');
  255. fwrite($fh, $data, 8024);
  256. fclose($fh);
  257. $mime = $this->detect($tmpFile);
  258. unset($tmpFile);
  259. return $mime;
  260. }
  261. }
  262. /**
  263. * Get a secure mimetype that won't expose potential XSS.
  264. *
  265. * @param string $mimeType
  266. * @return string
  267. */
  268. public function getSecureMimeType($mimeType) {
  269. $this->loadMappings();
  270. return isset($this->secureMimeTypes[$mimeType])
  271. ? $this->secureMimeTypes[$mimeType]
  272. : 'application/octet-stream';
  273. }
  274. /**
  275. * Get path to the icon of a file type
  276. * @param string $mimetype the MIME type
  277. * @return string the url
  278. */
  279. public function mimeTypeIcon($mimetype) {
  280. $this->loadAliases();
  281. while (isset($this->mimeTypeAlias[$mimetype])) {
  282. $mimetype = $this->mimeTypeAlias[$mimetype];
  283. }
  284. if (isset($this->mimetypeIcons[$mimetype])) {
  285. return $this->mimetypeIcons[$mimetype];
  286. }
  287. // Replace slash and backslash with a minus
  288. $icon = str_replace('/', '-', $mimetype);
  289. $icon = str_replace('\\', '-', $icon);
  290. // Is it a dir?
  291. if ($mimetype === 'dir') {
  292. $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder.svg');
  293. return $this->mimetypeIcons[$mimetype];
  294. }
  295. if ($mimetype === 'dir-shared') {
  296. $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-shared.svg');
  297. return $this->mimetypeIcons[$mimetype];
  298. }
  299. if ($mimetype === 'dir-external') {
  300. $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/folder-external.svg');
  301. return $this->mimetypeIcons[$mimetype];
  302. }
  303. // Icon exists?
  304. try {
  305. $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $icon . '.svg');
  306. return $this->mimetypeIcons[$mimetype];
  307. } catch (\RuntimeException $e) {
  308. // Specified image not found
  309. }
  310. // Try only the first part of the filetype
  311. $mimePart = substr($icon, 0, strpos($icon, '-'));
  312. try {
  313. $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/' . $mimePart . '.svg');
  314. return $this->mimetypeIcons[$mimetype];
  315. } catch (\RuntimeException $e) {
  316. // Image for the first part of the mimetype not found
  317. }
  318. $this->mimetypeIcons[$mimetype] = $this->urlGenerator->imagePath('core', 'filetypes/file.svg');
  319. return $this->mimetypeIcons[$mimetype];
  320. }
  321. }