SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Controllers/Admin/FileAdminController.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Controllers\Admin;
  15.  
  16. use Exception;
  17.  
  18. use Kohana;
  19. use Kohana_404_Exception;
  20.  
  21. use karmabunny\pdb\Exceptions\QueryException;
  22. use Sprout\Exceptions\WorkerJobException;
  23. use Sprout\Helpers\Admin;
  24. use Sprout\Helpers\AdminAuth;
  25. use Sprout\Helpers\AdminPerms;
  26. use Sprout\Helpers\Category;
  27. use Sprout\Helpers\ColModifierBinary;
  28. use Sprout\Helpers\ColModifierLookupArray;
  29. use Sprout\Helpers\Cron;
  30. use Sprout\Helpers\Csrf;
  31. use Sprout\Helpers\Enc;
  32. use Sprout\Helpers\File;
  33. use Sprout\Helpers\FileConstants;
  34. use Sprout\Helpers\FileIndexing;
  35. use Sprout\Helpers\FileUpload;
  36. use Sprout\Helpers\Form;
  37. use Sprout\Helpers\FrontEndSearch;
  38. use Sprout\Helpers\Image;
  39. use Sprout\Helpers\Json;
  40. use Sprout\Helpers\Notification;
  41. use Sprout\Helpers\Pdb;
  42. use Sprout\Helpers\RefineBar;
  43. use Sprout\Helpers\RefineWidgetSelect;
  44. use Sprout\Helpers\RefineWidgetTextbox;
  45. use Sprout\Helpers\Replication;
  46. use Sprout\Helpers\Search;
  47. use Sprout\Helpers\Security;
  48. use Sprout\Helpers\Sprout;
  49. use Sprout\Helpers\Text;
  50. use Sprout\Helpers\Upload;
  51. use Sprout\Helpers\Url;
  52. use Sprout\Helpers\Validator;
  53. use Sprout\Helpers\View;
  54. use Sprout\Helpers\WorkerCtrl;
  55.  
  56.  
  57. /**
  58. * Handles most of the processing for files
  59. **/
  60. class FileAdminController extends HasCategoriesAdminController implements FrontEndSearch
  61. {
  62. protected $controller_name = 'file';
  63. protected $friendly_name = 'Files';
  64. protected $add_defaults = array(
  65. 'categories' => array(),
  66. 'indexing' => 1,
  67. );
  68. protected $category_archive = true;
  69. protected $main_delete = true;
  70.  
  71.  
  72. /**
  73.   * Constructor
  74.   **/
  75. public function __construct()
  76. {
  77. $this->main_columns = [
  78. 'Name' => 'name',
  79. 'Type' => [new ColModifierLookupArray(FileConstants::$type_names), 'type'],
  80. 'Filename' => 'filename',
  81. 'Author' => 'author',
  82. 'Show Author' => [new ColModifierBinary(), 'embed_author'],
  83. ];
  84.  
  85. $this->main_where = array(
  86. "item.type != 0",
  87. "item.name != ''",
  88. );
  89.  
  90. $this->refine_bar = new RefineBar();
  91. $this->refine_bar->setGroup('Files');
  92. $this->refine_bar->addWidget(new RefineWidgetTextbox('name', 'Name'));
  93. $this->refine_bar->addWidget(new RefineWidgetSelect('type', 'Type', FileConstants::$type_names));
  94. $this->refine_bar->addWidget(new RefineWidgetTextbox('filename', 'Filename'));
  95.  
  96. $this->refine_bar->setGroup('Documents');
  97. $this->refine_bar->addWidget(new RefineWidgetSelect('document_type', 'Document Type', Pdb::lookup('document_types')));
  98.  
  99. $this->main_modes['thumb'] = array('Thumbnails', 'grid');
  100.  
  101. parent::__construct();
  102. }
  103.  
  104.  
  105. /**
  106.   * Return the fields to show in the sidebar when adding or editing a record.
  107.   * These fields are shown under a heading of "Visibility"
  108.   *
  109.   * Key is the field name, value is the field label
  110.   *
  111.   * @return array
  112.   */
  113. public function _getVisibilityFields()
  114. {
  115. $file_id = Admin::getRecordId();
  116. $file = !empty($file_id) ? Pdb::get('files', $file_id) : null;
  117. $list = [];
  118.  
  119. if (!empty($file['type']) and $file['type'] == FileConstants::TYPE_IMAGE) $list['embed_author'] = 'Embed author credit in image';
  120.  
  121. $list['enable_indexing'] = 'Show in search results';
  122.  
  123. return $list;
  124. }
  125.  
  126.  
  127. /**
  128.   * Is the "add" action saved?
  129.   * These may be false if the UI provides its own save mechanism (e.g. multi-add)
  130.   *
  131.   * @return bool True if they are saved, false if they are not
  132.   */
  133. public function _isAddSaved()
  134. {
  135. return false;
  136. }
  137.  
  138.  
  139. /**
  140.   * Hook called by _getAddForm() just before the view is rendered
  141.   **/
  142. protected function _addPreRender($view)
  143. {
  144. parent::_addPreRender($view);
  145.  
  146. $opts = array();
  147. $opts['chunk_url'] = 'admin/call/file/ajaxDragdropChunk';
  148. $opts['done_url'] = 'admin/call/file/ajaxDragdropDone';
  149. $opts['form_url'] = 'admin/call/file/ajaxDragdropForm';
  150. $opts['cancel_url'] = 'admin/call/file/ajaxDragdropCancel';
  151. $opts['form_params'] = [];
  152. $opts['form_el'] = '.drag-drop__form';
  153. $opts['max_files'] = 1000;
  154.  
  155. $view->opts = $opts;
  156. }
  157.  
  158.  
  159. /**
  160.   * Save a single chunk of a multi-part file upload
  161.   *
  162.   * @post string chunk Binary data
  163.   * @post int index Chunk index, 0-based
  164.   * @post string code Unique code for this upload
  165.   * @return void Outputs JSON
  166.   **/
  167. public function ajaxDragdropChunk()
  168. {
  169. if (!preg_match('/^[0-9]+$/', $_POST['index'])) {
  170. Json::error('Invalid "index" param');
  171. }
  172. if (!preg_match('/^[A-Za-z0-9]{32}$/', $_POST['code'])) {
  173. Json::error('Invalid "code" param');
  174. }
  175.  
  176. if (!is_dir(APPPATH . 'temp')) {
  177. Json::error('Temporary directory does not exist');
  178. }
  179. if (!is_writable(APPPATH . 'temp')) {
  180. Json::error('Temporary directory is not writable');
  181. }
  182.  
  183. $filename = APPPATH . 'temp/chunk-' . $_POST['code'] . '-' . $_POST['index'] . '.dat';
  184. $result = rename($_FILES['chunk']['tmp_name'], $filename);
  185. if (!$result) {
  186. Json::error('Move of chunk to temporary directory failed');
  187. }
  188.  
  189. Json::confirm();
  190. }
  191.  
  192.  
  193. /**
  194.   * Stitch together uploaded chunks into an actual file
  195.   *
  196.   * Outputs a JSON response.
  197.   * The field "success" will be checked (= 1) to determine success.
  198.   * On error, the field "message" will be used as an error message.
  199.   * Other keys provided are passed to the ajaxDragdropForm method.
  200.   *
  201.   * @post num The total number of chunks uploaded
  202.   * @post string code Unique code for this upload
  203.   * @return void Outputs JSON
  204.   **/
  205. public function ajaxDragdropDone()
  206. {
  207. if (!preg_match('/^[0-9]+$/', $_POST['num'])) {
  208. Json::error('Invalid "num" param');
  209. }
  210. if (!preg_match('/^[A-Za-z0-9]{32}$/', $_POST['code'])) {
  211. Json::error('Invalid "code" param');
  212. }
  213.  
  214. $dest_filename = 'upload-' . time() . '-' . $_POST['code'] . '.dat';
  215.  
  216. try {
  217. $this->stitchChunks(APPPATH . 'temp/' . $dest_filename, $_POST['code'], $_POST['num']);
  218. } catch (Exception $ex) {
  219. Json::error($ex->getMessage());
  220. }
  221.  
  222. Json::confirm(array(
  223. 'tmp_file' => $dest_filename,
  224. ));
  225. }
  226.  
  227.  
  228. /**
  229.   * Stitch together the uploaded file from multiple chunks
  230.   *
  231.   * @param string $dest_filename The destination filename
  232.   * @param string $code Upload code
  233.   * @param string $num_chunks The number of chunks to stitch together
  234.   **/
  235. private function stitchChunks($dest_filename, $code, $num_chunks) {
  236. $num_chunks = (int) $num_chunks;
  237.  
  238. $out = @fopen($dest_filename, 'w');
  239. if (! $out) {
  240. throw new Exception('Unable to open file for writing');
  241. }
  242.  
  243. // Copy chunks into the file. If anything goes wrong, the file will not be complete so bail
  244. $damaged = false;
  245. for ($i = 0; $i < $num_chunks; ++$i) {
  246. $chunk = APPPATH . 'temp/chunk-' . $code . '-' . $i . '.dat';
  247. if (!file_exists($chunk)) {
  248. $damaged = true;
  249. break;
  250. }
  251.  
  252. $in = @fopen($chunk, 'r');
  253. if (! $in) {
  254. $damaged = true;
  255. break;
  256. }
  257.  
  258. $result = @stream_copy_to_stream($in, $out);
  259. if (! $result) {
  260. $damaged = true;
  261. break;
  262. }
  263.  
  264. $result = @fclose($in);
  265. if (! $result) {
  266. $damaged = true;
  267. break;
  268. }
  269. }
  270.  
  271. $result = fclose($out);
  272. if (! $result) {
  273. $damaged = true;
  274. }
  275.  
  276. // Nuke all the chunks prior to error handling
  277. for ($i = 0; $i < $num_chunks; ++$i) {
  278. $chunk = APPPATH . 'temp/chunk-' . $code . '-' . $i . '.dat';
  279. @unlink($chunk);
  280. }
  281.  
  282. if ($damaged) {
  283. throw new Exception('One or more chunks failed to be read');
  284. }
  285. }
  286.  
  287.  
  288. /**
  289.   * Returns the form for updating a file which has been uploaded
  290.   *
  291.   * @get array file File details, as per the File API; 'lastModifiedDate', 'name', 'size', 'type'
  292.   * @get array result The full JSON response from the ajaxDragdropDone call
  293.   * @get array form Details of the form shown above the drag-n-drop field
  294.   * @return void Outputs HTML
  295.   **/
  296. public function ajaxDragdropForm()
  297. {
  298. $_GET['file']['name'] = trim(Enc::cleanfunky($_GET['file']['name']));
  299.  
  300. if (!FileUpload::checkFilename($_GET['file']['name'])) {
  301. echo '<p>This type of file cannot be uploaded.</p>';
  302. return;
  303. }
  304.  
  305. $data = [];
  306. $data['name'] = str_replace('_', ' ', File::getNoext($_GET['file']['name']));
  307.  
  308. // Determine type from extension
  309. $data['type'] = FileConstants::TYPE_OTHER;
  310. $ext = strtolower(File::getExt($_GET['file']['name']));
  311. foreach (FileConstants::$type_exts as $type => $exts) {
  312. if (in_array($ext, $exts)) {
  313. $data['type'] = $type;
  314. break;
  315. }
  316. }
  317.  
  318. // Attempt to use the last modified date as the publish date
  319. $ts = strtotime(@$_GET['file']['lastModifiedDate']);
  320. if (!$ts) $ts = time();
  321. $data['date_published'] = date('Y-m-d', $ts);
  322.  
  323. $data['embed_author'] = 1;
  324.  
  325. $view = new View('sprout/admin/file_add_dragdrop_form');
  326. $view->tmp_file = $_GET['result']['tmp_file'];
  327. $view->orig_file = $_GET['file'];
  328. $view->size_bytes = filesize(APPPATH . 'temp/' . $_GET['result']['tmp_file']);
  329. $view->errors = [];
  330. $view->categories = Pdb::lookup('files_cat_list');
  331.  
  332. if ($data['type'] == FileConstants::TYPE_IMAGE) {
  333. $temp_path = APPPATH . 'temp/' . $view->tmp_file;
  334. try {
  335. $view->shrunk_img = File::base64Thumb($temp_path, 200, 200);
  336.  
  337. $max_dims = Kohana::config('image.original_size');
  338. if (!empty($max_dims)) {
  339. $shrink_original = false;
  340. if ($view->shrunk_img['original_width'] > $max_dims['width']) {
  341. $shrink_original = true;
  342. } else if ($view->shrunk_img['original_height'] > $max_dims['height']) {
  343. $shrink_original = true;
  344. }
  345. $view->shrink_original = $shrink_original;
  346. $data['shrink_original'] = 1;
  347. }
  348.  
  349. } catch (Exception $ex) {
  350. $view->image_too_large = true;
  351. }
  352. }
  353.  
  354. $view->data = $data;
  355.  
  356. // Only one category? Select that. Category specified? Select that.
  357. if (count($view->categories) == 1) {
  358. $view->data['category_id'] = key($view->categories);
  359. } else if (!empty($_GET['form']['category_id'])) {
  360. $view->data['category_id'] = $_GET['form']['category_id'];
  361. }
  362.  
  363. $q = "SELECT id, name
  364. FROM ~document_types
  365. ORDER BY record_order";
  366. $view->document_types = Pdb::q($q, [], 'map');
  367.  
  368. echo $view->render();
  369. }
  370.  
  371.  
  372. /**
  373.   * Handles the drag-and-drop upload form
  374.   *
  375.   * Output JSON should be:
  376.   * success 1 or 0
  377.   * message Error message, if success is 0
  378.   * html Confirmation HTML, if success is 1
  379.   *
  380.   * @return void Outputs JSON
  381.   **/
  382. public function ajaxDragdropSave()
  383. {
  384. Csrf::checkOrDie();
  385.  
  386. $_POST['orig_name'] = trim(Enc::cleanfunky($_POST['orig_name']));
  387. $_POST['name'] = trim(Enc::cleanfunky($_POST['name']));
  388.  
  389. if (!FileUpload::checkFilename($_POST['orig_name'])) {
  390. Json::error('This type of file cannot be uploaded');
  391. }
  392. if (!$_POST['name']) {
  393. Json::error('You must specify a name');
  394. }
  395.  
  396. $type = File::getType($_POST['orig_name']);
  397.  
  398. // For images, calculate the expected RAM requirement of the resizing
  399. // and confirm it's within the memory limit
  400. if ($type == FileConstants::TYPE_IMAGE) {
  401. $dimensions = getimagesize(APPPATH . 'temp/' . $_POST['tmp_file']);
  402. try {
  403. File::calculateResizeRam($dimensions);
  404. } catch (Exception $ex) {
  405. Json::error($ex);
  406. }
  407. }
  408.  
  409. Pdb::transact();
  410.  
  411. $update_fields = array();
  412. $update_fields['name'] = $_POST['name'];
  413. $update_fields['type'] = $type;
  414. $update_fields['date_added'] = Pdb::now();
  415. $update_fields['date_modified'] = Pdb::now();
  416.  
  417. if (isset($_POST['document_type'])) {
  418. $update_fields['document_type'] = $_POST['document_type'];
  419. }
  420.  
  421. if (isset($_POST['date_published'])) {
  422. $update_fields['date_published'] = $_POST['date_published'];
  423. } else {
  424. $update_fields['date_published'] = Pdb::now();
  425. }
  426.  
  427. $update_fields['author'] = @$_POST['author'];
  428. $update_fields['embed_author'] = @$_POST['embed_author'] ? 1 : 0;
  429.  
  430. $update_fields['sha1'] = hash_file('sha1', APPPATH . 'temp/' . $_POST['tmp_file'], false);
  431.  
  432. try {
  433. $file_id = Pdb::insert('files', $update_fields);
  434. } catch (QueryException $ex) {
  435. return Json::error('Database error');
  436. }
  437.  
  438. $filename = $file_id . '_' . File::filenameMakeSane($_POST['orig_name']);
  439.  
  440. // Filename is only set after upload because the ID is in the name
  441. $update_fields = array();
  442. $update_fields['filename'] = $filename;
  443. try {
  444. Pdb::update('files', $update_fields, ['id' => $file_id]);
  445. } catch (QueryException $ex) {
  446. return Json::error('Database error');
  447. }
  448.  
  449. // Update categories
  450. if (!empty($_POST['category_id'])) {
  451. Category::insertInto('files', $file_id, $_POST['category_id']);
  452. }
  453.  
  454. // Actually move the file in
  455. $src = APPPATH . 'temp/' . $_POST['tmp_file'];
  456. if (!empty($_POST['shrink_original'])) {
  457. $size = getimagesize($src);
  458. $max_dims = Kohana::config('image.original_size');
  459.  
  460. if ($size[0] > $max_dims['width'] or $size[1] > $max_dims['height']) {
  461. $temp_path = APPPATH . 'temp/original_image_' . time() . '_' . Sprout::randStr(4);
  462. $temp_path .= '.' . File::getExt($filename);
  463. $img = new Image($src);
  464. $img->resize($max_dims['width'], $max_dims['height']);
  465. $img->save($temp_path);
  466.  
  467. $result = File::putExisting($filename, $temp_path);
  468. unlink($temp_path);
  469. unlink($src);
  470. } else {
  471. $result = File::moveUpload($src, $filename);
  472. }
  473.  
  474. } else {
  475. $result = File::moveUpload($src, $filename);
  476. }
  477. if (!$result) {
  478. return Json::error('Copying temporary file into media repository failed');
  479. }
  480.  
  481. // Index documents, resize images, etc
  482. try {
  483. File::postUploadProcessing($filename, $file_id, $type);
  484. } catch (Exception $ex) {
  485. Json::error($ex);
  486. }
  487.  
  488. Pdb::commit();
  489.  
  490. $html = '<div class="file-upload__item__feedback__response file-upload__item__feedback__response--success file-upload__item__feedback__response--success--not-image">';
  491. $html .= '<p class="file-upload__item__feedback__name"><a href="admin/edit/file/' . $file_id . '" target="_blank">' . Enc::html($filename) . '</a></p>';
  492. $html .= '<p class="file-upload__item__feedback__size">' . File::humanSize(File::size($filename)) . '</p>';
  493. $html .= '</div>';
  494.  
  495. Json::confirm([
  496. 'html' => $html,
  497. 'file_id' => $file_id,
  498. 'filename' => $filename,
  499. ]);
  500. }
  501.  
  502.  
  503. /**
  504.   * Cancel an upload - delete temporary files.
  505.   *
  506.   * May receive two different variations on the provided POST data:
  507.   * [result][tmp_file] Whole file was uploaded
  508.   * [partial_upload][code] Only some chunks of the file have been uploaded
  509.   *
  510.   * @return void
  511.   */
  512. public function ajaxDragdropCancel()
  513. {
  514. // The file upload controller has a perfect implementation of this, so just use that
  515. $ctlr = new \Sprout\Controllers\FileUploadController();
  516. $ctlr->uploadCancel();
  517. }
  518.  
  519.  
  520. /**
  521.   * Matches user input against a list of possible authors for files
  522.   * @return void Outputs JSON directly (see {@see Json::out})
  523.   */
  524. public function ajaxAuthorLookup()
  525. {
  526. if (empty($_GET['term'])) Json::out([]);
  527.  
  528. $terms = preg_split('/\s+/', trim($_GET['term']));
  529.  
  530. // Check extant author list
  531. $conditions = [];
  532. foreach ($terms as $term) {
  533. $conditions[] = ['author', 'CONTAINS', Pdb::likeEscape($term)];
  534. }
  535.  
  536. $params = [];
  537. $clause = Pdb::buildClause($conditions, $params);
  538. $q = "SELECT DISTINCT author
  539. FROM ~files
  540. WHERE {$clause}
  541. ORDER BY author";
  542. Json::out(Pdb::q($q, $params, 'col'));
  543. }
  544.  
  545.  
  546. /**
  547.   * Not used.
  548.   **/
  549. public function _addSave(&$item_id)
  550. {
  551. return false;
  552. }
  553.  
  554.  
  555. /**
  556.   * Used by some AJAX stuff to return a JSON error
  557.   **/
  558. private function ajaxErr($message)
  559. {
  560. Json::out(array('valid' => 0, 'error' => $message));
  561. return 0;
  562. }
  563.  
  564.  
  565. /**
  566.   * Does a quick upload (from the fileselector)
  567.   * Returns JSON.
  568.   **/
  569. public function quickUpload()
  570. {
  571. Csrf::checkOrDie();
  572.  
  573. if (! AdminPerms::controllerAccess('file', 'add')) {
  574. throw new Kohana_404_Exception();
  575. }
  576.  
  577. $result = $this->doUpload(@$_POST['category_id']);
  578.  
  579. echo '<div>', json_encode($result), '</div>';
  580. }
  581.  
  582.  
  583. /**
  584.   * Used by the quick upload tool
  585.   * @param int $category_id ID of category to store file in
  586.   */
  587. private function doUpload($category_id)
  588. {
  589. $category_id = (int) $category_id;
  590.  
  591. // Check upload exists and has valid metadata
  592. $allowed_exts = [];
  593. if (@$_POST['type'] == 'image') {
  594. $allowed_exts = ['png', 'jpg', 'jpeg', 'gif'];
  595. }
  596. try {
  597. $temp_file = FileUpload::verify('admin_quick_upload', 'file', 0, $allowed_exts);
  598. $filename = @$_POST['file'][0];
  599. if (!$filename) {
  600. return ['error' => 'File uploading failed'];
  601. }
  602. } catch (Exception $ex) {
  603. return ['error' => $ex->getMessage()];
  604. }
  605.  
  606. $filename = File::filenameMakeSane($filename);
  607.  
  608. // Don't allow executable files
  609. if (!FileUpload::checkFilename($filename)) {
  610. return array('error' => 'This type of file cannot be uploaded');
  611. }
  612.  
  613. // Check name
  614. $name = trim($_POST['name']);
  615. if (!$name) {
  616. return array('error' => 'No name was supplied');
  617. }
  618.  
  619. // Get type
  620. $file_type = 0;
  621. $ext = File::getExt($filename);
  622. foreach (FileConstants::$type_exts as $type => $exts) {
  623. if (in_array($ext, $exts)) {
  624. $file_type = $type;
  625. break;
  626. }
  627. }
  628.  
  629. if ($file_type == 0) $file_type = FileConstants::TYPE_OTHER;
  630.  
  631.  
  632. Pdb::transact();
  633.  
  634. // Add file
  635. $update_data = array();
  636. $update_data['name'] = $name;
  637. $update_data['filename'] = $filename;
  638. $update_data['type'] = $file_type;
  639. $update_data['date_added'] = Pdb::now();
  640. $update_data['date_modified'] = Pdb::now();
  641. $update_data['date_file_modified'] = Pdb::now();
  642. $update_data['sha1'] = hash_file('sha1', $temp_file, false);
  643.  
  644. try {
  645. $file_id = Pdb::insert('files', $update_data);
  646. } catch (Exception $ex) {
  647. return array('error' => 'Unable to upload file; database error (main)');
  648. }
  649.  
  650. // Add category
  651. if (!empty($_POST['category_new'])) {
  652. if (!AdminPerms::controllerAccess('file', 'categories')) {
  653. return array('error' => 'Unable to create category; no permissions');
  654. }
  655. try {
  656. $category_id = Category::create('files', $_POST['category_new']);
  657. } catch (Exception $ex) {
  658. return array('error' => 'Unable to upload file; database error (cat)');
  659. }
  660. }
  661.  
  662. // Add file category
  663. if ($category_id) {
  664. try {
  665. Category::insertInto('files', $file_id, $category_id);
  666. } catch (Exception $ex) {
  667. return array('error' => 'Unable to upload file; database error (joiner)');
  668. }
  669. }
  670.  
  671. // Upload the file - uses the file id
  672. $filename = $file_id . '_' . $filename;
  673. $result = File::moveUpload($temp_file, $filename);
  674. if (! $result) {
  675. return array('error' => 'Failed to save the uploaded file in media repository');
  676. }
  677.  
  678. // Update file name and do image resizing, text indexing, and other postprocessing
  679. try {
  680. File::postUploadProcessing($filename, $file_id, $file_type);
  681. } catch (Exception $ex) {
  682. return array('error' => $ex->getMessage());
  683. }
  684.  
  685. Pdb::commit();
  686.  
  687.  
  688. return array(
  689. 'id' => $file_id,
  690. 'filename' => $filename,
  691. 'type' => $file_type,
  692. 'cat_id' => $category_id,
  693. 'rel_url' => File::relUrl($filename),
  694. );
  695. }
  696.  
  697.  
  698. /**
  699.   * Pre-render hook
  700.   **/
  701. public function _editPreRender($view, $item_id)
  702. {
  703. if ($view->data['type'] == FileConstants::TYPE_IMAGE) {
  704. $size = File::imageSize($view->item['filename']);
  705.  
  706. $view->img_dimensions = 'Unkown';
  707. $view->sizes = [];
  708. $view->original_image = '';
  709.  
  710. if (empty($size)) {
  711. Notification::error('Image may be missing from server');
  712. return;
  713. }
  714.  
  715. $view->img_dimensions = $size[0] . 'x' . $size[1];
  716.  
  717. $parts = explode('.', $view->item['filename']);
  718. $view->sizes = File::glob($parts[0] . '.*.' . $parts[1]);
  719.  
  720. $image_url = File::resizeUrl($view->data['filename'], 'r200x0');
  721. $image_url .= (strpos($image_url, '?') === false ? '?' : '&');
  722. $view->original_image = $image_url . 'version=' . Sprout::randStr(10);
  723.  
  724. } else if ($view->data['type'] == FileConstants::TYPE_DOCUMENT) {
  725. $view->document_types = Pdb::lookup('document_types');
  726.  
  727. // Date published is a DATETIME, but the datepicker can't handle that
  728. $view->data['date_published'] = date('Y-m-d', strtotime($view->data['date_published']));
  729.  
  730. // Clean up and prepare text preview
  731. $preview = trim(Enc::cleanFunky($view->data['plaintext']));
  732. $preview = Text::limitWords($preview, 50, '...');
  733. $preview = wordwrap($preview, 50);
  734. $view->preview = $preview;
  735. }
  736. }
  737.  
  738.  
  739. /**
  740.   * Return the sub-actions for editing; for spec {@see AdminController::renderSubActions}
  741.   * @return array
  742.   */
  743. public function _getEditSubActions($item_id)
  744. {
  745. $actions = parent::_getEditSubActions($item_id);
  746.  
  747. $actions['usage'] = [
  748. 'url' => 'admin/extra/' . $this->controller_name . '/find_usage/' . $item_id,
  749. 'name' => 'Find Usage',
  750. 'class' => 'icon-link-button icon-before icon-search',
  751. ];
  752.  
  753. return $actions;
  754. }
  755.  
  756.  
  757. /**
  758.   * Saves the provided POST data into this file in the database
  759.   *
  760.   * @param int $item_id The record to update
  761.   * @param bool True on success, false on failure
  762.   * @throws QueryException
  763.   */
  764. public function _editSave($item_id)
  765. {
  766. $item_id = (int) $item_id;
  767.  
  768. $file = Pdb::get('files', $item_id);
  769.  
  770. $_SESSION['admin']['field_values'] = Validator::trim($_POST);
  771.  
  772. $valid = new Validator($_POST);
  773. $valid->required(['name']);
  774. $valid->check('name', 'Validity::length', 1, 200);
  775. $valid->check('description', 'Validity::length', 1, 10000);
  776. $valid->check('author', 'Validity::length', 1, 80);
  777.  
  778. if (!empty($_FILES['replace']['name'])) {
  779. // Check upload is valid
  780. if (!Upload::valid($_FILES['replace'])) {
  781. Notification::error('Error with upload of replacement; you will need to re-select your file');
  782. $valid->addFieldError('replace', 'File upload failed');
  783. }
  784.  
  785. // Check type matches
  786. $file_type = File::getType($_FILES['replace']['name']);
  787. if ($file['type'] != $file_type) {
  788. Notification::error('Error with file upload; you will need to re-select your file');
  789. $valid->addFieldError('replace', 'File must be of type: ' . FileConstants::$type_names[$file['type']]);
  790. }
  791. }
  792.  
  793. if ($valid->hasErrors()) {
  794. $_SESSION['admin']['field_errors'] = $valid->getFieldErrors();
  795. $valid->createNotifications();
  796. return false;
  797. }
  798.  
  799. $needs_regenerate_sizes = false;
  800.  
  801. // Upload the new file
  802. $filename = $file['filename'];
  803. $original_filename = $filename;
  804. if (!empty($_FILES['replace']['name'])) {
  805. $new_filename = $item_id . '_' . File::filenameMakeSane($_FILES['replace']['name']);
  806.  
  807. if (!File::putExisting($new_filename, $_FILES['replace']['tmp_name'])) {
  808. Notification::error('File upload failed');
  809. return false;
  810. }
  811.  
  812. if ($file['type'] == FileConstants::TYPE_DOCUMENT) {
  813. // Do document indexing
  814. $ext = File::getExt($new_filename);
  815. $plain = '';
  816. if (FileIndexing::isExtSupported($ext)) {
  817. $plain = FileIndexing::getPlaintext($new_filename, $ext);
  818. }
  819.  
  820. } else if ($file['type'] == FileConstants::TYPE_IMAGE) {
  821. $needs_regenerate_sizes = true;
  822.  
  823. // No sense in keeping the focal point for a replaced image
  824. $_POST['focal_points'] = '';
  825. }
  826.  
  827. Notification::confirm('New file uploaded successfully');
  828.  
  829. // Delete original
  830. if ($filename != $new_filename) {
  831. File::delete($filename);
  832.  
  833. // Remove any redirects from new filename, to prevent unnecessary redirection
  834. $pattern = 'files/' . File::getNoext($filename) . '.';
  835. $params = [];
  836. $conditions = [['path_exact', 'BEGINS', $pattern]];
  837. $q = "DELETE FROM ~redirects WHERE " . Pdb::buildClause($conditions, $params);
  838. Pdb::q($q, $params, 'null');
  839. }
  840.  
  841. $filename = $new_filename;
  842. }
  843.  
  844. // Image manipulations
  845. if ($file['type'] == FileConstants::TYPE_IMAGE) {
  846.  
  847. // Do image manipulations, if requested
  848. if ($_POST['manipulate'] != '') {
  849. $temp_filename = File::createLocalCopy($file['filename']);
  850. if (! $temp_filename) return false;
  851.  
  852. $img = new Image($temp_filename);
  853. if (! $img) return false;
  854.  
  855. switch ($_POST['manipulate']) {
  856. case 'rotate-90-clockwise': $img->rotate(90); break;
  857. case 'rotate-90-counterclockwise': $img->rotate(-90); break;
  858. case 'rotate-180': $img->rotate(180); break;
  859. case 'flip-horizontal': $img->flip(Image::HORIZONTAL); break;
  860. case 'flip-vertical': $img->flip(Image::VERTICAL); break;
  861. default:
  862. throw new Exception('Invalid image manipulation "' . $_POST['manipulate'] . '"');
  863. }
  864.  
  865. $res = $img->save();
  866. if (! $res) return false;
  867.  
  868. $result = File::putExisting($file['filename'], $temp_filename);
  869. if (! $result) return false;
  870.  
  871. File::cleanupLocalCopy($temp_filename);
  872.  
  873. $res = Replication::postFileUpdate($file['filename']);
  874. if (! $res) return false;
  875.  
  876. $needs_regenerate_sizes = true;
  877.  
  878. // No sense in keeping the focal point for a manipulated image
  879. $_POST['focal_points'] = '';
  880.  
  881. Notification::confirm('Image was manipulated successfully');
  882. }
  883.  
  884. // If author (or embed option) has changed, the sizes will need regeneration
  885. if (
  886. $file['embed_author'] != (int) @$_POST['embed_author']
  887. or
  888. ((int) @$_POST['embed_author']) and $file['author'] != $_POST['author']
  889. ) {
  890. $needs_regenerate_sizes = true;
  891. }
  892. }
  893.  
  894.  
  895. Pdb::transact();
  896.  
  897. // Update record
  898. $data = [];
  899. $data['date_modified'] = Pdb::now();
  900. $data['name'] = $_POST['name'];
  901. $data['description'] = $_POST['description'];
  902. $data['author'] = $_POST['author'];
  903. $data['filename'] = $filename;
  904. $data['enable_indexing'] = (int) @$_POST['enable_indexing'];
  905.  
  906. if ($file['type'] == FileConstants::TYPE_IMAGE) {
  907. $data['embed_author'] = (int) @$_POST['embed_author'];
  908.  
  909. $points = @json_decode($_POST['focal_points'], true);
  910. if (is_array($points)) {
  911. foreach ($points as $key => $point) {
  912. if (!is_array($point) or count($point) != 2) {
  913. unset($points[$key]);
  914. continue;
  915. }
  916. if (!is_int($point[0]) or !is_int($point[1])) {
  917. unset($points[$key]);
  918. continue;
  919. }
  920. }
  921. $data['focal_points'] = json_encode($points);
  922. } else {
  923. $data['focal_points'] = '';
  924. }
  925.  
  926. if ($data['focal_points'] != $file['focal_points']) {
  927. $needs_regenerate_sizes = true;
  928. }
  929. } elseif ($file['type'] == FileConstants::TYPE_DOCUMENT) {
  930. $data['document_type'] = $_POST['document_type'];
  931. $data['date_published'] = $_POST['date_published'];
  932. }
  933.  
  934. Pdb::update('files', $data, ['id' => $item_id]);
  935.  
  936. $this->reindexItem($item_id, $_POST['name'], $file['plaintext'], $data['enable_indexing']);
  937.  
  938. if ($file['type'] == FileConstants::TYPE_IMAGE and $needs_regenerate_sizes) {
  939. File::touch($file['filename']);
  940. File::createDefaultSizes($filename);
  941. File::deleteCache($filename);
  942. }
  943.  
  944. $this->updateCategories($item_id, @$_POST['categories']);
  945.  
  946. if ($original_filename != $filename) {
  947. File::delete($original_filename);
  948. File::deleteCache($original_filename);
  949.  
  950. $variants = array('');
  951. if ($file['type'] == FileConstants::TYPE_IMAGE) {
  952. $variants = array_merge($variants, array_keys(Kohana::config('file.image_transformations')));
  953. }
  954.  
  955. // Make sure old links still function by adding a redirect from the old file name to the new one
  956. foreach ($variants as $variant) {
  957. $old_path = 'files/' . $original_filename;
  958. $new_path = 'file/download/' . $item_id;
  959.  
  960. // For image variants:
  961. // convert e.g. 123_blah.jpg to 123_blah.small.jpg
  962. // append size to redirect URL, e.g. file/123/small
  963. if ($variant) {
  964. $old_path = File::getResizeFilename($old_path, $variant);
  965. $new_path .= '/' . $variant;
  966. }
  967.  
  968. $dest_link_spec = json_encode([
  969. 'class' => '\\Sprout\\Helpers\\LinkSpecInternal',
  970. 'data' => $new_path,
  971. ]);
  972.  
  973. $redirect = [
  974. 'path_exact' => $old_path,
  975. 'destination' => $dest_link_spec,
  976. 'type' => 'Temporary',
  977. 'date_added' => Pdb::now(),
  978. 'date_modified' => Pdb::now(),
  979. ];
  980. Pdb::insert('redirects', $redirect);
  981. }
  982.  
  983. if ($file['type'] == FileConstants::TYPE_IMAGE) {
  984. Pdb::update('pages', ['banner' => $filename], ['banner' => $original_filename]);
  985. }
  986. }
  987.  
  988. Pdb::commit();
  989.  
  990. return true;
  991. }
  992.  
  993.  
  994. /**
  995.   * Deletes an item and logs the deleted data
  996.   *
  997.   * This method DOES NOT remove files from disk, in case the deleted DB record needs to be restored.
  998.   * They are removed later, when the deletion log is cleared; see {@see ActionLogAdminController::cronCleanup}.
  999.   * If lack of disk space is an issue, the log should be cleared more often, or alternate file backends should be
  1000.   * used; see {@see FilesBackend}.
  1001.   *
  1002.   * @param int $item_id The record to delete
  1003.   * @return bool True on success, false on failure
  1004.   * @throws QueryException
  1005.   */
  1006. public function _deleteSave($item_id)
  1007. {
  1008. return parent::_deleteSave($item_id);
  1009. }
  1010.  
  1011.  
  1012. /**
  1013.   * Does a re-index for a file
  1014.   *
  1015.   * @param int $item_id
  1016.   * @param string $name
  1017.   * @param string $plaintext
  1018.   * @param bool $enabled
  1019.   * @return bool True on success
  1020.   */
  1021. private function reindexItem($item_id, $name, $plaintext, $enabled = true)
  1022. {
  1023. $enabled = (bool) $enabled;
  1024. Search::selectIndex('file_keywords', $item_id);
  1025.  
  1026. $res = Search::clearIndex();
  1027. if (! $res) return false;
  1028.  
  1029. // File is marked as not to be included in search results
  1030. if (!$enabled) return true;
  1031.  
  1032. $res = Search::indexText($name, 4);
  1033. if (! $res) return false;
  1034.  
  1035. if ($plaintext) {
  1036. $res = Search::indexHtml($plaintext, 1);
  1037. if (! $res) return false;
  1038. }
  1039.  
  1040. $res = Search::cleanup('files');
  1041. if (! $res) return false;
  1042.  
  1043. return true;
  1044. }
  1045.  
  1046.  
  1047. /**
  1048.   * Does a complete re-index of all files
  1049.   **/
  1050. public function reindexAll()
  1051. {
  1052. AdminAuth::checkLogin();
  1053.  
  1054. Pdb::transact();
  1055.  
  1056. $q = "SELECT id, name, filename, plaintext, enable_indexing FROM ~files";
  1057. $res = Pdb::query($q, [], 'pdo');
  1058.  
  1059. foreach ($res as $row) {
  1060. $plain = '';
  1061.  
  1062. if ($row['plaintext'] == '') {
  1063. $ext = File::getExt($row['filename']);
  1064.  
  1065. if (FileIndexing::isExtSupported($ext)) {
  1066. $plain = FileIndexing::getPlaintext($row['filename'], $ext);
  1067. if ($plain) {
  1068. Pdb::update('files', ['plaintext' => $plain], ['id' => $row['id']]);
  1069. }
  1070. }
  1071. }
  1072.  
  1073. $this->reindexItem($row['id'], $row['name'], $plain ?: $row['plaintext'], $row['enable_indexing']);
  1074. }
  1075.  
  1076. $res->closeCursor();
  1077.  
  1078. Pdb::commit();
  1079.  
  1080. echo '<p>Success</p>';
  1081. }
  1082.  
  1083.  
  1084. /**
  1085.   * Process the results of a search.
  1086.   *
  1087.   * @param array $row A single row of data to output
  1088.   * @return string The result string
  1089.   **/
  1090. public function frontEndSearch($item_id, $relevancy, $keywords)
  1091. {
  1092. $q = "SELECT name, filename, plaintext, enable_indexing FROM sprout_files WHERE id = ?";
  1093. $row = Pdb::q($q, [$item_id], 'row');
  1094.  
  1095. // File is marked as not to be included in search results
  1096. if ($row['enable_indexing'] == 0) return null;
  1097.  
  1098. $text = strip_tags($row['plaintext']);
  1099. $text = substr($text, 0, 5000);
  1100.  
  1101. // Look for the first keyword in the text
  1102. $pos = 5000;
  1103. $matches = null;
  1104. foreach ($keywords as $k) {
  1105. $k = preg_quote($k);
  1106. if (preg_match("/(^|\W){$k}($|\W)/i", $text, $matches, PREG_OFFSET_CAPTURE)) {
  1107. $pos = min($pos, $matches[0][1]);
  1108. }
  1109. }
  1110.  
  1111. // If anything was found in first 5000 chars, show that bit
  1112. if ($pos < 5000) {
  1113. $pos -= 10;
  1114. if ($pos > 1) {
  1115. $text = '...' . substr($text, $pos);
  1116. }
  1117. }
  1118.  
  1119. // Limit to something more reasonable
  1120. $text = Text::limitWords($text, 40, '...');
  1121.  
  1122. // Bolden keywords
  1123. $name = $row['name'];
  1124. foreach ($keywords as $k) {
  1125. $k = preg_quote($k);
  1126. $name = preg_replace("/(^|\W)({$k})($|\W)/i", '$1<b>$2</b>$3', $name);
  1127. $text = preg_replace("/(^|\W)({$k})($|\W)/i", '$1<b>$2</b>$3', $text);
  1128. }
  1129.  
  1130. $view = new View('sprout/search_results_page');
  1131. $view->name = $name;
  1132. $view->url = File::url($row['filename']);
  1133. $view->text = $text;
  1134. $view->relevancy = $relevancy;
  1135.  
  1136. return $view->render();
  1137. }
  1138.  
  1139.  
  1140. /**
  1141.   * Return the list of sidebar tools
  1142.   **/
  1143. public function _getTools()
  1144. {
  1145. $tools = parent::_getTools();
  1146. unset($tools['import']);
  1147.  
  1148. $tools[] = '<li class="config"><a href="admin/extra/file/cleanup_invalid">Cleanup invalid files</a></li>';
  1149.  
  1150. if (AdminAuth::isSuper()) {
  1151. $tools[] = '<li class="config"><a href="admin/extra/file/redo_sizes">Recreate resized images</a></li>';
  1152. }
  1153.  
  1154. return $tools;
  1155. }
  1156.  
  1157.  
  1158. /**
  1159.   * Provides a UI for doing a 'cleanup' - removes invalid files
  1160.   **/
  1161. public function _extraCleanupInvalid()
  1162. {
  1163. $view = new View("sprout/admin/file_cleanup_invalid");
  1164. $view->count_delete = 0;
  1165.  
  1166. $q = "SELECT id, filename, name, type FROM ~files";
  1167. $res = Pdb::query($q, [], 'pdo');
  1168.  
  1169. foreach ($res as $file) {
  1170. if ($file['name'] == '') {
  1171. $view->count_delete++;
  1172. } else if ($file['type'] == FileConstants::TYPE_NONE) {
  1173. $view->count_delete++;
  1174. } else if (!File::exists($file['filename'])) {
  1175. $view->count_delete++;
  1176. }
  1177. }
  1178.  
  1179. $res->closeCursor();
  1180.  
  1181. return array(
  1182. 'title' => 'Cleanup invalid files',
  1183. 'content' => $view->render()
  1184. );
  1185. }
  1186.  
  1187.  
  1188. /**
  1189.   * Does file cleanup - wrapper (admin)
  1190.   **/
  1191. public function cleanupInvalidAction()
  1192. {
  1193. AdminAuth::checkLogin();
  1194. Csrf::checkOrDie();
  1195.  
  1196. try {
  1197. $this->cleanupInvalidActionInner();
  1198. } catch (QueryException $ex) {
  1199. Notification::error('Database error performing cleanup');
  1200. Url::redirect('admin/extra/file/cleanup_invalid');
  1201. }
  1202.  
  1203. Notification::confirm('Cleanup was successful');
  1204. Url::redirect('admin/intro/file');
  1205. }
  1206.  
  1207.  
  1208. /**
  1209.   * Does file cleanup - wrapper (cron)
  1210.   **/
  1211. public function cronCleanupInvalid()
  1212. {
  1213. Cron::start('Cleanup invalid files');
  1214. $this->cleanupInvalidActionInner();
  1215. Cron::success();
  1216. }
  1217.  
  1218.  
  1219. /**
  1220.   * Remove invalid files, such as:
  1221.   * - Files without a name
  1222.   * - Files without a type
  1223.   * - Files which don't actually exist
  1224.   *
  1225.   * @throws QueryExeption
  1226.   **/
  1227. private function cleanupInvalidActionInner()
  1228. {
  1229. Pdb::transact();
  1230.  
  1231. $joiner_table = Category::tableMain2joiner('files');
  1232.  
  1233. $q = "SELECT id, filename, name, type FROM ~files";
  1234. $res = Pdb::query($q, [], 'pdo');
  1235.  
  1236. foreach ($res as $file) {
  1237. $delete = false;
  1238. if ($file['name'] == '') {
  1239. $delete = true;
  1240. } else if ($file['type'] == FileConstants::TYPE_NONE) {
  1241. $delete = true;
  1242. } else if (!File::exists($file['filename'])) {
  1243. $delete = true;
  1244. }
  1245.  
  1246. if ($delete) {
  1247. Pdb::delete('files', ['id' => $file['id']]);
  1248. Cron::message("Deleted file: " . json_encode($file));
  1249. Pdb::delete($joiner_table, ['file_id' => $file['id']]);
  1250. }
  1251. }
  1252.  
  1253. $res->closeCursor();
  1254.  
  1255. Pdb::commit();
  1256. }
  1257.  
  1258.  
  1259. /**
  1260.   * Return HTML for a resultset of items
  1261.   * The returned HTML will be sandwiched between the refinebar and the pagination bar.
  1262.   *
  1263.   * @param Traversable $items The items to render.
  1264.   * @param string $mode The mode of the display.
  1265.   * @param StdClass $category Category details if a category has been selected.
  1266.   **/
  1267. public function _getContentsView($items, $mode, $category)
  1268. {
  1269. if ($mode == 'list') {
  1270. return $this->_getContentsViewList($items, $category);
  1271. } else if ($mode == 'thumb') {
  1272. return $this->_getContentsViewThumb($items, $category);
  1273. }
  1274. }
  1275.  
  1276.  
  1277. /**
  1278.   * Thumbnail view for files
  1279.   **/
  1280. private function _getContentsViewThumb($items, $category)
  1281. {
  1282. $view = new View("sprout/admin/file_contents_thumbs");
  1283. $view->controller_name = $this->controller_name;
  1284. $view->friendly_name = $this->friendly_name;
  1285. $view->items = $items;
  1286. $view->allow_add = $this->main_add;
  1287. $view->category = $category;
  1288.  
  1289. return $view->render();
  1290. }
  1291.  
  1292.  
  1293. /**
  1294.   * On the fly image resizing
  1295.   *
  1296.   * The size parameter is the new size.
  1297.   * The first character is taken to be the resize type, accepts 'r' or 'c'.
  1298.   * The width and height is specified width . 'x' . height (e.g. 200x100)
  1299.   **/
  1300. public function previewTransform($transform, $filename)
  1301. {
  1302. $filename = str_replace('/', '', $filename);
  1303. $temp_filename = File::createLocalCopy($filename);
  1304.  
  1305. $img = new Image($temp_filename);
  1306. $img->resize(200, 0);
  1307.  
  1308. switch ($transform) {
  1309. case 'rotate-90-clockwise':
  1310. $img->rotate(90);
  1311. break;
  1312.  
  1313. case 'rotate-90-counterclockwise':
  1314. $img->rotate(-90);
  1315. break;
  1316.  
  1317. case 'rotate-180':
  1318. $img->rotate(180);
  1319. break;
  1320.  
  1321. case 'flip-horizontal':
  1322. $img->flip(Image::HORIZONTAL);
  1323. break;
  1324.  
  1325. case 'flip-vertical':
  1326. $img->flip(Image::VERTICAL);
  1327. break;
  1328. }
  1329.  
  1330. // Content-type
  1331. $parts = explode('.', $filename);
  1332. $ext = array_pop($parts);
  1333. $mime = array(
  1334. 'gif' => 'image/gif',
  1335. 'jpg' => 'image/jpeg',
  1336. 'jpeg' => 'image/jpeg',
  1337. 'png' => 'image/png',
  1338. );
  1339. $mime = $mime[$ext];
  1340. if (! $mime) $mime = 'application/octet-stream';
  1341.  
  1342. header('Content-type: ' . $mime);
  1343. $img->render();
  1344.  
  1345. File::cleanupLocalCopy($temp_filename);
  1346. }
  1347.  
  1348.  
  1349. /**
  1350.   * Outputs the file selector HTML
  1351.   *
  1352.   * @return void
  1353.   */
  1354. public function selectorPopup()
  1355. {
  1356. $field_name = trim(@$_GET['field']);
  1357.  
  1358. $view = new View('sprout/admin/file_selector_popup');
  1359. $view->field_name = $field_name;
  1360. $view->f_type = (int) $_GET['f_type'];
  1361. $view->cats = Pdb::lookup('files_cat_list');
  1362.  
  1363. $view->upload = isset($_GET['upload']) ? (int) $_GET['upload'] : 1;
  1364. $view->browse = isset($_GET['browse']) ? (int) $_GET['browse'] : 1;
  1365. $view->req_category = isset($_GET['req_category']) ? (int) $_GET['req_category'] : 1;
  1366.  
  1367. if (! AdminPerms::controllerAccess('file', 'add')) {
  1368. $view->upload = false;
  1369. }
  1370. if (! AdminPerms::controllerAccess('file', 'contents')) {
  1371. $view->browse = false;
  1372. }
  1373. $view->cat_create = AdminPerms::controllerAccess('file', 'categories');
  1374.  
  1375. echo $view->render();
  1376. }
  1377.  
  1378.  
  1379. /**
  1380.   * Does the backend selector search
  1381.   **/
  1382. public function selectorPopupSearch()
  1383. {
  1384. if (! AdminPerms::controllerAccess('file', 'contents')) {
  1385. throw new Kohana_404_Exception();
  1386. }
  1387.  
  1388. $_GET['page'] = (int) $_GET['page'];
  1389. $_GET['f_type'] = (int) $_GET['f_type'];
  1390. $_GET['name'] = trim($_GET['name']);
  1391. $_GET['category_id'] = (int) $_GET['category_id'];
  1392.  
  1393. $joins = [];
  1394. $clauses = [];
  1395. $params = [];
  1396.  
  1397. if ($_GET['category_id'] > 0) {
  1398. $joins[] = "INNER JOIN ~files_cat_join AS joiner
  1399. ON files.id = joiner.file_id AND joiner.cat_id = ?";
  1400. $params[] = $_GET['category_id'];
  1401. }
  1402.  
  1403. if ($_GET['f_type'] > 0) {
  1404. $clauses[] = 'files.type = ?';
  1405. $params[] = $_GET['f_type'];
  1406.  
  1407. } else {
  1408. $clauses[] = 'files.type != 0';
  1409. }
  1410.  
  1411.  
  1412. if ($_GET['name']) {
  1413. $parts = preg_split('/\s+/', $_GET['name']);
  1414. foreach ($parts as $part) {
  1415. $part = Pdb::likeEscape($part);
  1416. $clauses[] = "(files.name LIKE CONCAT('%', ?, '%') OR files.filename LIKE CONCAT('%', ?, '%'))";
  1417.  
  1418. // Doubly add param, once for each LIKE clause
  1419. $params[] = $part;
  1420. $params[] = $part;
  1421. }
  1422. }
  1423.  
  1424.  
  1425. $clauses = implode(' AND ', $clauses);
  1426.  
  1427. $joins = implode("\n", $joins);
  1428.  
  1429. $q = "SELECT COUNT(DISTINCT id) AS C
  1430. FROM ~files AS files
  1431. {$joins}
  1432. WHERE {$clauses}";
  1433. $num_results = Pdb::q($q, $params, 'val');
  1434. $num_pages = ceil($num_results / 10);
  1435.  
  1436. $offset = 10 * $_GET['page'];
  1437. $q = "SELECT DISTINCT files.id, files.name, files.filename
  1438. FROM ~files AS files
  1439. {$joins}
  1440. WHERE {$clauses}
  1441. ORDER BY files.name
  1442. LIMIT 10 OFFSET {$offset}";
  1443. $res = Pdb::q($q, $params, 'arr');
  1444.  
  1445. // Json return object
  1446. $json = array();
  1447. $json[] = array(
  1448. 'num_results' => $num_results,
  1449. 'num_pages' => $num_pages,
  1450. 'curr_page' => $_GET['page'],
  1451. );
  1452.  
  1453. // Append one json record for each db record
  1454. foreach ($res as $row) {
  1455. $preview = $preview_large = '';
  1456. if ($_GET['f_type'] == FileConstants::TYPE_IMAGE or strpos(File::mimetype($row['filename']), 'image/') === 0) {
  1457. if (File::exists($row['filename'])) {
  1458. $preview = '<img src="' . str_replace('SITE/', '', File::resizeUrl($row['filename'], 'c50x50')) . '">';
  1459. $preview_large = str_replace('SITE/', '', File::resizeUrl($row['filename'], 'm400x150'));
  1460. }
  1461. }
  1462.  
  1463. $json[] = array(
  1464. 'id' => $row['id'],
  1465. 'filename' => $row['filename'],
  1466. 'name' => $row['name'],
  1467. 'preview' => $preview,
  1468. 'preview_large' => $preview_large,
  1469. );
  1470. }
  1471.  
  1472. Json::out($json);
  1473. }
  1474.  
  1475.  
  1476. /**
  1477.   * List of links to redo the sizes
  1478.   **/
  1479. public function _extraRedoSizes()
  1480. {
  1481. $sizes = Kohana::config('file.image_transformations');
  1482. $sz = ['' => 'All'];
  1483. foreach ($sizes as $size_name => $transform) {
  1484. $sz[$size_name] = ucfirst($size_name);
  1485. }
  1486.  
  1487. $out = '<form action="admin/call/file/redoSizesAction" method="post">';
  1488. $out .= Csrf::token();
  1489.  
  1490. Form::nextFieldDetails('Size', true);
  1491. $out .= Form::dropdown('size', [], $sz);
  1492.  
  1493. $out .= '<button type="submit" class="button">Re-generate sizes</button>';
  1494. $out .= '</form>';
  1495.  
  1496. return $out;
  1497. }
  1498.  
  1499.  
  1500. /**
  1501.   * Fixes files which don't have the sizes they should
  1502.   **/
  1503. public function redoSizesAction()
  1504. {
  1505. AdminAuth::checkLogin();
  1506. Csrf::checkOrDie();
  1507.  
  1508. try {
  1509. $info = WorkerCtrl::start('Sprout\\Helpers\\WorkerRedoSizes', $_POST['size']);
  1510. } catch (WorkerJobException $ex) {
  1511. Notification::error('Unable to create worker job');
  1512. Url::redirect('admin/intro/file');
  1513. }
  1514.  
  1515. Url::redirect($info['log_url']);
  1516. }
  1517.  
  1518.  
  1519. /**
  1520.   * Find usage of a given file
  1521.   *
  1522.   * @param int $file_id
  1523.   * @return array
  1524.   */
  1525. public function _extraFindUsage($file_id)
  1526. {
  1527. $file = Pdb::get('files', $file_id);
  1528.  
  1529. $view = new View('sprout/admin/file_usage');
  1530. $view->file = $file;
  1531. $view->usage = File::findUsage($file['filename']);
  1532.  
  1533. return [
  1534. 'title' => sprintf("Usage of file '%s'", $file['name'] ? $file['name'] : $file['filename']),
  1535. 'content' => $view
  1536. ];
  1537. }
  1538.  
  1539.  
  1540. /**
  1541.   * Renders a HTML subset containing a focal crop image
  1542.   *
  1543.   * @param string $size WxH, e.g. 300x200
  1544.   * @param string $filename E.g. 123_image.jpg
  1545.   * @param string $focal_point_data JSON to store in files.focal_points
  1546.   */
  1547. public function previewFocalCrop($size, $filename, $focal_point_data)
  1548. {
  1549. if ($size[0] != 'c') {
  1550. $size = 'c' . $size;
  1551. }
  1552.  
  1553. // Copy original file to test location
  1554. $content = File::getString($filename);
  1555. $temp_filename = 'focal_preview_' . $filename;
  1556. File::putString($temp_filename, $content);
  1557.  
  1558. Pdb::transact();
  1559.  
  1560. Pdb::update('files', ['focal_points' => $focal_point_data, 'filename' => $temp_filename], ['filename' => $filename]);
  1561.  
  1562. $_GET['s'] = Security::serverKeySign(['filename' => $temp_filename, 'size' => $size]);
  1563. $_GET['force'] = 1;
  1564.  
  1565. $cont = new \Sprout\Controllers\FileController();
  1566. $cont->resize($size, $temp_filename);
  1567.  
  1568. Pdb::rollback();
  1569.  
  1570. File::deleteCache($temp_filename);
  1571. File::delete($temp_filename);
  1572. }
  1573.  
  1574.  
  1575. /**
  1576.   * Provide the contents of a temporarily uploaded file, for e.g. listening to uploaded audio
  1577.   *
  1578.   * @return void Sets headers and outputs file content
  1579.   */
  1580. public function downloadTemp($filename)
  1581. {
  1582. $path = APPPATH . 'temp/' . $filename;
  1583.  
  1584. if (!preg_match('/^[a-zA-Z0-9-]*\.dat$/', $filename) or !file_exists($path)) {
  1585. http_response_code(404);
  1586. die();
  1587. }
  1588.  
  1589. header('Pragma: public');
  1590. header('Content-type: application/octet-stream');
  1591. header("Cache-Control: no-store, no-cache");
  1592. header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
  1593. header('Last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
  1594. header('Content-length: ' . filesize($path));
  1595. Kohana::closeBuffers();
  1596. readfile($path);
  1597. }
  1598. }
  1599.  
  1600.  
  1601.