filestore.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. <?php
  2. /**
  3. * Elgg filestore.
  4. * This file contains functions for saving and retrieving data from files.
  5. *
  6. * @package Elgg.Core
  7. * @subpackage DataModel.FileStorage
  8. */
  9. /**
  10. * Get the size of the specified directory.
  11. *
  12. * @param string $dir The full path of the directory
  13. * @param int $total_size Add to current dir size
  14. *
  15. * @return int The size of the directory in bytes
  16. */
  17. function get_dir_size($dir, $total_size = 0) {
  18. $handle = @opendir($dir);
  19. while ($file = @readdir($handle)) {
  20. if (in_array($file, array('.', '..'))) {
  21. continue;
  22. }
  23. if (is_dir($dir . $file)) {
  24. $total_size = get_dir_size($dir . $file . "/", $total_size);
  25. } else {
  26. $total_size += filesize($dir . $file);
  27. }
  28. }
  29. @closedir($handle);
  30. return($total_size);
  31. }
  32. /**
  33. * Get the contents of an uploaded file.
  34. * (Returns false if there was an issue.)
  35. *
  36. * @param string $input_name The name of the file input field on the submission form
  37. *
  38. * @return mixed|false The contents of the file, or false on failure.
  39. */
  40. function get_uploaded_file($input_name) {
  41. $files = _elgg_services()->request->files;
  42. if (!$files->has($input_name)) {
  43. return false;
  44. }
  45. $file = $files->get($input_name);
  46. if (empty($file)) {
  47. // a file input was provided but no file uploaded
  48. return false;
  49. }
  50. if ($file->getError() !== 0) {
  51. return false;
  52. }
  53. return file_get_contents($file->getPathname());
  54. }
  55. /**
  56. * Gets the jpeg contents of the resized version of an uploaded image
  57. * (Returns false if the uploaded file was not an image)
  58. *
  59. * @param string $input_name The name of the file input field on the submission form
  60. * @param int $maxwidth The maximum width of the resized image
  61. * @param int $maxheight The maximum height of the resized image
  62. * @param bool $square If set to true, will take the smallest
  63. * of maxwidth and maxheight and use it to set the
  64. * dimensions on all size; the image will be cropped.
  65. * @param bool $upscale Resize images smaller than $maxwidth x $maxheight?
  66. *
  67. * @return false|mixed The contents of the resized image, or false on failure
  68. */
  69. function get_resized_image_from_uploaded_file($input_name, $maxwidth, $maxheight,
  70. $square = false, $upscale = false) {
  71. $files = _elgg_services()->request->files;
  72. if (!$files->has($input_name)) {
  73. return false;
  74. }
  75. $file = $files->get($input_name);
  76. if (empty($file)) {
  77. // a file input was provided but no file uploaded
  78. return false;
  79. }
  80. if ($file->getError() !== 0) {
  81. return false;
  82. }
  83. return get_resized_image_from_existing_file($file->getPathname(), $maxwidth,
  84. $maxheight, $square, 0, 0, 0, 0, $upscale);
  85. }
  86. /**
  87. * Gets the jpeg contents of the resized version of an already uploaded image
  88. * (Returns false if the file was not an image)
  89. *
  90. * @param string $input_name The name of the file on the disk
  91. * @param int $maxwidth The desired width of the resized image
  92. * @param int $maxheight The desired height of the resized image
  93. * @param bool $square If set to true, takes the smallest of maxwidth and
  94. * maxheight and use it to set the dimensions on the new image.
  95. * If no crop parameters are set, the largest square that fits
  96. * in the image centered will be used for the resize. If square,
  97. * the crop must be a square region.
  98. * @param int $x1 x coordinate for top, left corner
  99. * @param int $y1 y coordinate for top, left corner
  100. * @param int $x2 x coordinate for bottom, right corner
  101. * @param int $y2 y coordinate for bottom, right corner
  102. * @param bool $upscale Resize images smaller than $maxwidth x $maxheight?
  103. *
  104. * @return false|mixed The contents of the resized image, or false on failure
  105. */
  106. function get_resized_image_from_existing_file($input_name, $maxwidth, $maxheight, $square = false,
  107. $x1 = 0, $y1 = 0, $x2 = 0, $y2 = 0, $upscale = false) {
  108. // Get the size information from the image
  109. $imgsizearray = getimagesize($input_name);
  110. if ($imgsizearray == false) {
  111. return false;
  112. }
  113. $width = $imgsizearray[0];
  114. $height = $imgsizearray[1];
  115. $accepted_formats = array(
  116. 'image/jpeg' => 'jpeg',
  117. 'image/pjpeg' => 'jpeg',
  118. 'image/png' => 'png',
  119. 'image/x-png' => 'png',
  120. 'image/gif' => 'gif'
  121. );
  122. // make sure the function is available
  123. $load_function = "imagecreatefrom" . $accepted_formats[$imgsizearray['mime']];
  124. if (!is_callable($load_function)) {
  125. return false;
  126. }
  127. // get the parameters for resizing the image
  128. $options = array(
  129. 'maxwidth' => $maxwidth,
  130. 'maxheight' => $maxheight,
  131. 'square' => $square,
  132. 'upscale' => $upscale,
  133. 'x1' => $x1,
  134. 'y1' => $y1,
  135. 'x2' => $x2,
  136. 'y2' => $y2,
  137. );
  138. $params = get_image_resize_parameters($width, $height, $options);
  139. if ($params == false) {
  140. return false;
  141. }
  142. // load original image
  143. $original_image = call_user_func($load_function, $input_name);
  144. if (!$original_image) {
  145. return false;
  146. }
  147. // allocate the new image
  148. $new_image = imagecreatetruecolor($params['newwidth'], $params['newheight']);
  149. if (!$new_image) {
  150. return false;
  151. }
  152. // color transparencies white (default is black)
  153. imagefilledrectangle(
  154. $new_image, 0, 0, $params['newwidth'], $params['newheight'],
  155. imagecolorallocate($new_image, 255, 255, 255)
  156. );
  157. $rtn_code = imagecopyresampled( $new_image,
  158. $original_image,
  159. 0,
  160. 0,
  161. $params['xoffset'],
  162. $params['yoffset'],
  163. $params['newwidth'],
  164. $params['newheight'],
  165. $params['selectionwidth'],
  166. $params['selectionheight']);
  167. if (!$rtn_code) {
  168. return false;
  169. }
  170. // grab a compressed jpeg version of the image
  171. ob_start();
  172. imagejpeg($new_image, null, 90);
  173. $jpeg = ob_get_clean();
  174. imagedestroy($new_image);
  175. imagedestroy($original_image);
  176. return $jpeg;
  177. }
  178. /**
  179. * Calculate the parameters for resizing an image
  180. *
  181. * @param int $width Width of the original image
  182. * @param int $height Height of the original image
  183. * @param array $options See $defaults for the options
  184. *
  185. * @return array or false
  186. * @since 1.7.2
  187. */
  188. function get_image_resize_parameters($width, $height, $options) {
  189. $defaults = array(
  190. 'maxwidth' => 100,
  191. 'maxheight' => 100,
  192. 'square' => false,
  193. 'upscale' => false,
  194. 'x1' => 0,
  195. 'y1' => 0,
  196. 'x2' => 0,
  197. 'y2' => 0,
  198. );
  199. $options = array_merge($defaults, $options);
  200. // Avoiding extract() because it hurts static analysis
  201. $maxwidth = $options['maxwidth'];
  202. $maxheight = $options['maxheight'];
  203. $square = $options['square'];
  204. $upscale = $options['upscale'];
  205. $x1 = $options['x1'];
  206. $y1 = $options['y1'];
  207. $x2 = $options['x2'];
  208. $y2 = $options['y2'];
  209. // crop image first?
  210. $crop = true;
  211. if ($x1 == 0 && $y1 == 0 && $x2 == 0 && $y2 == 0) {
  212. $crop = false;
  213. }
  214. // how large a section of the image has been selected
  215. if ($crop) {
  216. $selection_width = $x2 - $x1;
  217. $selection_height = $y2 - $y1;
  218. } else {
  219. // everything selected if no crop parameters
  220. $selection_width = $width;
  221. $selection_height = $height;
  222. }
  223. // determine cropping offsets
  224. if ($square) {
  225. // asking for a square image back
  226. // detect case where someone is passing crop parameters that are not for a square
  227. if ($crop == true && $selection_width != $selection_height) {
  228. return false;
  229. }
  230. // size of the new square image
  231. $new_width = $new_height = min($maxwidth, $maxheight);
  232. // find largest square that fits within the selected region
  233. $selection_width = $selection_height = min($selection_width, $selection_height);
  234. // set offsets for crop
  235. if ($crop) {
  236. $widthoffset = $x1;
  237. $heightoffset = $y1;
  238. $width = $x2 - $x1;
  239. $height = $width;
  240. } else {
  241. // place square region in the center
  242. $widthoffset = floor(($width - $selection_width) / 2);
  243. $heightoffset = floor(($height - $selection_height) / 2);
  244. }
  245. } else {
  246. // non-square new image
  247. $new_width = $maxwidth;
  248. $new_height = $maxheight;
  249. // maintain aspect ratio of original image/crop
  250. if (($selection_height / (float)$new_height) > ($selection_width / (float)$new_width)) {
  251. $new_width = floor($new_height * $selection_width / (float)$selection_height);
  252. } else {
  253. $new_height = floor($new_width * $selection_height / (float)$selection_width);
  254. }
  255. // by default, use entire image
  256. $widthoffset = 0;
  257. $heightoffset = 0;
  258. if ($crop) {
  259. $widthoffset = $x1;
  260. $heightoffset = $y1;
  261. }
  262. }
  263. if (!$upscale && ($selection_height < $new_height || $selection_width < $new_width)) {
  264. // we cannot upscale and selected area is too small so we decrease size of returned image
  265. if ($square) {
  266. $new_height = $selection_height;
  267. $new_width = $selection_width;
  268. } else {
  269. if ($selection_height < $new_height && $selection_width < $new_width) {
  270. $new_height = $selection_height;
  271. $new_width = $selection_width;
  272. }
  273. }
  274. }
  275. $params = array(
  276. 'newwidth' => $new_width,
  277. 'newheight' => $new_height,
  278. 'selectionwidth' => $selection_width,
  279. 'selectionheight' => $selection_height,
  280. 'xoffset' => $widthoffset,
  281. 'yoffset' => $heightoffset,
  282. );
  283. return $params;
  284. }
  285. /**
  286. * Delete an \ElggFile file
  287. *
  288. * @param int $guid \ElggFile GUID
  289. *
  290. * @return bool
  291. */
  292. function file_delete($guid) {
  293. $file = get_entity($guid);
  294. if (!$file || !$file->canEdit()) {
  295. return false;
  296. }
  297. $thumbnail = $file->thumbnail;
  298. $smallthumb = $file->smallthumb;
  299. $largethumb = $file->largethumb;
  300. if ($thumbnail) {
  301. $delfile = new \ElggFile();
  302. $delfile->owner_guid = $file->owner_guid;
  303. $delfile->setFilename($thumbnail);
  304. $delfile->delete();
  305. }
  306. if ($smallthumb) {
  307. $delfile = new \ElggFile();
  308. $delfile->owner_guid = $file->owner_guid;
  309. $delfile->setFilename($smallthumb);
  310. $delfile->delete();
  311. }
  312. if ($largethumb) {
  313. $delfile = new \ElggFile();
  314. $delfile->owner_guid = $file->owner_guid;
  315. $delfile->setFilename($largethumb);
  316. $delfile->delete();
  317. }
  318. return $file->delete();
  319. }
  320. /**
  321. * Delete a directory and all its contents
  322. *
  323. * @param string $directory Directory to delete
  324. *
  325. * @return bool
  326. */
  327. function delete_directory($directory) {
  328. // sanity check: must be a directory
  329. if (!$handle = opendir($directory)) {
  330. return false;
  331. }
  332. // loop through all files
  333. while (($file = readdir($handle)) !== false) {
  334. if (in_array($file, array('.', '..'))) {
  335. continue;
  336. }
  337. $path = "$directory/$file";
  338. if (is_dir($path)) {
  339. // recurse down through directory
  340. if (!delete_directory($path)) {
  341. return false;
  342. }
  343. } else {
  344. // delete file
  345. unlink($path);
  346. }
  347. }
  348. // remove empty directory
  349. closedir($handle);
  350. return rmdir($directory);
  351. }
  352. /**
  353. * Removes all entity files
  354. *
  355. * @warning This only deletes the physical files and not their entities.
  356. * This will result in FileExceptions being thrown. Don't use this function.
  357. *
  358. * @warning This must be kept in sync with \ElggDiskFilestore.
  359. *
  360. * @todo Remove this when all files are entities.
  361. *
  362. * @param \ElggEntity $entity An \ElggEntity
  363. *
  364. * @return void
  365. * @access private
  366. */
  367. function _elgg_clear_entity_files($entity) {
  368. $dir = new \Elgg\EntityDirLocator($entity->guid);
  369. $file_path = elgg_get_config('dataroot') . $dir;
  370. if (file_exists($file_path)) {
  371. delete_directory($file_path);
  372. }
  373. }
  374. /// Variable holding the default datastore
  375. $DEFAULT_FILE_STORE = null;
  376. /**
  377. * Return the default filestore.
  378. *
  379. * @return \ElggFilestore
  380. */
  381. function get_default_filestore() {
  382. global $DEFAULT_FILE_STORE;
  383. return $DEFAULT_FILE_STORE;
  384. }
  385. /**
  386. * Set the default filestore for the system.
  387. *
  388. * @param \ElggFilestore $filestore An \ElggFilestore object.
  389. *
  390. * @return true
  391. */
  392. function set_default_filestore(\ElggFilestore $filestore) {
  393. global $DEFAULT_FILE_STORE;
  394. $DEFAULT_FILE_STORE = $filestore;
  395. return true;
  396. }
  397. /**
  398. * Returns the category of a file from its MIME type
  399. *
  400. * @param string $mime_type The MIME type
  401. *
  402. * @return string 'document', 'audio', 'video', or 'general' if the MIME type was unrecognized
  403. * @since 1.10
  404. */
  405. function elgg_get_file_simple_type($mime_type) {
  406. $params = array('mime_type' => $mime_type);
  407. return elgg_trigger_plugin_hook('simple_type', 'file', $params, 'general');
  408. }
  409. /**
  410. * Initialize the file library.
  411. * Listens to system init and configures the default filestore
  412. *
  413. * @return void
  414. * @access private
  415. */
  416. function _elgg_filestore_init() {
  417. global $CONFIG;
  418. // Now register a default filestore
  419. if (isset($CONFIG->dataroot)) {
  420. set_default_filestore(new \ElggDiskFilestore($CONFIG->dataroot));
  421. }
  422. // Fix MIME type detection for Microsoft zipped formats
  423. elgg_register_plugin_hook_handler('mime_type', 'file', '_elgg_filestore_detect_mimetype');
  424. // Parse category of file from MIME type
  425. elgg_register_plugin_hook_handler('simple_type', 'file', '_elgg_filestore_parse_simpletype');
  426. // Unit testing
  427. elgg_register_plugin_hook_handler('unit_test', 'system', '_elgg_filestore_test');
  428. }
  429. /**
  430. * Fix MIME type detection for Microsoft zipped formats
  431. *
  432. * @param string $hook "mime_type"
  433. * @param string $type "file"
  434. * @param string $mime_type Detected MIME type
  435. * @param array $params Hook parameters
  436. *
  437. * @return string The MIME type
  438. * @access private
  439. */
  440. function _elgg_filestore_detect_mimetype($hook, $type, $mime_type, $params) {
  441. $original_filename = elgg_extract('original_filename', $params);
  442. $info = pathinfo($original_filename);
  443. // hack for Microsoft zipped formats
  444. $office_formats = array('docx', 'xlsx', 'pptx');
  445. if ($mime_type == "application/zip" && in_array($info['extension'], $office_formats)) {
  446. switch ($info['extension']) {
  447. case 'docx':
  448. $mime_type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
  449. break;
  450. case 'xlsx':
  451. $mime_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
  452. break;
  453. case 'pptx':
  454. $mime_type = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
  455. break;
  456. }
  457. }
  458. // check for bad ppt detection
  459. if ($mime_type == "application/vnd.ms-office" && $info['extension'] == "ppt") {
  460. $mime_type = "application/vnd.ms-powerpoint";
  461. }
  462. return $mime_type;
  463. }
  464. /**
  465. * Parse a file category of file from a MIME type
  466. *
  467. * @param string $hook "simple_type"
  468. * @param string $type "file"
  469. * @param string $simple_type The category of file
  470. * @param array $params Hook parameters
  471. *
  472. * @return string 'document', 'audio', 'video', or 'general' if the MIME type is unrecognized
  473. * @access private
  474. */
  475. function _elgg_filestore_parse_simpletype($hook, $type, $simple_type, $params) {
  476. $mime_type = elgg_extract('mime_type', $params);
  477. switch ($mime_type) {
  478. case "application/msword":
  479. case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
  480. case "application/pdf":
  481. return "document";
  482. case "application/ogg":
  483. return "audio";
  484. }
  485. if (preg_match('~^(audio|image|video)/~', $mime_type, $m)) {
  486. return $m[1];
  487. }
  488. if (0 === strpos($mime_type, 'text/') || false !== strpos($mime_type, 'opendocument')) {
  489. return "document";
  490. }
  491. // unrecognized MIME
  492. return $simple_type;
  493. }
  494. /**
  495. * Unit tests for files
  496. *
  497. * @param string $hook unit_test
  498. * @param string $type system
  499. * @param mixed $value Array of tests
  500. *
  501. * @return array
  502. * @access private
  503. */
  504. function _elgg_filestore_test($hook, $type, $value) {
  505. global $CONFIG;
  506. $value[] = "{$CONFIG->path}engine/tests/ElggCoreFilestoreTest.php";
  507. return $value;
  508. }
  509. return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) {
  510. $events->registerHandler('init', 'system', '_elgg_filestore_init', 100);
  511. };