diff --git a/.drone.star b/.drone.star new file mode 100644 index 0000000..db16ccf --- /dev/null +++ b/.drone.star @@ -0,0 +1,842 @@ +MINIO_MC = "minio/mc:RELEASE.2020-12-18T10-53-53Z" +OC_CI_ALPINE = "owncloudci/alpine:latest" +OC_CI_BAZEL_BUILDIFIER = "owncloudci/bazel-buildifier" +OC_CI_DRONE_CANCEL_PREVIOUS_BUILDS = "owncloudci/drone-cancel-previous-builds" +OC_CI_DRONE_SKIP_PIPELINE = "owncloudci/drone-skip-pipeline" +OC_CI_PHP = "owncloudci/php:%s" +PLUGINS_S3 = "plugins/s3" +PLUGINS_S3_CACHE = "plugins/s3-cache:1" +PLUGINS_SLACK = "plugins/slack:1" +SONARSOURCE_SONAR_SCANNER_CLI = "sonarsource/sonar-scanner-cli" + +DEFAULT_PHP_VERSION = "7.4" + +dir = { + "base": "/var/www/owncloud", +} + +config = { + "rocketchat": { + "channel": "builds", + "from_secret": "private_rocketchat", + }, + "branches": [ + "master", + ], + "codestyle": True, + "phpstan": False, + "phan": False, + "phpunit": { + "php74": { + "phpVersions": ["7.4"], + "coverage": False, + "extraCommandsBeforeTestRun": [ + "apt update -y", + "apt-get install php7.4-xdebug -y", + ], + }, + "php80": { + "phpVersions": ["8.0"], + "coverage": False, + "extraCommandsBeforeTestRun": [ + "apt update -y", + "apt-get install php8.0-xdebug -y", + ], + }, + "php81": { + "phpVersions": ["8.1"], + "coverage": False, + "extraCommandsBeforeTestRun": [ + "apt update -y", + "apt-get install php8.1-xdebug -y", + ], + }, + }, +} + +def main(ctx): + before = beforePipelines(ctx) + + coverageTests = coveragePipelines(ctx) + if (coverageTests == False): + print("Errors detected in coveragePipelines. Review messages above.") + return [] + + dependsOn(before, coverageTests) + + nonCoverageTests = nonCoveragePipelines(ctx) + if (nonCoverageTests == False): + print("Errors detected in nonCoveragePipelines. Review messages above.") + return [] + + dependsOn(before, nonCoverageTests) + + if (coverageTests == []): + afterCoverageTests = [] + else: + afterCoverageTests = afterCoveragePipelines(ctx) + dependsOn(coverageTests, afterCoverageTests) + + after = afterPipelines(ctx) + dependsOn(afterCoverageTests + nonCoverageTests, after) + + return before + coverageTests + afterCoverageTests + nonCoverageTests + after + +def beforePipelines(ctx): + return codestyle(ctx) + cancelPreviousBuilds() + phpstan(ctx) + phan(ctx) + phplint(ctx) + checkStarlark() + +def coveragePipelines(ctx): + # All unit test pipelines that have coverage or other test analysis reported + phpUnitPipelines = phpTests(ctx, "phpunit", True) + phpIntegrationPipelines = phpTests(ctx, "phpintegration", True) + if (phpUnitPipelines == False) or (phpIntegrationPipelines == False): + return False + + return phpUnitPipelines + phpIntegrationPipelines + +def nonCoveragePipelines(ctx): + # All unit test pipelines that do not have coverage or other test analysis reported + phpUnitPipelines = phpTests(ctx, "phpunit", False) + phpIntegrationPipelines = phpTests(ctx, "phpintegration", False) + if (phpUnitPipelines == False) or (phpIntegrationPipelines == False): + return False + + return phpUnitPipelines + phpIntegrationPipelines + +def afterCoveragePipelines(ctx): + return [ + sonarAnalysis(ctx), + ] + +def afterPipelines(ctx): + return [ + notify(), + ] + +def codestyle(ctx): + pipelines = [] + + if "codestyle" not in config: + return pipelines + + default = { + "phpVersions": [DEFAULT_PHP_VERSION], + } + + if "defaults" in config: + if "codestyle" in config["defaults"]: + for item in config["defaults"]["codestyle"]: + default[item] = config["defaults"]["codestyle"][item] + + codestyleConfig = config["codestyle"] + + if type(codestyleConfig) == "bool": + if codestyleConfig: + # the config has "codestyle" true, so specify an empty dict that will get the defaults + codestyleConfig = {} + else: + return pipelines + + if len(codestyleConfig) == 0: + # "codestyle" is an empty dict, so specify a single section that will get the defaults + codestyleConfig = {"doDefault": {}} + + for category, matrix in codestyleConfig.items(): + params = {} + for item in default: + params[item] = matrix[item] if item in matrix else default[item] + + for phpVersion in params["phpVersions"]: + name = "coding-standard-php%s" % phpVersion + + result = { + "kind": "pipeline", + "type": "docker", + "name": name, + "workspace": { + "base": dir["base"], + "path": "server/apps/%s" % ctx.repo.name, + }, + "steps": skipIfUnchanged(ctx, "lint") + + [ + { + "name": "coding-standard", + "image": OC_CI_PHP % phpVersion, + "commands": [ + "make test-php-style", + ], + }, + ], + "depends_on": [], + "trigger": { + "ref": [ + "refs/pull/**", + "refs/tags/**", + ], + }, + } + + for branch in config["branches"]: + result["trigger"]["ref"].append("refs/heads/%s" % branch) + + pipelines.append(result) + + return pipelines + +def cancelPreviousBuilds(): + return [{ + "kind": "pipeline", + "type": "docker", + "name": "cancel-previous-builds", + "clone": { + "disable": True, + }, + "steps": [{ + "name": "cancel-previous-builds", + "image": OC_CI_DRONE_CANCEL_PREVIOUS_BUILDS, + "settings": { + "DRONE_TOKEN": { + "from_secret": "drone_token", + }, + }, + }], + "depends_on": [], + "trigger": { + "ref": [ + "refs/pull/**", + ], + }, + }] + +def phpstan(ctx): + pipelines = [] + + if "phpstan" not in config: + return pipelines + + default = { + "phpVersions": [DEFAULT_PHP_VERSION], + } + + if "defaults" in config: + if "phpstan" in config["defaults"]: + for item in config["defaults"]["phpstan"]: + default[item] = config["defaults"]["phpstan"][item] + + phpstanConfig = config["phpstan"] + + if type(phpstanConfig) == "bool": + if phpstanConfig: + # the config has "phpstan" true, so specify an empty dict that will get the defaults + phpstanConfig = {} + else: + return pipelines + + if len(phpstanConfig) == 0: + # "phpstan" is an empty dict, so specify a single section that will get the defaults + phpstanConfig = {"doDefault": {}} + + for category, matrix in phpstanConfig.items(): + params = {} + for item in default: + params[item] = matrix[item] if item in matrix else default[item] + + for phpVersion in params["phpVersions"]: + name = "phpstan-php%s" % phpVersion + + result = { + "kind": "pipeline", + "type": "docker", + "name": name, + "workspace": { + "base": dir["base"], + "path": "server/apps/%s" % ctx.repo.name, + }, + "steps": skipIfUnchanged(ctx, "lint") + + [ + { + "name": "phpstan", + "image": OC_CI_PHP % phpVersion, + "commands": [ + "make test-php-phpstan", + ], + }, + ], + "depends_on": [], + "trigger": { + "ref": [ + "refs/pull/**", + "refs/tags/**", + ], + }, + } + + for branch in config["branches"]: + result["trigger"]["ref"].append("refs/heads/%s" % branch) + + pipelines.append(result) + + return pipelines + +def phan(ctx): + pipelines = [] + + if "phan" not in config: + return pipelines + + default = { + "phpVersions": [DEFAULT_PHP_VERSION], + } + + if "defaults" in config: + if "phan" in config["defaults"]: + for item in config["defaults"]["phan"]: + default[item] = config["defaults"]["phan"][item] + + phanConfig = config["phan"] + + if type(phanConfig) == "bool": + if phanConfig: + # the config has "phan" true, so specify an empty dict that will get the defaults + phanConfig = {} + else: + return pipelines + + if len(phanConfig) == 0: + # "phan" is an empty dict, so specify a single section that will get the defaults + phanConfig = {"doDefault": {}} + + for category, matrix in phanConfig.items(): + params = {} + for item in default: + params[item] = matrix[item] if item in matrix else default[item] + + for phpVersion in params["phpVersions"]: + name = "phan-php%s" % phpVersion + + result = { + "kind": "pipeline", + "type": "docker", + "name": name, + "workspace": { + "base": dir["base"], + "path": "server/apps/%s" % ctx.repo.name, + }, + "steps": skipIfUnchanged(ctx, "lint") + + [ + { + "name": "phan", + "image": OC_CI_PHP % phpVersion, + "commands": [ + "make test-php-phan", + ], + }, + ], + "depends_on": [], + "trigger": { + "ref": [ + "refs/pull/**", + "refs/tags/**", + ], + }, + } + + for branch in config["branches"]: + result["trigger"]["ref"].append("refs/heads/%s" % branch) + + pipelines.append(result) + + return pipelines + +def phpTests(ctx, testType, withCoverage): + pipelines = [] + + if testType not in config: + return pipelines + + errorFound = False + + # The default PHP unit test settings for a PR. + prDefault = { + "phpVersions": [DEFAULT_PHP_VERSION], + "coverage": True, + "includeKeyInMatrixName": False, + "extraSetup": [], + "extraEnvironment": {}, + "extraCommandsBeforeTestRun": [], + "skip": False, + } + + # The default PHP unit test settings for the cron job (usually runs nightly). + cronDefault = { + "phpVersions": [DEFAULT_PHP_VERSION], + "coverage": True, + "includeKeyInMatrixName": False, + "extraSetup": [], + "extraEnvironment": {}, + "extraCommandsBeforeTestRun": [], + "skip": False, + } + + if (ctx.build.event == "cron"): + default = cronDefault + else: + default = prDefault + + if "defaults" in config: + if testType in config["defaults"]: + for item in config["defaults"][testType]: + default[item] = config["defaults"][testType][item] + + phpTestConfig = config[testType] + + if type(phpTestConfig) == "bool": + if phpTestConfig: + # the config has just True, so specify an empty dict that will get the defaults + phpTestConfig = {} + else: + return pipelines + + if len(phpTestConfig) == 0: + # the PHP test config is an empty dict, so specify a single section that will get the defaults + phpTestConfig = {"doDefault": {}} + + for category, matrix in phpTestConfig.items(): + params = {} + for item in default: + params[item] = matrix[item] if item in matrix else default[item] + + if params["skip"]: + continue + + # if we only want pipelines with coverage, and this pipeline does not do coverage, then do not include it + if withCoverage and not params["coverage"]: + continue + + # if we only want pipelines without coverage, and this pipeline does coverage, then do not include it + if not withCoverage and params["coverage"]: + continue + + for phpVersion in params["phpVersions"]: + if testType == "phpunit": + if params["coverage"]: + command = "make test-php-unit-dbg" + else: + command = "make test-php-unit" + elif params["coverage"]: + command = "make test-php-integration-dbg" + else: + command = "make test-php-integration" + + # Get the first 3 characters of the PHP version (7.4 or 8.0 etc) + # And use that for constructing the pipeline name + # That helps shorten pipeline names when using owncloud-ci images + # that have longer names like 7.4-ubuntu20.04 + phpVersionForPipelineName = phpVersion[0:3] + + keyString = "-" + category if params["includeKeyInMatrixName"] else "" + name = "%s%s-php%s" % (testType, keyString, phpVersionForPipelineName) + maxLength = 50 + nameLength = len(name) + if nameLength > maxLength: + print("Error: generated phpunit stage name of length", nameLength, "is not supported. The maximum length is " + str(maxLength) + ".", name) + errorFound = True + + result = { + "kind": "pipeline", + "type": "docker", + "name": name, + "workspace": { + "base": dir["base"], + "path": "server/apps/%s" % ctx.repo.name, + }, + "steps": skipIfUnchanged(ctx, "unit-tests") + + params["extraSetup"] + + [ + { + "name": "%s-tests" % testType, + "image": OC_CI_PHP % phpVersion, + "environment": params["extraEnvironment"], + "commands": params["extraCommandsBeforeTestRun"] + [ + command, + ], + }, + ], + "depends_on": [], + "trigger": { + "ref": [ + "refs/pull/**", + "refs/tags/**", + ], + }, + } + + if params["coverage"]: + result["steps"].append({ + "name": "coverage-rename", + "image": OC_CI_PHP % phpVersion, + "commands": [ + "mv tests/output/clover.xml tests/output/clover-%s.xml" % (name), + ], + }) + result["steps"].append({ + "name": "coverage-cache-1", + "image": PLUGINS_S3, + "settings": { + "endpoint": { + "from_secret": "cache_s3_endpoint", + }, + "bucket": "cache", + "source": "tests/output/clover-%s.xml" % (name), + "target": "%s/%s" % (ctx.repo.slug, ctx.build.commit + "-${DRONE_BUILD_NUMBER}"), + "path_style": True, + "strip_prefix": "tests/output", + "access_key": { + "from_secret": "cache_s3_access_key", + }, + "secret_key": { + "from_secret": "cache_s3_secret_key", + }, + }, + }) + + for branch in config["branches"]: + result["trigger"]["ref"].append("refs/heads/%s" % branch) + + pipelines.append(result) + + if errorFound: + return False + + return pipelines + +def sonarAnalysis(ctx, phpVersion = DEFAULT_PHP_VERSION): + sonar_env = { + "SONAR_TOKEN": { + "from_secret": "sonar_token", + }, + "SONAR_SCANNER_OPTS": "-Xdebug", + } + + if ctx.build.event == "pull_request": + sonar_env.update({ + "SONAR_PULL_REQUEST_BASE": "%s" % (ctx.build.target), + "SONAR_PULL_REQUEST_BRANCH": "%s" % (ctx.build.source), + "SONAR_PULL_REQUEST_KEY": "%s" % (ctx.build.ref.replace("refs/pull/", "").split("/")[0]), + }) + + repo_slug = ctx.build.source_repo if ctx.build.source_repo else ctx.repo.slug + + result = { + "kind": "pipeline", + "type": "docker", + "name": "sonar-analysis", + "workspace": { + "base": dir["base"], + "path": "server/apps/%s" % ctx.repo.name, + }, + "clone": { + "disable": True, # Sonarcloud does not apply issues on already merged branch + }, + "steps": [ + { + "name": "clone", + "image": OC_CI_ALPINE, + "commands": [ + "git clone https://github.com/%s.git ." % repo_slug, + "git checkout $DRONE_COMMIT", + ], + }, + ] + + skipIfUnchanged(ctx, "unit-tests") + + cacheRestore() + + composerInstall(phpVersion) + + [ + { + "name": "sync-from-cache", + "image": MINIO_MC, + "environment": { + "MC_HOST_cache": { + "from_secret": "cache_s3_connection_url", + }, + }, + "commands": [ + "mkdir -p results", + "mc mirror cache/cache/%s/%s results/" % (ctx.repo.slug, ctx.build.commit + "-${DRONE_BUILD_NUMBER}"), + ], + }, + { + "name": "list-coverage-results", + "image": OC_CI_PHP % phpVersion, + "commands": [ + "ls -l results", + ], + }, + { + "name": "sonarcloud", + "image": SONARSOURCE_SONAR_SCANNER_CLI, + "environment": sonar_env, + "when": { + "instance": [ + "drone.owncloud.services", + "drone.owncloud.com", + ], + }, + }, + { + "name": "purge-cache", + "image": MINIO_MC, + "environment": { + "MC_HOST_cache": { + "from_secret": "cache_s3_connection_url", + }, + }, + "commands": [ + "mc rm --recursive --force cache/cache/%s/%s" % (ctx.repo.slug, ctx.build.commit + "-${DRONE_BUILD_NUMBER}"), + ], + }, + ], + "depends_on": [], + "trigger": { + "ref": [ + "refs/heads/master", + "refs/pull/**", + "refs/tags/**", + ], + }, + } + + for branch in config["branches"]: + result["trigger"]["ref"].append("refs/heads/%s" % branch) + + return result + +def notify(): + result = { + "kind": "pipeline", + "type": "docker", + "name": "chat-notifications", + "clone": { + "disable": True, + }, + "steps": [ + { + "name": "notify-rocketchat", + "image": PLUGINS_SLACK, + "settings": { + "webhook": { + "from_secret": config["rocketchat"]["from_secret"], + }, + "channel": config["rocketchat"]["channel"], + }, + }, + ], + "depends_on": [], + "trigger": { + "ref": [ + "refs/tags/**", + ], + "status": [ + "success", + "failure", + ], + }, + } + + for branch in config["branches"]: + result["trigger"]["ref"].append("refs/heads/%s" % branch) + + return result + +def cacheRestore(): + return [{ + "name": "cache-restore", + "image": PLUGINS_S3_CACHE, + "settings": { + "access_key": { + "from_secret": "cache_s3_access_key", + }, + "endpoint": { + "from_secret": "cache_s3_endpoint", + }, + "restore": True, + "secret_key": { + "from_secret": "cache_s3_secret_key", + }, + }, + "when": { + "instance": [ + "drone.owncloud.services", + "drone.owncloud.com", + ], + }, + }] + +def composerInstall(phpVersion): + return [{ + "name": "composer-install", + "image": OC_CI_PHP % phpVersion, + "environment": { + "COMPOSER_HOME": "/drone/src/.cache/composer", + }, + "commands": [ + "make vendor", + ], + }] + +def dependsOn(earlierStages, nextStages): + for earlierStage in earlierStages: + for nextStage in nextStages: + nextStage["depends_on"].append(earlierStage["name"]) + +def checkStarlark(): + return [{ + "kind": "pipeline", + "type": "docker", + "name": "check-starlark", + "steps": [ + { + "name": "format-check-starlark", + "image": OC_CI_BAZEL_BUILDIFIER, + "commands": [ + "buildifier --mode=check .drone.star", + ], + }, + { + "name": "show-diff", + "image": OC_CI_BAZEL_BUILDIFIER, + "commands": [ + "buildifier --mode=fix .drone.star", + "git diff", + ], + "when": { + "status": [ + "failure", + ], + }, + }, + ], + "depends_on": [], + "trigger": { + "ref": [ + "refs/pull/**", + ], + }, + }] + +def phplint(ctx): + pipelines = [] + + if "phplint" not in config: + return pipelines + + if type(config["phplint"]) == "bool": + if not config["phplint"]: + return pipelines + + result = { + "kind": "pipeline", + "type": "docker", + "name": "lint-test", + "workspace": { + "base": dir["base"], + "path": "server/apps/%s" % ctx.repo.name, + }, + "steps": skipIfUnchanged(ctx, "lint") + + lintTest(), + "depends_on": [], + "trigger": { + "ref": [ + "refs/heads/master", + "refs/tags/**", + "refs/pull/**", + ], + }, + } + + for branch in config["branches"]: + result["trigger"]["ref"].append("refs/heads/%s" % branch) + + pipelines.append(result) + + return pipelines + +def lintTest(): + return [{ + "name": "lint-test", + "image": OC_CI_PHP % DEFAULT_PHP_VERSION, + "commands": [ + "make test-lint", + ], + }] + +def skipIfUnchanged(ctx, type): + if ("full-ci" in ctx.build.title.lower()): + return [] + + skip_step = { + "name": "skip-if-unchanged", + "image": OC_CI_DRONE_SKIP_PIPELINE, + "when": { + "event": [ + "pull_request", + ], + }, + } + + # these files are not relevant for test pipelines + # if only files in this array are changed, then don't even run the "lint" + # pipelines (like code-style, phan, phpstan...) + allow_skip_if_changed = [ + "^.github/.*", + "^changelog/.*", + "^docs/.*", + "CHANGELOG.md", + "CONTRIBUTING.md", + "LICENSE", + "LICENSE.md", + "README.md", + ] + + if type == "lint": + skip_step["settings"] = { + "ALLOW_SKIP_CHANGED": allow_skip_if_changed, + } + return [skip_step] + + if type == "unit-tests": + # if any of these files are touched then run all unit tests + # note: some oC10 apps have various directories like handlers, rules, etc. + # so those are all listed here so that this starlark code can be + # the same for every oC10 app. + unit_files = [ + "^tests/integration/.*", + "^tests/js/.*", + "^tests/Unit/.*", + "^tests/unit/.*", + "^appinfo/.*", + "^command/.*", + "^controller/.*", + "^css/.*", + "^db/.*", + "^handlers/.*", + "^js/.*", + "^lib/.*", + "^rules/.*", + "^src/.*", + "^templates/.*", + "composer.json", + "composer.lock", + "Makefile", + "package.json", + "package-lock.json", + "phpunit.xml", + "yarn.lock", + "sonar-project.properties", + ] + skip_step["settings"] = { + "DISALLOW_SKIP_CHANGED": unit_files, + } + return [skip_step] + + return [] diff --git a/.gitignore b/.gitignore index 73c83b4..289a1eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ ./idea /build/ -/vendor/ \ No newline at end of file +/vendor/ +composer.lock +vendor-bin/**/vendor +vendor-bin/**/composer.lock +.php-cs-fixer.cache +.phpunit.result.cache + +# drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml +.drone.yml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..c12c70f --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,13 @@ +setUsingCache(true) + ->getFinder() + ->in(__DIR__) + ->exclude('build') + ->exclude('vendor-bin') + ->exclude('vendor'); + +return $config; \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 55054a1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,22 +0,0 @@ -language: php -php: - - 7.1 - - 7.2 - - 7.3 - - 7.4 - -branches: - only: - - master - -install: - - composer install --dev --no-interaction - -script: - - mkdir -p build/logs - - cd tests - - ../vendor/bin/phpunit --coverage-clover ../build/logs/clover.xml --configuration phpunit.xml - -after_script: - # Create coverage report - - bash <(curl -s https://codecov.io/bash) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..da56f94 --- /dev/null +++ b/Makefile @@ -0,0 +1,92 @@ +SHELL := /bin/bash + +COMPOSER_BIN := $(shell command -v composer 2> /dev/null) + +# bin file definitions +PHPUNIT=php -d zend.enable_gc=0 vendor/bin/phpunit +PHPUNITDBG=phpdbg -qrr -d memory_limit=4096M -d zend.enable_gc=0 vendor/bin/phpunit +PHP_CS_FIXER=php -d zend.enable_gc=0 vendor-bin/owncloud-codestyle/vendor/bin/php-cs-fixer +PHAN=php -d zend.enable_gc=0 vendor-bin/phan/vendor/bin/phan +PHPSTAN=php -d zend.enable_gc=0 vendor-bin/phpstan/vendor/bin/phpstan + +.PHONY: clean +clean: clean-deps + +# Installs and updates the composer dependencies. +.PHONY: composer +composer: + composer install --prefer-dist + composer update --prefer-dist + +##------------ +## Tests +##------------ + +.PHONY: test-php-unit +test-php-unit: ## Run php unit tests +test-php-unit: vendor/bin/phpunit + $(PHPUNIT) --configuration ./phpunit.xml --testsuite TarStreamer + +.PHONY: test-php-unit-dbg +test-php-unit-dbg: ## Run php unit tests using phpdbg +test-php-unit-dbg: vendor/bin/phpunit + $(PHPUNITDBG) --configuration ./phpunit.xml --testsuite TarStreamer + +.PHONY: test-php-style +test-php-style: ## Run php-cs-fixer and check owncloud code-style +test-php-style: vendor-bin/owncloud-codestyle/vendor + $(PHP_CS_FIXER) fix -v --diff --allow-risky yes --dry-run + +.PHONY: test-php-style-fix +test-php-style-fix: ## Run php-cs-fixer and fix code style issues +test-php-style-fix: vendor-bin/owncloud-codestyle/vendor + $(PHP_CS_FIXER) fix -v --diff --allow-risky yes + +.PHONY: test-php-phan +test-php-phan: ## Run phan +test-php-phan: vendor-bin/phan/vendor + $(PHAN) --config-file .phan/config.php --require-config-exists + +.PHONY: test-php-phpstan +test-php-phpstan: ## Run phpstan +test-php-phpstan: vendor-bin/phpstan/vendor + $(PHPSTAN) analyse --memory-limit=4G --configuration=./phpstan.neon --no-progress --level=5 appinfo lib + +.PHONY: clean-deps +clean-deps: + rm -rf ./vendor + rm -Rf vendor-bin/**/vendor vendor-bin/**/composer.lock + +# +# Dependency management +#-------------------------------------- + +composer.lock: composer.json + @echo composer.lock is not up to date. + +vendor: composer.lock + composer install --no-dev + +vendor/bin/phpunit: composer.lock + composer install + +vendor/bamarni/composer-bin-plugin: composer.lock + composer install + +vendor-bin/owncloud-codestyle/vendor: vendor/bamarni/composer-bin-plugin vendor-bin/owncloud-codestyle/composer.lock + composer bin owncloud-codestyle install --no-progress + +vendor-bin/owncloud-codestyle/composer.lock: vendor-bin/owncloud-codestyle/composer.json + @echo owncloud-codestyle composer.lock is not up to date. + +vendor-bin/phan/vendor: vendor/bamarni/composer-bin-plugin vendor-bin/phan/composer.lock + composer bin phan install --no-progress + +vendor-bin/phan/composer.lock: vendor-bin/phan/composer.json + @echo phan composer.lock is not up to date. + +vendor-bin/phpstan/vendor: vendor/bamarni/composer-bin-plugin vendor-bin/phpstan/composer.lock + composer bin phpstan install --no-progress + +vendor-bin/phpstan/composer.lock: vendor-bin/phpstan/composer.json + @echo phpstan composer.lock is not up to date. diff --git a/composer.json b/composer.json index 5244697..6789d1e 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,8 @@ "php": ">=7.1" }, "config" : { - "platform": { - "php": "7.1" + "allow-plugins": { + "bamarni/composer-bin-plugin": true } }, "autoload": { @@ -30,8 +30,14 @@ ] }, "require-dev": { - "phpunit/phpunit": "^7.5", + "phpunit/phpunit": "^7.5|^8.5|^9.6", "pear/pear-core-minimal": "v1.10.10", - "pear/archive_tar": "~1.4" + "pear/archive_tar": "~1.4", + "bamarni/composer-bin-plugin": "^1.5" + }, + "extra": { + "bamarni-bin": { + "bin-links": false + } } } diff --git a/tests/phpunit.xml b/phpunit.xml similarity index 72% rename from tests/phpunit.xml rename to phpunit.xml index 2bed36e..e80e212 100644 --- a/tests/phpunit.xml +++ b/phpunit.xml @@ -1,7 +1,7 @@ - + - ./ + ./tests diff --git a/src/TarHeader.php b/src/TarHeader.php index c852be6..d4f4736 100644 --- a/src/TarHeader.php +++ b/src/TarHeader.php @@ -37,32 +37,32 @@ class TarHeader { private $reserved = ''; - public function setName($name){ + public function setName($name) { $this->name = $name; return $this; } - public function setSize($size){ + public function setSize($size) { $this->size = $size; return $this; } - public function setMtime($mtime){ + public function setMtime($mtime) { $this->mtime = $mtime; return $this; } - public function setTypeflag($typeflag){ + public function setTypeflag($typeflag) { $this->typeflag = $typeflag; return $this; } - public function setPrefix($prefix){ + public function setPrefix($prefix) { $this->prefix = $prefix; return $this; } - public function getHeader(){ + public function getHeader() { $fields = [ ['a100', substr($this->name, 0, 100)], ['a8', str_pad($this->mode, 7, '0', STR_PAD_LEFT)], @@ -89,11 +89,11 @@ public function getHeader(){ // Compute header checksum $checksum = str_pad(decoct($this->computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT); - for ($i = 0; $i < 6; $i++){ + for ($i = 0; $i < 6; $i++) { $header[(148 + $i)] = substr($checksum, $i, 1); } - $header[154] = chr(0); - $header[155] = chr(32); + $header[154] = \chr(0); + $header[155] = \chr(32); return $header; } @@ -104,11 +104,11 @@ public function getHeader(){ * @param array $fields key being the format string and value being the data to pack * @return string binary packed data returned from pack() */ - protected function packFields($fields){ - list ($fmt, $args) = ['', []]; + protected function packFields($fields) { + list($fmt, $args) = ['', []]; // populate format string and argument list - foreach ($fields as $field){ + foreach ($fields as $field) { $fmt .= $field[0]; $args[] = $field[1]; } @@ -117,7 +117,7 @@ protected function packFields($fields){ array_unshift($args, $fmt); // build output string from header and compressed data - return call_user_func_array('pack', $args); + return \call_user_func_array('pack', $args); } /** @@ -126,15 +126,15 @@ protected function packFields($fields){ * @param string $header * @return string unsigned checksum */ - protected function computeUnsignedChecksum($header){ + protected function computeUnsignedChecksum($header) { $unsignedChecksum = 0; - for ($i = 0; $i < 512; $i++){ - $unsignedChecksum += ord($header[$i]); + for ($i = 0; $i < 512; $i++) { + $unsignedChecksum += \ord($header[$i]); } - for ($i = 0; $i < 8; $i++){ - $unsignedChecksum -= ord($header[148 + $i]); + for ($i = 0; $i < 8; $i++) { + $unsignedChecksum -= \ord($header[148 + $i]); } - $unsignedChecksum += ord(" ") * 8; + $unsignedChecksum += \ord(" ") * 8; return $unsignedChecksum; } diff --git a/src/TarStreamer.php b/src/TarStreamer.php index 5ee7675..71caae6 100644 --- a/src/TarStreamer.php +++ b/src/TarStreamer.php @@ -5,11 +5,10 @@ use ownCloud\TarStreamer\TarHeader; class TarStreamer { - - const REGTYPE = 0; - const DIRTYPE = 5; - const XHDTYPE = 'x'; - const LONGNAMETYPE = 'L'; + public const REGTYPE = 0; + public const DIRTYPE = 5; + public const XHDTYPE = 'x'; + public const LONGNAMETYPE = 'L'; /** * Process in 1 MB chunks @@ -24,13 +23,13 @@ class TarStreamer { * * @param array $options */ - public function __construct($options = []){ - if (isset($options['outstream'])){ + public function __construct($options = []) { + if (isset($options['outstream'])) { $this->outStream = $options['outstream']; } else { $this->outStream = fopen('php://output', 'w'); // turn off output buffering - while (ob_get_level() > 0){ + while (ob_get_level() > 0) { ob_end_flush(); } } @@ -49,13 +48,13 @@ public function __construct($options = []){ * @param string $contentType Content mime type to be set (optional, default 'application/x-tar') * @throws \Exception */ - public function sendHeaders($archiveName = 'archive.tar', $contentType = 'application/x-tar'){ + public function sendHeaders($archiveName = 'archive.tar', $contentType = 'application/x-tar') { $encodedArchiveName = rawurlencode($archiveName); - if (headers_sent($headerFile, $headerLine)){ + if (headers_sent($headerFile, $headerLine)) { throw new \Exception("Unable to send file $encodedArchiveName. HTML Headers have already been sent from $headerFile in line $headerLine"); } $buffer = ob_get_contents(); - if (!empty($buffer)){ + if (!empty($buffer)) { throw new \Exception("Unable to send file $encodedArchiveName. Output buffer already contains text (typically warnings or errors)."); } @@ -71,14 +70,14 @@ public function sendHeaders($archiveName = 'archive.tar', $contentType = 'applic ]; // Use UTF-8 filenames when not using Internet Explorer - if(strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') > 0) { + if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') > 0) { header('Content-Disposition: attachment; filename="' . rawurlencode($archiveName) . '"'); - } else { + } else { header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($archiveName) . '; filename="' . rawurlencode($archiveName) . '"'); } - foreach ($headers as $key => $value){ + foreach ($headers as $key => $value) { header("$key: $value"); } } @@ -94,15 +93,15 @@ public function sendHeaders($archiveName = 'archive.tar', $contentType = 'applic * * int timestamp: timestamp for the file (default: current time) * @return bool $success */ - public function addFileFromStream($stream, $filePath, $size, $options = []){ - if (!is_resource($stream) || get_resource_type($stream) != 'stream'){ + public function addFileFromStream($stream, $filePath, $size, $options = []) { + if (!\is_resource($stream) || get_resource_type($stream) != 'stream') { return false; } $this->initFileStreamTransfer($filePath, self::REGTYPE, $size, $options); // send file blocks - while ($data = fread($stream, $this->blockSize)){ + while ($data = fread($stream, $this->blockSize)) { // send data $this->streamFilePart($data); } @@ -117,12 +116,12 @@ public function addFileFromStream($stream, $filePath, $size, $options = []){ * Explicitly adds a directory to the tar (necessary for empty directories) * * @param string $name Name (path) of the directory - * @param array $opt Additional options to set + * @param array $opt Additional options to set * Valid options are: * * int timestamp: timestamp for the file (default: current time) * @return void */ - public function addEmptyDir($name, $opt = []){ + public function addEmptyDir($name, $opt = []) { $opt['type'] = self::DIRTYPE; // send header @@ -137,7 +136,7 @@ public function addEmptyDir($name, $opt = []){ * A closed archive can no longer have new files added to it. After * closing, the file is completely written to the output stream. * @return bool $success */ - public function finalize(){ + public function finalize() { // tar requires the end of the file have two 512 byte null blocks $this->send(pack('a1024', '')); @@ -156,13 +155,12 @@ public function finalize(){ * Valid options are: * * int timestamp: timestamp for the file (default: current time) */ - protected function initFileStreamTransfer($name, $type, $size, $opt = []){ - $dirName = (dirname($name) == '.') ? '' : dirname($name); + protected function initFileStreamTransfer($name, $type, $size, $opt = []) { + $dirName = (\dirname($name) == '.') ? '' : \dirname($name); $fileName = ($type == self::DIRTYPE) ? basename($name) . '/' : basename($name); - // handle long file names - if (strlen($fileName) > 99 || strlen($dirName) > 154){ + if (\strlen($fileName) > 99 || \strlen($dirName) > 154) { $this->writeLongName($fileName, $dirName); } @@ -176,29 +174,29 @@ protected function initFileStreamTransfer($name, $type, $size, $opt = []){ ->setTypeflag($type) ->setPrefix($dirName) ->getHeader() - ; + ; // print header $this->send($header); } - protected function writeLongName($fileName, $dirName){ + protected function writeLongName($fileName, $dirName) { $internalPath = trim($dirName . '/' . $fileName, '/'); if ($this->longNameHeaderType === self::XHDTYPE) { // Long names via PAX $pax = $this->paxGenerate([ 'path' => $internalPath]); - $paxSize = strlen($pax); + $paxSize = \strlen($pax); $this->initFileStreamTransfer('', self::XHDTYPE, $paxSize); $this->streamFilePart($pax); $this->completeFileStream($paxSize); } else { // long names via 'L' header - $pathSize = strlen($internalPath); + $pathSize = \strlen($internalPath); $tarHeader = new TarHeader(); $header = $tarHeader->setName('././@LongLink') ->setSize($pathSize) ->setTypeflag(self::LONGNAMETYPE) ->getHeader() - ; + ; $this->send($header); $this->streamFilePart($internalPath); $this->completeFileStream($pathSize); @@ -210,7 +208,7 @@ protected function writeLongName($fileName, $dirName){ * * @param string $data raw data to send */ - protected function streamFilePart($data){ + protected function streamFilePart($data) { // send data $this->send($data); @@ -222,9 +220,9 @@ protected function streamFilePart($data){ * Complete the current file stream * @param $size */ - protected function completeFileStream($size){ + protected function completeFileStream($size) { // ensure we pad the last block so that it is 512 bytes - if (($mod = ($size % 512)) > 0){ + if (($mod = ($size % 512)) > 0) { $this->send(pack('a' . (512 - $mod), '')); } @@ -237,8 +235,8 @@ protected function completeFileStream($size){ * * @param string $data data to send */ - protected function send($data){ - if ($this->needHeaders){ + protected function send($data) { + if ($this->needHeaders) { $this->sendHeaders(); } $this->needHeaders = false; @@ -253,12 +251,12 @@ protected function send($data){ * @return string PAX formated string * @link http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current tar / PAX spec */ - protected function paxGenerate($fields){ + protected function paxGenerate($fields) { $lines = ''; - foreach ($fields as $name => $value){ + foreach ($fields as $name => $value) { // build the line and the size $line = ' ' . $name . '=' . $value . "\n"; - $size = strlen(strlen($line)) + strlen($line); + $size = \strlen(\strlen($line)) + \strlen($line); // add the line $lines .= $size . $line; diff --git a/tests/Streamer.php b/tests/Streamer.php index 5a7578b..c76cfb1 100644 --- a/tests/Streamer.php +++ b/tests/Streamer.php @@ -6,24 +6,21 @@ use ownCloud\TarStreamer\TarStreamer; use PHPUnit\Framework\TestCase; -class Streamer extends TestCase -{ +class Streamer extends TestCase { /** @var string */ private $archive; /** @var TarStreamer */ private $streamer; - public function setUp() - { + public function setUp(): void { $this->archive = tempnam('/tmp', 'tar'); $this->streamer = new TarStreamer( ['outstream' => fopen($this->archive, 'w')] ); } - public function tearDown() - { + public function tearDown(): void { unlink($this->archive); } @@ -32,8 +29,7 @@ public function tearDown() * @param $fileName * @param $data */ - public function testSimpleFile($fileName, $data) - { + public function testSimpleFile($fileName, $data) { $dataStream = fopen('data://text/plain,' . $data, 'r'); $ret = $this->streamer->addFileFromStream($dataStream, $fileName, 10); $this->assertTrue($ret); @@ -48,8 +44,7 @@ public function testSimpleFile($fileName, $data) * @param $fileName * @param $data */ - public function testAddingNoResource($fileName, $data) - { + public function testAddingNoResource($fileName, $data) { $ret = $this->streamer->addFileFromStream($data, $fileName, 10); $this->assertFalse($ret); @@ -58,8 +53,7 @@ public function testAddingNoResource($fileName, $data) $this->assertFileNotInTar($fileName); } - public function testDir() - { + public function testDir() { $folderName = 'foo-folder'; $this->streamer->addEmptyDir($folderName); @@ -68,8 +62,7 @@ public function testDir() $this->assertFolderInTar($folderName); } - public function providesNameAndData() - { + public function providesNameAndData() { return [ ['foo.bar', '1234567890'], ['foobar1234foobar1234foobar1234foobar1234foobar1234foobar1234foobar1234foobar1234foobar1234foobar1234.txt', 'abcdefghij'] @@ -80,48 +73,48 @@ public function providesNameAndData() * @return array array(filename, mimetype), expectedMimetype, expectedFilename, $description, $browser */ public function providerSendHeadersOK() { - return array( + return [ // Regular browsers - array( - array(), + [ + [], 'application/x-tar', 'archive.tar', 'default headers', 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 'Content-Disposition: attachment; filename*=UTF-8\'\'archive.tar; filename="archive.tar"', - ), - array( - array( + ], + [ + [ 'file.tar', 'application/octet-stream', - ), + ], 'application/octet-stream', 'file.tar', 'specific headers', 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 'Content-Disposition: attachment; filename*=UTF-8\'\'file.tar; filename="file.tar"', - ), + ], // Internet Explorer - array( - array(), + [ + [], 'application/x-tar', 'archive.tar', 'default headers', 'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko', 'Content-Disposition: attachment; filename="archive.tar"', - ), - array( - array( + ], + [ + [ 'file.tar', 'application/octet-stream', - ), + ], 'application/octet-stream', 'file.tar', 'specific headers', 'Mozilla/5.0 (compatible, MSIE 11, Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko', 'Content-Disposition: attachment; filename="file.tar"', - ), - ); + ], + ]; } /** @@ -136,14 +129,16 @@ public function providerSendHeadersOK() { * @param string $browser * @param string $expectedDisposition */ - public function testSendHeadersOKWithRegularBrowser(array $arguments, - $expectedMimetype, - $expectedFilename, - $description, - $browser, - $expectedDisposition) { + public function testSendHeadersOKWithRegularBrowser( + array $arguments, + $expectedMimetype, + $expectedFilename, + $description, + $browser, + $expectedDisposition + ) { $_SERVER['HTTP_USER_AGENT'] = $browser; - call_user_func_array(array($this->streamer, "sendHeaders"), $arguments); + \call_user_func_array([$this->streamer, "sendHeaders"], $arguments); $headers = xdebug_get_headers(); $this->assertContains('Pragma: public', $headers); $this->assertContains('Expires: 0', $headers); @@ -154,22 +149,19 @@ public function testSendHeadersOKWithRegularBrowser(array $arguments, $this->assertContains($expectedDisposition, $headers); } - private function assertFileInTar($file) - { + private function assertFileInTar($file) { $elem = $this->getElementFromTar($file); $this->assertNotNull($elem); $this->assertEquals('0', $elem['typeflag']); } - private function assertFileNotInTar($file) - { + private function assertFileNotInTar($file) { $arc = new Archive_Tar($this->archive); $content = $arc->extractInString($file); $this->assertNull($content); } - private function assertFolderInTar($folderName) - { + private function assertFolderInTar($folderName) { $elem = $this->getElementFromTar($folderName . '/'); $this->assertNotNull($elem); $this->assertEquals('5', $elem['typeflag']); @@ -179,11 +171,10 @@ private function assertFolderInTar($folderName) * @param $folderName * @return array */ - private function getElementFromTar($folderName) - { + private function getElementFromTar($folderName) { $arc = new Archive_Tar($this->archive); $list = $arc->listContent(); - if (!is_array($list)){ + if (!\is_array($list)) { $list = []; } $elem = array_filter($list, function ($element) use ($folderName) { diff --git a/vendor-bin/owncloud-codestyle/composer.json b/vendor-bin/owncloud-codestyle/composer.json new file mode 100644 index 0000000..d16041f --- /dev/null +++ b/vendor-bin/owncloud-codestyle/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "owncloud/coding-standard": "^4.1" + } +}