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