Browse Source
* Route to PUT file contents for a Media * README update * Updating permissions * Updating thumbnails * Touching up testspull/756/head
dannylamb
7 years ago
committed by
Natkeeran
8 changed files with 516 additions and 2 deletions
@ -0,0 +1,130 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora\Controller; |
||||
|
||||
use Drupal\Core\Controller\ControllerBase; |
||||
use Drupal\Core\Database\Connection; |
||||
use Drupal\media_entity\MediaInterface; |
||||
use Drupal\islandora\MediaSource\MediaSourceService; |
||||
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||
use Symfony\Component\HttpFoundation\Request; |
||||
use Symfony\Component\HttpFoundation\Response; |
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; |
||||
use Symfony\Component\HttpKernel\Exception\HttpException; |
||||
|
||||
/** |
||||
* Class MediaSourceController. |
||||
* |
||||
* @package Drupal\islandora\Controller |
||||
*/ |
||||
class MediaSourceController extends ControllerBase { |
||||
|
||||
/** |
||||
* Service for business logic. |
||||
* |
||||
* @var \Drupal\islandora\MediaSource\MediaSourceService |
||||
*/ |
||||
protected $service; |
||||
|
||||
/** |
||||
* Database connection. |
||||
* |
||||
* @var \Drupal\Core\Database\Connection |
||||
*/ |
||||
protected $database; |
||||
|
||||
/** |
||||
* MediaSourceController constructor. |
||||
* |
||||
* @param \Drupal\islandora\MediaSource\MediaSourceService $service |
||||
* Service for business logic. |
||||
* @param \Drupal\Core\Database\Connection $database |
||||
* Database connection. |
||||
*/ |
||||
public function __construct( |
||||
MediaSourceService $service, |
||||
Connection $database |
||||
) { |
||||
$this->service = $service; |
||||
$this->database = $database; |
||||
} |
||||
|
||||
/** |
||||
* Controller's create method for dependecy injection. |
||||
* |
||||
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container |
||||
* The App Container. |
||||
* |
||||
* @return \Drupal\islandora\Controller\MediaSourceController |
||||
* Controller instance. |
||||
*/ |
||||
public static function create(ContainerInterface $container) { |
||||
return new static( |
||||
$container->get('islandora.media_source_service'), |
||||
$container->get('database') |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Updates a source file for a Media. |
||||
* |
||||
* @param \Drupal\media_entity\MediaInterface $media |
||||
* The media whose source file you want to update. |
||||
* @param \Symfony\Component\HttpFoundation\Request $request |
||||
* The request object. |
||||
* |
||||
* @return \Symfony\Component\HttpFoundation\Response |
||||
* 204 on success. |
||||
* |
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException |
||||
*/ |
||||
public function put(MediaInterface $media, Request $request) { |
||||
// Since we update both the Media and its File, do this in a transaction. |
||||
$transaction = $this->database->startTransaction(); |
||||
|
||||
try { |
||||
$content_type = $request->headers->get('Content-Type', ""); |
||||
|
||||
if (empty($content_type)) { |
||||
throw new BadRequestHttpException("Missing Content-Type header"); |
||||
} |
||||
|
||||
$content_length = $request->headers->get('Content-Length', 0); |
||||
|
||||
if ($content_length <= 0) { |
||||
throw new BadRequestHttpException("Missing Content-Length"); |
||||
} |
||||
|
||||
$content_disposition = $request->headers->get('Content-Disposition', ""); |
||||
|
||||
if (empty($content_disposition)) { |
||||
throw new BadRequestHttpException("Missing Content-Disposition header"); |
||||
} |
||||
|
||||
$matches = []; |
||||
if (!preg_match('/attachment; filename="(.*)"/', $content_disposition, $matches)) { |
||||
throw new BadRequestHttpException("Malformed Content-Disposition header"); |
||||
} |
||||
$filename = $matches[1]; |
||||
|
||||
$this->service->updateSourceField( |
||||
$media, |
||||
$request->getContent(TRUE), |
||||
$content_type, |
||||
$content_length, |
||||
$filename |
||||
); |
||||
|
||||
return new Response("", 204); |
||||
} |
||||
catch (HttpException $e) { |
||||
$transaction->rollBack(); |
||||
throw $e; |
||||
} |
||||
catch (\Exception $e) { |
||||
$transaction->rollBack(); |
||||
throw new HttpException(500, $e->getMessage()); |
||||
} |
||||
} |
||||
|
||||
} |
@ -0,0 +1,196 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\islandora\MediaSource; |
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface; |
||||
use Drupal\Core\Entity\EntityTypeManager; |
||||
use Drupal\Core\StreamWrapper\StreamWrapperManager; |
||||
use Drupal\media_entity\MediaInterface; |
||||
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 { |
||||
|
||||
/** |
||||
* Media bundle storage. |
||||
* |
||||
* @var \Drupal\Core\Entity\EntityStorageInterface |
||||
*/ |
||||
protected $mediaBundleStorage; |
||||
|
||||
/** |
||||
* Field config storage. |
||||
* |
||||
* @var \Drupal\Core\Entity\EntityStorageInterface |
||||
*/ |
||||
protected $fieldConfigStorage; |
||||
|
||||
/** |
||||
* Stream wrapper manager. |
||||
* |
||||
* @var \Drupal\Core\StreamWrapper\StreamWrapperManager |
||||
*/ |
||||
protected $streamWrapperManager; |
||||
|
||||
/** |
||||
* Constructor. |
||||
* |
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $media_bundle_storage |
||||
* Media bundle storage. |
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $field_config_storage |
||||
* Field config storage. |
||||
* @param \Drupal\Core\StreamWrapper\StreamWrapperManager $stream_wrapper_manager |
||||
* Stream wrapper manager. |
||||
*/ |
||||
public function __construct( |
||||
EntityStorageInterface $media_bundle_storage, |
||||
EntityStorageInterface $field_config_storage, |
||||
StreamWrapperManager $stream_wrapper_manager |
||||
) { |
||||
$this->mediaBundleStorage = $media_bundle_storage; |
||||
$this->fieldConfigStorage = $field_config_storage; |
||||
$this->streamWrapperManager = $stream_wrapper_manager; |
||||
} |
||||
|
||||
/** |
||||
* Factory. |
||||
* |
||||
* @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager |
||||
* The entity type manager. |
||||
* @param \Drupal\Core\StreamWrapper\StreamWrapperManager $stream_wrapper_manager |
||||
* Stream wrapper manager. |
||||
* |
||||
* @return \Drupal\islandora\MediaSource\MediaSourceService |
||||
* MediaSourceService instance. |
||||
*/ |
||||
public static function create( |
||||
EntityTypeManager $entity_type_manager, |
||||
StreamWrapperManager $stream_wrapper_manager |
||||
) { |
||||
return new static( |
||||
$entity_type_manager->getStorage('media_bundle'), |
||||
$entity_type_manager->getStorage('field_config'), |
||||
$stream_wrapper_manager |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Gets the name of a source field for a Media. |
||||
* |
||||
* @param string $media_bundle |
||||
* 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_bundle) { |
||||
$bundle = $this->mediaBundleStorage->load($media_bundle); |
||||
$type_configuration = $bundle->getTypeConfiguration(); |
||||
|
||||
if (!isset($type_configuration['source_field'])) { |
||||
return NULL; |
||||
} |
||||
|
||||
return $type_configuration['source_field']; |
||||
} |
||||
|
||||
/** |
||||
* Gets a list of valid file extensions for a field. |
||||
* |
||||
* @param string $entity_type |
||||
* Entity type (node, media, etc...). |
||||
* @param string $bundle |
||||
* Bundle the field belongs to. |
||||
* @param string $field |
||||
* The field whose valid extensions you're looking for. |
||||
* |
||||
* @return string |
||||
* Space delimited string containing valid extensions. |
||||
*/ |
||||
public function getFileFieldExtensions($entity_type, $bundle, $field) { |
||||
$field_config = $this->fieldConfigStorage->load("$entity_type.$bundle.$field"); |
||||
if (!$field_config) { |
||||
return ""; |
||||
} |
||||
return $field_config->getSetting('file_extensions'); |
||||
} |
||||
|
||||
/** |
||||
* Updates a media's source field with the supplied resource. |
||||
* |
||||
* @param \Drupal\media_entity\MediaInterface $media |
||||
* The media to update. |
||||
* @param resource $resource |
||||
* New file contents as a resource. |
||||
* @param string $mimetype |
||||
* New mimetype of contents. |
||||
* @param string $content_length |
||||
* New size of contents. |
||||
* @param string $filename |
||||
* New filename for contents. |
||||
* |
||||
* @throws HttpException |
||||
*/ |
||||
public function updateSourceField( |
||||
MediaInterface $media, |
||||
$resource, |
||||
$mimetype, |
||||
$content_length, |
||||
$filename |
||||
) { |
||||
// 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. |
||||
$files = $media->get($source_field)->referencedEntities(); |
||||
$file = reset($files); |
||||
|
||||
// Set relevant fields on file. |
||||
$file->setMimeType($mimetype); |
||||
$file->setFilename($filename); |
||||
$file->setSize($content_length); |
||||
|
||||
// Validate file extension. |
||||
$entity_type = $media->getEntityTypeId(); |
||||
$bundle = $media->bundle(); |
||||
$valid_extensions = $this->getFileFieldExtensions($entity_type, $bundle, $source_field); |
||||
$errors = file_validate_extensions($file, $valid_extensions); |
||||
|
||||
if (!empty($errors)) { |
||||
throw new BadRequestHttpException("Invalid file extension. Valid types are :$valid_extensions"); |
||||
} |
||||
|
||||
// Copy the contents over using streams. |
||||
$uri = $file->getFileUri(); |
||||
$file_stream_wrapper = $this->streamWrapperManager->getViaUri($uri); |
||||
$path = ""; |
||||
$file_stream_wrapper->stream_open($uri, 'w', STREAM_REPORT_ERRORS, $path); |
||||
$file_stream = $file_stream_wrapper->stream_cast(STREAM_CAST_AS_STREAM); |
||||
if (stream_copy_to_stream($resource, $file_stream) === FALSE) { |
||||
throw new HttpException(500, "The file could not be copied into $uri"); |
||||
} |
||||
$file->save(); |
||||
|
||||
// Set fields provided by type plugin and mapped in bundle configuration |
||||
// for the media. |
||||
foreach ($media->bundle->entity->field_map as $source => $destination) { |
||||
if ($media->hasField($destination) && $value = $media->getType()->getField($media, $source)) { |
||||
$media->set($destination, $value); |
||||
} |
||||
} |
||||
|
||||
// Flush the image cache for the image so thumbnails get regenerated. |
||||
image_path_flush($uri); |
||||
|
||||
$media->save(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,158 @@
|
||||
<?php |
||||
|
||||
namespace Drupal\Tests\islandora\Functional; |
||||
|
||||
use Drupal\Core\Url; |
||||
|
||||
/** |
||||
* Tests updating Media source File with PUT. |
||||
* |
||||
* @group islandora |
||||
*/ |
||||
class MediaSourceControllerTest extends IslandoraFunctionalTestBase { |
||||
|
||||
/** |
||||
* {@inheritdoc} |
||||
*/ |
||||
public function setUp() { |
||||
parent::setUp(); |
||||
|
||||
$media_rest_resource = $this->container->get('entity_type.manager')->getStorage('rest_resource_config')->create([ |
||||
'id' => 'entity.media', |
||||
'granularity' => 'resource', |
||||
'configuration' => [ |
||||
'methods' => ['GET'], |
||||
'authentication' => ['basic_auth'], |
||||
'formats' => ['json'], |
||||
], |
||||
'status' => TRUE, |
||||
]); |
||||
$media_rest_resource->save(TRUE); |
||||
|
||||
$this->container->get('router.builder')->rebuildIfNeeded(); |
||||
} |
||||
|
||||
/** |
||||
* @covers \Drupal\islandora\Controller\MediaSourceController::put |
||||
*/ |
||||
public function testMediaSourceUpdate() { |
||||
$account = $this->drupalCreateUser([ |
||||
'view media', |
||||
'create media', |
||||
'update media', |
||||
]); |
||||
$this->drupalLogin($account); |
||||
|
||||
// Make a media and give it a png. |
||||
$url = $this->createThumbnailWithFile(); |
||||
|
||||
// Hack out the guzzle client. |
||||
$client = $this->getSession()->getDriver()->getClient()->getClient(); |
||||
|
||||
// GET the media to stash its original values for comparison later. |
||||
$options = [ |
||||
'auth' => [$account->getUsername(), $account->pass_raw], |
||||
'http_errors' => FALSE, |
||||
]; |
||||
$response = $client->request('GET', $url . '?_format=json', $options); |
||||
$media = json_decode($response->getBody(), TRUE); |
||||
|
||||
$mid = $media['mid'][0]['value']; |
||||
$original_mimetype = $media['field_mimetype'][0]['value']; |
||||
$original_width = $media['field_width'][0]['value']; |
||||
$original_height = $media['field_height'][0]['value']; |
||||
$original_image = file_get_contents($media['field_image'][0]['url']); |
||||
|
||||
$media_update_url = Url::fromRoute('islandora.media_source_update', ['media' => $mid]) |
||||
->setAbsolute() |
||||
->toString(); |
||||
|
||||
$image = file_get_contents(__DIR__ . '/../../static/test.jpeg'); |
||||
|
||||
// Update without Content-Type header should fail with 400. |
||||
$options = [ |
||||
'auth' => [$account->getUsername(), $account->pass_raw], |
||||
'http_errors' => FALSE, |
||||
'headers' => [ |
||||
'Content-Disposition' => 'attachment; filename="test.jpeg"', |
||||
], |
||||
'body' => $image, |
||||
]; |
||||
$response = $client->request('PUT', $media_update_url, $options); |
||||
$this->assertTrue($response->getStatusCode() == 400, "Expected 400, received {$response->getStatusCode()}"); |
||||
|
||||
// Update without Content-Disposition header should fail with 400. |
||||
$options = [ |
||||
'auth' => [$account->getUsername(), $account->pass_raw], |
||||
'http_errors' => FALSE, |
||||
'headers' => [ |
||||
'Content-Type' => 'image/jpeg', |
||||
], |
||||
'body' => $image, |
||||
]; |
||||
$response = $client->request('PUT', $media_update_url, $options); |
||||
$this->assertTrue($response->getStatusCode() == 400, "Expected 400, received {$response->getStatusCode()}"); |
||||
|
||||
// Update with malformed Content-Disposition header should fail with 400. |
||||
$options = [ |
||||
'auth' => [$account->getUsername(), $account->pass_raw], |
||||
'http_errors' => FALSE, |
||||
'headers' => [ |
||||
'Content-Type' => 'image/jpeg', |
||||
'Content-Disposition' => 'attachment; garbage="test.jpeg"', |
||||
], |
||||
'body' => $image, |
||||
]; |
||||
$response = $client->request('PUT', $media_update_url, $options); |
||||
$this->assertTrue($response->getStatusCode() == 400, "Expected 400, received {$response->getStatusCode()}"); |
||||
|
||||
// Update without body should fail with 400. |
||||
$options = [ |
||||
'auth' => [$account->getUsername(), $account->pass_raw], |
||||
'http_errors' => FALSE, |
||||
'headers' => [ |
||||
'Content-Type' => 'image/jpeg', |
||||
'Content-Disposition' => 'attachment; filename="test.jpeg"', |
||||
], |
||||
]; |
||||
$response = $client->request('PUT', $media_update_url, $options); |
||||
$this->assertTrue($response->getStatusCode() == 400, "Expected 400, received {$response->getStatusCode()}"); |
||||
|
||||
// Should be successful. |
||||
$options = [ |
||||
'auth' => [$account->getUsername(), $account->pass_raw], |
||||
'http_errors' => FALSE, |
||||
'headers' => [ |
||||
'Content-Type' => 'image/jpeg', |
||||
'Content-Disposition' => 'attachment; filename="test.jpeg"', |
||||
], |
||||
'body' => $image, |
||||
]; |
||||
$response = $client->request('PUT', $media_update_url, $options); |
||||
$this->assertTrue($response->getStatusCode() == 204, "Expected 204, received {$response->getStatusCode()}"); |
||||
|
||||
// GET the media again and compare image and metadata. |
||||
$options = [ |
||||
'auth' => [$account->getUsername(), $account->pass_raw], |
||||
'http_errors' => FALSE, |
||||
]; |
||||
$response = $client->request('GET', $url . '?_format=json', $options); |
||||
$updated = json_decode($response->getBody(), TRUE); |
||||
|
||||
$updated_mimetype = $updated['field_mimetype'][0]['value']; |
||||
$updated_width = $updated['field_width'][0]['value']; |
||||
$updated_height = $updated['field_height'][0]['value']; |
||||
$updated_image = file_get_contents($updated['field_image'][0]['url']); |
||||
|
||||
$this->assertTrue($original_mimetype != $updated_mimetype, "Mimetypes should be updated with media source update"); |
||||
$this->assertTrue($original_width != $updated_width, "Height should be updated with media source update"); |
||||
$this->assertTrue($original_height != $updated_height, "Width should be updated with media source update"); |
||||
$this->assertTrue($original_image != $updated_image, "Width should be updated with media source update"); |
||||
|
||||
$this->assertTrue($updated_mimetype == "image/jpeg", "Invalid mimetype. Expected image/jpeg, received $updated_mimetype"); |
||||
$this->assertTrue($updated_width == 295, "Invalid width. Expected 295, received $updated_width"); |
||||
$this->assertTrue($updated_height == 70, "Invalid height. Expected 70, received $updated_height"); |
||||
$this->assertTrue($updated_image == file_get_contents(__DIR__ . '/../../static/test.jpeg'), "Updated image not the same as PUT body."); |
||||
} |
||||
|
||||
} |
After Width: | Height: | Size: 6.7 KiB |
Loading…
Reference in new issue