diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..48b8bf9
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+vendor/
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..3d9ead7
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,13 @@
+name: DX Scanner
+on: push
+jobs:
+ dx-scanner:
+ runs-on: ubuntu-latest
+ container: dxheroes/dx-scanner:latest
+ steps:
+ - uses: actions/checkout@v1
+ - name: Runs DX Scanner on the code
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
+ DXSCANNER_API_TOKEN: ${{ secrets.DXSCANNER_API_TOKEN }}
+ run: dx-scanner run --ci
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..eae43cc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+/vendor
+/.idea
+/data
+/tests/functional/*/source/data/in/state.json
+/tests/functional/*/expected/data/out/state.json
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..a55e7a1
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/linearb-test.iml b/.idea/linearb-test.iml
new file mode 100644
index 0000000..c956989
--- /dev/null
+++ b/.idea/linearb-test.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..85c3590
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..b9c5ce8
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1610610013444
+
+
+ 1610610013444
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..0b4cee4
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,33 @@
+sudo: required
+
+language: bash
+
+services:
+ - docker
+
+before_script:
+ - export APP_IMAGE=keboola/ex-ftp
+ - docker-compose -v
+ - docker-compose build
+ - docker-compose run app composer ci
+
+ # push test image to ECR
+ - docker pull quay.io/keboola/developer-portal-cli-v2:latest
+ - export REPOSITORY=`docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD -e KBC_DEVELOPERPORTAL_URL quay.io/keboola/developer-portal-cli-v2:latest ecr:get-repository $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP`
+ - docker tag $APP_IMAGE:latest $REPOSITORY:test
+ - eval $(docker run --rm -e KBC_DEVELOPERPORTAL_USERNAME -e KBC_DEVELOPERPORTAL_PASSWORD -e KBC_DEVELOPERPORTAL_URL quay.io/keboola/developer-portal-cli-v2:latest ecr:get-login $KBC_DEVELOPERPORTAL_VENDOR $KBC_DEVELOPERPORTAL_APP)
+ - docker push $REPOSITORY:test
+ - docker pull quay.io/keboola/syrup-cli:latest
+
+
+script:
+ # run test job inside KBC
+ # - docker run --rm -e KBC_STORAGE_TOKEN quay.io/keboola/syrup-cli:latest run-job $KBC_DEVELOPERPORTAL_APP $KBC_APP_TEST_CONFIG_ID test
+ - skip
+
+deploy:
+ provider: script
+ skip_cleanup: true
+ script: ./deploy.sh
+ on:
+ tags: true
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..63d60a1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,30 @@
+FROM php:7.2-cli
+
+ARG COMPOSER_FLAGS="--prefer-dist --no-interaction"
+ARG DEBIAN_FRONTEND=noninteractive
+ENV COMPOSER_ALLOW_SUPERUSER 1
+ENV COMPOSER_PROCESS_TIMEOUT 3600
+
+WORKDIR /code/
+
+COPY docker/php-prod.ini /usr/local/etc/php/php.ini
+COPY docker/composer-install.sh /tmp/composer-install.sh
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ git \
+ unzip \
+ && rm -r /var/lib/apt/lists/* \
+ && chmod +x /tmp/composer-install.sh \
+ && /tmp/composer-install.sh
+
+## Composer - deps always cached unless changed
+# First copy only composer files
+COPY composer.* /code/
+# Download dependencies, but don't run scripts or init autoloaders as the app is missing
+RUN composer install $COMPOSER_FLAGS --no-scripts --no-autoloader
+# copy rest of the app
+COPY . /code/
+# run normal composer - all deps are cached already
+RUN composer install $COMPOSER_FLAGS
+
+CMD ["php", "/code/src/run.php"]
diff --git a/Dockerfile-tests b/Dockerfile-tests
new file mode 100644
index 0000000..5df4612
--- /dev/null
+++ b/Dockerfile-tests
@@ -0,0 +1,4 @@
+FROM keboola/ex-ftp
+
+RUN pecl install xdebug \
+ && docker-php-ext-enable xdebug
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..eba4be0
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 Keboola, https://keboola.com
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..14717d5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,180 @@
+# FTP extractor
+
+[](https://travis-ci.com/keboola/ex-ftp)
+[](https://codeclimate.com/github/keboola/ex-ftp/maintainability)
+[](https://github.com/keboola/ex-ftp/blob/master/LICENSE.md)
+
+Download file(s) from FTP (optional TLS) or SFTP server. Supports glob syntax.
+# Configuration
+
+## Options
+
+The configuration requires following properties:
+
+- `host` - string (required): IP address or Hostname of FTP(s)/SFTP server
+- `port` - integer (required): Server port (default port is 21)
+- `username` - string (required): User with correct access rights
+- `password` - string (optional): Password for given User
+- `path` - string (required): Path to specific file or glob syntax path
+ - FTP(s) uses absolute path
+ - SFTP uses relative path according to user's HOME directory
+- `connectionType` - string (required): Type of connection (possible value [FTP|FTPS|SFTP])
+- `privateKey` - string (optional): Possible to use only with SFTP connectionType.
+- `onlyNewFiles` - boolean (optional): Compares timestamp of files from last run and download only new files
+- `listing` - string (optional, enum [manual|recursion] default: recursion): Use `manual` in case your FTP server does not support listing recursion.
+- `ignorePassiveAddress` - boolean (optional): Sets ignore passive address
+
+## Example
+Configuration to download specific file:
+
+```json
+ {
+ "parameters": {
+ "host":"ftp.example.com",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/dir1/file.csv",
+ "connectionType": "FTP"
+ }
+ }
+```
+
+Configuration to download files by glob syntax:
+
+```json
+ {
+ "parameters": {
+ "host":"ftp.example.com",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/dir1/*.csv",
+ "connectionType": "FTP"
+ }
+ }
+
+```
+Configuration to download files by glob syntax with recursion manually (when server does not support recursive listing):
+
+```json
+ {
+ "parameters": {
+ "host":"ftp.example.com",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/dir1/*/*.csv",
+ "connectionType": "FTP",
+ "listing": "manual"
+ }
+ }
+
+```
+Configuration to download only new files by glob syntax:
+
+```json
+ {
+ "parameters": {
+ "host":"ftp.example.com",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/dir1/*.csv",
+ "connectionType": "FTP",
+ "onlyNewFiles": true
+ }
+ }
+```
+Configuration to download only new *.csv files by glob syntax from SFTP server:
+(you need to use relative path)
+
+```json
+ {
+ "parameters": {
+ "host":"ftp.example.com",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 22,
+ "path": "**/*.csv",
+ "connectionType": "SFTP",
+ "onlyNewFiles": true
+ }
+ }
+```
+
+Configuration to download exact file on SFTP server
+(you need to use relative path)
+
+```json
+ {
+ "parameters": {
+ "host":"ftp.example.com",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 22,
+ "path": "files/data.csv",
+ "connectionType": "SFTP"
+ }
+ }
+```
+
+
+# Development
+
+Clone this repository and init the workspace with following command:
+
+```
+git clone https://github.com/keboola/ex-ftp
+cd ex-ftp
+docker-compose build
+docker-compose run --rm dev composer install --no-scripts
+```
+
+Build the image:
+```
+docker-compose build dev
+```
+
+## Tools
+
+- Tests: `docker-compose run --rm dev composer tests`
+ - Unit tests: `docker-compose run --rm dev composer tests-phpunit`
+ - Datadir tests: `docker-compose run --rm dev composer tests-datadir`
+- Code sniffer: `docker-compose run --rm dev composer phpcs`
+- Static analysis: `docker-compose run --rm dev composer phpstan`
+
+## New functional test
+
+Because FTP extractor works with file's timestamps, all `state.json`
+files must be crated at runtime. When you add new functional test with
+config option `onlyNewFiles` set to `false` add following to
+`tests/functional/DatadirTest.php`:
+```php
+$state = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => 0,
+ "last-timestamp-files" => [],
+ ],
+];
+JsonHelper::writeFile(__DIR__ . '/###NAME_OF_TEST###/expected/data/out/state.json', $state);
+
+```
+
+For tests with `onlyNewFiles` set to `true` you have to specify both state.json files:
+```php
+$state = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => $timestamps["dir1/alone.txt"],
+ "last-timestamp-files" => ["dir1/alone.txt"],
+ ],
+];
+JsonHelper::writeFile(__DIR__ . '/###NAME_OF_TEST###/expected/data/out/state.json', $state);
+JsonHelper::writeFile(__DIR__ . '/###NAME_OF_TEST###/source/data/in/state.json', $state);
+```
+Where `alone.txt` should be the single file in downloaded folder.
+
+
+# Integration
+
+For information about deployment and integration with KBC, please refer to the [deployment section of developers documentation](https://developers.keboola.com/extend/component/deployment/)
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..f7dc725
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,59 @@
+{
+ "require": {
+ "php": "^7.2",
+ "keboola/php-component": "^5.0",
+ "keboola/retry": "^0.5.0",
+ "keboola/sanitizer": "^0.1.0",
+ "league/flysystem": "^1.0",
+ "league/flysystem-sftp": "^1.0.21",
+ "webmozart/glob": "^4.1"
+ },
+ "require-dev": {
+ "jakub-onderka/php-parallel-lint": "^1.0",
+ "keboola/csv": "^2.0",
+ "keboola/coding-standard": "^4.0",
+ "keboola/php-temp": "^1.0",
+ "phpstan/phpstan-shim": "^0.9.2",
+ "phpunit/phpunit": "^7.0",
+ "symfony/process": "^4.0",
+ "keboola/datadir-tests": "^2.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Keboola\\FtpExtractor\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Keboola\\FtpExtractor\\Tests\\": "tests/phpunit/",
+ "Keboola\\FtpExtractor\\FunctionalTests\\": "tests/functional/"
+ }
+ },
+ "scripts": {
+ "tests-phpunit": "phpunit",
+ "tests-datadir": "phpunit tests/functional",
+ "tests": [
+ "@tests-phpunit",
+ "@tests-datadir"
+ ],
+
+ "phpstan": "phpstan analyse ./src ./tests --level=max --no-progress -c phpstan.neon",
+ "phpcs": "phpcs -n --ignore=vendor --extensions=php .",
+ "phpcbf": "phpcbf -n --ignore=vendor --extensions=php .",
+ "phplint": "parallel-lint -j 10 --exclude vendor .",
+ "build": [
+ "@phplint",
+ "@phpcs",
+ "@phpstan",
+ "@tests"
+ ],
+ "ci": [
+ "@composer validate --no-check-publish --no-check-all",
+ "@build"
+ ]
+ },
+ "config": {
+ "sort-packages": true,
+ "optimize-autoloader": true
+ }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644
index 0000000..e52cbb2
--- /dev/null
+++ b/composer.lock
@@ -0,0 +1,2703 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "e3636636253c1a2b9ed2a06204af7173",
+ "packages": [
+ {
+ "name": "keboola/php-component",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/keboola/php-component.git",
+ "reference": "264a7a5b7ebab3724ccb7d7202bc1344b58d3543"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/keboola/php-component/zipball/264a7a5b7ebab3724ccb7d7202bc1344b58d3543",
+ "reference": "264a7a5b7ebab3724ccb7d7202bc1344b58d3543",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "monolog/monolog": "^1.23",
+ "php": "^7.1",
+ "symfony/config": "^4.0",
+ "symfony/filesystem": "^4.0",
+ "symfony/finder": "^4.0",
+ "symfony/serializer": "^4.0"
+ },
+ "require-dev": {
+ "devedge/sami-github": "^1.0",
+ "jakub-onderka/php-parallel-lint": "^1.0",
+ "keboola/coding-standard": "^4.0",
+ "keboola/php-temp": "^1.0",
+ "phpstan/phpstan-shim": "^0.9.1",
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "project",
+ "autoload": {
+ "psr-4": {
+ "Keboola\\Component\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Keboola",
+ "email": "devel@keboola.com"
+ }
+ ],
+ "description": "Helper classes for developing Keboola PHP components",
+ "keywords": [
+ "component",
+ "docker",
+ "keboola"
+ ],
+ "time": "2018-10-23T14:20:29+00:00"
+ },
+ {
+ "name": "keboola/retry",
+ "version": "0.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/keboola/retry.git",
+ "reference": "afdb190a9186b30a27c75df2aaf24a6de07efebb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/keboola/retry/zipball/afdb190a9186b30a27c75df2aaf24a6de07efebb",
+ "reference": "afdb190a9186b30a27c75df2aaf24a6de07efebb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/log": "^1.1"
+ },
+ "replace": {
+ "vkartaviy/retry": "*"
+ },
+ "require-dev": {
+ "keboola/coding-standard": "^7.0",
+ "phpstan/phpstan-shim": "^0.10",
+ "phpunit/phpunit": "7.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Retry\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Keboola Dev",
+ "email": "devel@keboola.com"
+ }
+ ],
+ "description": "Library for repeatable and retryable operations",
+ "keywords": [
+ "backoff",
+ "proxy",
+ "repeat",
+ "retry"
+ ],
+ "time": "2020-01-31T14:20:00+00:00"
+ },
+ {
+ "name": "keboola/sanitizer",
+ "version": "0.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/keboola/sanitizer.git",
+ "reference": "6edda00cd177409a33f180b8f12bdad89bf893c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/keboola/sanitizer/zipball/6edda00cd177409a33f180b8f12bdad89bf893c5",
+ "reference": "6edda00cd177409a33f180b8f12bdad89bf893c5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "jakub-onderka/php-parallel-lint": "^1.0",
+ "phpunit/phpunit": "^5.7",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Keboola\\Utils\\Sanitizer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Keboola",
+ "email": "devel@keboola.com"
+ }
+ ],
+ "description": "Column name sanitizer",
+ "time": "2019-01-11T10:21:17+00:00"
+ },
+ {
+ "name": "league/flysystem",
+ "version": "1.0.64",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem.git",
+ "reference": "d13c43dbd4b791f815215959105a008515d1a2e0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d13c43dbd4b791f815215959105a008515d1a2e0",
+ "reference": "d13c43dbd4b791f815215959105a008515d1a2e0",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "php": ">=5.5.9"
+ },
+ "conflict": {
+ "league/flysystem-sftp": "<1.0.6"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^3.4",
+ "phpunit/phpunit": "^5.7.26"
+ },
+ "suggest": {
+ "ext-fileinfo": "Required for MimeType",
+ "ext-ftp": "Allows you to use FTP server storage",
+ "ext-openssl": "Allows you to use FTPS server storage",
+ "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
+ "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
+ "league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
+ "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
+ "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
+ "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
+ "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
+ "league/flysystem-webdav": "Allows you to use WebDAV storage",
+ "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
+ "spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
+ "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Flysystem\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frenky.net"
+ }
+ ],
+ "description": "Filesystem abstraction: Many filesystems, one API.",
+ "keywords": [
+ "Cloud Files",
+ "WebDAV",
+ "abstraction",
+ "aws",
+ "cloud",
+ "copy.com",
+ "dropbox",
+ "file systems",
+ "files",
+ "filesystem",
+ "filesystems",
+ "ftp",
+ "rackspace",
+ "remote",
+ "s3",
+ "sftp",
+ "storage"
+ ],
+ "time": "2020-02-05T18:14:17+00:00"
+ },
+ {
+ "name": "league/flysystem-sftp",
+ "version": "1.0.21",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem-sftp.git",
+ "reference": "4c2f2fcc4da251127c315d37eb3dfa5e94658df0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-sftp/zipball/4c2f2fcc4da251127c315d37eb3dfa5e94658df0",
+ "reference": "4c2f2fcc4da251127c315d37eb3dfa5e94658df0",
+ "shasum": ""
+ },
+ "require": {
+ "league/flysystem": "~1.0",
+ "php": ">=5.6.0",
+ "phpseclib/phpseclib": "~2.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "0.9.*",
+ "phpunit/phpunit": "^5.7.25"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\Flysystem\\Sftp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frenky.net"
+ }
+ ],
+ "description": "Flysystem adapter for SFTP",
+ "time": "2019-09-19T09:11:05+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "1.25.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
+ "reference": "70e65a5470a42cfec1a7da00d30edb6e617e8dcf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0",
+ "psr/log": "~1.0"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^2.4.9 || ^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "graylog2/gelf-php": "~1.0",
+ "jakub-onderka/php-parallel-lint": "0.9",
+ "php-amqplib/php-amqplib": "~2.4",
+ "php-console/php-console": "^3.1.3",
+ "phpunit/phpunit": "~4.5",
+ "phpunit/phpunit-mock-objects": "2.3.0",
+ "ruflin/elastica": ">=0.90 <3.0",
+ "sentry/sentry": "^0.13",
+ "swiftmailer/swiftmailer": "^5.3|^6.0"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-mongo": "Allow sending log messages to a MongoDB server",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "php-console/php-console": "Allow sending log messages to Google Chrome",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server",
+ "sentry/sentry": "Allow sending log messages to a Sentry server"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "http://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "time": "2019-09-06T13:49:17+00:00"
+ },
+ {
+ "name": "phpseclib/phpseclib",
+ "version": "2.0.23",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpseclib/phpseclib.git",
+ "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/c78eb5058d5bb1a183133c36d4ba5b6675dfa099",
+ "reference": "c78eb5058d5bb1a183133c36d4ba5b6675dfa099",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "phing/phing": "~2.7",
+ "phpunit/phpunit": "^4.8.35|^5.7|^6.0",
+ "sami/sami": "~2.0",
+ "squizlabs/php_codesniffer": "~2.0"
+ },
+ "suggest": {
+ "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
+ "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
+ "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
+ "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "phpseclib/bootstrap.php"
+ ],
+ "psr-4": {
+ "phpseclib\\": "phpseclib/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jim Wigginton",
+ "email": "terrafrost@php.net",
+ "role": "Lead Developer"
+ },
+ {
+ "name": "Patrick Monnerat",
+ "email": "pm@datasphere.ch",
+ "role": "Developer"
+ },
+ {
+ "name": "Andreas Fischer",
+ "email": "bantu@phpbb.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Hans-Jürgen Petrich",
+ "email": "petrich@tronic-media.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Graham Campbell",
+ "email": "graham@alt-three.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
+ "homepage": "http://phpseclib.sourceforge.net",
+ "keywords": [
+ "BigInteger",
+ "aes",
+ "asn.1",
+ "asn1",
+ "blowfish",
+ "crypto",
+ "cryptography",
+ "encryption",
+ "rsa",
+ "security",
+ "sftp",
+ "signature",
+ "signing",
+ "ssh",
+ "twofish",
+ "x.509",
+ "x509"
+ ],
+ "time": "2019-09-17T03:41:22+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "Psr/Log/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "time": "2018-11-20T15:27:04+00:00"
+ },
+ {
+ "name": "symfony/config",
+ "version": "v4.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/config.git",
+ "reference": "0acb26407a9e1a64a275142f0ae5e36436342720"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/config/zipball/0acb26407a9e1a64a275142f0ae5e36436342720",
+ "reference": "0acb26407a9e1a64a275142f0ae5e36436342720",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/filesystem": "~3.4|~4.0",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "symfony/finder": "<3.4"
+ },
+ "require-dev": {
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/event-dispatcher": "~3.4|~4.0",
+ "symfony/finder": "~3.4|~4.0",
+ "symfony/messenger": "~4.1",
+ "symfony/yaml": "~3.4|~4.0"
+ },
+ "suggest": {
+ "symfony/yaml": "To use the yaml reference dumper"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Config\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Config Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-09-19T15:51:53+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v4.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/9abbb7ef96a51f4d7e69627bc6f63307994e4263",
+ "reference": "9abbb7ef96a51f4d7e69627bc6f63307994e4263",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Filesystem Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-08-20T14:07:54+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v4.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "5e575faa95548d0586f6bedaeabec259714e44d1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/5e575faa95548d0586f6bedaeabec259714e44d1",
+ "reference": "5e575faa95548d0586f6bedaeabec259714e44d1",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Finder Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-09-16T11:29:48+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.12.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4",
+ "reference": "550ebaac289296ce228a706d0867afc34687e3f4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.12-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "time": "2019-08-06T08:03:45+00:00"
+ },
+ {
+ "name": "symfony/serializer",
+ "version": "v4.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/serializer.git",
+ "reference": "805eacc72d28e237ef31659344a4d72acef335ec"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/serializer/zipball/805eacc72d28e237ef31659344a4d72acef335ec",
+ "reference": "805eacc72d28e237ef31659344a4d72acef335ec",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-ctype": "~1.8"
+ },
+ "conflict": {
+ "phpdocumentor/type-resolver": "<0.2.1",
+ "symfony/dependency-injection": "<3.4",
+ "symfony/property-access": "<3.4",
+ "symfony/property-info": "<3.4",
+ "symfony/yaml": "<3.4"
+ },
+ "require-dev": {
+ "doctrine/annotations": "~1.0",
+ "doctrine/cache": "~1.0",
+ "phpdocumentor/reflection-docblock": "^3.0|^4.0",
+ "symfony/cache": "~3.4|~4.0",
+ "symfony/config": "~3.4|~4.0",
+ "symfony/dependency-injection": "~3.4|~4.0",
+ "symfony/http-foundation": "~3.4|~4.0",
+ "symfony/property-access": "~3.4|~4.0",
+ "symfony/property-info": "^3.4.13|~4.0",
+ "symfony/validator": "~3.4|~4.0",
+ "symfony/yaml": "~3.4|~4.0"
+ },
+ "suggest": {
+ "doctrine/annotations": "For using the annotation mapping. You will also need doctrine/cache.",
+ "doctrine/cache": "For using the default cached annotation reader and metadata cache.",
+ "psr/cache-implementation": "For using the metadata cache.",
+ "symfony/config": "For using the XML mapping loader.",
+ "symfony/http-foundation": "For using a MIME type guesser within the DataUriNormalizer.",
+ "symfony/property-access": "For using the ObjectNormalizer.",
+ "symfony/property-info": "To deserialize relations.",
+ "symfony/yaml": "For using the default YAML mapping loader."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Serializer\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Serializer Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-10-02T15:03:35+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "1.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/assert.git",
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4",
+ "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3 || ^7.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "time": "2019-08-24T08:43:50+00:00"
+ },
+ {
+ "name": "webmozart/glob",
+ "version": "4.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/glob.git",
+ "reference": "3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/glob/zipball/3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe",
+ "reference": "3cbf63d4973cf9d780b93d2da8eec7e4a9e63bbe",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.3|^7.0",
+ "webmozart/path-util": "^2.2"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.6",
+ "sebastian/version": "^1.0.1",
+ "symfony/filesystem": "^2.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Glob\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "A PHP implementation of Ant's glob.",
+ "time": "2015-12-29T11:14:33+00:00"
+ },
+ {
+ "name": "webmozart/path-util",
+ "version": "2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozart/path-util.git",
+ "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
+ "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "webmozart/assert": "~1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.6",
+ "sebastian/version": "^1.0.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\PathUtil\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.",
+ "time": "2015-12-17T08:42:14+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "doctrine/instantiator",
+ "version": "1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/instantiator.git",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/a2c590166b2133a4633738648b6b064edae0814a",
+ "reference": "a2c590166b2133a4633738648b6b064edae0814a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^6.0",
+ "ext-pdo": "*",
+ "ext-phar": "*",
+ "phpbench/phpbench": "^0.13",
+ "phpstan/phpstan-phpunit": "^0.11",
+ "phpstan/phpstan-shim": "^0.11",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Marco Pivetta",
+ "email": "ocramius@gmail.com",
+ "homepage": "http://ocramius.github.com/"
+ }
+ ],
+ "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+ "keywords": [
+ "constructor",
+ "instantiate"
+ ],
+ "time": "2019-03-17T17:37:11+00:00"
+ },
+ {
+ "name": "jakub-onderka/php-parallel-lint",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/JakubOnderka/PHP-Parallel-Lint.git",
+ "reference": "04fbd3f5fb1c83f08724aa58a23db90bd9086ee8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/JakubOnderka/PHP-Parallel-Lint/zipball/04fbd3f5fb1c83f08724aa58a23db90bd9086ee8",
+ "reference": "04fbd3f5fb1c83f08724aa58a23db90bd9086ee8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "require-dev": {
+ "jakub-onderka/php-console-highlighter": "~0.3",
+ "nette/tester": "~1.3",
+ "squizlabs/php_codesniffer": "~2.7"
+ },
+ "suggest": {
+ "jakub-onderka/php-console-highlighter": "Highlight syntax in code snippet"
+ },
+ "bin": [
+ "parallel-lint"
+ ],
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "./"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-2-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jakub Onderka",
+ "email": "ahoj@jakubonderka.cz"
+ }
+ ],
+ "description": "This tool check syntax of PHP files about 20x faster than serial check.",
+ "homepage": "https://github.com/JakubOnderka/PHP-Parallel-Lint",
+ "time": "2018-02-24T15:31:20+00:00"
+ },
+ {
+ "name": "keboola/coding-standard",
+ "version": "4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/keboola/phpcs-standard.git",
+ "reference": "17b1820b408cfbeed4498c2c5b5c7a456bebe850"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/keboola/phpcs-standard/zipball/17b1820b408cfbeed4498c2c5b5c7a456bebe850",
+ "reference": "17b1820b408cfbeed4498c2c5b5c7a456bebe850",
+ "shasum": ""
+ },
+ "require": {
+ "slevomat/coding-standard": "4.4.6",
+ "squizlabs/php_codesniffer": "^3.2"
+ },
+ "type": "phpcodesniffer-standard",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Keboola coding standard",
+ "time": "2018-05-15T15:09:36+00:00"
+ },
+ {
+ "name": "keboola/csv",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/keboola/php-csv.git",
+ "reference": "d46e83e973aeec11dbebe55deb6eb9c0f14cd271"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/keboola/php-csv/zipball/d46e83e973aeec11dbebe55deb6eb9c0f14cd271",
+ "reference": "d46e83e973aeec11dbebe55deb6eb9c0f14cd271",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "codeclimate/php-test-reporter": "^0.4",
+ "phpunit/phpunit": "^5.7",
+ "squizlabs/php_codesniffer": "^3.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Keboola\\Csv\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Keboola",
+ "email": "devel@keboola.com"
+ }
+ ],
+ "description": "Keboola CSV reader and writer",
+ "homepage": "http://keboola.com",
+ "keywords": [
+ "csv",
+ "rfc4180"
+ ],
+ "time": "2018-05-18T09:12:21+00:00"
+ },
+ {
+ "name": "keboola/datadir-tests",
+ "version": "2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/keboola/datadir-tests.git",
+ "reference": "5653b5f769bfe9252d6f3304e815e6014033ef81"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/keboola/datadir-tests/zipball/5653b5f769bfe9252d6f3304e815e6014033ef81",
+ "reference": "5653b5f769bfe9252d6f3304e815e6014033ef81",
+ "shasum": ""
+ },
+ "require": {
+ "keboola/php-temp": "^1.0",
+ "php": "^7.1",
+ "phpunit/phpunit": "^7.0",
+ "symfony/filesystem": "^4.0",
+ "symfony/finder": "^4.0",
+ "symfony/process": "^4.0"
+ },
+ "require-dev": {
+ "jakub-onderka/php-parallel-lint": "^1.0",
+ "keboola/coding-standard": "^2.0",
+ "phpstan/phpstan-shim": "^0.9.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Keboola\\DatadirTests\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Tool for functional testing of Keboola Connection components",
+ "time": "2018-11-05T12:33:25+00:00"
+ },
+ {
+ "name": "keboola/php-temp",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/keboola/php-temp.git",
+ "reference": "2e3c2fc4cce8536a84cbad2a1586eb2eaebe5d3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/keboola/php-temp/zipball/2e3c2fc4cce8536a84cbad2a1586eb2eaebe5d3b",
+ "reference": "2e3c2fc4cce8536a84cbad2a1586eb2eaebe5d3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3",
+ "symfony/filesystem": ">2.1.0"
+ },
+ "require-dev": {
+ "codeclimate/php-test-reporter": "dev-master",
+ "phpunit/phpunit": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "Keboola\\Temp": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Miro Cillik",
+ "email": "miro@keboola.cz"
+ },
+ {
+ "name": "Ondrej Vana",
+ "email": "kachna@keboola.cz"
+ }
+ ],
+ "description": "Temp service - handles application's temporary files",
+ "keywords": [
+ "filesystem",
+ "temp"
+ ],
+ "time": "2017-11-13T13:02:19+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.9.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea",
+ "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "replace": {
+ "myclabs/deep-copy": "self.version"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.0",
+ "doctrine/common": "^2.6",
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ },
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "time": "2019-08-09T12:45:53+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "phar-io/version": "^2.0",
+ "php": "^5.6 || ^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "time": "2018-07-08T19:23:20+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "time": "2018-07-08T19:19:57+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "time": "2018-08-07T13:53:10+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "4.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+ "reference": "b83ff7cfcfee7827e1e78b637a5904fe6a96698e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0",
+ "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+ "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
+ "webmozart/assert": "^1.0"
+ },
+ "require-dev": {
+ "doctrine/instantiator": "^1.0.5",
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^6.4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "time": "2019-09-12T14:27:41+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1",
+ "phpdocumentor/reflection-common": "^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "^7.1",
+ "mockery/mockery": "~1",
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "time": "2019-08-22T18:11:29+00:00"
+ },
+ {
+ "name": "phpspec/prophecy",
+ "version": "1.9.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpspec/prophecy.git",
+ "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/f6811d96d97bdf400077a0cc100ae56aa32b9203",
+ "reference": "f6811d96d97bdf400077a0cc100ae56aa32b9203",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.0.2",
+ "php": "^5.3|^7.0",
+ "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
+ "sebastian/comparator": "^1.1|^2.0|^3.0",
+ "sebastian/recursion-context": "^1.0|^2.0|^3.0"
+ },
+ "require-dev": {
+ "phpspec/phpspec": "^2.5|^3.2",
+ "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.8.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Konstantin Kudryashov",
+ "email": "ever.zet@gmail.com",
+ "homepage": "http://everzet.com"
+ },
+ {
+ "name": "Marcello Duarte",
+ "email": "marcello.duarte@gmail.com"
+ }
+ ],
+ "description": "Highly opinionated mocking framework for PHP 5.3+",
+ "homepage": "https://github.com/phpspec/prophecy",
+ "keywords": [
+ "Double",
+ "Dummy",
+ "fake",
+ "mock",
+ "spy",
+ "stub"
+ ],
+ "time": "2019-10-03T11:07:50+00:00"
+ },
+ {
+ "name": "phpstan/phpstan-shim",
+ "version": "0.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan-shim.git",
+ "reference": "e4720fb2916be05de02869780072253e7e0e8a75"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan-shim/zipball/e4720fb2916be05de02869780072253e7e0e8a75",
+ "reference": "e4720fb2916be05de02869780072253e7e0e8a75",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~7.0"
+ },
+ "replace": {
+ "phpstan/phpstan": "self.version"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.9-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan Phar distribution",
+ "time": "2018-01-28T14:29:27+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "6.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+ "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.1",
+ "phpunit/php-file-iterator": "^2.0",
+ "phpunit/php-text-template": "^1.2.1",
+ "phpunit/php-token-stream": "^3.0",
+ "sebastian/code-unit-reverse-lookup": "^1.0.1",
+ "sebastian/environment": "^3.1 || ^4.0",
+ "sebastian/version": "^2.0.1",
+ "theseer/tokenizer": "^1.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0"
+ },
+ "suggest": {
+ "ext-xdebug": "^2.6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "6.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "time": "2018-10-31T16:06:48+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "050bedf145a257b1ff02746c31894800e5122946"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946",
+ "reference": "050bedf145a257b1ff02746c31894800e5122946",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "time": "2018-09-13T20:33:42+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "1.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.3"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "time": "2015-06-21T13:50:34+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "2.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "1038454804406b0b5f5f520358e78c1c2f71501e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e",
+ "reference": "1038454804406b0b5f5f520358e78c1c2f71501e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "time": "2019-06-07T04:22:29+00:00"
+ },
+ {
+ "name": "phpunit/php-token-stream",
+ "version": "3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-token-stream.git",
+ "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff",
+ "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Wrapper around PHP's tokenizer extension.",
+ "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
+ "keywords": [
+ "tokenizer"
+ ],
+ "time": "2019-09-17T06:23:10+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "7.5.16",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "316afa6888d2562e04aeb67ea7f2017a0eb41661"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/316afa6888d2562e04aeb67ea7f2017a0eb41661",
+ "reference": "316afa6888d2562e04aeb67ea7f2017a0eb41661",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/instantiator": "^1.1",
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "myclabs/deep-copy": "^1.7",
+ "phar-io/manifest": "^1.0.2",
+ "phar-io/version": "^2.0",
+ "php": "^7.1",
+ "phpspec/prophecy": "^1.7",
+ "phpunit/php-code-coverage": "^6.0.7",
+ "phpunit/php-file-iterator": "^2.0.1",
+ "phpunit/php-text-template": "^1.2.1",
+ "phpunit/php-timer": "^2.1",
+ "sebastian/comparator": "^3.0",
+ "sebastian/diff": "^3.0",
+ "sebastian/environment": "^4.0",
+ "sebastian/exporter": "^3.1",
+ "sebastian/global-state": "^2.0",
+ "sebastian/object-enumerator": "^3.0.3",
+ "sebastian/resource-operations": "^2.0",
+ "sebastian/version": "^2.0.1"
+ },
+ "conflict": {
+ "phpunit/phpunit-mock-objects": "*"
+ },
+ "require-dev": {
+ "ext-pdo": "*"
+ },
+ "suggest": {
+ "ext-soap": "*",
+ "ext-xdebug": "*",
+ "phpunit/php-invoker": "^2.0"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "7.5-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "time": "2019-09-14T09:08:39+00:00"
+ },
+ {
+ "name": "sebastian/code-unit-reverse-lookup",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+ "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+ "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7 || ^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Looks up which function or method a line of code belongs to",
+ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+ "time": "2017-03-04T06:30:41+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+ "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1",
+ "sebastian/diff": "^3.0",
+ "sebastian/exporter": "^3.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "time": "2018-07-12T15:12:46+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+ "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.5 || ^8.0",
+ "symfony/process": "^2 || ^3.3 || ^4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "time": "2019-02-04T06:01:07+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "4.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
+ "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.5"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "http://www.github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "time": "2019-05-05T09:05:15+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "3.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e",
+ "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0",
+ "sebastian/recursion-context": "^3.0"
+ },
+ "require-dev": {
+ "ext-mbstring": "*",
+ "phpunit/phpunit": "^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.1.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "http://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "time": "2019-09-14T09:02:43+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+ "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0"
+ },
+ "suggest": {
+ "ext-uopz": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "http://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "time": "2017-04-27T15:39:26+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5",
+ "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0",
+ "sebastian/object-reflector": "^1.1.1",
+ "sebastian/recursion-context": "^3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "time": "2017-08-03T12:35:26+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "1.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "773f97c67f28de00d397be301821b06708fca0be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be",
+ "reference": "773f97c67f28de00d397be301821b06708fca0be",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "time": "2017-03-29T09:07:27+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
+ "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+ "time": "2017-03-03T06:23:57+00:00"
+ },
+ {
+ "name": "sebastian/resource-operations",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/resource-operations.git",
+ "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+ "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides a list of PHP built-in functions that operate on resources",
+ "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+ "time": "2018-10-04T04:07:39+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "time": "2016-10-03T07:35:21+00:00"
+ },
+ {
+ "name": "slevomat/coding-standard",
+ "version": "4.4.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/slevomat/coding-standard.git",
+ "reference": "861e7b55d348c81a9dd0b3655dbbc83076d60c05"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/861e7b55d348c81a9dd0b3655dbbc83076d60c05",
+ "reference": "861e7b55d348c81a9dd0b3655dbbc83076d60c05",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1",
+ "squizlabs/php_codesniffer": "^3.0.2"
+ },
+ "require-dev": {
+ "jakub-onderka/php-parallel-lint": "0.9.2",
+ "phing/phing": "2.16",
+ "phpstan/phpstan": "0.9.2",
+ "phpstan/phpstan-phpunit": "0.9.4",
+ "phpstan/phpstan-strict-rules": "0.9",
+ "phpunit/php-code-coverage": "6.0.1",
+ "phpunit/phpunit": "7.0.0"
+ },
+ "type": "phpcodesniffer-standard",
+ "autoload": {
+ "psr-4": {
+ "SlevomatCodingStandard\\": "SlevomatCodingStandard"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Slevomat Coding Standard for PHP_CodeSniffer complements Consistence Coding Standard by providing sniffs with additional checks.",
+ "time": "2018-02-15T17:13:28+00:00"
+ },
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
+ "reference": "0afebf16a2e7f1e434920fa976253576151effe9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/0afebf16a2e7f1e434920fa976253576151effe9",
+ "reference": "0afebf16a2e7f1e434920fa976253576151effe9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "bin": [
+ "bin/phpcs",
+ "bin/phpcbf"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "lead"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards"
+ ],
+ "time": "2019-09-26T23:12:26+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v4.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "50556892f3cc47d4200bfd1075314139c4c9ff4b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/50556892f3cc47d4200bfd1075314139c4c9ff4b",
+ "reference": "50556892f3cc47d4200bfd1075314139c4c9ff4b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony Process Component",
+ "homepage": "https://symfony.com",
+ "time": "2019-09-26T21:17:10+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "1.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "time": "2019-06-13T22:48:21+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "php": "^7.2"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.0.0"
+}
diff --git a/deploy.sh b/deploy.sh
new file mode 100644
index 0000000..80187de
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+set -e
+
+# Obtain the component repository and log in
+docker pull quay.io/keboola/developer-portal-cli-v2:latest
+export REPOSITORY=`docker run --rm \
+ -e KBC_DEVELOPERPORTAL_USERNAME \
+ -e KBC_DEVELOPERPORTAL_PASSWORD \
+ quay.io/keboola/developer-portal-cli-v2:latest \
+ ecr:get-repository ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP}`
+
+eval $(docker run --rm \
+ -e KBC_DEVELOPERPORTAL_USERNAME \
+ -e KBC_DEVELOPERPORTAL_PASSWORD \
+ quay.io/keboola/developer-portal-cli-v2:latest \
+ ecr:get-login ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP})
+
+# Push to the repository
+docker tag ${APP_IMAGE}:latest ${REPOSITORY}:${TRAVIS_TAG}
+docker tag ${APP_IMAGE}:latest ${REPOSITORY}:latest
+docker push ${REPOSITORY}:${TRAVIS_TAG}
+docker push ${REPOSITORY}:latest
+
+# Update the tag in Keboola Developer Portal -> Deploy to KBC
+if echo ${TRAVIS_TAG} | grep -c '^v\?[0-9]\+\.[0-9]\+\.[0-9]\+$'
+then
+ docker run --rm \
+ -e KBC_DEVELOPERPORTAL_USERNAME \
+ -e KBC_DEVELOPERPORTAL_PASSWORD \
+ quay.io/keboola/developer-portal-cli-v2:latest \
+ update-app-repository ${KBC_DEVELOPERPORTAL_VENDOR} ${KBC_DEVELOPERPORTAL_APP} ${TRAVIS_TAG} ecr ${REPOSITORY}
+else
+ echo "Skipping deployment to KBC, tag ${TRAVIS_TAG} is not allowed."
+fi
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..e213ee1
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,33 @@
+version: '2.2'
+services:
+ app:
+ build: .
+ image: keboola/ex-ftp
+ volumes:
+ - ./tests/ftpInitContent:/code/tests/ftpInitContent
+ links:
+ - ftp
+ dev: &devConfig
+ build: .
+ volumes:
+ - ./:/code
+ - ./data:/data
+ links:
+ - ftp
+ ftp:
+ image: stilliard/pure-ftpd
+ environment:
+ FTP_USER_NAME: ftpuser
+ FTP_USER_PASS: userpass
+ FTP_USER_HOME: /home/ftpusers/
+ PUBLICHOST: ftp
+ volumes:
+ - ./tests/ftpInitContent:/home/ftpusers
+ tests:
+ <<: *devConfig
+ build:
+ dockerfile: Dockerfile-tests
+ context: .
+ environment:
+ - XDEBUG_CONFIG=remote_enable=1 remote_mode=req remote_port=9000 remote_host=172.20.0.1 remote_connect_back=0
+ - PHP_IDE_CONFIG=serverName=ex-ftp
diff --git a/docker/composer-install.sh b/docker/composer-install.sh
new file mode 100644
index 0000000..3696f9d
--- /dev/null
+++ b/docker/composer-install.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+EXPECTED_SIGNATURE=$(curl -s https://composer.github.io/installer.sig)
+php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
+ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');")
+
+if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
+then
+ >&2 echo 'ERROR: Invalid installer signature'
+ rm composer-setup.php
+ exit 1
+fi
+
+php composer-setup.php --quiet --install-dir=/usr/local/bin/ --filename=composer
+RESULT=$?
+rm composer-setup.php
+exit $RESULT
diff --git a/docker/php-prod.ini b/docker/php-prod.ini
new file mode 100644
index 0000000..d33772f
--- /dev/null
+++ b/docker/php-prod.ini
@@ -0,0 +1,19 @@
+; Recommended production values
+display_errors = Off
+display_startup_errors = Off
+error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
+html_errors = On
+log_errors = On
+max_input_time = 60
+output_buffering = 4096
+register_argc_argv = Off
+request_order = "GP"
+session.gc_divisor = 1000
+session.sid_bits_per_character = 5
+short_open_tag = Off
+track_errors = Off
+variables_order = "GPCS"
+
+; Custom
+date.timezone = UTC
+memory_limit = -1
diff --git a/git-old/objects/pack/pack-a59d7d1b8e3e5f4416fe983fa571bc03a930c9d4.idx b/git-old/objects/pack/pack-a59d7d1b8e3e5f4416fe983fa571bc03a930c9d4.idx
new file mode 100644
index 0000000..00aa9a2
Binary files /dev/null and b/git-old/objects/pack/pack-a59d7d1b8e3e5f4416fe983fa571bc03a930c9d4.idx differ
diff --git a/git-old/objects/pack/pack-a59d7d1b8e3e5f4416fe983fa571bc03a930c9d4.pack b/git-old/objects/pack/pack-a59d7d1b8e3e5f4416fe983fa571bc03a930c9d4.pack
new file mode 100644
index 0000000..b50d1ec
Binary files /dev/null and b/git-old/objects/pack/pack-a59d7d1b8e3e5f4416fe983fa571bc03a930c9d4.pack differ
diff --git a/init.file b/init.file
deleted file mode 100644
index e28a081..0000000
Binary files a/init.file and /dev/null differ
diff --git a/phpcs.xml b/phpcs.xml
new file mode 100644
index 0000000..67e37cf
--- /dev/null
+++ b/phpcs.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..ceb4a21
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,4 @@
+parameters:
+ ignoreErrors:
+ - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::scalarNode\(\)#'
+ - '#Calling method end\(\) on possibly null value of type Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface|null.#'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..ee73999
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,14 @@
+
+
+
+ tests/phpunit
+
+
diff --git a/src/AdapterFactory.php b/src/AdapterFactory.php
new file mode 100644
index 0000000..bdfcebf
--- /dev/null
+++ b/src/AdapterFactory.php
@@ -0,0 +1,76 @@
+getConnectionType()) {
+ case ConfigDefinition::CONNECTION_TYPE_FTP:
+ return static::createFtpAdapter($config);
+ break;
+ case ConfigDefinition::CONNECTION_TYPE_SSL_EXPLICIT:
+ return static::createSslFtpImplicitAdapter($config);
+ break;
+ case ConfigDefinition::CONNECTION_TYPE_SFTP:
+ return static::createSftpAdapter($config);
+ break;
+ default:
+ throw new \InvalidArgumentException("Specified adapter not found");
+ break;
+ }
+ }
+
+ private static function createFtpAdapter(Config $config): AbstractAdapter
+ {
+ return new Ftp(
+ $config->getConnectionConfig()
+ );
+ }
+
+ private static function createSslFtpImplicitAdapter(Config $config): AbstractAdapter
+ {
+ return new Ftp(
+ array_merge($config->getConnectionConfig(), ['ssl' => true])
+ );
+ }
+
+ private static function createSftpAdapter(Config $config): AbstractAdapter
+ {
+ if ($config->getPrivateKey() === '') {
+ $adapter = new SftpAdapter($config->getConnectionConfig());
+ } else {
+ $adapter = new SftpAdapter(
+ array_merge($config->getConnectionConfig(), ['privateKey' => $config->getPrivateKey()])
+ );
+ }
+ static::setSftpRoot($adapter, $config->getPathToCopy());
+ return $adapter;
+ }
+
+ private static function setSftpRoot(SftpAdapter $adapter, string $sourcePath): void
+ {
+ if (substr($sourcePath, 0, 1) === '/') {
+ $adapter->setRoot('/');
+ return;
+ }
+ try {
+ $pwd = $adapter->getConnection()->pwd();
+ $adapter->setRoot($pwd);
+ } catch (\RuntimeException $e) {
+ throw new UserException($e->getMessage(), $e->getCode(), $e);
+ } catch (\LogicException $e) {
+ throw new UserException($e->getMessage(), $e->getCode(), $e);
+ } catch (\ErrorException $e) {
+ throw new UserException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+}
diff --git a/src/Config.php b/src/Config.php
new file mode 100644
index 0000000..54163d4
--- /dev/null
+++ b/src/Config.php
@@ -0,0 +1,53 @@
+ $this->getValue(['parameters', 'host']),
+ 'username' => $this->getValue(['parameters', 'username']),
+ 'password' => $this->getValue(['parameters', '#password']),
+ 'port' => $this->getValue(['parameters', 'port']),
+ 'timeout' => $this->getValue(['parameters', 'timeout']),
+ 'recurseManually' => $this->shouldUseManualRecursion(),
+ 'ignorePassiveAddress' => $this->ignorePassiveAddress(),
+ ];
+ }
+
+ public function getConnectionType(): string
+ {
+ return $this->getValue(['parameters', 'connectionType']);
+ }
+
+ public function getPathToCopy(): string
+ {
+ return $this->getValue(['parameters', 'path']);
+ }
+
+ public function isOnlyForNewFiles(): bool
+ {
+ return $this->getValue(['parameters', 'onlyNewFiles']);
+ }
+
+ public function getPrivateKey(): string
+ {
+ return $this->getValue(['parameters', '#privateKey']);
+ }
+
+ private function shouldUseManualRecursion(): bool
+ {
+ return $this->getValue(['parameters', 'listing']) === ConfigDefinition::LISTING_MANUAL;
+ }
+
+ public function ignorePassiveAddress(): bool
+ {
+ return $this->getValue(['parameters', 'ignorePassiveAddress']);
+ }
+}
diff --git a/src/ConfigDefinition.php b/src/ConfigDefinition.php
new file mode 100644
index 0000000..66dad16
--- /dev/null
+++ b/src/ConfigDefinition.php
@@ -0,0 +1,85 @@
+children()
+ ->scalarNode('host')
+ ->isRequired()
+ ->cannotBeEmpty()
+ ->end()
+ ->scalarNode('username')
+ ->isRequired()
+ ->cannotBeEmpty()
+ ->end()
+ ->scalarNode('#password')
+ ->defaultValue('')
+ ->end()
+ ->scalarNode('path')
+ ->isRequired()
+ ->cannotBeEmpty()
+ ->end()
+ ->booleanNode('onlyNewFiles')
+ ->defaultFalse()
+ ->end()
+ ->integerNode('port')
+ ->min(1)->max(65535)
+ ->defaultValue(21)
+ ->end()
+ ->scalarNode('connectionType')
+ ->isRequired()
+ ->validate()->ifNotInArray([
+ self::CONNECTION_TYPE_FTP,
+ self::CONNECTION_TYPE_SSL_EXPLICIT,
+ self::CONNECTION_TYPE_SFTP,
+ ])->thenInvalid(
+ sprintf(
+ 'Connection type must be one of %s.',
+ implode(', ', [
+ self::CONNECTION_TYPE_FTP,
+ self::CONNECTION_TYPE_SSL_EXPLICIT,
+ self::CONNECTION_TYPE_SFTP,
+ ])
+ )
+ )
+ ->end()
+ ->end()
+ ->scalarNode('#privateKey')
+ ->defaultValue('')
+ ->end()
+ ->integerNode('timeout')
+ ->min(1)
+ ->defaultValue(60)
+ ->end()
+ ->enumNode('listing')
+ ->values([self::LISTING_MANUAL, self::LISTING_RECURSION])
+ ->defaultValue(self::LISTING_RECURSION)
+ ->end()
+ ->booleanNode('ignorePassiveAddress')
+ ->defaultFalse()
+ ->end()
+ ->end()
+ ;
+ // @formatter:on
+ return $parametersNode;
+ }
+}
diff --git a/src/Exception/ApplicationException.php b/src/Exception/ApplicationException.php
new file mode 100644
index 0000000..31a7d2c
--- /dev/null
+++ b/src/Exception/ApplicationException.php
@@ -0,0 +1,9 @@
+getMessage()
+ ));
+ }
+
+ self::handleCommonException($e);
+ }
+
+ private static function handleCommonException(\Throwable $e): void
+ {
+ if ($e instanceof SftpAdapterException) {
+ self::toUserException($e);
+ }
+
+ if ($e instanceof FilesystemException) {
+ self::toUserException($e);
+ }
+
+ // Make the message clear for user (ftp_rawlist(): php_connect_nonb() failed: Operation now in progress)
+ if ($e instanceof \ErrorException
+ && preg_match_all('/Operation now in progress \(115\)/', $e->getMessage())) {
+ self::toUserException($e, sprintf(
+ 'Connection was terminated. Check that the connection is not blocked by Firewall ' .
+ 'or set ignore passive address: %s',
+ $e->getMessage()
+ ));
+ }
+
+ // Catch user_error from phpseclib
+ // phpcs:disable
+ if (preg_match_all('/(getaddrinfo failed)|(Cannot connect to)|(The authenticity of)|(Connection closed prematurely)/', $e->getMessage())) {
+ self::toUserException($e);
+ }
+ // phpcs:enable
+
+ self::toApplicationException($e);
+ }
+
+ private static function toUserException(\Throwable $e, ?string $customMessage = null): void
+ {
+ throw new UserException($customMessage ?: $e->getMessage(), $e->getCode(), $e);
+ }
+
+ private static function toApplicationException(\Throwable $e): void
+ {
+ throw new ApplicationException($e->getMessage(), $e->getCode(), $e);
+ }
+}
diff --git a/src/FileStateRegistry.php b/src/FileStateRegistry.php
new file mode 100644
index 0000000..70bddd8
--- /dev/null
+++ b/src/FileStateRegistry.php
@@ -0,0 +1,66 @@
+newestTimestamp = 0;
+ $this->filesWithNewestTimestamp = [];
+ if (isset($stateFile[self::STATE_FILE_KEY])) {
+ $cfg = $stateFile[self::STATE_FILE_KEY];
+
+ if (isset($cfg[self::NEWEST_TIMESTAMP_KEY])) {
+ $this->newestTimestamp = intval($cfg[self::NEWEST_TIMESTAMP_KEY]);
+ }
+
+ if (isset($cfg[self::FILES_WITH_NEWEST_TIMESTAMP_KEY])) {
+ $this->filesWithNewestTimestamp = (array) $cfg[self::FILES_WITH_NEWEST_TIMESTAMP_KEY];
+ }
+ }
+ }
+
+ public function shouldBeFileUpdated(string $remotePath, int $timestamp): bool
+ {
+ if ($this->newestTimestamp <= $timestamp && !in_array($remotePath, $this->filesWithNewestTimestamp)) {
+ return true;
+ }
+ return false;
+ }
+
+ public function updateOutputState(string $remotePath, int $timestamp): void
+ {
+ // if the file has a greater timestamp than our newest, then reset our values.
+ if ($this->newestTimestamp < $timestamp) {
+ $this->newestTimestamp = $timestamp;
+ $this->filesWithNewestTimestamp = [$remotePath];
+ } else if ($this->newestTimestamp = $timestamp) {
+ $this->filesWithNewestTimestamp[] = $remotePath;
+ }
+ }
+
+ public function getFileStates(): array
+ {
+ return [
+ self::NEWEST_TIMESTAMP_KEY => $this->newestTimestamp,
+ self::FILES_WITH_NEWEST_TIMESTAMP_KEY => $this->filesWithNewestTimestamp,
+ ];
+ }
+}
diff --git a/src/FtpExtractor.php b/src/FtpExtractor.php
new file mode 100644
index 0000000..081ddc5
--- /dev/null
+++ b/src/FtpExtractor.php
@@ -0,0 +1,195 @@
+ftpFilesystem = $ftpFs;
+ $this->onlyNewFiles = $onlyNewFiles;
+ $this->filesToDownload = [];
+ $this->registry = $registry;
+ $this->logger = $logger;
+ }
+
+ public function copyFiles(string $sourcePath, string $destinationPath): int
+ {
+ try {
+ /** @var AbstractFtpAdapter $adapter */
+ $adapter = $this->ftpFilesystem->getAdapter();
+ $this->logger->info('Connecting to host ...');
+
+ (new RetryProxy(
+ new SimpleRetryPolicy(self::CONNECTION_RETRIES),
+ new ExponentialBackOffPolicy(self::RETRY_BACKOFF),
+ $this->logger
+ ))->call(static function () use ($adapter): void {
+ $adapter->getConnection();
+ });
+
+ $this->logger->info('Connection successful');
+ } catch (\Throwable $e) {
+ ExceptionConverter::handleCopyFilesException($e);
+ }
+
+ $this->prepareToDownloadFolder($sourcePath, $destinationPath);
+ return $this->download();
+ }
+
+ private function prepareToDownloadFolder(string $sourcePath, string $destinationPath): void
+ {
+ $items = $this->getPotentialFiles($sourcePath);
+ $i = 0;
+ foreach ($items as $item) {
+ if ($i % self::LOGGER_INFO_LOOP === 0) {
+ $this->logger->info(
+ sprintf(
+ "Checked %d of a possible %d files and found %d to download so far",
+ $i,
+ count($items),
+ count($this->filesToDownload)
+ )
+ );
+ }
+ $i++;
+ if (!GlobValidator::validatePathAgainstGlob($item['path'], $sourcePath)) {
+ continue;
+ }
+ $timestamp = 0;
+ if ($this->onlyNewFiles) {
+ try {
+ $timestamp = (int) $this->ftpFilesystem->getTimestamp($item['path']);
+ if (!$this->registry->shouldBeFileUpdated($item['path'], $timestamp)) {
+ continue;
+ }
+ } catch (\Throwable $e) {
+ ExceptionConverter::handlePrepareToDownloadException($e);
+ }
+ }
+ $destination = $destinationPath . '/' . strtr($item['path'], ['/' => '-']);
+ $this->filesToDownload[] = [
+ self::FILE_DESTINATION_KEY => $destination,
+ self::FILE_SOURCE_KEY => $item['path'],
+ self::FILE_TIMESTAMP_KEY => $timestamp,
+ ];
+ }
+ $this->logger->info(sprintf("%d files are ready for download", count($this->filesToDownload)));
+ }
+
+ private function getPotentialFiles(string $sourcePath): array
+ {
+ $absSourcePath = GlobValidator::convertToAbsolute($sourcePath); //because Glob work with absolute paths
+
+ $items = [];
+ try {
+ if (Glob::getStaticPrefix($absSourcePath) === $absSourcePath) { //means is file
+ $file = $this->ftpFilesystem->get($absSourcePath);
+ $items[] = [
+ 'path' => $file->getPath(),
+ 'type' => ($file->isFile()) ? ItemFilter::FTP_FILETYPE_FILE : '',
+ ];
+ } else { //means is glob based path
+ $this->logger->info("Fetching list of files in base path");
+ $basePath = Glob::getBasePath($absSourcePath);
+ $items = $this->ftpFilesystem->listContents($basePath, self::RECURSIVE_COPY);
+ }
+ $countBeforeFilter = count($items);
+ $this->logger->info(
+ sprintf(
+ "Base path listing contains %s item(s) including directories",
+ $countBeforeFilter
+ )
+ );
+ $items = ItemFilter::getOnlyFiles($items);
+ $this->logger->info(
+ sprintf(
+ "%s item(s) filtered out",
+ $countBeforeFilter - count($items)
+ )
+ );
+ } catch (\Throwable $e) {
+ ExceptionConverter::handlePrepareToDownloadException($e);
+ }
+ $this->logger->info(sprintf("Base path contains %s files(s)", count($items)));
+ return $items;
+ }
+
+ private function download(): int
+ {
+ $cbTimestampSort = function (array $a, array $b) {
+ return intval($a[self::FILE_TIMESTAMP_KEY]) <=> intval($b[self::FILE_TIMESTAMP_KEY]);
+ };
+ uasort($this->filesToDownload, $cbTimestampSort);
+
+ $fs = new Filesystem();
+ $downloadedFiles = 0;
+ foreach ($this->filesToDownload as $file) {
+ $file[self::FILE_DESTINATION_KEY] = ColumnNameSanitizer::toAscii($file[self::FILE_DESTINATION_KEY]);
+
+ $this->logger->info(sprintf("Downloading file %s", $file[self::FILE_SOURCE_KEY]));
+
+ try {
+ $fs->dumpFile(
+ $file[self::FILE_DESTINATION_KEY],
+ $this->ftpFilesystem->read($file[self::FILE_SOURCE_KEY])
+ );
+ } catch (\Throwable $e) {
+ ExceptionConverter::handleDownloadException($e);
+ }
+ $this->registry->updateOutputState($file[self::FILE_SOURCE_KEY], $file[self::FILE_TIMESTAMP_KEY]);
+ $downloadedFiles++;
+ }
+ return $downloadedFiles;
+ }
+}
diff --git a/src/FtpExtractorComponent.php b/src/FtpExtractorComponent.php
new file mode 100644
index 0000000..3964421
--- /dev/null
+++ b/src/FtpExtractorComponent.php
@@ -0,0 +1,51 @@
+getConfig();
+ $registry = new FileStateRegistry($this->getInputState());
+ $ftpFs = new Filesystem(AdapterFactory::getAdapter($config));
+ $ftpExtractor = new FtpExtractor(
+ $config->isOnlyForNewFiles(),
+ $ftpFs,
+ $registry,
+ $this->getLogger()
+ );
+ $count = $ftpExtractor->copyFiles(
+ $config->getPathToCopy(),
+ $this->getOutputDirectory()
+ );
+ $this->writeOutputStateToFile(
+ array_merge(
+ $this->getInputState(),
+ [FileStateRegistry::STATE_FILE_KEY => $registry->getFileStates()]
+ )
+ );
+ $this->getLogger()->info(sprintf("%d file(s) downloaded", $count));
+ }
+
+ private function getOutputDirectory(): string
+ {
+ return $this->getDataDir() . '/out/files/';
+ }
+
+ protected function getConfigClass(): string
+ {
+ return Config::class;
+ }
+
+ protected function getConfigDefinitionClass(): string
+ {
+ return ConfigDefinition::class;
+ }
+}
diff --git a/src/GlobValidator.php b/src/GlobValidator.php
new file mode 100644
index 0000000..0949c65
--- /dev/null
+++ b/src/GlobValidator.php
@@ -0,0 +1,29 @@
+run();
+ exit(0);
+} catch (UserException $e) {
+ $logger->error($e->getMessage());
+ exit(1);
+} catch (\Throwable $e) {
+ $logger->critical(
+ get_class($e) . ':' . $e->getMessage(),
+ [
+ 'errFile' => $e->getFile(),
+ 'errLine' => $e->getLine(),
+ 'errCode' => $e->getCode(),
+ 'errTrace' => $e->getTraceAsString(),
+ 'errPrevious' => $e->getPrevious() ? get_class($e->getPrevious()) : '',
+ ]
+ );
+ exit(2);
+}
diff --git "a/tests/ftpInitContent/Zvl\303\241\305\241\305\245 z\303\241ke\305\231n\303\275 u\304\215e\305\210 s \304\217ol\303\255\304\215ky b\304\233\305\276\303\255 pod\303\251l z\303\263ny \303\272l\305\257.csv" "b/tests/ftpInitContent/Zvl\303\241\305\241\305\245 z\303\241ke\305\231n\303\275 u\304\215e\305\210 s \304\217ol\303\255\304\215ky b\304\233\305\276\303\255 pod\303\251l z\303\263ny \303\272l\305\257.csv"
new file mode 100644
index 0000000..5be0054
--- /dev/null
+++ "b/tests/ftpInitContent/Zvl\303\241\305\241\305\245 z\303\241ke\305\231n\303\275 u\304\215e\305\210 s \304\217ol\303\255\304\215ky b\304\233\305\276\303\255 pod\303\251l z\303\263ny \303\272l\305\257.csv"
@@ -0,0 +1,19 @@
+id,text,tag
+1,"D: Co bylo dříve, slepice nebo vejce?
+O: Dříve bylo všechno - slepice, vejce, cukr, mouka, máslo, maso, ovoce...",Rádio Jerevan
+2,"D: Kdy bude lépe?
+O: Lépe už bylo.",Rádio Jerevan
+3,"D: Slyšela jsem, že je v Moskvě maso. Bude i v Minsku?
+O: Ano, výstava je putovní.",Rádio Jerevan
+4,"D: Slyšel jsem, že v Ukrajinské SSR roste kukuřice jako telegrafní sloupy. Je to pravda?
+O: Ano, někde i hustěji.",Rádio Jerevan
+5,"D: Může se stát Bůh členem Komunistické strany?
+O: V principu ano, ale nejprve musí vystoupit z církve.",Rádio Jerevan
+6,"D: Co je to chaos?
+O: Na otázky týkající se našeho průmyslu pro rozsáhlost odpovědi neodpovídáme.",Rádio Jerevan
+7,"D: Na co umřel Stalin?
+O: Naštěstí.",Rádio Jerevan
+8,"D: Je v Sovětském svaze stejná svoboda projevu jako na Západě?
+O: Ano, ale na Západě je i svoboda po projevu.",Rádio Jerevan
+9,"D: Slyšela jsem, že i list papíru je účinnější metoda antikoncepce než západní přípravky. Je to pravda?
+O: V zásadě ano, drží-li jej žena mezi koleny.",Rádio Jerevan
diff --git a/tests/ftpInitContent/dir1/dir1_1/specific-file-2.csv b/tests/ftpInitContent/dir1/dir1_1/specific-file-2.csv
new file mode 100644
index 0000000..38ac044
--- /dev/null
+++ b/tests/ftpInitContent/dir1/dir1_1/specific-file-2.csv
@@ -0,0 +1 @@
+This is also good content;
\ No newline at end of file
diff --git a/tests/ftpInitContent/dir1/dir1_1/specific-file.csv b/tests/ftpInitContent/dir1/dir1_1/specific-file.csv
new file mode 100644
index 0000000..acbf3aa
--- /dev/null
+++ b/tests/ftpInitContent/dir1/dir1_1/specific-file.csv
@@ -0,0 +1 @@
+This is a great fajl!
\ No newline at end of file
diff --git a/tests/ftpInitContent/dir1/recursive.bin b/tests/ftpInitContent/dir1/recursive.bin
new file mode 100644
index 0000000..736686c
--- /dev/null
+++ b/tests/ftpInitContent/dir1/recursive.bin
@@ -0,0 +1 @@
+010101
diff --git a/tests/ftpInitContent/file_1.txt b/tests/ftpInitContent/file_1.txt
new file mode 100644
index 0000000..2d063b7
--- /dev/null
+++ b/tests/ftpInitContent/file_1.txt
@@ -0,0 +1 @@
+testing1
\ No newline at end of file
diff --git a/tests/functional/DatadirTest.php b/tests/functional/DatadirTest.php
new file mode 100644
index 0000000..886c411
--- /dev/null
+++ b/tests/functional/DatadirTest.php
@@ -0,0 +1,141 @@
+files()->in(__DIR__ . '/../ftpInitContent/');
+ $timestamps = [];
+ foreach ($files as $file) {
+ /** @var SplFileInfo $file */
+ if ($file->getFilename() === 'a_brand_new_file.csv') {
+ unlink(__DIR__ . '/../ftpInitContent/a_brand_new_file.csv');
+ continue;
+ }
+ $timestamps[$file->getRelativePathname()] = $file->getMTime();
+ }
+
+ // --- normal-donwload test ----
+ $state = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => 0,
+ "last-timestamp-files" => [],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/normal-download/expected/data/out/state.json', $state);
+
+ // --- special-chars test ---
+ $state = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => 0,
+ "last-timestamp-files" => [],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/special-chars/expected/data/out/state.json', $state);
+
+ // --- nothing-to-update tests ---
+ $state = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => $timestamps["dir1/recursive.bin"],
+ "last-timestamp-files" => ["dir1/recursive.bin"],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/nothing-to-update/expected/data/out/state.json', $state);
+ JsonHelper::writeFile(__DIR__ . '/nothing-to-update/source/data/in/state.json', $state);
+
+ // --- specific-directory test ----
+ $state = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => 0,
+ "last-timestamp-files" => [],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/specific-directory/expected/data/out/state.json', $state);
+
+ // --- manual-recursion test ----
+ $state = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => 0,
+ "last-timestamp-files" => [],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/manual-recursion/expected/data/out/state.json', $state);
+
+ // --- only-new-files tests ---
+ $inputState = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => 0,
+ "last-timestamp-files" => [],
+ ],
+ ];
+ $outputState = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => $timestamps["file_1.txt"],
+ "last-timestamp-files" => ["file_1.txt", "Zvlášť zákeřný učeň s ďolíčky běží podél zóny úlů.csv"],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/only-new-files/expected/data/out/state.json', $outputState);
+ JsonHelper::writeFile(__DIR__ . '/only-new-files/source/data/in/state.json', $inputState);
+
+ // -- new-files-from-old-state test --
+ $inputState = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => $timestamps["file_1.txt"],
+ "last-timestamp-files" => ["file_1.txt", "Zvlášť zákeřný učeň s ďolíčky běží podél zóny úlů.csv"],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/new-files-from-old-state/source/data/in/state.json', $inputState);
+ }
+
+ /**
+ * @dataProvider provideDatadirSpecifications
+ */
+ public function testDatadir(DatadirTestSpecificationInterface $specification): void
+ {
+ $tempDatadir = $this->getTempDatadir($specification);
+
+ $sourceDatadir = $specification->getSourceDatadirDirectory();
+
+ if ($this->doesNameMatchDatadir('new-files-from-old-state', $sourceDatadir)) {
+ // -- new-files-from-old-state test --
+ $newCsvFile = __DIR__ . '/../ftpInitContent/a_brand_new_file.csv';
+ $expectingCsvFile = __DIR__ . '/new-files-from-old-state/expected/data/out/files/a_brand_new_file.csv';
+
+ $csvWriter = new CsvWriter($newCsvFile);
+ $csvWriter->writeRow(['a', 'csv', 'file']);
+ $fs = new Filesystem();
+ $fs->copy($newCsvFile, $expectingCsvFile);
+ $freshTimestamp = (new SplFileInfo($newCsvFile, "", ""))->getMTime();
+ $outputState = [
+ "ex-ftp-state" => [
+ "newest-timestamp" => $freshTimestamp,
+ "last-timestamp-files" => ["a_brand_new_file.csv"],
+ ],
+ ];
+ JsonHelper::writeFile(__DIR__ . '/new-files-from-old-state/expected/data/out/state.json', $outputState);
+ }
+
+ $process = $this->runScript($tempDatadir->getTmpFolder());
+
+ $this->assertMatchesSpecification($specification, $process, $tempDatadir->getTmpFolder());
+ }
+
+ private function doesNameMatchDatadir(string $testName, string $datadir): bool
+ {
+ return in_array($testName, explode('/', $datadir));
+ }
+}
diff --git a/tests/functional/manual-recursion/expected/data/out/files/.gitkeep b/tests/functional/manual-recursion/expected/data/out/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/manual-recursion/expected/data/out/files/dir1-dir1_1-specific-file-2.csv b/tests/functional/manual-recursion/expected/data/out/files/dir1-dir1_1-specific-file-2.csv
new file mode 100644
index 0000000..38ac044
--- /dev/null
+++ b/tests/functional/manual-recursion/expected/data/out/files/dir1-dir1_1-specific-file-2.csv
@@ -0,0 +1 @@
+This is also good content;
\ No newline at end of file
diff --git a/tests/functional/manual-recursion/expected/data/out/files/dir1-dir1_1-specific-file.csv b/tests/functional/manual-recursion/expected/data/out/files/dir1-dir1_1-specific-file.csv
new file mode 100644
index 0000000..acbf3aa
--- /dev/null
+++ b/tests/functional/manual-recursion/expected/data/out/files/dir1-dir1_1-specific-file.csv
@@ -0,0 +1 @@
+This is a great fajl!
\ No newline at end of file
diff --git a/tests/functional/manual-recursion/expected/data/out/tables/.gitkeep b/tests/functional/manual-recursion/expected/data/out/tables/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/manual-recursion/source/data/config.json b/tests/functional/manual-recursion/source/data/config.json
new file mode 100644
index 0000000..c02590e
--- /dev/null
+++ b/tests/functional/manual-recursion/source/data/config.json
@@ -0,0 +1,11 @@
+{
+ "parameters": {
+ "host": "ftp",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/dir1/*/*.csv",
+ "connectionType": "FTP",
+ "listing": "manual"
+ }
+}
diff --git a/tests/functional/new-files-from-old-state/expected/data/out/files/.gitkeep b/tests/functional/new-files-from-old-state/expected/data/out/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/new-files-from-old-state/expected/data/out/files/a_brand_new_file.csv b/tests/functional/new-files-from-old-state/expected/data/out/files/a_brand_new_file.csv
new file mode 100644
index 0000000..df0c963
--- /dev/null
+++ b/tests/functional/new-files-from-old-state/expected/data/out/files/a_brand_new_file.csv
@@ -0,0 +1 @@
+"a","csv","file"
diff --git a/tests/functional/new-files-from-old-state/expected/data/out/tables/.gitkeep b/tests/functional/new-files-from-old-state/expected/data/out/tables/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/new-files-from-old-state/source/data/config.json b/tests/functional/new-files-from-old-state/source/data/config.json
new file mode 100644
index 0000000..22e5c44
--- /dev/null
+++ b/tests/functional/new-files-from-old-state/source/data/config.json
@@ -0,0 +1,11 @@
+{
+ "parameters": {
+ "host": "ftp",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "*",
+ "onlyNewFiles": true,
+ "connectionType" : "FTP"
+ }
+}
diff --git a/tests/functional/new-files-from-old-state/source/data/in/files/.gitkeep b/tests/functional/new-files-from-old-state/source/data/in/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/normal-download/expected/data/out/files/.gitkeep b/tests/functional/normal-download/expected/data/out/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/normal-download/expected/data/out/files/dir1-recursive.bin b/tests/functional/normal-download/expected/data/out/files/dir1-recursive.bin
new file mode 100644
index 0000000..344a792
--- /dev/null
+++ b/tests/functional/normal-download/expected/data/out/files/dir1-recursive.bin
@@ -0,0 +1 @@
+010101
\ No newline at end of file
diff --git a/tests/functional/normal-download/expected/data/out/tables/.gitkeep b/tests/functional/normal-download/expected/data/out/tables/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/normal-download/source/data/config.json b/tests/functional/normal-download/source/data/config.json
new file mode 100644
index 0000000..28a568c
--- /dev/null
+++ b/tests/functional/normal-download/source/data/config.json
@@ -0,0 +1,10 @@
+{
+ "parameters": {
+ "host": "ftp",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/*/*",
+ "connectionType": "FTP"
+ }
+}
diff --git a/tests/functional/nothing-to-update/expected/data/out/files/.gitkeep b/tests/functional/nothing-to-update/expected/data/out/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/nothing-to-update/expected/data/out/tables/.gitkeep b/tests/functional/nothing-to-update/expected/data/out/tables/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/nothing-to-update/source/data/config.json b/tests/functional/nothing-to-update/source/data/config.json
new file mode 100644
index 0000000..5ddd8ad
--- /dev/null
+++ b/tests/functional/nothing-to-update/source/data/config.json
@@ -0,0 +1,11 @@
+{
+ "parameters": {
+ "host": "ftp",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/*/*",
+ "onlyNewFiles": true,
+ "connectionType" : "FTP"
+ }
+}
\ No newline at end of file
diff --git a/tests/functional/only-new-files/expected/data/out/files/.gitkeep b/tests/functional/only-new-files/expected/data/out/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/only-new-files/expected/data/out/files/Zvlast zakerny ucen s dolicky bezi podel zony ulu.csv b/tests/functional/only-new-files/expected/data/out/files/Zvlast zakerny ucen s dolicky bezi podel zony ulu.csv
new file mode 100644
index 0000000..5be0054
--- /dev/null
+++ b/tests/functional/only-new-files/expected/data/out/files/Zvlast zakerny ucen s dolicky bezi podel zony ulu.csv
@@ -0,0 +1,19 @@
+id,text,tag
+1,"D: Co bylo dříve, slepice nebo vejce?
+O: Dříve bylo všechno - slepice, vejce, cukr, mouka, máslo, maso, ovoce...",Rádio Jerevan
+2,"D: Kdy bude lépe?
+O: Lépe už bylo.",Rádio Jerevan
+3,"D: Slyšela jsem, že je v Moskvě maso. Bude i v Minsku?
+O: Ano, výstava je putovní.",Rádio Jerevan
+4,"D: Slyšel jsem, že v Ukrajinské SSR roste kukuřice jako telegrafní sloupy. Je to pravda?
+O: Ano, někde i hustěji.",Rádio Jerevan
+5,"D: Může se stát Bůh členem Komunistické strany?
+O: V principu ano, ale nejprve musí vystoupit z církve.",Rádio Jerevan
+6,"D: Co je to chaos?
+O: Na otázky týkající se našeho průmyslu pro rozsáhlost odpovědi neodpovídáme.",Rádio Jerevan
+7,"D: Na co umřel Stalin?
+O: Naštěstí.",Rádio Jerevan
+8,"D: Je v Sovětském svaze stejná svoboda projevu jako na Západě?
+O: Ano, ale na Západě je i svoboda po projevu.",Rádio Jerevan
+9,"D: Slyšela jsem, že i list papíru je účinnější metoda antikoncepce než západní přípravky. Je to pravda?
+O: V zásadě ano, drží-li jej žena mezi koleny.",Rádio Jerevan
diff --git a/tests/functional/only-new-files/expected/data/out/files/file_1.txt b/tests/functional/only-new-files/expected/data/out/files/file_1.txt
new file mode 100644
index 0000000..2d063b7
--- /dev/null
+++ b/tests/functional/only-new-files/expected/data/out/files/file_1.txt
@@ -0,0 +1 @@
+testing1
\ No newline at end of file
diff --git a/tests/functional/only-new-files/expected/data/out/tables/.gitkeep b/tests/functional/only-new-files/expected/data/out/tables/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/only-new-files/source/data/config.json b/tests/functional/only-new-files/source/data/config.json
new file mode 100644
index 0000000..22e5c44
--- /dev/null
+++ b/tests/functional/only-new-files/source/data/config.json
@@ -0,0 +1,11 @@
+{
+ "parameters": {
+ "host": "ftp",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "*",
+ "onlyNewFiles": true,
+ "connectionType" : "FTP"
+ }
+}
diff --git a/tests/functional/special-chars/expected/data/out/files/.gitkeep b/tests/functional/special-chars/expected/data/out/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/special-chars/expected/data/out/files/Zvlast zakerny ucen s dolicky bezi podel zony ulu.csv b/tests/functional/special-chars/expected/data/out/files/Zvlast zakerny ucen s dolicky bezi podel zony ulu.csv
new file mode 100644
index 0000000..5be0054
--- /dev/null
+++ b/tests/functional/special-chars/expected/data/out/files/Zvlast zakerny ucen s dolicky bezi podel zony ulu.csv
@@ -0,0 +1,19 @@
+id,text,tag
+1,"D: Co bylo dříve, slepice nebo vejce?
+O: Dříve bylo všechno - slepice, vejce, cukr, mouka, máslo, maso, ovoce...",Rádio Jerevan
+2,"D: Kdy bude lépe?
+O: Lépe už bylo.",Rádio Jerevan
+3,"D: Slyšela jsem, že je v Moskvě maso. Bude i v Minsku?
+O: Ano, výstava je putovní.",Rádio Jerevan
+4,"D: Slyšel jsem, že v Ukrajinské SSR roste kukuřice jako telegrafní sloupy. Je to pravda?
+O: Ano, někde i hustěji.",Rádio Jerevan
+5,"D: Může se stát Bůh členem Komunistické strany?
+O: V principu ano, ale nejprve musí vystoupit z církve.",Rádio Jerevan
+6,"D: Co je to chaos?
+O: Na otázky týkající se našeho průmyslu pro rozsáhlost odpovědi neodpovídáme.",Rádio Jerevan
+7,"D: Na co umřel Stalin?
+O: Naštěstí.",Rádio Jerevan
+8,"D: Je v Sovětském svaze stejná svoboda projevu jako na Západě?
+O: Ano, ale na Západě je i svoboda po projevu.",Rádio Jerevan
+9,"D: Slyšela jsem, že i list papíru je účinnější metoda antikoncepce než západní přípravky. Je to pravda?
+O: V zásadě ano, drží-li jej žena mezi koleny.",Rádio Jerevan
diff --git a/tests/functional/special-chars/expected/data/out/tables/.gitkeep b/tests/functional/special-chars/expected/data/out/tables/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/special-chars/source/data/config.json b/tests/functional/special-chars/source/data/config.json
new file mode 100644
index 0000000..525b5db
--- /dev/null
+++ b/tests/functional/special-chars/source/data/config.json
@@ -0,0 +1,11 @@
+{
+ "parameters": {
+ "host": "ftp",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/*.csv",
+ "onlyNewFiles": false,
+ "connectionType" : "FTP"
+ }
+}
\ No newline at end of file
diff --git a/tests/functional/specific-directory/expected/data/out/files/.gitkeep b/tests/functional/specific-directory/expected/data/out/files/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/specific-directory/expected/data/out/files/dir1-dir1_1-specific-file-2.csv b/tests/functional/specific-directory/expected/data/out/files/dir1-dir1_1-specific-file-2.csv
new file mode 100644
index 0000000..38ac044
--- /dev/null
+++ b/tests/functional/specific-directory/expected/data/out/files/dir1-dir1_1-specific-file-2.csv
@@ -0,0 +1 @@
+This is also good content;
\ No newline at end of file
diff --git a/tests/functional/specific-directory/expected/data/out/files/dir1-dir1_1-specific-file.csv b/tests/functional/specific-directory/expected/data/out/files/dir1-dir1_1-specific-file.csv
new file mode 100644
index 0000000..acbf3aa
--- /dev/null
+++ b/tests/functional/specific-directory/expected/data/out/files/dir1-dir1_1-specific-file.csv
@@ -0,0 +1 @@
+This is a great fajl!
\ No newline at end of file
diff --git a/tests/functional/specific-directory/expected/data/out/tables/.gitkeep b/tests/functional/specific-directory/expected/data/out/tables/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/functional/specific-directory/source/data/config.json b/tests/functional/specific-directory/source/data/config.json
new file mode 100644
index 0000000..ff69ece
--- /dev/null
+++ b/tests/functional/specific-directory/source/data/config.json
@@ -0,0 +1,10 @@
+{
+ "parameters": {
+ "host": "ftp",
+ "username": "ftpuser",
+ "#password": "userpass",
+ "port": 21,
+ "path": "/dir1/*/*.csv",
+ "connectionType": "FTP"
+ }
+}
diff --git a/tests/phpunit/AdapterFactoryTest.php b/tests/phpunit/AdapterFactoryTest.php
new file mode 100644
index 0000000..089a03f
--- /dev/null
+++ b/tests/phpunit/AdapterFactoryTest.php
@@ -0,0 +1,81 @@
+assertInstanceOf(
+ $expectedClass,
+ AdapterFactory::getAdapter($config)
+ );
+ }
+
+ public function adapterConfigProvider(): array
+ {
+ return [
+ [$this->provideTestConfig(ConfigDefinition::CONNECTION_TYPE_FTP), Ftp::class],
+ [$this->provideTestConfig(ConfigDefinition::CONNECTION_TYPE_SFTP), SftpAdapter::class],
+ [$this->provideTestConfig(ConfigDefinition::CONNECTION_TYPE_SSL_EXPLICIT), Ftp::class],
+ ];
+ }
+
+ public function testWrongConnectionType(): void
+ {
+ $this->expectException(InvalidConfigurationException::class);
+ $this->provideTestConfig("Blanka");
+ }
+
+ public function testInvalidSftpAdapterWithRelativePath(): void
+ {
+ $config = new Config(
+ [
+ 'parameters' => [
+ 'host' => 'ftp',
+ 'username' => 'ftpuser',
+ '#password' => 'userpass',
+ 'port' => 21,
+ 'path' => 'rel',
+ 'connectionType' => 'SFTP',
+ 'timeout' => 1,
+ ],
+ ],
+ new ConfigDefinition()
+ );
+ $this->expectException(UserException::class);
+ $this->expectExceptionMessageRegExp('/Could not login/');
+ AdapterFactory::getAdapter($config);
+ }
+
+ private function provideTestConfig(string $connectionType): Config
+ {
+ return new Config(
+ [
+ 'parameters' => [
+ 'host' => 'ftp',
+ 'username' => 'ftpuser',
+ '#password' => 'userpass',
+ 'port' => 21,
+ 'path' => '/absolute/path/*',
+ 'connectionType' => $connectionType,
+ ],
+ ],
+ new ConfigDefinition()
+ );
+ }
+}
diff --git a/tests/phpunit/ConfigTest.php b/tests/phpunit/ConfigTest.php
new file mode 100644
index 0000000..b62a0ad
--- /dev/null
+++ b/tests/phpunit/ConfigTest.php
@@ -0,0 +1,68 @@
+ [
+ 'host' => 'ftp',
+ 'username' => 'ftpuser',
+ '#password' => 'userpass',
+ 'port' => 21,
+ 'path' => 'rel',
+ 'connectionType' => 'SFTP',
+ ],
+ ];
+ if ($listing) {
+ $configArray['parameters']['listing'] = $listing;
+ }
+ $config = new Config(
+ $configArray,
+ new ConfigDefinition()
+ );
+ $this->assertSame($recurseManually, $config->getConnectionConfig()['recurseManually']);
+ }
+
+
+ public function testInvalidListingOption(): void
+ {
+ $configArray = [
+ 'parameters' => [
+ 'host' => 'ftp',
+ 'username' => 'ftpuser',
+ '#password' => 'userpass',
+ 'port' => 21,
+ 'path' => 'rel',
+ 'connectionType' => 'SFTP',
+ 'listing' => 'non-existing',
+ ],
+ ];
+
+ $this->expectException(InvalidConfigurationException::class);
+
+ new Config(
+ $configArray,
+ new ConfigDefinition()
+ );
+ }
+}
diff --git a/tests/phpunit/ConnectionTest.php b/tests/phpunit/ConnectionTest.php
new file mode 100644
index 0000000..198b82f
--- /dev/null
+++ b/tests/phpunit/ConnectionTest.php
@@ -0,0 +1,122 @@
+pushHandler($handler)
+ );
+
+ try {
+ $extractor->copyFiles('source', 'destination');
+ } catch (\Throwable $e) {
+ $this->assertInstanceOf(UserException::class, $e);
+ $this->assertCount(3, $handler->getRecords());
+ $this->assertRegExp(
+ '/(Could not login)|(getaddrinfo failed)|(Could not connect to)|(Cannot connect to)/',
+ $e->getMessage()
+ );
+
+ foreach ($handler->getRecords() as $count => $record) {
+ if ($count === 0) {
+ $this->assertEquals('Connecting to host ...', $record['message']);
+ continue;
+ }
+
+ $this->assertRegExp(
+ '/(Could not login)|(getaddrinfo failed)|(Could not connect to)|(Cannot connect to)/',
+ $record['message']
+ );
+ $this->assertRegExp(sprintf('/Retrying\.\.\. \[%dx\]$/', $count), $record['message']);
+ }
+ }
+ }
+
+
+ public function invalidConnectionProvider(): array
+ {
+ return [
+ 'ftp-non-existing-server' => [
+ new Ftp([
+ 'host' => 'localhost',
+ 'username' => 'bob',
+ 'password' => 'marley',
+ 'port' => 21,
+ ]),
+ ],
+ 'ftps-non-existing-server' => [
+ new Ftp([
+ 'host' => 'localhost',
+ 'username' => 'bob',
+ 'password' => 'marley',
+ 'port' => 21,
+ 'ssl' => 1,
+ ]),
+ ],
+ 'sftp-non-existing-server' => [
+ new SftpAdapter([
+ 'host' => 'localhost',
+ 'username' => 'bob',
+ 'password' => 'marley',
+ 'port' => 22,
+ ]),
+ ],
+ 'sftp-non-existing-host' => [
+ new SftpAdapter([
+ 'host' => 'non-existing-host.keboola',
+ 'username' => 'bob',
+ 'password' => 'marley',
+ 'port' => 22,
+ ]),
+ ],
+ 'sftp-non-existing-server-and-port' => [
+ new SftpAdapter([
+ 'host' => 'non-existing-host.keboola',
+ 'username' => 'bob',
+ 'password' => 'marley',
+ 'port' => 220,
+ 'path' => 'non-exists',
+ ]),
+ ],
+ 'ftp-non-existing-host' => [
+ new Ftp([
+ 'host' => 'non-existing-host.keboola',
+ 'username' => 'bob',
+ 'password' => 'marley',
+ 'port' => 21,
+ ]),
+ ],
+ 'ftp-non-existing-host-and-port' => [
+ new Ftp([
+ 'host' => 'non-existing-host.keboola',
+ 'username' => 'bob',
+ 'password' => 'marley',
+ 'port' => 50000,
+ ]),
+ ],
+ ];
+ }
+}
diff --git a/tests/phpunit/ExceptionConverterTest.php b/tests/phpunit/ExceptionConverterTest.php
new file mode 100644
index 0000000..68528d2
--- /dev/null
+++ b/tests/phpunit/ExceptionConverterTest.php
@@ -0,0 +1,175 @@
+expectException($expectedException);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+
+ try {
+ throw $throwException;
+ } catch (\Throwable $e) {
+ ExceptionConverter::handleCopyFilesException($e);
+ }
+ }
+
+ /**
+ * @dataProvider exceptionMessageProvider
+ */
+ public function testHandlePrepareToDownloadException(
+ string $expectedException,
+ string $expectedExceptionMessage,
+ \Throwable $throwException
+ ): void {
+ $this->expectException($expectedException);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+
+ try {
+ throw $throwException;
+ } catch (\Throwable $e) {
+ ExceptionConverter::handlePrepareToDownloadException($e);
+ }
+ }
+
+ /**
+ * @dataProvider downloadExceptionMessageProvider
+ */
+ public function testHandleDownloadException(
+ string $expectedException,
+ string $expectedExceptionMessage,
+ \Throwable $throwException
+ ): void {
+ $this->expectException($expectedException);
+ $this->expectExceptionMessage($expectedExceptionMessage);
+
+ try {
+ throw $throwException;
+ } catch (\Throwable $e) {
+ ExceptionConverter::handleDownloadException($e);
+ }
+ }
+
+ public function exceptionMessageProvider(): array
+ {
+ return [
+ [
+ UserException::class,
+ 'Foo bar',
+ new InvalidRootException('Foo bar'),
+ ],
+ [
+ UserException::class,
+ 'Foo bar',
+ new ConnectionErrorException('Foo bar'),
+ ],
+ [
+ UserException::class,
+ 'Foo bar',
+ new FileNotFoundException('Foo bar'),
+ ],
+ [
+ UserException::class,
+ 'Could not login with username: foo bar',
+ new ConnectionErrorException('Could not login with username: foo bar'),
+ ],
+ [
+ UserException::class,
+ 'php_network_getaddresses: getaddrinfo failed: nodename nor servname provided, or not known',
+ new \RuntimeException(
+ 'php_network_getaddresses: getaddrinfo failed: nodename nor servname provided, or not known'
+ ),
+ ],
+ [
+ UserException::class,
+ 'Could not connect to server to verify public key.',
+ new ConnectionRuntimeException('Could not connect to server to verify public key.'),
+ ],
+ [
+ UserException::class,
+ 'The authenticity of host foo can\'t be established.',
+ new \RuntimeException('The authenticity of host foo can\'t be established.'),
+ ],
+ [
+ UserException::class,
+ 'Cannot connect to foo bar',
+ new \RuntimeException('Cannot connect to foo bar'),
+ ],
+ [
+ UserException::class,
+ 'Root is invalid or does not exist: /foo/bar',
+ new InvalidRootException('Root is invalid or does not exist: /foo/bar'),
+ ],
+ [
+ UserException::class,
+ 'Foo bar',
+ new ConnectionErrorException('Foo bar'),
+ ],
+ [
+ ApplicationException::class,
+ 'Foo bar',
+ new \RuntimeException('Foo bar'),
+ ],
+ [
+ UserException::class,
+ sprintf(
+ 'Connection was terminated. Check that the connection is not blocked by Firewall ' .
+ 'or set ignore passive address: Operation now in progress (115)'
+ ),
+ new \ErrorException('Operation now in progress (115)'),
+ ],
+ ];
+ }
+
+ public function downloadExceptionMessageProvider(): array
+ {
+ $filePtah = '/foo/bar.jpg';
+ $progressMessage = 'Operation now in progress (115)';
+
+ return [
+ [
+ UserException::class,
+ sprintf('Error while trying to download file: File not found at path: %s', $filePtah),
+ new FileNotFoundException($filePtah),
+ ],
+ [
+ UserException::class,
+ sprintf(
+ 'Connection was terminated. Check that the connection is not blocked by Firewall ' .
+ 'or set ignore passive address: %s',
+ $progressMessage
+ ),
+ new \ErrorException($progressMessage),
+ ],
+ [
+ ApplicationException::class,
+ 'Foo Bar',
+ new \ErrorException('Foo Bar'),
+ ],
+ [
+ ApplicationException::class,
+ 'Foo Bar',
+ new \RuntimeException('Foo Bar'),
+ ],
+ ];
+ }
+}
diff --git a/tests/phpunit/FileStateRegistryTest.php b/tests/phpunit/FileStateRegistryTest.php
new file mode 100644
index 0000000..3fe875b
--- /dev/null
+++ b/tests/phpunit/FileStateRegistryTest.php
@@ -0,0 +1,67 @@
+assertSame(
+ $file['expected'],
+ $registry->shouldBeFileUpdated($file['name'], $file['timestamp']),
+ sprintf("Bad decision for %s with timestamp %s", $file['name'], $file['timestamp'])
+ );
+ }
+ }
+
+ public function firstRunDataProvider(): array
+ {
+ $firstRunFiles = [
+ ['name' => 'dir1/files/1.txt', 'timestamp' => 900, 'expected' => false],
+ ['name' => 'dir1/files/2.txt', 'timestamp' => 1000, 'expected' => true],
+ ['name' => 'dir1/files/3.txt', 'timestamp' => 1002, 'expected' => true],
+ ['name' => 'dir1/files/4.txt', 'timestamp' => 1005, 'expected' => true],
+ ];
+
+ $sameSecondUpdateFiles = [
+ ['name' => '/dir2/file2.csv', 'timestamp' => 1000, 'expected' => false],
+ ['name' => '/dir2/file1.csv', 'timestamp' => 1000, 'expected' => false],
+ ['name' => '/dir2/file3.csv', 'timestamp' => 1000, 'expected' => true],
+ ['name' => '/dir2/file5.csv', 'timestamp' => 1000, 'expected' => true],
+ ['name' => '/dir3/file1.csv', 'timestamp' => 1001, 'expected' => true],
+ ['name' => '/dir3/file2.csv', 'timestamp' => 1001, 'expected' => true],
+ ['name' => '/dir5/file3.csv', 'timestamp' => 1005, 'expected' => true],
+ ];
+
+ $lastFiles = [
+ '/dir2/file1.csv',
+ '/dir2/file2.csv',
+ ];
+
+ return [
+ 'firstRun' => [$firstRunFiles, $this->getRegistry(1000, [])],
+ 'secondRunWithLastFiles' => [$sameSecondUpdateFiles, $this->getRegistry(1000, $lastFiles)],
+ ];
+ }
+
+ private function getRegistry(int $newestTimestamp, array $files): FileStateRegistry
+ {
+ $stateFile = [
+ FileStateRegistry::STATE_FILE_KEY => [
+ FileStateRegistry::NEWEST_TIMESTAMP_KEY => $newestTimestamp,
+ FileStateRegistry::FILES_WITH_NEWEST_TIMESTAMP_KEY => $files,
+ ],
+ ];
+
+ return new FileStateRegistry($stateFile);
+ }
+}
diff --git a/tests/phpunit/GlobValidatorTest.php b/tests/phpunit/GlobValidatorTest.php
new file mode 100644
index 0000000..1c1032a
--- /dev/null
+++ b/tests/phpunit/GlobValidatorTest.php
@@ -0,0 +1,50 @@
+assertTrue(GlobValidator::validatePathAgainstGlob($path, $glob));
+ }
+
+ public function positiveDataProvider(): array
+ {
+ return [
+ ['/files/data/test.txt', '/*/*/*.txt'],
+ ['files/data/test.txt', '*/*/*.txt'],
+ ['files/data/test.txt', '/*/data/test.*'],
+ ['files/data/test.txt', '/**/*'],
+ ['files/data/test.txt', 'files/data/test.txt'],
+ ['files/data/test.txt', '/files/data/test.txt'],
+ ];
+ }
+
+ /**
+ * @group Glob
+ * @dataProvider negativeDataProvider
+ */
+ public function testNegativeGlobMatchingPatterns(string $path, string $glob): void
+ {
+ $this->assertFalse(GlobValidator::validatePathAgainstGlob($path, $glob));
+ }
+
+ public function negativeDataProvider(): array
+ {
+ return [
+ ['files/data/func1.txt', 'file/*/*.txt'],
+ ['files/data/func1.ptx', 'files/*/*.txt'],
+ ['/files/data/func1.bin', '*/*/*/*/*.bin'],
+ ];
+ }
+}
diff --git a/tests/phpunit/ItemFilterTest.php b/tests/phpunit/ItemFilterTest.php
new file mode 100644
index 0000000..5d38c05
--- /dev/null
+++ b/tests/phpunit/ItemFilterTest.php
@@ -0,0 +1,27 @@
+ 'directory'],
+ ['type' => ItemFilter::FTP_FILETYPE_FILE],
+ ['type' => ItemFilter::FTP_FILETYPE_FILE],
+ ];
+
+ $expected = [
+ ['type' => ItemFilter::FTP_FILETYPE_FILE],
+ ['type' => ItemFilter::FTP_FILETYPE_FILE],
+ ];
+
+ $this->assertSame($expected, ItemFilter::getOnlyFiles($items));
+ }
+}
diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php
new file mode 100644
index 0000000..7d169dc
--- /dev/null
+++ b/tests/phpunit/bootstrap.php
@@ -0,0 +1,5 @@
+