3 namespace Drupal\dropzonejs;
5 use Drupal\Component\Render\PlainTextOutput;
6 use Drupal\Core\Config\ConfigFactoryInterface;
7 use Drupal\Core\Entity\EntityTypeManagerInterface;
8 use Drupal\Core\Logger\LoggerChannelFactoryInterface;
9 use Drupal\Core\Render\RendererInterface;
10 use Drupal\Core\Session\AccountProxyInterface;
11 use Drupal\Core\StringTranslation\StringTranslationTrait;
12 use Drupal\Core\Utility\Token;
13 use Drupal\file\FileInterface;
14 use Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface;
15 use Drupal\Core\File\FileSystemInterface;
18 * A service that saves files uploaded by the dropzonejs element as files.
20 * Most of this file mimics or directly copies what core does. For more
21 * information and comments see file_save_upload().
23 class DropzoneJsUploadSave implements DropzoneJsUploadSaveInterface {
25 use StringTranslationTrait;
28 * Entity manager service.
30 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
32 protected $entityTypeManager;
35 * Mime type guesser service.
37 * @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface
39 protected $mimeTypeGuesser;
42 * The file system service.
44 * @var \Drupal\Core\File\FileSystemInterface
46 protected $fileSystem;
51 * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
56 * The renderer service.
58 * @var \Drupal\Core\Render\RendererInterface
63 * Config factory service.
65 * @var \Drupal\Core\Config\ConfigFactoryInterface
67 protected $configFactory;
72 * @var \Drupal\Core\Utility\Token
77 * Construct the DropzoneUploadSave object.
79 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
80 * Entity type manager service.
81 * @param \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $mimetype_guesser
82 * The mime type guesser service.
83 * @param \Drupal\Core\File\FileSystemInterface $file_system
84 * The file system service.
85 * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
86 * The logger factory service.
87 * @param \Drupal\Core\Render\RendererInterface $renderer
88 * The renderer service.
89 * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
90 * Config factory service.
91 * @param \Drupal\Core\Utility\Token $token
94 public function __construct(EntityTypeManagerInterface $entity_type_manager, MimeTypeGuesserInterface $mimetype_guesser, FileSystemInterface $file_system, LoggerChannelFactoryInterface $logger_factory, RendererInterface $renderer, ConfigFactoryInterface $config_factory, Token $token) {
95 $this->entityTypeManager = $entity_type_manager;
96 $this->mimeTypeGuesser = $mimetype_guesser;
97 $this->fileSystem = $file_system;
98 $this->logger = $logger_factory->get('dropzonejs');
99 $this->renderer = $renderer;
100 $this->configFactory = $config_factory;
101 $this->token = $token;
107 public function createFile($uri, $destination, $extensions, AccountProxyInterface $user, array $validators = []) {
108 // Create the file entity.
109 $uri = file_stream_wrapper_uri_normalize($uri);
110 $file_info = new \SplFileInfo($uri);
112 /** @var \Drupal\file\FileInterface $file */
113 $file = $this->entityTypeManager->getStorage('file')->create([
114 'uid' => $user->id(),
116 'filename' => $file_info->getFilename(),
118 'filesize' => $file_info->getSize(),
119 'filemime' => $this->mimeTypeGuesser->guess($uri),
122 // Replace tokens. As the tokens might contain HTML we convert it to plain
124 $destination = PlainTextOutput::renderFromHtml($this->token->replace($destination));
126 // Handle potentialy dangerous extensions.
127 $renamed = $this->renameExecutableExtensions($file);
128 // The .txt extension may not be in the allowed list of extensions. We have
129 // to add it here or else the file upload will fail.
130 if ($renamed && !empty($extensions)) {
131 $extensions .= ' txt';
132 drupal_set_message($this->t('For security reasons, your upload has been renamed to %filename.', ['%filename' => $file->getFilename()]));
135 // Validate the file.
136 $errors = $this->validateFile($file, $extensions, $validators);
137 if (!empty($errors)) {
140 '#markup' => $this->t('The specified file %name could not be uploaded.', ['%name' => $file->getFilename()]),
143 '#theme' => 'item_list',
147 drupal_set_message($this->renderer->renderPlain($message), 'error');
151 // Prepare destination.
152 if (!$this->prepareDestination($file, $destination)) {
153 drupal_set_message($this->t('The file could not be uploaded because the destination %destination is invalid.', ['%destination' => $destination]), 'error');
157 // Move uploaded files from PHP's upload_tmp_dir to destination.
158 $move_result = file_unmanaged_move($uri, $file->getFileUri());
160 drupal_set_message($this->t('File upload error. Could not move uploaded file.'), 'error');
161 $this->logger->notice('Upload error. Could not move uploaded file %file to destination %destination.', ['%file' => $file->getFilename(), '%destination' => $file->getFileUri()]);
165 // Set the permissions on the new file.
166 $this->fileSystem->chmod($file->getFileUri());
174 public function validateFile(FileInterface $file, $extensions, array $additional_validators = []) {
175 $validators = $additional_validators;
177 if (!empty($extensions)) {
178 $validators['file_validate_extensions'] = [$extensions];
180 $validators['file_validate_name_length'] = [];
182 // Call the validation functions specified by this function's caller.
183 return file_validate($file, $validators);
187 * Rename potentially executable files.
189 * @param \Drupal\file\FileInterface $file
190 * The file entity object.
193 * Whether the file was renamed or not.
195 protected function renameExecutableExtensions(FileInterface $file) {
196 if (!$this->configFactory->get('system.file')->get('allow_insecure_uploads') && preg_match('/\.(php|pl|py|cgi|asp|js)(\.|$)/i', $file->getFilename()) && (substr($file->getFilename(), -4) != '.txt')) {
197 $file->setMimeType('text/plain');
198 // The destination filename will also later be used to create the URI.
199 $file->setFilename($file->getFilename() . '.txt');
206 * Validate and set destination the destination URI.
208 * @param \Drupal\file\FileInterface $file
209 * The file entity object.
210 * @param string $destination
211 * A string containing the URI that the file should be copied to. This must
212 * be a stream wrapper URI.
215 * True if the destination was sucesfully validated and set, otherwise
218 protected function prepareDestination(FileInterface $file, $destination) {
219 // Assert that the destination contains a valid stream.
220 $destination_scheme = $this->fileSystem->uriScheme($destination);
221 if (!$this->fileSystem->validScheme($destination_scheme)) {
225 // Prepare the destination dir.
226 if (!file_exists($destination)) {
227 $this->fileSystem->mkdir($destination, NULL, TRUE);
230 // A file URI may already have a trailing slash or look like "public://".
231 if (substr($destination, -1) != '/') {
234 $destination = file_destination($destination . $file->getFilename(), FILE_EXISTS_RENAME);
235 $file->setFileUri($destination);