SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/FileUpload.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\Helpers;
  15.  
  16. use Exception;
  17. use DomainException;
  18. use InvalidArgumentException;
  19.  
  20. use Sprout\Helpers\File;
  21. use Sprout\Exceptions\FileUploadException;
  22.  
  23.  
  24. /**
  25.  * Used for managing backend processing of fields which have had data submitted via chunked file uploads,
  26.  * i.e. where chunked upload data has been stitched together by {@see FileUploadController}
  27.  */
  28. class FileUpload
  29. {
  30. /**
  31.   * Verify that the temporary file specified by the user exists and has valid content,
  32.   * and that the original file name is acceptable
  33.   * This is to be used during form processing upon a POST submission.
  34.   *
  35.   * @example
  36.   * // Expects $_POST['avatar'][0] and $_POST['avatar_temp'][0] to both be set
  37.   * // Expects $_SESSION['file_uploads']['user_details']['avatar']['code'] to be set
  38.   * $result = FileUpload::verify('user_details', 'avatar', 0, ['jpg', 'gif', 'png']);
  39.   * // Result will be something like '/home/.../sprout/temp/xxxx'
  40.   *
  41.   * @param string $sess_key Session key related to the form, e.g. 'user-register';
  42.   * see $params['sess_key'] of {@see Fb::chunkedUpload}
  43.   * @param string $field Name of field to process, e.g. 'image'
  44.   * @param int $index Array index; 0 for the first file, 1 for the next and so on
  45.   * @param array $allowed_exts Array of string file-types that are allowed
  46.   * @post string $field
  47.   * @post string $field . '_temp'
  48.   * @return string Path to temporarily uploaded file; this can be used as the first argument to
  49.   * {@see File::moveUpload} to put the file in the desired permanent location.
  50.   * @throws DomainException If the $_POST upload state is invalid.
  51.   * @throws FileUploadException If there was an issue the uploader should know about, e.g. the file extension isn't permitted.
  52.   * This exception always has front-end safe exception messages.
  53.   */
  54. public static function verify($sess_key, $field, $index, array $allowed_exts)
  55. {
  56. $index = (int) $index;
  57. if (empty($_POST[$field][$index])) {
  58. throw new DomainException('Missing original name');
  59. }
  60. if (empty($_POST[$field . '_temp'][$index])) {
  61. throw new DomainException('Missing temp name');
  62. }
  63.  
  64. // Check for tampered temp file name
  65. $temp = $_POST[$field . '_temp'][$index];
  66. $res = preg_match('/^upload-[0-9]+-([A-Za-z0-9]{32}).dat$/', $temp, $matches);
  67. if (!$res) {
  68. throw new DomainException('Invalid temp file name');
  69. }
  70. $upload_code = $matches[1];
  71.  
  72. // Check file exists
  73. $src_path = APPPATH . 'temp/' . $temp;
  74. if (!file_exists($src_path)) {
  75. throw new DomainException('Temp file missing');
  76. }
  77.  
  78. // Check to see that the user is actually the one who uploaded the file
  79. if (!isset($_SESSION['file_uploads'][$sess_key][$field][$upload_code])) {
  80. throw new FileUploadException('Upload session lost');
  81. }
  82.  
  83. // Validate original file name
  84. if (!self::checkFilename($_POST[$field][$index])) {
  85. throw new FileUploadException("This type of file cannot be uploaded for security reasons");
  86. }
  87.  
  88. $ext = strtolower(File::getExt($_POST[$field][$index]));
  89. if (!empty($allowed_exts) and !in_array($ext, $allowed_exts)) {
  90. throw new FileUploadException("Invalid file extension");
  91. }
  92.  
  93. if (File::checkFileContentsExtension($src_path, $ext) === false) {
  94. throw new FileUploadException("File content doesn't match extension");
  95. }
  96.  
  97. return $src_path;
  98. }
  99.  
  100.  
  101. /**
  102.   * Check a given filename is allowed to be uploaded - blocks PHP files etc
  103.   *
  104.   * @param string $filename
  105.   * @return bool True if allowed, false if not
  106.   */
  107. public static function checkFilename($filename)
  108. {
  109. if (strpos($filename, '.') === false) {
  110. return false;
  111. }
  112.  
  113. // .-------- PHP ---------. .---- WIN ----. .------- LINUX ------.
  114. $execs = '/\.(php|phar|phtml|php[345s]|bat|com|cmd|exe|sh|bin|csh|ksh|out|run|htaccess)?$/i';
  115. if (preg_match($execs, $filename)) {
  116. return false;
  117. }
  118.  
  119. return true;
  120. }
  121.  
  122.  
  123. /**
  124.   * Generates a fake upload, including a session entry and a temp symlink, from an existing file on disk.
  125.   *
  126.   * This allows reuse of existing uploaded files on Form::chunkedUpload fields which support multiple files
  127.   *
  128.   * @param string $filename The name of a file which exists in the files dir, e.g. 'image.jpg'
  129.   * @param string $session_key The session key used for file uploads, e.g. 'user-register'
  130.   * (see {@see Fb::chunkedUpload})
  131.   * @param string $field_name The name of the field supplied to {@see Fb::chunkedUpload}, e.g. 'photos'
  132.   * @return string The filename of the newly generated symlink, which matches the naming format of files
  133.   * uploaded via the chunked uploader
  134.   * @throws InvalidArgumentException if the filename is invalid
  135.   */
  136. public static function generateFromDisk($filename, $session_key, $field_name)
  137. {
  138. if (!File::exists($filename)) {
  139. throw new InvalidArgumentException('Invalid filename');
  140. }
  141.  
  142. $real_file = DOCROOT . 'files/' . $filename;
  143.  
  144. $code = Security::randStr(32);
  145.  
  146. $temp_file = 'upload-' . time() . '-' . $code . '.dat';
  147. if (!symlink(DOCROOT . 'files/' . $filename, APPPATH . 'temp/' . $temp_file)) {
  148. throw new InvalidArgumentException('Failed to create symlink');
  149. }
  150.  
  151. $_SESSION['file_uploads'][$session_key][$field_name][$code] = ['size' => filesize($real_file)];
  152.  
  153. return $temp_file;
  154. }
  155. }
  156.