photos-6.0.x-dev/src/Form/PhotosUploadForm.php
src/Form/PhotosUploadForm.php
<?php
namespace Drupal\photos\Form;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Connection;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystem;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Image\ImageFactory;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StreamWrapper\PrivateStream;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\file\FileInterface;
use Drupal\node\NodeInterface;
use Drupal\photos\PhotosUploadInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Defines a form to upload photos to this site.
*/
class PhotosUploadForm extends FormBase {
use DependencySerializationTrait;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystem
*/
protected $fileSystem;
/**
* The image factory.
*
* @var \Drupal\Core\Image\ImageFactory
*/
protected $imageFactory;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* The messenger.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The current request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* The photos upload handler.
*
* @var \Drupal\photos\PhotosUploadInterface
*/
protected $photosUpload;
/**
* Constructor.
*
* @param \Drupal\Core\Database\Connection $connection
* The database connection.
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\File\FileSystem $file_system
* The file system service.
* @param \Drupal\Core\Image\ImageFactory $image_factory
* The image factory.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The current request stack.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Drupal\photos\PhotosUploadInterface $photos_upload
* The photos upload handler.
*/
public function __construct(Connection $connection, EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_manager, FileSystem $file_system, ImageFactory $image_factory, MessengerInterface $messenger, ModuleHandlerInterface $module_handler, RequestStack $request_stack, RouteMatchInterface $route_match, PhotosUploadInterface $photos_upload) {
$this->connection = $connection;
$this->entityFieldManager = $entity_field_manager;
$this->entityTypeManager = $entity_manager;
$this->fileSystem = $file_system;
$this->imageFactory = $image_factory;
$this->logger = $this->getLogger('photos');
$this->messenger = $messenger;
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
$this->routeMatch = $route_match;
$this->photosUpload = $photos_upload;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('database'),
$container->get('entity_field.manager'),
$container->get('entity_type.manager'),
$container->get('file_system'),
$container->get('image.factory'),
$container->get('messenger'),
$container->get('module_handler'),
$container->get('request_stack'),
$container->get('current_route_match'),
$container->get('photos.upload')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'photos_upload';
}
/**
* A custom access check.
*
* @param \Drupal\node\NodeInterface $node
* The album node entity.
*
* @return \Drupal\Core\Access\AccessResult
* The access result.
*/
public function access(NodeInterface $node) {
// Check if user can edit this album.
if ($node->getType() == 'photos' && $node->access('update')) {
return AccessResult::allowed();
}
else {
return AccessResult::forbidden();
}
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ?NodeInterface $node = NULL) {
$config = $this->config('photos.settings');
$nid = $node->id();
$form['#attributes']['enctype'] = 'multipart/form-data';
$form['new'] = [
'#title' => $this->t('Image upload'),
'#weight' => -4,
'#type' => 'details',
'#open' => TRUE,
];
$allow_zip = (($config->get('photos_upzip')) ? ' zip' : '');
// Check if plupload is installed.
if ($config->get('photos_plupload_status')) {
$form['new']['plupload'] = [
'#type' => 'plupload',
'#title' => $this->t('Upload photos'),
'#description' => $this->t('Upload multiple images.'),
'#autoupload' => TRUE,
'#submit_element' => '#edit-submit',
'#upload_validators' => [
'FileExtension' => [
'extensions' => 'jpg jpeg gif png' . $allow_zip,
],
],
'#plupload_settings' => [
'chunk_size' => '1mb',
],
];
}
else {
// Manual upload form.
$form['new']['#description'] = $this->t('Allowed types: jpg gif png jpeg@zip', ['@zip' => $allow_zip]);
$album_photo_limit = $config->get('album_photo_limit');
$classic_field_count = $config->get('photos_num');
if ($album_photo_limit && ($classic_field_count > $album_photo_limit)) {
$classic_field_count = $album_photo_limit;
}
for ($i = 0; $i < $classic_field_count; ++$i) {
$form['new']['images_' . $i] = [
'#type' => 'file',
];
$form['new']['title_' . $i] = [
'#type' => 'textfield',
'#title' => $this->t('Image title'),
];
$form['new']['des_' . $i] = [
'#type' => 'textarea',
'#title' => $this->t('Image description'),
'#cols' => 40,
'#rows' => 3,
];
}
}
if ($this->moduleHandler->moduleExists('media_library_form_element')) {
// Check photos default multi-upload field.
$uploadField = $this->config('photos.settings')->get('multi_upload_default_field');
$uploadFieldParts = explode(':', $uploadField);
$field = $uploadFieldParts[0] ?? 'field_image';
$allBundleFields = $this->entityFieldManager->getFieldDefinitions('photos_image', 'photos_image');
if (isset($allBundleFields[$field])) {
$fieldType = $allBundleFields[$field]->getType();
// Check if media field.
if ($fieldType == 'entity_reference') {
$mediaField = $uploadFieldParts[1] ?? '';
$mediaBundle = $uploadFieldParts[2] ?? '';
if ($mediaField && $mediaBundle) {
$form['new']['media_images'] = [
'#type' => 'media_library',
'#allowed_bundles' => [$mediaBundle],
'#title' => $this->t('Select media images'),
'#default_value' => NULL,
'#description' => $this->t('Select media images to add to this album.'),
'#cardinality' => -1,
];
}
}
}
}
// @todo album_id is redundant unless albums become own entity.
// - maybe make album_id serial and add nid... or entity_id.
$form['new']['album_id'] = [
'#type' => 'value',
'#value' => $nid,
];
$form['new']['nid'] = [
'#type' => 'value',
'#value' => $nid,
];
$form['new']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Confirm upload'),
'#weight' => 10,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
$config = $this->config('photos.settings');
$album_id = $form_state->getValue('album_id');
$album_photo_limit = $config->get('album_photo_limit');
$photo_count = $this->connection->query("SELECT count FROM {photos_album} WHERE album_id = :album_id", [
':album_id' => $album_id,
])->fetchField();
if ($album_photo_limit && ($photo_count >= $album_photo_limit)) {
$form_state->setErrorByName('new', $this->t('Maximum number of photos reached for this album.'));
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$batch = FALSE;
$user = $this->currentUser();
$config = $this->config('photos.settings');
$album_photo_limit = $config->get('album_photo_limit') ?? 0;
$count = 0;
$nid = $form_state->getValue('nid');
$album_id = $form_state->getValue('album_id');
$photo_count = $this->connection->query("SELECT count FROM {photos_album} WHERE album_id = :album_id", [
':album_id' => $album_id,
])->fetchField() ?? 0;
// If photos_access is enabled check viewid.
$scheme = 'default';
if ($this->moduleHandler->moduleExists('photos_access')) {
$node = $this->entityTypeManager->getStorage('node')->load($nid);
if (isset($node->photos_privacy) && isset($node->photos_privacy['viewid'])) {
$album_viewid = $node->photos_privacy['viewid'];
if ($album_viewid > 0) {
// Check for private file path.
if (PrivateStream::basePath()) {
$scheme = 'private';
}
else {
// Set warning message.
$this->messenger->addWarning($this->t('Warning: image
files can still be accessed by visiting the direct URL. For
better security, ask your website admin to setup a private file
path.'));
}
}
}
}
// Check if plupload is enabled.
// @todo check for plupload library?
if ($config->get('photos_plupload_status')) {
$plupload_files = $form_state->getValue('plupload');
$plupload_chunks = array_chunk($plupload_files, 10);
$total_count = count($plupload_files);
// Prep batch operations.
$operations = [];
foreach ($plupload_chunks as $files) {
$batch_operation_params = [
$files,
$nid,
$album_id,
$user->id(),
$scheme,
$total_count,
$album_photo_limit,
$photo_count,
$config->get('photos_upzip'),
];
$operations[] = [
['\Drupal\photos\Form\PhotosUploadForm', 'pluploadSubmitBatch'], $batch_operation_params,
];
}
$batch = [
'title' => $this->t('Processing images'),
'operations' => $operations,
'finished' => ['\Drupal\photos\Form\PhotosUploadForm', 'batchFinished'],
'progress_message' => '',
];
// Start batch.
batch_set($batch);
$batch = TRUE;
}
else {
// Manual upload form.
$uploaded_files = $this->requestStack->getCurrentRequest()->files->get('files');
$photos_num = $config->get('photos_num');
for ($i = 0; $i < $photos_num; ++$i) {
if (!empty($uploaded_files['images_' . $i])) {
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $uploaded_file */
$uploaded_file = $uploaded_files['images_' . $i];
if ($album_photo_limit && ($photo_count >= $album_photo_limit)) {
$this->messenger()->addWarning($this->t('Maximum number of photos reached for this album.'));
break;
}
$ext = $uploaded_file->getClientOriginalExtension();
if ($ext != 'zip' && $ext != 'ZIP') {
// Prepare directory.
$photosPath = $this->photosUpload->path($scheme);
$photosName = $uploaded_file->getClientOriginalName();
$file_uri = $this->fileSystem
->getDestinationFilename($photosPath . '/' . $photosName, FileExists::Rename);
$file_real_path = $uploaded_file->getRealPath();
if ($this->fileSystem->move($file_real_path, $file_uri)) {
$path_parts = pathinfo($file_uri);
$image = $this->imageFactory->get($file_uri);
// Check if image is valid.
if ($image->isValid() && isset($path_parts['extension']) && $path_parts['extension'] && $image->getWidth()) {
// Create a file entity.
/** @var \Drupal\file\FileInterface $file */
$file = $this->entityTypeManager->getStorage('file')->create([
'uri' => $file_uri,
'uid' => $user->id(),
'status' => FileInterface::STATUS_PERMANENT,
'album_id' => $form_state->getValue('album_id'),
'nid' => $form_state->getValue('nid'),
'filename' => $photosName,
'filesize' => $image->getFileSize(),
'filemime' => $image->getMimeType(),
'title' => $form_state->getValue('title_' . $i),
'des' => $form_state->getValue('des_' . $i),
]);
if ($file->save()) {
$this->photosUpload->saveImage($file);
$photo_count++;
}
$count++;
}
else {
$this->fileSystem->delete($file_uri);
$this->logger->notice('Wrong file type');
}
}
}
else {
// Zip upload from manual upload form.
if (!$config->get('photos_upzip')) {
$this->messenger->addError($this->t('Please update settings to allow zip uploads.'));
}
else {
$directory = $this->photosUpload->path();
$this->fileSystem->prepareDirectory($directory);
$zip_filename = $uploaded_file->getClientOriginalName();
$zip = $this->fileSystem
->getDestinationFilename($directory . '/' . trim(basename($zip_filename)), FileExists::Rename);
$file_real_path = $uploaded_file->getRealPath();
if ($this->fileSystem->move($file_real_path, $zip)) {
$params = [];
$params['album_id'] = $album_id;
$params['photo_count'] = $photo_count;
$params['album_photo_limit'] = $album_photo_limit;
$params['nid'] = $form_state->getValue('nid') ? $form_state->getValue('nid') : $form_state->getValue('album_id');
$params['description'] = $form_state->getValue('des_' . $i);
$params['title'] = $form_state->getValue('title_' . $i);
if (!$file_count = $this->photosUpload->unzip($zip, $params, $scheme)) {
// Upload failed.
}
else {
$count = $count + $file_count;
$photo_count = $photo_count + $file_count;
}
}
}
}
}
}
}
// Handle media field.
$selected_media = $form_state->getValue('media_images') ? explode(',', $form_state->getValue('media_images')) : [];
foreach ($selected_media as $media_id) {
if ($album_photo_limit && ($photo_count >= $album_photo_limit)) {
$this->messenger()->addWarning($this->t('Maximum number of photos reached for this album.'));
break;
}
// Save media to album.
$mediaSaved = $this->photosUpload->saveExistingMedia($media_id, $nid);
if ($mediaSaved) {
$photo_count++;
$count++;
}
}
if (!$batch) {
// Clear node and album page cache.
Cache::invalidateTags(['node:' . $nid, 'photos:album:' . $nid]);
$message = $this->formatPlural($count, '1 image uploaded.', '@count images uploaded.');
$this->messenger->addMessage($message);
}
}
/**
* Process plupload images.
*
* @param array $plupload_files
* An array of files uploaded with the plupload widget.
* @param int $nid
* The album node ID.
* @param int $album_id
* The album ID (same as nid for now).
* @param int $uid
* The user uploading the files.
* @param string $scheme
* The file scheme (private or public for example).
* @param int $total_count
* The total number of plupload files being processed.
* @param int $album_photo_limit
* The maximum number of photos per album setting.
* @param int $photo_count
* The number of photos already in this photo album.
* @param bool $zip_allowed
* If zip archives are allowed.
* @param array|\ArrayAccess $context
* The batch context array, passed by reference.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public static function pluploadSubmitBatch(array $plupload_files, int $nid, int $album_id, int $uid, string $scheme, int $total_count, int $album_photo_limit, int $photo_count, bool $zip_allowed, &$context) {
if (!isset($context['results']['count'])) {
$context['results']['progress'] = 0;
$context['results']['max'] = $total_count;
// Pass album nid to batch finished.
$context['results']['nid'] = $nid;
$context['results']['messages'] = [
'warning' => [],
'error' => [],
];
// Photos saved during this batch run.
$context['results']['count'] = 0;
// The album total photo count.
$context['results']['photo_count'] = $photo_count;
}
/** @var \Drupal\photos\PhotosUploadInterface $photos_upload */
$photos_upload = \Drupal::service('photos.upload');
/** @var \Drupal\file\FileStorageInterface $file_storage */
$file_storage = \Drupal::entityTypeManager()->getStorage('file');
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
/** @var \Drupal\Core\Image\ImageFactory $image_factory */
$image_factory = \Drupal::service('image.factory');
foreach ($plupload_files as $uploaded_file) {
if ($uploaded_file['status'] == 'done') {
if ($album_photo_limit && ($context['results']['photo_count'] >= $album_photo_limit)) {
$context['results']['messages']['warning'][] = t('Maximum number of photos reached for this album.');
break;
}
// Check for zip files.
$ext = mb_substr($uploaded_file['name'], -3);
if ($ext != 'zip' && $ext != 'ZIP') {
// Prepare directory.
// @todo move path to after entity is created or move again later if needed.
// @todo generate temp path before tokens are available.
$photosPath = $photos_upload->path($scheme);
$photosName = $uploaded_file['name'];
$file_uri = $file_system->getDestinationFilename($photosPath . '/' . $photosName, FileExists::Rename);
if ($file_system->move($uploaded_file['tmppath'], $file_uri)) {
$path_parts = pathinfo($file_uri);
$image = $image_factory->get($file_uri);
if (isset($path_parts['extension']) && $path_parts['extension'] && $image->getWidth()) {
// Create a file entity.
/** @var \Drupal\file\FileInterface $file */
$file = $file_storage->create([
'uri' => $file_uri,
'uid' => $uid,
'status' => FileInterface::STATUS_PERMANENT,
'album_id' => $album_id,
'nid' => $nid,
'filename' => $photosName,
'filesize' => $image->getFileSize(),
'filemime' => $image->getMimeType(),
]);
if ($file->save()) {
$context['results']['photo_count']++;
$photos_upload->saveImage($file);
}
$context['results']['count']++;
}
else {
$file_system->delete($file_uri);
\Drupal::logger('photos')->notice('Wrong file type');
}
}
else {
\Drupal::logger('photos')->notice('Upload error. Could not move temp file.');
}
}
else {
if (!$zip_allowed) {
$context['results']['messages']['error'][] = t('Please set Album photos to open zip uploads.');
}
$directory = $photos_upload->path();
$file_system->prepareDirectory($directory);
$zip = $file_system->getDestinationFilename($directory . '/' . $uploaded_file['name'], FileExists::Rename);
if ($file_system->move($uploaded_file['tmppath'], $zip)) {
$params = [];
$params['album_id'] = $album_id;
$params['photo_count'] = $context['results']['photo_count'];
$params['album_photo_limit'] = $album_photo_limit;
$params['nid'] = $nid;
$params['title'] = $uploaded_file['name'];
$params['description'] = '';
// Unzip it.
if (!$file_count = $photos_upload->unzip($zip, $params, $scheme)) {
$context['results']['messages']['error'][] = t('Zip upload failed.');
}
else {
// Update image upload count.
$context['results']['count'] = $context['results']['count'] + $file_count;
$context['results']['photo_count'] = $context['results']['photo_count'] + $file_count;
}
}
}
}
else {
$context['results']['messages']['error'][] = t('Error uploading some photos.');
}
$context['results']['progress']++;
}
$context['message'] = new TranslatableMarkup('Completed @percentage% (@current of @total).', [
'@percentage' => round(100 * $context['results']['progress'] / $context['results']['max']),
'@current' => $context['results']['progress'],
'@total' => $context['results']['max'],
]);
}
/**
* Implements callback_batch_finished().
*
* Batch results.
*/
public static function batchFinished($success, $results, $operations) {
$nid = $results['nid'];
// Clear node and album page cache.
Cache::invalidateTags(['node:' . $nid, 'photos:album:' . $nid]);
// Add any error messages.
if (isset($results['messages']['error'])) {
foreach ($results['messages']['error'] as $message) {
\Drupal::messenger()->addError($message);
}
}
// Add any warning messages.
if (isset($results['messages']['warning'])) {
foreach ($results['messages']['warning'] as $message) {
\Drupal::messenger()->addWarning($message);
}
}
$message = \Drupal::translation()->formatPlural($results['count'], '1 image uploaded.', '@count images uploaded.');
\Drupal::messenger()->addMessage($message);
}
}
