Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ private function addCloudinaryConfiguration(ArrayNodeDefinition $rootNode): void
->booleanNode('log_requests')
->defaultValue(false)
->end()
->booleanNode('append_extension')
->defaultValue(true)
->end()
->booleanNode('unique_filenames')
->defaultValue(false)
->end()
->scalarNode('encryption_key')
->defaultNull()
->end()
Expand Down
10 changes: 10 additions & 0 deletions bundle/DependencyInjection/NetgenRemoteMediaExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,16 @@ public function load(array $configs, ContainerBuilder $container): void
$config['cloudinary']['folder_mode'],
);

$container->setParameter(
'netgen_remote_media.cloudinary.append_extension',
$config['cloudinary']['append_extension'],
);

$container->setParameter(
'netgen_remote_media.cloudinary.unique_filenames',
$config['cloudinary']['unique_filenames'],
);

$loader->load('default_parameters.yaml');
$loader->load('services/**/*.yaml', 'glob');
}
Expand Down
2 changes: 2 additions & 0 deletions bundle/Resources/config/services/core.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ services:
arguments:
- '@netgen_remote_media.provider.cloudinary.converter.visibility_type'
- '%netgen_remote_media.cloudinary.folder_mode%'
- '%netgen_remote_media.cloudinary.append_extension%'
- '%netgen_remote_media.cloudinary.unique_filenames%'

