diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..564a87c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Dependabot configuration. +# +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + commit-message: + prefix: "GH Actions:" + labels: + - "testing/chores/QA" diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml new file mode 100644 index 0000000..1af918d --- /dev/null +++ b/.github/workflows/cs.yml @@ -0,0 +1,67 @@ +name: CS + +on: + # Run on all pushes and on all pull requests. + push: + pull_request: + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + actionlint: #---------------------------------------------------------------------- + name: 'Check GHA workflows' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Add problem matcher + if: ${{ github.event_name == 'pull_request' }} + shell: bash + run: | + curl -o actionlint-matcher.json https://raw.githubusercontent.com/rhysd/actionlint/main/.github/actionlint-matcher.json + echo "::add-matcher::actionlint-matcher.json" + + - name: Check workflow files + uses: docker://rhysd/actionlint:latest + with: + args: -color + + phpcs: #---------------------------------------------------------------------- + name: 'PHPCS' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 'latest' + coverage: none + tools: cs2pr + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + with: + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + # Check the code-style consistency of the PHP files. + - name: Check PHP code style + id: phpcs + run: composer checkcs -- --report-full --report-checkstyle=./phpcs-report.xml + + - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs.outcome == 'failure' }} + run: cs2pr ./phpcs-report.xml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..0dda897 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,50 @@ +name: Lint + +on: + # Run on all pushes and on all pull requests. + push: + pull_request: + # Allow manually triggering the workflow. + workflow_dispatch: + +# Cancels all previous workflow runs for the same branch that have not yet completed. +concurrency: + # The concurrency group contains the workflow name and the branch name. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: #---------------------------------------------------------------------- + runs-on: ubuntu-latest + + strategy: + matrix: + php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'] + + name: "Lint: PHP ${{ matrix.php }}" + continue-on-error: ${{ matrix.php == '8.5' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: error_reporting=-1, display_errors=On, log_errors_max_len=0 + coverage: none + tools: cs2pr + + # Install dependencies and handle caching in one go. + # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer + - name: Install Composer dependencies + uses: "ramsey/composer-install@v3" + with: + # For PHP "nightly", we need to install with ignore platform reqs. + composer-options: ${{ matrix.php == '8.5' && '--ignore-platform-req=php+' || '' }} + # Bust the cache at least once a month - output format: YYYY-MM. + custom-cache-suffix: $(date -u "+%Y-%m") + + - name: Lint against parse errors + run: composer lint -- --checkstyle | cs2pr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a99da57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Ignore temporary files. +/bin/http.log +/bin/http.pid + +# Ignore composer related files +/composer.lock +/vendor + +# Ignore local overrides of the PHPCS config file. +.phpcs.xml +phpcs.xml diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000..ed4f5cf --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,72 @@ + + + + Requests Test Server rules for PHP_CodeSniffer + + + + + . + + + */vendor/* + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/.gitignore b/bin/.gitignore deleted file mode 100644 index 216b80e..0000000 --- a/bin/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -http.log -http.pid \ No newline at end of file diff --git a/bin/serve.php b/bin/serve.php index 071148c..e7121cf 100644 --- a/bin/serve.php +++ b/bin/serve.php @@ -1,19 +1,19 @@ $value) { if ($name === 'CONTENT_TYPE') { if ($value !== '') { @@ -66,7 +64,9 @@ 'url' => $scheme . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], 'headers' => $headers, 'origin' => $_SERVER['REMOTE_ADDR'], - 'args' => empty($_SERVER['QUERY_STRING']) ? new stdClass : Requests\TestServer\parse_params_rfc( $_SERVER['QUERY_STRING'] ), + 'args' => empty($_SERVER['QUERY_STRING']) + ? new stdClass() + : Requests\TestServer\parse_params_rfc($_SERVER['QUERY_STRING']), ]; $routes = Requests\TestServer\get_routes(); @@ -81,8 +81,9 @@ foreach ($routes as $route => $callback) { $route = preg_replace('#<(\w+)>#i', '(?P<\1>\w+)', $route); $match = preg_match('#^' . $route . '$#i', $here, $matches); - if (empty($match)) + if (empty($match)) { continue; + } $data = $callback; break; @@ -95,10 +96,9 @@ while (is_callable($data)) { $data = call_user_func($data, $matches); } -} -catch (Exception $e) { +} catch (Exception $e) { http_response_code($e->getCode()); - $data = [ 'message' => $e->getMessage() ]; + $data = ['message' => $e->getMessage()]; } -echo json_encode($data, JSON_PRETTY_PRINT); \ No newline at end of file +echo json_encode($data, JSON_PRETTY_PRINT); diff --git a/composer.json b/composer.json index 7e4aa01..791bf66 100644 --- a/composer.json +++ b/composer.json @@ -1,18 +1,65 @@ { "name": "requests/test-server", "description": "Test server to respond to Requests' tests!", - "homepage": "http://github.com/RequestsPHP/test-server", "license": "ISC", - "keywords": ["http", "test"], + "type": "library", + "keywords": [ + "http", + "test" + ], "authors": [ { "name": "Ryan McCue", "homepage": "http://ryanmccue.info" + }, + { + "name": "Alain Schlesser", + "homepage": "https://github.com/schlessera" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl" + }, + { + "name": "Contributors", + "homepage": "https://github.com/RequestsPHP/test-server/graphs/contributors" } ], + "homepage": "http://github.com/RequestsPHP/test-server", "require": { "php": ">=5.6" }, - "type": "library", - "bin": [ "bin/start.sh", "bin/stop.sh", "bin/serve.php" ] + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpcompatibility/php-compatibility": "dev-develop", + "squizlabs/php_codesniffer": "^3.11" + }, + "bin": [ + "bin/serve.php", + "bin/start.sh", + "bin/stop.sh" + ], + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "lock": false + }, + "scripts": { + "checkcs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs" + ], + "fixcs": [ + "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf" + ], + "lint": [ + "@php ./vendor/php-parallel-lint/php-parallel-lint/parallel-lint . --show-deprecated -e php --exclude vendor --exclude .git" + ] + }, + "scripts-descriptions": { + "checkcs": "Check the entire codebase for code-style issues.", + "fixcs": "Fix all auto-fixable code-style issues in the entire codebase.", + "lint": "Lint PHP files to find parse errors." + } } diff --git a/lib/routes.php b/lib/routes.php index 0c57d3c..02bf549 100644 --- a/lib/routes.php +++ b/lib/routes.php @@ -4,12 +4,13 @@ use Exception; -function get_routes() { +function get_routes() +{ global $request_data, $base_url; $routes = []; // Request data! - $routes['/get'] = function () use ($request_data) { + $routes['/get'] = static function () use ($request_data) { if ($_SERVER['REQUEST_METHOD'] === 'HEAD') { exit; } @@ -19,70 +20,70 @@ function get_routes() { } return $request_data; }; - $routes['/post'] = function () { + $routes['/post'] = static function () { if ($_SERVER['REQUEST_METHOD'] !== 'POST') { throw new Exception('Method not allowed', 405); } - return Response::generate_post_data(); + return Response::generatePostData(); }; - $routes['/put'] = function () { + $routes['/put'] = static function () { if ($_SERVER['REQUEST_METHOD'] !== 'PUT') { throw new Exception('Method not allowed', 405); } - return Response::generate_post_data(); + return Response::generatePostData(); }; - $routes['/patch'] = function () { + $routes['/patch'] = static function () { if ($_SERVER['REQUEST_METHOD'] !== 'PATCH') { throw new Exception('Method not allowed', 405); } - return Response::generate_post_data(); + return Response::generatePostData(); }; - $routes['/delete'] = function () { + $routes['/delete'] = static function () { if ($_SERVER['REQUEST_METHOD'] !== 'DELETE') { throw new Exception('Method not allowed', 405); } - return Response::generate_post_data(); + return Response::generatePostData(); }; - $routes['/options'] = function () { + $routes['/options'] = static function () { if ($_SERVER['REQUEST_METHOD'] !== 'OPTIONS') { throw new Exception('Method not allowed', 405); } - return Response::generate_post_data(); + return Response::generatePostData(); }; - $routes['/trace'] = function () use ($request_data) { + $routes['/trace'] = static function () use ($request_data) { if ($_SERVER['REQUEST_METHOD'] !== 'TRACE') { throw new Exception('Method not allowed', 405); } return $request_data; }; - $routes['/purge'] = function () { + $routes['/purge'] = static function () { if ($_SERVER['REQUEST_METHOD'] !== 'PURGE') { throw new Exception('Method not allowed', 405); } - return Response::generate_post_data(); + return Response::generatePostData(); }; - $routes['/lock'] = function () { + $routes['/lock'] = static function () { if ($_SERVER['REQUEST_METHOD'] !== 'LOCK') { throw new Exception('Method not allowed', 405); } - return Response::generate_post_data(); + return Response::generatePostData(); }; // Cookies! - $routes['/cookies'] = function () { + $routes['/cookies'] = static function () { return [ 'cookies' => $_COOKIE, ]; }; - $routes['/cookies/set'] = function () { + $routes['/cookies/set'] = static function () { foreach ($_GET as $key => $value) { setcookie($key, $value, 0, '/'); } @@ -90,14 +91,14 @@ function get_routes() { Response::redirect('/cookies'); exit; }; - $routes['/cookies/set//'] = function ($args) { + $routes['/cookies/set//'] = static function ($args) { $expiry = isset($_GET['expiry']) ? (int) $_GET['expiry'] : 0; setcookie($args['key'], $args['value'], $expiry, '/'); Response::redirect('/cookies'); exit; }; - $routes['/cookies/delete'] = function () { + $routes['/cookies/delete'] = static function () { foreach ($_GET as $key => $value) { setcookie($key, '', time() - 3600, '/'); } @@ -106,7 +107,7 @@ function get_routes() { exit; }; - $routes['/basic-auth//'] = function ($args) { + $routes['/basic-auth//'] = static function ($args) { $supplied = [ 'user' => empty($_SERVER['PHP_AUTH_USER']) ? false : $_SERVER['PHP_AUTH_USER'], 'password' => empty($_SERVER['PHP_AUTH_PW']) ? false : $_SERVER['PHP_AUTH_PW'], @@ -114,7 +115,7 @@ function get_routes() { if ($args['user'] !== $supplied['user'] || $args['password'] !== $supplied['password']) { http_response_code(401); - header( 'WWW-Authenticate: Basic realm="Fake Realm"' ); + header('WWW-Authenticate: Basic realm="Fake Realm"'); return; } @@ -125,7 +126,7 @@ function get_routes() { }; // Redirects! - $routes['/redirect/'] = function ($args) use ($routes) { + $routes['/redirect/'] = static function ($args) use ($routes) { $num = (int) max((int) $args['number'], 1); if ($num === 1) { Response::redirect('/get'); @@ -137,12 +138,12 @@ function get_routes() { Response::redirect(sprintf('/redirect/%d', $num)); exit; }; - $routes['/redirect-to'] = function () { + $routes['/redirect-to'] = static function () { $location = $_GET['url']; header('Location: ' . $location, true, 302); exit; }; - $routes['/relative-redirect/'] = function ($args) { + $routes['/relative-redirect/'] = static function ($args) { $num = (int) max((int) $args['number'], 1); if ($num === 1) { Response::redirect('/get', 302, true); @@ -156,13 +157,13 @@ function get_routes() { }; // Miscellaneous! - $routes['/delay/'] = function ($args) use ($routes) { + $routes['/delay/'] = static function ($args) use ($routes) { $delay = min($args['delay'], 10); sleep($delay); return $routes['/get']; }; - $routes['/status/'] = function ($args) use ($base_url) { + $routes['/status/'] = static function ($args) use ($base_url) { $code = (int) $args['code']; switch ($code) { @@ -186,25 +187,25 @@ function get_routes() { http_response_code($code); exit; }; - $routes['/stream/'] = function ($args) use ($request_data) { + $routes['/stream/'] = static function ($args) use ($request_data) { $response = $request_data; $num = min($args['num'], 100); - $generate_stream = function () use ($num, $response) { + $generate_stream = static function () use ($num, $response) { foreach (range(0, $num - 1) as $n) { $response['id'] = $n; - yield json_encode( $response, JSON_PRETTY_PRINT ) . "\n"; + yield json_encode($response, JSON_PRETTY_PRINT) . "\n"; } }; header('Transfer-Encoding: chunked'); - foreach ( $generate_stream() as $response ) { + foreach ($generate_stream() as $response) { printf("%x\r\n%s\r\n", strlen($response), $response); flush(); } echo "0\r\n\r\n"; exit; }; - $routes['/gzip'] = function () use ($request_data) { + $routes['/gzip'] = static function () use ($request_data) { $response = $request_data; $response['gzipped'] = true; @@ -217,7 +218,7 @@ function get_routes() { echo $response; exit; }; - $routes['/bytes/'] = function ($args) { + $routes['/bytes/'] = static function ($args) { header('Content-Type: application/octet-stream'); mt_srand(0); @@ -232,12 +233,12 @@ function get_routes() { }; // Finally, the index! - $routes['/'] = function () use ($routes) { + $routes['/'] = static function () use ($routes) { header('Content-Type: text/html; charset=utf-8'); echo '
    '; foreach ($routes as $url => $_) { - echo '
  • ' . htmlspecialchars( $url ) . '
  • '; + echo '
  • ' . htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401) . '
  • '; } echo '
