From 6cc002ff2f2f59b7722cf47d437d8f6c6744b337 Mon Sep 17 00:00:00 2001 From: Wilmer Arambula <42547589+terabytesoftw@users.noreply.github.com> Date: Thu, 16 Nov 2023 05:36:39 -0300 Subject: [PATCH] Initital commit. (#1) --- .github/workflows/build.yml | 1 + .github/workflows/mutation.yml | 32 ++ README.md | 70 ++- composer-require-checker.json | 7 + composer.json | 55 ++- config/extension.php | 17 + phpstan.neon | 2 +- phpunit.xml.dist | 4 +- psalm.xml | 20 + src/Asset/Cdn/FilePondEncodePlugin.php | 21 + src/Asset/Cdn/FilePondImageCropPlugin.php | 29 ++ .../FilePondImageExifOrientationPlugin.php | 21 + src/Asset/Cdn/FilePondImagePreviewPlugin.php | 28 ++ .../Cdn/FilePondImageTransformPlugin.php | 29 ++ src/Asset/Cdn/FilePondPdfPreviewPlugin.php | 36 ++ src/Asset/Cdn/FilePondRenamePlugin.php | 29 ++ src/Asset/Cdn/FilePondValidateSizePlugin.php | 21 + src/Asset/Cdn/FilePondValidateTypePlugin.php | 21 + src/Asset/Dev/FilePondEncodePlugin.php | 34 ++ src/Asset/Dev/FilePondImageCropPlugin.php | 42 ++ .../FilePondImageExifOrientationPlugin.php | 34 ++ src/Asset/Dev/FilePondImagePreviewPlugin.php | 42 ++ .../Dev/FilePondImageTransformPlugin.php | 42 ++ src/Asset/Dev/FilePondPdfPreviewPlugin.php | 50 +++ src/Asset/Dev/FilePondRenamePlugin.php | 42 ++ src/Asset/Dev/FilePondValidateSizePlugin.php | 34 ++ src/Asset/Dev/FilePondValidateTypePlugin.php | 34 ++ src/Asset/FilePondAsset.php | 53 +++ src/Asset/FilePondCdnAsset.php | 38 ++ src/Asset/FilePondProdAsset.php | 53 +++ src/Asset/Prod/FilePondEncodePlugin.php | 34 ++ src/Asset/Prod/FilePondImageCropPlugin.php | 42 ++ .../FilePondImageExifOrientationPlugin.php | 34 ++ src/Asset/Prod/FilePondImagePreviewPlugin.php | 42 ++ .../Prod/FilePondImageTransformPlugin.php | 42 ++ src/Asset/Prod/FilePondPdfPreviewPlugin.php | 50 +++ src/Asset/Prod/FilePondRenamePlugin.php | 42 ++ src/Asset/Prod/FilePondValidateSizePlugin.php | 34 ++ src/Asset/Prod/FilePondValidateTypePlugin.php | 34 ++ src/Example.php | 13 - src/File.php | 57 +++ src/FilePond.php | 418 ++++++++++++++++++ src/FileProcessing.php | 93 ++++ tests/ExampleTest.php | 18 - tests/ExceptionTest.php | 32 ++ tests/FileProcessingTest.php | 189 ++++++++ tests/RenderTest.php | 268 +++++++++++ tests/Support/TestForm.php | 12 + tests/Support/main.php | 9 + tests/Support/runtime/.gitkeep | 0 tests/TestCase.php | 68 +++ tests/bootstrap.php | 19 + 52 files changed, 2435 insertions(+), 56 deletions(-) create mode 100644 .github/workflows/mutation.yml create mode 100644 composer-require-checker.json create mode 100644 config/extension.php create mode 100644 psalm.xml create mode 100644 src/Asset/Cdn/FilePondEncodePlugin.php create mode 100644 src/Asset/Cdn/FilePondImageCropPlugin.php create mode 100644 src/Asset/Cdn/FilePondImageExifOrientationPlugin.php create mode 100644 src/Asset/Cdn/FilePondImagePreviewPlugin.php create mode 100644 src/Asset/Cdn/FilePondImageTransformPlugin.php create mode 100644 src/Asset/Cdn/FilePondPdfPreviewPlugin.php create mode 100644 src/Asset/Cdn/FilePondRenamePlugin.php create mode 100644 src/Asset/Cdn/FilePondValidateSizePlugin.php create mode 100644 src/Asset/Cdn/FilePondValidateTypePlugin.php create mode 100644 src/Asset/Dev/FilePondEncodePlugin.php create mode 100644 src/Asset/Dev/FilePondImageCropPlugin.php create mode 100644 src/Asset/Dev/FilePondImageExifOrientationPlugin.php create mode 100644 src/Asset/Dev/FilePondImagePreviewPlugin.php create mode 100644 src/Asset/Dev/FilePondImageTransformPlugin.php create mode 100644 src/Asset/Dev/FilePondPdfPreviewPlugin.php create mode 100644 src/Asset/Dev/FilePondRenamePlugin.php create mode 100644 src/Asset/Dev/FilePondValidateSizePlugin.php create mode 100644 src/Asset/Dev/FilePondValidateTypePlugin.php create mode 100644 src/Asset/FilePondAsset.php create mode 100644 src/Asset/FilePondCdnAsset.php create mode 100644 src/Asset/FilePondProdAsset.php create mode 100644 src/Asset/Prod/FilePondEncodePlugin.php create mode 100644 src/Asset/Prod/FilePondImageCropPlugin.php create mode 100644 src/Asset/Prod/FilePondImageExifOrientationPlugin.php create mode 100644 src/Asset/Prod/FilePondImagePreviewPlugin.php create mode 100644 src/Asset/Prod/FilePondImageTransformPlugin.php create mode 100644 src/Asset/Prod/FilePondPdfPreviewPlugin.php create mode 100644 src/Asset/Prod/FilePondRenamePlugin.php create mode 100644 src/Asset/Prod/FilePondValidateSizePlugin.php create mode 100644 src/Asset/Prod/FilePondValidateTypePlugin.php delete mode 100644 src/Example.php create mode 100644 src/File.php create mode 100644 src/FilePond.php create mode 100644 src/FileProcessing.php delete mode 100644 tests/ExampleTest.php create mode 100644 tests/ExceptionTest.php create mode 100644 tests/FileProcessingTest.php create mode 100644 tests/RenderTest.php create mode 100644 tests/Support/TestForm.php create mode 100644 tests/Support/main.php create mode 100644 tests/Support/runtime/.gitkeep create mode 100644 tests/TestCase.php create mode 100644 tests/bootstrap.php diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 719d5a9..d7d0a2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,6 +25,7 @@ jobs: phpunit: uses: yiisoft/actions/.github/workflows/phpunit.yml@master with: + extensions: intl os: >- ['ubuntu-latest', 'windows-latest'] php: >- diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 0000000..a787f09 --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,32 @@ +on: + pull_request: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - 'psalm.xml' + + push: + paths-ignore: + - 'docs/**' + - 'README.md' + - 'CHANGELOG.md' + - '.gitignore' + - '.gitattributes' + - 'psalm.xml' + +name: mutation test + +jobs: + mutation: + uses: php-forge/actions/.github/workflows/roave-infection.yml@main + secrets: + AUTH_TOKEN: ${{ secrets.AUTH_TOKEN }} + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + with: + os: >- + ['ubuntu-latest'] + php: >- + ['8.1'] diff --git a/README.md b/README.md index 7fc0a59..8d0d338 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

- + -

Yii2-Template.

+

FilePond.


@@ -13,32 +13,76 @@ yii2-version - - PHPUnit + + PHPUnit - - Codecov + + Codecov - + + Infection + + PHPStan - - PHPStan level + + PHPStan level Code style

-## Requirements +## Installation + +The preferred way to install this extension is through [composer](https://getcomposer.org/download/). + +Either run + +``` +php composer.phar require --dev --prefer-dist yii2-extensions/filepond +``` -The minimun version of `PHP` required by this package is `PHP 8.1`. +or add -For install this package, you need [composer](https://getcomposer.org/). +``` +"yii2-extensions/filepond": "dev-main" +``` + +to the require-dev section of your `composer.json` file. ## Usage -[Check the documentation docs](/docs/README.md) to learn about usage. +### View + +```php +field($formModel, 'image_file') + ->widget( + FilePond::class, + [ + 'labelIdle' => Yii::t( + 'yii.blog', 'Drag & Drop your files or Browse', + ), + 'loadFileDefault' => $imageFile, + 'imagePreviewHeight' => 170, + 'imageCropAspectRatio' => '1:1', + ], + ) +?> +``` + +### Controller or Model + +```php +$imageFile = FileProcessing::saveWithReturningFile( + $categoryForm->image_file, + Yii::getAlias('@uploads'), + "category{$category->id}", + false +); +``` ## Testing diff --git a/composer-require-checker.json b/composer-require-checker.json new file mode 100644 index 0000000..0f177e7 --- /dev/null +++ b/composer-require-checker.json @@ -0,0 +1,7 @@ +{ + "symbol-whitelist": [ + "YII_BEGIN_TIME", + "YII_DEBUG", + "YII_ENV" + ] +} diff --git a/composer.json b/composer.json index cd5155d..8f75cfc 100644 --- a/composer.json +++ b/composer.json @@ -1,41 +1,78 @@ { - "name": "yii2/template", - "type": "library", - "description": "_____", + "name": "yii2-extensions/filepond", + "type": "yii2-extension", + "description": "FilePond widget for PHP.", "keywords": [ - "_____" + "yii2", + "filepond", + "widget" ], "license": "mit", "minimum-stability": "dev", "prefer-stable": true, "require": { "php": ">=8.1", - "yiisoft/yii2": "^2.2" + "ext-intl": "*", + "npm-asset/filepond": "^4.3", + "npm-asset/filepond-plugin-file-encode": "^2.1", + "npm-asset/filepond-plugin-file-rename": "^1.1", + "npm-asset/filepond-plugin-file-validate-size": "^2.2", + "npm-asset/filepond-plugin-file-validate-type": "^1.2", + "npm-asset/filepond-plugin-image-crop": "^2.0", + "npm-asset/filepond-plugin-image-exif-orientation": "^1.0", + "npm-asset/filepond-plugin-image-preview": "^4.6", + "npm-asset/filepond-plugin-image-transform": "^3.8", + "npm-asset/filepond-plugin-pdf-preview": "^1.0", + "oomphinc/composer-installers-extender": "^2.0", + "php-forge/awesome-widget": "dev-main", + "php-forge/html": "dev-main", + "yiisoft/yii2": "*" }, "require-dev": { "maglnet/composer-require-checker": "^4.6", + "php-forge/support": "dev-main", "phpunit/phpunit": "^10.2", + "roave/infection-static-analysis-plugin": "^1.32", "yii2-extensions/phpstan": "dev-main" }, "autoload": { "psr-4": { - "yii\\template\\": "src" + "Yii\\FilePond\\": "src" } }, "autoload-dev": { "psr-4": { - "yii\\template\\tests\\": "tests" + "Yii\\FilePond\\Tests\\": "tests" } }, "extra": { "branch-alias": { - "dev-main": "1.0.x-dev" + "dev-master": "1.0.x-dev" + }, + "config-plugin": { + "yii2-filepond": "extension.php" + }, + "config-plugin-options": { + "source-directory": "config" + }, + "installer-types": [ + "bower-asset", + "npm-asset" + ], + "installer-paths": { + "./node_modules/{$name}/": [ + "type:bower-asset", + "type:npm-asset" + ] } }, "config": { "sort-packages": true, "allow-plugins": { - "yiisoft/yii2-composer": true + "yiisoft/yii2-composer": true, + "composer/installers": true, + "oomphinc/composer-installers-extender": true, + "infection/extension-installer": true } }, "scripts": { diff --git a/config/extension.php b/config/extension.php new file mode 100644 index 0000000..1509abf --- /dev/null +++ b/config/extension.php @@ -0,0 +1,17 @@ + [ + 'i18n' => [ + 'translations' => [ + 'yii.filepond' => [ + 'class' => PhpMessageSource::class, + ], + ], + ], + ], +]; diff --git a/phpstan.neon b/phpstan.neon index 0b239cb..3d3be82 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,7 +8,7 @@ parameters: - YII_ENV_PROD - YII_ENV_TEST - level: 2 + level: 5 paths: - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f29a28d..cf30a25 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,7 +2,7 @@ - + tests diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..cefdd03 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + diff --git a/src/Asset/Cdn/FilePondEncodePlugin.php b/src/Asset/Cdn/FilePondEncodePlugin.php new file mode 100644 index 0000000..aebd548 --- /dev/null +++ b/src/Asset/Cdn/FilePondEncodePlugin.php @@ -0,0 +1,21 @@ + [ + 'dist/filepond-plugin-file-encode.js', + ], + ]; +} diff --git a/src/Asset/Dev/FilePondImageCropPlugin.php b/src/Asset/Dev/FilePondImageCropPlugin.php new file mode 100644 index 0000000..682f549 --- /dev/null +++ b/src/Asset/Dev/FilePondImageCropPlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-image-crop.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondAsset::class, + ]; +} diff --git a/src/Asset/Dev/FilePondImageExifOrientationPlugin.php b/src/Asset/Dev/FilePondImageExifOrientationPlugin.php new file mode 100644 index 0000000..139fc75 --- /dev/null +++ b/src/Asset/Dev/FilePondImageExifOrientationPlugin.php @@ -0,0 +1,34 @@ + [ + 'dist/filepond-plugin-image-exif-orientation.js', + ], + ]; +} diff --git a/src/Asset/Dev/FilePondImagePreviewPlugin.php b/src/Asset/Dev/FilePondImagePreviewPlugin.php new file mode 100644 index 0000000..fc4bf18 --- /dev/null +++ b/src/Asset/Dev/FilePondImagePreviewPlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-image-preview.css', + 'dist/filepond-plugin-image-preview.js', + ], + ]; +} diff --git a/src/Asset/Dev/FilePondImageTransformPlugin.php b/src/Asset/Dev/FilePondImageTransformPlugin.php new file mode 100644 index 0000000..6883592 --- /dev/null +++ b/src/Asset/Dev/FilePondImageTransformPlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-image-transform.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondAsset::class, + ]; +} diff --git a/src/Asset/Dev/FilePondPdfPreviewPlugin.php b/src/Asset/Dev/FilePondPdfPreviewPlugin.php new file mode 100644 index 0000000..14e6c63 --- /dev/null +++ b/src/Asset/Dev/FilePondPdfPreviewPlugin.php @@ -0,0 +1,50 @@ + [ + 'dist/filepond-plugin-pdf-preview.css', + 'dist/filepond-plugin-pdf-preview.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondAsset::class, + ]; +} diff --git a/src/Asset/Dev/FilePondRenamePlugin.php b/src/Asset/Dev/FilePondRenamePlugin.php new file mode 100644 index 0000000..21d4bd0 --- /dev/null +++ b/src/Asset/Dev/FilePondRenamePlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-file-rename.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondAsset::class, + ]; +} diff --git a/src/Asset/Dev/FilePondValidateSizePlugin.php b/src/Asset/Dev/FilePondValidateSizePlugin.php new file mode 100644 index 0000000..2eed593 --- /dev/null +++ b/src/Asset/Dev/FilePondValidateSizePlugin.php @@ -0,0 +1,34 @@ + [ + 'dist/filepond-plugin-file-validate-size.js', + ], + ]; +} diff --git a/src/Asset/Dev/FilePondValidateTypePlugin.php b/src/Asset/Dev/FilePondValidateTypePlugin.php new file mode 100644 index 0000000..e12dbb0 --- /dev/null +++ b/src/Asset/Dev/FilePondValidateTypePlugin.php @@ -0,0 +1,34 @@ + [ + 'dist/filepond-plugin-file-validate-type.js', + ], + ]; +} diff --git a/src/Asset/FilePondAsset.php b/src/Asset/FilePondAsset.php new file mode 100644 index 0000000..9fafa60 --- /dev/null +++ b/src/Asset/FilePondAsset.php @@ -0,0 +1,53 @@ + [ + 'dist/filepond.css', + 'dist/filepond.js', + ], + ]; +} diff --git a/src/Asset/FilePondCdnAsset.php b/src/Asset/FilePondCdnAsset.php new file mode 100644 index 0000000..e5e7710 --- /dev/null +++ b/src/Asset/FilePondCdnAsset.php @@ -0,0 +1,38 @@ + [ + 'dist/filepond.min.css', + 'dist/filepond.min.js', + ], + ]; +} diff --git a/src/Asset/Prod/FilePondEncodePlugin.php b/src/Asset/Prod/FilePondEncodePlugin.php new file mode 100644 index 0000000..1ba80be --- /dev/null +++ b/src/Asset/Prod/FilePondEncodePlugin.php @@ -0,0 +1,34 @@ + [ + 'dist/filepond-plugin-file-encode.min.js', + ], + ]; +} diff --git a/src/Asset/Prod/FilePondImageCropPlugin.php b/src/Asset/Prod/FilePondImageCropPlugin.php new file mode 100644 index 0000000..83174f5 --- /dev/null +++ b/src/Asset/Prod/FilePondImageCropPlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-image-crop.min.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondAsset::class, + ]; +} diff --git a/src/Asset/Prod/FilePondImageExifOrientationPlugin.php b/src/Asset/Prod/FilePondImageExifOrientationPlugin.php new file mode 100644 index 0000000..7ec8bae --- /dev/null +++ b/src/Asset/Prod/FilePondImageExifOrientationPlugin.php @@ -0,0 +1,34 @@ + [ + 'dist/filepond-plugin-image-exif-orientation.min.js', + ], + ]; +} diff --git a/src/Asset/Prod/FilePondImagePreviewPlugin.php b/src/Asset/Prod/FilePondImagePreviewPlugin.php new file mode 100644 index 0000000..cd359b7 --- /dev/null +++ b/src/Asset/Prod/FilePondImagePreviewPlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-image-preview.min.css', + 'dist/filepond-plugin-image-preview.min.js', + ], + ]; +} diff --git a/src/Asset/Prod/FilePondImageTransformPlugin.php b/src/Asset/Prod/FilePondImageTransformPlugin.php new file mode 100644 index 0000000..76e009c --- /dev/null +++ b/src/Asset/Prod/FilePondImageTransformPlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-image-transform.min.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondAsset::class, + ]; +} diff --git a/src/Asset/Prod/FilePondPdfPreviewPlugin.php b/src/Asset/Prod/FilePondPdfPreviewPlugin.php new file mode 100644 index 0000000..4493f9f --- /dev/null +++ b/src/Asset/Prod/FilePondPdfPreviewPlugin.php @@ -0,0 +1,50 @@ + [ + 'dist/filepond-plugin-pdf-preview.min.css', + 'dist/filepond-plugin-pdf-preview.min.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondProdAsset::class, + ]; +} diff --git a/src/Asset/Prod/FilePondRenamePlugin.php b/src/Asset/Prod/FilePondRenamePlugin.php new file mode 100644 index 0000000..880c239 --- /dev/null +++ b/src/Asset/Prod/FilePondRenamePlugin.php @@ -0,0 +1,42 @@ + [ + 'dist/filepond-plugin-file-rename.min.js', + ], + ]; + + /** + * {@inheritDoc} + */ + public $depends = [ + FilePondProdAsset::class, + ]; +} diff --git a/src/Asset/Prod/FilePondValidateSizePlugin.php b/src/Asset/Prod/FilePondValidateSizePlugin.php new file mode 100644 index 0000000..a6348cd --- /dev/null +++ b/src/Asset/Prod/FilePondValidateSizePlugin.php @@ -0,0 +1,34 @@ + [ + 'dist/filepond-plugin-file-validate-size.min.js', + ], + ]; +} diff --git a/src/Asset/Prod/FilePondValidateTypePlugin.php b/src/Asset/Prod/FilePondValidateTypePlugin.php new file mode 100644 index 0000000..60d63a6 --- /dev/null +++ b/src/Asset/Prod/FilePondValidateTypePlugin.php @@ -0,0 +1,34 @@ + [ + 'dist/filepond-plugin-file-validate-type.min.js', + ], + ]; +} diff --git a/src/Example.php b/src/Example.php deleted file mode 100644 index 067eeb8..0000000 --- a/src/Example.php +++ /dev/null @@ -1,13 +0,0 @@ -attributes; + $attributes['type'] = 'file'; + + if (!array_key_exists('name', $attributes)) { + $name = Utils::generateInputName($this->formModel->formName(), $this->attribute); + } else { + $name = is_string($attributes['name']) ? $attributes['name'] : ''; + } + + $attributes['id'] = Utils::generateInputId($this->formModel->formName(), $this->attribute); + $attributes['name'] = Utils::generateArrayableName($name); + + // input type="file" not supported value attribute. + unset($attributes['value']); + + return HtmlBuilder::create('input', '', $attributes); + } +} diff --git a/src/FilePond.php b/src/FilePond.php new file mode 100644 index 0000000..1caa72c --- /dev/null +++ b/src/FilePond.php @@ -0,0 +1,418 @@ + return `my_new_name${file.extension}`; + */ + public string $fileRename = ''; + /** + * @var string The file validate type detect type function. + * + * ```JS + * fileValidateTypeDetectType: (source, type) => + * new Promise((resolve, reject) => { + * // Do custom type detection here and return with promise + * + * resolve(type); + * }), + * ``` + * + * @link https://pqina.nl/filepond/docs/api/plugins/file-validate-type/#custom-type-detection + */ + public string $fileValidateTypeDetectType = ''; + /** + * @var string The file validate When message shown to indicate the allowed file types. + */ + public string $fileValidateTypeLabelExpectedTypes = ''; + /** + * @var string The aspect ratio of the crop in human-readable format, for example, '1:1' or '16:10'. + */ + public string|null $imageCropAspectRatio = null; + /** + * @var string The image preview height. + * + * Fixed image preview height, overrides min and max preview height. + */ + public string|null $imagePreviewHeight = null; + /** + * @var bool Whether to show the image preview markup. + */ + public bool $imagePreviewMarkupShow = true; + /** + * @var string The image preview max file size. + * + * Maximum file size for images to preview immediately, if files are larger and the browser doesn't support + * createImageBitmap the preview is queued till FilePond is in rest state. + * + * By default, no maximum file size is defined, expects a string, like `2MB` or `500KB`. + */ + public string|null $imagePreviewMaxFileSize = null; + /** + * @var int The image preview max height. + * + * Maximum height of the image preview in pixels. + */ + public int $imagePreviewMaxHeight = 256; + /** + * @var int The image preview max instant preview file size. + * + * Maximum file size for images to preview immediately, if files are larger and the browser doesn't support + * createImageBitmap the preview is queued till FilePond is in rest state. + */ + public int $imagePreviewMaxInstantPreviewFileSize = 1000000; + /** + * @var int The image preview min height. + * + * Minimum height of the image preview in pixels. + */ + public int $imagePreviewMinHeight = 44; + /** + * @var string The image preview transparency indicator. + * + * Show a grid behind the preview, set to a color value (for example '#f00') to set transparent image background + * color. + * + * Please note that this is only for preview purposes. + * + * The background color or grid isn't embedded in the output image. + */ + public string|null $imagePreviewTransparencyIndicator = null; + /** + * @var array The image transform after create blob. + * + * A hook to make changes to the file after the file has been created. + */ + public array|null $imageTransformAfterCreateBlob = null; + /** + * @var array The image transform before create blob. + * + * A hook to make changes to the canvas before the file is created. + */ + public array|null $imageTransformBeforeCreateBlob = null; + /** + * @var int The image transform output quality. + * + * A number between 0 and 100 indicating image quality (e.g. 92 => 92%). + */ + public int|null $imageTransformOutputQuality = null; + /** + * @var array The image transform client transforms. + * + * An array of transforms to apply on the client, useful if we, for instance, want to do resizing on the client but + * cropping on the server. Null means apply all transforms ('resize', 'crop'). + */ + public array|null $imageTransformClientTransforms = null; + /** + * @var string The image transform output quality mode. + * + * Should output quality be enforced, set the 'optional' to only apply when a transform is required due to other + * requirements (e.g. resize or crop). + */ + public string $imageTransformOutputQualityMode = 'always'; + /** + * @var bool Whether to strip the image head. + */ + public bool $imageTransformOutputStripImageHead = true; + /** + * @var array The image transform variants. + * + * An object that can be used to output many files based on different transform instructions. + */ + public array|null $imageTransformVariants = null; + /** + * @var bool Whether the image transform variants include original. + * + * Should the transform plugin output the original file. + */ + public bool $imageTransformVariantsIncludeDefault = true; + /** + * @var string The image transform variants default name. + * + * The name to use in front of the file name. + */ + public string|null $imageTransformVariantsDefaultName = null; + /** + * @var bool Whether the image transform variants include original. + * + * Should the transform plugin output the original file. + */ + public bool $imageTransformVariantsIncludeOriginal = false; + /** + * @var string The label to show when there are no files. + */ + public string $labelIdle = 'Drag & Drop your files or Browse '; + /** + * @var string Label for max files size. + */ + public string $labelMaxFileSize = ''; + /** + * @var string Label for max file size exceeded. + */ + public string $labelMaxFileSizeExceeded = ''; + /** + * @var string Label for max total file size. + */ + public string $labelMaxTotalFileSize = ''; + /** + * @var string Label for max total file size exceeded. + */ + public string $labelMaxTotalFileSizeExceeded = ''; + /** + * @var string Label for a file type not allowed error. + */ + public string $labelFileTypeNotAllowed = ''; + /** + * @var string The default file to load. + */ + public string $loadFileDefault = ''; + /** + * @var int The maximum number of files allowed. + */ + public int $maxFiles = 1; + /** + * @var string The maximum file size allowed. + */ + public string|null $maxFileSize = null; + /** + * @var string The maximum total file size allowed. + */ + public string|null $maxTotalFileSize = null; + /** + * @var string The minimum file size allowed. + */ + public string|null $minFileSize = null; + /** + * @phpstan-var string[] The default plugins to load. + */ + public array $pluginDefault = [ + 'FilePondPluginFileEncode', + 'FilePondPluginFileValidateSize', + 'FilePondPluginFileValidateType', + 'FilePondPluginImageExifOrientation', + 'FilePondPluginImagePreview', + ]; + /** + * @var int The pdf preview height. + */ + public int $pdfPreviewHeight = 320; + /** + * @var string The pdf component extra params. + */ + public string $pdfComponentExtraParams = 'toolbar=0&view=fit&page=1'; + /** + * @var bool Whether the field is required. + */ + public bool $required = false; + + public function init(): void + { + $this->config = array_merge( + [ + 'acceptedFileTypes' => $this->acceptedFileTypes, + 'allowFileRename' => $this->allowFileRename, + 'allowFileTypeValidation' => $this->allowFileTypeValidation, + 'allowFileValidateSize' => $this->allowFileValidateSize, + 'allowImageCrop' => $this->allowImageCrop, + 'allowImageExifOrientation' => $this->allowImageExifOrientation, + 'allowImagePreview' => $this->allowImagePreview, + 'allowImageTransform' => $this->allowImageTransform, + 'allowMultiple' => $this->allowMultiple, + 'className' => $this->cssClass, + 'fileValidateTypeLabelExpectedTypes' => Yii::t( + 'yii.filepond', + 'Expects {allButLastType} or {lastType}', + ), + 'imageCropAspectRatio' => $this->imageCropAspectRatio, + 'imagePreviewHeight' => $this->imagePreviewHeight, + 'imagePreviewMarkupShow' => $this->imagePreviewMarkupShow, + 'imagePreviewMaxFileSize' => $this->imagePreviewMaxFileSize, + 'imagePreviewMaxHeight' => $this->imagePreviewMaxHeight, + 'imagePreviewMaxInstantPreviewFileSize' => $this->imagePreviewMaxFileSize, + 'imagePreviewMinHeight' => $this->imagePreviewMinHeight, + 'imagePreviewTransparencyIndicator' => $this->imagePreviewTransparencyIndicator, + 'imageTransformAfterCreateBlob' => $this->imageTransformAfterCreateBlob, + 'imageTransformBeforeCreateBlob' => $this->imageTransformBeforeCreateBlob, + 'imageTransformClientTransforms' => $this->imageTransformClientTransforms, + 'imageTransformOutputQuality' => $this->imageTransformOutputQuality, + 'imageTransformOutputQualityMode' => $this->imageTransformOutputQuality, + 'imageTransformOutputStripImageHead' => $this->imageTransformOutputStripImageHead, + 'imageTransformVariants' => $this->imageTransformVariants, + 'imageTransformVariantsDefaultName' => $this->imageTransformVariantsDefaultName, + 'imageTransformVariantsIncludeOriginal' => $this->imageTransformVariantsIncludeDefault, + 'labelFileTypeNotAllowed' => Yii::t('yii.filepond', 'File type not allowed'), + 'labelIdle' => $this->labelIdle, + 'labelMaxFileSize' => Yii::t('yii.filepond', 'Maximum file size is {filesize}'), + 'labelMaxFileSizeExceeded' => Yii::t('yii.filepond', 'File is too large'), + 'labelMaxTotalFileSize' => Yii::t('yii.filepond', 'Maximum total file size is {filesize}'), + 'labelMaxTotalFileSizeExceeded' => Yii::t('yii.filepond', 'Maximum total size exceeded'), + 'maxFiles' => $this->maxFiles, + 'maxFileSize' => $this->maxFileSize, + 'maxTotalFileSize' => $this->maxTotalFileSize, + 'minFileSize' => $this->minFileSize, + 'pdfPreviewHeight' => $this->pdfPreviewHeight, + 'pdfComponentExtraParams' => $this->pdfComponentExtraParams, + 'required' => $this->required, + ], + $this->config, + ); + } + + public function run(): string + { + if ($this->model === null) { + throw new RuntimeException('The model is not set.'); + } + + if ($this->attribute === null) { + throw new RuntimeException('The attribute is not set.'); + } + + $this->registerClientScript(); + + return $this->renderInputFile(); + } + + /** + * @throws JsonException + */ + private function getScript(): string + { + $closure = $this->fileRename; + + if ($this->fileValidateTypeDetectType !== '') { + $closure = "{$this->fileValidateTypeDetectType} {$closure}"; + } + + $id = Utils::generateInputId($this->model->formName(), $this->attribute); + $loadFileDefault = $this->loadFileDefault; + $pluginConfig = implode(', ', $this->pluginDefault); + $setOptions = json_encode($this->config, JSON_THROW_ON_ERROR); + + return <<getView(); + + if ($this->environment === '') { + $this->environment = YII_ENV; + } + + match ($this->environment) { + 'cdn' => FilePondCdnAsset::register($view), + 'prod' => FilePondProdAsset::register($view), + default => FilePondAsset::register($view), + }; + + $view->registerJs($this->getScript()); + } + + /** + * @return string the generated input tag. + */ + private function renderInputFile(): string + { + $attributes = $this->attributes; + + if (array_key_exists('allowMultiple', $this->config) && $this->config['allowMultiple']) { + $attributes['multiple'] = true; + } + + if (array_key_exists('className', $this->config) && is_string($this->config['className'])) { + CssClass::add($attributes, $this->config['className']); + } + + if (array_key_exists('required', $this->config) && $this->config['required']) { + $attributes['required'] = true; + } + + CssClass::add($attributes, 'filepond'); + + return File::widget($this->model, $this->attribute)->attributes($attributes)->render(); + } +} diff --git a/src/FileProcessing.php b/src/FileProcessing.php new file mode 100644 index 0000000..ddf7bae --- /dev/null +++ b/src/FileProcessing.php @@ -0,0 +1,93 @@ +data) && is_string($file->name)) { + $filename = self::sanitizeFilename($file->name, $newFileName); + + $result = self::writeFile($path, base64_decode($file->data), $filename); + + if ($result) { + $savedFiles[] = $withPath ? $path . $filename : $filename; + } + } + } + + return $savedFiles; + } + + private static function sanitizeFilename(string $filename, $newFileName): string + { + $info = pathinfo($filename); + + $name = ($newFileName !== null) + ? self::sanitizeFilenamePart($newFileName) + : self::sanitizeFilenamePart($info['filename']); + + $extension = isset($info['extension']) ? self::sanitizeFilenamePart($info['extension']) : ''; + + return ($name !== '' ? $name : '_') . '.' . $extension; + } + + private static function sanitizeFilenamePart(string $str): string + { + return preg_replace("/[^a-zA-Z0-9\s]/", '', $str); + } + + private static function writeFile(string $path, string $data, string $filename): bool + { + $handle = fopen($path . DIRECTORY_SEPARATOR . $filename, 'wb'); + $result = fwrite($handle, $data); + + fclose($handle); + + return $result !== false; + } +} diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 2825a4e..0000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,18 +0,0 @@ -assertTrue($example->getExample()); - } -} diff --git a/tests/ExceptionTest.php b/tests/ExceptionTest.php new file mode 100644 index 0000000..ed740db --- /dev/null +++ b/tests/ExceptionTest.php @@ -0,0 +1,32 @@ +expectException(RuntimeException::class); + $this->expectExceptionMessage('The attribute is not set.'); + + FilePond::widget(['model' => new TestForm()]); + } + + public function testNotSetModel(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The model is not set.'); + + FilePond::widget(['attribute' => 'array']); + } +} diff --git a/tests/FileProcessingTest.php b/tests/FileProcessingTest.php new file mode 100644 index 0000000..3419f8a --- /dev/null +++ b/tests/FileProcessingTest.php @@ -0,0 +1,189 @@ + json_encode( + [ + 'id' => 'opqgdavos', + 'name' => 'test.txt', + 'type' => 'text/plain', + 'size' => 7, + 'metadata' => [], + 'data' => 'VGVzdE1lCg==', + ], + JSON_THROW_ON_ERROR, + ), + ], + __DIR__ . '/Support/runtime/', + ); + + $this->assertFileExists(__DIR__ . '/Support/runtime/test.txt'); + } + + /** + * @throws JsonException + */ + public function testSaveWithEmptyData(): void + { + FileProcessing::save( + [ + 0 => json_encode([], JSON_THROW_ON_ERROR), + ], + __DIR__ . '/Support/runtime', + ); + + $this->assertFileDoesNotExist(__DIR__ . '/Support/runtime/test.txt'); + } + + /** + * @throws JsonException + */ + public function testSaveWithReturningFile(): void + { + $files = FileProcessing::saveWithReturningFile( + [ + 0 => json_encode( + [ + 'id' => 'opqgdavos', + 'name' => 'test.txt', + 'type' => 'text/plain', + 'size' => 7, + 'metadata' => [], + 'data' => 'VGVzdE1lCg==', + ], + JSON_THROW_ON_ERROR, + ), + ], + __DIR__ . '/Support/runtime/', + 'category', + false, + ); + + $this->assertFileExists(__DIR__ . '/Support/runtime/category.txt'); + $this->assertSame('category.txt', $files); + } + + /** + * @throws JsonException + */ + public function testSaveWithReturningFiles(): void + { + $files = FileProcessing::saveWithReturningFiles( + [ + 0 => json_encode( + [ + 'id' => 'opqgdavos', + 'name' => 'test.txt', + 'type' => 'text/plain', + 'size' => 7, + 'metadata' => [], + 'data' => 'VGVzdE1lCg==', + ], + JSON_THROW_ON_ERROR, + ), + 1 => json_encode( + [ + 'id' => 'opqgdavo2', + 'name' => 'test1.txt', + 'type' => 'text/plain', + 'size' => 7, + 'metadata' => [], + 'data' => 'VGVzdE1lCg==', + ], + JSON_THROW_ON_ERROR, + ), + ], + __DIR__ . '/Support/runtime/', + ); + + $this->assertFileExists(__DIR__ . '/Support/runtime/test.txt'); + $this->assertFileExists(__DIR__ . '/Support/runtime/test1.txt'); + $this->assertSame( + [ + __DIR__ . '/Support/runtime/test.txt', + __DIR__ . '/Support/runtime/test1.txt', + ], + $files, + ); + } + + /** + * @throws JsonException + */ + public function testSaveWithReturningFilesEmptyData(): void + { + $files = FileProcessing::saveWithReturningFiles( + [ + 0 => json_encode([], JSON_THROW_ON_ERROR), + ], + __DIR__ . '/Support/runtime/', + ); + + $this->assertFileDoesNotExist(__DIR__ . '/Support/runtime/test.txt'); + $this->assertSame([], $files); + } + + public function testSaveWithReturningFilesWithoutPath(): void + { + $files = FileProcessing::saveWithReturningFiles( + [ + 0 => json_encode( + [ + 'id' => 'opqgdavos', + 'name' => 'test.txt', + 'type' => 'text/plain', + 'size' => 7, + 'metadata' => [], + 'data' => 'VGVzdE1lCg==', + ], + JSON_THROW_ON_ERROR, + ), + 1 => json_encode( + [ + 'id' => 'opqgdavo2', + 'name' => 'test1.txt', + 'type' => 'text/plain', + 'size' => 7, + 'metadata' => [], + 'data' => 'VGVzdE1lCg==', + ], + JSON_THROW_ON_ERROR, + ), + ], + __DIR__ . '/Support/runtime/', + withPath: false, + ); + + $this->assertFileExists(__DIR__ . '/Support/runtime/test.txt'); + $this->assertFileExists(__DIR__ . '/Support/runtime/test1.txt'); + $this->assertSame(['test.txt', 'test1.txt'], $files); + } +} diff --git a/tests/RenderTest.php b/tests/RenderTest.php new file mode 100644 index 0000000..71d53e7 --- /dev/null +++ b/tests/RenderTest.php @@ -0,0 +1,268 @@ +mockApplication(); + + $filePond = FilePond::widget( + [ + 'attribute' => 'array', + 'allowMultiple' => true, + 'model' => new TestForm(), + ], + ); + + $this->assertSame( + '', + $filePond, + ); + } + + public function testCssClass(): void + { + $this->mockApplication(); + + $view = Yii::$app->getView(); + + FilePond::widget( + [ + 'attribute' => 'array', + 'cssClass' => 'TestClass', + 'model' => new TestForm(), + ], + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertStringContainsString('"className":"TestClass"', $result); + } + + public function testConfig(): void + { + $this->mockApplication(); + + $view = Yii::$app->getView(); + + FilePond::widget( + [ + 'attribute' => 'array', + 'config' => ['forceRevert' => true, 'storeAsFile' => true], + 'model' => new TestForm(), + ], + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertStringContainsString('"forceRevert":true,"storeAsFile":true', $result); + } + + public function testConfigWithClassName(): void + { + $this->mockApplication(); + + $filePond = FilePond::widget( + [ + 'attribute' => 'array', + 'cssClass' => 'test-class', + 'model' => new TestForm(), + ], + ); + + $this->assertSame( + '', + $filePond, + ); + } + + public function testFileRename(): void + { + $this->mockApplication(); + + $view = Yii::$app->getView(); + + $filePond = FilePond::widget( + [ + 'attribute' => 'array', + 'fileRename' => << new TestForm(), + ], + ); + + $this->assertSame( + '', + $filePond, + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertStringContainsString('functionFileRename()', $result); + + $filePond = FilePond::widget( + [ + 'attribute' => 'array', + 'fileRename' => << << + new Promise((resolve, reject) => { + // Do custom type detection here and return with promise + resolve(type); + }), + JS, + 'model' => new TestForm(), + ], + ); + + $this->assertSame( + '', + $filePond, + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertStringContainsString('fileValidateTypeDetectType: (source, type) =>', $result); + $this->assertStringContainsString('functionFileRename()', $result); + } + + public function testLabelIdle(): void + { + $this->mockApplication(); + + $view = Yii::$app->getView(); + + FilePond::widget( + [ + 'attribute' => 'array', + 'labelIdle' => 'Drag & Drop or Browse ', + 'model' => new TestForm(), + ], + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertStringContainsString( + '"labelIdle":"Drag & Drop or Browse <\/span>"', + $result, + ); + } + + public function testMaxFiles(): void + { + $this->mockApplication(); + + $view = Yii::$app->getView(); + + FilePond::widget( + [ + 'attribute' => 'array', + 'maxFiles' => 3, + 'model' => new TestForm(), + ], + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertStringContainsString('"maxFiles":3', $result); + } + + public function testName(): void + { + $this->mockApplication(); + + $filePond = FilePond::widget( + [ + 'attribute' => 'array', + 'attributes' => ['name' => 'test-name'], + 'model' => new TestForm(), + ], + ); + + $this->assertSame( + '', + $filePond, + ); + } + + public function testPluingDefault(): void + { + $this->mockApplication(); + + $view = Yii::$app->getView(); + + FilePond::widget( + [ + 'attribute' => 'array', + 'maxFiles' => 3, + 'model' => new TestForm(), + ], + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertStringContainsString('FilePondPluginFileEncode', $result); + $this->assertStringContainsString('FilePondPluginFileValidateSize', $result); + $this->assertStringContainsString('FilePondPluginFileValidateType', $result); + $this->assertStringContainsString('FilePondPluginImageExifOrientation', $result); + $this->assertStringContainsString('FilePondPluginImagePreview', $result); + } + + public function testRender(): void + { + $this->mockApplication(); + + $filePond = FilePond::widget( + [ + 'attribute' => 'array', + 'model' => new TestForm(), + ], + ); + + $this->assertSame( + '', + $filePond, + ); + } + + public function testRequired(): void + { + $this->mockApplication(); + + $view = Yii::$app->getView(); + + $filePond = FilePond::widget( + [ + 'attribute' => 'array', + 'model' => new TestForm(), + 'required' => true, + ], + ); + + $result = $view->renderFile(__DIR__ . '/Support/main.php'); + + $this->assertSame( + '', + $filePond, + ); + $this->assertStringContainsString('"required":true', $result); + } +} diff --git a/tests/Support/TestForm.php b/tests/Support/TestForm.php new file mode 100644 index 0000000..d2c738e --- /dev/null +++ b/tests/Support/TestForm.php @@ -0,0 +1,12 @@ + +beginPage();?>1head();?>2beginBody();?>3endBody();?>4endPage(); diff --git a/tests/Support/runtime/.gitkeep b/tests/Support/runtime/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..35b7b16 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,68 @@ +destroyApplication(); + } + + protected function mockApplication(): void + { + new Application( + [ + 'id' => 'testapp', + 'aliases' => [ + '@bower' => dirname(__DIR__) . '/node_modules', + '@npm' => dirname(__DIR__) . '/node_modules', + ], + 'basePath' => __DIR__, + 'vendorPath' => dirname(__DIR__) . '/vendor', + 'components' => [ + 'assetManager' => [ + 'basePath' => __DIR__ . '/Support/runtime', + 'baseUrl' => '/', + ], + 'i18n' => [ + 'translations' => [ + 'yii.filepond' => [ + 'class' => PhpMessageSource::class, + ], + ], + ], + 'request' => [ + 'cookieValidationKey' => 'wefJDF8sfdsfSDefwqdxj9oq', + 'scriptFile' => __DIR__ . '/index.php', + 'scriptUrl' => '/index.php', + ], + ], + ], + ); + } + + /** + * Destroys application in Yii::$app by setting it to null. + */ + protected function destroyApplication() + { + Yii::$app = null; + Yii::$container = new Container(); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..3b39113 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,19 @@ +