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

/modules/backend/formwidgets/FileUpload.php

https://gitlab.com/fbi/october
PHP | 455 lines | 265 code | 76 blank | 114 comment | 40 complexity | 9d40e87e2f1cff30df892fc18d282312 MD5 | raw file
  1. <?php namespace Backend\FormWidgets;
  2. use Str;
  3. use Lang;
  4. use Input;
  5. use Request;
  6. use Response;
  7. use Validator;
  8. use System\Models\File;
  9. use ApplicationException;
  10. use Backend\Classes\FormField;
  11. use Backend\Classes\FormWidgetBase;
  12. use Backend\Controllers\Files as FilesController;
  13. use ValidationException;
  14. use Exception;
  15. /**
  16. * File upload field
  17. * Renders a form file uploader field.
  18. *
  19. * Supported options:
  20. * - mode: image-single, image-multi, file-single, file-multi
  21. * - upload-label: Add file
  22. * - empty-label: No file uploaded
  23. *
  24. * @package october\backend
  25. * @author Alexey Bobkov, Samuel Georges
  26. */
  27. class FileUpload extends FormWidgetBase
  28. {
  29. //
  30. // Configurable properties
  31. //
  32. /**
  33. * @var string Prompt to display if no record is selected.
  34. */
  35. public $prompt = 'backend::lang.fileupload.default_prompt';
  36. /**
  37. * @var int Preview image width
  38. */
  39. public $imageWidth = null;
  40. /**
  41. * @var int Preview image height
  42. */
  43. public $imageHeight = null;
  44. /**
  45. * @var mixed Collection of acceptable file types.
  46. */
  47. public $fileTypes = false;
  48. /**
  49. * @var mixed Collection of acceptable mime types.
  50. */
  51. public $mimeTypes = false;
  52. /**
  53. * @var array Options used for generating thumbnails.
  54. */
  55. public $thumbOptions = [
  56. 'mode' => 'crop',
  57. 'extension' => 'auto'
  58. ];
  59. /**
  60. * @var boolean Allow the user to set a caption.
  61. */
  62. public $useCaption = true;
  63. //
  64. // Object properties
  65. //
  66. /**
  67. * {@inheritDoc}
  68. */
  69. protected $defaultAlias = 'fileupload';
  70. /**
  71. * {@inheritDoc}
  72. */
  73. public function init()
  74. {
  75. $this->fillFromConfig([
  76. 'prompt',
  77. 'imageWidth',
  78. 'imageHeight',
  79. 'fileTypes',
  80. 'mimeTypes',
  81. 'thumbOptions',
  82. 'useCaption'
  83. ]);
  84. $this->checkUploadPostback();
  85. }
  86. /**
  87. * {@inheritDoc}
  88. */
  89. public function render()
  90. {
  91. $this->prepareVars();
  92. return $this->makePartial('fileupload');
  93. }
  94. /**
  95. * Prepares the view data
  96. */
  97. protected function prepareVars()
  98. {
  99. if ($this->previewMode) {
  100. $this->useCaption = false;
  101. }
  102. $this->vars['fileList'] = $fileList = $this->getFileList();
  103. $this->vars['singleFile'] = $fileList->first();
  104. $this->vars['displayMode'] = $this->getDisplayMode();
  105. $this->vars['emptyIcon'] = $this->getConfig('emptyIcon', 'icon-plus');
  106. $this->vars['imageHeight'] = $this->imageHeight;
  107. $this->vars['imageWidth'] = $this->imageWidth;
  108. $this->vars['acceptedFileTypes'] = $this->getAcceptedFileTypes(true);
  109. $this->vars['cssDimensions'] = $this->getCssDimensions();
  110. $this->vars['cssBlockDimensions'] = $this->getCssDimensions('block');
  111. $this->vars['useCaption'] = $this->useCaption;
  112. $this->vars['prompt'] = str_replace('%s', '<i class="icon-upload"></i>', trans($this->prompt));
  113. }
  114. protected function getFileList()
  115. {
  116. $list = $this
  117. ->getRelationObject()
  118. ->withDeferred($this->sessionKey)
  119. ->orderBy('sort_order')
  120. ->get()
  121. ;
  122. /*
  123. * Decorate each file with thumb and custom download path
  124. */
  125. $list->each(function($file){
  126. $this->decorateFileAttributes($file);
  127. });
  128. return $list;
  129. }
  130. /**
  131. * Returns the display mode for the file upload. Eg: file-multi, image-single, etc.
  132. * @return string
  133. */
  134. protected function getDisplayMode()
  135. {
  136. $mode = $this->getConfig('mode', 'image');
  137. if (str_contains($mode, '-')) {
  138. return $mode;
  139. }
  140. $relationType = $this->getRelationType();
  141. $mode .= ($relationType == 'attachMany' || $relationType == 'morphMany') ? '-multi' : '-single';
  142. return $mode;
  143. }
  144. /**
  145. * Returns the CSS dimensions for the uploaded image,
  146. * uses auto where no dimension is provided.
  147. * @param string $mode
  148. * @return string
  149. */
  150. protected function getCssDimensions($mode = null)
  151. {
  152. if (!$this->imageWidth && !$this->imageHeight) {
  153. return '';
  154. }
  155. $cssDimensions = '';
  156. if ($mode == 'block') {
  157. $cssDimensions .= ($this->imageWidth)
  158. ? 'width: '.$this->imageWidth.'px;'
  159. : 'width: '.$this->imageHeight.'px;';
  160. $cssDimensions .= ($this->imageHeight)
  161. ? 'height: '.$this->imageHeight.'px;'
  162. : 'height: auto;';
  163. }
  164. else {
  165. $cssDimensions .= ($this->imageWidth)
  166. ? 'width: '.$this->imageWidth.'px;'
  167. : 'width: auto;';
  168. $cssDimensions .= ($this->imageHeight)
  169. ? 'height: '.$this->imageHeight.'px;'
  170. : 'height: auto;';
  171. }
  172. return $cssDimensions;
  173. }
  174. /**
  175. * Returns the specified accepted file types, or the default
  176. * based on the mode. Image mode will return:
  177. * - jpg,jpeg,bmp,png,gif,svg
  178. * @return string
  179. */
  180. public function getAcceptedFileTypes($includeDot = false)
  181. {
  182. $types = $this->fileTypes;
  183. if ($types === false) {
  184. $isImage = starts_with($this->getDisplayMode(), 'image');
  185. $types = implode(',', File::getDefaultFileTypes($isImage));
  186. }
  187. if (!$types || $types == '*') {
  188. return null;
  189. }
  190. if (!is_array($types)) {
  191. $types = explode(',', $types);
  192. }
  193. $types = array_map(function($value) use ($includeDot) {
  194. $value = trim($value);
  195. if (substr($value, 0, 1) == '.') {
  196. $value = substr($value, 1);
  197. }
  198. if ($includeDot) {
  199. $value = '.'.$value;
  200. }
  201. return $value;
  202. }, $types);
  203. return implode(',', $types);
  204. }
  205. /**
  206. * Returns the value as a relation object from the model,
  207. * supports nesting via HTML array.
  208. * @return Relation
  209. */
  210. protected function getRelationObject()
  211. {
  212. list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
  213. if (!$model->hasRelation($attribute)) {
  214. throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
  215. 'class' => get_class($model),
  216. 'relation' => $attribute
  217. ]));
  218. }
  219. return $model->{$attribute}();
  220. }
  221. /**
  222. * Returns the value as a relation type from the model,
  223. * supports nesting via HTML array.
  224. * @return Relation
  225. */
  226. protected function getRelationType()
  227. {
  228. list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
  229. return $model->getRelationType($attribute);
  230. }
  231. /**
  232. * Removes a file attachment.
  233. */
  234. public function onRemoveAttachment()
  235. {
  236. if (($file_id = post('file_id')) && ($file = File::find($file_id))) {
  237. $this->getRelationObject()->remove($file, $this->sessionKey);
  238. }
  239. }
  240. /**
  241. * Sorts file attachments.
  242. */
  243. public function onSortAttachments()
  244. {
  245. if ($sortData = post('sortOrder')) {
  246. $ids = array_keys($sortData);
  247. $orders = array_values($sortData);
  248. $file = new File;
  249. $file->setSortableOrder($ids, $orders);
  250. }
  251. }
  252. /**
  253. * Loads the configuration form for an attachment, allowing title and description to be set.
  254. */
  255. public function onLoadAttachmentConfig()
  256. {
  257. if (($file_id = post('file_id')) && ($file = File::find($file_id))) {
  258. $file = $this->decorateFileAttributes($file);
  259. $this->vars['file'] = $file;
  260. $this->vars['displayMode'] = $this->getDisplayMode();
  261. $this->vars['cssDimensions'] = $this->getCssDimensions();
  262. return $this->makePartial('config_form');
  263. }
  264. throw new ApplicationException('Unable to find file, it may no longer exist');
  265. }
  266. /**
  267. * Commit the changes of the attachment configuration form.
  268. */
  269. public function onSaveAttachmentConfig()
  270. {
  271. try {
  272. if (($file_id = post('file_id')) && ($file = File::find($file_id))) {
  273. $file->title = post('title');
  274. $file->description = post('description');
  275. $file->save();
  276. return ['displayName' => $file->title ?: $file->file_name];
  277. }
  278. throw new ApplicationException('Unable to find file, it may no longer exist');
  279. }
  280. catch (Exception $ex) {
  281. return json_encode(['error' => $ex->getMessage()]);
  282. }
  283. }
  284. /**
  285. * {@inheritDoc}
  286. */
  287. protected function loadAssets()
  288. {
  289. $this->addCss('css/fileupload.css', 'core');
  290. $this->addJs('js/fileupload.js', 'core');
  291. }
  292. /**
  293. * {@inheritDoc}
  294. */
  295. public function getSaveValue($value)
  296. {
  297. return FormField::NO_SAVE_DATA;
  298. }
  299. /**
  300. * Checks the current request to see if it is a postback containing a file upload
  301. * for this particular widget.
  302. */
  303. protected function checkUploadPostback()
  304. {
  305. if (!($uniqueId = Request::header('X-OCTOBER-FILEUPLOAD')) || $uniqueId != $this->getId()) {
  306. return;
  307. }
  308. try {
  309. if (!Input::hasFile('file_data')) {
  310. throw new ApplicationException('File missing from request');
  311. }
  312. $uploadedFile = Input::file('file_data');
  313. $validationRules = ['max:'.File::getMaxFilesize()];
  314. if ($fileTypes = $this->getAcceptedFileTypes()) {
  315. $validationRules[] = 'extensions:'.$fileTypes;
  316. }
  317. if ($this->mimeTypes) {
  318. $validationRules[] = 'mimes:'.$this->mimeTypes;
  319. }
  320. $validation = Validator::make(
  321. ['file_data' => $uploadedFile],
  322. ['file_data' => $validationRules]
  323. );
  324. if ($validation->fails()) {
  325. throw new ValidationException($validation);
  326. }
  327. if (!$uploadedFile->isValid()) {
  328. throw new ApplicationException('File is not valid');
  329. }
  330. $fileRelation = $this->getRelationObject();
  331. $file = new File();
  332. $file->data = $uploadedFile;
  333. $file->is_public = $fileRelation->isPublic();
  334. $file->save();
  335. $fileRelation->add($file, $this->sessionKey);
  336. $file = $this->decorateFileAttributes($file);
  337. $result = [
  338. 'id' => $file->id,
  339. 'thumb' => $file->thumbUrl,
  340. 'path' => $file->pathUrl
  341. ];
  342. Response::json($result, 200)->send();
  343. }
  344. catch (Exception $ex) {
  345. Response::json($ex->getMessage(), 400)->send();
  346. }
  347. exit;
  348. }
  349. /**
  350. * Adds the bespoke attributes used internally by this widget.
  351. * - thumbUrl
  352. * - pathUrl
  353. * @return System\Models\File
  354. */
  355. protected function decorateFileAttributes($file)
  356. {
  357. /*
  358. * File is protected, create a secure public path
  359. */
  360. if (!$file->isPublic()) {
  361. $path = $thumb = FilesController::getDownloadUrl($file);
  362. if ($this->imageWidth || $this->imageHeight) {
  363. $thumb = FilesController::getThumbUrl($file, $this->imageWidth, $this->imageHeight, $this->thumbOptions);
  364. }
  365. }
  366. /*
  367. * Otherwise use public paths
  368. */
  369. else {
  370. $path = $thumb = $file->getPath();
  371. if ($this->imageWidth || $this->imageHeight) {
  372. $thumb = $file->getThumb($this->imageWidth, $this->imageHeight, $this->thumbOptions);
  373. }
  374. }
  375. $file->pathUrl = $path;
  376. $file->thumbUrl = $thumb;
  377. return $file;
  378. }
  379. }