'; exit; diff --git a/lib/utils.php b/lib/utils.php index 96544a2..e57a866 100644 --- a/lib/utils.php +++ b/lib/utils.php @@ -2,8 +2,10 @@ namespace Requests\TestServer; -class Response { - public static function redirect ($path, $code = 302, $relative = false) { +class Response +{ + public static function redirect($path, $code = 302, $relative = false) + { global $base_url; $url = $path; if (!$relative) { @@ -13,31 +15,39 @@ public static function redirect ($path, $code = 302, $relative = false) { header('Location: ' . $url, true, $code); } - public static function generate_post_data() { + public static function generatePostData() + { global $request_data; $data = $request_data; $data['data'] = file_get_contents('php://input'); $data['form'] = ''; - if (strpos($data['data'], '&') !== false) + if (strpos($data['data'], '&') !== false) { $data['form'] = parse_params_rfc($data['data']); + } $data['json'] = json_decode($data['data']); - $data['files'] = array_map(function ($data) { - return file_get_contents($data['tmp_name']); - }, $_FILES); + $data['files'] = array_map( + static function ($data) { + return file_get_contents($data['tmp_name']); + }, + $_FILES + ); return $data; } } -function parse_params_rfc($input) { - if (!isset($input) || !$input) return array(); +function parse_params_rfc($input) +{ + if (!isset($input) || !$input) { + return []; + } $pairs = explode('&', $input); - $parsed = array(); + $parsed = []; foreach ($pairs as $pair) { $split = explode('=', $pair, 2); $parameter = urldecode($split[0]); @@ -45,4 +55,4 @@ function parse_params_rfc($input) { $parsed[$parameter] = $value; } return $parsed; -} \ No newline at end of file +}