<?php namespace Drupal\islandora\MediaSource; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\EntityReferenceFieldItemListInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\file\FileInterface; use Drupal\islandora\IslandoraUtils; use Drupal\media\MediaInterface; use Drupal\media\MediaTypeInterface; use Drupal\node\NodeInterface; use Drupal\taxonomy\TermInterface; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Utility functions for working with source files for Media. */ class MediaSourceService { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * Current user. * * @var \Drupal\Core\Session\AccountInterface */ protected $account; /** * Language manager. * * @var \Drupal\Core\Language\LanguageManagerInterface */ protected $languageManager; /** * File system service. * * @var \Drupal\Core\File\FileSystemInterface */ protected $fileSystem; /** * Islandora Utility service. * * @var \Drupal\islandora\IslandoraUtils */ protected $islandoraUtils; /** * Constructor. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. * @param \Drupal\Core\Session\AccountInterface $account * The current user. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager * Language manager. * @param \Drupal\Core\File\FileSystemInterface $file_system * File system service. * @param \Drupal\islandora\IslandoraUtils $islandora_utils * Utility service. */ public function __construct( EntityTypeManagerInterface $entity_type_manager, AccountInterface $account, LanguageManagerInterface $language_manager, FileSystemInterface $file_system, IslandoraUtils $islandora_utils ) { $this->entityTypeManager = $entity_type_manager; $this->account = $account; $this->languageManager = $language_manager; $this->fileSystem = $file_system; $this->islandoraUtils = $islandora_utils; } /** * Gets the name of a source field for a Media. * * @param string $media_type * Media bundle whose source field you are searching for. * * @return string|null * Field name if it exists in configuration, else NULL. */ public function getSourceFieldName($media_type) { $bundle = $this->entityTypeManager->getStorage('media_type')->load($media_type); if (!$bundle) { throw new NotFoundHttpException("Bundle $media_type does not exist"); } $type_configuration = $bundle->get('source_configuration'); if (!isset($type_configuration['source_field'])) { return NULL; } return $type_configuration['source_field']; } /** * Gets the value of a source field for a Media. * * @param \Drupal\media\MediaInterface $media * Media whose source field you are searching for. * * @return \Drupal\file\FileInterface|\Drupal\Core\Entity\EntityInterface|false|null * The first source entity if there is one, generally expected to be of * \Drupal\file\FileInterface. Boolean FALSE if there was no such entity. * NULL if the source field does not refer to Drupal entities (as in, the * field is not a \Drupal\Core\Field\EntityReferenceFieldItemListInterface * implementation). * * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException */ public function getSourceFile(MediaInterface $media) { // Get the source field for the media type. $source_field = $this->getSourceFieldName($media->bundle()); if (empty($source_field)) { throw new NotFoundHttpException("Source field not set for {$media->bundle()} media"); } // Get the file from the media. $source_list = $media->get($source_field); if ($source_list instanceof EntityReferenceFieldItemListInterface) { $files = $source_list->referencedEntities(); return reset($files); } return NULL; } /** * Updates a media's source field with the supplied resource. * * @param \Drupal\media\MediaInterface $media * The media to update. * @param resource $resource * New file contents as a resource. * @param string $mimetype * New mimetype of contents. * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function updateSourceField( MediaInterface $media, $resource, $mimetype ) { $source_field = $this->getSourceFieldName($media->bundle()); $file = $this->getSourceFile($media); // Update it. $this->updateFile($file, $resource, $mimetype); $file->save(); // Set fields provided by type plugin and mapped in bundle configuration // for the media. foreach ($media->bundle->entity->getFieldMap() as $source => $destination) { if ($media->hasField($destination) && $value = $media->getSource()->getMetadata($media, $source)) { $media->set($destination, $value); } // Ensure width and height are updated on File reference when it's an // image. Otherwise you run into scaling problems when updating images // with different sizes. if ($source == 'width' || $source == 'height') { $media->get($source_field)->first()->set($source, $value); } } $media->save(); } /** * Updates a File's binary contents on disk. * * @param \Drupal\file\FileInterface $file * File to update. * @param resource $resource * Stream holding the new contents. * @param string $mimetype * Mimetype of new contents. */ protected function updateFile(FileInterface $file, $resource, $mimetype = NULL) { $uri = $file->getFileUri(); $destination = fopen($uri, 'wb'); if (!$destination) { throw new HttpException(500, "File $uri could not be opened to write."); } $content_length = stream_copy_to_stream($resource, $destination); fclose($destination); if ($content_length === FALSE) { throw new HttpException(500, "Request body could not be copied to $uri"); } if ($content_length === 0) { // Clean up the newly created, empty file. unlink($uri); throw new HttpException(400, "No bytes were copied to $uri"); } if (!empty($mimetype)) { $file->setMimeType($mimetype); } // Flush the image cache for the image so thumbnails get regenerated. image_path_flush($uri); } /** * Creates a new Media using the provided resource, adding it to a Node. * * @param \Drupal\node\NodeInterface $node * The node to reference the newly created Media. * @param \Drupal\media\MediaTypeInterface $media_type * Media type for new media. * @param \Drupal\taxonomy\TermInterface $taxonomy_term * Term from the 'Behavior' vocabulary to give to new media. * @param resource $resource * New file contents as a resource. * @param string $mimetype * New mimetype of contents. * @param string $content_location * Drupal/PHP stream wrapper for where to upload the binary. * * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function putToNode( NodeInterface $node, MediaTypeInterface $media_type, TermInterface $taxonomy_term, $resource, $mimetype, $content_location ) { $existing = $this->islandoraUtils->getMediaReferencingNodeAndTerm($node, $taxonomy_term); if (!empty($existing)) { // Just update already existing media. $media = $this->entityTypeManager->getStorage('media')->load(reset($existing)); $this->updateSourceField( $media, $resource, $mimetype ); return FALSE; } else { // Otherwise, the media doesn't exist yet. // So make everything by hand. // Get the source field for the media type. $bundle = $media_type->id(); $source_field = $this->getSourceFieldName($bundle); if (empty($source_field)) { throw new NotFoundHttpException("Source field not set for $bundle media"); } // Construct the File. $file = $this->entityTypeManager->getStorage('file')->create([ 'uid' => $this->account->id(), 'uri' => $content_location, 'filename' => $this->fileSystem->basename($content_location), 'filemime' => $mimetype, ]); $file->setPermanent(); // Validate file extension. $source_field_config = $this->entityTypeManager->getStorage('field_config')->load("media.$bundle.$source_field"); $valid_extensions = $source_field_config->getSetting('file_extensions'); $errors = file_validate_extensions($file, $valid_extensions); if (!empty($errors)) { throw new BadRequestHttpException("Invalid file extension. Valid types are $valid_extensions"); } $directory = $this->fileSystem->dirname($content_location); // Check if the directory is writable. if (!is_writable(dirname($directory))) { $parent_directory = dirname($directory); $error_current_user = shell_exec('whoami'); $sanitized_current_user = str_replace(array("\n", "\r"), '', $error_current_user); throw new HttpException(500, "The user '$sanitized_current_user' does not have permissions to write to: '$parent_directory' . Error"); } if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { throw new HttpException(500, "The destination directory does not exist, could not be created, or is not writable: $directory"); } // Copy over the file content. $this->updateFile($file, $resource, $mimetype); $file->save(); // Construct the Media. $media_struct = [ 'bundle' => $bundle, 'uid' => $this->account->id(), 'name' => $file->getFilename(), 'langcode' => $this->languageManager->getDefaultLanguage()->getId(), "$source_field" => [ 'target_id' => $file->id(), ], IslandoraUtils::MEDIA_OF_FIELD => [ 'target_id' => $node->id(), ], IslandoraUtils::MEDIA_USAGE_FIELD => [ 'target_id' => $taxonomy_term->id(), ], ]; // Set alt text. if ($source_field_config->getSetting('alt_field') && $source_field_config->getSetting('alt_field_required')) { $media_struct[$source_field]['alt'] = $file->getFilename(); } $media = $this->entityTypeManager->getStorage('media')->create($media_struct); $media->save(); return $media; } } /** * Creates a new File using the provided resource, adding it to a Media. * * @param \Drupal\media\MediaInterface $media * The Media that will receive the new file. * @param string $destination_field * The field on the media where the file will go. * @param resource $resource * New file contents as a resource. * @param string $mimetype * New mimetype of contents. * @param string $content_location * Drupal/PHP stream wrapper for where to upload the binary. * * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ public function putToMedia( MediaInterface $media, $destination_field, $resource, $mimetype, $content_location ) { if ($media->hasField($destination_field)) { // Construct the File. $file = $this->entityTypeManager->getStorage('file')->create([ 'uid' => $this->account->id(), 'uri' => $content_location, 'filename' => $this->fileSystem->basename($content_location), 'filemime' => $mimetype, ]); $file->setPermanent(); // Validate file extension. $bundle = $media->bundle(); $destination_field_config = $this->entityTypeManager->getStorage('field_config')->load("media.$bundle.$destination_field"); $valid_extensions = $destination_field_config->getSetting('file_extensions'); $errors = file_validate_extensions($file, $valid_extensions); if (!empty($errors)) { throw new BadRequestHttpException("Invalid file extension. Valid types are $valid_extensions"); } $directory = $this->fileSystem->dirname($content_location); if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) { throw new HttpException(500, "The destination directory does not exist, could not be created, or is not writable"); } // Copy over the file content. $this->updateFile($file, $resource, $mimetype); $file->save(); // Update the media. $media->{$destination_field}->setValue([ 'target_id' => $file->id(), ]); $media->save(); } else { throw new BadRequestHttpException("Media does not have destination field $destination_field"); } } }