netgen_remote_media.provider.cloudinary.resolver.search_expression:
class: Netgen\RemoteMedia\Core\Provider\Cloudinary\Resolver\SearchExpression
Expand Down
2 changes: 1 addition & 1 deletion bundle/Resources/public/css/remotemedia.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion bundle/Resources/public/js/remotemedia.js

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions bundle/Resources/translations/ngremotemedia.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,13 @@ ngrm:
select: 'Select'
create: 'Create'
upload: 'Upload'
use_existing_resource: 'use existing resource'
checkbox:
overwrite: 'Overwrite'
placeholder:
new_folder: 'New folder name'
error:
missing_file: 'Missing file to upload'
existing_resource: 'Different resource with same name already exists in this folder! Change folder, filename, use overwrite or '
existing_resource: 'Different resource with same name already exists in this folder! Change folder, filename, use overwrite or use existing resource:'
unsupported_resource_type: 'Resource has been uploaded but it\s type is not supported so it can be used here! Supported types: '
invalid_visibility: 'Invalid visibility option "%visibility%", supported options: "%supported_visibilities%"'
file_upload_failed: 'File upload failed; the file might be too big or corrupted. Check your server file upload size settings.'
3 changes: 1 addition & 2 deletions bundle/Resources/translations/ngremotemedia.hr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,13 @@ ngrm:
select: 'Odaberi'
create: 'Kreiraj'
upload: 'Učitaj'
use_existing_resource: 'koristi postojeći resurs'
checkbox:
overwrite: 'Prepiši'
placeholder:
new_folder: 'Ime nove mape'
error:
missing_file: 'Nedostaje datoteka za učitavanje'
existing_resource: 'Drugi resurs s istim imenom već postoji u ovoj mapi! Promjenite mapu, ime datoteke, odaberite opciju za prepisivanje ili '
existing_resource: 'Drugi resurs s istim imenom već postoji u ovoj mapi! Promjenite mapu, ime datoteke, odaberite opciju za prepisivanje ili upotrijebite postojeći resurs:'
unsupported_resource_type: 'Resurs je uspješno učitan no njegov tip nije podržan na ovome mjestu! Podržani tipovi: '
invalid_visibility: 'Odabrana je nepodržana vidljivost "%visibility%", podržane vidljivosti: "%supported_visibilities%"'
file_upload_failed: 'Učitavanje datoteke neuspješno; datoteka je možda prevelika ili neispravna. Provjerite postavke servera za učitavanje datoteka.'
3 changes: 1 addition & 2 deletions bundle/Resources/translations/ngremotemedia.no.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,13 @@ ngrm:
select: 'Select'
create: 'Create'
upload: 'Upload'
use_existing_resource: 'use existing resource'
checkbox:
overwrite: 'Overwrite'
placeholder:
new_folder: 'New folder name'
error:
missing_file: 'Missing file to upload'
existing_resource: 'Different resource with same name already exists in this folder! Change folder, filename, use overwrite or '
existing_resource: 'Different resource with same name already exists in this folder! Change folder, filename, use overwrite or use existing resource:'
unsupported_resource_type: "Resource has been uploaded but it's type is not supported so it can't be used here! Supported types: "
invalid_visibility: 'Invalid visibility option "%visibility%", supported options: "%supported_visibilities%"'
file_upload_failed: 'File upload failed; the file might be too big or corrupted. Check your server file upload size settings.'
1 change: 0 additions & 1 deletion bundle/Resources/views/app/interactions.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@
'upload_button_select': 'ngrm.edit.vue.upload.button.select'|trans,
'upload_button_create': 'ngrm.edit.vue.upload.button.create'|trans,
'upload_button_upload': 'ngrm.edit.vue.upload.button.upload'|trans,
'upload_button_use_existing_resource': 'ngrm.edit.vue.upload.button.use_existing_resource'|trans,
'upload_checkbox_overwrite': 'ngrm.edit.vue.upload.checkbox.overwrite'|trans,
'upload_placeholder_new_folder': 'ngrm.edit.vue.upload.placeholder.new_folder'|trans,
'upload_error_existing_resource': 'ngrm.edit.vue.upload.error.existing_resource'|trans,
Expand Down
32 changes: 32 additions & 0 deletions docs/INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,38 @@ netgen_remote_media:
encryption_key: [YOUR_CLOUDINARY_ENCRYPTION_KEY]
```

#### Append extension to files

Cloudinary by default doesn't add extension to `image` (including also some documents, like PDF) and `video` (including audio) resources. So if you upload eg. `sample.jpg` or `sample.pdf`, it will be uploaded as `sample`. This is because transformations, since those files can be served in different formats (eg. you can serve `sample.jpg` in PNG or WEBP or any other image format, and you can serve PDFs or videos as images, like a screenshot). RAW files don't have this problem, and they are uploaded with an extension.

This can be problematic because, once you upload an extensionless resource, that name will be taken and you can't upload any other file with that name. Eg. if you upload `sample.jpg`, any other file named `sample` (eg. `sample.pdf` or `sample.zip`) will fail to upload because resource with the same name already exists.

In order to avoid that, this bundle appends extension to files (eg. `sample.jpg` will become `sample_jpg.jpg` before upload). This can be configured with:

```yaml
netgen_remote_media:
cloudinary:
append_extension: true
```

(default: `true`)

**WARNING:** if you disable it, you have to be careful when eg. uploading resources via scripts, because you will have to handle the existing resource exception and, if you use overwrite flag, you will overwrite the existing resource which can be a completely different file (eg. you can overwrite an image `sample.jpg` with a document `sample.pdf` and break your website).

#### Unique filenames

If you want to avoid all problems related to existing resources, you can enable the flag for unique filenames. This is a Cloudinary's built-in feature which will append a unique string to the filename before the extension. This can be configured with:

```yaml
netgen_remote_media:
cloudinary:
unique_filenames: true
```

(default: `false`)

**WARNING:** if you enable this, all resources will be always unique and you can easily create a mess on your cloud by uploading the same identical file multiple times.

### Upload prefix

If you need to change Cloudinary API url (to use eg. GEO specific URLs), there's a parameter `upload_prefix` (set to `https://api.cloudinary.com` by default):
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/UploadModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<div v-if="this.error" class="error">
{{ this.error }}
<a v-if="this.existingResourceButton" href="javascript:void(0);" @click="$emit('uploaded', existingResource)">
{{ this.config.translations.upload_button_use_existing_resource }}
{{ existingResource.originalFilename }} ({{ existingResource.type }}/{{ existingResource.format }})
</a>
</div>
<input type="text" :class="error ? 'error' : ''" v-model="filename"/>
Expand Down
103 changes: 17 additions & 86 deletions lib/Core/Provider/Cloudinary/Resolver/UploadOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,61 +4,41 @@

namespace Netgen\RemoteMedia\Core\Provider\Cloudinary\Resolver;

use Netgen\RemoteMedia\API\Upload\FileStruct;
use Netgen\RemoteMedia\API\Upload\ResourceStruct;
use Netgen\RemoteMedia\Core\Provider\Cloudinary\CloudinaryProvider;
use Netgen\RemoteMedia\Core\Provider\Cloudinary\Converter\VisibilityType as VisibilityTypeConverter;
use Netgen\RemoteMedia\Exception\MimeCategoryParseException;
use Netgen\RemoteMedia\Exception\MimeTypeNotFoundException;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Mime\MimeTypesInterface;

use function count;
use function explode;
use function get_headers;
use function in_array;
use function is_string;
use function md5_file;
use function preg_replace;
use function rtrim;
use function pathinfo;

final class UploadOptions
{
public function __construct(
private VisibilityTypeConverter $visibilityTypeConverter,
private string $folderMode,
private array $noExtensionMimeTypes = ['image', 'video', 'audio'],
private ?MimeTypesInterface $mimeTypes = null
) {
$this->mimeTypes = $this->mimeTypes ?? MimeTypes::getDefault();
}
private bool $appendExtension,
private bool $uniqueFilenames,
) {}

