Uploader.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Framework\File;
  7. /**
  8. * File upload class
  9. *
  10. * ATTENTION! This class must be used like abstract class and must added
  11. * validation by protected file extension list to extended class
  12. *
  13. * @api
  14. * @since 100.0.2
  15. */
  16. class Uploader
  17. {
  18. /**
  19. * Uploaded file handle (copy of $_FILES[] element)
  20. *
  21. * @var array
  22. * @access protected
  23. */
  24. protected $_file;
  25. /**
  26. * Uploaded file mime type
  27. *
  28. * @var string
  29. * @access protected
  30. */
  31. protected $_fileMimeType;
  32. /**
  33. * Upload type. Used to right handle $_FILES array.
  34. *
  35. * @var \Magento\Framework\File\Uploader::SINGLE_STYLE|\Magento\Framework\File\Uploader::MULTIPLE_STYLE
  36. * @access protected
  37. */
  38. protected $_uploadType;
  39. /**
  40. * The name of uploaded file. By default it is original file name, but when
  41. * we will change file name, this variable will be changed too.
  42. *
  43. * @var string
  44. * @access protected
  45. */
  46. protected $_uploadedFileName;
  47. /**
  48. * The name of destination directory
  49. *
  50. * @var string
  51. * @access protected
  52. */
  53. protected $_uploadedFileDir;
  54. /**
  55. * If this variable is set to TRUE, our library will be able to automatically create
  56. * non-existent directories.
  57. *
  58. * @var bool
  59. * @access protected
  60. */
  61. protected $_allowCreateFolders = true;
  62. /**
  63. * If this variable is set to TRUE, uploaded file name will be changed if some file with the same
  64. * name already exists in the destination directory (if enabled).
  65. *
  66. * @var bool
  67. * @access protected
  68. */
  69. protected $_allowRenameFiles = false;
  70. /**
  71. * If this variable is set to TRUE, files dispertion will be supported.
  72. *
  73. * @var bool
  74. * @access protected
  75. */
  76. protected $_enableFilesDispersion = false;
  77. /**
  78. * This variable is used both with $_enableFilesDispersion == true
  79. * It helps to avoid problems after migrating from case-insensitive file system to case-insensitive
  80. * (e.g. NTFS->ext or ext->NTFS)
  81. *
  82. * @var bool
  83. * @access protected
  84. */
  85. protected $_caseInsensitiveFilenames = true;
  86. /**
  87. * @var string
  88. * @access protected
  89. */
  90. protected $_dispretionPath = null;
  91. /**
  92. * @var bool
  93. */
  94. protected $_fileExists = false;
  95. /**
  96. * @var null|string[]
  97. */
  98. protected $_allowedExtensions = null;
  99. /**
  100. * Validate callbacks storage
  101. *
  102. * @var array
  103. * @access protected
  104. */
  105. protected $_validateCallbacks = [];
  106. /**
  107. * @var \Magento\Framework\File\Mime
  108. */
  109. private $fileMime;
  110. /**#@+
  111. * File upload type (multiple or single)
  112. */
  113. const SINGLE_STYLE = 0;
  114. const MULTIPLE_STYLE = 1;
  115. /**#@-*/
  116. /**
  117. * Temp file name empty code
  118. */
  119. const TMP_NAME_EMPTY = 666;
  120. /**
  121. * Maximum Image Width resolution in pixels. For image resizing on client side
  122. * @deprecated
  123. * @see \Magento\Framework\Image\Adapter\UploadConfigInterface::getMaxWidth()
  124. */
  125. const MAX_IMAGE_WIDTH = 1920;
  126. /**
  127. * Maximum Image Height resolution in pixels. For image resizing on client side
  128. * @deprecated
  129. * @see \Magento\Framework\Image\Adapter\UploadConfigInterface::getMaxHeight()
  130. */
  131. const MAX_IMAGE_HEIGHT = 1200;
  132. /**
  133. * Resulting of uploaded file
  134. *
  135. * @var array|bool Array with file info keys: path, file. Result is
  136. * FALSE when file not uploaded
  137. */
  138. protected $_result;
  139. /**
  140. * Init upload
  141. *
  142. * @param string|array $fileId
  143. * @param \Magento\Framework\File\Mime|null $fileMime
  144. * @throws \Exception
  145. */
  146. public function __construct(
  147. $fileId,
  148. Mime $fileMime = null
  149. ) {
  150. $this->_setUploadFileId($fileId);
  151. if (!file_exists($this->_file['tmp_name'])) {
  152. $code = empty($this->_file['tmp_name']) ? self::TMP_NAME_EMPTY : 0;
  153. throw new \Exception('The file was not uploaded.', $code);
  154. } else {
  155. $this->_fileExists = true;
  156. }
  157. $this->fileMime = $fileMime ?: \Magento\Framework\App\ObjectManager::getInstance()->get(Mime::class);
  158. }
  159. /**
  160. * After save logic
  161. *
  162. * @param array $result
  163. * @return $this
  164. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  165. */
  166. protected function _afterSave($result)
  167. {
  168. return $this;
  169. }
  170. /**
  171. * Used to save uploaded file into destination folder with original or new file name (if specified).
  172. *
  173. * @param string $destinationFolder
  174. * @param string $newFileName
  175. * @return array
  176. * @throws \Exception
  177. * @SuppressWarnings(PHPMD.NPathComplexity)
  178. */
  179. public function save($destinationFolder, $newFileName = null)
  180. {
  181. $this->_validateFile();
  182. $this->validateDestination($destinationFolder);
  183. $this->_result = false;
  184. $destinationFile = $destinationFolder;
  185. $fileName = isset($newFileName) ? $newFileName : $this->_file['name'];
  186. $fileName = static::getCorrectFileName($fileName);
  187. if ($this->_enableFilesDispersion) {
  188. $fileName = $this->correctFileNameCase($fileName);
  189. $this->setAllowCreateFolders(true);
  190. $this->_dispretionPath = static::getDispersionPath($fileName);
  191. $destinationFile .= $this->_dispretionPath;
  192. $this->_createDestinationFolder($destinationFile);
  193. }
  194. if ($this->_allowRenameFiles) {
  195. $fileName = static::getNewFileName(
  196. static::_addDirSeparator($destinationFile) . $fileName
  197. );
  198. }
  199. $destinationFile = static::_addDirSeparator($destinationFile) . $fileName;
  200. try {
  201. $this->_result = $this->_moveFile($this->_file['tmp_name'], $destinationFile);
  202. } catch (\Exception $e) {
  203. // if the file exists and we had an exception continue anyway
  204. if (file_exists($destinationFile)) {
  205. $this->_result = true;
  206. } else {
  207. throw $e;
  208. }
  209. }
  210. if ($this->_result) {
  211. if ($this->_enableFilesDispersion) {
  212. $fileName = str_replace('\\', '/', self::_addDirSeparator($this->_dispretionPath)) . $fileName;
  213. }
  214. $this->_uploadedFileName = $fileName;
  215. $this->_uploadedFileDir = $destinationFolder;
  216. $this->_result = $this->_file;
  217. $this->_result['path'] = $destinationFolder;
  218. $this->_result['file'] = $fileName;
  219. $this->_afterSave($this->_result);
  220. }
  221. return $this->_result;
  222. }
  223. /**
  224. * Validates destination directory to be writable
  225. *
  226. * @param string $destinationFolder
  227. * @return void
  228. * @throws \Exception
  229. */
  230. private function validateDestination($destinationFolder)
  231. {
  232. if ($this->_allowCreateFolders) {
  233. $this->_createDestinationFolder($destinationFolder);
  234. }
  235. if (!is_writable($destinationFolder)) {
  236. throw new \Exception('Destination folder is not writable or does not exists.');
  237. }
  238. }
  239. /**
  240. * Set access permissions to file.
  241. *
  242. * @param string $file
  243. * @return void
  244. *
  245. * @deprecated 100.0.8
  246. */
  247. protected function chmod($file)
  248. {
  249. chmod($file, 0777);
  250. }
  251. /**
  252. * Move files from TMP folder into destination folder
  253. *
  254. * @param string $tmpPath
  255. * @param string $destPath
  256. * @return bool|void
  257. */
  258. protected function _moveFile($tmpPath, $destPath)
  259. {
  260. if (is_uploaded_file($tmpPath)) {
  261. return move_uploaded_file($tmpPath, $destPath);
  262. } elseif (is_file($tmpPath)) {
  263. return rename($tmpPath, $destPath);
  264. }
  265. }
  266. /**
  267. * Validate file before save
  268. *
  269. * @return void
  270. * @throws \Exception
  271. */
  272. protected function _validateFile()
  273. {
  274. if ($this->_fileExists === false) {
  275. return;
  276. }
  277. //is file extension allowed
  278. if (!$this->checkAllowedExtension($this->getFileExtension())) {
  279. throw new \Exception('Disallowed file type.');
  280. }
  281. //run validate callbacks
  282. foreach ($this->_validateCallbacks as $params) {
  283. if (is_object($params['object'])
  284. && method_exists($params['object'], $params['method'])
  285. && is_callable([$params['object'], $params['method']])
  286. ) {
  287. $params['object']->{$params['method']}($this->_file['tmp_name']);
  288. }
  289. }
  290. }
  291. /**
  292. * Returns extension of the uploaded file
  293. *
  294. * @return string
  295. */
  296. public function getFileExtension()
  297. {
  298. return $this->_fileExists ? pathinfo($this->_file['name'], PATHINFO_EXTENSION) : '';
  299. }
  300. /**
  301. * Add validation callback model for us in self::_validateFile()
  302. *
  303. * @param string $callbackName
  304. * @param object $callbackObject
  305. * @param string $callbackMethod Method name of $callbackObject. It must
  306. * have interface (string $tmpFilePath)
  307. * @return \Magento\Framework\File\Uploader
  308. */
  309. public function addValidateCallback($callbackName, $callbackObject, $callbackMethod)
  310. {
  311. $this->_validateCallbacks[$callbackName] = ['object' => $callbackObject, 'method' => $callbackMethod];
  312. return $this;
  313. }
  314. /**
  315. * Delete validation callback model for us in self::_validateFile()
  316. *
  317. * @param string $callbackName
  318. * @access public
  319. * @return \Magento\Framework\File\Uploader
  320. */
  321. public function removeValidateCallback($callbackName)
  322. {
  323. if (isset($this->_validateCallbacks[$callbackName])) {
  324. unset($this->_validateCallbacks[$callbackName]);
  325. }
  326. return $this;
  327. }
  328. /**
  329. * Correct filename with special chars and spaces
  330. *
  331. * @param string $fileName
  332. * @return string
  333. */
  334. public static function getCorrectFileName($fileName)
  335. {
  336. $fileName = preg_replace('/[^a-z0-9_\\-\\.]+/i', '_', $fileName);
  337. $fileInfo = pathinfo($fileName);
  338. if (preg_match('/^_+$/', $fileInfo['filename'])) {
  339. $fileName = 'file.' . $fileInfo['extension'];
  340. }
  341. return $fileName;
  342. }
  343. /**
  344. * Convert filename to lowercase in case of case-insensitive file names
  345. *
  346. * @param string $fileName
  347. * @return string
  348. */
  349. public function correctFileNameCase($fileName)
  350. {
  351. if ($this->_caseInsensitiveFilenames) {
  352. return strtolower($fileName);
  353. }
  354. return $fileName;
  355. }
  356. /**
  357. * Add directory separator
  358. *
  359. * @param string $dir
  360. * @return string
  361. */
  362. protected static function _addDirSeparator($dir)
  363. {
  364. if (substr($dir, -1) != '/') {
  365. $dir .= '/';
  366. }
  367. return $dir;
  368. }
  369. /**
  370. * Used to check if uploaded file mime type is valid or not
  371. *
  372. * @param string[] $validTypes
  373. * @access public
  374. * @return bool
  375. */
  376. public function checkMimeType($validTypes = [])
  377. {
  378. if (count($validTypes) > 0) {
  379. if (!in_array($this->_getMimeType(), $validTypes)) {
  380. return false;
  381. }
  382. }
  383. return true;
  384. }
  385. /**
  386. * Returns a name of uploaded file
  387. *
  388. * @access public
  389. * @return string
  390. */
  391. public function getUploadedFileName()
  392. {
  393. return $this->_uploadedFileName;
  394. }
  395. /**
  396. * Used to set {@link _allowCreateFolders} value
  397. *
  398. * @param bool $flag
  399. * @access public
  400. * @return $this
  401. */
  402. public function setAllowCreateFolders($flag)
  403. {
  404. $this->_allowCreateFolders = $flag;
  405. return $this;
  406. }
  407. /**
  408. * Used to set {@link _allowRenameFiles} value
  409. *
  410. * @param bool $flag
  411. * @access public
  412. * @return $this
  413. */
  414. public function setAllowRenameFiles($flag)
  415. {
  416. $this->_allowRenameFiles = $flag;
  417. return $this;
  418. }
  419. /**
  420. * Used to set {@link _enableFilesDispersion} value
  421. *
  422. * @param bool $flag
  423. * @access public
  424. * @return $this
  425. */
  426. public function setFilesDispersion($flag)
  427. {
  428. $this->_enableFilesDispersion = $flag;
  429. return $this;
  430. }
  431. /**
  432. * File names Case-sensitivity setter
  433. *
  434. * @param bool $flag
  435. * @return $this
  436. */
  437. public function setFilenamesCaseSensitivity($flag)
  438. {
  439. $this->_caseInsensitiveFilenames = $flag;
  440. return $this;
  441. }
  442. /**
  443. * Set allowed extensions
  444. *
  445. * @param string[] $extensions
  446. * @return $this
  447. */
  448. public function setAllowedExtensions($extensions = [])
  449. {
  450. foreach ((array)$extensions as $extension) {
  451. $this->_allowedExtensions[] = strtolower($extension);
  452. }
  453. return $this;
  454. }
  455. /**
  456. * Check if specified extension is allowed
  457. *
  458. * @param string $extension
  459. * @return boolean
  460. */
  461. public function checkAllowedExtension($extension)
  462. {
  463. if (!is_array($this->_allowedExtensions) || empty($this->_allowedExtensions)) {
  464. return true;
  465. }
  466. return in_array(strtolower($extension), $this->_allowedExtensions);
  467. }
  468. /**
  469. * Return file mime type
  470. *
  471. * @return string
  472. */
  473. private function _getMimeType()
  474. {
  475. return $this->fileMime->getMimeType($this->_file['tmp_name']);
  476. }
  477. /**
  478. * Set upload field id
  479. *
  480. * @param string|array $fileId
  481. * @return void
  482. * @throws \Exception
  483. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  484. */
  485. private function _setUploadFileId($fileId)
  486. {
  487. if (is_array($fileId)) {
  488. $this->_uploadType = self::MULTIPLE_STYLE;
  489. $this->_file = $fileId;
  490. } else {
  491. if (empty($_FILES)) {
  492. throw new \Exception('$_FILES array is empty');
  493. }
  494. preg_match("/^(.*?)\[(.*?)\]$/", $fileId, $file);
  495. if (is_array($file) && count($file) > 0 && !empty($file[0]) && !empty($file[1])) {
  496. array_shift($file);
  497. $this->_uploadType = self::MULTIPLE_STYLE;
  498. $fileAttributes = $_FILES[$file[0]];
  499. $tmpVar = [];
  500. foreach ($fileAttributes as $attributeName => $attributeValue) {
  501. $tmpVar[$attributeName] = $attributeValue[$file[1]];
  502. }
  503. $fileAttributes = $tmpVar;
  504. $this->_file = $fileAttributes;
  505. } elseif (!empty($fileId) && isset($_FILES[$fileId])) {
  506. $this->_uploadType = self::SINGLE_STYLE;
  507. $this->_file = $_FILES[$fileId];
  508. } elseif ($fileId == '') {
  509. throw new \Exception('Invalid parameter given. A valid $_FILES[] identifier is expected.');
  510. }
  511. }
  512. }
  513. /**
  514. * Create destination folder
  515. *
  516. * @param string $destinationFolder
  517. * @return \Magento\Framework\File\Uploader
  518. * @throws \Exception
  519. */
  520. private function _createDestinationFolder($destinationFolder)
  521. {
  522. if (!$destinationFolder) {
  523. return $this;
  524. }
  525. if (substr($destinationFolder, -1) == '/') {
  526. $destinationFolder = substr($destinationFolder, 0, -1);
  527. }
  528. if (!(@is_dir($destinationFolder)
  529. || @mkdir($destinationFolder, 0777, true)
  530. )) {
  531. throw new \Exception("Unable to create directory '{$destinationFolder}'.");
  532. }
  533. return $this;
  534. }
  535. /**
  536. * Get new file name if the same is already exists
  537. *
  538. * @param string $destinationFile
  539. * @return string
  540. */
  541. public static function getNewFileName($destinationFile)
  542. {
  543. $fileInfo = pathinfo($destinationFile);
  544. if (file_exists($destinationFile)) {
  545. $index = 1;
  546. $baseName = $fileInfo['filename'] . '.' . $fileInfo['extension'];
  547. while (file_exists($fileInfo['dirname'] . '/' . $baseName)) {
  548. $baseName = $fileInfo['filename'] . '_' . $index . '.' . $fileInfo['extension'];
  549. $index++;
  550. }
  551. $destFileName = $baseName;
  552. } else {
  553. return $fileInfo['basename'];
  554. }
  555. return $destFileName;
  556. }
  557. /**
  558. * Get dispertion path
  559. *
  560. * @param string $fileName
  561. * @return string
  562. * @deprecated 101.0.4
  563. */
  564. public static function getDispretionPath($fileName)
  565. {
  566. return self::getDispersionPath($fileName);
  567. }
  568. /**
  569. * Get dispertion path
  570. *
  571. * @param string $fileName
  572. * @return string
  573. * @since 101.0.4
  574. */
  575. public static function getDispersionPath($fileName)
  576. {
  577. $char = 0;
  578. $dispertionPath = '';
  579. while ($char < 2 && $char < strlen($fileName)) {
  580. if (empty($dispertionPath)) {
  581. $dispertionPath = '/' . ('.' == $fileName[$char] ? '_' : $fileName[$char]);
  582. } else {
  583. $dispertionPath = self::_addDirSeparator(
  584. $dispertionPath
  585. ) . ('.' == $fileName[$char] ? '_' : $fileName[$char]);
  586. }
  587. $char++;
  588. }
  589. return $dispertionPath;
  590. }
  591. }