foldershare-8.x-1.2/src/Entity/FolderShareTraits/OperationAddFileTrait.php
src/Entity/FolderShareTraits/OperationAddFileTrait.php
<?php
namespace Drupal\foldershare\Entity\FolderShareTraits;
use Drupal\Component\Utility\Environment;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\FileInterface;
use Drupal\file\Entity\File;
use Drupal\foldershare\ManageLog;
use Drupal\foldershare\ManageFilenameExtensions;
use Drupal\foldershare\ManageFileSystem;
use Drupal\foldershare\Utilities\FileUtilities;
use Drupal\foldershare\Utilities\FormatUtilities;
use Drupal\foldershare\FolderShareInterface;
use Drupal\foldershare\Entity\Exception\LockException;
use Drupal\foldershare\Entity\Exception\ValidationException;
/**
* Add File entity as FolderShare entity.
*
* This trait includes methods to wrap File entities as FolderShare
* entities with kind 'file' or 'image'. The File object's ID is saved
* to the 'file' or 'image' entity reference field.
*
* <B>Internal trait</B>
* This trait is internal to the FolderShare module and used to define
* features of the FolderShare entity class. It is a mechanism to group
* functionality to improve code management.
*
* @ingroup foldershare
*/
trait OperationAddFileTrait {
/*---------------------------------------------------------------------
*
* Add file(s) to root.
*
*---------------------------------------------------------------------*/
/**
* Adds a file to the root list.
*
* If $allowRename is FALSE, an exception is thrown if the file's
* name is not unique within the root list. But if $allowRename is
* TRUE and the name is not unique, the file's name is adjusted
* to include a sequence number immediately before the first "."
* in the name, or at the end of the name if there is no "."
* (e.g. "myfile.png" becomes "myfile 1.png").
*
* An exception is thrown if the file is already in a folder.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_add_files" hook.
*
* <B>Process locks</B>
* The user's root list is locked for exclusive editing access by this
* function for the duration of the modification.
*
* <B>Activity log</B>
* This method posts a log message after each file is added.
*
* @param \Drupal\file\FileInterface $file
* The file to be added to the root list.
* @param bool $allowRename
* (optional) When TRUE, the file's name should be automatically renamed to
* insure it is unique within the folder. When FALSE, non-unique
* file names cause an exception to be thrown. Defaults to FALSE.
*
* @return \Drupal\foldershare\FolderShareInterface
* Returns the new FolderShare entity that wraps the given File entity.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
* @throws \Drupal\foldershare\Entity\Exception\ValidationException
* If the file could not be added because the file is already
* in a folder, or if the file doesn't pass validation because
* the name is invalid or already in use (if $allowRename FALSE).
*
* @see ::addFiles()
*/
public static function addFileToRoot(
FileInterface $file,
bool $allowRename = FALSE) {
// Add the file, checking that the name is legal (possibly rename it).
// Check if the file is already in the root list, and lock the root list
// as needed.
$ary = self::addFilesInternal(
NULL,
[$file],
(-1),
TRUE,
$allowRename,
TRUE);
if (empty($ary) === TRUE) {
return NULL;
}
return $ary[0];
}
/**
* Adds files to the root list.
*
* If $allowRename is FALSE, an exception is thrown if the file's
* name is not unique within the root list. But if $allowRename is
* TRUE and the name is not unique, the file's name is adjusted
* to include a sequence number immediately before the first "."
* in the name, or at the end of the name if there is no "."
* (e.g. "myfile.png" becomes "myfile 1.png").
*
* An exception is thrown if any file is already in a folder.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_add_files" hook.
*
* <B>Process locks</B>
* The user's root list is locked for exclusive editing access by this
* function for the duration of the modification.
*
* <B>Activity log</B>
* This method posts a log message after each file is added.
*
* @param \Drupal\file\FileInterface[] $files
* An array of files to be added to the root list. NULL files
* are silently skipped.
* @param bool $allowRename
* (optional) When TRUE, the file's name should be automatically renamed to
* insure it is unique within the folder. When FALSE, non-unique
* file names cause an exception to be thrown. Defaults to FALSE.
*
* @return \Drupal\foldershare\FolderShareInterface
* Returns the new FolderShare entity that wraps the given File entity.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
* @throws \Drupal\foldershare\Entity\Exception\ValidationException
* If the file could not be added because the file is already
* in a folder, or if the file doesn't pass validation because
* the name is invalid or already in use (if $allowRename FALSE).
*
* @see ::addFiles()
*/
public static function addFilesToRoot(
array $files,
bool $allowRename = FALSE) {
// Add the files, checking that their names are legal (possibly rename it).
// Check if the files are already in the folder, and lock the folder
// as needed.
$ary = self::addFilesInternal(
NULL,
$files,
(-1),
TRUE,
$allowRename,
TRUE);
if (empty($ary) === TRUE) {
return NULL;
}
return $ary[0];
}
/*---------------------------------------------------------------------
*
* Add file(s) to folder.
*
*---------------------------------------------------------------------*/
/**
* {@inheritdoc}
*/
public function addFile(
FileInterface $file,
bool $allowRename = FALSE) {
// Add the file, checking that the name is legal (possibly rename it).
// Check if the file is already in the folder, and lock the folder
// as needed.
$ary = self::addFilesInternal(
$this,
[$file],
(-1),
TRUE,
$allowRename,
TRUE);
if (empty($ary) === TRUE) {
return NULL;
}
return $ary[0];
}
/**
* {@inheritdoc}
*/
public function addFiles(
array $files,
bool $allowRename = FALSE) {
// Add the files, checking that their names are legal (possibly rename it).
// Check if the files are already in the folder, and lock the folder
// as needed.
$ary = self::addFilesInternal(
$this,
$files,
(-1),
TRUE,
$allowRename,
TRUE);
if (empty($ary) === TRUE) {
return NULL;
}
return $ary[0];
}
/*---------------------------------------------------------------------
*
* Add files implementation.
*
*---------------------------------------------------------------------*/
/**
* Implements addition of files to this folder.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B>
*
* If $allowRename is FALSE, an exception is thrown if the file's
* name is not unique within the folder. But if $allowRename is
* TRUE and the name is not unique, the file's name is adjusted
* to include a sequence number immediately before the first "."
* in the name, or at the end of the name if there is no "."
* (e.g. "myfile.png" becomes "myfile 1.png").
*
* All files are checked and renamed before any files are added
* to the folder. If any file is invalid, an exception is thrown
* and no files are added.
*
* An exception is thrown if any file is already in a folder, or if
* this entity is not a folder.
*
* <B>Process locks</B>
* This method locks the user's root list for additions at the root level,
* or locks the destination parent's root folder tree for additions to a
* folder. Locks are maintained for the duration of the addition.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_add_files" hook.
*
* <B>Activity log</B>
* This method posts a log message after each file is added.
*
* @param \Drupal\foldershare\FolderShareInterface $parent
* The parent entity, or NULL for the root list, to which to add
* the files.
* @param \Drupal\foldershare\file\FileInterface[] $files
* An array of File enities to be added to this folder. An empty
* array and NULL file objects are silently skipped.
* @param int $requestingUid
* (optional, default = current user) The user ID of the user requesting
* the operation. When interactive, this is the current user. When this
* is a background task, this is the original requesting user.
* @param bool $checkNames
* When TRUE, each file's name is checked to be sure that it is
* valid and not already in use. When FALSE, name checking is
* skipped (including renaming) and the caller must assure that
* names are good.
* @param bool $allowRename
* When TRUE (and when $checkNames is TRUE), each file's name
* will be automatically renamed, if needed, to insure that it
* is unique within the folder. When FALSE (and when $checkNames
* is TRUE), non-unique file names cause an exception to be thrown.
* @param bool $checkForInFolder
* When TRUE, an exception is thrown if the file is already in a
* folder. When FALSE, this expensive check is skipped.
*
* @return \Drupal\foldershare\FolderShareInterface[]
* Returns an array of the newly created FolderShare file entities
* added to this folder using the given $files File entities.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
* @throws \Drupal\foldershare\Entity\Exception\ValidationException
* Thrown if addition of the files does not validate (very unlikely).
* If $checkForInFolder, also thrown if any file is already in a
* folder. If $checkNames, also thrown if any name is illegal.
* If $checkNames and $allowRename, also thrown if a unique name
* could not be found. If $checkNames and !$allowRename, also
* thrown if a name is already in use.
*
* @see ::addFile()
* @see ::addFiles()
* @see ::addUploadFiles()
*/
private static function addFilesInternal(
FolderShareInterface $parent = NULL,
array $files = [],
int $requestingUid = (-1),
bool $checkNames = TRUE,
bool $allowRename = FALSE,
bool $checkForInFolder = TRUE) {
if (empty($files) === TRUE) {
// Nothing to add.
return NULL;
}
if ($requestingUid < 0) {
$requestingUid = self::getCurrentUserId()[0];
}
//
// Validate
// --------
// Confirm the parent is provide and it is a folder.
if ($parent !== NULL && $parent->isFolder() === FALSE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'@method was called with an item that is not a folder.',
[
'@method' => __METHOD__,
])));
}
//
// Check that files are not in a folder.
// -------------------------------------
// Check that all of the files are NOT in a folder already.
$filesWithGoodParentage = [];
if ($checkForInFolder === TRUE) {
foreach ($files as $index => $file) {
if ($file === NULL) {
continue;
}
// Get the file's parent folder ID. There are three cases:
// - FALSE = file has no parent, which is what we want.
// - $parent->id = file's parent is this folder (if any), so skip it.
// - other = file already has parent, so error.
$fileParentId = self::findFileWrapperId($file);
if ($fileParentId !== FALSE) {
// The file is apparently already wrapped by a FolderShare entity.
if ($parent !== NULL && $fileParentId === (int) $parent->id()) {
// Already in this folder. Skip it.
continue;
}
// Complain.
$v = new ValidationException(FormatUtilities::createFormattedMessage(
t(
'@method was called to add a file to a folder, but it is already there.',
[
'@method' => __METHOD__,
])));
$v->setItemNumber($index);
throw $v;
}
// The file is not already in a folder, so add it to the list
// to process.
$filesWithGoodParentage[] = $file;
}
}
else {
// Files are assumed to be not in a folder.
foreach ($files as $file) {
if ($file !== NULL) {
$filesWithGoodParentage[] = $file;
}
}
}
if (empty($filesWithGoodParentage) === TRUE) {
// Nothing to add.
return NULL;
}
//
// Check name legality.
// --------------------
// Check that all files have legal names.
if ($checkNames === TRUE) {
foreach ($filesWithGoodParentage as $index => $file) {
// Check that the new name is legal.
$name = $file->getFilename();
if (self::isNameLegal($name) === FALSE) {
throw new ValidationException(
self::getStandardIllegalNameExceptionMessage($name));
}
// Verify that the name meets any file name extension restrictions.
if (ManageFilenameExtensions::isNameExtensionAllowed($name, NULL) === FALSE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'The file type used by "@name" is not supported.',
[
'@name' => $name,
]),
t(
'The file name uses a file type extension "@extension" that is not supported on this site.',
[
'@extension' => ManageFilenameExtensions::getExtensionFromPath($name),
]),
t('Supported file type extensions:'),
ManageFilenameExtensions::getAllowedNameExtensions()));
}
}
}
//
// Lock root list or root folder.
// ------------------------------
// When adding files to the user's root list, lock that root list.
// This lock is needed while we check for name collisions there.
//
// When adding files to a folder, lock that folder. This too is
// needed while we check for name collisions theer.
if ($parent === NULL) {
// LOCK USER'S ROOT LIST.
if (self::acquireUserRootListLock() === FALSE) {
throw new LockException(
self::getStandardLockExceptionMessage(t('added'), $name));
}
}
else {
// LOCK PARENT ROOT'S FOLDER TREE.
$rootId = $parent->getRootItemId();
if (self::acquireRootOperationLock($rootId) === FALSE) {
throw new LockException(
self::getStandardLockExceptionMessage(t('added'), $name));
}
}
//
// Check names.
// ------------
// Check that names do not collide with whatever is already there.
$filesToAdd = [];
$originalNames = [];
$savedException = NULL;
if ($checkNames === TRUE) {
// Get a list of names to check against.
if ($parent === NULL) {
$childNames = self::findAllRootItemNames($requestingUid);
}
else {
$childNames = $parent->findChildrenNames();
}
foreach ($filesWithGoodParentage as $index => $file) {
$name = $file->getFilename();
if ($allowRename === TRUE) {
// If not unique, rename.
$uniqueName = self::createUniqueName($childNames, $name);
if ($uniqueName === FALSE) {
$savedException = new ValidationException(
self::getStandardCannotCreateUniqueNameExceptionMessage('new file'));
$savedException->setItemNumber($index);
break;
}
// Change public file name. This name also appears in the URI.
$originalNames[(int) $file->id()] = $name;
$file->setFilename($uniqueName);
$file->setFileUri(ManageFileSystem::getFileUri($file));
$file->save();
// Add to the child name list so that further checks
// will catch collisions.
$childNames[$uniqueName] = 1;
}
else {
if (isset($childNames[$name]) === TRUE) {
// Not unique and not allowed to rename. Fail.
$savedException = new ValidationException(
self::getStandardNameInUseExceptionMessage($name));
$savedException->setItemNumber($index);
break;
}
// Add to the child name list so that further checks
// will catch collisions.
$childNames[$name] = 1;
}
$filesToAdd[] = $file;
}
if ($savedException !== NULL) {
// Something went wrong. Some files might already have
// been renamed. Restore them to their prior names.
if ($allowRename === TRUE) {
foreach ($filesWithGoodParentage as $file) {
if (isset($originalNames[(int) $file->id()]) === TRUE) {
// Restore the prior name.
$file->setFilename($originalNames[(int) $file->id()]);
$file->setFileUri(ManageFileSystem::getFileUri($file));
$file->save();
}
}
}
if ($parent === NULL) {
// UNLOCK USER'S ROOT LIST.
self::releaseUserRootListLock();
}
else {
// UNLOCK PARENT ROOT'S FOLDER TREE.
self::releaseRootOperationLock($parent->getRootItemId());
}
throw $savedException;
}
}
else {
// All of the files are assumed to have safe names.
$filesToAdd = $filesWithGoodParentage;
}
if (empty($filesToAdd) === TRUE) {
if ($parent === NULL) {
// UNLOCK USER'S ROOT LIST.
self::releaseUserRootListLock();
}
else {
// UNLOCK PARENT ROOT'S FOLDER TREE.
self::releaseRootOperationLock($parent->getRootItemId());
}
return NULL;
}
//
// Add files.
// ----------
// Create FolderShare entities for the files. For each one,
// set the parent ID to be the parent folder, and the root ID to
// be the parent folder's root.
$parentid = ($parent === NULL) ? NULL : $parent->id();
$rootid = ($parent === NULL) ? NULL : $parent->getRootItemId();
$newEntities = [];
foreach ($filesToAdd as $file) {
// Get the MIME type for the file.
$mimeType = $file->getMimeType();
// If the file is an image, set the 'image' field to the file ID
// and the kind to IMAGE_KIND. Otherwise set the 'file' field to
// the file ID and the kind to FILE_KIND.
if (self::isMimeTypeImage($mimeType) === TRUE) {
$valueForFileField = NULL;
$valueForImageField = $file->id();
$valueForKindField = self::IMAGE_KIND;
}
else {
$valueForFileField = $file->id();
$valueForImageField = NULL;
$valueForKindField = self::FILE_KIND;
}
// Create new FolderShare entity in the parent folder or the root list.
// - Automatic id.
// - Automatic uuid.
// - Automatic creation date.
// - Automatic changed date.
// - Automatic langcode.
// - Empty description.
// - Empty author grants.
// - Empty view grants.
// - Empty disabled grants.
$f = self::create([
'name' => $file->getFilename(),
'uid' => $requestingUid,
'kind' => $valueForKindField,
'mime' => $mimeType,
'file' => $valueForFileField,
'image' => $valueForImageField,
'size' => $file->getSize(),
'parentid' => $parentid,
'rootid' => $rootid,
]);
// Add default grants to a root item.
$f->addDefaultAccessGrants();
$f->save();
$newEntities[] = $f;
}
// Update the sizes of the parent folder and its ancestors.
if ($parent !== NULL) {
$parent->updateSizeAndAncestors();
}
//
// Unlock.
// -------
// Unlock the user's root list or the parent root folder tree, depending
// on which one was locked earlier.
if ($parent === NULL) {
// UNLOCK USER'S ROOT LIST.
self::releaseUserRootListLock();
}
else {
// UNLOCK PARENT ROOT'S FOLDER TREE.
self::releaseRootOperationLock($parent->getRootItemId());
}
//
// Hook & log.
// -----------
// Announce the addition.
if ($parent === NULL) {
self::postOperationHook(
'add_files',
[
NULL,
$newEntities,
$requestingUid,
]);
foreach ($newEntities as $e) {
ManageLog::activity(
"Added file '@name' (# @id) to top level.",
[
'@id' => $e->id(),
'@name' => $e->getName(),
'entity' => $e,
'uid' => $requestingUid,
]);
}
}
else {
self::postOperationHook(
'add_files',
[
$parent,
$newEntities,
$requestingUid,
]);
foreach ($newEntities as $e) {
ManageLog::activity(
"Added file '@name' (# @id) to '@parentName' (# @parentId).",
[
'@id' => $e->id(),
'@name' => $e->getName(),
'@parentId' => $parent->id(),
'@parentName' => $parent->getName(),
'entity' => $e,
'uid' => $requestingUid,
]);
}
}
return $newEntities;
}
/*---------------------------------------------------------------------
*
* File utilities.
*
* These functions handle quirks of the File module.
*
*---------------------------------------------------------------------*/
/**
* Creates a File entity for an existing local file.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B>
*
* A new File entity is created using the existing file, and named using
* the given file name. In the process, the file is moved into the proper
* FolderShare directory tree and the stored file renamed using the
* module's numeric naming scheme.
*
* The filename, MIME type, and file size are set, the File entity is
* marked as a permanent file, and the file's permissions are set for
* access by the web server.
*
* <B>Process locks</B>
* This function does not lock access. The caller should lock around changes
* to the entity.
*
* @param string $uri
* The URI to a stored file on the local file system.
* @param string $name
* The publically visible file name for the new File entity. This should
* be a legal file name, without a preceding path or scheme. The MIME
* type of the new File entity is based on this name.
*
* @return \Drupal\file\FileInterface
* Returns a new File entity with the given file name, and a properly
* set MIME type and size. The entity is owned by the current user.
* The File's URI points to a FolderShare directory tree file moved
* from the given local path. A NULL is returned if the local path is
* empty.
*
* @throws \Drupal\foldershare\Entity\Exception\SystemException
* Thrown if an error occurs when trying to create any file or directory
* or move the local file to the proper directory.
*/
private static function createFileEntityFromLocalFile(
string $uri,
string $name = NULL) {
// If the path is empty, there is no file to create.
if (empty($uri) === TRUE || empty($name) === TRUE) {
return NULL;
}
//
// Setup
// -----
// Get the new file's MIME type and size.
$mimeType = \Drupal::service('file.mime_type.guesser')->guess($name);
$fileSize = FileUtilities::filesize($uri);
//
// Create initial File entity
// --------------------------
// Create a File object that wraps the file in its current location.
// This is not the final File object, which must be adjusted by moving
// the file from its current location to FolderShare's directory tree.
//
// The status of 0 marks the file as temporary. If left this way, the
// File module will automatically delete the file in the future.
$file = File::create([
'uid' => \Drupal::currentUser()->id(),
'uri' => $uri,
'filename' => $name,
'filemime' => $mimeType,
'filesize' => $fileSize,
'status' => 0,
]);
$file->save();
//
// Move file and update File entity
// --------------------------------
// Creating the File object assigns it a unique ID. We need this ID
// to create a new long-term local file name and location when the
// file is in its proper location in the FolderShare directory tree.
//
// Create the new proper file URI with a numeric ID-based name and path.
$storedUri = ManageFileSystem::getFileUri($file);
// Move the stored file from its current location to the FolderShare
// directory tree and rename it using the File entity's ID. This updates
// the File entity.
//
// Moving the file also changes the file name and MIME type to values
// based on the new URI. This is not what we want, so we'll have to
// fix this below.
$newFile = file_move(
$file,
$storedUri,
FileSystemInterface::EXISTS_REPLACE);
if ($newFile === FALSE) {
// The file move has failed.
$file->delete();
ManageLog::critical(
"Local file system: A file at '@name' could not be moved to '@destination'.\nThere may be a problem with server's directories or permissions.",
[
'@name' => $file->getFilename(),
'@destination' => 'temp directory',
]);
throw new SystemException(t(
"System error. A file at '@name' could not be moved to '@destination'.\nThere may be a problem with directories or permissions. Please report this to the site administrator.",
[
'@name' => $name,
'@destination' => $storedUri,
]));
}
// Mark the file permanent and fix the name and MIME type.
$newFile->setPermanent();
$newFile->setFilename($name);
$newFile->setFileUri(ManageFileSystem::getFileUri($newFile));
$newFile->setMimeType($mimeType);
$newFile->save();
return $newFile;
}
/**
* Duplicates a File object.
*
* <B>This method is internal and strictly for use by the FolderShare
* module itself.</B>
*
* The file is duplicated, creating a new copy on the local file system.
*
* Exceptions are very unlikely and should only occur when something
* catastrophic happens to the underlying file system, such as if it
* runs out of space, if someone deletes key directories, or if the
* file system goes offline.
*
* <B>Process locks</B>
* This function does not lock access. The caller should lock around changes
* to the entity.
*
* @param \Drupal\file\FileInterface $file
* The file to copy.
* @param string $newName
* (optional, default = '') The new name for the file copy. If empty,
* the existing name is used.
* @param int $newOwnerId
* (optional, default = (-1)) The new owner ID for the file copy.
* If negative, the existing owner ID is used.
*
* @return \Drupal\file\FileInterface
* The new file copy.
*
* @throws \Drupal\foldershare\Entity\Exception\SystemException
* Throws an exception if a serious system error occurred, such as a
* file system becomes unreadable/unwritable, gets full, or goes offline.
*
* @see ::copyAndAddFilesInternal()
*/
private static function duplicateFileEntityInternal(
FileInterface $file,
string $newName = '',
int $newOwnerId = (-1)) {
//
// Implementation note:
//
// The File module's file_copy() will copy a File object into a new
// File object with a new URI for the file name. However, our
// file naming scheme for local storage file names uses the object's
// entity ID, and we don't know that before calling file_copy().
//
// So, this function calls file_copy() to copy the given file into a
// temp file, and then calls file_move() to move the temp file into
// a file with the proper entity ID-based name.
//
// Complicating things, file_copy() and file_move() both invoke
// hook functions that can rename the file, which is not appropriate
// here. So, after moving the file, the file name is restored to
// something reasonable.
if ($file === NULL) {
return NULL;
}
// Copy the file into a temp location.
//
// Allow the file to be renamed in order to avoid name collisions
// with any other temp files in progress.
$newFile = file_copy(
$file,
ManageFileSystem::createLocalTempFile(),
FileSystemInterface::EXISTS_RENAME);
if ($newFile === FALSE) {
// Unfortunately, file_copy returns FALSE on an error
// and provides no further details to us on the problem.
// Instead, it writes a message to the log file and/or
// to the output page, which is not useful to us or
// meaningful to the user.
//
// Looking at the source code, the following types of
// errors could occur:
// - The source file doesn't exist.
// - The destination directory doesn't exist.
// - The destination directory can't be written.
// - The destination filename is in use.
// - The source and destination are the same.
// - Some other error occurred (probably a system error).
//
// Since the directory and file names are under our control
// and are valid, the only errors that can occur here are
// catastrophic, such as:
// - File deleted out from under us.
// - File system changed out from under Drupal.
// - File system full, offline, hard disk dead, etc.
//
// For any of these, it is unlikely we can continue with
// copying anything. Time to abort.
ManageLog::critical(
"Local file system: A local file at \"@name\" could not be copied to the site's temporary directory.\nThere may be a problem with server's directories and/or permissions.",
[
'@name' => $file->getFilename(),
]);
throw new SystemException(FormatUtilities::createFormattedMessage(
t(
'System error. A local file at "@name" could not be copied to the site\'s temporary directory.',
[
'@name' => $file->getFilename(),
]),
t("There may be a problem with the site's directories or permissions. Please report this to the site administrator.")));
}
// The File copy's fields are mostly correct:
// - 'filesize' matches the original file's size.
// - 'uid' is for the current user.
// - 'fid', 'uuid', 'created', and 'changed' are new and uptodate.
// - 'langcode' is at a default.
// - 'status' is permanent.
//
// The following fields need correcting:
// - 'uri' and 'filename' are for a temp file.
//
// Change the copied file's user-visible file name to match the
// original file's name. This does not change the name on disk.
// This must be set before we create the new URI because the filename's
// extension is used in the new URI.
//
// Change the copied file's MIME type to match the original file's
// MIME type.
//
// The URI is fixed now by moving the file.
//
// With an entity ID for the new file, create the correct URI
// and move the file there.
$newFile->setFilename($file->getFilename());
$newUri = ManageFileSystem::getFileUri($newFile);
$newerFile = file_move(
$newFile,
$newUri,
FileSystemInterface::EXISTS_REPLACE);
// Because of EXISTS_REPLACE, the returned File object in
// $newerFile has the same entity ID as $newFile, unless the
// call returns FALSE to indicate an error.
if ($newerFile === FALSE) {
// See the above comment about ways file_copy() can fail.
// The same applies here.
$newFile->delete();
ManageLog::critical(
"Local file system: A local file at '@name' could not be moved to '@destination'.\nThere may be a problem with the server's directories and/or permissions.",
[
'@name' => $file->getFilename(),
'@destination' => $newUri,
]);
throw new SystemException(FormatUtilities::createFormattedMessage(
t(
'System error. A local file at "@name" could not be moved to "@destination".',
[
'@name' => $file->getFilename(),
'@destination' => $newUri,
]),
t("There may be a problem with the site's directories or permissions. Please report this to the site administrator.")));
}
unset($newUri);
$newerFile->setMimeType($file->getMimeType());
if (empty($newName) === FALSE) {
$newerFile->setFilename($newName);
}
if ($newOwnerId >= 0) {
$newerFile->setOwnerId($newOwnerId);
}
$newerFile->save();
// Garbage collect. The file_move() above creates multiple objects
// then deletes them. Flush them from memory ASAP.
gc_collect_cycles();
return $newerFile;
}
/*---------------------------------------------------------------------
*
* Upload files.
*
*---------------------------------------------------------------------*/
/**
* Adds uploaded files for the named form field into the root list.
*
* When a file is uploaded via an HTTP form post from a browser, PHP
* automatically saves the data into "upload" files saved in a
* PHP-managed temporary directory. This method sweeps those uploaded
* files, pulls out the ones associated with the named form field,
* and adds them to this folder with their original names.
*
* If there are no uploaded files, this method returns immediately.
*
* Files may be automatically renamed, if needed, to insure they have
* unique names within the folder.
*
* <B>Process locks</B>
* This method locks the user's root list for the duration of the addition.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_add_files" hook.
*
* <B>Activity log</B>
* This method posts a log message after the files have been added.
*
* @param string $formFieldName
* The name of the form field with associated uploaded files pending.
* @param bool $allowRename
* (optional, default = TRUE) When TRUE, if a file's name collides with the
* name of an existing entity, the name is modified by adding a number on
* the end so that it doesn't collide. When FALSE, a file name collision
* throws an exception.
*
* @return array
* The returned array contains one entry per uploaded file.
* An entry may be a File object uploaded into the current folder,
* or a string containing an error message about that file and
* indicating that it could not be uploaded and added to the folder.
* An empty array is returned if there were no files to upload
* and add to the folder.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
*
* @see ::addFile()
* @see ::addFiles()
*
* @todo The error messages returned here should be removed in favor of
* error codes and arguments so that the caller can know which file
* had which error and do something about it or report their own
* error message. Returning text messages here is not flexible.
*/
public static function addUploadFilesToRoot(
string $formFieldName,
bool $allowRename = TRUE) {
return self::addUploadFilesInternal(
NULL,
$formFieldName,
(-1),
$allowRename);
}
/**
* {@inheritdoc}
*/
public function addUploadFiles(
string $formFieldName,
bool $allowRename = TRUE) {
return self::addUploadFilesInternal(
$this,
$formFieldName,
(-1),
$allowRename);
}
/**
* Adds uploaded files to the selected parent folder or root list.
*
* <B>Process locks</B>
* If there is no parent, the user's root list is locked for the duration
* of the addition. If there is a parent, the parent's root folder tree is
* locked for the duration of the addition.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_add_files" hook.
*
* <B>Activity log</B>
* This method posts a log message after the files have been added.
*
* @param \Drupal\foldershare\FolderShareInterface $parent
* The parent folder into which to place the uploaded files. If NULL,
* uploaded files are added to the root list.
* @param string $formFieldName
* The name of the form field with associated uploaded files pending.
* @param int $requestingUid
* (optional, default = current user) The user ID of the user requesting
* the operation. When interactive, this is the current user. When this
* is a background task, this is the original requesting user.
* @param bool $allowRename
* (optional, default = TRUE) When TRUE, if a file's name collides with the
* name of an existing entity, the name is modified by adding a number on
* the end so that it doesn't collide. When FALSE, a file name collision
* throws an exception.
*
* @return array
* The returned array contains one entry per uploaded file.
* An entry may be a File object uploaded into the current folder,
* or a string containing an error message about that file and
* indicating that it could not be uploaded and added to the folder.
* An empty array is returned if there were no files to upload
* and add to the folder.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
*
* @todo The error messages returned here should be removed in favor of
* error codes and arguments so that the caller can know which file
* had which error and do something about it or report their own
* error message. Returning text messages here is not flexible.
*/
private static function addUploadFilesInternal(
FolderShareInterface $parent = NULL,
string $formFieldName = '',
int $requestingUid = (-1),
bool $allowRename = TRUE) {
static $uploadCache;
//
// Drupal's file_save_upload() function is widely used for handling
// uploaded files. It does several activities at once:
//
// - 1. Collect all uploaded files.
// - 2. Validate that the uploads completed.
// - 3. Run plug-in validators.
// - 4. Optionally check for file name extensions.
// - 5. Add ".txt" on executable files.
// - 6. Rename inner extensions (e.g. ".tar.gz" -> "._tar.gz").
// - 7. Validate file names are not too long.
// - 8. Validate the destination exists.
// - 9. Optionally rename files to avoid destination collisions.
// - 10. chmod the file to be accessible.
// - 11. Create a File object.
// - 12. Set the File object's URI, filename, and MIME type.
// - 13. Optionally replace a prior File object with a new file.
// - 14. Save the File object.
// - 15. Cache the File objects.
//
// There are several problems, though.
//
// - The plug-in validators in (3) are not needed by us.
//
// - The extension checking in (4) can only be turned off for
// the first file checked, due to a bug in the current code.
//
// - The extension changes in (5) and (6) are mandatory, but
// nonsense when we'll be storing files without extensions.
//
// - The file name length check in (7) uses PHP functions that
// are not multi-byte character safe, and it limits names to
// 240 characters, independent of the actual field length.
//
// - The file name handling loses the original file name that
// we need to maintain and show to users.
//
// - The file movement can't leave the file in our desired
// destination directory because that directory's name is a
// function of the entity ID, which isn't known until after
// file_save_upload() has created the file and moved it to
// what it thinks is the final destination.
//
// - Any errors generated are logged and reported directly to
// to the user. No exceptions are thrown. The only error
// indicator returned to the caller is that the array of
// returned File objects can include a FALSE for a file that
// failed. But there is no indication about which file it was
// that failed, or why.
//
// THIS function repeats some of the steps in file_save_upload(),
// but skips ones we don't need. It also keeps track of errors and
// returns error messages instead of blurting them out to the user.
//
// Validate inputs
// ---------------
// Validate uploads exist and that they are for this field.
if (empty($formFieldName) === TRUE) {
// No field to get files for.
return [];
}
if ($requestingUid < 0) {
$requestingUid = self::getCurrentUserId()[0];
}
// Get a list of all uploaded files, across all ongoing activity
// for all uploads of any type.
$allFiles = \Drupal::request()->files->get('files', []);
// If there is nothing for the requested form field, return.
if (isset($allFiles[$formFieldName]) === FALSE) {
return [];
}
// If the file list for the requested form field is empty, return.
$filesToProcess = $allFiles[$formFieldName];
unset($allFiles);
if (empty($filesToProcess) === TRUE) {
return [];
}
// If there is just one item, turn it into an array of items
// to simplify further code.
if (is_array($filesToProcess) === FALSE) {
$filesToProcess = [$filesToProcess];
}
//
// Cache shortcut
// --------------
// It is conceivable that this function gets called multiple
// times on the same form. To avoid redundant processing,
// check a cache of recently uploaded files and return from
// that cache quickly if possible.
//
// The cache will be cleared and set to a new list of files
// at the end of this function.
if (isset($uploadCache[$formFieldName]) === TRUE) {
return $uploadCache[$formFieldName];
}
//
// Validate upload success
// -----------------------
// Loop through the available uploaded files and separate out the
// ones that failed, along with an error message about why it failed.
$goodFiles = [];
$failedMessages = [];
foreach ($filesToProcess as $index => $fileInfo) {
if ($fileInfo === NULL) {
// Odd. A file is listed in the uploads, but it isn't really there.
$failedMessages[$index] = (string) t(
"System error. The @index-th uploaded file could not be found. Please try again.",
[
'@index' => $index,
]);
continue;
}
$filename = $fileInfo->getClientOriginalName();
// Check for errors. On any error, create an error message
// and add it to the messages array. If the error is very
// severe, also log it.
switch ($fileInfo->getError()) {
case UPLOAD_ERR_INI_SIZE:
// Exceeds max PHP size.
case UPLOAD_ERR_FORM_SIZE:
// Exceeds max form size.
$failedMessages[$index] = (string) t(
"Maximum file size limit exceeded.\nThe file '@file' could not be added to the folder because it exceeds the website's maximum allowed file size of @maxsize.",
[
'@file' => $filename,
'@maxsize' => FormatUtilities::formatBytes(Environment::getUploadMaxSize()),
]);
break;
case UPLOAD_ERR_PARTIAL:
// Uploaded only partially uploaded.
$failedMessages[$index] = (string) t(
"Interrupted file upload.\nThe file '@file' could not be added to the folder because the upload was interrupted and only part of the file was received.",
[
'@file' => $filename,
]);
break;
case UPLOAD_ERR_NO_FILE:
// Upload wasn't started.
$failedMessages[$index] = (string) t(
"Maximum upload number limit exceeded.\nThe file '@file' could not be added to the folder because its inclusion exceeds the website's maximum allowed number of file uploads at one time.",
[
'@file' => $filename,
]);
break;
case UPLOAD_ERR_NO_TMP_DIR:
// No temp directory configured.
$failedMessages[$index] = (string) t(
"Website configuration problem.\nThe file '@file' could not be added to the folder because the website encountered a site configuration error about a missing temporary directory. Please report this to the site administrator.",
[
'@file' => $filename,
]);
ManageLog::critical(
"Local file system: File upload failed because the server's PHP temporary directory is missing!");
break;
case UPLOAD_ERR_CANT_WRITE:
// Temp directory not writable.
$failedMessages[$index] = (string) t(
"Website configuration problem.\nThe file '@file' could not be added to the folder because the website encountered a site configuration error. The site's temporary directory is missing write permissions. Please report this to the site administrator.",
[
'@file' => $filename,
]);
ManageLog::critical(
"Local file system: File upload failed because the server's PHP temporary directory is not writable!");
break;
case UPLOAD_ERR_EXTENSION:
// PHP extension failed for some reason.
$failedMessages[$index] = (string) t(
"Website configuration problem.\nThe file '@file' could not be added to the folder because the website encountered a site configuration error. Please report this to the site administrator.",
[
'@file' => $filename,
]);
ManageLog::critical(
"Misconfigured web site: File upload failed because a PHP extension failed for an unknown reason.\nThe server's installation of PHP appears to be misconfigured.");
break;
case UPLOAD_ERR_OK:
// Success!
if (is_uploaded_file($fileInfo->getRealPath()) === FALSE) {
// But the file doesn't actually exist!
$failedMessages[$index] = (string) t(
"Website internal error.\nThe file '@file' could not be added to the folder because the data was lost during the upload.",
[
'@file' => $filename,
]);
ManageLog::error(
"Local file system: File upload failed because the local uploaded file went missing after the upload completed.");
}
else {
$goodFiles[$index] = $fileInfo;
}
break;
default:
// Unknown error.
$failedMessages[$index] = (string) t(
"Website internal error.\nThe file '@file' could not be added to the folder because of an unknown problem.",
[
'@file' => $filename,
]);
ManageLog::warning(
"Local file system: File upload failed with an unrecognized error '@code'.",
[
'@code' => $fileInfo->getError(),
]);
break;
}
}
unset($filesToProcess);
//
// Validate names
// --------------
// Check that all of the original file names are legal for storage
// in this module. This checks file name length and character content,
// and allows for multi-byte characters.
$passedFiles = [];
foreach ($goodFiles as $index => $fileInfo) {
$filename = $fileInfo->getClientOriginalName();
if (self::isNameLegal($filename) === FALSE) {
$failedMessages[$index] = (string) t(
"The name '@name' cannot be used.\nThe file '@file' could not be added to the folder because it's name must be between 1 and 255 characters long and the name cannot use ':', '/', or '\\' characters.",
[
'@file' => $filename,
]);
}
else {
$passedFiles[$index] = $fileInfo;
}
}
// And reduce the good files list to the ones that passed.
$goodFiles = $passedFiles;
unset($passedFiles);
// If there are no good files left, return the errors.
if (empty($goodFiles) === TRUE) {
$uploadCache[$formFieldName] = $failedMessages;
return $failedMessages;
}
//
// Validate extensions
// -------------------
// The folder's 'file' field contains the allowed filename extensions
// for this site. If the list is empty, do not do extension checking.
//
// Note that we specifically DO NOT do some of the extension handling
// found in file_save_upload():
//
// - We do not add ".txt" to the end of executable files
// (.php, .pl, .py, .cgi, .asp, and .js). This was intended
// to protect web servers from unintentionally executing
// uploaded files. However, for this module all uploaded files
// will stored without extensions, so this is not necessary.
//
// - We do not replace inner extensions (e.g. "archive.tar.gz")
// with a "._". Again, this was intended to protect web servers
// from falling back from the last extension to an inner
// extension and, again, unintentionally executing uploaded
// files. However, for this module all uploaded files will be
// stored without extensions, so this is not necessary.
$extensionsString = ManageFilenameExtensions::getAllowedNameExtensions();
if (empty($extensionsString) === FALSE) {
// Break up the extensions.
$extensions = mb_split(' ', $extensionsString);
// Loop through the good files again and split off the
// ones with good extensions.
$passedFiles = [];
foreach ($goodFiles as $index => $fileInfo) {
$filename = $fileInfo->getClientOriginalName();
if (ManageFilenameExtensions::isNameExtensionAllowed($filename, $extensions) === FALSE) {
// Extension is not allowed.
$failedMessages[$index] = (string) t(
"Unsupported file type.\nThe file '@file' could not be added to the folder because it uses a file name extension that is not allowed by this website.",
[
'@file' => $filename,
]);
}
else {
$passedFiles[$index] = $fileInfo;
}
}
// And reduce the good files list to the ones that passed.
$goodFiles = $passedFiles;
unset($passedFiles);
// If there are no good files left, return the errors.
if (empty($goodFiles) === TRUE) {
$uploadCache[$formFieldName] = $failedMessages;
return $failedMessages;
}
}
//
// Process files
// -------------
// At this point we have a list of uploaded files that all exist
// on the server and have acceptable file name lengths and extensions.
// We can now try to create File objects.
//
// Get the file system service.
$fileSystem = \Drupal::service('file_system');
// Get the MIME type service.
$mimeGuesser = \Drupal::service('file.mime_type.guesser');
// Loop through the files and create initial File objects. Move
// each file into the Drupal temporary directory.
$fileObjects = [];
foreach ($goodFiles as $index => $fileInfo) {
$filename = $fileInfo->getClientOriginalName();
$filemime = $mimeGuesser->guess($filename);
$filesize = $fileInfo->getSize();
$uploadPath = $fileInfo->getRealPath();
$tempUri = $fileSystem->getDestinationFilename(
'temporary://' . $filename,
FileSystemInterface::EXISTS_RENAME);
// Move file to Drupal temp directory.
//
// The file needs to be moved out of PHP's temporary directory
// into Drupal's temporary directory.
//
// PHP's move_uploaded_file() can do this, but it doesn't
// handle Drupal streams. So use Drupal's file system for this.
//
// Let the URI get changed to avoid collisions. This does not
// affect the user-visible file name.
if ($fileSystem->moveUploadedFile($uploadPath, $tempUri) === FALSE) {
// Failure likely means a site problem, such as a bad
// file system, full disk, etc. Try to keep going with
// the rest of the files.
$drupalTemp = file_directory_temp();
$failedMessages[$index] = (string) t(
"Website configuration error.\nThe file '@file' could not be added to the folder because the website encountered a site configuration error about a missing Drupal temporary directory. Please report this to the site administrator.",
[
'@file' => $filename,
]);
ManageLog::critical(
"Local file system: File upload failed because local Drupal temporary directory '@dir' is missing!",
[
'@dir' => $drupalTemp,
]);
continue;
}
// Set permissions. Make the file accessible to the web server, etc.
FileUtilities::chmod($tempUri);
// Create a File object. Make it owned by the current user. Give
// it the temp URI, file name, MIME type, etc. A status of 0 means
// the file is temporary still.
$file = File::create([
'uid' => $requestingUid,
'uri' => $tempUri,
'filename' => $filename,
'filemime' => $filemime,
'filesize' => $filesize,
'status' => 0,
'source' => $formFieldName,
]);
// Save! Saving the File object assigns it a unique entity ID.
$file->save();
$fileObjects[$index] = $file;
}
unset($goodFiles);
// If there are no good files left, return the errors.
if (empty($fileObjects) === TRUE) {
$uploadCache[$formFieldName] = $failedMessages;
return $failedMessages;
}
//
// Move into local directory
// -------------------------
// This module manages files within a directory tree built from
// the entity ID. This entity ID is not known until after the
// File object is done. So we now need another pass through the
// File objects to use their entity IDs and move the files to their
// final destinations.
//
// Along the way we also mark the file as permanent and attach it
// to this folder.
$movedObjects = [];
foreach ($fileObjects as $index => $file) {
// Create the final destination URI. This is the URI that uses
// the entity ID.
$finalUri = ManageFileSystem::getFileUri($file);
// Move it there. The directory will be created automatically
// if needed.
$newFile = file_move(
$file,
$finalUri,
FileSystemInterface::EXISTS_REPLACE);
if ($newFile === FALSE) {
// Unfortunately, file_move() just returns FALSE on an error
// and provides no further details to us on the problem.
// Instead, it writes a message to the log file and/or
// to the output page, which is not useful to us or
// meaningful to the user.
//
// Looking at the source code, the following types of
// errors could occur:
// - The source file doesn't exist
// - The destination directory doesn't exist
// - The destination directory can't be written
// - The destination filename is in use
// - The source and destination are the same
// - Some other error occurred (probably a system error)
//
// Since the directory and file names are under our control
// and are valid, the only errors that can occur here are
// catastrophic, such as:
// - File deleted out from under us
// - File system changed out from under Drupal
// - File system full, offline, hard disk dead, etc.
//
// For any of these, it is unlikely we can continue with
// anything.
$failedMessages[$index] = (string) t(
'The file "@file" could not be added to the folder because the website encountered a system failure.',
['@file' => $filename]);
$file->delete();
}
else {
// On success, $file has already been deleted and we now
// need to use $newFile.
$movedObjects[$index] = $newFile;
// Mark it permanent.
$newFile->setPermanent();
$newFile->save();
}
}
// And reduce the good files list to the ones that got moved.
$fileObjects = $movedObjects;
unset($movedObjects);
// If there are no good files left, return the errors.
if (empty($fileObjects) === TRUE) {
$uploadCache[$formFieldName] = $failedMessages;
return $failedMessages;
}
//
// Add to folder
// -------------
// At this point, $fileObjects contains a list of fully-created
// File objects for files that have already been moved into their
// correct locations. Add them to the folder!
try {
// Add them. Watch for bad names or name collisions and rename
// if needed. Don't bother checking if the files are already in
// the folder since we know they aren't. Do lock.
self::addFilesInternal(
$parent,
$fileObjects,
$requestingUid,
TRUE,
$allowRename,
FALSE);
}
catch (\Exception $e) {
// The add can fail if:
// - A file name is illegal (but we already checked).
//
// - A unique name could not be created because $allowRename was FALSE.
//
// - A lock could not be acquired.
//
// On any failure, none of the files have been added.
foreach ($fileObjects as $index => $file) {
$filename = $file->getFilename();
$failedMessages[$index] = (string) t(
'The file "@file" could not be added to the folder because the folder is locked for exclusive use by another user.',
['@file' => $filename]);
$file->delete();
}
$uploadCache[$formFieldName] = $failedMessages;
return $failedMessages;
}
//
// Cache and return
// ----------------
// $fileObjects contains the new File objects, indexed by the original
// upload indexes.
//
// $failedMessages contains text messages for all failures, indexed
// by the original upload indexes.
//
// Merge these. We cannot use PHP's array_merge() because it will
// renumber the entries.
$result = $fileObjects;
foreach ($failedMessages as $index => $message) {
$result[$index] = $message;
}
$uploadCache[$formFieldName] = $result;
return $result;
}
/*---------------------------------------------------------------------
*
* Redirect input to a file.
*
*---------------------------------------------------------------------*/
/**
* Reads a PHP input stream into a new temporary file.
*
* PHP's input stream is opened and read to acquire incoming data
* to route into a new Drupal temporary file. The URI of the new
* file is returned.
*
* @return string
* The URI of the new temporary file.
*
* @throws \Drupal\foldershare\Entity\Exception\SystemException
* Throws an exception if one of the following occurs:
* - The input stream cannot be read.
* - A temporary file cannot be created.
* - A temporary file cannot be written.
*/
private static function inputDataToFile() {
//
// Open stream
// -----------
// Use the "php" stream to access incoming data appended to the
// current HTTP request. The stream is opened for reading in
// binary (the binary part is required for Windows).
$stream = fopen('php://input', 'rb');
if ($stream === FALSE) {
throw new SystemException(t(
"System error. An input stream cannot be opened or read."));
}
//
// Create temp file
// ----------------
// Use the Drupal "temproary" stream to create a temporary file and
// open it for writing in binary.
$tempUri = FileUtilities::tempnam('temporary://', 'file');
$temp = fopen($tempUri, 'wb');
if ($temp === FALSE) {
fclose($stream);
throw new SystemException(t(
"System error. A temporary file at '@path' could not be created.\nThere may be a problem with directories or permissions. Please report this to the site administrator.",
[
'@path' => $tempUri,
]));
}
//
// Copy stream to file
// -------------------
// Loop through the input stream until EOF, copying data into the
// temporary file.
while (feof($stream) === FALSE) {
$data = fread($stream, 8192);
if ($data === FALSE) {
// We aren't at EOF, but the read failed. Something has gone wrong.
fclose($stream);
fclose($temp);
throw new SystemException(t(
"System error. An input stream cannot be opened or read."));
}
if (fwrite($temp, $data) === FALSE) {
fclose($stream);
fclose($temp);
throw new SystemException(t(
"System error. A file at '@path' could not be written.\nThere may be a problem with permissions. Please report this to the site administrator.",
[
'@path' => $temp,
]));
}
}
//
// Clean up
// --------
// Close the stream and file.
fclose($stream);
fclose($temp);
return $tempUri;
}
/**
* Adds a PHP input stream file into the root list.
*
* When a file is uploaded via an HTTP post handled by a web services
* "REST" resource, the file's data is available via the PHP input
* stream. This method reads that stream, creates a file, and adds
* that file to this folder with the given name.
*
* <B>Post-operation hooks</B>
* This method calls the "hook_foldershare_post_operation_add_files" hook.
*
* <B>Process locks</B>
* The user's root list is locked for exclusive use for the duration of
* this operation.
*
* <B>Activity log</B>
* This method posts a log message after the file has been added.
*
* @param string $filename
* The name for the new file.
* @param bool $allowRename
* (optional, default = TRUE) When TRUE, if $filename collides with the
* name of an existing entity, the name is modified by adding a number on
* the end so that it doesn't collide. When FALSE, a file name collision
* throws an exception.
*
* @return \Drupal\foldershare\FolderShareInterface
* Returns the newly added FolderShare entity wrapping the file.
*
* @throws \Drupal\foldershare\Entity\Exception\LockException
* Throws an exception if an access lock could not be acquired.
*
* @see ::addFile()
* @see ::addFiles()
* @see ::addUploadFiles()
*/
public static function addInputFileToRoot(
string $filename,
bool $allowRename = TRUE) {
//
// Validate
// --------
// There must be a file name.
if (empty($filename) === TRUE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'The name "@name" cannot be used.',
[
'@name' => $filename,
]),
t('The file could not be added created because it\'s name is too long or it uses one of the prohibited ":", "/", or "\\" punctuation marks.')));
}
//
// Read data into file
// -------------------
// Read PHP input into a local temporary file. This throws an exception
// if the input stream cannot be read or a temporary file created.
$tempUri = self::inputDataToFile();
//
// Create a File entity
// --------------------
// Create a File entity that wraps the given file. This moves the file
// into the module's directory tree. An exception is thrown the file
// cannot be moved.
$file = self::createFileEntityFromLocalFile($tempUri, $filename);
//
// Add file to root list
// ---------------------
// Add the File entity to the root list, adjusting the file name if
// needed.
try {
return self::addFileToRoot($file, $allowRename);
}
catch (\Exception $e) {
// Delete the file.
$file->delete();
throw $e;
}
}
/**
* {@inheritdoc}
*/
public function addInputFile(string $filename, bool $allowRename = TRUE) {
//
// Validate
// --------
// There must be a file name.
if (empty($filename) === TRUE) {
throw new ValidationException(FormatUtilities::createFormattedMessage(
t(
'The name "@name" cannot be used.',
[
'@name' => $filename,
]),
t('The file could not be added to the folder because it\'s name is too long or it uses one of the prohibited ":", "/", or "\\" punctuation marks.')));
}
//
// Read data into file
// -------------------
// Read PHP input into a local temporary file. This throws an exception
// if the input stream cannot be read or a temporary file created.
$tempUri = self::inputDataToFile();
//
// Create a File entity
// --------------------
// Create a File entity that wraps the given file. This moves the file
// into the module's directory tree. An exception is thrown the file
// cannot be moved.
$file = self::createFileEntityFromLocalFile($tempUri, $filename);
//
// Add file to folder
// ------------------
// Add the File entity to this folder, adjusting the file name if
// needed.
try {
return $this->addFile($file, $allowRename);
}
catch (\Exception $e) {
// Delete the file.
$file->delete();
throw $e;
}
}
}
