/** * Return a string as a safe filename * * @param string $string * The candidtate filename. * @param array $options * - array extensions: allowable extensions no periods * - string ext: default extension if non found; blank for none no period * * @return string * - lowercased, with only letters, numbers, dots and hyphens * * @see file_munge_filename(). */ protected function filenameSafe($string, $options = array()) { $options += array('extensions' => array('txt', 'md'), 'ext' => 'txt'); $string = preg_replace('/[^a-z0-9\\-\\.]/', '-', strtolower($string)); $string = preg_replace('/-{2,}/', '-', $string); // Add an extension if not found if ($options['ext'] && !preg_match('/\\.[a-z]{1,5}$/', $string)) { $string .= '.' . trim($options['ext'], '.'); } if ($string && function_exists('file_munge_filename')) { $string = file_munge_filename($string, implode(' ', $options['extensions']), FALSE); } //@todo Add in the module that cleans name if it's installed return $string; }
/** * {@inheritdoc} */ public static function valueCallback(&$element, $input, FormStateInterface $form_state) { $file_names = []; $return['uploaded_files'] = NULL; if ($input !== FALSE) { $user_input = NestedArray::getValue($form_state->getUserInput(), $element['#parents'] + ['uploaded_files']); if (!empty($user_input['uploaded_files'])) { $file_names = array_filter(explode(';', $user_input['uploaded_files'])); $tmp_override = \Drupal::config('dropzonejs.settings')->get('tmp_dir'); $temp_path = $tmp_override ? $tmp_override : \Drupal::config('system.file')->get('path.temporary'); foreach ($file_names as $name) { // The upload handler appended the txt extension to the file for // security reasons. We will remove it in this callback. $old_filepath = "{$temp_path}/{$name}"; // The upload handler appended the txt extension to the file for // security reasons. Because here we know the acceptable extensions // we can remove that extension and sanitize the filename. $name = self::fixTmpFilename($name); $name = file_munge_filename($name, self::getValidExtensions($element)); // Finaly rename the file and add it to results. $new_filepath = "{$temp_path}/{$name}"; $move_result = file_unmanaged_move($old_filepath, $new_filepath); if ($move_result) { $return['uploaded_files'][] = ['path' => $move_result, 'filename' => $name]; } else { drupal_set_message(t('There was a problem while processing the file named @name', ['@name' => $name]), 'error'); } } } $form_state->setValueForElement($element, $return); return $return; } }
/** * Ensure that unmunge gets your name back. */ function testUnMunge() { $munged_name = file_munge_filename($this->name, '', FALSE); $unmunged_name = file_unmunge_filename($munged_name); $this->assertIdentical($unmunged_name, $this->name, format_string('The unmunged (%unmunged) filename matches the original (%original)', array('%unmunged' => $unmunged_name, '%original' => $this->name))); }
/** * An adaptation of file_save_upload() that includes more verbose errors. * * @param string $source * A string specifying the filepath or URI of the uploaded file to save. * * @return stdClass * The saved file object. * * @throws \RestfulBadRequestException * @throws \RestfulServiceUnavailable * * @see file_save_upload() */ protected function fileSaveUpload($source) { static $upload_cache; $account = $this->getAccount(); $options = $this->getPluginKey('options'); $validators = $options['validators']; $destination = $options['scheme'] . "://"; $replace = $options['replace']; // Return cached objects without processing since the file will have // already been processed and the paths in _FILES will be invalid. if (isset($upload_cache[$source])) { return $upload_cache[$source]; } // Make sure there's an upload to process. if (empty($_FILES['files']['name'][$source])) { return NULL; } // Check for file upload errors and return FALSE if a lower level system // error occurred. For a complete list of errors: // See http://php.net/manual/features.file-upload.errors.php. switch ($_FILES['files']['error'][$source]) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: $message = format_string('The file %file could not be saved, because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $_FILES['files']['name'][$source], '%maxsize' => format_size(file_upload_max_size()))); throw new \RestfulBadRequestException($message); case UPLOAD_ERR_PARTIAL: case UPLOAD_ERR_NO_FILE: $message = format_string('The file %file could not be saved, because the upload did not complete.', array('%file' => $_FILES['files']['name'][$source])); throw new \RestfulBadRequestException($message); case UPLOAD_ERR_OK: // Final check that this is a valid upload, if it isn't, use the // default error handler. if (is_uploaded_file($_FILES['files']['tmp_name'][$source])) { break; } // Unknown error default: $message = format_string('The file %file could not be saved. An unknown error has occurred.', array('%file' => $_FILES['files']['name'][$source])); throw new \RestfulServiceUnavailable($message); } // Begin building file object. $file = new stdClass(); $file->uid = $account->uid; $file->status = 0; $file->filename = trim(drupal_basename($_FILES['files']['name'][$source]), '.'); $file->uri = $_FILES['files']['tmp_name'][$source]; $file->filemime = file_get_mimetype($file->filename); $file->filesize = $_FILES['files']['size'][$source]; $extensions = ''; if (isset($validators['file_validate_extensions'])) { if (isset($validators['file_validate_extensions'][0])) { // Build the list of non-munged extensions if the caller provided them. $extensions = $validators['file_validate_extensions'][0]; } else { // If 'file_validate_extensions' is set and the list is empty then the // caller wants to allow any extension. In this case we have to remove the // validator or else it will reject all extensions. unset($validators['file_validate_extensions']); } } else { // No validator was provided, so add one using the default list. // Build a default non-munged safe list for file_munge_filename(). $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'; $validators['file_validate_extensions'] = array(); $validators['file_validate_extensions'][0] = $extensions; } if (!empty($extensions)) { // Munge the filename to protect against possible malicious extension hiding // within an unknown file type (ie: filename.html.foo). $file->filename = file_munge_filename($file->filename, $extensions); } // Rename potentially executable files, to help prevent exploits (i.e. will // rename filename.php.foo and filename.php to filename.php.foo.txt and // filename.php.txt, respectively). Don't rename if 'allow_insecure_uploads' // evaluates to TRUE. if (!variable_get('allow_insecure_uploads', 0) && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { $file->filemime = 'text/plain'; $file->uri .= '.txt'; $file->filename .= '.txt'; // The .txt extension may not be in the allowed list of extensions. We have // to add it here or else the file upload will fail. if (!empty($extensions)) { $validators['file_validate_extensions'][0] .= ' txt'; // Unlike file_save_upload() we don't need to let the user know that // for security reasons, your upload has been renamed, since RESTful // will return the file name in the response. } } // If the destination is not provided, use the temporary directory. if (empty($destination)) { $destination = 'temporary://'; } // Assert that the destination contains a valid stream. $destination_scheme = file_uri_scheme($destination); if (!$destination_scheme || !file_stream_wrapper_valid_scheme($destination_scheme)) { $message = format_string('The file could not be uploaded, because the destination %destination is invalid.', array('%destination' => $destination)); throw new \RestfulServiceUnavailable($message); } $file->source = $source; // A URI may already have a trailing slash or look like "public://". if (substr($destination, -1) != '/') { $destination .= '/'; } $file->destination = file_destination($destination . $file->filename, $replace); // If file_destination() returns FALSE then $replace == FILE_EXISTS_ERROR and // there's an existing file so we need to bail. if ($file->destination === FALSE) { $message = format_string('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $source, '%directory' => $destination)); throw new \RestfulServiceUnavailable($message); } // Add in our check of the the file name length. $validators['file_validate_name_length'] = array(); // Call the validation functions specified by this function's caller. $errors = file_validate($file, $validators); // Check for errors. if (!empty($errors)) { $message = format_string('The specified file %name could not be uploaded.', array('%name' => $file->filename)); if (count($errors) > 1) { $message .= theme('item_list', array('items' => $errors)); } else { $message .= ' ' . array_pop($errors); } throw new \RestfulServiceUnavailable($message); } // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary // directory. This overcomes open_basedir restrictions for future file // operations. $file->uri = $file->destination; if (!drupal_move_uploaded_file($_FILES['files']['tmp_name'][$source], $file->uri)) { watchdog('file', 'Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->uri)); $message = 'File upload error. Could not move uploaded file.'; throw new \RestfulServiceUnavailable($message); } // Set the permissions on the new file. drupal_chmod($file->uri); // If we are replacing an existing file re-use its database record. if ($replace == FILE_EXISTS_REPLACE) { $existing_files = file_load_multiple(array(), array('uri' => $file->uri)); if (count($existing_files)) { $existing = reset($existing_files); $file->fid = $existing->fid; } } // If we made it this far it's safe to record this file in the database. if ($file = file_save($file)) { // Add file to the cache. $upload_cache[$source] = $file; return $file; } // Something went wrong, so throw a general exception. throw new \RestfulServiceUnavailable('Unknown error has occurred.'); }
/** * Submit handler for reading a stream wrapper. * * Drupal now has full support for PHP's stream wrappers, which means that * instead of the traditional use of all the file functions * ($fp = fopen("/tmp/some_file.txt");) far more sophisticated and generalized * (and extensible) things can be opened as if they were files. Drupal itself * provides the public:// and private:// schemes for handling public and * private files. PHP provides file:// (the default) and http://, so that a * URL can be read or written (as in a POST) as if it were a file. In * addition, new schemes can be provided for custom applications. The Stream * Wrapper Example, if installed, impleents a custom 'session' scheme that * you can test with this example. * * Here we take the stream wrapper provided in the form. We grab the * contents with file_get_contents(). Notice that's it's as simple as that: * file_get_contents("http://example.com") or * file_get_contents("public://somefile.txt") just works. Although it's * not necessary, we use file_unmanaged_save_data() to save this file locally * and then find a local URL for it by using file_create_url(). * * @param array $form * An associative array containing the structure of the form. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. */ public function handleFileRead(array &$form, FormStateInterface $form_state) { $form_values = $form_state->getValues(); $uri = $form_values['fileops_file']; if (empty($uri) or !is_file($uri)) { drupal_set_message(t('The file "%uri" does not exist', array('%uri' => $uri)), 'error'); return; } // Make a working filename to save this by stripping off the (possible) // file portion of the streamwrapper. If it's an evil file extension, // file_munge_filename() will neuter it. $filename = file_munge_filename(preg_replace('@^.*/@', '', $uri), '', TRUE); $buffer = file_get_contents($uri); if ($buffer) { $sourcename = file_unmanaged_save_data($buffer, 'public://' . $filename); if ($sourcename) { $url = $this->getExternalUrl($sourcename); $this->setDefaultFile($sourcename); if ($url) { drupal_set_message($this->t('The file was read and copied to %filename which is accessible at <a href=":url">this URL</a>', array('%filename' => $sourcename, ':url' => $url->toString()))); } else { drupal_set_message($this->t('The file was read and copied to %filename (not accessible externally)', array('%filename' => $sourcename))); } } else { drupal_set_message(t('Failed to save the file')); } } else { // We failed to get the contents of the requested file. drupal_set_message(t('Failed to retrieve the file %file', array('%file' => $uri))); } }
/** * {@inheritdoc} * * @see ManagedFile::valueCallback * @see file_managed_file_save_upload() */ public static function valueCallback(&$element, $input, FormStateInterface $form_state) { $id = $element['#id']; // If a unique identifier added with '--', we need to exclude it if (preg_match('/(.*)(--[0-9]+)$/', $id, $reg)) { $id = $reg[1]; } // Seems cleaner to use something like this, but it's empty. // $request_files = \Drupal::request()->files; $input = $form_state->getUserInput(); $files = array(); foreach ($input as $key => $value) { if (preg_match('/' . $id . '_([0-9]+)_(.*)/', $key, $reg)) { $i = $reg[1]; $key = $reg[2]; // Only add the keys we expect. if (!in_array($key, array('tmpname', 'name', 'status'))) { continue; } // Munge the submitted file names for security. // // Similar munging is normally done by file_save_upload(), but submit // handlers for forms containing plupload elements can't use // file_save_upload(), for reasons discussed in plupload_test_submit(). // So we have to do this for them. // // Note that we do the munging here in the value callback function // (rather than during form validation or elsewhere) because we want to // actually modify the submitted values rather than reject them outright; // file names that require munging can be innocent and do not necessarily // indicate an attempted exploit. Actual validation of the file names is // performed later, in plupload_element_validate(). if (in_array($key, array('tmpname', 'name'))) { // Find the whitelist of extensions to use when munging. If there are // none, we'll be adding default ones in plupload_element_process(), so // use those here. if (isset($element['#upload_validators']['file_validate_extensions'][0])) { $extensions = $element['#upload_validators']['file_validate_extensions'][0]; } else { $validators = _plupload_default_upload_validators(); $extensions = $validators['file_validate_extensions'][0]; } $value = file_munge_filename($value, $extensions, FALSE); // To prevent directory traversal issues, make sure the file name does // not contain any directory components in it. (This more properly // belongs in the form validation step, but it's simpler to do here so // that we don't have to deal with the temporary file names during form // validation and can just focus on the final file name.) // // This step is necessary since this module allows a large amount of // flexibility in where its files are placed (for example, they could // be intended for public://subdirectory rather than public://, and we // don't want an attacker to be able to get them back into the top // level of public:// in that case). $value = rtrim(drupal_basename($value), '.'); // Based on the same feture from file_save_upload(). if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match('/\\.(php|pl|py|cgi|asp|js)(\\.|$)/i', $value) && substr($value, -4) != '.txt') { $value .= '.txt'; // The .txt extension may not be in the allowed list of extensions. // We have to add it here or else the file upload will fail. if (!empty($extensions)) { $element['#upload_validators']['file_validate_extensions'][0] .= ' txt'; drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $value))); } } } // The temporary file name has to be processed further so it matches what // was used when the file was written; see plupload_handle_uploads(). if ($key == 'tmpname') { $value = _plupload_fix_temporary_filename($value); // We also define an extra key 'tmppath' which is useful so that submit // handlers do not need to know which directory plupload stored the // temporary files in before trying to copy them. $files[$i]['tmppath'] = \Drupal::config('plupload.settings')->get('temporary_uri') . $value; } elseif ($key == 'name') { $value = \Drupal::service('transliteration')->transliterate($value); } // Store the final value in the array we will return. $files[$i][$key] = $value; } } return $files; }
/** * Uploads the file to EMDB. * * @param string $form_field_name * A string that is the associative array key of the upload form element in * the form array. * @param string $catalog_id * The catalog id for the catalog we are uploading to. * @param array $metadata * Additional properties that can be passed into EMDB and stored as metadata * on the file. These values are not stored locally in Drupal. * @param array $validators * An optional, associative array of callback functions used to validate the * file. * @param string|bool $destination_dir * A string containing the URI that the file should be copied to. This must * be a stream wrapper URI. If this value is omitted, Drupal's temporary * files scheme will be used ("temporary://"). * @param int $delta * Delta of the file to save or NULL to save all files. Defaults to NULL. * @param int $replace * Replace behavior when the destination file already exists: * - FILE_EXISTS_REPLACE: Replace the existing file. * - FILE_EXISTS_RENAME: Append _{incrementing number} until the filename is * unique. * - FILE_EXISTS_ERROR: Do nothing and return FALSE. * * @return \Drupal\embridge\EmbridgeAssetEntityInterface[] * Function returns array of files or a single file object if $delta * != NULL. Each file object contains the file information if the * upload succeeded or FALSE in the event of an error. Function * returns NULL if no file was uploaded. * * The docs for the "File interface" group, which you can find under * Related topics, or the header at the top of this file, documents the * components of a file entity. In addition to the standard components, * this function adds: * - source: Path to the file before it is moved. * - destination: Path to the file after it is moved (same as 'uri'). */ public static function saveUpload($form_field_name, $catalog_id, $metadata = array(), $validators = array(), $destination_dir = FALSE, $delta = NULL, $replace = FILE_EXISTS_RENAME) { $user = \Drupal::currentUser(); static $upload_cache; $file_upload = \Drupal::request()->files->get("files[{$form_field_name}]", NULL, TRUE); // Make sure there's an upload to process. if (empty($file_upload)) { return NULL; } // Return cached objects without processing since the file will have // already been processed and the paths in $_FILES will be invalid. if (isset($upload_cache[$form_field_name])) { if (isset($delta)) { return $upload_cache[$form_field_name][$delta]; } return $upload_cache[$form_field_name]; } // Prepare uploaded files info. Representation is slightly different // for multiple uploads and we fix that here. /** @var \Symfony\Component\HttpFoundation\File\UploadedFile[] $uploaded_files */ $uploaded_files = $file_upload; if (!is_array($file_upload)) { $uploaded_files = array($file_upload); } $assets = array(); foreach ($uploaded_files as $i => $file_info) { // Check for file upload errors and return FALSE for this file if a lower // level system error occurred. For a complete list of errors: // See http://php.net/manual/features.file-upload.errors.php. switch ($file_info->getError()) { case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: drupal_set_message(t('The file %file could not be saved because it exceeds %maxsize, the maximum allowed size for uploads.', array('%file' => $file_info->getFilename(), '%maxsize' => format_size(file_upload_max_size()))), 'error'); $assets[$i] = FALSE; continue; case UPLOAD_ERR_PARTIAL: case UPLOAD_ERR_NO_FILE: drupal_set_message(t('The file %file could not be saved because the upload did not complete.', array('%file' => $file_info->getFilename())), 'error'); $assets[$i] = FALSE; continue; case UPLOAD_ERR_OK: // Final check that this is a valid upload, if it isn't, use the // default error handler. if (is_uploaded_file($file_info->getRealPath())) { break; } default: // Unknown error. drupal_set_message(t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $file_info->getFilename())), 'error'); $assets[$i] = FALSE; continue; } // Begin building file entity. $values = array('uid' => $user->id(), 'filename' => $file_info->getClientOriginalName(), 'filesize' => $file_info->getSize(), 'catalog_id' => $catalog_id); $values['filemime'] = \Drupal::service('file.mime_type.guesser')->guess($values['filename']); // Create our Embridge Entity. /** @var \Drupal\embridge\EmbridgeAssetEntityInterface $asset */ $asset = EmbridgeAssetEntity::create($values); $extensions = ''; if (isset($validators['embridge_asset_validate_file_extensions'])) { if (isset($validators['embridge_asset_validate_file_extensions'][0])) { // Build the list of non-munged exts if the caller provided them. $extensions = $validators['embridge_asset_validate_file_extensions'][0]; } else { // If 'file_validate_extensions' is set and the list is empty then the // caller wants to allow any extension. In this case we have to remove // the validator or else it will reject all extensions. unset($validators['embridge_asset_validate_file_extensions']); } } else { // No validator was provided, so add one using the default list. // Build a default non-munged safe list for file_munge_filename(). $extensions = 'jpg jpeg gif png txt doc xls pdf ppt pps odt ods odp'; $validators['embridge_asset_validate_file_extensions'] = array(); $validators['embridge_asset_validate_file_extensions'][0] = $extensions; } if (!empty($extensions)) { // Munge the filename to protect against possible malicious extension // hiding within an unknown file type (ie: filename.html.foo). $asset->setFilename(file_munge_filename($asset->getFilename(), $extensions)); } // Rename potentially executable files, to help prevent exploits. if (!\Drupal::config('system.file')->get('allow_insecure_uploads') && preg_match('/\\.(php|pl|py|cgi|asp|js)(\\.|$)/i', $asset->getFilename()) && substr($asset->getFilename(), -4) != '.txt') { $asset->setMimeType('text/plain'); // The destination filename will also later be used to create the URI. $asset->setFilename($asset->getFilename() . '.txt'); // The .txt extension may not be in the allowed list of extensions. // We have to add it here or else the file upload will fail. if (!empty($extensions)) { $validators['embridge_asset_validate_file_extensions'][0] .= ' txt'; drupal_set_message(t('For security reasons, your upload has been renamed to %filename.', array('%filename' => $asset->getFilename()))); } } // If the destination is not provided, use the temporary directory. if (empty($destination_dir)) { $destination_dir = 'temporary://'; } // Assert that the destination contains a valid stream. $destination_scheme = file_uri_scheme($destination_dir); if (!file_stream_wrapper_valid_scheme($destination_scheme)) { drupal_set_message(t('The file could not be uploaded because the destination %destination is invalid.', array('%destination' => $destination_dir)), 'error'); $assets[$i] = FALSE; continue; } // A file URI may already have a trailing slash or look like "public://". if (substr($destination_dir, -1) != '/') { $destination_dir .= '/'; } $asset_destination = file_destination($destination_dir . $asset->getFilename(), $replace); // If file_destination() returns FALSE then $replace === FILE_EXISTS_ERROR // and there's an existing file so we need to bail. if ($asset_destination === FALSE) { drupal_set_message(t('The file %source could not be uploaded because a file by that name already exists in the destination %directory.', array('%source' => $form_field_name, '%directory' => $destination_dir)), 'error'); $assets[$i] = FALSE; continue; } // Add in our check of the file name length. // TODO: Do we need this? // $validators['file_validate_name_length'] = array(); // Call the validation functions specified by this function's caller. $errors = embridge_asset_validate($asset, $validators); // Check for errors. if (!empty($errors)) { $message = array('error' => array('#markup' => t('The specified file %name could not be uploaded.', array('%name' => $asset->getFilename()))), 'item_list' => array('#theme' => 'item_list', '#items' => $errors)); // @todo Add support for render arrays in drupal_set_message()? See // https://www.drupal.org/node/2505497. drupal_set_message(\Drupal::service('renderer')->renderPlain($message), 'error'); $assets[$i] = FALSE; continue; } // Move uploaded files from PHP's upload_tmp_dir to Drupal's temporary // directory. This overcomes open_basedir restrictions for future file // operations. $asset->setSourcePath($asset_destination); if (!drupal_move_uploaded_file($file_info->getRealPath(), $asset->getSourcePath())) { drupal_set_message(t('File upload error. Could not move uploaded file.'), 'error'); \Drupal::logger('file')->notice('Upload error. Could not move uploaded file %file to destination %destination.', array('%file' => $asset->getFilename(), '%destination' => $asset->getSourcePath())); $assets[$i] = FALSE; continue; } // Set the permissions on the new file. drupal_chmod($asset->getSourcePath()); // If we are replacing an existing file re-use its database record. // @todo Do not create a new entity in order to update it. See // https://www.drupal.org/node/2241865. if ($replace == FILE_EXISTS_REPLACE) { $existing_files = entity_load_multiple_by_properties('embridge_asset_entity', array('uri' => $asset->getSourcePath())); if (count($existing_files)) { $existing = reset($existing_files); $asset->setOriginalId($existing->id()); } } /** @var \Drupal\embridge\EnterMediaDbClientInterface $embridge_client */ $embridge_client = \Drupal::getContainer()->get('embridge.client'); try { $embridge_client->upload($asset, $metadata); } catch (\Exception $e) { $message = $e->getMessage(); drupal_set_message(t('Uploading the file "%file" to EnterMedia failed with the message "%message".', array('%file' => $asset->getFilename(), '%message' => $message)), 'error'); $assets[$i] = FALSE; continue; } // If we made it this far it's safe to record this file in the database. $asset->save(); $assets[$i] = $asset; } // Add files to the cache. $upload_cache[$form_field_name] = $assets; return isset($delta) ? $assets[$delta] : $assets; }