/** * Helper function that moves an uploaded file. * * @param string $filename * The path of the file to move. * @param string $uri * The path where to move the file. * * @return bool * TRUE if the file was moved. FALSE otherwise. */ protected static function moveUploadedFile($filename, $uri) { if (drupal_move_uploaded_file($filename, $uri)) { return TRUE; } return variable_get('restful_insecure_uploaded_flag', FALSE) && (bool) file_unmanaged_move($filename, $uri); }
/** * Helper function that moves an uploaded file. * * @param string $filename * The path of the file to move. * @param string $uri * The path where to move the file. * * @return bool * TRUE if the file was moved. FALSE otherwise. */ protected static function moveUploadedFile($filename, $uri) { return (bool) drupal_move_uploaded_file($filename, $uri); }
/** * 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.'); }
/** * 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; }