[
  {
    "path": ".gitattributes",
    "content": "/docs               export-ignore\n/tests              export-ignore\n/.gitattributes     export-ignore\n/.gitignore         export-ignore\n/.scrutinizer.yml   export-ignore\n/.travis.yml        export-ignore\n/couscous.yml       export-ignore\n/index.md           export-ignore\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug.md",
    "content": "---\nname: 🐛 Bug\nabout: Did you encounter a bug?\n---\n\n### Bug Report\n\n<!-- Fill in the relevant information below to help triage your issue. -->\n\n|    Q        |   A\n|------------ | ------\n| BC Break    | yes/no\n| Version     | x.y.z\n\n#### Summary\n\n<!-- Provide a summary describing the problem you are experiencing. -->\n\n#### How to reproduce\n\n<!--\nProvide steps to reproduce the issue.\nIf possible, also add a code snippet.\n-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Feature_Request.md",
    "content": "---\nname: 🎉 Feature Request\nabout: Do you have a new feature in mind?\n---\n\n### Feature Request\n\n<!-- Fill in the relevant information below to help triage your issue. -->\n\n|    Q        |   A\n|------------ | ------\n| New Feature | yes/no\n| BC Break    | yes/no\n\n#### Scenario / Use-case\n\n<!-- Provide an explain in which scenario the feature would be helpful. --> \n\n#### Summary\n\n<!-- Provide a summary of the feature you would like to see implemented. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Question.md",
    "content": "---\nname: ❓ Question\nabout: Are you unsure about something?\n---\n\n### Question\n\n<!-- Fill in the relevant information below to help triage your issue. -->\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n    strategy:\n      matrix:\n        php: ['8.1', '8.2', '8.3']\n        include:\n            - php: '8.1'\n              send-to-scrutinizer: 'yes'\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n    - name: Setup PHP with fail-fast\n      uses: shivammathur/setup-php@v2\n      with:\n        send-to-scrutinizer: 'no'\n        phpunit-flags: '--no-coverage'\n        php-version: ${{ matrix.php }}\n        coverage: xdebug\n      env:\n        fail-fast: true\n    - name: Validate composer.json and composer.lock\n      run: composer validate\n\n    - name: Cache Composer packages\n      id: composer-cache\n      uses: actions/cache@v2\n      with:\n        path: vendor\n        key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}\n        restore-keys: |\n          ${{ runner.os }}-php-\n\n    - name: Install dependencies\n      if: steps.composer-cache.outputs.cache-hit != 'true'\n      run: composer install --prefer-dist --no-progress --no-suggest\n\n    - name: Run test suite\n      run: |\n        composer run stan\n        sudo composer run test\n"
  },
  {
    "path": ".github/workflows/docs.yml",
    "content": "name: Publish docs\n\non:\n  push:\n    tags:\n      - '*.*.*'\njobs:\n  docs:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v1\n\n      - name: Build docs\n        run: |\n          curl -OS https://couscous.io/couscous.phar\n          php couscous.phar generate --target=build/docs/ ./docs\n\n      - name: FTP Deployer\n        uses: sand4rt/ftp-deployer@v1.1\n        with:\n          host: ${{ secrets.DOCS_FTP_HOST }}\n          username: ${{ secrets.DOCS_FTP_USER }}\n          password: ${{ secrets.DOCS_FTP_PASSWORD }}\n          remote_folder: upload\n          # The local folder location\n          local_folder: build/docs/\n          # Remove existing files inside FTP remote folder\n          cleanup: false # optional\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.settings/\n.buildpath\n.project\nbuild/\nvendor/\ncomposer.lock\natlassian-ide-plugin.xml\ndocs/couscous.phar\ndocs/.couscous/\ncouscous.phar\nphp-cs-fixer.phar\nphpcbf.phar\nphpcs.phar\n"
  },
  {
    "path": ".php_cs.cache",
    "content": "{\"php\":\"7.2.4\",\"version\":\"2.16.1\",\"indent\":\"    \",\"lineEnding\":\"\\n\",\"rules\":{\"blank_line_after_namespace\":true,\"braces\":true,\"class_definition\":true,\"constant_case\":true,\"elseif\":true,\"function_declaration\":true,\"indentation_type\":true,\"line_ending\":true,\"lowercase_keywords\":true,\"method_argument_space\":{\"on_multiline\":\"ensure_fully_multiline\"},\"no_break_comment\":true,\"no_closing_tag\":true,\"no_spaces_after_function_name\":true,\"no_spaces_inside_parenthesis\":true,\"no_trailing_whitespace\":true,\"no_trailing_whitespace_in_comment\":true,\"single_blank_line_at_eof\":true,\"single_class_element_per_statement\":{\"elements\":[\"property\"]},\"single_import_per_statement\":true,\"single_line_after_imports\":true,\"switch_case_semicolon_to_colon\":true,\"switch_case_space\":true,\"visibility_required\":true,\"encoding\":true,\"full_opening_tag\":true},\"hashes\":{\"src\\\\Container\\\\ContainerInterface.php\":1583588918,\"src\\\\Container\\\\Local.php\":71709479,\"src\\\\Exception\\\\InvalidContainerException.php\":1907413747,\"src\\\\Exception\\\\InvalidResultException.php\":466931389,\"src\\\\Handler.php\":534695839,\"src\\\\HandlerAggregate.php\":1499917949,\"src\\\\Result\\\\Collection.php\":2897468334,\"src\\\\Result\\\\File.php\":1434642790,\"src\\\\UploadHandlerInterface.php\":2299182802,\"src\\\\Util\\\\Arr.php\":1378696061,\"src\\\\Result\\\\ResultInterface.php\":844276353,\"src\\\\Util\\\\Helper.php\":3774982220}}"
  },
  {
    "path": ".scrutinizer.yml",
    "content": "filter:\n    paths: [\"src/*\"]\ntools:\n    external_code_coverage: true\n    php_code_coverage: true\n    php_sim: true\n    php_mess_detector: true\n    php_pdepend: true\n    php_analyzer: true\n    php_cpd: true\n"
  },
  {
    "path": ".travis.yml",
    "content": "sudo: false\nlanguage: php\n\nmatrix:\n  include:\n    - php: 7.1\n      dist: bionic\n      env: DEPENDENCIES='low'\n    - php: 7.2\n      dist: bionic\n      env: DEPENDENCIES='low'\n    - php: 7.2\n      dist: bionic\n    - php: 7.3\n      dist: bionic\n    - php: 7.4\n      dist: bionic\n    - php: nightly\n      env: COMPOSER_FLAGS='--ignore-platform-reqs'\n    - php: nightly\n      env: COMPOSER_FLAGS='--ignore-platform-reqs' COMPOSER_FORCE='phpunit/phpunit ^9.0@dev'\n  fast_finish: true\n  allow_failures:\n    - php: 7.1\n    - php: nightly\n\nbefore_script:\n - composer self-update\n - composer install --prefer-source\n\nscript:\n  - mkdir -p build/logs\n  - mkdir -p build/coverage\n  - vendor/bin/phpunit -c tests/phpunit.xml\n  - cd ../\n\nafter_script:\n  - wget https://scrutinizer-ci.com/ocular.phar\n  - if [ \"$TRAVIS_PHP_VERSION\" == \"7.2\" ]; then php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml; fi\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# CHANGELOG\n\n## 2.0.0\n\n- changed the `__constructor` parameters. Now you inject an optional `Sirius\\Validation\\ValueValidator` instance instead of an `Sirius\\Validation\\ErrorMessage` instance\n- changed dependency to Sirius\\Validation~2.0"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2014 Adrian Miu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Sirius\\Upload\n\n[![Source Code](https://img.shields.io/badge/source-siriusphp/upload-blue.svg?style=flat-square)](https://github.com/siriusphp/upload)\n[![Latest Version](https://img.shields.io/packagist/v/siriusphp/upload.svg?style=flat-square)](https://github.com/siriusphp/upload/releases)\n[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/siriusphp/upload/blob/master/LICENSE)\n[![Build Status](https://github.com/siriusphp/upload/actions/workflows/ci.yml/badge.svg)](https://github.com/siriusphp/upload/actions/workflows/ci.yml)\n[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload/code-structure)\n[![Quality Score](https://img.shields.io/scrutinizer/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload)\n[![Total Downloads](https://img.shields.io/packagist/dt/siriusphp/upload.svg?style=flat-square)](https://packagist.org/packages/siriusphp/upload)\n\nFramework agnostic upload handler library.\n\n## Features\n\n1. Validates files against usual rules: extension, file size, image size (wdith, height, ratio). It uses [Sirius Validation](https://github.com/siriusphp/validation) for this purpose.\n2. Moves valid uploaded files into containers. Containers are usually local folders but you can implement your own or use other filesystem abstractions like [Gaufrette](https://github.com/KnpLabs/Gaufrette) or [Flysystem](https://github.com/FrenkyNet/Flysystem).\n3. Works with PSR7 `UploadedFileInterface` objects and with Symfony's `UploadedFile`s (see [integrations](docs/integrations.md)).\n\nUsed by [Bolt CMS](https://bolt.cm/)\n\n## Elevator pitch\n\n```php\nuse Sirius\\Upload\\Handler as UploadHandler;\n$uploadHandler = new UploadHandler('/path/to/local_folder');\n\n// validation rules\n$uploadHandler->addRule('extension', ['allowed' => ['jpg', 'jpeg', 'png']], '{label} should be a valid image (jpg, jpeg, png)', 'Profile picture');\n$uploadHandler->addRule('size', ['max' => '20M'], '{label} should have less than {max}', 'Profile picture');\n\n$result = $uploadHandler->process($_FILES['picture']); // ex: subdirectory/my_headshot.png\n\nif ($result->isValid()) {\n\t// do something with the image like attaching it to a model etc\n\ttry {\n\t\t$profile->picture = $result->name;\n\t\t$profile->save();\n\t\t$result->confirm(); // this will remove the .lock file\n\t} catch (\\Exception $e) {\n\t\t// something wrong happened, we don't need the uploaded files anymore\n\t\t$result->clear();\n\t\tthrow $e;\n\t}\n} else {\n\t// image was not moved to the container, where are error messages\n\t$messages = $result->getMessages();\n}\n```\n\n## Links\n\n- [documentation](https://www.sirius.ro/php/sirius/upload/)\n- [changelog](CHANGELOG.md)\n"
  },
  {
    "path": "autoload.php",
    "content": "<?php\nspl_autoload_register(function ($class) {\n    \n    // what namespace prefix should be recognized?\n    $prefix = 'Sirius\\Upload\\\\';\n    \n    // does the requested class match the namespace prefix?\n    $prefix_len = strlen($prefix);\n    if (substr($class, 0, $prefix_len) !== $prefix) {\n        return;\n    }\n    \n    // strip the prefix off the class\n    $class = substr($class, $prefix_len);\n    \n    // a partial filename\n    $part = str_replace('\\\\', DIRECTORY_SEPARATOR, $class) . '.php';\n    \n    // directories where we can find classes\n    $dirs = array(\n        __DIR__ . DIRECTORY_SEPARATOR . 'src',\n        __DIR__ . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'src',\n    );\n    \n    // go through the directories to find classes\n    foreach ($dirs as $dir) {\n        $file = $dir . DIRECTORY_SEPARATOR . $part;\n        if (is_readable($file)) {\n            require $file;\n            return;\n        }\n    }\n});"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"siriusphp/upload\",\n    \"description\": \"Framework agnostic upload library\",\n    \"type\": \"library\",\n    \"license\": \"MIT\",\n    \"keywords\": [\n        \"form\",\n        \"upload\",\n        \"validation\",\n        \"file\",\n        \"file upload\",\n        \"security\",\n        \"psr-7\"\n    ],\n    \"authors\": [\n        {\n            \"name\": \"Adrian Miu\",\n            \"email\": \"adrian@adrianmiu.ro\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=8.1\",\n        \"siriusphp/validation\": \"^4.0\"\n    },\n    \"require-dev\": {\n        \"laminas/laminas-diactoros\": \"^3.3\",\n        \"symfony/http-foundation\": \"^6.3\",\n        \"pestphp/pest\": \"^2.24\",\n        \"pestphp/pest-plugin-drift\": \"^2.5\",\n        \"symfony/mime\": \"^6.3\",\n        \"phpstan/phpstan\": \"^1.10\"\n    },\n    \"suggest\": {\n    \t\"league/flysystem\": \"To upload to different destinations, not just to the local file system\",\n    \t\"knplabs/gaufrette\": \"Alternative filesystem abstraction library for upload destinations\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"Sirius\\\\Upload\\\\\": \"src/\"\n        }\n    },\n    \"scripts\": {\n        \"stan\": [\n            \"php vendor/bin/phpstan analyse\"\n        ],\n        \"csfix\": [\n            \"tools/php-cs-fixer/vendor/bin/php-cs-fixer fix  --standard=PSR-2 src\"\n        ],\n        \"test\": [\n            \"php vendor/bin/pest\"\n        ],\n        \"build-docs\": [\n            \"php couscous.phar generate --target=build/docs/ ./docs\"\n        ],\n        \"docs\": [\n            \"cd docs && php ../couscous.phar preview\"\n        ]\n    },\n    \"config\": {\n        \"allow-plugins\": {\n            \"pestphp/pest-plugin\": true\n        }\n    }\n}\n"
  },
  {
    "path": "docs/cloud_upload.md",
    "content": "---\ntitle: Upload into the cloud | Sirius Upload\n---\n\n# Upload into the cloud\n\nIf you want to store uploaded files in different locations your containers must implement the `Sirius\\Upload\\Container\\ContainerInterface`.\n\nThe example below is not based on real-life code, it's for illustration purposes only.\n\n```php\n$amazonBucket = new AmazonBucket();\n$container = new AmazonContainer($amazonBucket);\n$uploadHandler = new UploadHandler($container);\n```\n\nYou can easily create upload containers on top of [Gaufrette](https://github.com/KnpLabs/Gaufrette) or [Flysystem](https://github.com/FrenkyNet/Flysystem)."
  },
  {
    "path": "docs/couscous.yml",
    "content": "template:\n    url: https://github.com/siriusphp/Template-ReadTheDocs\n\n# List of directories to exclude from the processing (default contains \"vendor\" and \"website\")\n# Paths are relative to the repository root\nexclude:\n    - website\n    - vendor\n    - test\n    - src\n    - build\n\n# Base URL of the published website (no \"/\" at the end!)\n# You are advised to set and use this variable to write your links in the HTML layouts\nbaseUrl: https://www.sirius.ro/php/sirius/upload\npaypal: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=SGXDKNJCXFPJU\ngacode: UA-535999-18\n\nprojectName: Sirius\\Upload\ntitle: Sirius\\Upload\nsubTitle: Framework agnostic upload library\n\n# The left menu bar\nmenu:\n    sections:\n#        versions:\n#            name: Versions\n#            items:\n#                two:\n#                    text: \"2.0\"\n#                    relativeUrl:\n#                one:\n#                    text: \"1.0\"\n#                    relativeUrl: 1_0/\n        guide:\n            name: Getting started\n            items:\n                getting_started:\n                    text: Introduction\n                    relativeUrl:\n                installation:\n                    text: Installation\n                    relativeUrl: installation.html\n                simple_example:\n                    text: Simple example\n                    relativeUrl: simple_example.html\n                validators:\n                    text: Validation rules\n                    relativeUrl: validation_rules.html\n                aggregator:\n                    text: Upload aggregator\n                    relativeUrl: upload_aggregator.html\n        advanced:\n            name: Advanced topics\n            items:\n                file_locking:\n                    text: File locking\n                    relativeUrl: file_locking.html\n                options:\n                    text: Upload options\n                    relativeUrl: upload_options.html\n        cookbook:\n            name: Cookbook\n            items:\n                cloud_upload:\n                    text: Uploads into the cloud\n                    relativeUrl: cloud_upload.html\n                custom_validation:\n                    text: Custom validation\n                    relativeUrl: custom_validation.html\n                integrations:\n                    text: Integrations\n                    relativeUrl: integrations.html\n"
  },
  {
    "path": "docs/custom_validation.md",
    "content": "---\ntitle: Custom upload validation | Sirius Upload\n---\n\n# Custom validation\n\nIf you need implement specific file upload validation rules remember to look at the documentation for the [Sirius\\Validation](https://www.gihub.com/siriusphp/validation) library.\n\nFor example if you have a system where the users have upload quotas and you want to make sure that they don't exceed their allocated quota you ca do the following\n\n```php\n\nfunction check_user_quota($file) {\n    return User::instance()->getRemainingQuota() < $file['size'];\n}\n\n$uploadHandler->addRule('callback', array('callback' => 'check_user_quota'), 'Sorry, but you don\\'t have enough space to upload this file');\n```\n\n# Use a custom validator\n\nYou can inject a `Sirius\\Validation\\ValueValidator` upon construction. The example below assumes there is a dependency injection container.\n\n```php\n$ruleFactory = $container->get('Sirius\\Validation\\RuleFactory');\n$ruleFactory->register('quota_is_met', 'MyApp\\Validation\\Rule\\FileQuota');\n\n$valueValidator = $container->get('Sirius\\Validation\\ValueValidator');\n\n$handler = new Sirius\\Upload\\Handler('/path/to/dir', array(), $valueValidator);\n\n$handler->addRule('quota_is_met');\n```"
  },
  {
    "path": "docs/file_locking.md",
    "content": "---\ntitle: File locking during uploads | Sirius Upload\n---\n\n# What is 'file locking'?\n\nUsually, an application accepts file uploads to store them for future use (product images, people resumes etc). \nBut from the time an uploaded file is moved to its container (the folder on disk, an S3 bucket) until the actual data is saved there are things that can go wrong (eg: the database goes down and the uploaded image cannot be attached to a model).\n\nThe `locking` functionality was implemented for this reason. Whenever a file is uploaded, on the same location another file with the `.lock` extension is created. This file is removed when the upload is confirmed.\n\nWorst case scenario (when the system breaks down so you cannot even execute the `clear()` method) you will be able to look into the upload container (local directory, S3 bucket) and \"spot\" the unused files. \n\nIf you want to take advantage of this feature you are **REQUIRED** use `confirm()` or you will end up with `.lock` files everywhere.\n\nIf you don't like it, use `$uploadHandler->setAutoconfirm(true)` and all uploaded files will automatically confirmed.\n\n"
  },
  {
    "path": "docs/index.md",
    "content": "[![Source Code](https://img.shields.io/badge/source-siriusphp/upload-blue.svg?style=flat-square)](https://github.com/siriusphp/upload)\n[![Latest Version](https://img.shields.io/packagist/v/siriusphp/upload.svg?style=flat-square)](https://github.com/siriusphp/upload/releases)\n[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](https://github.com/siriusphp/upload/blob/master/LICENSE)\n[![Build Status](https://img.shields.io/travis/siriusphp/upload/master.svg?style=flat-square)](https://travis-ci.org/siriusphp/upload)\n[![PHP 7 ready](https:////php7ready.timesplinter.ch/siriusphp/upload/master/badge.svg)](https://travis-ci.org/siriusphp/upload)\n[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload/code-structure)\n[![Quality Score](https://img.shields.io/scrutinizer/g/siriusphp/upload.svg?style=flat-square)](https://scrutinizer-ci.com/g/siriusphp/upload)\n[![Total Downloads](https://img.shields.io/packagist/dt/siriusphp/upload.svg?style=flat-square)](https://packagist.org/packages/siriusphp/upload)\n\n# Sirius Upload\n\nThis is a framework agnostic upload handler library that is flexible and easy to use.\n\n## Features\n\n1. Validates files against usual rules: extension, file size, image size (width, height, ratio). It uses [Sirius Validation](https://github.com/siriusphp/validation) for this purpose.\n2. Moves valid uploads into containers. Containers are usually local folders but you can implement your own or use other filesystem abstractions like [Gaufrette](https://github.com/KnpLabs/Gaufrette) or [Flysystem](https://github.com/FrenkyNet/Flysystem).\n3. Works with PSR7 `UploadedFileInterface` objects and with Symfony's `UploadedFile`s (see [integrations](integrations.md)).\n\nUsed by [Bolt CMS](https://bolt.cm/)\n\n## How it works\n\n1. Uploaded file is validated against the rules. By default the library will check if the upload is valid (ie: no errors during upload)\n2. The name of the uploaded file is sanitized (keep only letters, numbers and underscore). You may implement your own sanitization function if you want.\n3. If overwrite is not allowed, and a file with the same name already exists in the container, the library will prepend the timestamp to the filename.\n4. Moves the uploaded file to the container. It also create a lock file (filename + '.lock') so that we know the upload is not confirmed. See [file locking](file_locking.md)\n5. If something wrong happens in your app and you want to get rid of the uploaded file you can `clear()` the uploaded file which will remove the file and its `.lock` file. Only files that have a corresponding `.lock` file attached can be cleared\n6. If everything is in order you can `confirm` the upload. This will remove the `.lock` file attached to the upload file.\n\n## Important notes\n\n##### 1. The library makes no assumptions about the \"web availability\" of the uploaded file.\n\nMost of the times once you have a valid upload the new file will be reachable on the internet. You may upload your files to `/var/www/public/images/users/` and have the files accessible at `//cdn.domain.com/users/`. It's up to you to make your app work with the result of the upload.\n\n##### 2. You can handle multiple uploads at once if they have the same name\n\nIf you upload multiple files with the same name (eg: `<input type=\"file\" name=\"pictures[]\">`) but you have to keep in mind that the `process()` and `getMessages()` methods will return arrays\n\n```php\n$result = $uploadHandler->process($_FILES['pictures']);\n// will return a collection of files which implements \\Iterator interface\n$messages = $result->getMessages();\n// may return if the second file is not valid\narray(\n\t'1' => 'File type not accepted'\n);\n```\n\nIn this case the library normalizes the `$_FILES` array as PHP messes up the upload array.\nIt is up to you to decide what you want to do when some files fail to upload (eg: keep the valid files and discard the failed image or display error messages for the invalid images)\n"
  },
  {
    "path": "docs/installation.md",
    "content": "---\ntitle: Installing Sirius\\Upload | Sirius Upload\n---\n\n#Installation\n\n## Using composer\n\nSirius\\Validation is available on [Packagist](https://packagist.org/packages/siriusphp/upload) so you can use\n```\ncomposer require siriusphp/upload\n```\n\nMake sure to include the Composer autoload file in your project\n```php\nrequire 'vendor/autoload.php';\n```\n\n## Downloading a `.zip` file\n\nThis project is also available for download as a `.zip` file on GitHub. Visit the [releases page](https://github.com/siriusphp/upload/releases), select the version you want, and click the \"Source code (zip)\" download button.\n"
  },
  {
    "path": "docs/integrations.md",
    "content": "---\ntitle: Integrations | Sirius Upload\n---\n\n# Integrations with other libraries\n\n## PSR-7's UploadedFileInterface\n\nIf you are using a library like [Laminas Diactoros](https://github.com/laminas/laminas-diactoros) that can provide an array of objects that implement `UploadedFileInterface` from the PSR-7 standard you can do the following\n\n```php\n/** @var Sirius\\Upload\\Handler $uploadHandler */\n/** @var Laminas\\Diactoros\\ServerRequest $request */\n\n$result = $uploadHandler->process($request->getUploadedFiles());\n```\n\n## Symfony's UploadedFile\n\nIf you integrate this into a project that uses Symfony's HTTP Foundation component you can do the following:\n\n```php\n/** @var Sirius\\Upload\\Handler $uploadHandler */\n/** @var Symfony\\Component\\HttpFoundation\\Request $request */\n\n$result = $uploadHandler->process($request->files->all());\n```\n\n\n"
  },
  {
    "path": "docs/simple_example.md",
    "content": "---\ntitle: Simple upload example | Sirius Upload\n---\n\n# Simple example\n\n### Initialize the upload handler class\n\nLet's consider a simple contact form that has the following field: `name`, `email`, `phone` and `message`.\n\n```php\nuse Sirius\\Upload\\Handler as UploadHandler;\n$uploadHandler = new UploadHandler('/path/to/local_folder');\n\n// set up the validation rules\n$uploadHandler->addRule('extension', ['allowed' => 'jpg', 'jpeg', 'png'], '{label} should be a valid image (jpg, jpeg, png)', 'Profile picture');\n$uploadHandler->addRule('size', ['size' => '20M'], '{label} should be less than {size}', 'Profile picture');\n$uploadHandler->addRule('imageratio', ['ratio' => 1], '{label} should be a square image', 'Profile picture');\n\n```\n\n### Process the upload\n\n```php\n\n$result = $uploadHandler->process($_FILES['picture']); // ex: subdirectory/my_headshot.png\n\nif ($result->isValid()) {\n    try {\n\n        // do something with the image like attaching it to a model etc\n        $profile->picture = $result->name;\n        $profile->save();\n        $result->confirm(); // this will remove the .lock file\n\n    } catch (\\Exception $e) {\n\n        // something wrong happened, we don't need the uploaded files anymore\n        $result->clear();\n        throw $e;\n\n    }\n} else {\n\n    // image was not moved to the container, where are error messages\n    $messages = $result->getMessages();\n\n}\n```\n"
  },
  {
    "path": "docs/upload_aggregator.md",
    "content": "---\ntitle: The upload aggregator | Sirius Upload\n---\n\n# The upload aggregator\n\nSometimes your form may upload multiple files to the server. To reduce the number of `process()`, `clear()` and `confirm()` calls you can use an \"upload handler aggregate\"\n\n```php\nuse Sirius\\Upload\\HandlerAggregate as UploadHandlerAggregate;\n$uploadHandlerAggregate = new UploadHandlerAggregate();\n$uploadHandlerAggregate->addHandler('picture', $previouslyCreatedUploadHandlerForTheProfilePicture);\n$uploadHandlerAggregate->addHandler('resume', $previouslyCreatedUploadHandlerForTheResume);\n\n$result = $uploadHandlerAggregate->process($_FILES);\n\nif ($result->isValid()) {\n\t// do something with the image like attaching it to a model etc\n\ttry {\n\t\t$profile->picture = $result['picture']->name;\n\t\t$profile->resume = $result['resume']->name;\n\t\t$profile->save();\n\t\t$result->confirm(); // this will remove the .lock files\n\t} catch (\\Exception $e) {\n\t\t// something wrong happened, we don't need the uploaded files anymore\n\t\t$result->clear();\n\t\tthrow $e;\n\t}\n} else {\n\t// image was not moved to the container, where are error messages\n\t$messages = $result->getMessages();\n}\n```\n\nYou can see the aggregator and handlers in action in the [tests/web/index.php](https://www.github.com/siriusphp/upload/blob/master/tests/web/index.php)\n"
  },
  {
    "path": "docs/upload_options.md",
    "content": "---\ntitle: Upload options | Sirius Upload\n---\n\n#Upload options\n\nThere are a few options you can choose to use while using the Sirius\\Upload library\n\n## Configure the uploader upon construction\n\n```php\nuser \\Sirius\\Upload\\Handler;\n$uploadHandler = Handler('/path/to/dir', array(\n    Handler::OPTION_AUTOCONFIRM => true,\n    Handler::OPTION_OVERWRITE => true,\n    Handler::OPTION_PREFIX => '/subdirectory/' . time() . '_',    \n));\n```\n\n## Set options during execution\n\n#### Overwrite existing files\n\nA file is saved into the destination folder under it's own name. And there is a chance a file with that name might already be there.\nYou can choose to overwrite the existing file if you want. The library doesn't overwrite files by default.\n\n```php\n$uploadHandler->setOverwrite(true);\n```\n\n#### Auto-confirm uploads\n\nAs explained in the \"[file locking](file_locking.md)\" section, the uploaded files are `locked` and you have to manually `confirm()` the uploads to unlock them.\nYou can override this default behaviour via:\n\n```php\n$uploadHandler->setAutoconfirm(false);\n```\n\n#### Prefixing uploads\n\nSometimes you want to set up a prefix for your uploaded files (which can be a subdirectory, a timestamp etc). You can do this via:\n\n```php\n$uploadHandler->setPrefix('subdirectory/append_');\n```\n\nYou can use a function/callback as the prefix\n\n```php\nfunction upload_prefix($file_name) {\n    return substr(md5($file_name), 0, 5) . '/';\n}\n$uploadHandler->setPrefix('upload_prefix');\n```\n\n## Filename sanitization\n\nBy default the library cleans up the name of the uploaded file by preserving only letters and numbers. If you want something else you set up a sanitizer callback:\n\n```php\n$uploadHandler->setSanitizerCallback(function($name){\n    return mktime() . preg_replace('/[^a-z0-9\\.]+/', '-', strtolower($name));\n});\n```\n"
  },
  {
    "path": "docs/validation_rules.md",
    "content": "---\ntitle: File upload validation rules | Sirius Upload\n---\n\n# Upload validation rules\n\nFor validating the uploads the library uses the [Sirius\\Validation](https://www.sirius.ro/php/sirius/validation) library.\n\nYou add validation rules using the following command:\n\n```php\n$uploadHandler->addRule($ruleName, $ruleOptions, $errorMessage, $fieldLabel);\n```\n\nThe following validation rules are available.\n\n### Upload validators\n\n#### extension\n\n```php\n$uploadHandler->addRule('extension', ['allowed' => 'doc,pdf']);\n// or any other format that is understandable by the Sirius\\Validation library, like\n$uploadHandler->addRule('extension', 'allowed=doc,pdf', '{label} should be a DOC or PDF file', 'The resume');\n```\n\n#### image\n\n```php\n$uploadHandler->addRule('image', 'allowed=jpg,png');\n```\n\n#### size\n\nThe `size` option can be a number or a string like '10K', '0.5M' or '1.3G` (default: 2M)\n```php\n$uploadHandler->addRule('size', 'size=2M');\n```\n\n#### imagewidth\n\nThe options `min` and `max` are presented in pixels\n```php\n$uploadHandler->addRule('imagewidth', 'min=100&max=2000');\n```\n\n#### imageheight\n\nThe options `min` and `max` are presented in pixels\n```php\n$uploadHandler->addRule('imageheight', 'min=100&max=2000');\n```\n\n#### imageratio\n\nThe option `ratio` can be a number (eg: 1.3) or a ratio-like string (eg: 4:3, 16:9).\nThe option `error_margin` specifies how much the image is allowed to deviate from the target ratio. Default value is 0\n```php\n$uploadHandler->addRule('imageratio', 'ratio=4:3&error_margin=0.01');\n```\n\n*Note!* The upload validators use only the `tmp_name` and `name` values to perform the validation"
  },
  {
    "path": "phpstan.neon",
    "content": "parameters:\n\tlevel: 8\n\tcheckGenericClassInNonGenericObjectType: false\n\tpaths:\n\t\t- src\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/10.3/phpunit.xsd\"\n         bootstrap=\"vendor/autoload.php\"\n         colors=\"true\"\n>\n    <testsuites>\n        <testsuite name=\"Test Suite\">\n            <directory suffix=\"Test.php\">./tests</directory>\n        </testsuite>\n    </testsuites>\n    <source>\n        <include>\n            <directory suffix=\".php\">./app</directory>\n            <directory suffix=\".php\">./src</directory>\n        </include>\n    </source>\n</phpunit>\n"
  },
  {
    "path": "src/Container/ContainerInterface.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Container;\n\ninterface ContainerInterface\n{\n\n    /**\n     * Check if the container is writable\n     */\n    public function isWritable(): bool;\n\n    /**\n     * This will check if a file is in the container\n     */\n    public function has(string $file): bool;\n\n    /**\n     * Saves the $content string as a file\n     *\n     * @param string $file\n     * @param string $content\n     */\n    public function save(string $file, string $content): bool;\n\n    /**\n     * Delete the file from the container\n     */\n    public function delete(string $file): bool;\n\n    /**\n     * Moves a temporary uploaded file to a destination in the container\n     */\n    public function moveUploadedFile(string $localFile, string $destination): bool;\n}\n"
  },
  {
    "path": "src/Container/Local.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Container;\n\nclass Local implements ContainerInterface\n{\n    protected string $baseDirectory;\n\n    public function __construct(string $baseDirectory)\n    {\n        $this->baseDirectory = $this->normalizePath($baseDirectory) . DIRECTORY_SEPARATOR;\n        $this->ensureDirectory($this->baseDirectory);\n    }\n\n    protected function normalizePath(string $path): string\n    {\n        $path = dirname(rtrim($path, '\\\\/') . DIRECTORY_SEPARATOR . 'xxx');\n\n        return rtrim($path, DIRECTORY_SEPARATOR);\n    }\n\n    protected function ensureDirectory(string $directory): bool\n    {\n        if ( ! is_dir($directory)) {\n            mkdir($directory, 0755, true);\n        }\n\n        return is_dir($directory) && $this->isWritable();\n    }\n\n    /**\n     * Check if the container is writable\n     */\n    public function isWritable(): bool\n    {\n        return is_writable($this->baseDirectory);\n    }\n\n    /**\n     * This will check if a file is in the container\n     *\n     * @param string $file\n     *\n     * @return bool\n     */\n    public function has(string $file): bool\n    {\n        return $file && file_exists($this->baseDirectory . $file);\n    }\n\n    public function save(string $file, string $content): bool\n    {\n        $file = $this->normalizePath($file);\n        $dir  = dirname($this->baseDirectory . $file);\n        if ($this->ensureDirectory($dir)) {\n            return (bool)file_put_contents($this->baseDirectory . $file, $content);\n        }\n\n        return false;\n    }\n\n    public function delete(string $file): bool\n    {\n        $file = $this->normalizePath($file);\n        if (file_exists($this->baseDirectory . $file)) {\n            return unlink($this->baseDirectory . $file);\n        }\n\n        return true;\n    }\n\n    public function moveUploadedFile(string $localFile, string $destination): bool\n    {\n        $dir = dirname($this->baseDirectory . $destination);\n        if (file_exists($localFile) && $this->ensureDirectory($dir)) {\n            if (is_readable($localFile)) {\n                // rename() would be good but this is better because $localFile may become 'unwritable'\n                $result = copy($localFile, $this->baseDirectory . $destination);\n                @unlink($localFile);\n\n                return $result;\n            }\n        }\n\n        return false;\n    }\n}\n"
  },
  {
    "path": "src/Exception/InvalidContainerException.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Exception;\n\nclass InvalidContainerException extends \\RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Exception/InvalidResultException.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Exception;\n\nclass InvalidResultException extends \\RuntimeException\n{\n}\n"
  },
  {
    "path": "src/Handler.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload;\n\nuse Sirius\\Upload\\Container\\ContainerInterface;\nuse Sirius\\Upload\\Container\\Local as LocalContainer;\nuse Sirius\\Upload\\Exception\\InvalidContainerException;\nuse Sirius\\Upload\\Result\\ResultInterface;\nuse Sirius\\Upload\\Util\\Helper;\nuse Sirius\\Validation\\ValueValidator;\n\nclass Handler implements UploadHandlerInterface\n{\n    // constants for constructor options\n    const OPTION_PREFIX = 'prefix';\n    const OPTION_OVERWRITE = 'overwrite';\n    const OPTION_AUTOCONFIRM = 'autoconfirm';\n\n    // constants for validation rules\n    const RULE_EXTENSION = 'extension';\n    const RULE_SIZE = 'size';\n    const RULE_IMAGE = 'image';\n    const RULE_IMAGE_HEIGHT = 'imageheight';\n    const RULE_IMAGE_WIDTH = 'imagewidth';\n    const RULE_IMAGE_RATIO = 'imageratio';\n\n    protected ContainerInterface $container;\n\n    /**\n     * Prefix to be added to the file.\n     * It can be a subfolder (if it ends with '/', a string to be used as prefix)\n     * or a callback that returns a string\n     *\n     * @var string|callable\n     */\n    protected mixed $prefix = '';\n\n    /**\n     * When uploading a file that has the same name as a file that is\n     * already in the container should it overwrite it or use another name\n     */\n    protected bool $overwrite = false;\n\n    /**\n     * Whether or not the uploaded files are auto confirmed\n     */\n    protected bool $autoconfirm = false;\n\n    protected ?ValueValidator $validator = null;\n\n    /**\n     * @var callable\n     */\n    protected mixed $sanitizerCallback = null;\n\n    /**\n     * @param string|ContainerInterface $directoryOrContainer\n     * @param array<string, mixed> $options\n     *\n     * @throws InvalidContainerException\n     */\n    public function __construct(mixed $directoryOrContainer, array $options = [], ValueValidator $validator = null)\n    {\n        $container = $directoryOrContainer;\n        if (is_string($directoryOrContainer)) {\n            $container = new LocalContainer($directoryOrContainer);\n        }\n        if ( ! $container instanceof ContainerInterface) {\n            throw new InvalidContainerException('Destination container for uploaded files is not valid');\n        }\n        $this->container = $container;\n\n        // create the validator\n        if ( ! $validator) {\n            $validator = new ValueValidator();\n        }\n        $this->validator = $validator;\n\n        // set options\n        $availableOptions = [\n            static::OPTION_PREFIX      => 'setPrefix',\n            static::OPTION_OVERWRITE   => 'setOverwrite',\n            static::OPTION_AUTOCONFIRM => 'setAutoconfirm'\n        ];\n        foreach ($availableOptions as $key => $method) {\n            if (isset($options[$key])) {\n                $this->{$method}($options[$key]);\n            }\n        }\n    }\n\n    public function setOverwrite(bool $overwrite): self\n    {\n        $this->overwrite = (bool)$overwrite;\n\n        return $this;\n    }\n\n    /**\n     * File prefix for the upload. Can be\n     * - a folder (if it ends with /)\n     * - a string to be used as prefix\n     * - a function that returns a string\n     *\n     * @param string|callable $prefix\n     */\n    public function setPrefix(mixed $prefix): self\n    {\n        $this->prefix = $prefix;\n\n        return $this;\n    }\n\n    /**\n     * Enable/disable upload autoconfirmation\n     * Autoconfirmation does not require calling `confirm()`\n     */\n    public function setAutoconfirm(bool $autoconfirm): self\n    {\n        $this->autoconfirm = (bool)$autoconfirm;\n\n        return $this;\n    }\n\n    /**\n     * Set the sanitizer function for cleaning up the file names\n     * @throws \\InvalidArgumentException\n     */\n    public function setSanitizerCallback(callable|\\Closure $callback): self\n    {\n        if ( ! is_callable($callback)) {\n            throw new \\InvalidArgumentException('The $callback parameter is not a valid callable entity');\n        }\n        $this->sanitizerCallback = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Add validation rule (extension|size|width|height|ratio)\n     *\n     * @param array<string, mixed> $options\n     */\n    public function addRule(string $name, array $options = [], string $errorMessageTemplate = null, string $label = null): self\n    {\n        $predefinedRules = [\n            static::RULE_EXTENSION,\n            static::RULE_IMAGE,\n            static::RULE_SIZE,\n            static::RULE_IMAGE_WIDTH,\n            static::RULE_IMAGE_HEIGHT,\n            static::RULE_IMAGE_RATIO\n        ];\n        // convert to a name that is known by the default RuleFactory\n        if (in_array($name, $predefinedRules)) {\n            $name = 'upload' . $name;\n        }\n        if ($this->validator) {\n            $this->validator->add($name, $options, $errorMessageTemplate, $label);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Processes a file upload and returns an upload result file/collection\n     * @return Result\\Collection|Result\\File|ResultInterface\n     */\n    public function process(mixed $files): ResultInterface\n    {\n        $files = Helper::normalizeFiles($files);\n\n        foreach ($files as $k => $file) {\n            $files[$k] = $this->processSingleFile($file);\n        }\n\n        if (count($files) == 1) {\n            return new Result\\File(array_pop($files), $this->container);\n        }\n\n        return new Result\\Collection($files, $this->container);\n    }\n\n    /**\n     * Processes a single uploaded file\n     * - sanitize the name\n     * - validates the file\n     * - if valid, moves the file to the container\n     *\n     * @param array<string, mixed> $file\n     *\n     * @return array<string, mixed>\n     */\n    protected function processSingleFile(array $file): array\n    {\n        // store it for future reference\n        $file['original_name'] = $file['name'];\n\n        // sanitize the file name\n        $file['name'] = $this->sanitizeFileName($file['name']);\n\n        $file = $this->validateFile($file);\n        // if there are messages the file is not valid\n        if (isset($file['messages']) && $file['messages']) {\n            return $file;\n        }\n\n        // add the prefix\n        $prefix = '';\n        if (is_callable($this->prefix)) {\n            $prefix = (string)call_user_func($this->prefix, $file['name']);\n        } elseif (is_string($this->prefix)) {\n            $prefix = (string)$this->prefix;\n        }\n\n        // if overwrite is not allowed, check if the file is already in the container\n        if ( ! $this->overwrite) {\n            if ($this->container->has($prefix . $file['name'])) {\n                // add the timestamp to ensure the file is unique\n                // method is not bulletproof but it's pretty safe\n                $file['name'] = time() . '_' . $file['name'];\n            }\n        }\n\n        // attempt to move the uploaded file into the container\n        if ( ! $this->container->moveUploadedFile($file['tmp_name'], $prefix . $file['name'])) {\n            $file['name'] = false;\n\n            return $file;\n        }\n\n        $file['name'] = $prefix . $file['name'];\n        // create the lock file if autoconfirm is disabled\n        if ( ! $this->autoconfirm) {\n            $this->container->save($file['name'] . '.lock', (string)time());\n        }\n\n        return $file;\n    }\n\n    /**\n     * Validates a file according to the rules configured on the handler\n     *\n     * @param array<string, mixed> $file\n     *\n     * @return array<string, mixed>\n     */\n    protected function validateFile(array $file): array\n    {\n        if ($this->validator && ! $this->validator->validate($file)) {\n            $file['messages'] = $this->validator->getMessages();\n        }\n\n        return $file;\n    }\n\n    /**\n     * Sanitize the name of the uploaded file by stripping away bad characters\n     * and replacing \"invalid\" characters with underscore _\n     */\n    protected function sanitizeFileName(string $name): string\n    {\n        if (is_callable($this->sanitizerCallback)) {\n            return call_user_func($this->sanitizerCallback, $name);\n        }\n\n        return preg_replace('/[^A-Za-z0-9\\.]+/', '_', $name); // @phpstan-ignore-line\n    }\n}\n"
  },
  {
    "path": "src/HandlerAggregate.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload;\n\nuse Sirius\\Upload\\Result\\Collection;\nuse Sirius\\Validation\\Util\\Arr;\n\nclass HandlerAggregate implements \\IteratorAggregate\n{\n    /**\n     * @var array<string, Handler> $handlers\n     */\n    protected array $handlers = [];\n\n    /**\n     * Adds a handler on the aggregate\n     */\n    public function addHandler(string $selector, Handler $handler): self\n    {\n        $this->handlers[$selector] = $handler;\n\n        return $this;\n    }\n\n    /**\n     * @param array<string, mixed> $files\n     *\n     * @return Result\\Collection\n     */\n    public function process(mixed $files): mixed\n    {\n        $result = new Collection();\n        foreach ($this->handlers as $selector => $handler) {\n            /* @var $handler Handler */\n            $selectedFiles = Arr::getBySelector($files, $selector);\n\n            if (empty($selectedFiles)) {\n                continue;\n            }\n\n            foreach ($selectedFiles as $path => $file) {\n                if (is_array($file)) {\n                    $result[$path] = $handler->process($file);\n                }\n            }\n        }\n\n        return $result;\n    }\n\n    public function getIterator(): \\Traversable\n    {\n        return new \\ArrayIterator($this->handlers);\n    }\n}\n"
  },
  {
    "path": "src/Result/Collection.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Result;\n\nuse Sirius\\Upload\\Container\\ContainerInterface;\n\nclass Collection extends \\ArrayIterator implements ResultInterface\n{\n    /**\n     * @param array<int, mixed> $files\n     */\n    public function __construct(array $files = [], ContainerInterface $container = null)\n    {\n        $filesArray = [];\n        if ($container && ! empty($files)) {\n            foreach ($files as $key => $file) {\n                $filesArray[$key] = new File($file, $container);\n            }\n        }\n        parent::__construct($filesArray);\n    }\n\n    public function clear(): void\n    {\n        foreach ($this as $file) {\n            /* @var $file \\Sirius\\Upload\\Result\\File */\n            $file->clear();\n        }\n    }\n\n    public function confirm(): void\n    {\n        foreach ($this as $file) {\n            /* @var $file \\Sirius\\Upload\\Result\\File */\n            $file->confirm();\n        }\n    }\n\n    public function isValid(): bool\n    {\n        foreach ($this->getMessages() as $messages) {\n            if ($messages) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * @return array<string, array<string, mixed>>\n     */\n    public function getMessages(): array\n    {\n        $messages = [];\n        foreach ($this as $key => $file) {\n            /* @var $file \\Sirius\\Upload\\Result\\File */\n            $messages[$key] = $file->getMessages();\n        }\n\n        return $messages;\n    }\n}\n"
  },
  {
    "path": "src/Result/File.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Result;\n\nuse Sirius\\Upload\\Container\\ContainerInterface;\n\n/**\n * @property string $name\n */\nclass File implements ResultInterface\n{\n\n    /**\n     * Array containing the details of the uploaded file:\n     * - name (uploaded name)\n     * - original name\n     * - tmp_name\n     * etc\n     *\n     * @var array<string, mixed>\n     */\n    protected array $file;\n\n    /**\n     * The container to which this file belongs to\n     */\n    protected ContainerInterface $container;\n\n    /**\n     * @param array<string, mixed> $file\n     * @param ContainerInterface $container\n     */\n    public function __construct(array $file, ContainerInterface $container)\n    {\n        $this->file      = $file;\n        $this->container = $container;\n    }\n\n    /**\n     * Returns if the uploaded file is valid\n     *\n     * @return bool\n     */\n    public function isValid(): bool\n    {\n        return $this->file['name'] && count($this->getMessages()) === 0;\n    }\n\n    /**\n     * Returns the validation error messages\n     *\n     * @return array<string, mixed>\n     */\n    public function getMessages(): array\n    {\n        if (isset($this->file['messages'])) {\n            return $this->file['messages'];\n        } else {\n            return [];\n        }\n    }\n\n    /**\n     * The file that was saved during process() and has a .lock file attached\n     * will be cleared, in case the form processing fails\n     */\n    public function clear(): void\n    {\n        $this->container->delete($this->name);\n        $this->container->delete($this->name . '.lock');\n        $this->file['name'] = null;\n    }\n\n    /**\n     * Remove the .lock file attached to the file that was saved during process()\n     * This should happen if the form fails validation/processing\n     */\n    public function confirm(): void\n    {\n        $this->container->delete($this->name . '.lock');\n    }\n\n    public function __get(string $name): mixed\n    {\n        if (isset($this->file[$name])) {\n            return $this->file[$name];\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Result/ResultInterface.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Result;\n\ninterface ResultInterface\n{\n    /**\n     * Returns if the uploaded file is valid\n     *\n     * @return bool\n     */\n    public function isValid(): bool;\n\n    /**\n     * Returns the validation error messages\n     *\n     * @return array<string, mixed>\n     */\n    public function getMessages(): array;\n\n    /**\n     * The file that was saved during process() and has a .lock file attached\n     * will be cleared, in case the form processing fails\n     */\n    public function clear(): void;\n\n    /**\n     * Remove the .lock file attached to the file that was saved during process()\n     * This should happen if the form fails validation/processing\n     */\n    public function confirm(): void;\n}\n"
  },
  {
    "path": "src/UploadHandlerInterface.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload;\n\nuse Psr\\Http\\Message\\UploadedFileInterface;\nuse Sirius\\Upload\\Result\\ResultInterface;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\ninterface UploadHandlerInterface\n{\n\n    /**\n     * This function will process the files received from $_FILES,\n     * validate them and save them into the container.\n     *\n     * Along with the file saved into the container a .lock file should\n     * be added by the container save() method so, in case the form is\n     * not validated, the uploaded file will be removed.\n     *\n     * @param array<string, mixed>|UploadedFileInterface|UploadedFile $files\n     *\n     * @return Result\\Collection|Result\\File|ResultInterface\n     */\n    public function process(mixed $files): mixed;\n}\n"
  },
  {
    "path": "src/Util/Helper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace Sirius\\Upload\\Util;\n\nuse Psr\\Http\\Message\\UploadedFileInterface;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\nclass Helper\n{\n    const PSR7_UPLOADED_FILE_CLASS = '\\Psr\\Http\\Message\\UploadedFileInterface';\n    const SYMFONY_UPLOADED_FILE_CLASS = 'Symfony\\Component\\HttpFoundation\\File\\UploadedFile';\n\n    /**\n     * We do not type-hint or import the class since it may not be used\n     * @return array<string, mixed>\n     */\n    public static function extractFromUploadedFileInterface(UploadedFileInterface $file): array\n    {\n        $tempName = tempnam(sys_get_temp_dir(), 'srsupld_');\n        if (!$tempName) {\n            throw new \\RuntimeException('Could not create temporary directory');\n        }\n        $file->moveTo($tempName);\n        $result = [\n            'name'     => $file->getClientFilename(),\n            'tmp_name' => $tempName,\n            'type'     => $file->getClientMediaType(),\n            'error'    => $file->getError(),\n            'size'     => $file->getSize()\n        ];\n\n        return $result;\n    }\n\n    /**\n     * @return array<string, mixed>\n     */\n    public static function extractFromSymfonyFile(UploadedFile $file): array\n    {\n        $result = [\n            'name'     => $file->getClientOriginalName(),\n            'tmp_name' => $file->getPathname(),\n            'type'     => $file->getMimeType(),\n            'error'    => $file->getError(),\n            'size'     => $file->getSize()\n        ];\n\n        return $result;\n    }\n\n    /**\n     * @param array<string, mixed> $files\n     *\n     * @return array<string|int, mixed>\n     */\n    public static function remapFilesArray(array $files): array\n    {\n        $result = [];\n        foreach (array_keys($files['name']) as $k) {\n            $result[$k] = [\n                'name'     => $files['name'][$k],\n                'type'     => $files['type'][$k] ?? null,\n                'size'     => $files['size'][$k] ?? null,\n                'error'    => $files['error'][$k] ?? null,\n                'tmp_name' => $files['tmp_name'][$k] ?? null\n            ];\n        }\n\n        return $result;\n    }\n\n    /**\n     * Fixes the $_FILES array problem and ensures the result is an array of files\n     *\n     * PHP's $_FILES variable is not properly formatted for iteration when\n     * multiple files are uploaded under the same name\n     * @see https://www.php.net/manual/en/features.file-upload.php\n     *\n     * @param array<string, mixed|UploadedFile|UploadedFileInterface>|UploadedFile|UploadedFileInterface $files\n     *\n     * @return array<int, mixed>\n     */\n    public static function normalizeFiles(mixed $files): array\n    {\n        if (empty($files)) {\n            return [];\n        }\n\n        if (is_object($files)) {\n            if (is_subclass_of($files, self::PSR7_UPLOADED_FILE_CLASS)) {\n                return [self::extractFromUploadedFileInterface($files)];\n            }\n            if (get_class($files) == self::SYMFONY_UPLOADED_FILE_CLASS) {\n                return [self::extractFromSymfonyFile($files)];\n            }\n        }\n\n        // If caller passed in an array of objects (Either PSR7 or Symfony)\n        if (is_array($files) && is_object(reset($files))) {\n            $firstFile = reset($files);\n            if ($firstFile instanceof UploadedFileInterface) {\n                $result = [];\n                foreach ($files as $file) {\n                    $result[] = self::extractFromUploadedFileInterface($file);\n                }\n\n                return $result;\n            }\n\n            if ($firstFile instanceof UploadedFile) {\n                $result = [];\n                foreach ($files as $file) {\n                    $result[] = self::extractFromSymfonyFile($file);\n                }\n\n                return $result;\n            }\n        }\n\n        // The caller passed $_FILES['some_field_name']\n        if (isset($files['name'])) {\n            // we have a single file\n            if ( ! is_array($files['name'])) {\n                return [$files];\n            } else {\n                // we have list of files, which PHP messes up\n                return Helper::remapFilesArray($files); // @phpstan-ignore-line\n            }\n        } else {\n            // The caller passed $_FILES\n            $keys = array_keys($files); // @phpstan-ignore-line\n            if (isset($keys[0]) && isset($files[$keys[0]]['name'])) {\n                if ( ! is_array($files[$keys[0]]['name'])) {\n                    // $files is in the correct format already, even in the\n                    // case it contains a single element.\n                    return $files; //@phpstan-ignore-line\n                } else {\n                    // we have list of files, which PHP messes up\n                    return Helper::remapFilesArray($files[$keys[0]]); // @phpstan-ignore-line\n                }\n            }\n        }\n\n        // If we got here, the $file argument is wrong\n        return [];\n    }\n}\n"
  },
  {
    "path": "tests/.phpunit.result.cache",
    "content": "C:37:\"PHPUnit\\Runner\\DefaultTestResultCache\":2714:{a:2:{s:7:\"defects\";a:16:{s:64:\"Sirius\\Upload\\HandlerTest::testExceptionTrwonForInvalidContainer\";i:4;s:76:\"Sirius\\Upload\\HandlerTest::testExceptionThrownForInvalidSanitizationCallback\";i:4;s:47:\"Sirius\\Upload\\HandlerAggregateTest::testProcess\";i:4;s:48:\"Sirius\\Upload\\HandlerTest::testPsr7UploadedFiles\";i:3;s:51:\"Sirius\\Upload\\HandlerTest::testSymfonyUploadedFiles\";i:4;s:52:\"Sirius\\Upload\\HandlerTest::testBasicUploadWithPrefix\";i:4;s:46:\"Sirius\\Upload\\HandlerTest::testUploadOverwrite\";i:4;s:48:\"Sirius\\Upload\\HandlerTest::testUploadAutoconfirm\";i:4;s:55:\"Sirius\\Upload\\HandlerTest::testSingleUploadConfirmation\";i:4;s:51:\"Sirius\\Upload\\HandlerTest::testSingleUploadClearing\";i:4;s:42:\"Sirius\\Upload\\HandlerTest::testMultiUpload\";i:4;s:50:\"Sirius\\Upload\\HandlerTest::testOriginalMultiUpload\";i:4;s:46:\"Sirius\\Upload\\HandlerTest::testWrongFilesArray\";i:4;s:53:\"Sirius\\Upload\\HandlerTest::testSingleUploadValidation\";i:4;s:52:\"Sirius\\Upload\\HandlerTest::testMultiUploadValidation\";i:4;s:57:\"Sirius\\Upload\\HandlerTest::testCustomSanitizationCallback\";i:4;}s:5:\"times\";a:24:{s:43:\"Sirius\\Upload\\Container\\LocalTest::testSave\";d:0.03;s:45:\"Sirius\\Upload\\Container\\LocalTest::testDelete\";d:0.011;s:59:\"Sirius\\Upload\\Container\\LocalTest::testDeleteInexistingFile\";d:0.007;s:55:\"Sirius\\Upload\\Container\\LocalTest::testMoveUploadedFile\";d:0.014;s:62:\"Sirius\\Upload\\Container\\LocalTest::testMoveMissingUploadedFile\";d:0.007;s:47:\"Sirius\\Upload\\HandlerAggregateTest::testProcess\";d:0.1;s:48:\"Sirius\\Upload\\HandlerAggregateTest::testIterator\";d:0.024;s:52:\"Sirius\\Upload\\HandlerTest::testBasicUploadWithPrefix\";d:0.019;s:46:\"Sirius\\Upload\\HandlerTest::testUploadOverwrite\";d:0.042;s:48:\"Sirius\\Upload\\HandlerTest::testUploadAutoconfirm\";d:0.018;s:55:\"Sirius\\Upload\\HandlerTest::testSingleUploadConfirmation\";d:0.024;s:51:\"Sirius\\Upload\\HandlerTest::testSingleUploadClearing\";d:0.024;s:42:\"Sirius\\Upload\\HandlerTest::testMultiUpload\";d:0.038;s:50:\"Sirius\\Upload\\HandlerTest::testOriginalMultiUpload\";d:0.032;s:46:\"Sirius\\Upload\\HandlerTest::testWrongFilesArray\";d:0.012;s:64:\"Sirius\\Upload\\HandlerTest::testExceptionTrwonForInvalidContainer\";d:0.013;s:53:\"Sirius\\Upload\\HandlerTest::testSingleUploadValidation\";d:0.017;s:52:\"Sirius\\Upload\\HandlerTest::testMultiUploadValidation\";d:0.018;s:57:\"Sirius\\Upload\\HandlerTest::testCustomSanitizationCallback\";d:0.022;s:76:\"Sirius\\Upload\\HandlerTest::testExceptionThrownForInvalidSanitizationCallback\";d:0.012;s:48:\"Sirius\\Upload\\HandlerTest::testPsr7UploadedFiles\";d:0.045;s:51:\"Sirius\\Upload\\HandlerTest::testSymfonyUploadedFiles\";d:0.07;s:56:\"Sirius\\Upload\\HandlerTest::testSingleSymfonyUploadedFile\";d:0.03;s:53:\"Sirius\\Upload\\HandlerTest::testSinglePsr7UploadedFile\";d:0.026;}}}"
  },
  {
    "path": "tests/Pest.php",
    "content": "<?php\n\n/*\n|--------------------------------------------------------------------------\n| Test Case\n|--------------------------------------------------------------------------\n|\n| The closure you provide to your test functions is always bound to a specific PHPUnit test\n| case class. By default, that class is \"PHPUnit\\Framework\\TestCase\". Of course, you may\n| need to change it using the \"uses()\" function to bind a different classes or traits.\n|\n*/\nrequire_once __DIR__ . '/TestCase.php';\n uses(Tests\\TestCase::class)->in('src');\n\n/*\n|--------------------------------------------------------------------------\n| Expectations\n|--------------------------------------------------------------------------\n|\n| When you're writing tests, you often need to check that values meet certain conditions. The\n| \"expect()\" function gives you access to a set of \"expectations\" methods that you can use\n| to assert different things. Of course, you may extend the Expectation API at any time.\n|\n*/\n\nexpect()->extend('toBeOne', function () {\n    return $this->toBe(1);\n});\n\n/*\n|--------------------------------------------------------------------------\n| Functions\n|--------------------------------------------------------------------------\n|\n| While Pest is very powerful out-of-the-box, you may have some testing code specific to your\n| project that you don't want to repeat in every file. Here you can also expose helpers as\n| global functions to help you to reduce the number of lines of code in your test files.\n|\n*/\n\nfunction something()\n{\n    // ..\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nnamespace Tests;\n\nuse PHPUnit\\Framework\\TestCase as BaseTestCase;\n\nabstract class TestCase extends BaseTestCase\n{\n    function createTemporaryFile($name, $content = \"\")\n    {\n        file_put_contents($this->tmpFolder . '/' . $name, $content);\n    }\n}\n"
  },
  {
    "path": "tests/phpunit.xml",
    "content": "<phpunit bootstrap=\"phpunit_bootstrap.php\"\n         backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         colors=\"true\"\n         verbose=\"false\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\"\n         stopOnError=\"false\">\n    <logging>\n        <log type=\"coverage-clover\" target=\"../build/logs/clover.xml\"/>\n        <log type=\"coverage-html\" target=\"../build/coverage/\"/>\n    </logging>\n    <filter>\n        <whitelist addUncoveredFilesFromWhitelist=\"true\">\n            <directory suffix=\".php\">./../src/</directory>\n        </whitelist>\n\t</filter>\n    <testsuites>\n        <testsuite name=\"Sirius Upload Test Suite\">\n            <directory>./src/</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "tests/phpunit_bootstrap.php",
    "content": "<?php\n\nrequire_once(__DIR__ . '/../vendor/autoload.php');\n\nerror_reporting(E_ERROR);\n"
  },
  {
    "path": "tests/src/Container/LocalTest.php",
    "content": "<?php\n\nuse Sirius\\Upload\\Container\\Local as LocalContainer;\n\n\nfunction rrmdir($dir)\n{\n    if (is_dir($dir)) {\n        $objects = scandir($dir);\n        foreach ($objects as $object) {\n            if ($object != \".\" && $object != \"..\") {\n                if (filetype($dir . \"/\" . $object) == \"dir\") {\n                    rrmdir($dir . \"/\" . $object);\n                } else {\n                    unlink($dir . \"/\" . $object);\n                }\n            }\n        }\n        reset($objects);\n        rmdir($dir);\n    }\n}\n\nbeforeEach(function () {\n    $this->dir = realpath(__DIR__ . '/../../') . '/fixture/';\n    $this->container = new LocalContainer($this->dir);\n});\n\nafterEach(function () {\n    rrmdir($this->dir);\n});\n\ntest('save', function () {\n    $file = 'subdir/test.txt';\n    expect($this->container->save($file, 'cool'))->toBeTrue();\n    expect(file_exists($this->dir . $file))->toBeTrue();\n    expect($this->container->has($file))->toBeTrue();\n    expect(file_get_contents($this->dir . $file))->toEqual('cool');\n});\n\ntest('delete', function () {\n    $file = 'subdir/test.txt';\n    $this->container->save($file, 'cool');\n    expect(file_exists($this->dir . $file))->toBeTrue();\n    expect($this->container->delete($file))->toBeTrue();\n    expect(file_exists($this->dir . $file))->toBeFalse();\n});\n\ntest('delete inexisting file', function () {\n    $file = 'subdir/test.txt';\n    expect($this->container->delete($file))->toBeTrue();\n});\n\ntest('move uploaded file', function () {\n    $file = 'test.txt';\n    $file2 = 'sub/test.txt';\n    $this->container->save($file, 'cool');\n    expect($this->container->moveUploadedFile($this->dir . $file, $file2))->toBeTrue();\n    expect(file_get_contents($this->dir . $file2))->toEqual('cool');\n});\n\ntest('move missing uploaded file', function () {\n    $file = 'subdir/test.txt';\n    expect($this->container->moveUploadedFile($this->dir . $file, $file))->toBeFalse();\n});\n"
  },
  {
    "path": "tests/src/HandlerAggregateTest.php",
    "content": "<?php\n\nuse \\Sirius\\Upload\\Handler;\nuse \\Sirius\\Upload\\HandlerAggregate;\n\nbeforeEach(function () {\n    $this->tmpFolder = realpath(__DIR__ . '/../fixitures/');\n    if (!is_dir($this->tmpFolder)) {\n        @mkdir($this->tmpFolder . '/container');\n    }\n    $this->uploadFolder = realpath(__DIR__ . '/../fixitures/container/');\n\n    $this->agg = new HandlerAggregate();\n    $this->agg->addHandler(\n        'user_picture',\n        new Handler(\n            $this->uploadFolder . '/user_picture', array(\n                Handler::OPTION_PREFIX => '',\n                Handler::OPTION_OVERWRITE => false,\n                Handler::OPTION_AUTOCONFIRM => false\n            )\n        )\n    );\n    $this->agg->addHandler(\n        'resume',\n        new Handler(\n            $this->uploadFolder . '/resume', array(\n                Handler::OPTION_PREFIX => '',\n                Handler::OPTION_OVERWRITE => false,\n                Handler::OPTION_AUTOCONFIRM => false\n            )\n        )\n    );\n    $this->agg->addHandler(\n        'portfolio[photos]',\n        new Handler(\n            $this->uploadFolder . '/photo', array(\n                Handler::OPTION_PREFIX => '',\n                Handler::OPTION_OVERWRITE => false,\n                Handler::OPTION_AUTOCONFIRM => false\n            )\n        )\n    );\n});\n\nafterEach(function () {\n    $files = glob($this->uploadFolder . '/*');\n    // get all file names\n    foreach ($files as $file) { // iterate files\n        if (is_file($file)) {\n            unlink($file);\n        } // delete file\n    }\n});\n\ntest('process', function () {\n    $this->createTemporaryFile('abc.tmp');\n    $this->createTemporaryFile('def.tmp');\n    $files = array(\n        'user_picture' => array(\n            'name' => 'pic.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        ),\n        'resume' => array(\n            'name' => 'resume.doc',\n            'tmp_name' => $this->tmpFolder . '/def.tmp'\n        )\n    );\n    $result = $this->agg->process($files);\n\n    expect(file_exists($this->uploadFolder . '/user_picture/' . $result['user_picture']->name))->toBeTrue();\n    expect(file_exists($this->uploadFolder . '/user_picture/' . $result['user_picture']->name . '.lock'))->toBeTrue();\n\n    $result->confirm();\n    expect(file_exists($this->uploadFolder . '/user_picture/' . $result['user_picture']->name . '.lock'))->toBeFalse();\n});\n\ntest('iterator', function () {\n    $handlers = $this->agg->getIterator();\n    expect($handlers['user_picture'] instanceof Handler)->toBeTrue();\n});\n"
  },
  {
    "path": "tests/src/HandlerTest.php",
    "content": "<?php\n\nuse Laminas\\Diactoros\\StreamFactory;\nuse \\Sirius\\Upload\\Handler;\nuse Laminas\\Diactoros\\UploadedFile;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile as SymfonyUploadedFile;\n\n\nbeforeEach(function () {\n    $this->tmpFolder = realpath(__DIR__ . '/../fixitures/');\n    if (!is_dir($this->tmpFolder)) {\n        @mkdir($this->tmpFolder . '/container');\n    }\n    $this->uploadFolder = realpath(__DIR__ . '/../fixitures/container/');\n    $this->handler      = new Handler(\n        $this->uploadFolder, array(\n            Handler::OPTION_PREFIX      => '',\n            Handler::OPTION_OVERWRITE   => false,\n            Handler::OPTION_AUTOCONFIRM => false\n        )\n    );\n});\n\nafterEach(function () {\n    $files = glob($this->uploadFolder . '/*');\n    // get all file names\n    foreach ($files as $file) { // iterate files\n        if (is_file($file)) {\n            unlink($file);\n        } // delete file\n    }\n});\n\ntest('basic upload with prefix', function () {\n    $this->handler->setPrefix('subfolder/');\n    $this->createTemporaryFile('abc.tmp');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();\n\n    // tearDown does not clean the subfolders\n    unlink($this->uploadFolder . '/' . $result->name);\n    unlink($this->uploadFolder . '/' . $result->name . '.lock');\n});\n\ntest('upload overwrite', function () {\n    $this->createTemporaryFile('abc.tmp', 'first_file');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect('first_file')->toEqual(file_get_contents($this->uploadFolder . '/abc.jpg'));\n\n    // no overwrite, the first upload should be preserved\n    $this->handler->setOverwrite(false);\n    $this->createTemporaryFile('abc.tmp', 'second_file');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect('first_file')->toEqual(file_get_contents($this->uploadFolder . '/abc.jpg'));\n\n    // overwrite, the first uploaded file should be changed\n    $this->handler->setOverwrite(true);\n    $this->createTemporaryFile('abc.tmp', 'second_file');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect('second_file')->toEqual(file_get_contents($this->uploadFolder . '/abc.jpg'));\n});\n\ntest('upload autoconfirm', function () {\n    $this->handler->setAutoconfirm(true);\n    $this->createTemporaryFile('abc.tmp', 'first_file');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();\n});\n\ntest('single upload confirmation', function () {\n    $this->createTemporaryFile('abc.tmp', 'first_file');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();\n\n    $result->confirm();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();\n});\n\ntest('single upload clearing', function () {\n    $this->createTemporaryFile('abc.tmp', 'first_file');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();\n\n    $fileName = $result->name;\n    $result->clear();\n\n    expect(file_exists($this->uploadFolder . '/' . $fileName))->toBeFalse();\n    expect(file_exists($this->uploadFolder . '/' . $fileName . '.lock'))->toBeFalse();\n});\n\ntest('multi upload', function () {\n    $this->createTemporaryFile('abc.tmp', 'first_file');\n    $this->createTemporaryFile('def.tmp', 'first_file');\n\n    // array is already properly formatted\n    $result = $this->handler->process(\n        array(\n            array(\n                'name'     => 'abc.jpg',\n                'tmp_name' => $this->tmpFolder . '/abc.tmp'\n            ),\n            array(\n                'name'     => 'def.jpg',\n                'tmp_name' => $this->tmpFolder . '/def.tmp'\n            )\n        )\n    );\n\n    expect($result->isValid())->toBeTrue();\n\n    #        var_dump(glob($this->uploadFolder . '/*'));\n    foreach ($result as $file) {\n        expect(file_exists($this->uploadFolder . '/' . $file->name))->toBeTrue();\n        expect(file_exists($this->uploadFolder . '/' . $file->name . '.lock'))->toBeTrue();\n    }\n\n    // confirmation removes the .lock files\n    $result->confirm();\n    foreach ($result as $file) {\n        expect(file_exists($this->uploadFolder . '/' . $file->name))->toBeTrue();\n        expect(file_exists($this->uploadFolder . '/' . $file->name . '.lock'))->toBeFalse();\n    }\n\n    // clearing removes the uploaded files and their locks (which are already removed)\n    $result->clear();\n    foreach ($result as $file) {\n        expect($file->name)->toBeNull();\n    }\n});\n\ntest('original multi upload', function () {\n    $this->createTemporaryFile('abc.tmp', 'first_file');\n    $this->createTemporaryFile('def.tmp', 'first_file');\n\n    // array is as provided by PHP\n    $result = $this->handler->process(\n        array(\n            'name'     => array(\n                'abc.jpg',\n                'def.jpg',\n            ),\n            'tmp_name' => array(\n                $this->tmpFolder . '/abc.tmp',\n                $this->tmpFolder . '/def.tmp'\n            ),\n        )\n    );\n\n    expect(2)->toEqual(count($result));\n    foreach ($result as $file) {\n        expect(file_exists($this->uploadFolder . '/' . $file->name))->toBeTrue();\n        expect(file_exists($this->uploadFolder . '/' . $file->name . '.lock'))->toBeTrue();\n    }\n});\n\ntest('wrong files array', function () {\n    $result = $this->handler->process(array('names' => 'abc.jpg'));\n    expect(0)->toEqual(count($result));\n});\n\ntest('exception trwon for invalid container', function () {\n    $this->expectException('Sirius\\Upload\\Exception\\InvalidContainerException');\n\n    $handler = new Handler(new \\stdClass());\n});\n\ntest('single upload validation', function () {\n    $this->createTemporaryFile('abc.tmp', 'non image file');\n\n    // uploaded files must be an image\n    $this->handler->addRule(Handler::RULE_IMAGE);\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'abc.jpg',\n            'tmp_name' => $this->tmpFolder . '/abc.tmp'\n        )\n    );\n\n    expect($result->isValid())->toBeFalse();\n    expect(1)->toEqual(count($result->getMessages()));\n    expect($result->nonAttribute)->toBeNull();\n});\n\ntest('multi upload validation', function () {\n\n    $this->createTemporaryFile('abc.tmp', 'first_file');\n    $this->createTemporaryFile('def.tmp', 'second_file');\n\n    // uploaded file must be an image\n    $this->handler->addRule(Handler::RULE_IMAGE);\n\n    // array is as provided by PHP\n    $result   = $this->handler->process(\n        array(\n            'name'     => array(\n                'abc.jpg',\n                'def.jpg',\n            ),\n            'tmp_name' => array(\n                $this->tmpFolder . '/abc.tmp',\n                $this->tmpFolder . '/def.tmp'\n            ),\n        )\n    );\n    $messages = $result->getMessages();\n\n    expect($result->isValid())->toBeFalse();\n    expect(2)->toEqual(count($messages));\n    expect(1)->toEqual(count($messages[0]));\n});\n\ntest('custom sanitization callback', function () {\n    $this->handler->setSanitizerCallback(function ($name) {\n        return preg_replace('/[^A-Za-z0-9\\.]+/', '-', strtolower($name));\n    });\n    $this->createTemporaryFile('ABC 123.tmp', 'non image file');\n\n    $result = $this->handler->process(\n        array(\n            'name'     => 'ABC 123.tmp',\n            'tmp_name' => $this->tmpFolder . '/ABC 123.tmp'\n        )\n    );\n\n    expect(file_exists($this->uploadFolder . '/abc-123.tmp'))->toBeTrue();\n});\n\ntest('psr7 uploaded files', function () {\n    $files = ['abc.tmp', 'def.tmp'];\n\n    $psr7Files = [];\n\n    foreach ($files as $file) {\n        $this->createTemporaryFile($file, 'first_file');\n\n        $factory     = new StreamFactory();\n        $stream      = $factory->createStreamFromFile($this->tmpFolder . '/' . $file);\n        $psr7Files[] = new UploadedFile(\n            $stream,\n            $stream->getSize(),\n            UPLOAD_ERR_OK,\n            $file\n        );\n    }\n\n\n    $result = $this->handler->process($psr7Files);\n\n    foreach ($result as $item) {\n        expect(file_exists($this->uploadFolder . '/' . $item->name))->toBeTrue();\n        expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeTrue();\n\n        $item->confirm();\n        expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeFalse();\n    }\n});\n\ntest('single psr7 uploaded file', function () {\n    $file = 'abc.tmp';\n\n    $this->createTemporaryFile($file, 'first_file');\n\n    $factory  = new StreamFactory();\n    $stream   = $factory->createStreamFromFile($this->tmpFolder . '/' . $file);\n    $psr7File = new UploadedFile(\n        $stream,\n        $stream->getSize(),\n        UPLOAD_ERR_OK,\n        $file\n    );\n\n    $result = $this->handler->process($psr7File);\n\n    expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();\n\n    $result->confirm();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();\n});\n\ntest('symfony uploaded files', function () {\n    $files = ['abc.tmp', 'def.tmp'];\n\n    $symfonyFiles = [];\n\n    foreach ($files as $file) {\n        $this->createTemporaryFile($file, 'first_file');\n\n        $symfonyFiles[] = new SymfonyUploadedFile($this->tmpFolder . '/' . $file, $file);\n    }\n\n    $result = $this->handler->process($symfonyFiles);\n\n    foreach ($result as $item) {\n        expect(file_exists($this->uploadFolder . '/' . $item->name))->toBeTrue();\n        expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeTrue();\n\n        $item->confirm();\n        expect(file_exists($this->uploadFolder . '/' . $item->name . '.lock'))->toBeFalse();\n    }\n});\n\ntest('single symfony uploaded file', function () {\n    $file = 'abc.tmp';\n\n    $this->createTemporaryFile($file, 'first_file');\n\n    $symfonyFile = new SymfonyUploadedFile($this->tmpFolder . '/' . $file, $file);\n\n    $result = $this->handler->process($symfonyFile);\n\n    expect(file_exists($this->uploadFolder . '/' . $result->name))->toBeTrue();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeTrue();\n\n    $result->confirm();\n    expect(file_exists($this->uploadFolder . '/' . $result->name . '.lock'))->toBeFalse();\n});\n"
  },
  {
    "path": "tests/web/index.php",
    "content": "<?php\ninclude('../../autoload.php');\ninclude('../../vendor/autoload.php');\n\n$destination = realpath('../fixitures/container');\n\n$pictureHandler = new Sirius\\Upload\\Handler($destination);\n$pictureHandler->addRule(Sirius\\Upload\\Handler::RULE_IMAGE, array('allowed' => array('jpg', 'png')));\n\n$resumeHandler = new Sirius\\Upload\\Handler($destination);\n$resumeHandler->addRule(Sirius\\Upload\\Handler::RULE_EXTENSION, array('allowed' => array('doc', 'docx', 'pdf')));\n\n$upload = new Sirius\\Upload\\HandlerAggregate();\n$upload->addHandler('picture', $pictureHandler);\n$upload->addHandler('resume', $resumeHandler);\n\n$result = false;\nif ($_FILES) {\n    $result = $upload->process($_FILES);\n}\n\n?>\n<!DOCTYPE html>\n<html>\n<head lang=\"en\">\n    <meta charset=\"UTF-8\">\n    <title>Sirius\\Upload test</title>\n</head>\n<body>\n\n<?php if ($result) { ?>\n\n    <?php foreach ($result as $key => $file) { ?>\n        <h3><?php echo $key ?> upload results:</h3>\n        <?php if ($file->isValid()) { ?>\n            SUCCESS! Uploaded as <?php echo $file->name ?>\n        <?php } else { ?>\n            ERROR! Error messages:<br/>\n            <?php echo implode('<br>', $file->getMessages());?>\n        <?php } ?>\n    <?php } ?>\n\n<?php } else { ?>\n\n    <form method=\"post\" enctype=\"multipart/form-data\">\n    <div>Profile picture: <input type=\"file\" name=\"picture\"></div>\n    <div>Resume: <input type=\"file\" name=\"resume\"></div>\n    <input type=\"submit\" value=\"Send\">\n</form>\n\n<?php } ?>\n\n</body>\n</html>"
  }
]