public function resolve(ResourceStruct $resourceStruct): array
{
$clean = preg_replace(
'#[^\p{L}|\p{N}]+#u',
'_',
$resourceStruct->getFilename(),
);

$cleanFileName = preg_replace('#[\p{Z}]{2,}#u', '_', $clean);
$fileName = rtrim($cleanFileName, '_');

$publicId = $this->appendExtension($fileName, $resourceStruct->getFileStruct());
$filenameOverride = $resourceStruct->getFilename();

if ($resourceStruct->doHideFilename()) {
$publicId = md5_file($resourceStruct->getFileStruct()->getUri());
}

if ($resourceStruct->getFolder() && $this->folderMode === CloudinaryProvider::FOLDER_MODE_FIXED) {
$publicId = $resourceStruct->getFolder()->getPath() . '/' . $publicId;
if ($this->appendExtension === true) {
$pathInfo = pathinfo($resourceStruct->getFilename());
$filenameOverride = ($pathInfo['extension'] ?? null)
? $pathInfo['filename'] . '_' . $pathInfo['extension'] . '.' . $pathInfo['extension']
: $pathInfo['filename'];
}

$options = [
'public_id' => $publicId,
'use_filename' => true,
'use_filename_as_display_name' => true,
'unique_filename' => $this->uniqueFilenames,
'filename_override' => $filenameOverride,
'overwrite' => $resourceStruct->doOverwrite(),
'invalidate' => $resourceStruct->doInvalidate() || $resourceStruct->doOverwrite(),
'discard_original_filename' => true,
'context' => $this->resolveContext($resourceStruct),
'type' => $this->visibilityTypeConverter->toCloudinaryType($resourceStruct->getVisibility()),
'resource_type' => $resourceStruct->getResourceType(),
Expand All @@ -71,60 +51,11 @@ public function resolve(ResourceStruct $resourceStruct): array
$options['asset_folder'] = $resourceStruct->getFolder()->getPath();
}

return $options;
}

private function appendExtension(string $publicId, FileStruct $fileStruct): string
{
$extension = $fileStruct->getOriginalExtension();

if ($extension === '') {
return $publicId;
}

try {
$mimeCategory = $this->resolveMimeCategory($fileStruct);
} catch (MimeCategoryParseException $exception) {
return $publicId;
}

// cloudinary handles pdf in a weird way - it is considered an "image" but it delivers it with proper extension on download
if ($extension !== 'pdf' && !in_array($mimeCategory, $this->noExtensionMimeTypes, true)) {
$publicId .= '.' . $extension;
}

return $publicId;
}

private function resolveMimeType(FileStruct $fileStruct): ?string
{
if ($fileStruct->isExternal()) {
$headers = get_headers($fileStruct->getUri(), true);

return $headers['content-type'] ?? $headers['Content-Type'] ?? null;
}

return $this->mimeTypes->guessMimeType($fileStruct->getUri());
}

/**
* @throws MimeCategoryParseException
*/
private function resolveMimeCategory(FileStruct $fileStruct): string
{
$mimeType = $this->resolveMimeType($fileStruct);

if ($mimeType === null) {
throw new MimeTypeNotFoundException($fileStruct->getUri(), $fileStruct->getType());
}

$parsedMime = explode('/', $mimeType);

if (count($parsedMime) !== 2) {
throw new MimeCategoryParseException($mimeType);
if ($resourceStruct->getFolder() && $this->folderMode === CloudinaryProvider::FOLDER_MODE_FIXED) {
$options['folder'] = $resourceStruct->getFolder()->getPath();
}

return $parsedMime[0];
return $options;
}

/**
Expand Down
17 changes: 0 additions & 17 deletions lib/Exception/MimeCategoryParseException.php

This file was deleted.

17 changes: 0 additions & 17 deletions lib/Exception/MimeTypeNotFoundException.php

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public function testItSetsValidContainerParameters(): void
$this->assertContainerBuilderHasParameter('netgen_remote_media.cache.pool_name', 'cache.app');
$this->assertContainerBuilderHasParameter('netgen_remote_media.cache.ttl', 3600);
$this->assertContainerBuilderHasParameter('netgen_remote_media.encryption_key', 'dsf45z45hh45f43f43f');
$this->assertContainerBuilderHasParameter('netgen_remote_media.cloudinary.append_extension', true);
$this->assertContainerBuilderHasParameter('netgen_remote_media.cloudinary.unique_filenames', false);

$this->assertContainerBuilderHasParameter(
'netgen_remote_media.named_remote_resources',
Expand Down Expand Up @@ -114,6 +116,8 @@ protected function getMinimalConfiguration(): array
'cloudinary' => [
'cache_requests' => true,
'log_requests' => false,
'append_extension' => true,
'unique_filenames' => false,
'encryption_key' => 'dsf45z45hh45f43f43f',
],
'image_variations' => [
Expand Down
Loading