diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..8b2e1a8956 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*.php] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..55a15e036f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +/.github/ export-ignore +/docs/ export-ignore +/examples/ export-ignore +/scripts/ export-ignore +/tests/ export-ignore +/www/ export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/TESTING.md export-ignore +/TROUBLESHOOTING.md export-ignore +/composer.json export-ignore \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000000..bc1b32be22 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,6 @@ +### Contributing to PHP Curl Class +1. Check for open issues or open a new issue to start a discussion around a bug or feature. +1. Fork the repository on GitHub to start making your changes. +1. Write one or more tests for the new feature or that expose the bug. +1. Make code changes to implement the feature or fix the bug. +1. Send a pull request to get your changes merged and published. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..129d8b4b73 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,10 @@ +What is the expected behavior? + +What is the actual behavior? + +What steps will reproduce the problem? + +Code: +```php + +``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..3425bfabf6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,39 @@ +version: 2 +updates: + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: ⬆ + versioning-strategy: "increase" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: ⬆ + labels: + - "cleanup-no-release-required" + - "dependencies" + - "github_actions" + + - package-ecosystem: "npm" + directory: "/www/scripts/" + schedule: + interval: "daily" + commit-message: + prefix: ⬆ + versioning-strategy: "increase" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + commit-message: + prefix: ⬆ + versioning-strategy: "increase" + allow: + - dependency-type: "all" + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..0644638553 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: + - master + - php5.x-master + pull_request: + schedule: + - cron: '0 17 * * *' + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + matrix: + php-version: + - '8.0' + - '8.1' + - '8.2' + - '8.3' + - '8.4' + future-release: [false] + include: + - php-version: '8.5' + future-release: true + fail-fast: false + name: PHP ${{ matrix.php-version }} + continue-on-error: ${{ matrix.future-release }} + + steps: + - name: Set up php + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Test + run: bash tests/run.sh + env: + CI_PHP_VERSION: ${{ matrix.php-version }} + CI_PHP_FUTURE_RELEASE: ${{ matrix.future-release }} diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000000..a5ba44bfeb --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,56 @@ +name: Dependabot auto-merge + +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + steps: + - name: Dependabot metadata + id: dependabot-metadata + uses: dependabot/fetch-metadata@v2.4.0 + + - name: Approve and label updates + run: | + review_status="$( + gh pr view "${PR_URL}" \ + --json="reviewDecision" \ + --jq=".reviewDecision" + )" + echo "Pull request review status: ${review_status}" + if [[ "${review_status}" != "APPROVED" ]]; then + gh pr review --approve "${PR_URL}" + fi + gh pr edit --add-label "cleanup-no-release-required" "${PR_URL}" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge Dependabot patch updates + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-patch' }} + run: | + gh pr merge --auto --merge "${PR_URL}" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge Dependabot minor updates + if: ${{ steps.dependabot-metadata.outputs.update-type == 'version-update:semver-minor' }} + run: | + gh pr merge --auto --merge "${PR_URL}" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Merge Dependabot indirect dependency updates + if: ${{ steps.dependabot-metadata.outputs.dependency-type == 'indirect' }} + run: | + gh pr merge --auto --merge "${PR_URL}" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..4dcc53d9bb --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,45 @@ +name: Lint + +on: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Get changed python files + id: changed-files + uses: tj-actions/changed-files@v47 + with: + files: "**/*.py" + + - name: Install requirements + if: steps.changed-files.outputs.any_changed == 'true' + run: pip install black flake8 isort + + - name: black changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + black --check --color --diff --quiet ${{ steps.changed-files.outputs.all_changed_files }} + + - name: flake8 changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + flake8 --config="tests/setup.cfg" ${{ steps.changed-files.outputs.all_changed_files }} + + - name: isort changed files + if: steps.changed-files.outputs.any_changed == 'true' + run: | + set +e + isort --check-only --force-single-line-imports --profile black ${{ steps.changed-files.outputs.all_changed_files }} + exit_code="${?}" + if [[ "${exit_code}" -ne 0 ]]; then + isort --force-single-line-imports --profile black ${{ steps.changed-files.outputs.all_changed_files }} + git diff --color + exit "${exit_code}" + fi diff --git a/.github/workflows/outdated-track.yml b/.github/workflows/outdated-track.yml new file mode 100644 index 0000000000..93213ca73a --- /dev/null +++ b/.github/workflows/outdated-track.yml @@ -0,0 +1,81 @@ +name: Track outdated dependencies + +on: + schedule: + - cron: '0 18 * * 1-4' + workflow_dispatch: + +jobs: + run: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Gather requirements + id: check-outdated + run: | + python -m venv venv + source venv/bin/activate + python -m pip install --upgrade pip + pip install --requirement scripts/make_release_requirements.txt + outdated="$(pip list --outdated --format json | jq)" + outdated_count="$(echo "$outdated" | jq 'length')" + echo "${outdated}" | jq "[{\"outdated_count\":\"${outdated_count}\"}] + ." > scripts/make_release_requirements.json + git diff --color + changes="$(git diff)" + delimiter="$(openssl rand -hex 8)" + echo "changes<<${delimiter}" >> "${GITHUB_OUTPUT}" + echo "${changes}" >> "${GITHUB_OUTPUT}" + echo "${delimiter}" >> "${GITHUB_OUTPUT}" + + - name: Create or update pull request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + commit-message: Update outdated dependencies list + labels: cleanup-no-release-required, dependencies, github_actions + title: ⬆ Update outdated dependencies list + body: | + ```diff + ${{ steps.check-outdated.outputs.changes }} + ``` + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + branch: create-pull-request/outdated + committer: GitHub + delete-branch: true + token: ${{ secrets.PHP_CURL_CLASS_BOT_TOKEN }} + push-to-fork: php-curl-class-helper/php-curl-class + env: + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_BOT_TOKEN }} + + - name: Pull request created or updated + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + echo "Pull request: ${{ steps.cpr.outputs.pull-request-url }}" + + - name: Approve outdated dependencies pull request + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + review_status="$( + gh pr view "${PR_URL}" \ + --json="reviewDecision" \ + --jq=".reviewDecision" + )" + echo "Pull request review status: ${review_status}" + if [[ "${review_status}" != "APPROVED" ]]; then + gh pr review --approve "${PR_URL}" + fi + env: + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_TOKEN }} + + - name: Enable auto-merge for outdated dependencies pull request + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + gh pr merge --auto --merge "${PR_URL}" + env: + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/outdated-update-package.yml b/.github/workflows/outdated-update-package.yml new file mode 100644 index 0000000000..967f2b0403 --- /dev/null +++ b/.github/workflows/outdated-update-package.yml @@ -0,0 +1,87 @@ +name: Update outdated package + +on: + workflow_dispatch: + inputs: + package_name: + required: true + type: string + package_version: + required: true + type: string + +jobs: + upgrade: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Install pip-tools + run: | + python -m venv venv + source venv/bin/activate + python -m pip install --upgrade pip pip-tools + + - name: Upgrade outdated package + env: + PACKAGE_NAME: ${{ github.event.inputs.package_name }} + PACKAGE_VERSION: ${{ github.event.inputs.package_version }} + run: | + source venv/bin/activate + + set -x + echo "Upgrading ${PACKAGE_NAME} to version ${PACKAGE_VERSION}" + upgrade_package="${PACKAGE_NAME}==${PACKAGE_VERSION}" + pip-compile \ + --upgrade-package="${upgrade_package}" \ + --output-file="scripts/make_release_requirements.txt" \ + "scripts/make_release_requirements.in" + + - name: Create or update pull request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + commit-message: "Update package ${{ github.event.inputs.package_name }}" + labels: cleanup-no-release-required, dependencies, github_actions + title: ⬆ Update ${{ github.event.inputs.package_name }} + body: "Updating ${{ github.event.inputs.package_name }}" + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + branch: "create-pull-request/upgrade-${{ github.event.inputs.package_name }}" + committer: GitHub + delete-branch: true + token: ${{ secrets.PHP_CURL_CLASS_BOT_TOKEN }} + push-to-fork: php-curl-class-helper/php-curl-class + env: + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_BOT_TOKEN }} + + - name: Pull request created or updated + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + echo "Pull request: ${{ steps.cpr.outputs.pull-request-url }}" + + - name: Enable auto-merge for pull request + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + gh pr merge --auto --merge "${PR_URL}" + env: + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Approve update + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + review_status="$( + gh pr view "${PR_URL}" \ + --json="reviewDecision" \ + --jq=".reviewDecision" + )" + echo "Pull request review status: ${review_status}" + if [[ "${review_status}" != "APPROVED" ]]; then + gh pr review --approve "${PR_URL}" + fi + env: + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_TOKEN }} diff --git a/.github/workflows/outdated-update-packages.yml b/.github/workflows/outdated-update-packages.yml new file mode 100644 index 0000000000..18dc7288c8 --- /dev/null +++ b/.github/workflows/outdated-update-packages.yml @@ -0,0 +1,40 @@ +name: Update outdated packages + +on: + schedule: + - cron: '0 20 * * 1-4' + workflow_dispatch: + +jobs: + update-dependencies: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Upgrade outdated packages + run: | + items=($( + jq -r '.[1:]' "scripts/make_release_requirements.json" | + jq -r '.[] | "\(.name)|\(.version)|\(.latest_version)"' + )) + for item in "${items[@]}"; do + IFS='|' read -r "package_name" "package_version" "package_latest_version" <<< "${item}" + echo "found outdated package:" + echo "name: ${package_name}" + echo "version: ${package_version}" + echo "latest_version: ${package_latest_version}" + curl \ + --request "POST" \ + --silent \ + --header "Accept: application/vnd.github+json" \ + --header "Authorization: Bearer ${TOKEN}" \ + "https://api.github.com/repos/php-curl-class/php-curl-class/actions/workflows/outdated-update-package.yml/dispatches" \ + --data "{\"ref\":\"master\",\"inputs\":{\"package_name\":\"${package_name}\",\"package_version\":\"${package_latest_version}\"}}" + done + + echo "done" + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pre-commit-auto-update.yml b/.github/workflows/pre-commit-auto-update.yml new file mode 100644 index 0000000000..93762d663d --- /dev/null +++ b/.github/workflows/pre-commit-auto-update.yml @@ -0,0 +1,82 @@ +name: Pre-commit auto-update + +on: + schedule: + - cron: '0 18 * * 1-4' + workflow_dispatch: + +jobs: + update-pre-commit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + ref: 'master' + persist-credentials: false + + - name: Update dependencies + run: | + set -x + python -m pip install --upgrade pip + pip install pre-commit + pre-commit autoupdate --config="tests/.pre-commit-config.yaml" + + - name: Gather changes + id: gather-changes + run: | + git diff --color + changes="$(git diff)" + delimiter="$(openssl rand -hex 8)" + echo "changes<<${delimiter}" >> "${GITHUB_OUTPUT}" + echo "${changes}" >> "${GITHUB_OUTPUT}" + echo "${delimiter}" >> "${GITHUB_OUTPUT}" + + - name: Create or update pull request + id: cpr + uses: peter-evans/create-pull-request@v7 + with: + commit-message: Update pre-commit dependencies + labels: cleanup-no-release-required, dependencies, github_actions + title: ⬆ Update pre-commit dependencies + body: | + ```diff + ${{ steps.gather-changes.outputs.changes }} + ``` + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + branch: create-pull-request/pre-commit + committer: GitHub + delete-branch: true + token: ${{ secrets.PHP_CURL_CLASS_BOT_TOKEN }} + push-to-fork: php-curl-class-helper/php-curl-class + env: + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_BOT_TOKEN }} + + - name: Pull request created or updated + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + echo "Pull request: ${{ steps.cpr.outputs.pull-request-url }}" + + - name: Enable auto-merge for pull request + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + gh pr merge --auto --merge "${PR_URL}" + env: + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Approve update + if: ${{ steps.cpr.outputs.pull-request-number }} + run: | + review_status="$( + gh pr view "${PR_URL}" \ + --json="reviewDecision" \ + --jq=".reviewDecision" + )" + echo "Pull request review status: ${review_status}" + if [[ "${review_status}" != "APPROVED" ]]; then + gh pr review --approve "${PR_URL}" + fi + env: + PR_URL: ${{ steps.cpr.outputs.pull-request-url }} + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..e199d2925f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +on: + schedule: + - cron: '0 18 * * 1-4' + workflow_dispatch: + +jobs: + release: + # Prevent this workflow from running elsewhere. + if: github.repository_owner == 'php-curl-class' + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Install requirements + run: | + python -m pip install --upgrade pip + pip install --requirement scripts/make_release_requirements.txt + + - name: Set git details + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Make release + run: python scripts/make_release.py + env: + GITHUB_TOKEN: ${{ secrets.PHP_CURL_CLASS_TOKEN }} diff --git a/.gitignore b/.gitignore index d1502b087b..e4e24c16cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ vendor/ +venv/ composer.lock + +.idea/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5e046cc32b..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -language: php - -php: - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - hhvm - - nightly - -matrix: - allow_failures: - - php: nightly - -before_script: - - bash tests/before_script.sh - -script: - - bash tests/script.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..27ea363fa3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,356 @@ +# Change Log + +List of changes for PHP Curl Class (https://github.com/php-curl-class/php-curl-class). + +PHP Curl Class uses semantic versioning with version numbers written as `MAJOR.MINOR.PATCH`. You may safely update +`MINOR` and `PATCH` version changes. It is recommended to review `MAJOR` changes prior to upgrade as there may be +backwards-incompatible changes that will affect existing usage. + + + +## 12.0.1 - 2025-11-03 + +- Remove calls to deprecated functions ([#1005](https://github.com/php-curl-class/php-curl-class/pull/1005)) + +## 12.0.0 - 2025-03-25 + +- Drop support for PHP 7.4 ([#937](https://github.com/php-curl-class/php-curl-class/pull/937)) + +## 11.1.0 - 2025-03-24 + +- Add methods like Curl::setGet() for each HTTP request method ([#936](https://github.com/php-curl-class/php-curl-class/pull/936)) + +## 11.0.5 - 2025-03-11 + +- Fix PHPStan static analysis errors ([#929](https://github.com/php-curl-class/php-curl-class/pull/929)) + +## 11.0.4 - 2025-02-20 + +- Increase psalm strictness ([#925](https://github.com/php-curl-class/php-curl-class/pull/925)) + +## 11.0.3 - 2025-02-19 + +- Use default for Psalm ensureOverrideAttribute ([#923](https://github.com/php-curl-class/php-curl-class/pull/923)) + +## 11.0.2 - 2025-02-18 + +- Fix CI (PHPUnit) ([#918](https://github.com/php-curl-class/php-curl-class/pull/918)) + +## 11.0.1 - 2025-01-13 + +- Increase Psalm strictness ([#909](https://github.com/php-curl-class/php-curl-class/pull/909)) +- Increase PHPStan strictness ([#908](https://github.com/php-curl-class/php-curl-class/pull/908)) + +## 11.0.0 - 2024-08-22 + +- Drop support for PHP 7.3 ([#889](https://github.com/php-curl-class/php-curl-class/pull/889)) +- Drop support for PHP 7.2 ([#888](https://github.com/php-curl-class/php-curl-class/pull/888)) +- Drop support for PHP 7.1 ([#887](https://github.com/php-curl-class/php-curl-class/pull/887)) + +## 10.0.1 - 2024-08-21 + +- Use nullable type declaration ([#882](https://github.com/php-curl-class/php-curl-class/pull/882)) + +## 10.0.0 - 2024-08-20 + +- Drop support for PHP 7.0 ([#880](https://github.com/php-curl-class/php-curl-class/pull/880)) +- Add public method getActiveCurls ([#871](https://github.com/php-curl-class/php-curl-class/pull/871)) + +## 9.19.2 - 2024-04-09 + +- Fix CI: Use nullable type declaration ([#859](https://github.com/php-curl-class/php-curl-class/pull/859)) + +## 9.19.1 - 2024-02-27 + +- Fix afterSend not being called ([#848](https://github.com/php-curl-class/php-curl-class/pull/848)) + +## 9.19.0 - 2024-01-18 + +- Allow displaying curl option value without specifying value ([#837](https://github.com/php-curl-class/php-curl-class/pull/837)) + +## 9.18.2 - 2023-09-11 + +- Fix use of mb_strpos() causing error when polyfill is used ([#813](https://github.com/php-curl-class/php-curl-class/pull/813)) + +## 9.18.1 - 2023-08-29 + +- Add additional check for decoding gzip-encoded responses ([#808](https://github.com/php-curl-class/php-curl-class/pull/808)) + +## 9.18.0 - 2023-08-28 + +- Implement Curl::setError() and MultiCurl::setError() ([#805](https://github.com/php-curl-class/php-curl-class/pull/805)) +- Rename ::setError() to ::afterSend() ([#807](https://github.com/php-curl-class/php-curl-class/pull/807)) + +## 9.17.4 - 2023-07-10 + +- Add coding standards rule to use the null coalescing operator ?? where possible ([#801](https://github.com/php-curl-class/php-curl-class/pull/801)) +- Replace isset with null coalescing operator ([#800](https://github.com/php-curl-class/php-curl-class/pull/800)) + +## 9.17.3 - 2023-07-04 + +- Update PHP_CodeSniffer ruleset: PSR2 → PSR12 ([#797](https://github.com/php-curl-class/php-curl-class/pull/797)) +- Add additional coding standard checks ([#796](https://github.com/php-curl-class/php-curl-class/pull/796)) + +## 9.17.2 - 2023-06-27 + +- Use short array syntax ([#793](https://github.com/php-curl-class/php-curl-class/pull/793)) +- Add PHP-CS-Fixer to check for unused imports ([#794](https://github.com/php-curl-class/php-curl-class/pull/794)) +- Replace `uniqid` by `random_bytes` ([#792](https://github.com/php-curl-class/php-curl-class/pull/792)) + +## 9.17.1 - 2023-06-14 + +- Improve and add tests for Curl::fastDownload() ([#791](https://github.com/php-curl-class/php-curl-class/pull/791)) + +## 9.17.0 - 2023-06-13 + +- Make method to display curl option value public ([#790](https://github.com/php-curl-class/php-curl-class/pull/790)) + +## 9.16.1 - 2023-06-12 + +- Differentiate between internal options and user-set options ([#788](https://github.com/php-curl-class/php-curl-class/pull/788)) +- Create method to display a curl option value ([#785](https://github.com/php-curl-class/php-curl-class/pull/785)) +- Fix existing header overwritten after using MultiCurl::addCurl() ([#787](https://github.com/php-curl-class/php-curl-class/pull/787)) + +## 9.16.0 - 2023-05-25 + +- Graduate Curl::fastDownload() ([#783](https://github.com/php-curl-class/php-curl-class/pull/783)) + +## 9.15.1 - 2023-05-24 + +- Fix PHP CodeSniffer errors ([#782](https://github.com/php-curl-class/php-curl-class/pull/782)) + +## 9.15.0 - 2023-05-22 + +- Update Curl::diagnose() to detect bit flags with negative values ([#781](https://github.com/php-curl-class/php-curl-class/pull/781)) +- Display bit flags in use when calling Curl::diagnose() ([#779](https://github.com/php-curl-class/php-curl-class/pull/779)) + +## 9.14.5 - 2023-05-16 + +- Handle missing content-type response header in Curl::diagnose() ([#778](https://github.com/php-curl-class/php-curl-class/pull/778)) + +## 9.14.4 - 2023-05-08 + +- Update article in Curl::diagnose() Allow header warning ([#776](https://github.com/php-curl-class/php-curl-class/pull/776)) + +## 9.14.3 - 2023-03-13 + +- Remove use of array_merge() inside loop ([#774](https://github.com/php-curl-class/php-curl-class/pull/774)) + +## 9.14.2 - 2023-03-09 + +- Clean up: Reduce nesting ([#771](https://github.com/php-curl-class/php-curl-class/pull/771)) + +## 9.14.1 - 2023-02-27 + +- Remove coding standard ruleset exclusion ([#768](https://github.com/php-curl-class/php-curl-class/pull/768)) + +## 9.14.0 - 2023-02-26 + +- Make https:// and http:// the allowed request protocols by default ([#767](https://github.com/php-curl-class/php-curl-class/pull/767)) + +## 9.13.1 - 2023-01-16 + +- Allow uploads with CURLStringFile type ([#762](https://github.com/php-curl-class/php-curl-class/pull/762)) + +## 9.13.0 - 2023-01-13 + +- Implement abstract class BaseCurl for Curl and MultiCurl ([#759](https://github.com/php-curl-class/php-curl-class/pull/759)) +- Display error messages found in Curl::diagnose() ([#758](https://github.com/php-curl-class/php-curl-class/pull/758)) +- Fix Curl::diagnose() request type output for POST requests ([#757](https://github.com/php-curl-class/php-curl-class/pull/757)) + +## 9.12.6 - 2023-01-11 + +- Replace use of #[\AllowDynamicProperties] ([#756](https://github.com/php-curl-class/php-curl-class/pull/756)) +- silence PHP 8.2 deprecation notices ([#754](https://github.com/php-curl-class/php-curl-class/pull/754)) + +## 9.12.5 - 2022-12-20 + +- Fix static analysis error ([#752](https://github.com/php-curl-class/php-curl-class/pull/752)) + +## 9.12.4 - 2022-12-17 + +- Exclude additional files from git archive ([#751](https://github.com/php-curl-class/php-curl-class/pull/751)) + +## 9.12.3 - 2022-12-13 + +- Ensure string response before gzip decode ([#749](https://github.com/php-curl-class/php-curl-class/pull/749)) + +## 9.12.2 - 2022-12-11 + +- Disable warning when gzip-decoding response errors ([#748](https://github.com/php-curl-class/php-curl-class/pull/748)) + +## 9.12.1 - 2022-12-08 + +- Include option constant that uses the CURLINFO_ prefix ([#745](https://github.com/php-curl-class/php-curl-class/pull/745)) + +## 9.12.0 - 2022-12-07 + +- Add automatic gzip decoding of response ([#744](https://github.com/php-curl-class/php-curl-class/pull/744)) + +## 9.11.1 - 2022-12-06 + +- change: remove unused namespace import ([#743](https://github.com/php-curl-class/php-curl-class/pull/743)) + +## 9.11.0 - 2022-12-05 + +- Add Curl::diagnose() HTTP method check matches methods allowed ([#741](https://github.com/php-curl-class/php-curl-class/pull/741)) +- Add temporary fix missing template params ([#742](https://github.com/php-curl-class/php-curl-class/pull/742)) + +## 9.10.0 - 2022-11-07 + +- Display request options in Curl::diagnose() output ([#739](https://github.com/php-curl-class/php-curl-class/pull/739)) + +## 9.9.0 - 2022-11-06 + +- Fix MultiCurl::setCookieString() ([#738](https://github.com/php-curl-class/php-curl-class/pull/738)) +- Pass MultiCurl options to new Curl instances earlier ([#737](https://github.com/php-curl-class/php-curl-class/pull/737)) +- Add deferred constant curlErrorCodeConstants ([#736](https://github.com/php-curl-class/php-curl-class/pull/736)) + +## 9.8.0 - 2022-10-01 + +- Include curl error code constant in curl error message ([#733](https://github.com/php-curl-class/php-curl-class/pull/733)) + +## 9.7.0 - 2022-09-29 + +- Implement ArrayUtil::arrayRandomIndex() ([#732](https://github.com/php-curl-class/php-curl-class/pull/732)) + +## 9.6.3 - 2022-09-24 + +- Remove filter flag constants deprecated as of PHP 7.3 ([#730](https://github.com/php-curl-class/php-curl-class/pull/730)) + +## 9.6.2 - 2022-09-24 + +- Call MultiCurl::beforeSend() before each request is made ([#723](https://github.com/php-curl-class/php-curl-class/pull/723)) +- Encode keys for post data with numeric keys ([#726](https://github.com/php-curl-class/php-curl-class/pull/726)) +- Fix building post data with object ([#728](https://github.com/php-curl-class/php-curl-class/pull/728)) + +## 9.6.1 - 2022-06-30 + +### Fixed + +- Attempt to stop active requests when `MultiCurl::stop()` is called + [#714](https://github.com/php-curl-class/php-curl-class/issues/714) + [#718](https://github.com/php-curl-class/php-curl-class/issues/718) +- Retain keys for arrays with null values when building post data + [#712](https://github.com/php-curl-class/php-curl-class/issues/712) + +## 9.6.0 - 2022-03-17 + +### Added + +- Method `MultiCurl::stop()` for stopping subsequent requests + [#708](https://github.com/php-curl-class/php-curl-class/issues/708) + +## 9.5.1 - 2021-12-14 + +### Fixed + +- Silence PHP 8.1 deprecations [#691](https://github.com/php-curl-class/php-curl-class/issues/691) +- Remove data parameter from additional request types + [#689](https://github.com/php-curl-class/php-curl-class/issues/689) + +## 9.5.0 - 2021-11-21 + +### Added + +- Method `Curl::setStop()` for stopping requests early without downloading the full response body + [#681](https://github.com/php-curl-class/php-curl-class/issues/681) + +### Fixed + +- Fixed constructing request url when using `MultiCurl::addPost()` + [#686](https://github.com/php-curl-class/php-curl-class/issues/686) + +## 9.4.0 - 2021-09-04 + +### Changed + +- Method `Url::parseUrl()` is now public + +### Fixed + +- Fix parsing schemeless urls [#679](https://github.com/php-curl-class/php-curl-class/issues/679) + +## 9.3.1 - 2021-08-05 + +### Changed + +- Enabled strict types (`declare(strict_types=1);`) + +### Fixed + +- Fixed `Curl::downloadFileName` not being set correctly + +## 9.3.0 - 2021-07-23 + +### Added + +- Method `Curl::diagnose()` for troubleshooting requests + +## 9.2.0 - 2021-06-23 + +### Added + +- Additional Curl::set\* and MultiCurl::set\* helper methods + + ``` + Curl::setAutoReferer() + Curl::setAutoReferrer() + Curl::setFollowLocation() + Curl::setForbidReuse() + Curl::setMaximumRedirects() + MultiCurl::setAutoReferer() + MultiCurl::setAutoReferrer() + MultiCurl::setFollowLocation() + MultiCurl::setForbidReuse() + MultiCurl::setMaximumRedirects() + ``` + +### Fixed + +- Closing curl handles [#670](https://github.com/php-curl-class/php-curl-class/issues/670) +- Use of "$this" in non-object context [#671](https://github.com/php-curl-class/php-curl-class/pull/671) + +## 9.1.0 - 2021-03-24 + +### Added + +- Support for using relative urls with MultiCurl::add\*() methods [#628](https://github.com/php-curl-class/php-curl-class/issues/628) + +## 9.0.0 - 2021-03-19 + +### Changed + +- Use short array syntax + +### Removed + +- Support for PHP 5.3, 5.4, 5.5, and 5.6 [#380](https://github.com/php-curl-class/php-curl-class/issues/380) + +## Manual Review + +A manual review of changes is possible using the +[comparison page](https://github.com/php-curl-class/php-curl-class/compare/). For example, visit +[7.4.0...8.0.0](https://github.com/php-curl-class/php-curl-class/compare/7.4.0...8.0.0) to compare the changes for +the `MAJOR` upgrade from 7.4.0 to 8.0.0. Comparing against `HEAD` is also possible using the `tag...HEAD` syntax +([8.3.0...HEAD](https://github.com/php-curl-class/php-curl-class/compare/8.3.0...HEAD)). + +View the log between releases: + + $ git fetch --tags + $ git log 7.4.0...8.0.0 + +View the code changes between releases: + + $ git fetch --tags + $ git diff 7.4.0...8.0.0 + +View only the source log and code changes between releases: + + $ git log 7.4.0...8.0.0 "src/" + $ git diff 7.4.0...8.0.0 "src/" + +View only the source log and code changes between a release and the current checked-out commit: + + $ git log 8.0.0...head "src/" + $ git diff 8.0.0...head "src/" diff --git a/README.md b/README.md index 77ee6f6f0f..70487139af 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,96 @@ # PHP Curl Class: HTTP requests made easy -[![Build Status](https://travis-ci.org/php-curl-class/php-curl-class.png?branch=master)](https://travis-ci.org/php-curl-class/php-curl-class) -[![Downloads](https://img.shields.io/packagist/dt/php-curl-class/php-curl-class.svg)](https://packagist.org/packages/php-curl-class/php-curl-class) +[![](https://img.shields.io/github/release/php-curl-class/php-curl-class.svg?style=for-the-badge&sort=semver)](https://github.com/php-curl-class/php-curl-class/releases/) +[![](https://img.shields.io/github/license/php-curl-class/php-curl-class.svg?style=for-the-badge)](https://github.com/php-curl-class/php-curl-class/blob/master/LICENSE) +[![](https://img.shields.io/github/actions/workflow/status/php-curl-class/php-curl-class/ci.yml?style=for-the-badge&label=build&branch=master)](https://github.com/php-curl-class/php-curl-class/actions/workflows/ci.yml) +[![](https://img.shields.io/github/actions/workflow/status/php-curl-class/php-curl-class/release.yml?style=for-the-badge&label=release&branch=master)](https://github.com/php-curl-class/php-curl-class/releases/) +[![](https://img.shields.io/github/actions/workflow/status/php-curl-class/php-curl-class/dependabot/dependabot-updates?style=for-the-badge&label=Dependabot&branch=master)](https://github.com/php-curl-class/php-curl-class/actions/workflows/dependabot/dependabot-updates) +[![](https://img.shields.io/packagist/dt/php-curl-class/php-curl-class.svg?style=for-the-badge)](https://github.com/php-curl-class/php-curl-class/releases/) -[![License](https://img.shields.io/packagist/l/php-curl-class/php-curl-class.svg)](https://github.com/php-curl-class/php-curl-class/blob/master/LICENSE) +PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs. -PHP Curl Class is an object-oriented wrapper of the PHP cURL extension that makes it easy to send HTTP requests and integrate with web APIs. +![PHP Curl Class screencast](www/img/screencast.gif) --- -- [Installation](#installation) -- [Requirements](#requirements) -- [Quick Start and Examples](#quick-start-and-examples) -- [Available Methods](#available-methods) -- [Contribute](#contribute) +- [⚙️ Installation](#%EF%B8%8F-installation) +- [📋 Requirements](#-requirements) +- [🚀 Quick Start and Examples](#-quick-start-and-examples) +- [📖 Available Methods](#-available-methods) +- [🔒 Security](#-security) +- [🛠️ Troubleshooting](#%EF%B8%8F-troubleshooting) +- [🧪 Testing](#-testing) +- [🤝 Contributing](#-contributing) --- -### Installation +### ⚙️ Installation -To install PHP Curl Class, simply: +To install PHP Curl Class, run the following command: - $ composer require php-curl-class/php-curl-class + composer require php-curl-class/php-curl-class -### Requirements +To install the latest commit version: -PHP Curl Class works with PHP 5.3, 5.4, 5.5, 5.6, and HHVM. + composer require php-curl-class/php-curl-class @dev -### Quick Start and Examples +Installation instructions to use the `composer` command can be found on https://github.com/composer/composer. + +### 📋 Requirements + +PHP Curl Class works with PHP versions 8.4, 8.3, 8.2, 8.1, and 8.0. + +### 🚀 Quick Start and Examples + +More examples are available under [/examples](https://github.com/php-curl-class/php-curl-class/tree/master/examples). ```php require __DIR__ . '/vendor/autoload.php'; -use \Curl\Curl; +use Curl\Curl; $curl = new Curl(); -$curl->get('http://www.example.com/'); +$curl->get('https://www.example.com/'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + $curl->diagnose(); +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} ``` ```php +// https://www.example.com/search?q=keyword $curl = new Curl(); -$curl->get('http://www.example.com/search', array( +$curl->get('https://www.example.com/search', [ 'q' => 'keyword', -)); +]); ``` ```php $curl = new Curl(); -$curl->post('http://www.example.com/login/', array( +$curl->post('https://www.example.com/login/', [ 'username' => 'myusername', 'password' => 'mypassword', -)); +]); ``` ```php $curl = new Curl(); $curl->setBasicAuthentication('username', 'password'); -$curl->setUserAgent(''); -$curl->setReferrer(''); +$curl->setUserAgent('MyUserAgent/0.0.1 (+https://www.example.com/bot.html)'); +$curl->setReferrer('https://www.example.com/url?url=https%3A%2F%2Fwww.example.com%2F'); $curl->setHeader('X-Requested-With', 'XMLHttpRequest'); $curl->setCookie('key', 'value'); -$curl->get('http://www.example.com/'); +$curl->get('https://www.example.com/'); if ($curl->error) { - echo 'Error: ' . $curl->errorCode . ': ' . $curl->errorMessage; -} -else { - echo $curl->response; + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); } var_dump($curl->requestHeaders); @@ -75,44 +99,44 @@ var_dump($curl->responseHeaders); ```php $curl = new Curl(); -$curl->setOpt(CURLOPT_SSL_VERIFYPEER, false); -$curl->get('https://encrypted.example.com/'); +$curl->setFollowLocation(); +$curl->get('https://shortn.example.com/bHbVsP'); ``` ```php $curl = new Curl(); -$curl->put('http://api.example.com/user/', array( +$curl->put('https://api.example.com/user/', [ 'first_name' => 'Zach', 'last_name' => 'Borboa', -)); +]); ``` ```php $curl = new Curl(); -$curl->patch('http://api.example.com/profile/', array( +$curl->patch('https://api.example.com/profile/', [ 'image' => '@path/to/file.jpg', -)); +]); ``` ```php $curl = new Curl(); -$curl->patch('http://api.example.com/profile/', array( +$curl->patch('https://api.example.com/profile/', [ 'image' => new CURLFile('path/to/file.jpg'), -)); +]); ``` ```php $curl = new Curl(); -$curl->delete('http://api.example.com/user/', array( +$curl->delete('https://api.example.com/user/', [ 'id' => '1234', -)); +]); ``` ```php -// Enable gzip compression and download a file. +// Enable all supported encoding types and download a file. $curl = new Curl(); -$curl->setOpt(CURLOPT_ENCODING , 'gzip'); -$curl->download('https://www.example.com/image.png', '/tmp/myimage.png'); +$curl->setOpt(CURLOPT_ENCODING , ''); +$curl->download('https://www.example.com/file.bin', '/tmp/myfile.bin'); ``` ```php @@ -124,26 +148,27 @@ echo $curl->responseHeaders['CoNTeNT-TyPE'] . "\n"; // image/png ``` ```php +// Manual clean up. $curl->close(); ``` ```php // Example access to curl object. curl_set_opt($curl->curl, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1'); -curl_close($curl->curl); ``` ```php require __DIR__ . '/vendor/autoload.php'; -use \Curl\MultiCurl; +use Curl\MultiCurl; // Requests in parallel with callback functions. $multi_curl = new MultiCurl(); $multi_curl->success(function($instance) { echo 'call to "' . $instance->url . '" was successful.' . "\n"; - echo 'response: ' . $instance->response . "\n"; + echo 'response:' . "\n"; + var_dump($instance->response); }); $multi_curl->error(function($instance) { echo 'call to "' . $instance->url . '" was unsuccessful.' . "\n"; @@ -154,103 +179,227 @@ $multi_curl->complete(function($instance) { echo 'call completed' . "\n"; }); -$multi_curl->addGet('https://www.google.com/search', array( +$multi_curl->addGet('https://www.google.com/search', [ 'q' => 'hello world', -)); -$multi_curl->addGet('https://duckduckgo.com/', array( +]); +$multi_curl->addGet('https://duckduckgo.com/', [ 'q' => 'hello world', -)); -$multi_curl->addGet('https://www.bing.com/search', array( +]); +$multi_curl->addGet('https://www.bing.com/search', [ 'q' => 'hello world', -)); +]); -$multi_curl->start(); +$multi_curl->start(); // Blocks until all items in the queue have been processed. ``` -### Available Methods +More examples are available under [/examples](https://github.com/php-curl-class/php-curl-class/tree/master/examples). + +### 📖 Available Methods ```php -Curl::__construct($base_url = null) +Curl::__construct($base_url = null, $options = []) Curl::__destruct() +Curl::__get($name) +Curl::__isset($name) +Curl::afterSend($callback) +Curl::attemptRetry() Curl::beforeSend($callback) Curl::buildPostData($data) Curl::call() Curl::close() Curl::complete($callback) -Curl::delete($url, $query_parameters = array(), $data = array()) +Curl::delete($url, $query_parameters = [], $data = []) +Curl::diagnose($return = false) +Curl::disableTimeout() +Curl::displayCurlOptionValue($option, $value = null) Curl::download($url, $mixed_filename) -Curl::downloadComplete($fh) Curl::error($callback) Curl::exec($ch = null) -Curl::get($url, $data = array()) +Curl::execDone() +Curl::fastDownload($url, $filename, $connections = 4) +Curl::get($url, $data = []) +Curl::getAttempts() +Curl::getBeforeSendCallback() +Curl::getCompleteCallback() Curl::getCookie($key) +Curl::getCurl() +Curl::getCurlErrorCode() +Curl::getCurlErrorMessage() +Curl::getDownloadCompleteCallback() +Curl::getDownloadFileName() +Curl::getErrorCallback() +Curl::getErrorCode() +Curl::getErrorMessage() +Curl::getFileHandle() +Curl::getHttpErrorMessage() +Curl::getHttpStatusCode() +Curl::getId() +Curl::getInfo($opt = null) +Curl::getJsonDecoder() Curl::getOpt($option) +Curl::getOptions() +Curl::getRawResponse() +Curl::getRawResponseHeaders() +Curl::getRemainingRetries() +Curl::getRequestHeaders() +Curl::getResponse() Curl::getResponseCookie($key) -Curl::head($url, $data = array()) -Curl::headerCallback($ch, $header) -Curl::options($url, $data = array()) -Curl::patch($url, $data = array()) -Curl::post($url, $data = array()) +Curl::getResponseCookies() +Curl::getResponseHeaders() +Curl::getRetries() +Curl::getRetryDecider() +Curl::getSuccessCallback() +Curl::getUrl() +Curl::getUserSetOptions() +Curl::getXmlDecoder() +Curl::head($url, $data = []) +Curl::isChildOfMultiCurl() +Curl::isCurlError() +Curl::isError() +Curl::isHttpError() +Curl::options($url, $data = []) +Curl::patch($url, $data = []) +Curl::post($url, $data = '', $follow_303_with_post = false) Curl::progress($callback) -Curl::put($url, $data = array()) +Curl::put($url, $data = []) +Curl::removeHeader($key) +Curl::reset() +Curl::search($url, $data = []) +Curl::setAutoReferer($auto_referer = true) +Curl::setAutoReferrer($auto_referrer = true) Curl::setBasicAuthentication($username, $password = '') Curl::setConnectTimeout($seconds) Curl::setCookie($key, $value) Curl::setCookieFile($cookie_file) Curl::setCookieJar($cookie_jar) +Curl::setCookieString($string) +Curl::setCookies($cookies) +Curl::setDefaultDecoder($mixed = 'json') +Curl::setDefaultHeaderOut() Curl::setDefaultJsonDecoder() Curl::setDefaultTimeout() Curl::setDefaultUserAgent() +Curl::setDefaultXmlDecoder() +Curl::setDelete($url, $query_parameters = [], $data = []) Curl::setDigestAuthentication($username, $password = '') +Curl::setFile($file) +Curl::setFollowLocation($follow_location = true) +Curl::setForbidReuse($forbid_reuse = true) +Curl::setGet($url, $data = []) +Curl::setHead($url, $data = []) Curl::setHeader($key, $value) -Curl::setJsonDecoder($function) +Curl::setHeaders($headers) +Curl::setInterface($interface) +Curl::setJsonDecoder($mixed) +Curl::setMaxFilesize($bytes) +Curl::setMaximumRedirects($maximum_redirects) Curl::setOpt($option, $value) +Curl::setOptions($url, $data = []) +Curl::setOpts($options) +Curl::setPatch($url, $data = []) Curl::setPort($port) +Curl::setPost($url, $data = '', $follow_303_with_post = false) +Curl::setProtocols($protocols) +Curl::setProxy($proxy, $port = null, $username = null, $password = null) +Curl::setProxyAuth($auth) +Curl::setProxyTunnel($tunnel = true) +Curl::setProxyType($type) +Curl::setPut($url, $data = []) +Curl::setRange($range) +Curl::setRedirectProtocols($redirect_protocols) Curl::setReferer($referer) Curl::setReferrer($referrer) +Curl::setRetry($mixed) +Curl::setSearch($url, $data = []) +Curl::setStop($callback = null) Curl::setTimeout($seconds) -Curl::setURL($url, $data = array()) +Curl::setUrl($url, $mixed_data = '') Curl::setUserAgent($user_agent) +Curl::setXmlDecoder($mixed) +Curl::stop() Curl::success($callback) Curl::unsetHeader($key) -Curl::verbose($on = true, $output=STDERR) -Curl::http_build_multi_query($data, $key = null) -Curl::is_array_assoc($array) -Curl::is_array_multidim($array) +Curl::unsetProxy() +Curl::verbose($on = true, $output = 'STDERR') MultiCurl::__construct($base_url = null) MultiCurl::__destruct() -MultiCurl::addDelete($url, $query_parameters = array(), $data = array()) +MultiCurl::addCurl(http://github.com/Curl%20$curl) +MultiCurl::addDelete($url, $query_parameters = [], $data = []) MultiCurl::addDownload($url, $mixed_filename) -MultiCurl::addGet($url, $data = array()) -MultiCurl::addHead($url, $data = array()) -MultiCurl::addOptions($url, $data = array()) -MultiCurl::addPatch($url, $data = array()) -MultiCurl::addPost($url, $data = array()) -MultiCurl::addPut($url, $data = array()) +MultiCurl::addGet($url, $data = []) +MultiCurl::addHead($url, $data = []) +MultiCurl::addOptions($url, $data = []) +MultiCurl::addPatch($url, $data = []) +MultiCurl::addPost($url, $data = '', $follow_303_with_post = false) +MultiCurl::addPut($url, $data = []) +MultiCurl::addSearch($url, $data = []) +MultiCurl::afterSend($callback) MultiCurl::beforeSend($callback) MultiCurl::close() MultiCurl::complete($callback) +MultiCurl::disableTimeout() MultiCurl::error($callback) +MultiCurl::getActiveCurls() MultiCurl::getOpt($option) +MultiCurl::removeHeader($key) +MultiCurl::setAutoReferer($auto_referer = true) +MultiCurl::setAutoReferrer($auto_referrer = true) MultiCurl::setBasicAuthentication($username, $password = '') +MultiCurl::setConcurrency($concurrency) +MultiCurl::setConnectTimeout($seconds) MultiCurl::setCookie($key, $value) MultiCurl::setCookieFile($cookie_file) MultiCurl::setCookieJar($cookie_jar) +MultiCurl::setCookieString($string) +MultiCurl::setCookies($cookies) MultiCurl::setDigestAuthentication($username, $password = '') +MultiCurl::setFile($file) +MultiCurl::setFollowLocation($follow_location = true) +MultiCurl::setForbidReuse($forbid_reuse = true) MultiCurl::setHeader($key, $value) -MultiCurl::setJsonDecoder($function) +MultiCurl::setHeaders($headers) +MultiCurl::setInterface($interface) +MultiCurl::setJsonDecoder($mixed) +MultiCurl::setMaximumRedirects($maximum_redirects) MultiCurl::setOpt($option, $value) +MultiCurl::setOpts($options) +MultiCurl::setPort($port) +MultiCurl::setProxies($proxies) +MultiCurl::setProxy($proxy, $port = null, $username = null, $password = null) +MultiCurl::setProxyAuth($auth) +MultiCurl::setProxyTunnel($tunnel = true) +MultiCurl::setProxyType($type) +MultiCurl::setRange($range) +MultiCurl::setRateLimit($rate_limit) MultiCurl::setReferer($referer) MultiCurl::setReferrer($referrer) +MultiCurl::setRequestTimeAccuracy() +MultiCurl::setRetry($mixed) MultiCurl::setTimeout($seconds) -MultiCurl::setURL($url) +MultiCurl::setUrl($url, $mixed_data = '') MultiCurl::setUserAgent($user_agent) +MultiCurl::setXmlDecoder($mixed) MultiCurl::start() +MultiCurl::stop() MultiCurl::success($callback) MultiCurl::unsetHeader($key) -MultiCurl::verbose($on = true) +MultiCurl::unsetProxy() +MultiCurl::verbose($on = true, $output = 'STDERR') ``` -### Contribute +### 🔒 Security + +See [SECURITY](https://github.com/php-curl-class/php-curl-class/blob/master/SECURITY.md) for security considerations. + +### 🛠️ Troubleshooting + +See [TROUBLESHOOTING](https://github.com/php-curl-class/php-curl-class/blob/master/TROUBLESHOOTING.md) for help troubleshooting. + +### 🧪 Testing + +See [TESTING](https://github.com/php-curl-class/php-curl-class/blob/master/TESTING.md) for testing information. + +### 🤝 Contributing + 1. Check for open issues or open a new issue to start a discussion around a bug or feature. 1. Fork the repository on GitHub to start making your changes. 1. Write one or more tests for the new feature or that expose the bug. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..08628a7b23 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,127 @@ +# Security Considerations + +### Url may point to system files + +* Don't blindly accept urls from users as they may point to system files. Curl supports many protocols including `FILE`. + The following would show the contents of `file:///etc/passwd`. + +```bash +# Attacker. +$ curl https://www.example.com/display_webpage.php?url=file%3A%2F%2F%2Fetc%2Fpasswd +``` + +```php +// display_webpage.php +$url = $_GET['url']; // DANGER! +$curl = new Curl(); +$curl->get($url); +echo $curl->response; +``` + +Safer: + +```php +function is_allowed_url(http://github.com/$url,%20$allowed_url_schemes%20=%20['http',%20'https']) { + $valid_url = filter_var($url, FILTER_VALIDATE_URL) !== false; + if ($valid_url) { + $scheme = parse_url(http://github.com/$url,%20PHP_URL_SCHEME); + return in_array($scheme, $allowed_url_schemes, true); + } + $valid_ip = filter_var($url, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false; + return $valid_ip; +} + +$url = $_GET['url']; +if (!is_allowed_url(http://github.com/$url)) { + die('Unsafe url detected.'); +} + +$curl = new Curl(); +$curl->setProtocols(CURLPROTO_HTTPS); +$curl->setRedirectProtocols(CURLPROTO_HTTPS); +$curl->get($url); +``` + +### Url may point to internal urls + +* Url may point to internal urls including those behind a firewall (e.g. http://192.168.0.1/ or ftp://192.168.0.1/). Use + a whitelist to allow certain urls rather than a blacklist. + +* Use `Curl::setProtocols()` and `Curl::setRedirectProtocols()` to restrict allowed protocols. + +```php +// Allow only HTTPS protocols. +$curl->setProtocols(CURLPROTO_HTTPS); +$curl->setRedirectProtocols(CURLPROTO_HTTPS); +``` + +```php +// Allow HTTPS and HTTP protocols. +$curl->setProtocols(CURLPROTO_HTTPS | CURLPROTO_HTTP); +$curl->setRedirectProtocols(CURLPROTO_HTTPS | CURLPROTO_HTTP); +``` + +### Request data may refer to system files + +* Request data prefixed with the `@` character may have special interpretation and read from system files. + +```bash +# Attacker. +$ curl https://www.example.com/upload_photo.php --data "photo=@/etc/passwd" +``` + +```php +// upload_photo.php +$curl = new Curl(); +$curl->post('http://www.anotherwebsite.com/', [ + 'photo' => $_POST['photo'], // DANGER! +]); +``` + +### Unsafe response with redirection enabled + +* Requests with redirection enabled may return responses from unexpected sources. + Downloading https://www.example.com/image.png may redirect and download https://www.evil.com/virus.exe + +```php +$curl = new Curl(); +$curl->setOpt(CURLOPT_FOLLOWLOCATION, true); // DANGER! +$curl->download('https://www.example.com/image.png', 'my_image.png'); +``` + +```php +$curl = new Curl(); +$curl->setOpt(CURLOPT_FOLLOWLOCATION, true); // DANGER! +$curl->get('https://www.example.com/image.png'); +``` + +### Keep SSL protections enabled + +* Do not disable SSL protections. + +```php +curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // DANGER! +curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // DANGER! +``` + +### Prevent XML External Entity injection + +* Set the following when using the default PHP XML parser to prevent XML external entity injection. + +```php +libxml_disable_entity_loader(true); +``` + +### Prevent PHP execution of library files + +PHP files in this library are not intended to be accessible by users browsing websites. Prevent direct access to library files by moving the library folder at least one level higher than the web root directory. Alternatively, configure the server to disable php file execution for all library files. + +#### For WordPress plugin developers + +WordPress plugin developers that wish to incorporate the PHP Curl Class library into their plugin, should take special care to include only the "core" library files. + +Do one of the following: + +Option 1. Download an official release from the [releases page](https://github.com/php-curl-class/php-curl-class/releases) and incorporate the files contained in the compressed file into the plugin. The releases include only the necessary php files for the library to function. + +Option 2. Manually copy only the [src/](https://github.com/php-curl-class/php-curl-class/tree/master/src) directory into your plugin. Be sure not to copy any other php files as they may be executable by users visiting the php files directly. diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000000..aeaf8f627a --- /dev/null +++ b/TESTING.md @@ -0,0 +1,33 @@ +# Testing + +### Run Tests Locally + +To run tests: + +```bash +git clone https://github.com/php-curl-class/php-curl-class.git +cd php-curl-class/ +composer update +./tests/run.sh +``` + +To run select tests: + +```bash +git clone https://github.com/php-curl-class/php-curl-class.git +cd php-curl-class/ +composer update +./tests/run.sh --filter=keyword +``` + +To test all PHP versions in containers: + +```bash +git clone https://github.com/php-curl-class/php-curl-class.git +cd php-curl-class/ +./tests/test_all.sh +``` + +### Continuous Integration Tests + +Continuous integration runs [tests/run.sh](https://github.com/php-curl-class/php-curl-class/blob/master/tests/run.sh) on supported PHP versions and is configured with [.github/workflows/ci.yml](https://github.com/php-curl-class/php-curl-class/blob/master/.github/workflows/ci.yml). diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000000..3b4cb9f2ed --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,110 @@ +# Troubleshooting + +### Debug using the diagnose method + +```php +$curl = new Curl(); +$curl->get('https://www.example.com/'); +$curl->diagnose(); // ← HERE +``` + +### Debug the entire curl instance + +```php +$curl = new Curl(); +$curl->get('https://www.example.com/'); +var_dump($curl); // ← HERE +``` + +### Ensure you have the latest version of the library installed + +```bash +$ cd php-curl-class/ +$ composer update +$ composer info +``` +Compare your version with latest release ![](https://img.shields.io/github/release/php-curl-class/php-curl-class.svg?style=flat-square&sort=semver&color=rgba(0,0,0,0)&label=) which is also listed on the [releases page](https://github.com/php-curl-class/php-curl-class/releases). + +### Ensure php is using the latest version of curl + +```bash +$ php -r 'var_dump(curl_version());' +``` + +Compare your version of curl with latest release ![](https://img.shields.io/github/v/release/curl/curl.svg?style=flat-square&color=rgba(0,0,0,0)&label=) which is also listed on [curl's releases page](https://github.com/curl/curl/releases). + +### Turn on error reporting + +```php +error_reporting(E_ALL); +``` + +### Print some information that may hint at the cause of failure + +```php +error_reporting(E_ALL); +$curl = new Curl(); +$curl->get('https://www.example.com/'); +echo 'error: ' . $curl->error . "\n"; +echo 'errorCode: ' . $curl->errorCode . "\n"; +echo 'errorMessage: ' . $curl->errorMessage . "\n"; +echo 'curlError: ' . $curl->error . "\n"; +echo 'curlErrorCode: ' . $curl->errorCode . "\n"; +echo 'curlErrorMessage: ' . $curl->errorMessage . "\n"; +echo 'httpError: ' . $curl->httpError . "\n"; +echo 'httpStatusCode: ' . $curl->httpStatusCode . "\n"; +echo 'httpErrorMessage: ' . $curl->httpErrorMessage . "\n"; +echo 'requestHeaders:' . "\n"; +var_dump($curl->requestHeaders); +echo 'responseHeaders:' . "\n"; +var_dump($curl->responseHeaders); +``` + +### Turn on verbose mode + +```php +error_reporting(E_ALL); +$curl = new Curl(); +$curl->verbose(); +$curl->get('https://www.example.com/'); +var_dump($curl); +``` + +### Compare request with and without the library + +```php +error_reporting(E_ALL); +$curl = new Curl(); +$curl->get('https://www.example.com/'); +var_dump($curl); +``` + +```php +error_reporting(E_ALL); +$ch = curl_init(); +curl_setopt($ch, CURLINFO_HEADER_OUT, true); +curl_setopt($ch, CURLOPT_HEADER, false); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_URL, 'https://www.example.com/'); +curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); +curl_setopt($ch, CURLOPT_HTTPGET, true); +$raw_response = curl_exec($ch); +$curl_error_code = curl_errno($ch); +$curl_error_message = curl_error($ch); +$http_status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$request_headers = curl_getinfo($ch, CURLINFO_HEADER_OUT); +var_dump($http_status_code); +var_dump($curl_error_code); +var_dump($curl_error_message); +var_dump($request_headers); +var_dump($raw_response); +``` + +### Ensure you have the latest version of composer installed + +```bash +$ composer self-update +$ composer --version +``` + +Compare your version of composer with latest release ![](https://img.shields.io/github/v/release/composer/composer.svg?style=flat-square&color=rgba(0,0,0,0)&label=) which is also listed on [composer's releases page](https://github.com/composer/composer/releases). diff --git a/composer.json b/composer.json index 52c180ae25..37e568954a 100644 --- a/composer.json +++ b/composer.json @@ -1,27 +1,53 @@ { "name": "php-curl-class/php-curl-class", - "description": "PHP Curl Class is an object-oriented wrapper of the PHP cURL extension.", + "description": "PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs.", "homepage": "https://github.com/php-curl-class/php-curl-class", "license": "Unlicense", "keywords": [ - "php", "curl", "class", "api", "client", "framework", "http client", - "http", "json", "requests", "rest", "restful", "web service", "xml" + "php", "curl", "class", "api", "api-client", "client", "framework", "http", "http-client", "http-proxy", "json", + "php-curl", "php-curl-library", "proxy", "requests", "restful", "web-scraper", "web-scraping", "web-service", + "xml" ], "authors": [ { "name": "Zach Borboa" + }, + { + "name": "Contributors", + "homepage": "https://github.com/php-curl-class/php-curl-class/graphs/contributors" } ], "require": { - "php": ">=5.3", + "php": ">=8.0", "ext-curl": "*" }, "require-dev": { - "phpunit/phpunit": "*" + "dealerdirect/phpcodesniffer-composer-installer": "*", + "ext-gd": "*", + "friendsofphp/php-cs-fixer": "*", + "phpcompatibility/php-compatibility": "dev-develop", + "phpcsstandards/phpcsutils": "@alpha", + "phpstan/phpstan": "*", + "phpunit/phpunit": "*", + "squizlabs/php_codesniffer": "*" + }, + "suggest": { + "ext-mbstring": "*" }, "autoload": { "psr-4": { "Curl\\": "src/Curl/" } + }, + "autoload-dev": { + "files": [ + "./tests/Helper.php", + "./tests/User.php" + ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index 567609b123..0000000000 --- a/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 1563c75eb7..0000000000 --- a/docs/Makefile +++ /dev/null @@ -1,192 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PHPCurlClass.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PHPCurlClass.qhc" - -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PHPCurlClass" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PHPCurlClass" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/README.md b/docs/README.md index 99e3f8a24b..40ed971c42 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,2 +1,6 @@ # PHP Curl Class Documentation -http://www.phpcurlclass.com/ +https://www.phpcurlclass.com/ + +The [examples](https://github.com/php-curl-class/php-curl-class/tree/master/examples) are a great starting point. + +See also [TROUBLESHOOTING](https://github.com/php-curl-class/php-curl-class/blob/master/TROUBLESHOOTING.md) for troubleshooting. diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index 81d645232a..0000000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,291 +0,0 @@ -# -*- coding: utf-8 -*- -# -# PHP Curl Class documentation build configuration file, created by -# sphinx-quickstart. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os -import shlex - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', - 'sphinx.ext.coverage', - 'sphinx.ext.mathjax', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['ntemplates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'PHP Curl Class' -copyright = u'2015, Zach Borboa' -author = u'Zach Borboa' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '4.8.1' -# The full version, including alpha/beta/rc tags. -release = '4.8.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = [] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['nstatic'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'PHPCurlClassdoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'PHPCurlClass.tex', u'PHP Curl Class Documentation', - u'Zach Borboa', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'phpcurlclass', u'PHP Curl Class Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'PHPCurlClass', u'PHP Curl Class Documentation', - author, 'PHPCurlClass', '', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False diff --git a/docs/source/faq.rst b/docs/source/faq.rst deleted file mode 100644 index 8defc493b4..0000000000 --- a/docs/source/faq.rst +++ /dev/null @@ -1,38 +0,0 @@ -=== -FAQ -=== - - -How do I set custom cURL options? -================================= - -Set custom `cURL options -`_ using the -``Curl::setOpt`` and ``MultiCurl::setOpt`` methods. - -.. code-block:: php - - $curl->setOpt(CURLOPT_ENCODING , 'gzip'); - -Can PHP Curl Class send asynchronous requests? -============================================== - -Yes. Use the ``MultiCurl`` class to send an asynchronous requests. - -.. code-block:: php - - require __DIR__ . '/vendor/autoload.php'; - - use \Curl\MultiCurl; - - $multi_curl = new MultiCurl(); - $multi_curl->complete(function($instance) { - echo 'call completed' . "\n"; - echo $instance->response; - }); - $multi_curl->addGet('https://httpbin.org/get'); - $multi_curl->addGet('https://httpbin.org/get'); - $multi_curl->addGet('https://httpbin.org/get'); - echo 'Starting...'; - $multi_curl->start(); - echo 'All done!'; diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index b5ce08d754..0000000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. title:: PHP Curl Class - -============================ -PHP Curl Class Documentation -============================ - -PHP Curl Class makes it easy to send HTTP requests and integrate with web APIs. - -.. code-block:: php - - $curl = new Curl(); - $curl->get('https://httpbin.org/get', array( - 'q' => 'keyword', - )); - echo $curl->httpStatusCode; // 200 - echo $curl->responseHeaders['content-type']; // "application/json" - - // Send asynchronous requests. - $multi_curl = new MultiCurl(); - $multi_curl->complete(function($instance) { - echo 'call completed' . "\n"; - echo $instance->response; - }); - $multi_curl->addPost('https://httpbin.org/post'); - $multi_curl->addGet('https://httpbin.org/get'); - $multi_curl->addDelete('https://httpbin.org/delete'); - $multi_curl->start(); - - -User Guide -========== - -.. toctree:: - :maxdepth: 3 - - overview - faq diff --git a/docs/source/overview.rst b/docs/source/overview.rst deleted file mode 100644 index 8ab7ba55a0..0000000000 --- a/docs/source/overview.rst +++ /dev/null @@ -1,75 +0,0 @@ -======== -Overview -======== - -Requirements -============ - -#. PHP 5.3, 5.4, 5.5, 5.6, or HHVM. -#. PHP compiled with cURL. - -.. _installation: - - -Installation -============ - -The recommended way to install PHP Curl Class is with -`Composer `_. Composer is a dependency manager for php. - -.. code-block:: bash - - # Install Composer - curl -sS https://getcomposer.org/installer | php - -Install PHP Curl Class as a dependency using composer: - -.. code-block:: bash - - composer require php-curl-class/php-curl-class - -Composer generates a ``vendor/autoload.php`` file. You can simply include this -file and you will get autoloading for free: - -.. code-block:: php - - require __DIR__ . 'vendor/autoload.php'; - - -License -======= - -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -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 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. - -For more information, please refer to - - -Contributing -============ - -#. Check for open issues or open a new issue to start a discussion around a bug or feature. -#. Fork the repository on GitHub to start making your changes. -#. Write one or more tests for the new feature or that expose the bug. -#. Make code changes to implement the feature or fix the bug. -#. Send a pull request to get your changes merged and published. diff --git a/docs/v5.x.md b/docs/v5.x.md new file mode 100644 index 0000000000..62968571b3 --- /dev/null +++ b/docs/v5.x.md @@ -0,0 +1,9 @@ +# PHP Curl Class v5.x + +PHP Curl Class previously supported 5.3, 5.4, 5.5, and 5.6. + +### Installation + +To install PHP Curl Class for PHP 5.x versions: + + $ composer require php-curl-class/php-curl-class:dev-php5.x-master diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000000..620b34936d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,2 @@ +# Didn't find an example that fits your use case? +Please [file a ticket](https://github.com/php-curl-class/php-curl-class/issues/new). Thanks! diff --git a/examples/before_send_retry.php b/examples/before_send_retry.php new file mode 100644 index 0000000000..9be2c6ee6e --- /dev/null +++ b/examples/before_send_retry.php @@ -0,0 +1,27 @@ +setRetry($max_retries); + +$curl->beforeSend(function ($instance) { + echo 'current attempts: ' . $instance->attempts . "\n"; + echo 'current retries: ' . $instance->retries . "\n"; + echo 'about to make request to ' . $instance->url . "\n"; +}); + +$curl->get('https://httpbin.org/status/503'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + echo 'final attempts: ' . $curl->attempts . "\n"; + echo 'final retries: ' . $curl->retries . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/coinbase_account_balance.php b/examples/coinbase_account_balance.php index 9814facf67..23d7a9f435 100644 --- a/examples/coinbase_account_balance.php +++ b/examples/coinbase_account_balance.php @@ -1,10 +1,11 @@ setHeader('CB-VERSION', '2016-01-01'); +$curl->get('https://api.coinbase.com/v2/prices/BTC-USD/spot'); + +echo + 'The current price of BTC at Coinbase is ' . + '$' . $curl->response->data->amount . ' ' . $curl->response->data->currency . '.' . "\n"; diff --git a/examples/coinbase_eth_spot_price.php b/examples/coinbase_eth_spot_price.php new file mode 100644 index 0000000000..3c668406f7 --- /dev/null +++ b/examples/coinbase_eth_spot_price.php @@ -0,0 +1,13 @@ +setHeader('CB-VERSION', '2016-01-01'); +$curl->get('https://api.coinbase.com/v2/prices/ETH-USD/spot'); + +echo + 'The current price of ETH at Coinbase is ' . + '$' . $curl->response->data->amount . ' ' . $curl->response->data->currency . '.' . "\n"; diff --git a/examples/coinbase_spot_rate.php b/examples/coinbase_spot_rate.php deleted file mode 100644 index ea7d4431ab..0000000000 --- a/examples/coinbase_spot_rate.php +++ /dev/null @@ -1,11 +0,0 @@ -get('https://coinbase.com/api/v1/prices/spot_rate'); - -echo - 'The current price of bitcoin at Coinbase is ' . - '$' . $curl->response->amount . ' ' . $curl->response->currency . '.' . "\n"; diff --git a/examples/curl_after_send.php b/examples/curl_after_send.php new file mode 100644 index 0000000000..6319a269a0 --- /dev/null +++ b/examples/curl_after_send.php @@ -0,0 +1,44 @@ +setRetry($max_retries); + +$curl->beforeSend(function ($instance) { + echo 'about to make request to ' . $instance->url . "\n"; +}); + +$curl->error(function ($instance) { + echo 'not lucky this round' . "\n"; +}); + +$curl->success(function ($instance) { + echo + 'success!' . "\n" . + 'got number ' . $instance->response->args->number . ' ' . + 'after ' . $instance->attempts . ' attempt(s).' . "\n"; +}); + +$curl->afterSend(function ($instance) { + $random_number = (int)$instance->response->args->number; + $lucky = $random_number === 7; + $instance->error = !$lucky; + + if (!$lucky) { + $instance->setUrl('https://httpbin.org/get?number=' . random_int(0, 10)); + } +}); + +$curl->get('https://httpbin.org/get?number=' . random_int(0, 10)); + +// $ php curl_after_send.php +// about to make request to https://httpbin.org/get?number=3 +// about to make request to https://httpbin.org/get?number=1 +// about to make request to https://httpbin.org/get?number=7 +// success! +// got number 7 after 3 attempt(s). diff --git a/examples/curl_display_curl_option_value.php b/examples/curl_display_curl_option_value.php new file mode 100644 index 0000000000..b6f9b554ed --- /dev/null +++ b/examples/curl_display_curl_option_value.php @@ -0,0 +1,17 @@ +verbose(); + +$curl->displayCurlOptionValue(CURLOPT_VERBOSE); +// "CURLOPT_VERBOSE: true". + +$curl->displayCurlOptionValue(41); +// "CURLOPT_VERBOSE: true". + +$curl->displayCurlOptionValue(CURLOPT_PROTOCOLS); +// "CURLOPT_PROTOCOLS: 3 (CURLPROTO_HTTP | CURLPROTO_HTTPS)". diff --git a/examples/curl_display_curl_option_values.php b/examples/curl_display_curl_option_values.php new file mode 100644 index 0000000000..49715487d6 --- /dev/null +++ b/examples/curl_display_curl_option_values.php @@ -0,0 +1,42 @@ +setUserAgent('some agent'); +$curl->setTimeout(60); + +foreach ($curl->getOptions() as $option => $value) { + echo 'option ' . $option . ':' . "\n"; + $curl->displayCurlOptionValue($option, $value); + echo "\n"; +} + +// option 181: +// CURLOPT_PROTOCOLS: 3 (CURLPROTO_HTTP | CURLPROTO_HTTPS) +// +// option 182: +// CURLOPT_REDIR_PROTOCOLS: 3 (CURLPROTO_HTTP | CURLPROTO_HTTPS) +// +// option 10018: +// CURLOPT_USERAGENT: "some agent" +// +// option 13: +// CURLOPT_TIMEOUT: 60 +// +// option 2: +// CURLINFO_HEADER_OUT: true +// +// option 20056: +// CURLOPT_PROGRESSFUNCTION: (callable) +// +// option 43: +// CURLOPT_NOPROGRESS: false +// +// option 20079: +// CURLOPT_HEADERFUNCTION: (callable) +// +// option 19913: +// CURLOPT_RETURNTRANSFER: true diff --git a/examples/curl_display_curl_option_values_user_set.php b/examples/curl_display_curl_option_values_user_set.php new file mode 100644 index 0000000000..63839423eb --- /dev/null +++ b/examples/curl_display_curl_option_values_user_set.php @@ -0,0 +1,21 @@ +setUserAgent('some agent'); +$curl->setTimeout(60); + +foreach ($curl->getUserSetOptions() as $option => $value) { + echo 'user set option ' . $option . ':' . "\n"; + $curl->displayCurlOptionValue($option, $value); + echo "\n"; +} + +// user set option 10018: +// CURLOPT_USERAGENT: "some agent" +// +// user set option 13: +// CURLOPT_TIMEOUT: 60 diff --git a/examples/curl_progress.php b/examples/curl_progress.php new file mode 100644 index 0000000000..84495759bc --- /dev/null +++ b/examples/curl_progress.php @@ -0,0 +1,24 @@ +progress(function ($client, $download_size, $downloaded, $upload_size, $uploaded) { + if ($download_size === 0) { + return; + } + + $percent = floor($downloaded * 100 / $download_size); + echo ' ' . $percent . '%' . "\r"; +}); +$curl->download('https://www.php.net/distributions/manual/php_manual_en.html.gz', '/tmp/php_manual_en.html.gz'); + +if ($curl->error) { + echo 'Download error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Download complete' . "\n"; +} diff --git a/examples/curl_progress_advanced.php b/examples/curl_progress_advanced.php new file mode 100644 index 0000000000..8fd1a66baf --- /dev/null +++ b/examples/curl_progress_advanced.php @@ -0,0 +1,37 @@ +progress(function ($client, $download_size, $downloaded, $upload_size, $uploaded) { + if ($download_size === 0) { + return; + } + + // Display a progress bar: xxx% [=======> ] + $progress_size = 40; + $fraction_downloaded = $downloaded / $download_size; + $dots = round($fraction_downloaded * $progress_size); + printf('%3.0f%% [', $fraction_downloaded * 100); + $i = 0; + for (; $i < $dots - 1; $i++) { + echo '='; + } + echo '>'; + for (; $i < $progress_size - 1; $i++) { + echo ' '; + } + echo ']' . "\r"; +}); +$curl->complete(function ($instance) { + if ($instance->error) { + echo "\n" . 'Download error: ' . $instance->errorMessage . "\n"; + } else { + echo "\n" . 'Download complete' . "\n"; + } +}); +$curl->download('https://www.php.net/distributions/manual/php_manual_en.html.gz', '/tmp/php_manual_en.html.gz'); diff --git a/examples/custom.php b/examples/custom.php new file mode 100644 index 0000000000..1b3c9782a1 --- /dev/null +++ b/examples/custom.php @@ -0,0 +1,19 @@ +setOpt(CURLOPT_CUSTOMREQUEST, 'GET'); +$curl->setOpt(CURLOPT_NOBODY, true); +$curl->setOpt(CURLOPT_HEADER, true); +$curl->setUrl('https://httpbin.org/get'); +$curl->exec(); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/delete.php b/examples/delete.php new file mode 100644 index 0000000000..6852553d0f --- /dev/null +++ b/examples/delete.php @@ -0,0 +1,27 @@ +delete( + 'https://httpbin.org/delete', + [ + 'key' => 'value', + ], + [ + 'a' => '1', + 'b' => '2', + 'c' => '3', + ] +); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Data server received via DELETE:' . "\n"; + var_dump($curl->response->form); +} diff --git a/examples/deviant_art_rss.php b/examples/deviant_art_rss.php index 0ed7b756ca..dcf9df947c 100644 --- a/examples/deviant_art_rss.php +++ b/examples/deviant_art_rss.php @@ -1,13 +1,14 @@ get('http://backend.deviantart.com/rss.xml', array( +$curl->get('https://backend.deviantart.com/rss.xml', [ 'q' => 'boost:popular in:photography/people/fashion', 'type' => 'deviation', -)); +]); foreach ($curl->response->channel->item as $entry) { $thumbnails = $entry->children('http://search.yahoo.com/mrss/')->thumbnail; diff --git a/examples/diagnose_request.php b/examples/diagnose_request.php new file mode 100644 index 0000000000..15e955ab84 --- /dev/null +++ b/examples/diagnose_request.php @@ -0,0 +1,18 @@ +get('https://httpbin.org/status/400'); + +if ($curl->error) { + echo 'An error occurred:' . "\n"; + $curl->diagnose(); +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/download_file.php b/examples/download_file.php deleted file mode 100644 index 9bc0fe8be4..0000000000 --- a/examples/download_file.php +++ /dev/null @@ -1,7 +0,0 @@ -download('https://php.net/images/logos/php-med-trans.png', '/tmp/php-med-trans.png'); diff --git a/examples/download_file_with_callback.php b/examples/download_file_with_callback.php deleted file mode 100644 index 91f56cf9ca..0000000000 --- a/examples/download_file_with_callback.php +++ /dev/null @@ -1,12 +0,0 @@ -download('https://php.net/images/logos/php-med-trans.png', function($instance, $tmpfile) { - $save_to_path = '/tmp/' . basename($instance->url); - $fh = fopen($save_to_path, 'wb'); - stream_copy_to_stream($tmpfile, $fh); - fclose($fh); -}); diff --git a/examples/download_file_with_redirect.php b/examples/download_file_with_redirect.php new file mode 100644 index 0000000000..ef205176a3 --- /dev/null +++ b/examples/download_file_with_redirect.php @@ -0,0 +1,14 @@ +setOpt(CURLOPT_FOLLOWLOCATION, true); +$curl->download($start_url, '/tmp/php-med-trans.png'); + +assert($final_url === $curl->effectiveUrl); diff --git a/examples/download_files.php b/examples/download_files.php new file mode 100644 index 0000000000..2cb3036fef --- /dev/null +++ b/examples/download_files.php @@ -0,0 +1,9 @@ +download('https://www.php.net/images/logos/php-med-trans.png', '/tmp/php-med-trans.png'); +$curl->download('https://upload.wikimedia.org/wikipedia/commons/c/c1/PHP_Logo.png', '/tmp/PHP_Logo.png'); diff --git a/examples/download_files_with_callback.php b/examples/download_files_with_callback.php new file mode 100644 index 0000000000..72cfe79bbe --- /dev/null +++ b/examples/download_files_with_callback.php @@ -0,0 +1,16 @@ +url); + $fh = fopen($save_to_path, 'wb'); + stream_copy_to_stream($tmpfile, $fh); + fclose($fh); +}; + +$curl = new Curl(); +$curl->download('https://www.php.net/images/logos/php-med-trans.png', $callback); +$curl->download('https://upload.wikimedia.org/wikipedia/commons/c/c1/PHP_Logo.png', $callback); diff --git a/examples/flickr.class.php b/examples/flickr.class.php index e815b08a7c..86bfc61b4d 100644 --- a/examples/flickr.class.php +++ b/examples/flickr.class.php @@ -1,6 +1,9 @@ md5(microtime() . mt_rand()), 'oauth_timestamp' => time(), 'oauth_consumer_key' => FLICKR_API_KEY, 'oauth_signature_method' => 'HMAC-SHA1', 'oauth_version' => '1.0', - ); + ]; } private function getSignature($request_method, $url, $parameters) { ksort($parameters, SORT_STRING); - $request = implode('&', array( + $request = implode('&', [ rawurlencode($request_method), rawurlencode($url), rawurlencode(http_build_query($parameters, '', '&', PHP_QUERY_RFC3986)), - )); + ]); $key = FLICKR_API_SECRET . '&'; if (!empty($_SESSION['oauth_access_token_secret'])) { $key .= $_SESSION['oauth_access_token_secret']; @@ -73,12 +76,12 @@ private function getSignature($request_method, $url, $parameters) private function getRequestToken() { $oauth_data = $this->getOAuthParameters(); - $oauth_data['oauth_callback'] = implode('', array( - isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http', + $oauth_data['oauth_callback'] = implode('', [ + isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http', '://', $_SERVER['SERVER_NAME'], $_SERVER['SCRIPT_NAME'], - )); + ]); $request_token_url = 'https://www.flickr.com/services/oauth/request_token'; $oauth_data['oauth_signature'] = $this->getSignature('POST', $request_token_url, $oauth_data); @@ -90,10 +93,10 @@ private function getRequestToken() $_SESSION['oauth_token_secret'] = $parts['oauth_token_secret']; // Continue to Flickr for user's authorization. - header('Location: https://secure.flickr.com/services/oauth/authorize?' . http_build_query(array( + header('Location: https://secure.flickr.com/services/oauth/authorize?' . http_build_query([ 'oauth_token' => $parts['oauth_token'], 'perms' => 'write', - ))); + ])); exit; } diff --git a/examples/flickr_photo_search.php b/examples/flickr_photo_search.php index f69bb0c348..649893b41b 100644 --- a/examples/flickr_photo_search.php +++ b/examples/flickr_photo_search.php @@ -1,19 +1,12 @@ farm . '.staticflickr.com/' . $photo->server . '/' . - $photo->id . '_' . $photo->secret . '_' . $size . '.' . $ext; - - return $url; -} +use Curl\Curl; -define('FLICKR_API_KEY', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'); +const FLICKR_API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; -$data = array( +$data = [ 'method' => 'flickr.photos.search', 'api_key' => FLICKR_API_KEY, 'text' => 'happy', @@ -21,11 +14,15 @@ function construct_url(http://github.com/$photo,%20$size%20=%20's',%20$ext%20=%20'jpg') 'safe_search' => '3', 'format' => 'json', 'nojsoncallback' => '1', -); +]; $curl = new Curl(); $curl->get('https://api.flickr.com/services/rest/', $data); foreach ($curl->response->photos->photo as $photo) { - echo ''; + $size = 's'; + $ext = 'jpg'; + $url = 'https://farm/' . $photo->farm . '.staticflickr.com/' . $photo->server . '/' . + $photo->id . '_' . $photo->secret . '_' . $size . '.' . $ext; + echo ''; } diff --git a/examples/flickr_upload_photo.php b/examples/flickr_upload_photo.php index 035a2a214d..aaa0cda6b2 100644 --- a/examples/flickr_upload_photo.php +++ b/examples/flickr_upload_photo.php @@ -1,8 +1,8 @@ authenticate(); @@ -23,7 +23,7 @@ } else { $user_id = $_SESSION['user_id']; $photo_id = $result->response->photoid; - $photo_url = 'http://www.flickr.com/photos/' . $user_id . '/' . $photo_id; + $photo_url = 'https://www.flickr.com/photos/' . $user_id . '/' . $photo_id; echo '

Photo uploaded successfully. View photo.

'; } } diff --git a/examples/get.php b/examples/get.php new file mode 100644 index 0000000000..78ff21b3d6 --- /dev/null +++ b/examples/get.php @@ -0,0 +1,19 @@ +get('https://httpbin.org/get', [ + 'key' => 'value', +]); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/get_base_url_1.php b/examples/get_base_url_1.php new file mode 100644 index 0000000000..601c958224 --- /dev/null +++ b/examples/get_base_url_1.php @@ -0,0 +1,13 @@ +get([ + 'page' => $i, + ]); + // TODO: Do something with result $curl->response. +} diff --git a/examples/get_base_url_2.php b/examples/get_base_url_2.php new file mode 100644 index 0000000000..6a8bf44681 --- /dev/null +++ b/examples/get_base_url_2.php @@ -0,0 +1,14 @@ +setUrl('https://httpbin.org/get'); +for ($i = 1; $i <= 10; $i++) { + $curl->get([ + 'page' => $i, + ]); + // TODO: Do something with result $curl->response. +} diff --git a/examples/get_first_n_bytes.php b/examples/get_first_n_bytes.php new file mode 100644 index 0000000000..95e723371c --- /dev/null +++ b/examples/get_first_n_bytes.php @@ -0,0 +1,19 @@ +setRange('0-49'); +$curl->get('https://code.jquery.com/jquery-1.11.2.min.js'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + var_dump($curl->responseHeaders['status-line']); // HTTP/1.1 206 Partial Content + var_dump($curl->responseHeaders['content-length']); // 50 + var_dump($curl->responseHeaders['content-range']); // bytes 0-49/95931 + var_dump($curl->response); +} diff --git a/examples/get_pages.php b/examples/get_pages.php new file mode 100644 index 0000000000..c398d017ef --- /dev/null +++ b/examples/get_pages.php @@ -0,0 +1,13 @@ +get('https://httpbin.org/get', [ + 'page' => $i, + ]); + // TODO: Do something with result $curl->response. +} diff --git a/examples/get_relative.php b/examples/get_relative.php new file mode 100644 index 0000000000..68f08049e6 --- /dev/null +++ b/examples/get_relative.php @@ -0,0 +1,21 @@ +get('/api/test', [ + 'key' => 'value', +]); +assert($curl->url === 'https://www.example.com/api/test?key=value'); +assert($curl->url === $curl->effectiveUrl); + +// https://www.example.com/root?key=value +$response = $curl->get('/root', [ + 'key' => 'value', +]); +assert($curl->url === 'https://www.example.com/root?key=value'); +assert($curl->url === $curl->effectiveUrl); diff --git a/examples/get_response_cookies.php b/examples/get_response_cookies.php new file mode 100644 index 0000000000..47ef2e0e08 --- /dev/null +++ b/examples/get_response_cookies.php @@ -0,0 +1,16 @@ +get('https://www.php.net/'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response cookies:' . "\n"; + var_dump($curl->responseCookies); + var_dump($curl->getResponseCookies()); +} diff --git a/examples/get_with_callable_retry.php b/examples/get_with_callable_retry.php new file mode 100644 index 0000000000..ea0b61ce44 --- /dev/null +++ b/examples/get_with_callable_retry.php @@ -0,0 +1,22 @@ +setRetry(function ($instance) use ($max_retries) { + return $instance->retries < $max_retries; +}); +$curl->get('https://httpbin.org/status/503'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + echo 'attempts: ' . $curl->attempts . "\n"; + echo 'retries: ' . $curl->retries . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/get_with_callable_retry_based_on_http_status_code.php b/examples/get_with_callable_retry_based_on_http_status_code.php new file mode 100644 index 0000000000..82baba6a5b --- /dev/null +++ b/examples/get_with_callable_retry_based_on_http_status_code.php @@ -0,0 +1,23 @@ +setRetry(function ($instance) use ($max_retries) { + // Retry when the result of curl_getinfo($instance->curl, CURLINFO_HTTP_CODE) is 500, 503. + return $instance->retries < $max_retries && in_array($instance->httpStatusCode, [500, 503], true); +}); +$curl->get('https://httpbin.org/status/503'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + echo 'attempts: ' . $curl->attempts . "\n"; + echo 'retries: ' . $curl->retries . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/get_with_port.php b/examples/get_with_port.php new file mode 100644 index 0000000000..789636f705 --- /dev/null +++ b/examples/get_with_port.php @@ -0,0 +1,19 @@ +get('https://httpbin.org/get', [ + 'key' => 'value', +]); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/get_with_retry.php b/examples/get_with_retry.php new file mode 100644 index 0000000000..c060ffb596 --- /dev/null +++ b/examples/get_with_retry.php @@ -0,0 +1,20 @@ +setRetry($max_retries); +$curl->get('https://httpbin.org/status/503'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + echo 'attempts: ' . $curl->attempts . "\n"; + echo 'retries: ' . $curl->retries . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/get_without_downloading_full_error_response.php b/examples/get_without_downloading_full_error_response.php new file mode 100644 index 0000000000..21d25891f2 --- /dev/null +++ b/examples/get_without_downloading_full_error_response.php @@ -0,0 +1,40 @@ +setStop(function ($ch, $header) { + // Stop requests returning error responses early without downloading the + // full error response. + // + // Check the header for the status line starting with "HTTP/". + // Status-Line per RFC 2616: + // 6.1 Status-Line: + // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF + if (stripos($header, 'HTTP/') === 0) { + $status_line_parts = explode(' ', $header); + if (isset($status_line_parts['1'])) { + $http_status_code = $status_line_parts['1']; + $http_error = in_array((int) floor($http_status_code / 100), [4, 5], true); + if ($http_error) { + // Return true to stop receiving the response. + return true; + } + } + } + + // Return false to continue receiving the response. + return false; +}); + +$curl->get('https://www.example.com/large-500-error'); +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + echo 'Response content-length: ' . $curl->responseHeaders['content-length'] . "\n"; + echo 'Actual response size downloaded: ' . $curl->getInfo(CURLINFO_SIZE_DOWNLOAD) . "\n"; +} else { + echo 'Response content-length: ' . $curl->responseHeaders['content-length'] . "\n"; + echo 'Actual response size downloaded: ' . $curl->getInfo(CURLINFO_SIZE_DOWNLOAD) . "\n"; +} diff --git a/examples/github_create_gist.php b/examples/github_create_gist.php index ca06052016..21cfba2a5a 100644 --- a/examples/github_create_gist.php +++ b/examples/github_create_gist.php @@ -1,7 +1,8 @@ post('https://api.github.com/gists', json_encode(array( +$curl->setHeader('Content-Type', 'application/json'); +$curl->post('https://api.github.com/gists', [ 'description' => 'PHP-Curl-Class test.', 'public' => 'true', - 'files' => array( - 'Untitled.php' => array( + 'files' => [ + 'Untitled.php' => [ 'content' => $content, - ), - ), -))); + ], + ], +]); echo 'Gist created at ' . $curl->response->html_url . "\n"; diff --git a/examples/gmail_send_email.php b/examples/gmail_send_email.php index 3fdbfb59ca..325c4d12fc 100644 --- a/examples/gmail_send_email.php +++ b/examples/gmail_send_email.php @@ -1,10 +1,11 @@ post('https://accounts.google.com/o/oauth2/token', array( + $curl->post('https://accounts.google.com/o/oauth2/token', [ 'code' => $code, 'client_id' => CLIENT_ID, 'client_secret' => CLIENT_SECRET, - 'redirect_uri' => implode('', array( - isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http', + 'redirect_uri' => implode('', [ + isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http', '://', $_SERVER['SERVER_NAME'], $_SERVER['SCRIPT_NAME'], - )), + ]), 'grant_type' => 'authorization_code', - )); + ]); if ($curl->error) { echo $curl->response->error . ': ' . $curl->response->error_description; @@ -33,6 +34,7 @@ $_SESSION['access_token'] = $curl->response->access_token; header('Location: ?'); + exit; } elseif (!empty($_SESSION['access_token'])) { // Use the access token to send an email. $curl = new Curl(); @@ -62,18 +64,18 @@ echo 'Email ' . $curl->response->id . ' was sent.'; } else { $curl = new Curl(); - $curl->get('https://accounts.google.com/o/oauth2/auth', array( + $curl->get('https://accounts.google.com/o/oauth2/auth', [ 'scope' => 'https://www.googleapis.com/auth/gmail.compose', - 'redirect_uri' => implode('', array( - isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http', + 'redirect_uri' => implode('', [ + isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http', '://', $_SERVER['SERVER_NAME'], $_SERVER['SCRIPT_NAME'], - )), + ]), 'response_type' => 'code', 'client_id' => CLIENT_ID, 'approval_prompt' => 'force', - )); + ]); $url = $curl->responseHeaders['Location']; echo 'Continue'; diff --git a/examples/google_maps_geocode_address.php b/examples/google_maps_geocode_address.php index 52f008294a..688528c118 100644 --- a/examples/google_maps_geocode_address.php +++ b/examples/google_maps_geocode_address.php @@ -1,13 +1,14 @@ get('http://maps.googleapis.com/maps/api/geocode/json', array( +$curl->get('https://maps.googleapis.com/maps/api/geocode/json', [ 'address' => $address, -)); +]); if ($curl->response->status === 'OK') { $result = $curl->response->results['0']; diff --git a/examples/google_plus_profile.php b/examples/google_plus_profile.php index dea0c47482..77cbf0f3a0 100644 --- a/examples/google_plus_profile.php +++ b/examples/google_plus_profile.php @@ -1,10 +1,11 @@ post('https://accounts.google.com/o/oauth2/token', array( + $curl->post('https://accounts.google.com/o/oauth2/token', [ 'code' => $code, 'client_id' => CLIENT_ID, 'client_secret' => CLIENT_SECRET, - 'redirect_uri' => implode('', array( - isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http', + 'redirect_uri' => implode('', [ + isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http', '://', $_SERVER['SERVER_NAME'], $_SERVER['SCRIPT_NAME'], - )), + ]), 'grant_type' => 'authorization_code', - )); + ]); if ($curl->error) { echo $curl->response->error . ': ' . $curl->response->error_description; @@ -33,6 +34,7 @@ $_SESSION['access_token'] = $curl->response->access_token; header('Location: ?'); + exit; } elseif (!empty($_SESSION['access_token']) && !isset($_GET['retry'])) { // Use the access token to retrieve the profile. $curl = new Curl(); @@ -49,18 +51,18 @@ echo 'Hi ' . $curl->response->displayName . '.'; } else { $curl = new Curl(); - $curl->get('https://accounts.google.com/o/oauth2/auth', array( + $curl->get('https://accounts.google.com/o/oauth2/auth', [ 'scope' => 'https://www.googleapis.com/auth/plus.me', - 'redirect_uri' => implode('', array( - isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http', + 'redirect_uri' => implode('', [ + isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http', '://', $_SERVER['SERVER_NAME'], $_SERVER['SCRIPT_NAME'], - )), + ]), 'response_type' => 'code', 'client_id' => CLIENT_ID, 'approval_prompt' => 'force', - )); + ]); $url = $curl->responseHeaders['Location']; echo 'Continue'; diff --git a/examples/google_spreadsheet_values_update.php b/examples/google_spreadsheet_values_update.php new file mode 100644 index 0000000000..28be8bfe45 --- /dev/null +++ b/examples/google_spreadsheet_values_update.php @@ -0,0 +1,72 @@ + 'offline', + 'approval_prompt' => 'force', + 'client_id' => CLIENT_ID, + 'redirect_uri' => REDIRECT_URI, + 'response_type' => 'code', + 'scope' => 'https://www.googleapis.com/auth/spreadsheets', +]); +echo 'Open the following link in your browser:' . "\n"; +echo $auth_url . "\n"; +echo 'Enter verification code: '; +$code = trim(fgets(STDIN)); + +// Exchange authorization code for an access token. +$curl = new Curl(); +$curl->post(OAUTH2_TOKEN_URI, [ + 'client_id' => CLIENT_ID, + 'client_secret' => CLIENT_SECRET, + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => REDIRECT_URI, +]); +$access_token = $curl->response; + +// Update spreadsheet. +$spreadsheet_id = '1Z2cXhdG-K44KgSzHTcGhx1dY-xY31yuYGwX21F4GeUp'; +$range = 'Sheet1!A1'; +$url = 'https://sheets.googleapis.com/v4/spreadsheets/' . $spreadsheet_id . '/values/' . $range; +$url .= '?' . http_build_query([ + 'valueInputOption' => 'USER_ENTERED', +]); + +$data = [ + 'values' => [ + [ + 'This is cell A1', + 'B1', + 'C1', + 'and D1', + ], + ], +]; + +$curl = new Curl(); +$curl->setHeader('Content-Type', 'application/json'); +$curl->setHeader('Authorization', 'Bearer ' . $access_token->access_token); +$curl->put($url, $data); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + var_dump($curl); +} else { + var_dump($curl->response); +} diff --git a/examples/gratipay_send_tip.php b/examples/gratipay_send_tip.php index e3f08dad29..160d265fc0 100644 --- a/examples/gratipay_send_tip.php +++ b/examples/gratipay_send_tip.php @@ -1,28 +1,29 @@ 'user' . mt_rand(), 'platform' => 'gratipay', 'amount' => '0.02', - ), - array( + ], + [ 'username' => 'user' . mt_rand(), 'platform' => 'gratipay', 'amount' => '0.02', - ), -); + ], +]; $curl = new Curl(); $curl->setHeader('Content-Type', 'application/json'); $curl->setBasicAuthentication(GRATIPAY_API_KEY); -$curl->post('https://gratipay.com/' . GRATIPAY_USERNAME . '/tips.json', json_encode($data)); +$curl->post('https://gratipay.com/' . GRATIPAY_USERNAME . '/tips.json', $data); foreach ($curl->response as $tip) { echo $tip->amount . ' given to ' . $tip->username . '.' . "\n"; diff --git a/examples/head.php b/examples/head.php new file mode 100644 index 0000000000..037d023a82 --- /dev/null +++ b/examples/head.php @@ -0,0 +1,19 @@ +head('http://127.0.0.1:8000/', [ + 'key' => 'value', +]); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response headers:' . "\n"; + var_dump($curl->responseHeaders); +} diff --git a/examples/instagram_popular_media.php b/examples/instagram_popular_media.php index a29e6d57a2..bb61ae3a9d 100644 --- a/examples/instagram_popular_media.php +++ b/examples/instagram_popular_media.php @@ -1,31 +1,32 @@ post('https://api.instagram.com/oauth/access_token', array( + $curl->post('https://api.instagram.com/oauth/access_token', [ 'client_id' => INSTAGRAM_CLIENT_ID, 'client_secret' => INSTAGRAM_CLIENT_SECRET, 'grant_type' => 'authorization_code', 'redirect_uri' => $redirect_uri, 'code' => $code, - )); + ]); if ($curl->error) { echo $curl->response->error_type . ': ' . $curl->response->errorMessage . '
'; @@ -38,9 +39,9 @@ if (isset($_SESSION['access_token'])) { $curl = new Curl(); - $curl->get('https://api.instagram.com/v1/media/popular', array( + $curl->get('https://api.instagram.com/v1/media/popular', [ 'access_token' => $_SESSION['access_token'], - )); + ]); foreach ($curl->response->data as $media) { echo '' . @@ -48,10 +49,10 @@ ''; } } else { - header('Location: https://api.instagram.com/oauth/authorize/?' . http_build_query(array( + header('Location: https://api.instagram.com/oauth/authorize/?' . http_build_query([ 'client_id' => INSTAGRAM_CLIENT_ID, 'redirect_uri' => $redirect_uri, 'response_type' => 'code', - ))); + ])); exit; } diff --git a/examples/instagram_search_photos.php b/examples/instagram_search_photos.php index 8682f71c3e..1b2a9afb7e 100644 --- a/examples/instagram_search_photos.php +++ b/examples/instagram_search_photos.php @@ -1,16 +1,17 @@ get('https://api.instagram.com/v1/media/search', array( +$curl->get('https://api.instagram.com/v1/media/search', [ 'client_id' => INSTAGRAM_CLIENT_ID, 'lat' => '37.8296', 'lng' => '-122.4832', -)); +]); foreach ($curl->response->data as $media) { $image = $media->images->low_resolution; diff --git a/examples/mailchimp_subscribe_email_address.php b/examples/mailchimp_subscribe_email_address.php index f7ad797007..6692972f86 100644 --- a/examples/mailchimp_subscribe_email_address.php +++ b/examples/mailchimp_subscribe_email_address.php @@ -1,17 +1,18 @@ get(MAILCHIMP_BASE_URL . '/lists/list.json', array( +$curl->get($MAILCHIMP_BASE_URL . '/lists/list.json', [ 'apikey' => MAILCHIMP_API_KEY, -)); +]); if ($curl->response->total === 0) { echo 'No lists found'; @@ -21,13 +22,13 @@ $lists = $curl->response->data; $list = $lists['0']; -$curl->post(MAILCHIMP_BASE_URL . '/lists/subscribe.json', array( +$curl->post($MAILCHIMP_BASE_URL . '/lists/subscribe.json', [ 'apikey' => MAILCHIMP_API_KEY, 'id' => $list->id, - 'email' => array( + 'email' => [ 'email' => 'user@example.com', - ), -)); + ], +]); if ($curl->error) { echo $curl->response->name . ': ' . $curl->response->error . "\n"; diff --git a/examples/memory_leak_test_curl.php b/examples/memory_leak_test_curl.php new file mode 100644 index 0000000000..da3ca02619 --- /dev/null +++ b/examples/memory_leak_test_curl.php @@ -0,0 +1,14 @@ +get('http://127.0.0.1:8000/'); + } + echo 'memory ' . $i . ': ' . memory_get_usage(true) . "\n"; +} diff --git a/examples/memory_leak_test_multi_curl.php b/examples/memory_leak_test_multi_curl.php new file mode 100644 index 0000000000..978b04b842 --- /dev/null +++ b/examples/memory_leak_test_multi_curl.php @@ -0,0 +1,15 @@ +addGet('http://127.0.0.1:8000/'); + } + $multi_curl->start(); + echo 'memory ' . $i . ': ' . memory_get_usage(true) . "\n"; +} diff --git a/examples/multi_curl_add_curl.php b/examples/multi_curl_add_curl.php new file mode 100644 index 0000000000..be5b809c33 --- /dev/null +++ b/examples/multi_curl_add_curl.php @@ -0,0 +1,29 @@ +complete(function ($instance) { + echo 'call to "' . $instance->url . '" completed.' . "\n"; +}); + +$curl_1 = new Curl(); +$curl_1->setPost('https://httpbin.org/post', [ + 'to' => 'alice', + 'subject' => 'hi', + 'body' => 'hi Alice', +]); +$multi_curl->addCurl(http://github.com/$curl_1); + +$curl_2 = new Curl(); +$curl_2->setPost('https://httpbin.org/post', [ + 'to' => 'bob', + 'subject' => 'hi', + 'body' => 'hi Bob', +]); +$multi_curl->addCurl(http://github.com/$curl_2); + +$multi_curl->start(); diff --git a/examples/multi_curl_add_curl_from_url_list.php b/examples/multi_curl_add_curl_from_url_list.php new file mode 100644 index 0000000000..a148fe7bd2 --- /dev/null +++ b/examples/multi_curl_add_curl_from_url_list.php @@ -0,0 +1,42 @@ +setConcurrency($concurrency); +$multi_curl->complete(function ($instance) use (&$multi_curl, &$urls) { + echo 'complete:' . $instance->url . "\n"; + + // Queue another request each time a request completes. Fetch the oldest url + // next using array_shift($urls) or use the most recently added url using + // array_pop($urls). + // $next_url = array_shift($urls); + // $next_url = array_pop($urls); + $next_url = array_shift($urls); + + if ($next_url !== null) { + $multi_curl->addGet($next_url); + } +}); + +// Queue a few requests. +for ($i = 0; $i < $concurrency; $i++) { + $next_url = array_shift($urls); + if ($next_url !== null) { + $multi_curl->addGet($next_url); + } +} + +$multi_curl->start(); diff --git a/examples/multi_curl_add_curl_low_level.php b/examples/multi_curl_add_curl_low_level.php new file mode 100644 index 0000000000..f932205a39 --- /dev/null +++ b/examples/multi_curl_add_curl_low_level.php @@ -0,0 +1,33 @@ +complete(function ($instance) { + echo 'call to "' . $instance->url . '" completed.' . "\n"; +}); + +$curl_1 = new Curl(); +$curl_1->setOpt(CURLOPT_POST, true); +$curl_1->setOpt(CURLOPT_POSTFIELDS, [ + 'to' => 'alice', + 'subject' => 'hi', + 'body' => 'hi Alice', +]); +$curl_1->setUrl('https://httpbin.org/post'); +$multi_curl->addCurl(http://github.com/$curl_1); + +$curl_2 = new Curl(); +$curl_2->setOpt(CURLOPT_POST, true); +$curl_2->setOpt(CURLOPT_POSTFIELDS, [ + 'to' => 'bob', + 'subject' => 'hi', + 'body' => 'hi Bob', +]); +$curl_2->setUrl('https://httpbin.org/post'); +$multi_curl->addCurl(http://github.com/$curl_2); + +$multi_curl->start(); diff --git a/examples/multi_curl_after_send.php b/examples/multi_curl_after_send.php new file mode 100644 index 0000000000..dc37c67562 --- /dev/null +++ b/examples/multi_curl_after_send.php @@ -0,0 +1,44 @@ +setRetry($max_retries); + +$multi_curl->beforeSend(function ($instance) { + echo 'about to make request to ' . $instance->url . "\n"; +}); + +$multi_curl->error(function ($instance) { + echo 'not lucky this round' . "\n"; +}); + +$multi_curl->success(function ($instance) { + echo + 'success!' . "\n" . + 'got number ' . $instance->response->args->number . ' ' . + 'after ' . $instance->attempts . ' attempt(s).' . "\n"; +}); + +$multi_curl->afterSend(function ($instance) { + $random_number = (int)$instance->response->args->number; + $lucky = $random_number === 7; + $instance->error = !$lucky; + + if (!$lucky) { + $instance->setUrl('https://httpbin.org/get?number=' . random_int(0, 10)); + } +}); + +$multi_curl->addGet('https://httpbin.org/get?number=' . random_int(0, 10)); +$multi_curl->start(); + +// $ php multi_curl_after_send.php +// about to make request to https://httpbin.org/get?number=4 +// about to make request to https://httpbin.org/get?number=7 +// success! +// got number 7 after 2 attempt(s). diff --git a/examples/multi_curl_before_send.php b/examples/multi_curl_before_send.php new file mode 100644 index 0000000000..f0a03beb73 --- /dev/null +++ b/examples/multi_curl_before_send.php @@ -0,0 +1,24 @@ + 'application/json', + 'X-CUSTOM-HEADER' => 'my-custom-header', +]; + +$multi_curl = new MultiCurl(); + +$multi_curl->beforeSend(function ($instance) use ($headers) { + foreach ($headers as $key => $value) { + $instance->setHeader($key, $value); + } +}); + +$multi_curl->addGet('https://www.example.com/'); +$multi_curl->addGet('https://www.example.org/'); +$multi_curl->addGet('https://www.example.net/'); + +$multi_curl->start(); diff --git a/examples/multi_curl_before_send_retry.php b/examples/multi_curl_before_send_retry.php new file mode 100644 index 0000000000..3f4587de62 --- /dev/null +++ b/examples/multi_curl_before_send_retry.php @@ -0,0 +1,31 @@ +setRetry($max_retries); + +$multi_curl->beforeSend(function ($instance) { + echo 'current attempts: ' . $instance->attempts . "\n"; + echo 'current retries: ' . $instance->retries . "\n"; + echo 'about to make request to ' . $instance->url . "\n"; +}); + +$multi_curl->complete(function ($instance) { + if ($instance->error) { + echo 'Error: ' . $instance->errorMessage . "\n"; + echo 'final attempts: ' . $instance->attempts . "\n"; + echo 'final retries: ' . $instance->retries . "\n"; + } else { + echo 'Response:' . "\n"; + var_dump($instance->response); + } +}); + +$multi_curl->addGet('https://httpbin.org/status/503'); + +$multi_curl->start(); diff --git a/examples/multi_curl_delete.php b/examples/multi_curl_delete.php index ad6465d35f..b9f8670dc3 100644 --- a/examples/multi_curl_delete.php +++ b/examples/multi_curl_delete.php @@ -1,16 +1,16 @@ addDelete('https://httpbin.org/delete', array( +$multi_curl->addDelete('https://httpbin.org/delete', [ 'id' => '123', -)); -$multi_curl->addDelete('https://httpbin.org/delete', array( +]); +$multi_curl->addDelete('https://httpbin.org/delete', [ 'id' => '456', -)); +]); $multi_curl->start(); diff --git a/examples/multi_curl_download.php b/examples/multi_curl_download.php deleted file mode 100644 index 21dedf36b0..0000000000 --- a/examples/multi_curl_download.php +++ /dev/null @@ -1,10 +0,0 @@ -addDownload('https://php.net/images/logos/php-med-trans.png', '/tmp/php-med-trans.png'); -$multi_curl->addDownload('https://upload.wikimedia.org/wikipedia/commons/c/c1/PHP_Logo.png', '/tmp/PHP_Logo.png'); -$multi_curl->start(); diff --git a/examples/multi_curl_download_files.php b/examples/multi_curl_download_files.php new file mode 100644 index 0000000000..615ae95780 --- /dev/null +++ b/examples/multi_curl_download_files.php @@ -0,0 +1,10 @@ +addDownload('https://www.php.net/images/logos/php-med-trans.png', '/tmp/php-med-trans.png'); +$multi_curl->addDownload('https://upload.wikimedia.org/wikipedia/commons/c/c1/PHP_Logo.png', '/tmp/PHP_Logo.png'); +$multi_curl->start(); diff --git a/examples/multi_curl_download_with_callback.php b/examples/multi_curl_download_files_with_callback.php similarity index 60% rename from examples/multi_curl_download_with_callback.php rename to examples/multi_curl_download_files_with_callback.php index bd058265a9..5b48302d24 100644 --- a/examples/multi_curl_download_with_callback.php +++ b/examples/multi_curl_download_files_with_callback.php @@ -1,10 +1,10 @@ url); $fh = fopen($save_to_path, 'wb'); stream_copy_to_stream($tmpfile, $fh); @@ -12,6 +12,6 @@ }; $multi_curl = new MultiCurl(); -$multi_curl->addDownload('https://php.net/images/logos/php-med-trans.png', $callback); +$multi_curl->addDownload('https://www.php.net/images/logos/php-med-trans.png', $callback); $multi_curl->addDownload('https://upload.wikimedia.org/wikipedia/commons/c/c1/PHP_Logo.png', $callback); $multi_curl->start(); diff --git a/examples/multi_curl_download_files_with_callbacks.php b/examples/multi_curl_download_files_with_callbacks.php new file mode 100644 index 0000000000..d56174f7de --- /dev/null +++ b/examples/multi_curl_download_files_with_callbacks.php @@ -0,0 +1,22 @@ +success(function ($instance) { + echo 'call to "' . $instance->url . '" was successful.' . "\n"; +}); +$multi_curl->error(function ($instance) { + echo 'call to "' . $instance->url . '" was unsuccessful.' . "\n"; + echo 'error code: ' . $instance->errorCode . "\n"; + echo 'error message: ' . $instance->errorMessage . "\n"; +}); +$multi_curl->complete(function ($instance) { + echo 'call to "' . $instance->url . '" completed.' . "\n"; +}); + +$multi_curl->addDownload('https://www.php.net/images/logos/php-med-trans.png', '/tmp/php-med-trans.png'); +$multi_curl->addDownload('https://upload.wikimedia.org/wikipedia/commons/c/c1/PHP_Logo.png', '/tmp/PHP_Logo.png'); +$multi_curl->start(); diff --git a/examples/multi_curl_get.php b/examples/multi_curl_get.php index 6f924a6e73..8bd3aca0e6 100644 --- a/examples/multi_curl_get.php +++ b/examples/multi_curl_get.php @@ -1,19 +1,19 @@ addGet('https://www.google.com/search', array( +$multi_curl->addGet('https://www.google.com/search', [ 'q' => 'hello world', -)); -$multi_curl->addGet('https://duckduckgo.com/', array( +]); +$multi_curl->addGet('https://duckduckgo.com/', [ 'q' => 'hello world', -)); -$multi_curl->addGet('https://www.bing.com/search', array( +]); +$multi_curl->addGet('https://www.bing.com/search', [ 'q' => 'hello world', -)); +]); $multi_curl->start(); diff --git a/examples/multi_curl_get_callbacks.php b/examples/multi_curl_get_callbacks.php index f8c1cba29d..805c03e1d7 100644 --- a/examples/multi_curl_get_callbacks.php +++ b/examples/multi_curl_get_callbacks.php @@ -1,32 +1,32 @@ success(function($instance) { +$multi_curl->success(function ($instance) { echo 'call to "' . $instance->url . '" was successful.' . "\n"; echo 'response: ' . $instance->response . "\n"; }); -$multi_curl->error(function($instance) { +$multi_curl->error(function ($instance) { echo 'call to "' . $instance->url . '" was unsuccessful.' . "\n"; echo 'error code: ' . $instance->errorCode . "\n"; echo 'error message: ' . $instance->errorMessage . "\n"; }); -$multi_curl->complete(function($instance) { - echo 'call completed' . "\n"; +$multi_curl->complete(function ($instance) { + echo 'call to "' . $instance->url . '" completed.' . "\n"; }); -$multi_curl->addGet('https://www.google.com/search', array( +$multi_curl->addGet('https://www.google.com/search', [ 'q' => 'hello world', -)); -$multi_curl->addGet('https://duckduckgo.com/', array( +]); +$multi_curl->addGet('https://duckduckgo.com/', [ 'q' => 'hello world', -)); -$multi_curl->addGet('https://www.bing.com/search', array( +]); +$multi_curl->addGet('https://www.bing.com/search', [ 'q' => 'hello world', -)); +]); $multi_curl->start(); diff --git a/examples/multi_curl_get_load_test.php b/examples/multi_curl_get_load_test.php new file mode 100644 index 0000000000..bf015dd9a9 --- /dev/null +++ b/examples/multi_curl_get_load_test.php @@ -0,0 +1,43 @@ +setConcurrency(30); + +$success = 0; +$error = 0; +$complete = 0; + +$multi_curl->success(function ($instance) use (&$success) { + $success += 1; +}); +$multi_curl->error(function ($instance) use (&$error) { + $error += 1; +}); +$multi_curl->complete(function ($instance) use (&$complete) { + $complete += 1; +}); + +$limit = 1000; +for ($i = 0; $i < $limit; $i++) { + $url = $urls[mt_rand(0, count($urls) - 1)]; + $multi_curl->addGet($url); +} + +$multi_curl->start(); + +echo 'complete: ' . $complete . "\n"; +echo 'success: ' . $success . "\n"; +echo 'error: ' . $error . "\n"; +echo 'done' . "\n"; diff --git a/examples/multi_curl_get_process_later.php b/examples/multi_curl_get_process_later.php new file mode 100644 index 0000000000..ea9debb983 --- /dev/null +++ b/examples/multi_curl_get_process_later.php @@ -0,0 +1,32 @@ +addGet($url); +} +$multi_curl->complete(function ($instance) use (&$responses) { + // Store responses. + $responses[] = $instance->response; + + // Alternatively, process each response here inside the callback as it is received. +}); +$multi_curl->start(); + +// Process responses. +foreach ($responses as $response) { + var_dump($response); +} diff --git a/examples/multi_curl_get_relative.php b/examples/multi_curl_get_relative.php new file mode 100644 index 0000000000..ee83d9e49b --- /dev/null +++ b/examples/multi_curl_get_relative.php @@ -0,0 +1,18 @@ +addGet('page1.html'); +assert($get_1->url === 'https://www.example.com/sites/page1.html'); + +$get_2 = $multi_curl->addGet('page2.html'); +assert($get_2->url === 'https://www.example.com/sites/page2.html'); + +$get_3 = $multi_curl->addGet('page3.html'); +assert($get_3->url === 'https://www.example.com/sites/page3.html'); + +$multi_curl->start(); diff --git a/examples/multi_curl_get_with_callable_retry.php b/examples/multi_curl_get_with_callable_retry.php new file mode 100644 index 0000000000..fd76f69afc --- /dev/null +++ b/examples/multi_curl_get_with_callable_retry.php @@ -0,0 +1,23 @@ +setRetry(function ($instance) use ($max_retries) { + return $instance->retries < $max_retries; +}); +$multi_curl->complete(function ($instance) { + echo 'call to "' . $instance->url . '" completed.' . "\n"; + echo 'attempts: ' . $instance->attempts . "\n"; + echo 'retries: ' . $instance->retries . "\n"; +}); + +$multi_curl->addGet('https://httpbin.org/status/503?a'); +$multi_curl->addGet('https://httpbin.org/status/503?b'); +$multi_curl->addGet('https://httpbin.org/status/503?c'); + +$multi_curl->start(); diff --git a/examples/multi_curl_get_with_new_random_proxy.php b/examples/multi_curl_get_with_new_random_proxy.php new file mode 100644 index 0000000000..662dd34056 --- /dev/null +++ b/examples/multi_curl_get_with_new_random_proxy.php @@ -0,0 +1,42 @@ +setProxyType(CURLPROXY_SOCKS5); +$multi_curl->setProxies($proxies); + +$multi_curl->setRetry(function ($instance) use ($proxies, $max_retries) { + if ($instance->retries < $max_retries) { + $new_random_proxy = ArrayUtil::arrayRandom($proxies); + $instance->setProxy($new_random_proxy); + return true; + } else { + return false; + } +}); + +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/ip'); + +$multi_curl->complete(function ($instance) { + echo + 'curl id ' . $instance->id . ' completed:' . "\n" . + '- ip: ' . $instance->response->origin . "\n" . + '- proxy: ' . $instance->getOpt(CURLOPT_PROXY) . "\n" . + '- url: ' . $instance->effectiveUrl . '' . "\n" . + ''; +}); + +$multi_curl->start(); diff --git a/examples/multi_curl_get_with_new_random_unique_proxy.php b/examples/multi_curl_get_with_new_random_unique_proxy.php new file mode 100644 index 0000000000..05405cecbc --- /dev/null +++ b/examples/multi_curl_get_with_new_random_unique_proxy.php @@ -0,0 +1,50 @@ +setProxyType(CURLPROXY_SOCKS5); +$multi_curl->beforeSend(function ($instance) use ($proxies, &$current_proxies) { + if (!count($current_proxies)) { + $current_proxies = $proxies; + } + + // Use random proxy that hasn't been used yet. + $rand_key = ArrayUtil::arrayRandomIndex($current_proxies); + $new_random_proxy = $current_proxies[$rand_key]; + $instance->setProxy($new_random_proxy); + + // Remove proxy from list of current proxies. + unset($current_proxies[$rand_key]); + + // Re-index list of current proxies. + $current_proxies = array_values($current_proxies); + + echo 'about to make request ' . $instance->id . ' using proxy "' . $new_random_proxy . '".' . "\n"; +}); + +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/ip'); + +$multi_curl->complete(function ($instance) { + echo + 'curl id ' . $instance->id . ' completed:' . "\n" . + '- ip: ' . $instance->response->origin . "\n" . + '- proxy: ' . $instance->getOpt(CURLOPT_PROXY) . "\n" . + '- url: ' . $instance->effectiveUrl . '' . "\n" . + ''; +}); + +$multi_curl->start(); diff --git a/examples/multi_curl_get_with_rate_limit.php b/examples/multi_curl_get_with_rate_limit.php new file mode 100644 index 0000000000..612182bcbd --- /dev/null +++ b/examples/multi_curl_get_with_rate_limit.php @@ -0,0 +1,49 @@ +setRateLimit('2/10s'); + +$multi_curl->beforeSend(function ($instance) use ($start_time) { + echo + sprintf('%.6f', round(microtime(true) - $start_time, 6)) . ' - ' . + 'request ' . $instance->id . ' start' . "\n"; +}); + +$multi_curl->success(function ($instance) use ($start_time) { + echo + sprintf('%.6f', round(microtime(true) - $start_time, 6)) . ' - ' . + 'request ' . $instance->id . ' successful (' . $instance->url . ')' . "\n"; +}); +$multi_curl->error(function ($instance) use ($start_time) { + echo + sprintf('%.6f', round(microtime(true) - $start_time, 6)) . ' - ' . + 'request ' . $instance->id . ' unsuccessful (' . $instance->url . ')' . "\n"; +}); + +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/status/503'); + +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/status/503'); + +$multi_curl->addGet('https://httpbin.org/ip'); + +$multi_curl->start(); + +// $ php multi_curl_get_with_rate_limit.php +// 0.021839 - request 0 start +// 0.021894 - request 1 start +// 0.661308 - request 0 successful (https://httpbin.org/ip) +// 0.661968 - request 1 unsuccessful (https://httpbin.org/status/503) +// 10.024627 - request 2 start +// 10.024694 - request 3 start +// 10.114304 - request 2 successful (https://httpbin.org/ip) +// 10.117299 - request 3 unsuccessful (https://httpbin.org/status/503) +// 20.029945 - request 4 start +// 20.112836 - request 4 successful (https://httpbin.org/ip) diff --git a/examples/multi_curl_get_with_retry.php b/examples/multi_curl_get_with_retry.php new file mode 100644 index 0000000000..11a246acd5 --- /dev/null +++ b/examples/multi_curl_get_with_retry.php @@ -0,0 +1,21 @@ +setRetry($max_retries); +$multi_curl->complete(function ($instance) { + echo 'call to "' . $instance->url . '" completed.' . "\n"; + echo 'attempts: ' . $instance->attempts . "\n"; + echo 'retries: ' . $instance->retries . "\n"; +}); + +$multi_curl->addGet('https://httpbin.org/status/503?a'); +$multi_curl->addGet('https://httpbin.org/status/503?b'); +$multi_curl->addGet('https://httpbin.org/status/503?c'); + +$multi_curl->start(); diff --git a/examples/multi_curl_patch.php b/examples/multi_curl_patch.php index 921b0bada3..4a7e244a41 100644 --- a/examples/multi_curl_patch.php +++ b/examples/multi_curl_patch.php @@ -1,18 +1,18 @@ addPatch('https://httpbin.org/patch', array( +$multi_curl->addPatch('https://httpbin.org/patch', [ 'id' => '123', 'body' => 'hello world!', -)); -$multi_curl->addPatch('https://httpbin.org/patch', array( +]); +$multi_curl->addPatch('https://httpbin.org/patch', [ 'id' => '456', 'body' => 'hello world!', -)); +]); $multi_curl->start(); diff --git a/examples/multi_curl_post.php b/examples/multi_curl_post.php index 95d953a749..08ddf1e522 100644 --- a/examples/multi_curl_post.php +++ b/examples/multi_curl_post.php @@ -1,20 +1,20 @@ addPost('https://httpbin.org/post', array( +$multi_curl->addPost('https://httpbin.org/post', [ 'to' => 'alice', 'subject' => 'hi', 'body' => 'hi Alice', -)); -$multi_curl->addPost('https://httpbin.org/post', array( +]); +$multi_curl->addPost('https://httpbin.org/post', [ 'to' => 'bob', 'subject' => 'hi', 'body' => 'hi Bob', -)); +]); $multi_curl->start(); diff --git a/examples/multi_curl_progress_advanced.php b/examples/multi_curl_progress_advanced.php new file mode 100644 index 0000000000..663e29a9ff --- /dev/null +++ b/examples/multi_curl_progress_advanced.php @@ -0,0 +1,137 @@ + ] +// 23% [========> ] +// 15% [=====> ] +// +// Note: The server needs to send a content-length header for progress updates to work. + +use Curl\MultiCurl; + +// Keep track of download progress for each of the downloads. +$download_status = []; + +// Keep track of when the screen was last updated so it not updated too frequently. +$last_updated_time = 0; + +$multi_curl = new MultiCurl(); + +$urls_to_download = [ + 'https://www.php.net/distributions/manual/php_manual_en.html.gz', + 'https://www.php.net/distributions/manual/php_manual_en.tar.gz', + 'https://www.php.net/distributions/manual/php_manual_en.chm', +]; + +$i = 0; +foreach ($urls_to_download as $url) { + $filename = basename($url); + echo 'will be downloading ' . $url . ' and saving as "' . $filename . '"' . "\n"; + + $download_status[$i] = [ + 'position' => $i, + 'complete' => false, + 'filename' => $filename, + 'size' => 0, + 'downloaded' => 0, + ]; + + $curl = $multi_curl->addDownload($url, $filename); + + // Increase timeout to avoid error: + // "Operation timed out after 30000 milliseconds with ... out of ... bytes + // received". + $curl->setTimeout(500); + + // Slow the download. Comment the following lines to remove the download + // throttling. + $curl->setOpt(CURLOPT_MAX_RECV_SPEED_LARGE, 500000); + $curl->setOpt(CURLOPT_BUFFERSIZE, 1024); + + $curl->progress(function ( + $client, + $download_size, + $downloaded, + $upload_size, + $uploaded + ) use ( + $i, + &$download_status, + &$last_updated_time + ) { + if ($download_size === 0) { + return 0; + } + + $download_completed = $downloaded === $download_size; + $current_time = time(); + + // Avoid sending an update if we're within the same second and the + // download has not yet completed. + if (!$download_completed && $current_time === $last_updated_time) { + return 0; + } + + $last_updated_time = $current_time; + + // Update progress of this download. + $download_status[$i]['complete'] = $download_completed; + $download_status[$i]['size'] = $download_size; + $download_status[$i]['downloaded'] = $downloaded; + + // Generate response including completion status of all downloads and + // status of each individual download. + $response = [ + 'status' => '', + 'downloads' => [], + ]; + $all_downloads_completed = true; + foreach ($download_status as $key => $value) { + $response['downloads'][] = $value; + $all_downloads_completed = $all_downloads_completed && $value['complete']; + } + $response['status'] = $all_downloads_completed ? 'done' : 'active'; + $json_response = json_encode($response); + + $out = fopen('/tmp/myfifo', 'w'); + + // TODO: Catch broken pipe: + // PHP Notice: fwrite(): Write of 52 bytes failed with errno=32 + // Broken pipe in ./multi_curl_progress_advanced.php on line [...] + fwrite($out, $json_response . "\n"); + + fclose($out); + + // Comment the following line to hide the download progress updates + // being sent to the named pipe. + echo $json_response . "\n"; + + return 0; + }); + + $i += 1; +} + +echo 'starting download' . "\n"; +$multi_curl->start(); + +echo 'all done' . "\n"; diff --git a/examples/multi_curl_progress_advanced_watch_curses.py b/examples/multi_curl_progress_advanced_watch_curses.py new file mode 100644 index 0000000000..0f50424446 --- /dev/null +++ b/examples/multi_curl_progress_advanced_watch_curses.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 + +# Reads a named pipe file and displays progress bars using curses. +# +# See multi_curl_progress_advanced.php +# +# $ ipython multi_curl_progress_advanced_watch_curses.py +# 56% [=====================> ] +# 23% [========> ] +# 15% [=====> ] + +import curses +import json +import os + +FIFO = "/tmp/myfifo" + + +def main(stdscr): + # Create named pipe file if it doesn't exist. + if not os.path.exists(FIFO): + os.mkfifo(FIFO) + + curses.noecho() + curses.cbreak() + + try: + while True: + display_progress_bars(stdscr) + except KeyboardInterrupt: + # Handle Control-C pressed. + pass + finally: + curses.echo() + curses.nocbreak() + curses.endwin() + + # Return exit code 0. + return 0 + + +def display_progress_bars(stdscr): + display_notice = True + + while True: + if display_notice: + display_notice = False + stdscr.clear() + stdscr.addstr(0, 0, "waiting for input") + stdscr.refresh() + + # Read named pipe file. + with open(FIFO) as f: + for line in f: + response = json.loads(line) + + # Update progress for each of the files being downloaded. + for entry in response.get("downloads", []): + # Display a progress bar: xxx% [=======> ] + progress_size = 40 + try: + fraction_downloaded = entry["downloaded"] / entry["size"] + except ZeroDivisionError: + fraction_downloaded = 0 + dots = round(fraction_downloaded * progress_size) + task_progress = "%3.0f%% [" % (fraction_downloaded * 100) + + i = 0 + while i < dots - 1: + task_progress += "=" + i += 1 + + task_progress += ">" + + while i < progress_size - 1: + task_progress += " " + i += 1 + + task_progress += "]" + + stdscr.addstr(entry["position"], 0, task_progress) + + # Refresh display of the progress bars. + stdscr.refresh() + + # Exit only after progress bars have been updated to end with + # each displaying 100%. + if response.get("status", "") == "done": + return + + +if __name__ == "__main__": + # Avoid getting the terminal in an unmanagable state by using the curses + # wrapper. The wrapper restores the terminal to its previous state even when + # there is an uncaught exception. + curses.wrapper(main) diff --git a/examples/multi_curl_progress_advanced_watch_tqdm.py b/examples/multi_curl_progress_advanced_watch_tqdm.py new file mode 100644 index 0000000000..f82158997c --- /dev/null +++ b/examples/multi_curl_progress_advanced_watch_tqdm.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 + +# Reads a named pipe file and displays progress bars using tqdm. +# +# See multi_curl_progress_advanced.php +# +# $ ipython multi_curl_progress_advanced_watch_tqdm.py +# php_manual_en.html.gz: 56%|████████████████████████ | 2.99M/5.34M [00:02<00:02, 1.14MB/s] +# php_manual_en.tar.gz: 23%|██████████▎ | 2.37M/10.4M [00:02<00:08, 901kB/s] +# php_manual_en.chm: 15%|███████▏ | 2.06M/13.7M [00:02<00:14, 783kB/s] + +import json +import os +import sys + +from tqdm import tqdm + +FIFO = "/tmp/myfifo" + + +def main(): + # Create named pipe file if it doesn't exist. + if not os.path.exists(FIFO): + os.mkfifo(FIFO) + + try: + while True: + display_progress_bars() + except KeyboardInterrupt: + # Handle Control-C pressed. + pass + + # Return exit code 0. + return 0 + + +def display_progress_bars(): + display_notice = True + progress_bars = {} + + while True: + if display_notice: + display_notice = False + print("waiting for input") + + # Read named pipe file. + with open(FIFO) as f: + for line in f: + response = json.loads(line) + + # Update progress for each of the files being downloaded. + for entry in response.get("downloads", []): + # Get or create progress bar. + progress_bar = progress_bars.get(entry["position"]) + if progress_bar is None: + progress_bar = tqdm( + desc=entry["filename"], + total=entry["size"], + position=entry["position"], + unit_scale=True, + unit_divisor=1000, + unit="B", + ) + progress_bars[entry["position"]] = progress_bar + + # Update download progress. + progress_bar.n = entry["downloaded"] + + # Update progress bar's total size. Initial size may have + # been sent as 0 until content-length was determined. + progress_bar.total = entry["size"] + + # Refresh display of the progress bar. + progress_bar.refresh() + + # Exit only after progress bars have been updated to end with + # each displaying 100%. + if response.get("status", "") == "done": + return + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/multi_curl_proxies.php b/examples/multi_curl_proxies.php new file mode 100644 index 0000000000..1474c92c93 --- /dev/null +++ b/examples/multi_curl_proxies.php @@ -0,0 +1,25 @@ +setProxies([ + 'someproxy.com:9999', + 'someproxy.com:80', + 'someproxy.com:443', + 'someproxy.com:1080', + 'someproxy.com:3128', + 'someproxy.com:8080', +]); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->complete(function ($instance) { + echo + 'curl id ' . $instance->id . ' used proxy ' . + $instance->getOpt(CURLOPT_PROXY) . ' and ' . + 'ip is ' . $instance->response->origin . "\n"; +}); +$multi_curl->start(); diff --git a/examples/multi_curl_proxy.php b/examples/multi_curl_proxy.php new file mode 100644 index 0000000000..867cf57194 --- /dev/null +++ b/examples/multi_curl_proxy.php @@ -0,0 +1,14 @@ +setProxy('someproxy.com', '9999', 'username', 'password'); +$multi_curl->setProxyTunnel(); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->complete(function ($instance) { + var_dump($instance->response); +}); +$multi_curl->start(); diff --git a/examples/multi_curl_proxy_socks5.php b/examples/multi_curl_proxy_socks5.php new file mode 100644 index 0000000000..a97df3404e --- /dev/null +++ b/examples/multi_curl_proxy_socks5.php @@ -0,0 +1,17 @@ +setProxy('127.0.0.1:8080'); +$multi_curl->setProxyType(CURLPROXY_SOCKS5); +$multi_curl->addGet('https://httpbin.org/ip'); +$multi_curl->complete(function ($instance) { + var_dump($instance->response); +}); +$multi_curl->start(); diff --git a/examples/multi_curl_put.php b/examples/multi_curl_put.php index 6543396113..e3b99f53e7 100644 --- a/examples/multi_curl_put.php +++ b/examples/multi_curl_put.php @@ -1,20 +1,20 @@ addPut('https://httpbin.org/put', array( +$multi_curl->addPut('https://httpbin.org/put', [ 'id' => '123', 'subject' => 'hello', 'body' => 'hello', -)); -$multi_curl->addPut('https://httpbin.org/put', array( +]); +$multi_curl->addPut('https://httpbin.org/put', [ 'id' => '456', 'subject' => 'hello', 'body' => 'hello', -)); +]); $multi_curl->start(); diff --git a/examples/multi_curl_set_custom_instance_tag.php b/examples/multi_curl_set_custom_instance_tag.php new file mode 100644 index 0000000000..8fa0c89808 --- /dev/null +++ b/examples/multi_curl_set_custom_instance_tag.php @@ -0,0 +1,48 @@ + 'https://httpbin.org/status/401', + 'tag4' => 'https://httpbin.org/status/200', + 'tag5' => 'https://httpbin.org/status/503', +]; + +$ids_to_tags = []; + +$multi_curl = new MultiCurl(); + +$multi_curl->success(function ($instance) use (&$ids_to_tags) { + echo + 'instance id ' . $instance->id . ' request with tag ' . + $ids_to_tags[$instance->id] . ' was successful.' . "\n"; +}); +$multi_curl->error(function ($instance) use (&$ids_to_tags) { + echo + 'instance id ' . $instance->id . ' request with tag ' . + $ids_to_tags[$instance->id] . ' was unsuccessful.' . "\n"; +}); +$multi_curl->complete(function ($instance) use (&$ids_to_tags) { + echo + 'instance id ' . $instance->id . ' request with tag ' . + $ids_to_tags[$instance->id] . ' completed.' . "\n"; +}); + +foreach ($tags_to_urls as $tag => $url) { + $curl = $multi_curl->addGet($url, ['myTag' => $tag]); + $ids_to_tags[$curl->id] = $tag; +} + +$multi_curl->start(); + +/* +$ php multi_curl_set_custom_instance_tag.php +instance id 0 request with tag tag3 was unsuccessful. +instance id 0 request with tag tag3 completed. +instance id 2 request with tag tag5 was unsuccessful. +instance id 2 request with tag tag5 completed. +instance id 1 request with tag tag4 was unsuccessful. +instance id 1 request with tag tag4 completed. +*/ diff --git a/examples/multi_curl_stop.php b/examples/multi_curl_stop.php new file mode 100644 index 0000000000..ac6e4e526e --- /dev/null +++ b/examples/multi_curl_stop.php @@ -0,0 +1,39 @@ +beforeSend(function ($instance) { + echo 'about to make request ' . $instance->id . ': "' . $instance->url . '".' . "\n"; +}); + +$multi_curl->success(function ($instance) use ($multi_curl) { + echo 'call to "' . $instance->url . '" was successful.' . "\n"; + + // Stop pending requests and attempt to stop active requests after the first + // successful request. + $multi_curl->stop(); +}); + +$multi_curl->error(function ($instance) { + echo 'call to "' . $instance->url . '" was unsuccessful.' . "\n"; +}); + +// Count the number of completed requests. +$request_count = 0; +$multi_curl->complete(function ($instance) use (&$request_count) { + echo 'call to "' . $instance->url . '" completed.' . "\n"; + $request_count += 1; +}); + +$multi_curl->addGet('https://httpbin.org/delay/4'); +$multi_curl->addGet('https://httpbin.org/delay/1'); +$multi_curl->addGet('https://httpbin.org/delay/3'); +$multi_curl->addGet('https://httpbin.org/delay/2'); + +$multi_curl->start(); + +assert($request_count === 1); diff --git a/examples/multi_curl_track_success_urls.php b/examples/multi_curl_track_success_urls.php new file mode 100644 index 0000000000..7060c4330d --- /dev/null +++ b/examples/multi_curl_track_success_urls.php @@ -0,0 +1,126 @@ + [], + 'successful' => [], + 'errors' => [], + 'completed' => [], + 'not_completed' => [], +]; + +$multi_curl = new MultiCurl(); +$multi_curl->setConcurrency(2); + +foreach ($urls as $url) { + // Queue requests. + $request = $multi_curl->addGet($url); + + // Track all requests queued. + $request_stats['all'][] = $request->url; +} + +// Track successful requests. +$multi_curl->success(function ($instance) use (&$request_stats, $multi_curl) { + $request_stats['successful'][] = $instance->url; + + // Optionally, stop additional requests based on some condition (e.g. stop + // after a number of successful requests). + if (count($request_stats['successful']) >= 3) { + $multi_curl->stop(); + } +}); + +// Track requests that errored. +$multi_curl->error(function ($instance) use (&$request_stats) { + $request_stats['errors'][] = $instance->url; +}); + +// Track requests that completed. +$multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats['completed'][] = $instance->url; +}); + +$multi_curl->start(); + +// Determine urls not completed. +$request_stats['not_completed'] = array_diff($request_stats['all'], $request_stats['completed']); + +// Display results. +var_dump($request_stats); + +// $ php multi_curl_track_success_urls.php +// array(5) { +// ["all"]=> +// array(8) { +// [0]=> +// string(30) "https://httpbin.org/status/503" +// [1]=> +// string(30) "https://httpbin.org/status/200" +// [2]=> +// string(30) "https://httpbin.org/status/401" +// [3]=> +// string(27) "https://httpbin.org/delay/3" +// [4]=> +// string(30) "https://httpbin.org/status/201" +// [5]=> +// string(27) "https://httpbin.org/delay/1" +// [6]=> +// string(30) "https://httpbin.org/status/500" +// [7]=> +// string(30) "https://httpbin.org/status/504" +// } +// ["successful"]=> +// array(3) { +// [0]=> +// string(30) "https://httpbin.org/status/200" +// [1]=> +// string(27) "https://httpbin.org/delay/3" +// [2]=> +// string(30) "https://httpbin.org/status/201" +// } +// ["errors"]=> +// array(2) { +// [0]=> +// string(30) "https://httpbin.org/status/503" +// [1]=> +// string(30) "https://httpbin.org/status/401" +// } +// ["completed"]=> +// array(5) { +// [0]=> +// string(30) "https://httpbin.org/status/200" +// [1]=> +// string(30) "https://httpbin.org/status/503" +// [2]=> +// string(30) "https://httpbin.org/status/401" +// [3]=> +// string(27) "https://httpbin.org/delay/3" +// [4]=> +// string(30) "https://httpbin.org/status/201" +// } +// ["not_completed"]=> +// array(3) { +// [5]=> +// string(27) "https://httpbin.org/delay/1" +// [6]=> +// string(30) "https://httpbin.org/status/500" +// [7]=> +// string(30) "https://httpbin.org/status/504" +// } +// } diff --git a/examples/multi_curl_upload_file.php b/examples/multi_curl_upload_file.php new file mode 100644 index 0000000000..568cdb0ef2 --- /dev/null +++ b/examples/multi_curl_upload_file.php @@ -0,0 +1,26 @@ +, +// then try uncommenting the following line: +// $multi_curl->setHeader('Content-Type', 'multipart/form-data'); + +$multi_curl->addPost('https://httpbin.org/post', [ + 'image' => new CURLFile('the-lorax.jpg'), +]); + +$multi_curl->addPost('https://httpbin.org/post', [ + 'image' => new CURLFile('swomee-swans.jpg'), +]); + +$multi_curl->addPost('https://httpbin.org/post', [ + 'image' => new CURLFile('truffula-trees.jpg'), +]); + +$multi_curl->start(); diff --git a/examples/options.php b/examples/options.php new file mode 100644 index 0000000000..e2bb7a8ede --- /dev/null +++ b/examples/options.php @@ -0,0 +1,19 @@ +options('http://127.0.0.1:8000/', [ + 'foo' => 'bar', +]); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/patch.php b/examples/patch.php new file mode 100644 index 0000000000..f4c86d1474 --- /dev/null +++ b/examples/patch.php @@ -0,0 +1,21 @@ +patch('https://httpbin.org/patch', [ + 'a' => '1', + 'b' => '2', + 'c' => '3', +]); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/post.php b/examples/post.php index 9ea4adc529..5545b73ee5 100644 --- a/examples/post.php +++ b/examples/post.php @@ -1,19 +1,21 @@ post('https://httpbin.org/post', [ 'id' => '1', 'content' => 'Hello world!', 'date' => date('Y-m-d H:i:s'), -); +]); -$curl = new Curl(); -$curl->post('https://httpbin.org/post', $data); -var_dump($curl->response->form); +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Data server received via POST:' . "\n"; + var_dump($curl->response->form); +} diff --git a/examples/post_json.php b/examples/post_json.php index 9675a27adc..a0533c9dce 100644 --- a/examples/post_json.php +++ b/examples/post_json.php @@ -1,18 +1,20 @@ '1', 'content' => 'Hello world!', 'date' => date('Y-m-d H:i:s'), -)); +]; $curl = new Curl(); $curl->setHeader('Content-Type', 'application/json'); diff --git a/examples/post_json_array_response.php b/examples/post_json_array_response.php new file mode 100644 index 0000000000..7dc1870922 --- /dev/null +++ b/examples/post_json_array_response.php @@ -0,0 +1,23 @@ + '1', + 'content' => 'Hello world!', + 'date' => date('Y-m-d H:i:s'), +]; + +$curl = new Curl(); +$curl->setDefaultJsonDecoder($assoc = true); +$curl->setHeader('Content-Type', 'application/json'); +$curl->post('https://httpbin.org/post', $data); +var_dump($curl->response); diff --git a/examples/post_json_manual_encoding.php b/examples/post_json_manual_encoding.php new file mode 100644 index 0000000000..6dba3945fd --- /dev/null +++ b/examples/post_json_manual_encoding.php @@ -0,0 +1,22 @@ + '1', + 'content' => 'Hello world!', + 'date' => date('Y-m-d H:i:s'), +], JSON_UNESCAPED_UNICODE); + +$curl = new Curl(); +$curl->setHeader('Content-Type', 'application/json'); +$curl->post('https://httpbin.org/post', $data); +var_dump($curl->response->json); diff --git a/examples/post_multiple_values_same_key_with_indexes_explicit.php b/examples/post_multiple_values_same_key_with_indexes_explicit.php new file mode 100644 index 0000000000..d92bd71cc4 --- /dev/null +++ b/examples/post_multiple_values_same_key_with_indexes_explicit.php @@ -0,0 +1,13 @@ +post('https://httpbin.org/post', [ + 'foo[0]' => 'bar', + 'foo[1]' => 'baz', +]); diff --git a/examples/post_multiple_values_same_key_with_indexes_implicit.php b/examples/post_multiple_values_same_key_with_indexes_implicit.php new file mode 100644 index 0000000000..6ccabb5f5d --- /dev/null +++ b/examples/post_multiple_values_same_key_with_indexes_implicit.php @@ -0,0 +1,15 @@ +post('https://httpbin.org/post', [ + 'foo' => [ + 'bar', + 'baz', + ], +]); diff --git a/examples/post_multiple_values_same_key_without_indexes.php b/examples/post_multiple_values_same_key_without_indexes.php new file mode 100644 index 0000000000..5e45ab58df --- /dev/null +++ b/examples/post_multiple_values_same_key_without_indexes.php @@ -0,0 +1,39 @@ + $value) { + $key = rawurlencode($key); + if (is_array($value)) { + foreach ($value as $v) { + $v = rawurlencode($v); + $array[] = $key . '=' . $v; + } + } else { + $value = rawurlencode($value); + $array[] = $key . '=' . $value; + } + } + return implode('&', $array); +} + +$curl = new Curl(); +$curl->post('https://httpbin.org/post', http_build_query_without_indexes([ + 'foo' => [ + 'bar', + 'baz', + ], +])); + +// @codingStandardsIgnoreFile diff --git a/examples/post_redirect_get.php b/examples/post_redirect_get.php new file mode 100644 index 0000000000..7100dedd5b --- /dev/null +++ b/examples/post_redirect_get.php @@ -0,0 +1,28 @@ +setOpt(CURLOPT_FOLLOWLOCATION, true); +$curl->post('https://www.example.com/login/', [ + 'username' => 'myusername', + 'password' => 'mypassword', +]); + +// POST data and follow 303 redirections by POSTing data again. Please note +// that 303 redirections should not be handled this way. +// https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4 +$curl = new Curl(); +$curl->setOpt(CURLOPT_FOLLOWLOCATION, true); +$curl->post('https://www.example.com/login/', [ + 'username' => 'myusername', + 'password' => 'mypassword', +], false); + +// A POST request performs a post-redirect-get by default. Other request +// methods force an option which conflicts with the post-redirect-get behavior. +// Due to technical limitations of PHP engines <5.5.11, it is not possible to +// reset this option. It is therefore impossible to perform a post-redirect-get +// request using a php-curl-class Curl object that has already been used to +// perform other types of requests. Either use a new php-curl-class Curl object +// or upgrade your PHP engine. diff --git a/examples/post_xml.php b/examples/post_xml.php new file mode 100644 index 0000000000..a01129c209 --- /dev/null +++ b/examples/post_xml.php @@ -0,0 +1,30 @@ + + + + + 1 + 33ee7e1eb504b6619c1b445ca1442c21 + <![CDATA[The Title]]> + + + + + 2 + b5c0b187fe309af0f4d35982fd961d7e + <![CDATA[Another Title]]> + + + + +'; + +$curl = new Curl(); +$curl->setHeader('Content-Type', 'text/xml'); +$curl->post('https://httpbin.org/post', $data); +var_dump($curl->response); diff --git a/examples/progress.php b/examples/progress.php deleted file mode 100644 index 228c88a421..0000000000 --- a/examples/progress.php +++ /dev/null @@ -1,15 +0,0 @@ -progress(function($client, $download_size, $downloaded, $upload_size, $uploaded) { - if ($download_size === 0) { - return; - } - - $percent = floor( $downloaded * 100 / $download_size ); - echo ' ' . $percent . '%' . "\r"; -}); -$curl->download('https://php.net/distributions/manual/php_manual_en.html.gz', '/tmp/php_manual_en.html.gz'); diff --git a/examples/progress_advanced.php b/examples/progress_advanced.php deleted file mode 100644 index b7541d9c1e..0000000000 --- a/examples/progress_advanced.php +++ /dev/null @@ -1,30 +0,0 @@ -progress(function($client, $download_size, $downloaded, $upload_size, $uploaded) { - if ($download_size === 0) { - return; - } - - // Display a progress bar: xxx% [=======> ] - $progress_size = 40; - $fraction_downloaded = $downloaded / $download_size; - $dots = round($fraction_downloaded * $progress_size); - printf('%3.0f%% [', $fraction_downloaded * 100 ); - $i = 0; - for ( ; $i < $dots - 1; $i++) { - echo '='; - } - echo '>'; - for ( ; $i < $progress_size - 1; $i++) { - echo ' '; - } - echo ']' . "\r"; -}); -$curl->complete(function($instance) { - echo "\n" . 'download complete' . "\n"; -}); -$curl->download('https://secure.php.net/distributions/manual/php_manual_en.html.gz', '/tmp/php_manual_en.html.gz'); diff --git a/examples/proxy.php b/examples/proxy.php new file mode 100644 index 0000000000..84a890c80b --- /dev/null +++ b/examples/proxy.php @@ -0,0 +1,11 @@ +setProxy('someproxy.com', '9999', 'username', 'password'); +$curl->setProxyTunnel(); +$curl->get('https://httpbin.org/get'); +var_dump($curl->response); diff --git a/examples/proxy_socks5.php b/examples/proxy_socks5.php new file mode 100644 index 0000000000..e655fb5d50 --- /dev/null +++ b/examples/proxy_socks5.php @@ -0,0 +1,14 @@ +setProxy('127.0.0.1:8080'); +$curl->setProxyType(CURLPROXY_SOCKS5); +$curl->get('https://httpbin.org/ip'); +var_dump($curl->response); diff --git a/examples/put.php b/examples/put.php index 338873b444..1e47b6d52c 100644 --- a/examples/put.php +++ b/examples/put.php @@ -1,15 +1,21 @@ put('https://httpbin.org/put', array( - 'id' => 1, +$curl->put('https://httpbin.org/put', [ + 'id' => '1', 'first_name' => 'Zach', 'last_name' => 'Borboa', -)); +]); -echo 'Data server received via PUT:' . "\n"; -var_dump($curl->response->form); +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Data server received via PUT:' . "\n"; + var_dump($curl->response->form); +} diff --git a/examples/put_large_file_chunked.php b/examples/put_large_file_chunked.php new file mode 100644 index 0000000000..020b81877f --- /dev/null +++ b/examples/put_large_file_chunked.php @@ -0,0 +1,34 @@ +setHeader('Transfer-Encoding', 'chunked'); +$curl->setOpt(CURLOPT_UPLOAD, true); +$curl->setOpt(CURLOPT_INFILE, $fp); +$curl->setOpt(CURLOPT_INFILESIZE, filesize($filename)); +$curl->setOpt(CURLOPT_READFUNCTION, 'read_file'); +$curl->put('http://127.0.0.1:8000/'); + +fclose($fp); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Success' . "\n"; +} + +// @codingStandardsIgnoreFile diff --git a/examples/receive_large_file_chunked.php b/examples/receive_large_file_chunked.php new file mode 100644 index 0000000000..df1c0452ab --- /dev/null +++ b/examples/receive_large_file_chunked.php @@ -0,0 +1,23 @@ +get('http://www.reddit.com/r/pics/top/.json', $data); +$curl->get('https://www.reddit.com/r/pics/top/.json', $data); echo '
    '; diff --git a/examples/search.php b/examples/search.php new file mode 100644 index 0000000000..99a8259553 --- /dev/null +++ b/examples/search.php @@ -0,0 +1,21 @@ +search('http://127.0.0.1:8000/', [ + 'a' => '1', + 'b' => '2', + 'c' => '3', +]); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/set_cookie.php b/examples/set_cookie.php new file mode 100644 index 0000000000..c3a09ef571 --- /dev/null +++ b/examples/set_cookie.php @@ -0,0 +1,10 @@ +setCookie('foo', 'bar'); +$curl->get('https://httpbin.org/cookies'); +var_dump($curl->response->cookies->foo === 'bar'); diff --git a/examples/set_url_1.php b/examples/set_url_1.php index a45fc2b81a..62bf745cf1 100644 --- a/examples/set_url_1.php +++ b/examples/set_url_1.php @@ -1,7 +1,8 @@ get(array( + $curl->get([ 'q' => $q, 'page' => $i, - )); + ]); } diff --git a/examples/set_url_2.php b/examples/set_url_2.php index ca1b129825..fc8b28dc48 100644 --- a/examples/set_url_2.php +++ b/examples/set_url_2.php @@ -1,19 +1,20 @@ setURL('https://www.example.com/search'); +$curl->setUrl('https://www.example.com/search'); for ($i = 1; $i <= $pages; $i++) { // https://www.example.com/search?q=coffee&page=N - $curl->get(array( + $curl->get([ 'q' => $q, 'page' => $i, - )); + ]); } diff --git a/examples/twitter_post_tweet.php b/examples/twitter_post_tweet.php index 9118dabc5c..8e0057f65e 100644 --- a/examples/twitter_post_tweet.php +++ b/examples/twitter_post_tweet.php @@ -1,16 +1,17 @@ API_KEY, 'oauth_nonce' => md5(microtime() . mt_rand()), 'oauth_signature_method' => 'HMAC-SHA1', @@ -18,15 +19,15 @@ 'oauth_token' => OAUTH_ACCESS_TOKEN, 'oauth_version' => '1.0', 'status' => $status, -); +]; $url = 'https://api.twitter.com/1.1/statuses/update.json'; -$request = implode('&', array( +$request = implode('&', [ 'POST', rawurlencode($url), rawurlencode(http_build_query($oauth_data, '', '&', PHP_QUERY_RFC3986)), -)); -$key = implode('&', array(API_SECRET, OAUTH_TOKEN_SECRET)); +]); +$key = implode('&', [API_SECRET, OAUTH_TOKEN_SECRET]); $oauth_data['oauth_signature'] = base64_encode(hash_hmac('sha1', $request, $key, true)); $data = http_build_query($oauth_data, '', '&'); diff --git a/examples/twitter_trending_topics.php b/examples/twitter_trending_topics.php index e60b14e325..2868b30b1d 100644 --- a/examples/twitter_trending_topics.php +++ b/examples/twitter_trending_topics.php @@ -1,16 +1,17 @@ $woeid, 'oauth_consumer_key' => API_KEY, 'oauth_nonce' => md5(microtime() . mt_rand()), @@ -18,31 +19,31 @@ 'oauth_timestamp' => time(), 'oauth_token' => OAUTH_ACCESS_TOKEN, 'oauth_version' => '1.0', -); +]; $request_values = $oauth_data; ksort($request_values); $url = 'https://api.twitter.com/1.1/trends/place.json'; -$request = implode('&', array( +$request = implode('&', [ 'GET', rawurlencode($url), rawurlencode(http_build_query($request_values, '', '&', PHP_QUERY_RFC3986)), -)); -$key = implode('&', array(rawurlencode(API_SECRET), rawurlencode(OAUTH_TOKEN_SECRET))); +]); +$key = implode('&', [rawurlencode(API_SECRET), rawurlencode(OAUTH_TOKEN_SECRET)]); $oauth_data['oauth_signature'] = base64_encode(hash_hmac('sha1', $request, $key, true)); -$authorization = array(); +$authorization = []; foreach ($oauth_data as $key => $value) { $authorization[] = $key . '="' . rawurlencode($value) . '"'; } $authorization = 'Authorization: OAuth ' . implode(', ', $authorization); $curl = new Curl(); -$curl->setOpt(CURLOPT_HTTPHEADER, array($authorization)); -$curl->get($url, array( +$curl->setOpt(CURLOPT_HTTPHEADER, [$authorization]); +$curl->get($url, [ 'id' => $woeid, -)); +]); echo 'Current trends:' . "\n"; foreach ($curl->response['0']->trends as $trend) { diff --git a/examples/upload_file.php b/examples/upload_file.php index 86218895ca..d09c46c562 100644 --- a/examples/upload_file.php +++ b/examples/upload_file.php @@ -1,14 +1,21 @@ post('https://httpbin.org/post', array( + +// HINT: If API documentation refers to using something like curl -F "myimage=image.png", +// curl --form "myimage=image.png", or the html form is similar to
    , +// then try uncommenting the following line: +// $curl->setHeader('Content-Type', 'multipart/form-data'); + +$curl->post('https://httpbin.org/post', [ 'myfile' => $myfile, -)); +]); if ($curl->error) { echo 'Error: ' . $curl->errorMessage . "\n"; diff --git a/examples/use_custom_xml_decoder.php b/examples/use_custom_xml_decoder.php new file mode 100644 index 0000000000..d388eb24a5 --- /dev/null +++ b/examples/use_custom_xml_decoder.php @@ -0,0 +1,24 @@ +setXmlDecoder($my_xml_decoder); +$curl->get('https://httpbin.org/xml'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; +} else { + echo 'Response:' . "\n"; + var_dump($curl->response); +} diff --git a/examples/vendor b/examples/vendor deleted file mode 120000 index 9c39cc9f86..0000000000 --- a/examples/vendor +++ /dev/null @@ -1 +0,0 @@ -../vendor \ No newline at end of file diff --git a/examples/youtube_list_playlist_videos.php b/examples/youtube_list_playlist_videos.php index eb883b0a3e..a98320a723 100644 --- a/examples/youtube_list_playlist_videos.php +++ b/examples/youtube_list_playlist_videos.php @@ -1,19 +1,20 @@ get('https://www.googleapis.com/youtube/v3/playlistItems', array( +$curl->get('https://www.googleapis.com/youtube/v3/playlistItems', [ 'key' => YOUTUBE_API_KEY, 'maxResults' => '50', 'part' => 'snippet', 'playlistId' => $playlistId, -)); +]); echo 'Songs in this playlist:' . "\n"; diff --git a/examples/youtube_video_count.php b/examples/youtube_video_count.php index be2a2d247b..484f886404 100644 --- a/examples/youtube_video_count.php +++ b/examples/youtube_video_count.php @@ -1,16 +1,17 @@ $message, + 'old_version' => $current_version, + 'new_version' => $new_version, +], JSON_PRETTY_PRINT) . "\n"; diff --git a/scripts/bump_minor_version.php b/scripts/bump_minor_version.php index 4fe189e26e..0de25185e0 100755 --- a/scripts/bump_minor_version.php +++ b/scripts/bump_minor_version.php @@ -1,20 +1,31 @@ -#!/usr/bin/php +#!/usr/bin/env php $message, + 'old_version' => $current_version, + 'new_version' => $new_version, +], JSON_PRETTY_PRINT) . "\n"; diff --git a/scripts/bump_patch_version.php b/scripts/bump_patch_version.php index 7e88bfcb44..0b6804ae3a 100755 --- a/scripts/bump_patch_version.php +++ b/scripts/bump_patch_version.php @@ -1,20 +1,31 @@ -#!/usr/bin/php +#!/usr/bin/env php $message, + 'old_version' => $current_version, + 'new_version' => $new_version, +], JSON_PRETTY_PRINT) . "\n"; diff --git a/scripts/make_release.py b/scripts/make_release.py new file mode 100644 index 0000000000..666b483007 --- /dev/null +++ b/scripts/make_release.py @@ -0,0 +1,255 @@ +import json +import os +import pprint +import subprocess +from copy import copy +from datetime import datetime +from datetime import timezone +from pathlib import Path + +from git.repo import Repo +from github import Github + +# The owner and repository name. For example, octocat/Hello-World. +GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY", "") + +GITHUB_TOKEN = os.getenv("GITHUB_TOKEN") +GITHUB_REF_NAME = os.getenv("GITHUB_REF_NAME") + +CURRENT_FILE = Path(__file__) +ROOT = CURRENT_FILE.parents[1] +CHANGELOG_PATH = ROOT / "CHANGELOG.md" +LIBRARY_FILE_PATH = ROOT / "src/Curl/Curl.php" + +# TODO: Adjust number of recent pull requests to include likely number of +# pull requests since the last release. +RECENT_PULL_REQUEST_LIMIT = 50 + + +def main(): + # Find most recent tag and timestamp. + # git for-each-ref --format="%(refname:short) | %(creatordate)" "refs/tags/*" + local_repo = Repo(ROOT) + + # Sort the tags by version. + # git tag --list | sort --reverse --version-sort + tags = sorted( + local_repo.tags, + key=lambda tag: list(map(int, tag.name.split("."))), + reverse=True, + ) + + most_recent_tag = tags[0] + print("most_recent_tag: {}".format(most_recent_tag)) + most_recent_tag_datetime = most_recent_tag.commit.committed_datetime + print("most_recent_tag_datetime: {}".format(most_recent_tag_datetime)) + + g = Github(login_or_token=GITHUB_TOKEN) + + # Find merged pull requests since the most recent tag. + github_repo = g.get_repo(GITHUB_REPOSITORY) + recent_pulls = github_repo.get_pulls( + state="closed", + sort="updated", + direction="desc", + )[:RECENT_PULL_REQUEST_LIMIT] + + pull_request_changes = [] + + # Group pull requests by semantic version change type. + pull_request_by_type = { + "major": [], + "minor": [], + "patch": [], + "cleanup": [], + "unspecified": [], + } + + # Track if any pull request is missing a semantic version change type. + pulls_missing_semver_label = [] + + for pull in recent_pulls: + # print('-' * 10) + + if not pull.merged: + # print('skipping since not merged: {}'.format(pull.title)) + # print(pull.html_url) + continue + + # Make merged_at timestamp offset-aware. Without this, the following + # error will appear: + # TypeError: can't compare offset-naive and offset-aware datetimes + pull_merged_at = copy(pull.merged_at).replace(tzinfo=timezone.utc) + + if pull_merged_at < most_recent_tag_datetime: + # print('skipping since merged prior to last release: {}'.format(pull.title)) + # print(pull.html_url) + continue + + pull_labels = {label.name for label in pull.labels} + if "major-incompatible-changes" in pull_labels: + group_name = "major" + elif "minor-backwards-compatible-added-functionality" in pull_labels: + group_name = "minor" + elif "patch-backwards-compatible-bug-fixes" in pull_labels: + group_name = "patch" + elif "cleanup-no-release-required" in pull_labels: + group_name = "cleanup" + else: + group_name = "unspecified" + pulls_missing_semver_label.append(pull) + pull_request_by_type[group_name].append(pull) + + # pprint.pprint(pull.title) + # pprint.pprint('most recent: {}'.format(most_recent_tag_datetime)) + # pprint.pprint('merged at: {}'.format(pull.merged_at)) + # print(pull.html_url) + + if group_name in ["major", "minor", "patch"]: + pull_request_changes.append( + "- {} ([#{}]({}))".format(pull.title, pull.number, pull.html_url) + ) + + # print('-' * 10) + + # pprint.pprint(pull_request_changes) + + # Raise error if any pull request is missing a semantic version change type. + # Do this check before checking for any pull request changes as all the pull + # requests changes might be missing semver labels. + if pulls_missing_semver_label: + error_message = ( + "Merged pull request(s) found without semantic version label:\n" + "{}".format( + "\n".join( + " {}".format(pull.html_url) for pull in pulls_missing_semver_label + ) + ) + ) + raise Exception(error_message) + + if not pull_request_changes: + print("No merged pull requests since the most recent tag release were found") + return + + # pprint.pprint(pull_request_by_type) + if pull_request_by_type.get("major"): + highest_semantic_version = "major" + php_file_path = "scripts/bump_major_version.php" + elif pull_request_by_type.get("minor"): + highest_semantic_version = "minor" + php_file_path = "scripts/bump_minor_version.php" + elif pull_request_by_type.get("patch"): + highest_semantic_version = "patch" + php_file_path = "scripts/bump_patch_version.php" + else: + highest_semantic_version = None + php_file_path = "" + print("highest_semantic_version: {}".format(highest_semantic_version)) + + # Bump version and get next semantic version. + command = ["php", php_file_path] + print("running command: {}".format(command)) + proc = subprocess.Popen( + command, shell=False, stdout=subprocess.PIPE, stdin=subprocess.PIPE + ) + stdout, stderr = proc.communicate() + print("stdout: {}".format(stdout)) + print("stderr: {}".format(stderr)) + result = json.loads(stdout) + pprint.pprint(result) + + release_version = result["new_version"] + today = datetime.today() + print("today: {} (tzinfo={})".format(today, today.tzinfo)) + today = today.replace(tzinfo=timezone.utc) + print("today: {} (tzinfo={})".format(today, today.tzinfo)) + release_date = today.strftime("%Y-%m-%d") + print("release_date: {}".format(release_date)) + release_title = "{} - {}".format(release_version, release_date) + print("release_title: {}".format(release_title)) + + release_content = "".join( + [ + "## {}\n", + "\n", + "{}", + ] + ).format(release_title, "\n".join(pull_request_changes)) + + old_content = CHANGELOG_PATH.read_text() + new_content = old_content.replace( + "", + "\n\n{}".format(release_content), + ) + # print(new_content[:800]) + CHANGELOG_PATH.write_text(new_content) + + # print('git status before adding files:') + # print(local_repo.git.status()) + + local_repo.git.add(CHANGELOG_PATH) + local_repo.git.add(LIBRARY_FILE_PATH) + + # print('git status after adding files:') + # print(local_repo.git.status()) + + # print('diff:') + # git diff --cached --color=always + # print(local_repo.git.diff(cached=True, color='always')) + + local_repo.git.commit( + message=result["message"], + author="{} <{}>".format( + local_repo.git.config("--get", "user.name"), + local_repo.git.config("--get", "user.email"), + ), + ) + + print("diff after commit:") + # git log --max-count=1 --patch --color=always + print(local_repo.git.log(max_count="1", patch=True, color="always")) + + # Push local changes. + server = "https://%7B%7D@github.com/%7B%7D.git".format(GITHUB_TOKEN, GITHUB_REPOSITORY) + print( + 'pushing changes to branch "{}" of repository "{}"'.format( + GITHUB_REF_NAME, GITHUB_REPOSITORY + ) + ) + local_repo.git.push(server, GITHUB_REF_NAME) + + # Create tag and release. + tag = result["new_version"] + tag_message = result["message"] + release_name = "Release {}".format(release_version) + release_message = ( + "See [change log]" + "(https://github.com/php-curl-class/php-curl-class/blob/master/CHANGELOG.md) for changes.\n" + "\n" + "https://github.com/php-curl-class/php-curl-class/compare/%7B%7D...%7B%7D".format( + result["old_version"], + result["new_version"], + ) + ) + commit_sha = local_repo.head.commit.hexsha + print("tag: {}".format(tag)) + print('tag_message: "{}"'.format(tag_message)) + print('release_name: "{}"'.format(release_name)) + print('release_message: "{}"'.format(release_message)) + print("commit_sha: {}".format(commit_sha)) + + github_repo.create_git_tag_and_release( + tag=tag, + tag_message=tag_message, + release_name=release_name, + release_message=release_message, + object=commit_sha, + type="commit", + draft=False, + ) + print("created tag and release") + + +if __name__ == "__main__": + main() diff --git a/scripts/make_release_recreate.sh b/scripts/make_release_recreate.sh new file mode 100755 index 0000000000..ae3dc14d9e --- /dev/null +++ b/scripts/make_release_recreate.sh @@ -0,0 +1,8 @@ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +rm -v "make_release_requirements.txt" + +pip-compile \ + --output-file="make_release_requirements.txt" \ + "make_release_requirements.in" diff --git a/scripts/make_release_requirements.in b/scripts/make_release_requirements.in new file mode 100644 index 0000000000..b444b66661 --- /dev/null +++ b/scripts/make_release_requirements.in @@ -0,0 +1,2 @@ +gitpython +pygithub diff --git a/scripts/make_release_requirements.json b/scripts/make_release_requirements.json new file mode 100644 index 0000000000..8da701d1de --- /dev/null +++ b/scripts/make_release_requirements.json @@ -0,0 +1,5 @@ +[ + { + "outdated_count": "0" + } +] diff --git a/scripts/make_release_requirements.txt b/scripts/make_release_requirements.txt new file mode 100644 index 0000000000..92c3466917 --- /dev/null +++ b/scripts/make_release_requirements.txt @@ -0,0 +1,40 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --output-file=scripts/make_release_requirements.txt scripts/make_release_requirements.in +# +certifi==2025.11.12 + # via requests +cffi==2.0.0 + # via + # cryptography + # pynacl +charset-normalizer==3.4.4 + # via requests +cryptography==46.0.3 + # via pyjwt +gitdb==4.0.12 + # via gitpython +gitpython==3.1.45 + # via -r scripts/make_release_requirements.in +idna==3.11 + # via requests +pycparser==2.23 + # via cffi +pygithub==2.8.1 + # via -r scripts/make_release_requirements.in +pyjwt[crypto]==2.10.1 + # via pygithub +pynacl==1.6.1 + # via pygithub +requests==2.32.5 + # via pygithub +smmap==5.0.2 + # via gitdb +typing-extensions==4.15.0 + # via pygithub +urllib3==2.5.0 + # via + # pygithub + # requests diff --git a/scripts/make_release_update_requirements.sh b/scripts/make_release_update_requirements.sh new file mode 100755 index 0000000000..9786d88273 --- /dev/null +++ b/scripts/make_release_update_requirements.sh @@ -0,0 +1,32 @@ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +update_package() { + local package_name="${1}" + local package_version="${2}" + + if [[ ! -z "${package_version}" ]]; then + echo "Updating ${package_name} to version ${package_version}" + local upgrade_package="${package_name}==${package_version}" + else + echo "Updating ${package_name}" + local upgrade_package="${package_name}" + fi + + pip-compile \ + --upgrade-package="${upgrade_package}" \ + --output-file="make_release_requirements.txt" \ + "make_release_requirements.in" +} + +package_name="${1}" +package_version="${2}" + +if [[ -z "${package_name}" ]]; then + echo "No package name provided. Usage: $0 []" + exit 1 +fi + +set -x + +update_package "${package_name}" "${package_version}" diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh new file mode 100755 index 0000000000..5ef8d3b18f --- /dev/null +++ b/scripts/pre-commit.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +list_of_files="${@}" +for file in $list_of_files; do + if [[ "${file}" == "composer.json" ]]; then + $(which composer) validate + exit "${?}" + else + echo "unsupported file: ${file}" + exit 1 + fi +done + +exit 0 diff --git a/scripts/update_readme_methods.sh b/scripts/update_readme_methods.sh index e1862f9680..5c8574513e 100755 --- a/scripts/update_readme_methods.sh +++ b/scripts/update_readme_methods.sh @@ -1,27 +1,45 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}/.." + before=$(head -n $( - grep --context="0" --line-number --max-count="1" "### Available Methods" "README.md" | + grep --context="0" --line-number --max-count="1" "### 📖 Available Methods" "README.md" | perl -pe 's/^(\d+):.*/\1/') "README.md") after=$(tail -n +$( - grep --context="0" --line-number --max-count="1" "### Contribute" "README.md" | + grep --context="0" --line-number --max-count="1" "### 🔒 Security" "README.md" | perl -pe 's/^(\d+):.*/\1/') "README.md") echo "${before}" > "README.md" -curl_max_line_number=$(grep --context="0" --line-number --max-count="1" '^}$' "src/Curl/Curl.php" | \ - perl -pe 's/^(\d+):.*/\1/') +basecurl_path="src/Curl/BaseCurl.php" +curl_path="src/Curl/Curl.php" +multicurl_path="src/Curl/MultiCurl.php" + echo '```php' >> "README.md" -head -n "${curl_max_line_number}" "src/Curl/Curl.php" | \ - egrep "^ .* function .*" | \ - egrep "^ public" | \ - sort | \ - perl -pe 's/^ public (.* )?function /Curl::/' \ - >> "README.md" -egrep "^ .* function .*" "src/Curl/MultiCurl.php" | \ - egrep "^ public" | \ - sort | \ - perl -pe 's/^ public (.* )?function /MultiCurl::/' \ - >> "README.md" + +curl_class_name="$(basename --suffix=".php" "${curl_path}")" && +curl_fns="$( + grep --extended-regexp "^ .* function .*" "${curl_path}" | + grep --extended-regexp "^ public" | + perl -pe "s/^ public (.* )?function /${curl_class_name}::/")" + +multicurl_class_name="$(basename --suffix=".php" "${multicurl_path}")" && +multicurl_fns="$( + grep --extended-regexp "^ .* function .*" "${multicurl_path}" | + grep --extended-regexp "^ public" | + perl -pe "s/^ public (.* )?function /${multicurl_class_name}::/")" + +common_fns="$( + grep --extended-regexp "^ .* function .*" "${basecurl_path}" | + grep --extended-regexp "^ public" | + perl -pe "s/^ public .* ?function (.*)/Curl::\1\nMultiCurl::\1/")" + +echo "${curl_fns} +${multicurl_fns} +${common_fns}" | sort | uniq >> "README.md" + echo '```' >> "README.md" echo >> "README.md" @@ -30,15 +48,29 @@ echo "${after}" >> "README.md" # Update table of contents. script=$(cat <<'EOF' $data = file_get_contents('README.md'); - preg_match_all('/^### ([\w ]+)/m', $data, $matches); - $toc = array(); + preg_match_all('/^### (.*)/m', $data, $matches); + $toc = []; foreach ($matches['1'] as $match) { - $href = '#' . str_replace(' ', '-', strtolower($match)); + $slug = urlencode( + strtolower( + str_replace( + ' ', + '-', + preg_replace( + '/[^A-Za-z\x{FE0F} ]/u', + '', + $match, + ), + ) + ) + ); + $href = '#' . $slug; $toc[] = '- [' . $match . '](' . $href . ')'; } $toc = implode("\n", $toc); $toc = '---' . "\n\n" . $toc . "\n\n" . '---' . "\n\n"; $data = preg_replace('/---\n\n(?:- .*\n)+?\n---\n\n/', $toc, $data); file_put_contents('README.md', $data); -EOF) +EOF +) php --run "${script}" diff --git a/scripts/v4_migration.php b/scripts/v4_migration.php deleted file mode 100644 index d78c3d0694..0000000000 --- a/scripts/v4_migration.php +++ /dev/null @@ -1,93 +0,0 @@ - array(), - 'files_found' => array(), - 'files_to_change' => array(), -); - -$migrations = array( - 'error_code' => 'errorCode', - 'error_message' => 'errorMessage', - 'curl_error' => 'curlError', - 'curl_error_code' => 'curlErrorCode', - 'curl_error_message' => 'curlErrorMessage', - 'http_error' => 'httpError', - 'http_status_code' => 'httpStatusCode', - 'http_error_message' => 'httpErrorMessage', - 'base_url' => 'baseUrl', - 'request_headers' => 'requestHeaders', - 'response_headers' => 'responseHeaders', - 'raw_response_headers' => 'rawResponseHeaders', - 'raw_response' => 'rawResponse', - 'before_send_function' => 'beforeSendFunction', - 'download_complete_function' => 'downloadCompleteFunction', -); - -$directory = new RecursiveDirectoryIterator($cwd); -$iterator = new RecursiveIteratorIterator($directory); -$regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); -foreach ($regex as $file) { - $filepath = $file['0']; - $results['files_found'][] = $filepath; - if ($filepath === __FILE__) { - continue; - } - $data = file_get_contents($filepath); - $short_path = str_replace($cwd, '', $filepath); - if ($data === false) { - $results['errors'][] = $filepath; - echo $short_path . ' [ERROR]' . "\n"; - } else { - foreach ($migrations as $old => $new) { - if (!(strpos($data, '->' . $old) === false)) { - $results['files_to_change'][] = $filepath; - echo $short_path . "\n"; - break; - } - } - } -} - -foreach ($results as $name => $files) { - $results[$name . '_count'] = count($files); -} -$results['errors_count'] = count($results['errors']); -$results['files_found_count'] = count($results['files_found']); -$results['files_to_change_count'] = count($results['files_to_change']); - -if ($results['errors_count'] > 0) { - echo 'ERROR: Unable to read files.' . "\n"; - exit(1); -} else if ($results['files_found_count'] === 0) { - echo 'Current directory "' . $cwd . '"' . "\n"; - echo 'ERROR: No read files found in current directory.' . "\n"; - exit(1); -} else if ($results['files_to_change_count'] === 0) { - echo 'OK: No files to change.' . "\n"; - exit(0); -} else if ($results['files_to_change_count'] > 0) { - echo $results['files_to_change_count'] . ' of ' . $results['files_found_count'] . ' files to change found.' . "\n"; - echo 'Continue? [y/n] '; - if (!in_array(trim(fgets(STDIN)), array('y', 'Y'))) { - die(); - } - foreach ($results['files_to_change'] as $filepath) { - $data = file_get_contents($filepath); - foreach ($migrations as $old => $new) { - $data = str_replace('->' . $old, '->' . $new, $data); - } - file_put_contents($filepath, $data); - } - echo 'Done' . "\n"; - exit(0); -} diff --git a/src/Curl/ArrayUtil.php b/src/Curl/ArrayUtil.php new file mode 100644 index 0000000000..20a83c8fdb --- /dev/null +++ b/src/Curl/ArrayUtil.php @@ -0,0 +1,159 @@ + $value) { + if (is_scalar($value)) { + if ($prefix) { + $arrays_to_merge[] = [ + $prefix . '[' . $key . ']' => $value, + ]; + } else { + $arrays_to_merge[] = [ + $key => $value, + ]; + } + } elseif ($value instanceof \CURLFile) { + $arrays_to_merge[] = [ + $key => $value, + ]; + } elseif ($value instanceof \CURLStringFile) { + $arrays_to_merge[] = [ + $key => $value, + ]; + } else { + $arrays_to_merge[] = self::arrayFlattenMultidim( + $value, + $prefix ? $prefix . '[' . $key . ']' : $key + ); + } + } + + $return = array_merge($return, ...$arrays_to_merge); + } + } elseif ($array === null) { + $return[$prefix] = $array; + } + return $return; + } + + /** + * Array Flatten Multidim + * + * @deprecated Use ArrayUtil::arrayFlattenMultidim(). + * @param $array + * @param $prefix + * @return array + */ + public static function array_flatten_multidim($array, $prefix = false) + { + return static::arrayFlattenMultidim($array, $prefix); + } + + /** + * Array Random + * + * @param $array + * @return mixed + */ + public static function arrayRandom($array) + { + return $array[static::arrayRandomIndex($array)]; + } + + /** + * Array Random Index + * + * @param $array + * @return int + */ + public static function arrayRandomIndex($array) + { + return mt_rand(0, count($array) - 1); + } + + /** + * Array Random + * + * @deprecated Use ArrayUtil::arrayRandom(). + * @param $array + * @return mixed + */ + public static function array_random($array) + { + return static::arrayRandom($array); + } +} diff --git a/src/Curl/BaseCurl.php b/src/Curl/BaseCurl.php new file mode 100644 index 0000000000..54d4a5072e --- /dev/null +++ b/src/Curl/BaseCurl.php @@ -0,0 +1,422 @@ +beforeSendCallback = $callback; + } + + abstract public function close(); + + /** + * Complete + * + * @param $callback callable|null + */ + public function complete($callback) + { + $this->completeCallback = $callback; + } + + /** + * Disable Timeout + */ + public function disableTimeout() + { + $this->setTimeout(null); + } + + /** + * Error + * + * @param $callback callable|null + */ + public function error($callback) + { + $this->errorCallback = $callback; + } + + /** + * Get Opt + * + * @param $option + * @return mixed + */ + public function getOpt($option) + { + return $this->options[$option] ?? null; + } + + /** + * Remove Header + * + * Remove an internal header from the request. + * Using `curl -H "Host:" ...' is equivalent to $curl->removeHeader('Host');. + * + * @param $key + */ + public function removeHeader($key) + { + $this->setHeader($key, ''); + } + + /** + * Set auto referer + * + * @param mixed $auto_referer + */ + public function setAutoReferer($auto_referer = true) + { + $this->setAutoReferrer($auto_referer); + } + + /** + * Set auto referrer + * + * @param mixed $auto_referrer + */ + public function setAutoReferrer($auto_referrer = true) + { + $this->setOpt(CURLOPT_AUTOREFERER, $auto_referrer); + } + + /** + * Set Basic Authentication + * + * @param $username + * @param $password + */ + public function setBasicAuthentication($username, $password = '') + { + $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + } + + /** + * Set Connect Timeout + * + * @param $seconds + */ + public function setConnectTimeout($seconds) + { + $this->setOpt(CURLOPT_CONNECTTIMEOUT, $seconds); + } + + abstract public function setCookie($key, $value); + abstract public function setCookieFile($cookie_file); + abstract public function setCookieJar($cookie_jar); + abstract public function setCookieString($string); + abstract public function setCookies($cookies); + + /** + * Set Digest Authentication + * + * @param $username + * @param $password + */ + public function setDigestAuthentication($username, $password = '') + { + $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + } + + /** + * After Send + * + * This function is called after the request has been sent. + * + * It can be used to override whether or not the request errored. The + * instance is passed as the first argument to the function and the instance + * has attributes like $instance->httpStatusCode and $instance->response to + * help decide if the request errored. Set $instance->error to true or false + * within the function. + * + * When $instance->error is true indicating a request error, the error + * callback set by Curl::error() is called. When $instance->error is false, + * the success callback set by Curl::success() is called. + * + * @param $callback callable|null + */ + public function afterSend($callback) + { + $this->afterSendCallback = $callback; + } + + /** + * Set File + * + * @param $file + */ + public function setFile($file) + { + $this->setOpt(CURLOPT_FILE, $file); + } + + protected function setFileInternal($file) + { + $this->setOptInternal(CURLOPT_FILE, $file); + } + + /** + * Set follow location + * + * @param mixed $follow_location + * @see Curl::setMaximumRedirects() + */ + public function setFollowLocation($follow_location = true) + { + $this->setOpt(CURLOPT_FOLLOWLOCATION, $follow_location); + } + + /** + * Set forbid reuse + * + * @param mixed $forbid_reuse + */ + public function setForbidReuse($forbid_reuse = true) + { + $this->setOpt(CURLOPT_FORBID_REUSE, $forbid_reuse); + } + + abstract public function setHeader($key, $value); + abstract public function setHeaders($headers); + + /** + * Set Interface + * + * The name of the outgoing network interface to use. + * This can be an interface name, an IP address or a host name. + * + * @param $interface + */ + public function setInterface($interface) + { + $this->setOpt(CURLOPT_INTERFACE, $interface); + } + + abstract public function setJsonDecoder($mixed); + + /** + * Set maximum redirects + * + * @param mixed $maximum_redirects + * @see Curl::setFollowLocation() + */ + public function setMaximumRedirects($maximum_redirects) + { + $this->setOpt(CURLOPT_MAXREDIRS, $maximum_redirects); + } + + abstract public function setOpt($option, $value); + + protected function setOptInternal($option, $value) + { + } + + abstract public function setOpts($options); + + /** + * Set Port + * + * @param $port + */ + public function setPort($port) + { + $this->setOpt(CURLOPT_PORT, (int) $port); + } + + /** + * Set Proxy + * + * Set an HTTP proxy to tunnel requests through. + * + * @param $proxy - The HTTP proxy to tunnel requests through. May include port number. + * @param $port - The port number of the proxy to connect to. This port number can also be set in $proxy. + * @param $username - The username to use for the connection to the proxy. + * @param $password - The password to use for the connection to the proxy. + */ + public function setProxy($proxy, $port = null, $username = null, $password = null) + { + $this->setOpt(CURLOPT_PROXY, $proxy); + if ($port !== null) { + $this->setOpt(CURLOPT_PROXYPORT, $port); + } + if ($username !== null && $password !== null) { + $this->setOpt(CURLOPT_PROXYUSERPWD, $username . ':' . $password); + } + } + + /** + * Set Proxy Auth + * + * Set the HTTP authentication method(s) to use for the proxy connection. + * + * @param $auth + */ + public function setProxyAuth($auth) + { + $this->setOpt(CURLOPT_PROXYAUTH, $auth); + } + + /** + * Set Proxy Tunnel + * + * Set the proxy to tunnel through HTTP proxy. + * + * @param $tunnel boolean + */ + public function setProxyTunnel($tunnel = true) + { + $this->setOpt(CURLOPT_HTTPPROXYTUNNEL, $tunnel); + } + + /** + * Set Proxy Type + * + * Set the proxy protocol type. + * + * @param $type + */ + public function setProxyType($type) + { + $this->setOpt(CURLOPT_PROXYTYPE, $type); + } + + /** + * Set Range + * + * @param $range + */ + public function setRange($range) + { + $this->setOpt(CURLOPT_RANGE, $range); + } + + protected function setRangeInternal($range) + { + $this->setOptInternal(CURLOPT_RANGE, $range); + } + + /** + * Set Referer + * + * @param $referer + */ + public function setReferer($referer) + { + $this->setReferrer($referer); + } + + /** + * Set Referrer + * + * @param $referrer + */ + public function setReferrer($referrer) + { + $this->setOpt(CURLOPT_REFERER, $referrer); + } + + abstract public function setRetry($mixed); + + /** + * Set Timeout + * + * @param $seconds + */ + public function setTimeout($seconds) + { + $this->setOpt(CURLOPT_TIMEOUT, $seconds); + } + + protected function setTimeoutInternal($seconds) + { + $this->setOptInternal(CURLOPT_TIMEOUT, $seconds); + } + + abstract public function setUrl($url, $mixed_data = ''); + + /** + * Set User Agent + * + * @param $user_agent + */ + public function setUserAgent($user_agent) + { + $this->setOpt(CURLOPT_USERAGENT, $user_agent); + } + + protected function setUserAgentInternal($user_agent) + { + $this->setOptInternal(CURLOPT_USERAGENT, $user_agent); + } + + abstract public function setXmlDecoder($mixed); + abstract public function stop(); + + /** + * Success + * + * @param $callback callable|null + */ + public function success($callback) + { + $this->successCallback = $callback; + } + + abstract public function unsetHeader($key); + + /** + * Unset Proxy + * + * Disable use of the proxy. + */ + public function unsetProxy() + { + $this->setOpt(CURLOPT_PROXY, null); + } + + /** + * Verbose + * + * @param bool $on + * @param resource|string $output + */ + public function verbose($on = true, $output = 'STDERR') + { + if ($output === 'STDERR') { + if (defined('STDERR')) { + $output = STDERR; + } else { + $output = fopen('php://stderr', 'wb'); + } + } + + // Turn off CURLINFO_HEADER_OUT for verbose to work. This has the side + // effect of causing Curl::requestHeaders to be empty. + if ($on) { + $this->setOpt(CURLINFO_HEADER_OUT, false); + } + $this->setOpt(CURLOPT_VERBOSE, $on); + $this->setOpt(CURLOPT_STDERR, $output); + } +} diff --git a/src/Curl/CaseInsensitiveArray.php b/src/Curl/CaseInsensitiveArray.php index 956514c080..bc2daca0e3 100644 --- a/src/Curl/CaseInsensitiveArray.php +++ b/src/Curl/CaseInsensitiveArray.php @@ -1,12 +1,17 @@ $value) { @@ -51,19 +54,17 @@ public function __construct(Array $initial = null) /** * Offset Set * - * Set data at a specified Offset. Converts the offset to lower-case, and - * stores the Case-Sensitive Offset and the Data at the lower-case indexes - * in $this->keys and @this->data. - * - * @see https://secure.php.net/manual/en/arrayaccess.offseteset.php - * - * @param string $offset The offset to store the data at (case-insensitive). - * @param mixed $value The data to store at the specified offset. + * Set data at a specified offset. Converts the offset to lowercase, and + * stores the case-sensitive offset and the data at the lowercase indexes in + * $this->keys and @this->data. * + * @param string $offset The offset to store the data at (case-insensitive). + * @param mixed $value The data to store at the specified offset. * @return void - * - * @access public + * @see https://secure.php.net/manual/en/arrayaccess.offsetset.php */ + #[\Override] + #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { if ($offset === null) { @@ -78,36 +79,32 @@ public function offsetSet($offset, $value) /** * Offset Exists * - * Checks if the Offset exists in data storage. The index is looked up with - * the lower-case version of the provided offset. + * Checks if the offset exists in data storage. The index is looked up with + * the lowercase version of the provided offset. * + * @param string $offset Offset to check + * @return bool If the offset exists. * @see https://secure.php.net/manual/en/arrayaccess.offsetexists.php - * - * @param string $offset Offset to check - * - * @return bool If the offset exists. - * - * @access public */ + #[\Override] + #[\ReturnTypeWillChange] public function offsetExists($offset) { - return (bool) array_key_exists(strtolower($offset), $this->data); + return array_key_exists(strtolower($offset), $this->data); } /** * Offset Unset * * Unsets the specified offset. Converts the provided offset to lowercase, - * and unsets the Case-Sensitive Key, as well as the stored data. - * - * @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php - * - * @param string $offset The offset to unset. + * and unsets the case-sensitive key, as well as the stored data. * + * @param string $offset The offset to unset. * @return void - * - * @access public + * @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php */ + #[\Override] + #[\ReturnTypeWillChange] public function offsetUnset($offset) { $offsetlower = strtolower($offset); @@ -119,49 +116,41 @@ public function offsetUnset($offset) * Offset Get * * Return the stored data at the provided offset. The offset is converted to - * lowercase and the lookup is done on the Data store directly. + * lowercase and the lookup is done on the data store directly. * + * @param string $offset Offset to lookup. + * @return mixed The data stored at the offset. * @see https://secure.php.net/manual/en/arrayaccess.offsetget.php - * - * @param string $offset Offset to lookup. - * - * @return mixed The data stored at the offset. - * - * @access public */ + #[\Override] + #[\ReturnTypeWillChange] public function offsetGet($offset) { $offsetlower = strtolower($offset); - return isset($this->data[$offsetlower]) ? $this->data[$offsetlower] : null; + return $this->data[$offsetlower] ?? null; } /** * Count * + * @return int The number of elements stored in the array. * @see https://secure.php.net/manual/en/countable.count.php - * - * @param void - * - * @return int The number of elements stored in the Array. - * - * @access public */ + #[\Override] + #[\ReturnTypeWillChange] public function count() { - return (int) count($this->data); + return count($this->data); } /** * Current * - * @see https://secure.php.net/manual/en/iterator.current.php - * - * @param void - * * @return mixed Data at the current position. - * - * @access public + * @see https://secure.php.net/manual/en/iterator.current.php */ + #[\Override] + #[\ReturnTypeWillChange] public function current() { return current($this->data); @@ -170,14 +159,11 @@ public function current() /** * Next * - * @see https://secure.php.net/manual/en/iterator.next.php - * - * @param void - * * @return void - * - * @access public + * @see https://secure.php.net/manual/en/iterator.next.php */ + #[\Override] + #[\ReturnTypeWillChange] public function next() { next($this->data); @@ -186,47 +172,38 @@ public function next() /** * Key * + * @return mixed Case-sensitive key at current position. * @see https://secure.php.net/manual/en/iterator.key.php - * - * @param void - * - * @return mixed Case-Sensitive key at current position. - * - * @access public */ + #[\Override] + #[\ReturnTypeWillChange] public function key() { $key = key($this->data); - return isset($this->keys[$key]) ? $this->keys[$key] : $key; + return $this->keys[$key] ?? $key; } /** * Valid * - * @see https://secure.php.net/manual/en/iterator.valid.php - * - * @param void - * * @return bool If the current position is valid. - * - * @access public + * @see https://secure.php.net/manual/en/iterator.valid.php */ + #[\Override] + #[\ReturnTypeWillChange] public function valid() { - return (bool) !(key($this->data) === null); + return (key($this->data) !== null); } /** * Rewind * - * @see https://secure.php.net/manual/en/iterator.rewind.php - * - * @param void - * * @return void - * - * @access public + * @see https://secure.php.net/manual/en/iterator.rewind.php */ + #[\Override] + #[\ReturnTypeWillChange] public function rewind() { reset($this->data); diff --git a/src/Curl/Curl.php b/src/Curl/Curl.php index 324f4817fc..83adcafe59 100644 --- a/src/Curl/Curl.php +++ b/src/Curl/Curl.php @@ -1,55 +1,15 @@ ', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', - // %x5D-7E - ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', - 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', - ), true); - } - - public static function RFC2616() { - return self::$RFC2616; - } - - public static function RFC6265() { - return self::$RFC6265; - } -} +declare(strict_types=1); -CurlCookieConst::Init(); +namespace Curl; -class Curl +class Curl extends BaseCurl { - const VERSION = '4.8.2'; - const DEFAULT_TIMEOUT = 30; + public const VERSION = '12.0.1'; + public const DEFAULT_TIMEOUT = 30; - public $curl; + public $curl = null; public $id = null; public $error = false; @@ -64,126 +24,200 @@ class Curl public $httpStatusCode = 0; public $httpErrorMessage = null; - public $baseUrl = null; public $url = null; - public $requestHeaders = null; + public $requestHeaders = []; + public $responseHeaders = null; public $rawResponseHeaders = ''; + public $responseCookies = []; public $response = null; public $rawResponse = null; - public $beforeSendFunction = null; - public $downloadCompleteFunction = null; - private $successFunction = null; - private $errorFunction = null; - private $completeFunction = null; + public $downloadCompleteCallback = null; + public $fileHandle = null; + public $downloadFileName = null; + + public $attempts = 0; + public $retries = 0; + public $childOfMultiCurl = false; + public $remainingRetries = 0; + public $retryDecider = null; - private $cookies = array(); - private $responseCookies = array(); - private $headers = array(); - private $options = array(); + public $jsonDecoder = null; + public $xmlDecoder = null; - private $jsonDecoder = null; + private $headerCallbackData; + private $cookies = []; + private $headers = []; + + private $jsonDecoderArgs = []; private $jsonPattern = '/^(?:application|text)\/(?:[a-z]+(?:[\.-][0-9a-z]+){0,}[\+\.]|x-)?json(?:-[a-z]+)?/i'; - private $xmlPattern = '~^(?:text/|application/(?:atom\+|rss\+)?)xml~i'; + private $xmlDecoderArgs = []; + private $xmlPattern = '~^(?:text/|application/(?:atom\+|rss\+|soap\+)?)xml~i'; + private $defaultDecoder = null; + + public static $RFC2616 = [ + // RFC 2616: "any CHAR except CTLs or separators". + // CHAR = + // CTL = + // separators = "(" | ")" | "<" | ">" | "@" + // | "," | ";" | ":" | "\" | <"> + // | "/" | "[" | "]" | "?" | "=" + // | "{" | "}" | SP | HT + // SP = + // HT = + // <"> = + '!', '#', '$', '%', '&', "'", '*', '+', '-', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', + 'Y', 'Z', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', + 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~', + ]; + public static $RFC6265 = [ + // RFC 6265: "US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash". + // %x21 + '!', + // %x23-2B + '#', '$', '%', '&', "'", '(', ')', '*', '+', + // %x2D-3A + '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', + // %x3C-5B + '<', '=', '>', '?', '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', + 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '[', + // %x5D-7E + ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', + 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', + ]; + + public $curlErrorCodeConstant; + public $curlErrorCodeConstants; + + private static $deferredProperties = [ + 'curlErrorCodeConstant', + 'curlErrorCodeConstants', + 'curlOptionCodeConstants', + 'effectiveUrl', + 'rfc2616', + 'rfc6265', + 'totalTime', + ]; + private array $deferredValues = []; /** * Construct * - * @access public - * @param $base_url + * @param $base_url + * @param mixed $options * @throws \ErrorException */ - public function __construct($base_url = null) + public function __construct($base_url = null, $options = []) { if (!extension_loaded('curl')) { throw new \ErrorException('cURL library is not loaded'); } - $this->curl = curl_init(); - $this->id = 1; - $this->setDefaultUserAgent(); - $this->setDefaultJsonDecoder(); - $this->setDefaultTimeout(); - $this->setOpt(CURLINFO_HEADER_OUT, true); - $this->setOpt(CURLOPT_HEADERFUNCTION, array($this, 'headerCallback')); - $this->setOpt(CURLOPT_RETURNTRANSFER, true); - $this->headers = new CaseInsensitiveArray(); - $this->setURL($base_url); - } + unset($this->deferredValues['curlErrorCodeConstant']); + unset($this->deferredValues['curlErrorCodeConstants']); + unset($this->deferredValues['curlOptionCodeConstants']); + unset($this->deferredValues['effectiveUrl']); + unset($this->deferredValues['rfc2616']); + unset($this->deferredValues['rfc6265']); + unset($this->deferredValues['totalTime']); - /** - * Before Send - * - * @access public - * @param $callback - */ - public function beforeSend($callback) - { - $this->beforeSendFunction = $callback; + $this->curl = curl_init(); + $this->initialize($base_url, $options); } /** * Build Post Data * - * @access public - * @param $data - * + * @param $data * @return array|string + * @throws \ErrorException */ public function buildPostData($data) { - if (is_array($data)) { - if (self::is_array_multidim($data)) { - if (isset($this->headers['Content-Type']) && - preg_match($this->jsonPattern, $this->headers['Content-Type'])) { - $json_str = json_encode($data); - if (!($json_str === false)) { - $data = $json_str; - } - } else { - $data = self::http_build_multi_query($data); - } - } else { - $binary_data = false; - foreach ($data as $key => $value) { - // Fix "Notice: Array to string conversion" when $value in curl_setopt($ch, CURLOPT_POSTFIELDS, - // $value) is an array that contains an empty array. - if (is_array($value) && empty($value)) { - $data[$key] = ''; - // Fix "curl_setopt(): The usage of the @filename API for file uploading is deprecated. Please use - // the CURLFile class instead". Ignore non-file values prefixed with the @ character. - } elseif (is_string($value) && strpos($value, '@') === 0 && is_file(substr($value, 1))) { - $binary_data = true; - if (class_exists('CURLFile')) { - $data[$key] = new \CURLFile(substr($value, 1)); - } - } elseif ($value instanceof \CURLFile) { - $binary_data = true; - } - } + $binary_data = false; + + // Return JSON-encoded string when the request's content-type is JSON and the data is serializable. + if ( + isset($this->headers['Content-Type']) && + preg_match($this->jsonPattern, $this->headers['Content-Type']) && + ( + is_array($data) || + ( + is_object($data) && + interface_exists('JsonSerializable', false) && + $data instanceof \JsonSerializable + ) + ) + ) { + $data = \Curl\Encoder::encodeJson($data); + } elseif (is_array($data)) { + // Manually build a single-dimensional array from a multi-dimensional array as using curl_setopt($ch, + // CURLOPT_POSTFIELDS, $data) doesn't correctly handle multi-dimensional arrays when files are + // referenced. + if (ArrayUtil::isArrayMultidim($data)) { + $data = ArrayUtil::arrayFlattenMultidim($data); + } - if (!$binary_data) { - if (isset($this->headers['Content-Type']) && - preg_match($this->jsonPattern, $this->headers['Content-Type'])) { - $json_str = json_encode($data); - if (!($json_str === false)) { - $data = $json_str; - } - } else { - $data = http_build_query($data, '', '&'); + // Modify array values to ensure any referenced files are properly handled depending on the support of + // the @filename API or CURLFile usage. This also fixes the warning "curl_setopt(): The usage of the + // @filename API for file uploading is deprecated. Please use the CURLFile class instead". Ignore + // non-file values prefixed with the @ character. + foreach ($data as $key => $value) { + if (is_string($value) && strpos($value, '@') === 0 && is_file(substr($value, 1))) { + $binary_data = true; + if (class_exists('CURLFile')) { + $data[$key] = new \CURLFile(substr($value, 1)); } + } elseif ($value instanceof \CURLFile) { + $binary_data = true; + } elseif ($value instanceof \CURLStringFile) { + $binary_data = true; } } } + if ( + !$binary_data && + (is_array($data) || is_object($data)) && + ( + !isset($this->headers['Content-Type']) || + !preg_match('/^multipart\/form-data/', $this->headers['Content-Type']) + ) + ) { + // Avoid using http_build_query() as keys with null values are + // unexpectedly excluded from the resulting string. + // + // $ php -a + // php > echo http_build_query(['a' => '1', 'b' => null, 'c' => '3']); + // a=1&c=3 + // php > echo http_build_query(['a' => '1', 'b' => '', 'c' => '3']); + // a=1&b=&c=3 + // + // $data = http_build_query($data, '', '&'); + $data = implode('&', array_map(function ($k, $v) { + // Encode keys and values using urlencode() to match the default + // behavior http_build_query() where $encoding_type is + // PHP_QUERY_RFC1738. + // + // Use strval() as urlencode() expects a string parameter: + // TypeError: urlencode() expects parameter 1 to be string, integer given + // TypeError: urlencode() expects parameter 1 to be string, null given + // + // php_raw_url_encode() + // php_url_encode() + // https://github.com/php/php-src/blob/master/ext/standard/http.c + return urlencode(strval($k)) . '=' . urlencode(strval($v)); + }, array_keys((array)$data), array_values((array)$data))); + } + return $data; } /** * Call - * - * @access public */ public function call() { @@ -197,34 +231,25 @@ public function call() /** * Close - * - * @access public */ + #[\Override] public function close() { - if (is_resource($this->curl)) { - curl_close($this->curl); - } + $this->curl = null; $this->options = null; + $this->userSetOptions = null; $this->jsonDecoder = null; - } - - /** - * Complete - * - * @access public - * @param $callback - */ - public function complete($callback) - { - $this->completeFunction = $callback; + $this->jsonDecoderArgs = null; + $this->xmlDecoder = null; + $this->xmlDecoderArgs = null; + $this->headerCallbackData = null; + $this->defaultDecoder = null; } /** * Progress * - * @access public - * @param $callback + * @param $callback callable|null */ public function progress($callback) { @@ -232,134 +257,298 @@ public function progress($callback) $this->setOpt(CURLOPT_NOPROGRESS, false); } + private function progressInternal($callback) + { + $this->setOptInternal(CURLOPT_PROGRESSFUNCTION, $callback); + $this->setOptInternal(CURLOPT_NOPROGRESS, false); + } + /** * Delete * - * @access public - * @param $url - * @param $query_parameters - * @param $data - * - * @return string + * @param $url + * @param $query_parameters + * @param $data + * @return mixed Returns the value provided by exec. */ - public function delete($url, $query_parameters = array(), $data = array()) + public function delete($url, $query_parameters = [], $data = []) { - if (is_array($url)) { - $data = $query_parameters; - $query_parameters = $url; - $url = $this->baseUrl; - } - - $this->setURL($url, $query_parameters); - $this->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE'); - $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data)); + $this->setDelete($url, $query_parameters, $data); return $this->exec(); } - /** - * Download Complete - * - * @access public - * @param $fh - */ - public function downloadComplete($fh) + public function setDelete($url, $query_parameters = [], $data = []) { - if (!$this->error && $this->downloadCompleteFunction) { - rewind($fh); - $this->call($this->downloadCompleteFunction, $fh); - $this->downloadCompleteFunction = null; + if (is_array($url)) { + $data = $query_parameters; + $query_parameters = $url; + $url = (string)$this->url; } - if (is_resource($fh)) { - fclose($fh); - } + $this->setUrl($url, $query_parameters); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE'); - // Fix "PHP Notice: Use of undefined constant STDOUT" when reading the - // PHP script from stdin. Using null causes "Warning: curl_setopt(): - // supplied argument is not a valid File-Handle resource". - if (!defined('STDOUT')) { - define('STDOUT', fopen('php://stdout', 'w')); + // Avoid including a content-length header in DELETE requests unless there is a message body. The following + // would include "Content-Length: 0" in the request header: + // curl_setopt($ch, CURLOPT_POSTFIELDS, []); + // RFC 2616 4.3 Message Body: + // The presence of a message-body in a request is signaled by the + // inclusion of a Content-Length or Transfer-Encoding header field in + // the request's message-headers. + if (!empty($data)) { + $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data)); } - - // Reset CURLOPT_FILE with STDOUT to avoid: "curl_exec(): CURLOPT_FILE - // resource has gone away, resetting to default". - $this->setOpt(CURLOPT_FILE, STDOUT); - - // Reset CURLOPT_RETURNTRANSFER to tell cURL to return subsequent - // responses as the return value of curl_exec(). Without this, - // curl_exec() will revert to returning boolean values. - $this->setOpt(CURLOPT_RETURNTRANSFER, true); } /** * Download * - * @access public - * @param $url - * @param $mixed_filename - * - * @return boolean + * @param $url + * @param $mixed_filename + * @return bool */ public function download($url, $mixed_filename) { + // Use tmpfile() or php://temp to avoid "Too many open files" error. if (is_callable($mixed_filename)) { - $this->downloadCompleteFunction = $mixed_filename; - $fh = tmpfile(); + $this->downloadCompleteCallback = $mixed_filename; + $this->downloadFileName = null; + $this->fileHandle = tmpfile(); } else { $filename = $mixed_filename; - $fh = fopen($filename, 'wb'); + + // Use a temporary file when downloading. Not using a temporary file can cause an error when an existing + // file has already fully completed downloading and a new download is started with the same destination save + // path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid, + // but unsatisfiable. + $download_filename = $filename . '.pccdownload'; + $this->downloadFileName = $download_filename; + + // Attempt to resume download only when a temporary download file exists and is not empty. + if (is_file($download_filename) && $filesize = filesize($download_filename)) { + $first_byte_position = $filesize; + $range = (string)$first_byte_position . '-'; + $this->setRange($range); + $this->fileHandle = fopen($download_filename, 'ab'); + } else { + $this->fileHandle = fopen($download_filename, 'wb'); + } + + // Move the downloaded temporary file to the destination save path. + $this->downloadCompleteCallback = function ($instance, $fh) use ($download_filename, $filename) { + // Close the open file handle before renaming the file. + if (is_resource($fh)) { + fclose($fh); + } + + rename($download_filename, $filename); + }; } - $this->setOpt(CURLOPT_FILE, $fh); + $this->setFile($this->fileHandle); $this->get($url); - $this->downloadComplete($fh); return ! $this->error; } /** - * Error + * Fast download * - * @access public - * @param $callback + * @param $url + * @param $filename + * @param $connections + * @return bool */ - public function error($callback) + public function fastDownload($url, $filename, $connections = 4) { - $this->errorFunction = $callback; + // Retrieve content length from the "Content-Length" header from the url + // to download. Use an HTTP GET request without a body instead of a HEAD + // request because not all hosts support HEAD requests. + $curl = new Curl(); + $curl->setOptInternal(CURLOPT_NOBODY, true); + + // Pass user-specified options to the instance checking for content-length. + $curl->setOpts($this->userSetOptions); + $curl->get($url); + + // Exit early when an error occurred. + if ($curl->error) { + return false; + } + + $content_length = $curl->responseHeaders['Content-Length'] ?? null; + + // Use a regular download when content length could not be determined. + if (!$content_length) { + return $this->download($url, $filename); + } + + // Divide chunk_size across the number of connections. + $chunk_size = (int)ceil($content_length / $connections); + + // Keep track of file name parts. + $part_file_names = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setConcurrency($connections); + + for ($part_number = 1; $part_number <= $connections; $part_number++) { + $range_start = ($part_number - 1) * $chunk_size; + $range_end = $range_start + $chunk_size - 1; + if ($part_number === $connections) { + $range_end = ''; + } + $range = (string)$range_start . '-' . (string)$range_end; + + $part_file_name = $filename . '.part' . (string)$part_number; + + // Save the file name of this part. + $part_file_names[] = $part_file_name; + + // Remove any existing file part. + if (is_file($part_file_name)) { + unlink($part_file_name); + } + + // Create file part. + $file_handle = tmpfile(); + + // Setup the instance downloading a part. + $curl = new Curl(); + $curl->setUrl($url); + + // Pass user-specified options to the instance downloading a part. + $curl->setOpts($this->userSetOptions); + + $curl->setOptInternal(CURLOPT_CUSTOMREQUEST, 'GET'); + $curl->setOptInternal(CURLOPT_HTTPGET, true); + $curl->setRangeInternal($range); + $curl->setFileInternal($file_handle); + $curl->fileHandle = $file_handle; + + $curl->downloadCompleteCallback = function ($instance, $tmpfile) use ($part_file_name) { + $fh = fopen($part_file_name, 'wb'); + if ($fh !== false) { + stream_copy_to_stream($tmpfile, $fh); + fclose($fh); + } + }; + + $multi_curl->addCurl(http://github.com/$curl); + } + + // Start the simultaneous downloads for each of the ranges in parallel. + $multi_curl->start(); + + // Remove existing download file name at destination. + if (is_file($filename)) { + unlink($filename); + } + + // Combine downloaded chunks into a single file. + $main_file_handle = fopen($filename, 'w'); + if ($main_file_handle === false) { + return false; + } + + foreach ($part_file_names as $part_file_name) { + if (!is_file($part_file_name)) { + return false; + } + + $file_handle = fopen($part_file_name, 'r'); + if ($file_handle === false) { + return false; + } + + stream_copy_to_stream($file_handle, $main_file_handle); + fclose($file_handle); + unlink($part_file_name); + } + + fclose($main_file_handle); + + return true; } /** * Exec * - * @access public - * @param $ch - * - * @return string + * @param $ch + * @return mixed Returns the value provided by parseResponse. */ public function exec($ch = null) { - $this->responseCookies = array(); - if (!($ch === null)) { - $this->rawResponse = curl_multi_getcontent($ch); - } else { - $this->call($this->beforeSendFunction); + $this->attempts += 1; + + if ($this->jsonDecoder === null) { + $this->setDefaultJsonDecoder(); + } + if ($this->xmlDecoder === null) { + $this->setDefaultXmlDecoder(); + } + + if ($ch === null) { + $this->responseCookies = []; + $this->call($this->beforeSendCallback); $this->rawResponse = curl_exec($this->curl); $this->curlErrorCode = curl_errno($this->curl); + $this->curlErrorMessage = curl_error($this->curl); + } else { + $this->rawResponse = curl_multi_getcontent($ch); + $this->curlErrorMessage = curl_error($ch); + } + $this->curlError = $this->curlErrorCode !== 0; + + // Ensure Curl::rawResponse is a string as curl_exec() can return false. + // Without this, calling strlen($curl->rawResponse) will error when the + // strict types setting is enabled. + if (!is_string($this->rawResponse)) { + $this->rawResponse = ''; + } + + // Transfer the header callback data and release the temporary store to avoid memory leak. + $this->rawResponseHeaders = $this->headerCallbackData->rawResponseHeaders; + $this->responseCookies = $this->headerCallbackData->responseCookies; + $this->headerCallbackData->rawResponseHeaders = ''; + $this->headerCallbackData->responseCookies = []; + $this->headerCallbackData->stopRequestDecider = null; + $this->headerCallbackData->stopRequest = false; + + // Include additional error code information in error message when possible. + if ($this->curlError) { + $curl_error_message = curl_strerror($this->curlErrorCode); + + if (isset($this->curlErrorCodeConstant)) { + $curl_error_message .= ' (' . $this->curlErrorCodeConstant . ')'; + } + + if (!empty($this->curlErrorMessage)) { + $curl_error_message .= ': ' . $this->curlErrorMessage; + } + + $this->curlErrorMessage = $curl_error_message; } - $this->curlErrorMessage = curl_error($this->curl); - $this->curlError = !($this->curlErrorCode === 0); - $this->httpStatusCode = curl_getinfo($this->curl, CURLINFO_HTTP_CODE); - $this->httpError = in_array(floor($this->httpStatusCode / 100), array(4, 5)); - $this->error = $this->curlError || $this->httpError; - $this->errorCode = $this->error ? ($this->curlError ? $this->curlErrorCode : $this->httpStatusCode) : 0; // NOTE: CURLINFO_HEADER_OUT set to true is required for requestHeaders // to not be empty (e.g. $curl->setOpt(CURLINFO_HEADER_OUT, true);). if ($this->getOpt(CURLINFO_HEADER_OUT) === true) { - $this->requestHeaders = $this->parseRequestHeaders(curl_getinfo($this->curl, CURLINFO_HEADER_OUT)); + $this->requestHeaders = $this->parseRequestHeaders($this->getInfo(CURLINFO_HEADER_OUT)); } $this->responseHeaders = $this->parseResponseHeaders($this->rawResponseHeaders); - list($this->response, $this->rawResponse) = $this->parseResponse($this->responseHeaders, $this->rawResponse); + $this->response = $this->parseResponse($this->responseHeaders, $this->rawResponse); + + $this->httpStatusCode = $this->getInfo(CURLINFO_HTTP_CODE); + $this->httpError = in_array((int) floor($this->httpStatusCode / 100), [4, 5], true); + $this->error = $this->curlError || $this->httpError; + + $this->call($this->afterSendCallback); + + if (!in_array($this->error, [true, false], true)) { + trigger_error('$instance->error MUST be set to true or false', E_USER_WARNING); + } + + $this->errorCode = $this->error ? ($this->curlError ? $this->curlErrorCode : $this->httpStatusCode) : 0; $this->httpErrorMessage = ''; if ($this->error) { @@ -369,249 +558,320 @@ public function exec($ch = null) } $this->errorMessage = $this->curlError ? $this->curlErrorMessage : $this->httpErrorMessage; - if (!$this->error) { - $this->call($this->successFunction); - } else { - $this->call($this->errorFunction); + // Reset select deferred properties so that they may be recalculated. + unset($this->deferredValues['curlErrorCodeConstant']); + unset($this->deferredValues['effectiveUrl']); + unset($this->deferredValues['totalTime']); + + // Reset content-length header possibly set from a PUT or SEARCH request. + $this->unsetHeader('Content-Length'); + + // Reset nobody setting possibly set from a HEAD request. + $this->setOptInternal(CURLOPT_NOBODY, false); + + // Allow multicurl to attempt retry as needed. + if ($this->isChildOfMultiCurl()) { + return; + } + + if ($this->attemptRetry()) { + return $this->exec($ch); } - $this->call($this->completeFunction); + $this->execDone(); return $this->response; } + public function execDone() + { + if ($this->error) { + $this->call($this->errorCallback); + } else { + $this->call($this->successCallback); + } + + $this->call($this->completeCallback); + + // Close open file handles and reset the curl instance. + if ($this->fileHandle !== null) { + $this->downloadComplete($this->fileHandle); + } + } + /** * Get * - * @access public - * @param $url - * @param $data - * - * @return string + * @param $url + * @param $data + * @return mixed Returns the value provided by exec. */ - public function get($url, $data = array()) + public function get($url, $data = []) + { + $this->setGet($url, $data); + return $this->exec(); + } + + public function setGet($url, $data = []) { if (is_array($url)) { $data = $url; - $url = $this->baseUrl; + $url = (string)$this->url; } - $this->setURL($url, $data); - $this->setOpt(CURLOPT_CUSTOMREQUEST, 'GET'); - $this->setOpt(CURLOPT_HTTPGET, true); - return $this->exec(); + $this->setUrl($url, $data); + $this->setOptInternal(CURLOPT_CUSTOMREQUEST, 'GET'); + $this->setOptInternal(CURLOPT_HTTPGET, true); } /** - * Get Opt - * - * @access public - * @param $option + * Get Info * + * @param $opt * @return mixed */ - public function getOpt($option) + public function getInfo($opt = null) { - return $this->options[$option]; + $args = []; + $args[] = $this->curl; + + if (func_num_args()) { + $args[] = $opt; + } + + return call_user_func_array('curl_getinfo', $args); } /** * Head * - * @access public - * @param $url - * @param $data - * - * @return string + * @param $url + * @param $data + * @return mixed Returns the value provided by exec. */ - public function head($url, $data = array()) + public function head($url, $data = []) + { + $this->setHead($url, $data); + return $this->exec(); + } + + public function setHead($url, $data = []) { if (is_array($url)) { $data = $url; - $url = $this->baseUrl; + $url = (string)$this->url; } - $this->setURL($url, $data); + $this->setUrl($url, $data); $this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD'); $this->setOpt(CURLOPT_NOBODY, true); - return $this->exec(); } /** - * Header Callback - * - * @access public - * @param $ch - * @param $header + * Options * - * @return integer + * @param $url + * @param $data + * @return mixed Returns the value provided by exec. */ - public function headerCallback($ch, $header) + public function options($url, $data = []) { - if (preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) == 1) { - $this->responseCookies[$cookie[1]] = $cookie[2]; - } - $this->rawResponseHeaders .= $header; - return strlen($header); + $this->setOptions($url, $data); + return $this->exec(); } - /** - * Options - * - * @access public - * @param $url - * @param $data - * - * @return string - */ - public function options($url, $data = array()) + public function setOptions($url, $data = []) { if (is_array($url)) { $data = $url; - $url = $this->baseUrl; + $url = (string)$this->url; } - $this->setURL($url, $data); - $this->unsetHeader('Content-Length'); + $this->setUrl($url, $data); $this->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS'); - return $this->exec(); } /** * Patch * - * @access public - * @param $url - * @param $data - * - * @return string + * @param $url + * @param $data + * @return mixed Returns the value provided by exec. */ - public function patch($url, $data = array()) + public function patch($url, $data = []) + { + $this->setPatch($url, $data); + return $this->exec(); + } + + public function setPatch($url, $data = []) { if (is_array($url)) { $data = $url; - $url = $this->baseUrl; + $url = (string)$this->url; } if (is_array($data) && empty($data)) { - $this->unsetHeader('Content-Length'); + $this->removeHeader('Content-Length'); } - $this->setURL($url); + $this->setUrl($url); $this->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH'); $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data)); - return $this->exec(); } /** * Post * - * @access public - * @param $url - * @param $data - * - * @return string + * @param $url + * @param $data + * @param $follow_303_with_post + * If true, will cause 303 redirections to be followed using a POST request + * (default: false). + * Notes: + * - Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set + * to true. + * - According to the HTTP specs (see [1]), a 303 redirection should be followed + * using the GET method. 301 and 302 must not. + * - In order to force a 303 redirection to be performed using the same method, + * the underlying cURL object must be set in a special state (the + * CURLOPT_CUSTOMREQUEST option must be set to the method to use after the + * redirection). Due to a limitation of the cURL extension of PHP < 5.5.11 ([2], + * [3]), it is not possible to reset this option. Using these PHP engines, it is + * therefore impossible to restore this behavior on an existing php-curl-class + * Curl object. + * @return mixed Returns the value provided by exec. + * + * [1] https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.2 + * [2] https://github.com/php/php-src/pull/531 + * [3] http://php.net/ChangeLog-5.php#5.5.11 */ - public function post($url, $data = array()) + public function post($url, $data = '', $follow_303_with_post = false) + { + $this->setPost($url, $data, $follow_303_with_post); + return $this->exec(); + } + + public function setPost($url, $data = '', $follow_303_with_post = false) { if (is_array($url)) { + $follow_303_with_post = (bool)$data; $data = $url; - $url = $this->baseUrl; + $url = (string)$this->url; + } + + $this->setUrl($url); + + // Set the request method to "POST" when following a 303 redirect with + // an additional POST request is desired. This is equivalent to setting + // the -X, --request command line option where curl won't change the + // request method according to the HTTP 30x response code. + if ($follow_303_with_post) { + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST'); + } elseif (isset($this->options[CURLOPT_CUSTOMREQUEST])) { + // Unset the CURLOPT_CUSTOMREQUEST option so that curl does not use + // a POST request after a post/redirect/get redirection. Without + // this, curl will use the method string specified for all requests. + $this->setOpt(CURLOPT_CUSTOMREQUEST, null); } - $this->setURL($url); - $this->setOpt(CURLOPT_CUSTOMREQUEST, 'POST'); $this->setOpt(CURLOPT_POST, true); $this->setOpt(CURLOPT_POSTFIELDS, $this->buildPostData($data)); - return $this->exec(); } /** * Put * - * @access public - * @param $url - * @param $data - * - * @return string + * @param $url + * @param $data + * @return mixed Returns the value provided by exec. */ - public function put($url, $data = array()) + public function put($url, $data = []) + { + $this->setPut($url, $data); + return $this->exec(); + } + + public function setPut($url, $data = []) { if (is_array($url)) { $data = $url; - $url = $this->baseUrl; + $url = (string)$this->url; } - $this->setURL($url); + $this->setUrl($url); $this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT'); $put_data = $this->buildPostData($data); if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) { - $this->setHeader('Content-Length', strlen($put_data)); + if (is_string($put_data)) { + $this->setHeader('Content-Length', strlen($put_data)); + } + } + if (!empty($put_data)) { + $this->setOpt(CURLOPT_POSTFIELDS, $put_data); } - $this->setOpt(CURLOPT_POSTFIELDS, $put_data); - return $this->exec(); } /** - * Set Basic Authentication + * Search * - * @access public - * @param $username - * @param $password + * @param $url + * @param $data + * @return mixed Returns the value provided by exec. */ - public function setBasicAuthentication($username, $password = '') + public function search($url, $data = []) { - $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + $this->setSearch($url, $data); + return $this->exec(); } - /** - * Set Digest Authentication - * - * @access public - * @param $username - * @param $password - */ - public function setDigestAuthentication($username, $password = '') + public function setSearch($url, $data = []) { - $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); - $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + if (is_array($url)) { + $data = $url; + $url = (string)$this->url; + } + $this->setUrl($url); + $this->setOpt(CURLOPT_CUSTOMREQUEST, 'SEARCH'); + $put_data = $this->buildPostData($data); + if (empty($this->options[CURLOPT_INFILE]) && empty($this->options[CURLOPT_INFILESIZE])) { + if (is_string($put_data)) { + $this->setHeader('Content-Length', strlen($put_data)); + } + } + if (!empty($put_data)) { + $this->setOpt(CURLOPT_POSTFIELDS, $put_data); + } } /** * Set Cookie * - * @access public - * @param $key - * @param $value + * @param $key + * @param $value */ + #[\Override] public function setCookie($key, $value) { - $name_chars = array(); - foreach (str_split($key) as $name_char) { - if (!array_key_exists($name_char, CurlCookieConst::RFC2616())) { - $name_chars[] = rawurlencode($name_char); - } else { - $name_chars[] = $name_char; - } - } + $this->setEncodedCookie($key, $value); + $this->buildCookies(); + } - $value_chars = array(); - foreach (str_split($value) as $value_char) { - if (!array_key_exists($value_char, CurlCookieConst::RFC6265())) { - $value_chars[] = rawurlencode($value_char); - } else { - $value_chars[] = $value_char; - } + /** + * Set Cookies + * + * @param $cookies + */ + #[\Override] + public function setCookies($cookies) + { + foreach ($cookies as $key => $value) { + $this->setEncodedCookie($key, $value); } - - $this->cookies[implode('', $name_chars)] = implode('', $value_chars); - $this->setOpt(CURLOPT_COOKIE, implode('; ', array_map(function($k, $v) { - return $k . '=' . $v; - }, array_keys($this->cookies), array_values($this->cookies)))); + $this->buildCookies(); } /** - * Get cookie. + * Get Cookie * - * @access public - * @param $key + * @param $key + * @return mixed */ public function getCookie($key) { @@ -619,313 +879,1095 @@ public function getCookie($key) } /** - * Get response cookie. + * Get Response Cookie * - * @access public - * @param $key + * @param $key + * @return mixed */ public function getResponseCookie($key) { - return isset($this->responseCookies[$key]) ? $this->responseCookies[$key] : null; + return $this->responseCookies[$key] ?? null; } /** - * Set Port + * Set Max Filesize * - * @access public - * @param $port + * @param $bytes */ - public function setPort($port) + public function setMaxFilesize($bytes) { - $this->setOpt(CURLOPT_PORT, intval($port)); + $callback = function ($resource, $download_size, $downloaded, $upload_size, $uploaded) use ($bytes) { + // Abort the transfer when $downloaded bytes exceeds maximum $bytes by returning a non-zero value. + return $downloaded > $bytes ? 1 : 0; + }; + $this->progress($callback); } /** - * Set Connect Timeout + * Set Cookie String * - * @access public - * @param $seconds + * @param $string + * @return bool */ - public function setConnectTimeout($seconds) + #[\Override] + public function setCookieString($string) { - $this->setOpt(CURLOPT_CONNECTTIMEOUT, $seconds); + return $this->setOpt(CURLOPT_COOKIE, $string); } /** * Set Cookie File * - * @access public - * @param $cookie_file + * @param $cookie_file + * @return bool */ + #[\Override] public function setCookieFile($cookie_file) { - $this->setOpt(CURLOPT_COOKIEFILE, $cookie_file); + return $this->setOpt(CURLOPT_COOKIEFILE, $cookie_file); } /** * Set Cookie Jar * - * @access public - * @param $cookie_jar + * @param $cookie_jar + * @return bool */ + #[\Override] public function setCookieJar($cookie_jar) { - $this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar); + return $this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar); } /** * Set Default JSON Decoder * - * @access public + * @param $assoc + * @param $depth + * @param $options */ public function setDefaultJsonDecoder() { - $this->jsonDecoder = function($response) { - $json_obj = json_decode($response, false); - if (!($json_obj === null)) { - $response = $json_obj; - } - return $response; - }; + $this->jsonDecoder = '\Curl\Decoder::decodeJson'; + $this->jsonDecoderArgs = func_get_args(); } /** - * Set Default Timeout + * Set Default XML Decoder * - * @access public + * @param $class_name + * @param $options + * @param $ns + * @param $is_prefix */ - public function setDefaultTimeout() + public function setDefaultXmlDecoder() { - $this->setTimeout(self::DEFAULT_TIMEOUT); + $this->xmlDecoder = '\Curl\Decoder::decodeXml'; + $this->xmlDecoderArgs = func_get_args(); } /** - * Set Default User Agent + * Set Default Decoder * - * @access public + * @param $mixed boolean|callable|string */ - public function setDefaultUserAgent() + public function setDefaultDecoder($mixed = 'json') { - $user_agent = 'PHP-Curl-Class/' . self::VERSION . ' (+https://github.com/php-curl-class/php-curl-class)'; - $user_agent .= ' PHP/' . PHP_VERSION; - $curl_version = curl_version(); - $user_agent .= ' curl/' . $curl_version['version']; - $this->setUserAgent($user_agent); + if ($mixed === false) { + $this->defaultDecoder = false; + } elseif ($mixed === 'json') { + $this->defaultDecoder = '\Curl\Decoder::decodeJson'; + } elseif ($mixed === 'xml') { + $this->defaultDecoder = '\Curl\Decoder::decodeXml'; + } elseif (is_callable($mixed)) { + $this->defaultDecoder = $mixed; + } + } + + /** + * Set Default Header Out + */ + public function setDefaultHeaderOut() + { + $this->setOpt(CURLINFO_HEADER_OUT, true); + } + + private function setDefaultHeaderOutInternal() + { + $this->setOptInternal(CURLINFO_HEADER_OUT, true); + } + + /** + * Set Default Timeout + */ + public function setDefaultTimeout() + { + $this->setTimeout(self::DEFAULT_TIMEOUT); + } + + private function setDefaultTimeoutInternal() + { + $this->setTimeoutInternal(self::DEFAULT_TIMEOUT); + } + + /** + * Set Default User Agent + */ + public function setDefaultUserAgent() + { + $this->setUserAgent($this->getDefaultUserAgent()); + } + + private function setDefaultUserAgentInternal() + { + $this->setUserAgentInternal($this->getDefaultUserAgent()); + } + + private function getDefaultUserAgent() + { + $user_agent = 'PHP-Curl-Class/' . self::VERSION . ' (+https://github.com/php-curl-class/php-curl-class)'; + $user_agent .= ' PHP/' . PHP_VERSION; + $curl_version = curl_version(); + $user_agent .= ' curl/' . $curl_version['version']; + return $user_agent; } /** * Set Header * - * @access public - * @param $key - * @param $value + * Add extra header to include in the request. * - * @return string + * @param $key + * @param $value */ + #[\Override] public function setHeader($key, $value) { $this->headers[$key] = $value; - $headers = array(); + $headers = []; + foreach ($this->headers as $key => $value) { + $headers[] = $key . ': ' . $value; + } + $this->setOpt(CURLOPT_HTTPHEADER, $headers); + } + + /** + * Set Headers + * + * Add extra headers to include in the request. + * + * @param $headers + */ + #[\Override] + public function setHeaders($headers) + { + if (ArrayUtil::isArrayAssoc($headers)) { + foreach ($headers as $key => $value) { + $key = trim($key); + $value = trim($value); + $this->headers[$key] = $value; + } + } else { + foreach ($headers as $header) { + list($key, $value) = array_pad(explode(':', $header, 2), 2, ''); + $key = trim($key); + $value = trim($value); + $this->headers[$key] = $value; + } + } + + $headers = []; foreach ($this->headers as $key => $value) { $headers[] = $key . ': ' . $value; } + $this->setOpt(CURLOPT_HTTPHEADER, $headers); } /** * Set JSON Decoder * - * @access public - * @param $function + * @param $mixed boolean|callable */ - public function setJsonDecoder($function) + #[\Override] + public function setJsonDecoder($mixed) { - if (is_callable($function)) { - $this->jsonDecoder = $function; + if ($mixed === false || is_callable($mixed)) { + $this->jsonDecoder = $mixed; + $this->jsonDecoderArgs = []; } } /** - * Set Opt + * Set XML Decoder * - * @access public - * @param $option - * @param $value + * @param $mixed boolean|callable + */ + #[\Override] + public function setXmlDecoder($mixed) + { + if ($mixed === false || is_callable($mixed)) { + $this->xmlDecoder = $mixed; + $this->xmlDecoderArgs = []; + } + } + + /** + * Set Opt * - * @return boolean + * @param $option + * @param $value + * @return bool */ + #[\Override] public function setOpt($option, $value) { - $required_options = array( + $required_options = [ CURLOPT_RETURNTRANSFER => 'CURLOPT_RETURNTRANSFER', - ); + ]; - if (in_array($option, array_keys($required_options), true) && !($value === true)) { + if (in_array($option, array_keys($required_options), true) && $value !== true) { trigger_error($required_options[$option] . ' is a required option', E_USER_WARNING); } - $this->options[$option] = $value; - return curl_setopt($this->curl, $option, $value); + $success = curl_setopt($this->curl, $option, $value); + if ($success) { + $this->options[$option] = $value; + $this->userSetOptions[$option] = $value; + } + return $success; } /** - * Set Referer + * Set Opt Internal * - * @access public - * @param $referer + * @param $option + * @param $value + * @return bool */ - public function setReferer($referer) + #[\Override] + protected function setOptInternal($option, $value) { - $this->setReferrer($referer); + $success = curl_setopt($this->curl, $option, $value); + if ($success) { + $this->options[$option] = $value; + } + return $success; } /** - * Set Referrer - * - * @access public - * @param $referrer + * Set Opts + * + * @param $options + * @return bool + * Returns true if all options were successfully set. If an + * option could not be successfully set, false is immediately + * returned, ignoring any future options in the options array. + * Similar to curl_setopt_array(). */ - public function setReferrer($referrer) + #[\Override] + public function setOpts($options) { - $this->setOpt(CURLOPT_REFERER, $referrer); + if (!count($options)) { + return true; + } + foreach ($options as $option => $value) { + if (!$this->setOpt($option, $value)) { + return false; + } + } + return true; } /** - * Set Timeout + * Set Protocols + * + * Limit what protocols libcurl will accept for a request. * - * @access public - * @param $seconds + * @param $protocols + * @see Curl::setRedirectProtocols() */ - public function setTimeout($seconds) + public function setProtocols($protocols) + { + $this->setOpt(CURLOPT_PROTOCOLS, $protocols); + } + + private function setProtocolsInternal($protocols) { - $this->setOpt(CURLOPT_TIMEOUT, $seconds); + $this->setOptInternal(CURLOPT_PROTOCOLS, $protocols); } /** - * Set Url + * Set Retry + * + * Number of retries to attempt or decider callable. + * + * When using a number of retries to attempt, the maximum number of attempts + * for the request is $maximum_number_of_retries + 1. * - * @access public - * @param $url - * @param $data + * When using a callable decider, the request will be retried until the + * function returns a value which evaluates to false. + * + * @param $mixed */ - public function setURL($url, $data = array()) + #[\Override] + public function setRetry($mixed) { - $this->baseUrl = $url; - $this->url = $this->buildURL($url, $data); - $this->setOpt(CURLOPT_URL, $this->url); + if (is_callable($mixed)) { + $this->retryDecider = $mixed; + } elseif (is_int($mixed)) { + $maximum_number_of_retries = $mixed; + $this->remainingRetries = $maximum_number_of_retries; + } } /** - * Set User Agent + * Set Redirect Protocols * - * @access public - * @param $user_agent + * Limit what protocols libcurl will accept when following a redirect. + * + * @param $redirect_protocols + * @see Curl::setProtocols() */ - public function setUserAgent($user_agent) + public function setRedirectProtocols($redirect_protocols) + { + $this->setOpt(CURLOPT_REDIR_PROTOCOLS, $redirect_protocols); + } + + private function setRedirectProtocolsInternal($redirect_protocols) { - $this->setOpt(CURLOPT_USERAGENT, $user_agent); + $this->setOptInternal(CURLOPT_REDIR_PROTOCOLS, $redirect_protocols); } /** - * Success + * Set Url * - * @access public - * @param $callback + * @param $url + * @param $mixed_data */ - public function success($callback) + #[\Override] + public function setUrl($url, $mixed_data = '') { - $this->successFunction = $callback; + $built_url = Url::buildUrl($url, $mixed_data); + + if ($this->url === null) { + $this->url = (string)new Url($built_url); + } else { + $this->url = (string)new Url($this->url, $built_url); + } + + $this->setOpt(CURLOPT_URL, $this->url); + } + + /** + * Attempt Retry + */ + public function attemptRetry() + { + $attempt_retry = false; + if ($this->error) { + if ($this->retryDecider === null) { + $attempt_retry = $this->remainingRetries >= 1; + } else { + $attempt_retry = call_user_func($this->retryDecider, $this); + } + if ($attempt_retry) { + $this->retries += 1; + if ($this->remainingRetries) { + $this->remainingRetries -= 1; + } + } + } + return $attempt_retry; } /** * Unset Header * - * @access public - * @param $key + * Remove extra header previously set using Curl::setHeader(). + * + * @param $key */ + #[\Override] public function unsetHeader($key) { - $this->setHeader($key, ''); unset($this->headers[$key]); + $headers = []; + foreach ($this->headers as $key => $value) { + $headers[] = $key . ': ' . $value; + } + $this->setOpt(CURLOPT_HTTPHEADER, $headers); } /** - * Verbose + * Diagnose * - * @access public - * @param $on + * @param bool $return */ - public function verbose($on = true, $output=STDERR) + public function diagnose($return = false) { - // Turn off CURLINFO_HEADER_OUT for verbose to work. This has the side - // effect of causing Curl::requestHeaders to be empty. - if ($on) { - $this->setOpt(CURLINFO_HEADER_OUT, false); + if ($return) { + ob_start(); + } + + echo "\n"; + echo '--- Begin PHP Curl Class diagnostic output ---' . "\n"; + echo 'PHP Curl Class version: ' . self::VERSION . "\n"; + echo 'PHP version: ' . PHP_VERSION . "\n"; + + $curl_version = curl_version(); + echo 'Curl version: ' . $curl_version['version'] . "\n"; + + if ($this->attempts === 0) { + echo 'No HTTP requests have been made.' . "\n"; + } else { + $request_types = [ + 'DELETE' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'DELETE', + 'GET' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'GET' || $this->getOpt(CURLOPT_HTTPGET), + 'HEAD' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'HEAD', + 'OPTIONS' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'OPTIONS', + 'PATCH' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'PATCH', + 'POST' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'POST' || $this->getOpt(CURLOPT_POST), + 'PUT' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'PUT', + 'SEARCH' => $this->getOpt(CURLOPT_CUSTOMREQUEST) === 'SEARCH', + ]; + $request_method = ''; + foreach ($request_types as $http_method_name => $http_method_used) { + if ($http_method_used) { + $request_method = $http_method_name; + break; + } + } + $request_url = $this->getOpt(CURLOPT_URL); + $request_options_count = count($this->options); + $request_headers_count = count($this->requestHeaders); + $request_body_empty = empty($this->getOpt(CURLOPT_POSTFIELDS)); + $response_header_length = $this->responseHeaders['Content-Length'] ?? '(not specified in response header)'; + $response_calculated_length = is_string($this->rawResponse) ? + strlen($this->rawResponse) : '(' . var_export($this->rawResponse, true) . ')'; + $response_headers_count = count($this->responseHeaders); + + echo + 'Request contained ' . (string)$request_options_count . ' ' . ( + $request_options_count === 1 ? 'option:' : 'options:' + ) . "\n"; + if ($request_options_count) { + $i = 1; + foreach ($this->options as $option => $value) { + echo ' ' . (string)$i . ' '; + $this->displayCurlOptionValue($option, $value); + $i += 1; + } + } + + echo + 'Sent an HTTP ' . $request_method . ' request to "' . $request_url . '".' . "\n" . + 'Request contained ' . (string)$request_headers_count . ' ' . ( + $request_headers_count === 1 ? 'header:' : 'headers:' + ) . "\n"; + if ($request_headers_count) { + $i = 1; + foreach ($this->requestHeaders as $key => $value) { + echo ' ' . (string)$i . ' ' . $key . ': ' . $value . "\n"; + $i += 1; + } + } + + echo 'Request contained ' . ($request_body_empty ? 'no body' : 'a body') . '.' . "\n"; + + if ( + $request_headers_count === 0 && ( + $this->getOpt(CURLOPT_VERBOSE) || + !$this->getOpt(CURLINFO_HEADER_OUT) + ) + ) { + echo + 'Warning: Request headers (Curl::requestHeaders) are expected to be empty ' . + '(CURLOPT_VERBOSE was enabled or CURLINFO_HEADER_OUT was disabled).' . "\n"; + } + + if (isset($this->responseHeaders['allow'])) { + $allowed_request_types = array_map(function ($v) { + return trim($v); + }, explode(',', strtoupper($this->responseHeaders['allow']))); + foreach ($request_types as $http_method_name => $http_method_used) { + if ($http_method_used && !in_array($http_method_name, $allowed_request_types, true)) { + echo + 'Warning: An HTTP ' . $http_method_name . ' request was made, but only the following ' . + 'request types are allowed: ' . implode(', ', $allowed_request_types) . "\n"; + } + } + } + + echo + 'Response contains ' . (string)$response_headers_count . ' ' . ( + $response_headers_count === 1 ? 'header:' : 'headers:' + ) . "\n"; + if ($this->responseHeaders !== null) { + $i = 1; + foreach ($this->responseHeaders as $key => $value) { + echo ' ' . (string)$i . ' ' . $key . ': ' . $value . "\n"; + $i += 1; + } + } + + if (!isset($this->responseHeaders['Content-Type'])) { + echo 'Response did not set a content type.' . "\n"; + } elseif (preg_match($this->jsonPattern, $this->responseHeaders['Content-Type'])) { + echo 'Response appears to be JSON.' . "\n"; + } elseif (preg_match($this->xmlPattern, $this->responseHeaders['Content-Type'])) { + echo 'Response appears to be XML.' . "\n"; + } + + if ($this->curlError) { + echo + 'A curl error (' . $this->curlErrorCode . ') occurred ' . + 'with message "' . $this->curlErrorMessage . '".' . "\n"; + } + if (!empty($this->httpStatusCode)) { + echo 'Received an HTTP status code of ' . $this->httpStatusCode . '.' . "\n"; + } + if ($this->httpError) { + echo + 'Received an HTTP ' . $this->httpStatusCode . ' error response ' . + 'with message "' . $this->httpErrorMessage . '".' . "\n"; + } + + if ($this->rawResponse === null) { + echo 'Received no response body (response=null).' . "\n"; + } elseif ($this->rawResponse === '') { + echo 'Received an empty response body (response="").' . "\n"; + } else { + echo 'Received a non-empty response body.' . "\n"; + if (isset($this->responseHeaders['Content-Length'])) { + echo 'Response content length (from content-length header): ' . $response_header_length . "\n"; + } else { + echo 'Response content length (calculated): ' . (string)$response_calculated_length . "\n"; + } + + if ( + isset($this->responseHeaders['Content-Type']) && + preg_match($this->jsonPattern, $this->responseHeaders['Content-Type']) + ) { + $parsed_response = json_decode($this->rawResponse, true); + if ($parsed_response !== null) { + $messages = []; + array_walk_recursive($parsed_response, function ($value, $key) use (&$messages) { + if (in_array($key, ['code', 'error', 'message'], true)) { + $message = $key . ': ' . $value; + $messages[] = $message; + } + }); + $messages = array_unique($messages); + + $messages_count = count($messages); + if ($messages_count) { + echo + 'Found ' . (string)$messages_count . ' ' . + ($messages_count === 1 ? 'message' : 'messages') . + ' in response:' . "\n"; + + $i = 1; + foreach ($messages as $message) { + echo ' ' . (string)$i . ' ' . $message . "\n"; + $i += 1; + } + } + } + } + } + } + + echo '--- End PHP Curl Class diagnostic output ---' . "\n"; + echo "\n"; + + if ($return) { + $output = ob_get_contents(); + ob_end_clean(); + return $output; } - $this->setOpt(CURLOPT_VERBOSE, $on); - $this->setOpt(CURLOPT_STDERR, $output); + } + + /** + * Reset + */ + public function reset() + { + if (is_resource($this->curl) || $this->curl instanceof \CurlHandle) { + curl_reset($this->curl); + } else { + $this->curl = curl_init(); + } + + $this->setDefaultUserAgentInternal(); + $this->setDefaultTimeoutInternal(); + $this->setDefaultHeaderOutInternal(); + + $this->initialize(); + } + + public function getCurl() + { + return $this->curl; + } + + public function getId() + { + return $this->id; + } + + public function isError() + { + return $this->error; + } + + public function getErrorCode() + { + return $this->errorCode; + } + + public function getErrorMessage() + { + return $this->errorMessage; + } + + public function isCurlError() + { + return $this->curlError; + } + + public function getCurlErrorCode() + { + return $this->curlErrorCode; + } + + public function getCurlErrorMessage() + { + return $this->curlErrorMessage; + } + + public function isHttpError() + { + return $this->httpError; + } + + public function getHttpStatusCode() + { + return $this->httpStatusCode; + } + + public function getHttpErrorMessage() + { + return $this->httpErrorMessage; + } + + public function getUrl() + { + return $this->url; + } + + public function getOptions() + { + return $this->options; + } + + public function getUserSetOptions() + { + return $this->userSetOptions; + } + + public function getRequestHeaders() + { + return $this->requestHeaders; + } + + public function getResponseHeaders() + { + return $this->responseHeaders; + } + + public function getRawResponseHeaders() + { + return $this->rawResponseHeaders; + } + + public function getResponseCookies() + { + return $this->responseCookies; + } + + public function getResponse() + { + return $this->response; + } + + public function getRawResponse() + { + return $this->rawResponse; + } + + public function getBeforeSendCallback() + { + return $this->beforeSendCallback; + } + + public function getDownloadCompleteCallback() + { + return $this->downloadCompleteCallback; + } + + public function getDownloadFileName() + { + return $this->downloadFileName; + } + + public function getSuccessCallback() + { + return $this->successCallback; + } + + public function getErrorCallback() + { + return $this->errorCallback; + } + + public function getCompleteCallback() + { + return $this->completeCallback; + } + + public function getFileHandle() + { + return $this->fileHandle; + } + + public function getAttempts() + { + return $this->attempts; + } + + public function getRetries() + { + return $this->retries; + } + + public function isChildOfMultiCurl() + { + return $this->childOfMultiCurl; + } + + public function getRemainingRetries() + { + return $this->remainingRetries; + } + + public function getRetryDecider() + { + return $this->retryDecider; + } + + public function getJsonDecoder() + { + return $this->jsonDecoder; + } + + public function getXmlDecoder() + { + return $this->xmlDecoder; } /** * Destruct - * - * @access public */ public function __destruct() { $this->close(); } + public function __get($name) + { + if (in_array($name, self::$deferredProperties, true)) { + if (isset($this->deferredValues[$name])) { + return $this->deferredValues[$name]; + } elseif (is_callable([$this, $getter = 'get' . ucfirst($name)])) { + $this->deferredValues[$name] = $this->$getter(); + return $this->deferredValues[$name]; + } + } + + return null; + } + + public function __isset($name) + { + if (in_array($name, self::$deferredProperties, true)) { + if (isset($this->deferredValues[$name])) { + return true; + } elseif (is_callable([$this, $getter = 'get' . ucfirst($name)])) { + $this->deferredValues[$name] = $this->$getter(); + return true; + } else { + return false; + } + } + + return isset($this->$name); + } + + /** + * Get Curl Error Code Constants + */ + private function getCurlErrorCodeConstants() + { + $constants = get_defined_constants(true); + $filtered_array = array_filter( + $constants['curl'], + function ($key) { + return strpos($key, 'CURLE_') !== false; + }, + ARRAY_FILTER_USE_KEY + ); + $curl_const_by_code = array_flip($filtered_array); + return $curl_const_by_code; + } + /** - * Build Url + * Get Curl Error Code Constant + */ + private function getCurlErrorCodeConstant() + { + $curl_const_by_code = $this->curlErrorCodeConstants ?? []; + if (isset($curl_const_by_code[$this->curlErrorCode])) { + return $curl_const_by_code[$this->curlErrorCode]; + } + return ''; + } + + /** + * Get Curl Option Code Constants + */ + private function getCurlOptionCodeConstants() + { + $constants = get_defined_constants(true); + $filtered_array = array_filter( + $constants['curl'], + function ($key) { + return strpos($key, 'CURLOPT_') !== false; + }, + ARRAY_FILTER_USE_KEY + ); + $curl_const_by_code = array_flip($filtered_array); + + if (!isset($curl_const_by_code[CURLINFO_HEADER_OUT])) { + $curl_const_by_code[CURLINFO_HEADER_OUT] = 'CURLINFO_HEADER_OUT'; + } + + return $curl_const_by_code; + } + + /** + * Display Curl Option Value. * - * @access private - * @param $url - * @param $data + * @param $option + * @param $value + */ + public function displayCurlOptionValue($option, $value = null) + { + if ($value === null) { + $value = $this->getOpt($option); + } + + if (isset($this->curlOptionCodeConstants[$option])) { + echo $this->curlOptionCodeConstants[$option] . ':'; + } else { + echo $option . ':'; + } + + if (is_string($value)) { + echo ' "' . $value . '"' . "\n"; + } elseif (is_int($value)) { + echo ' ' . (string)$value; + + $bit_flag_lookups = [ + 'CURLOPT_HTTPAUTH' => 'CURLAUTH_', + 'CURLOPT_PROTOCOLS' => 'CURLPROTO_', + 'CURLOPT_PROXYAUTH' => 'CURLAUTH_', + 'CURLOPT_PROXY_SSL_OPTIONS' => 'CURLSSLOPT_', + 'CURLOPT_REDIR_PROTOCOLS' => 'CURLPROTO_', + 'CURLOPT_SSH_AUTH_TYPES' => 'CURLSSH_AUTH_', + 'CURLOPT_SSL_OPTIONS' => 'CURLSSLOPT_', + ]; + if (isset($this->curlOptionCodeConstants[$option])) { + $option_name = $this->curlOptionCodeConstants[$option]; + if (in_array($option_name, array_keys($bit_flag_lookups), true)) { + $curl_const_prefix = $bit_flag_lookups[$option_name]; + $constants = get_defined_constants(true); + $curl_constants = array_filter( + $constants['curl'], + function ($key) use ($curl_const_prefix) { + return strpos($key, $curl_const_prefix) !== false; + }, + ARRAY_FILTER_USE_KEY + ); + + $bit_flags = []; + foreach ($curl_constants as $const_name => $const_value) { + // Attempt to detect bit flags in use that use constants with negative values (e.g. + // CURLAUTH_ANY, CURLAUTH_ANYSAFE, CURLPROTO_ALL, CURLSSH_AUTH_ANY, + // CURLSSH_AUTH_DEFAULT, etc.) + if ($value < 0 && $value === $const_value) { + $bit_flags[] = $const_name; + break; + } elseif ($value >= 0 && $const_value >= 0 && ($value & $const_value)) { + $bit_flags[] = $const_name; + } + } + + if (count($bit_flags)) { + asort($bit_flags); + echo ' (' . implode(' | ', $bit_flags) . ')'; + } + } + } + + echo "\n"; + } elseif (is_bool($value)) { + echo ' ' . ($value ? 'true' : 'false') . "\n"; + } elseif (is_array($value)) { + echo ' '; + var_dump($value); + } elseif (is_callable($value)) { + echo ' (callable)' . "\n"; + } else { + echo ' ' . gettype($value) . ':' . "\n"; + var_dump($value); + } + } + + /** + * Get Effective Url + */ + private function getEffectiveUrl() + { + return $this->getInfo(CURLINFO_EFFECTIVE_URL); + } + + /** + * Get RFC 2616 + */ + private function getRfc2616() + { + return array_fill_keys(self::$RFC2616, true); + } + + /** + * Get RFC 6265 + */ + private function getRfc6265() + { + return array_fill_keys(self::$RFC6265, true); + } + + /** + * Get Total Time + */ + private function getTotalTime() + { + return $this->getInfo(CURLINFO_TOTAL_TIME); + } + + /** + * Build Cookies + */ + private function buildCookies() + { + // Avoid changing CURLOPT_COOKIE if there are no cookies set. + if (count($this->cookies)) { + // Avoid using http_build_query() as unnecessary encoding is performed. + // http_build_query($this->cookies, '', '; '); + $cookies = []; + foreach ($this->cookies as $key => $value) { + $cookies[] = $key . '=' . $value; + } + $this->setOpt(CURLOPT_COOKIE, implode('; ', $cookies)); + } + } + + /** + * Download Complete * - * @return string + * @param $fh */ - private function buildURL($url, $data = array()) + private function downloadComplete($fh) { - return $url . (empty($data) ? '' : '?' . http_build_query($data)); + if ($this->error && is_file((string) $this->downloadFileName)) { + @unlink($this->downloadFileName); + } elseif (!$this->error && $this->downloadCompleteCallback) { + rewind($fh); + $this->call($this->downloadCompleteCallback, $fh); + $this->downloadCompleteCallback = null; + } + + if (is_resource($fh)) { + fclose($fh); + } + + // Fix "PHP Notice: Use of undefined constant STDOUT" when reading the + // PHP script from stdin. Using null causes "Warning: curl_setopt(): + // supplied argument is not a valid File-Handle resource". + if (defined('STDOUT')) { + $output = STDOUT; + } else { + $output = fopen('php://stdout', 'w'); + } + + // Reset CURLOPT_FILE with STDOUT to avoid: "curl_exec(): CURLOPT_FILE + // resource has gone away, resetting to default". + $this->setFile($output); + + // Reset CURLOPT_RETURNTRANSFER to tell cURL to return subsequent + // responses as the return value of curl_exec(). Without this, + // curl_exec() will revert to returning boolean values. + $this->setOpt(CURLOPT_RETURNTRANSFER, true); } /** * Parse Headers * - * @access private - * @param $raw_headers - * + * @param $raw_headers * @return array */ private function parseHeaders($raw_headers) { - $raw_headers = preg_split('/\r\n/', $raw_headers, null, PREG_SPLIT_NO_EMPTY); $http_headers = new CaseInsensitiveArray(); + $raw_headers = preg_split('/\r\n/', (string) $raw_headers, -1, PREG_SPLIT_NO_EMPTY); + if ($raw_headers === false) { + return ['', $http_headers]; + } $raw_headers_count = count($raw_headers); for ($i = 1; $i < $raw_headers_count; $i++) { - list($key, $value) = explode(':', $raw_headers[$i], 2); - $key = trim($key); - $value = trim($value); - // Use isset() as array_key_exists() and ArrayAccess are not compatible. - if (isset($http_headers[$key])) { - $http_headers[$key] .= ',' . $value; - } else { - $http_headers[$key] = $value; + if (strpos($raw_headers[$i], ':') !== false) { + list($key, $value) = array_pad(explode(':', $raw_headers[$i], 2), 2, ''); + $key = trim($key); + $value = trim($value); + // Use isset() as array_key_exists() and ArrayAccess are not compatible. + if (isset($http_headers[$key])) { + $http_headers[$key] .= ',' . $value; + } else { + $http_headers[$key] = $value; + } } } - return array(isset($raw_headers['0']) ? $raw_headers['0'] : '', $http_headers); + return [$raw_headers['0'] ?? '', $http_headers]; } /** * Parse Request Headers * - * @access private - * @param $raw_headers - * - * @return array + * @param $raw_headers + * @return \Curl\CaseInsensitiveArray */ private function parseRequestHeaders($raw_headers) { @@ -941,46 +1983,96 @@ private function parseRequestHeaders($raw_headers) /** * Parse Response * - * @access private - * @param $response_headers - * @param $raw_response + * @param $response_headers + * @param $raw_response + * @return mixed + * If the response content-type is json: Returns the json decoder's return value: A stdClass object + * when the default json decoder is used. * - * @return array + * If the response content-type is xml: Returns the xml decoder's return value: A SimpleXMLElement + * object when the default xml decoder is used. + * + * If the response content-type is something else: Returns the original raw response unless a default + * decoder has been set. + * + * If the response content-type cannot be determined: Returns the original raw response. + * + * If the response content-encoding is gzip: Returns the response gzip-decoded. */ private function parseResponse($response_headers, $raw_response) { $response = $raw_response; if (isset($response_headers['Content-Type'])) { if (preg_match($this->jsonPattern, $response_headers['Content-Type'])) { - $json_decoder = $this->jsonDecoder; - if (is_callable($json_decoder)) { - $response = $json_decoder($response); + if ($this->jsonDecoder) { + $args = $this->jsonDecoderArgs; + array_unshift($args, $response); + $response = call_user_func_array($this->jsonDecoder, $args); } } elseif (preg_match($this->xmlPattern, $response_headers['Content-Type'])) { - $xml_obj = @simplexml_load_string($response); - if (!($xml_obj === false)) { - $response = $xml_obj; + if ($this->xmlDecoder) { + $args = $this->xmlDecoderArgs; + array_unshift($args, $response); + $response = call_user_func_array($this->xmlDecoder, $args); + } + } else { + if ($this->defaultDecoder) { + $response = call_user_func($this->defaultDecoder, $response); } } } - return array($response, $raw_response); + if ( + ( + // Ensure that the server says the response is compressed with + // gzip and the response has not already been decoded. Use + // is_string() to ensure that $response is a string being passed + // to mb_strpos() and gzdecode(). Use extension_loaded() to + // ensure that mb_strpos() uses the mbstring extension and not a + // polyfill. + isset($response_headers['Content-Encoding']) && + $response_headers['Content-Encoding'] === 'gzip' && + is_string($response) && + ( + ( + extension_loaded('mbstring') && + mb_strpos($response, "\x1f" . "\x8b" . "\x08", 0, 'US-ASCII') === 0 + ) || + !extension_loaded('mbstring') + ) + ) || ( + // Or ensure that the response looks like it is compressed with + // gzip. Use is_string() to ensure that $response is a string + // being passed to mb_strpos() and gzdecode(). Use + // extension_loaded() to ensure that mb_strpos() uses the + // mbstring extension and not a polyfill. + is_string($response) && + extension_loaded('mbstring') && + mb_strpos($response, "\x1f" . "\x8b" . "\x08", 0, 'US-ASCII') === 0 + ) + ) { + // Use @ to suppress message "Warning gzdecode(): data error". + $decoded_response = @gzdecode($response); + if ($decoded_response !== false) { + $response = $decoded_response; + } + } + + return $response; } /** * Parse Response Headers * - * @access private - * @param $raw_response_headers - * - * @return array + * @param $raw_response_headers + * @return \Curl\CaseInsensitiveArray */ private function parseResponseHeaders($raw_response_headers) { $response_header_array = explode("\r\n\r\n", $raw_response_headers); - $response_header = ''; + $response_header = ''; for ($i = count($response_header_array) - 1; $i >= 0; $i--) { - if (stripos($response_header_array[$i], 'HTTP/') === 0) { + if (isset($response_header_array[$i]) && stripos($response_header_array[$i], 'HTTP/') === 0) { $response_header = $response_header_array[$i]; break; } @@ -996,64 +2088,185 @@ private function parseResponseHeaders($raw_response_headers) } /** - * Http Build Multi Query + * Set Encoded Cookie * - * @access public - * @param $data - * @param $key + * @param $key + * @param $value + */ + private function setEncodedCookie($key, $value) + { + $name_chars = []; + foreach (str_split($key) as $name_char) { + if (isset($this->rfc2616[$name_char])) { + $name_chars[] = $name_char; + } else { + $name_chars[] = rawurlencode($name_char); + } + } + + $value_chars = []; + foreach (str_split($value) as $value_char) { + if (isset($this->rfc6265[$value_char])) { + $value_chars[] = $value_char; + } else { + $value_chars[] = rawurlencode($value_char); + } + } + + $this->cookies[implode('', $name_chars)] = implode('', $value_chars); + } + + /** + * Initialize * - * @return string + * @param $base_url + * @param mixed $options */ - public static function http_build_multi_query($data, $key = null) + private function initialize($base_url = null, $options = []) { - $query = array(); + $this->setProtocolsInternal(CURLPROTO_HTTPS | CURLPROTO_HTTP); + $this->setRedirectProtocolsInternal(CURLPROTO_HTTPS | CURLPROTO_HTTP); - if (empty($data)) { - return $key . '='; + if (isset($options)) { + $this->setOpts($options); } - $is_array_assoc = self::is_array_assoc($data); + $this->id = bin2hex(random_bytes(16)); - foreach ($data as $k => $value) { - if (is_string($value) || is_numeric($value)) { - $brackets = $is_array_assoc ? '[' . $k . ']' : '[]'; - $query[] = urlencode($key === null ? $k : $key . $brackets) . '=' . rawurlencode($value); - } elseif (is_array($value)) { - $nested = $key === null ? $k : $key . '[' . $k . ']'; - $query[] = self::http_build_multi_query($value, $nested); - } + // Only set default user agent if not already set. + if (!array_key_exists(CURLOPT_USERAGENT, $this->options)) { + $this->setDefaultUserAgentInternal(); + } + + // Only set default timeout if not already set. + if (!array_key_exists(CURLOPT_TIMEOUT, $this->options)) { + $this->setDefaultTimeoutInternal(); } - return implode('&', $query); + if (!array_key_exists(CURLINFO_HEADER_OUT, $this->options)) { + $this->setDefaultHeaderOutInternal(); + } + + // Create a placeholder to temporarily store the header callback data. + $header_callback_data = new \stdClass(); + $header_callback_data->rawResponseHeaders = ''; + $header_callback_data->responseCookies = []; + $header_callback_data->stopRequestDecider = null; + $header_callback_data->stopRequest = false; + $this->headerCallbackData = $header_callback_data; + $this->setStopInternal(); + $this->setOptInternal(CURLOPT_HEADERFUNCTION, createHeaderCallback($header_callback_data)); + + $this->setOptInternal(CURLOPT_RETURNTRANSFER, true); + $this->headers = new CaseInsensitiveArray(); + + if ($base_url !== null) { + $this->setUrl($base_url); + } } /** - * Is Array Assoc + * Set Stop + * + * Specify a callable decider to stop the request early without waiting for + * the full response to be received. + * + * The callable is passed two parameters. The first is the cURL resource, + * the second is a string with header data. Both parameters match the + * parameters in the CURLOPT_HEADERFUNCTION callback. * - * @access public - * @param $array + * The callable must return a truthy value for the request to be stopped + * early. * - * @return boolean + * The callable may be set to null to avoid calling the stop request decider + * callback and instead just check the value of stopRequest for attempting + * to stop the request as used by Curl::stop(). + * + * @param $callback callable|null */ - public static function is_array_assoc($array) + public function setStop($callback = null) + { + $this->headerCallbackData->stopRequestDecider = $callback; + $this->headerCallbackData->stopRequest = false; + + $header_callback_data = $this->headerCallbackData; + $this->progress(createStopRequestFunction($header_callback_data)); + } + + private function setStopInternal($callback = null) { - return (bool)count(array_filter(array_keys($array), 'is_string')); + $this->headerCallbackData->stopRequestDecider = $callback; + $this->headerCallbackData->stopRequest = false; + + $header_callback_data = $this->headerCallbackData; + $this->progressInternal(createStopRequestFunction($header_callback_data)); } /** - * Is Array Multidim + * Stop * - * @access public - * @param $array + * Attempt to stop request. * - * @return boolean + * Used by MultiCurl::stop() when making multiple parallel requests. */ - public static function is_array_multidim($array) + #[\Override] + public function stop() { - if (!is_array($array)) { - return false; + $this->headerCallbackData->stopRequest = true; + } +} + +/** + * Create Header Callback + * + * Gather headers and parse cookies as response headers are received. Keep this function separate from the class so that + * unset($curl) automatically calls __destruct() as expected. Otherwise, manually calling $curl->close() will be + * necessary to prevent a memory leak. + * + * @param $header_callback_data + * @return callable + */ +function createHeaderCallback($header_callback_data) +{ + return function ($ch, $header) use ($header_callback_data) { + if (preg_match('/^Set-Cookie:\s*([^=]+)=([^;]+)/mi', $header, $cookie) === 1) { + $header_callback_data->responseCookies[$cookie[1]] = trim($cookie[2], " \n\r\t\0\x0B"); } - return (bool)count(array_filter($array, 'is_array')); - } + if ($header_callback_data->stopRequestDecider !== null) { + $stop_request_decider = $header_callback_data->stopRequestDecider; + if ($stop_request_decider($ch, $header)) { + $header_callback_data->stopRequest = true; + } + } + + $header_callback_data->rawResponseHeaders .= $header; + return strlen($header); + }; +} + +/** + * Create Stop Request Function + * + * Create a function for Curl::progress() that stops a request early when the + * stopRequest flag is on. Keep this function separate from the class to prevent + * a memory leak. + * + * @param $header_callback_data + * @return callable + */ +function createStopRequestFunction($header_callback_data) +{ + return function ( + $resource, + $download_size, + $downloaded, + $upload_size, + $uploaded + ) use ( + $header_callback_data + ) { + // Abort the transfer when the stop request flag has been set by returning a non-zero value. + return $header_callback_data->stopRequest ? 1 : 0; + }; } diff --git a/src/Curl/Decoder.php b/src/Curl/Decoder.php new file mode 100644 index 0000000000..b2aa0417db --- /dev/null +++ b/src/Curl/Decoder.php @@ -0,0 +1,45 @@ +multiCurl = curl_multi_init(); $this->headers = new CaseInsensitiveArray(); - $this->setURL($base_url); + + if ($base_url !== null) { + $this->setUrl($base_url); + } } /** * Add Delete * - * @access public - * @param $url - * @param $query_parameters - * @param $data - * + * @param $url + * @param $query_parameters + * @param $data * @return object */ - public function addDelete($url, $query_parameters = array(), $data = array()) + public function addDelete($url, $query_parameters = [], $data = []) { if (is_array($url)) { $data = $query_parameters; $query_parameters = $url; $url = $this->baseUrl; } - $curl = new Curl(); - $curl->setURL($url, $query_parameters); + + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url, $query_parameters); + $curl->setUrl($url, $query_parameters); $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'DELETE'); $curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data)); - $this->addHandle($curl); return $curl; } /** * Add Download * - * @access public - * @param $url - * @param $mixed_filename - * + * @param $url + * @param $mixed_filename * @return object */ public function addDownload($url, $mixed_filename) { - $curl = new Curl(); - $curl->setURL($url); + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url); + $curl->setUrl($url); + // Use tmpfile() or php://temp to avoid "Too many open files" error. if (is_callable($mixed_filename)) { - $callback = $mixed_filename; - $curl->downloadCompleteFunction = $callback; - $fh = tmpfile(); + $curl->downloadCompleteCallback = $mixed_filename; + $curl->downloadFileName = null; + $curl->fileHandle = tmpfile(); } else { $filename = $mixed_filename; - $fh = fopen($filename, 'wb'); + + // Use a temporary file when downloading. Not using a temporary file can cause an error when an existing + // file has already fully completed downloading and a new download is started with the same destination save + // path. The download request will include header "Range: bytes=$filesize-" which is syntactically valid, + // but unsatisfiable. + $download_filename = $filename . '.pccdownload'; + $curl->downloadFileName = $download_filename; + + // Attempt to resume download only when a temporary download file exists and is not empty. + if (is_file($download_filename) && $filesize = filesize($download_filename)) { + $first_byte_position = $filesize; + $range = (string)$first_byte_position . '-'; + $curl->setRange($range); + $curl->fileHandle = fopen($download_filename, 'ab'); + + // Move the downloaded temporary file to the destination save path. + $curl->downloadCompleteCallback = function ($instance, $fh) use ($download_filename, $filename) { + // Close the open file handle before renaming the file. + if (is_resource($fh)) { + fclose($fh); + } + + rename($download_filename, $filename); + }; + } else { + $curl->fileHandle = fopen('php://temp', 'wb'); + $curl->downloadCompleteCallback = function ($instance, $fh) use ($filename) { + $contents = stream_get_contents($fh); + if ($contents !== false) { + file_put_contents($filename, $contents); + } + }; + } } - $curl->setOpt(CURLOPT_FILE, $fh); + $curl->setFile($curl->fileHandle); $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET'); $curl->setOpt(CURLOPT_HTTPGET, true); - $this->addHandle($curl); - $this->curlFileHandles[$curl->id] = $fh; return $curl; } /** * Add Get * - * @access public - * @param $url - * @param $data - * + * @param $url + * @param $data * @return object */ - public function addGet($url, $data = array()) + public function addGet($url, $data = []) { if (is_array($url)) { $data = $url; $url = $this->baseUrl; } - $curl = new Curl(); - $curl->setURL($url, $data); + + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url, $data); + $curl->setUrl($url, $data); $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET'); $curl->setOpt(CURLOPT_HTTPGET, true); - $this->addHandle($curl); return $curl; } /** * Add Head * - * @access public - * @param $url - * @param $data - * + * @param $url + * @param $data * @return object */ - public function addHead($url, $data = array()) + public function addHead($url, $data = []) { if (is_array($url)) { $data = $url; $url = $this->baseUrl; } - $curl = new Curl(); - $curl->setURL($url, $data); + + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url, $data); + $curl->setUrl($url, $data); $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD'); $curl->setOpt(CURLOPT_NOBODY, true); - $this->addHandle($curl); return $curl; } /** * Add Options * - * @access public - * @param $url - * @param $data - * + * @param $url + * @param $data * @return object */ - public function addOptions($url, $data = array()) + public function addOptions($url, $data = []) { if (is_array($url)) { $data = $url; $url = $this->baseUrl; } - $curl = new Curl(); - $curl->setURL($url, $data); - $curl->unsetHeader('Content-Length'); + + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url, $data); + $curl->setUrl($url, $data); + $curl->removeHeader('Content-Length'); $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'OPTIONS'); - $this->addHandle($curl); return $curl; } /** * Add Patch * - * @access public - * @param $url - * @param $data - * + * @param $url + * @param $data * @return object */ - public function addPatch($url, $data = array()) + public function addPatch($url, $data = []) { if (is_array($url)) { $data = $url; $url = $this->baseUrl; } - $curl = new Curl(); - $curl->setURL($url); - $curl->unsetHeader('Content-Length'); + + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + + if (is_array($data) && empty($data)) { + $curl->removeHeader('Content-Length'); + } + + $this->queueHandle($curl); + $this->setUrl($url); + $curl->setUrl($url); $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'PATCH'); - $curl->setOpt(CURLOPT_POSTFIELDS, $data); - $this->addHandle($curl); + $curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data)); return $curl; } /** * Add Post * - * @access public - * @param $url - * @param $data - * + * @param $url + * @param $data + * @param $follow_303_with_post + * If true, will cause 303 redirections to be followed using a POST request + * (default: false). Note: Redirections are only followed if the + * CURLOPT_FOLLOWLOCATION option is set to true. * @return object */ - public function addPost($url, $data = array()) + public function addPost($url, $data = '', $follow_303_with_post = false) { if (is_array($url)) { + $follow_303_with_post = (bool)$data; $data = $url; $url = $this->baseUrl; } - $curl = new Curl(); + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url); if (is_array($data) && empty($data)) { - $curl->unsetHeader('Content-Length'); + $curl->removeHeader('Content-Length'); + } + + $curl->setUrl($url); + + // Set the request method to "POST" when following a 303 redirect with + // an additional POST request is desired. This is equivalent to setting + // the -X, --request command line option where curl won't change the + // request method according to the HTTP 30x response code. + if ($follow_303_with_post) { + $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST'); } - $curl->setURL($url); - $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'POST'); $curl->setOpt(CURLOPT_POST, true); $curl->setOpt(CURLOPT_POSTFIELDS, $curl->buildPostData($data)); - $this->addHandle($curl); return $curl; } /** * Add Put * - * @access public - * @param $url - * @param $data - * + * @param $url + * @param $data * @return object */ - public function addPut($url, $data = array()) + public function addPut($url, $data = []) { if (is_array($url)) { $data = $url; $url = $this->baseUrl; } - $curl = new Curl(); - $curl->setURL($url); + + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url); + $curl->setUrl($url); $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT'); $put_data = $curl->buildPostData($data); - $curl->setHeader('Content-Length', strlen($put_data)); + if (is_string($put_data)) { + $curl->setHeader('Content-Length', strlen($put_data)); + } $curl->setOpt(CURLOPT_POSTFIELDS, $put_data); - $this->addHandle($curl); return $curl; } /** - * Before Send - * - * @access public - * @param $callback - */ - public function beforeSend($callback) - { - $this->beforeSendFunction = $callback; - } - - /** - * Close + * Add Search * - * @access public + * @param $url + * @param $data + * @return object */ - public function close() + public function addSearch($url, $data = []) { - foreach ($this->curls as $ch) { - $ch->close(); + if (is_array($url)) { + $data = $url; + $url = $this->baseUrl; } - if (is_resource($this->multiCurl)) { - curl_multi_close($this->multiCurl); + $curl = new Curl(http://github.com/$this-%3EbaseUrl,%20$this-%3Eoptions); + $this->queueHandle($curl); + $this->setUrl($url); + $curl->setUrl($url); + $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'SEARCH'); + $put_data = $curl->buildPostData($data); + if (is_string($put_data)) { + $curl->setHeader('Content-Length', strlen($put_data)); } + $curl->setOpt(CURLOPT_POSTFIELDS, $put_data); + return $curl; } /** - * Complete + * Add Curl + * + * Add a Curl instance to the handle queue. * - * @access public - * @param $callback + * @param $curl + * @return object */ - public function complete($callback) + public function addCurl(http://github.com/Curl%20$curl) { - $this->completeFunction = $callback; + $this->queueHandle($curl); + return $curl; } /** - * Error - * - * @access public - * @param $callback + * Close */ - public function error($callback) + #[\Override] + public function close() { - $this->errorFunction = $callback; + foreach ($this->queuedCurls as $curl) { + $curl->close(); + } + + if (is_resource($this->multiCurl) || $this->multiCurl instanceof \CurlMultiHandle) { + curl_multi_close($this->multiCurl); + } + $this->multiCurl = null; } /** - * Get Opt - * - * @access public - * @param $option + * Set Concurrency * - * @return mixed + * @param $concurrency */ - public function getOpt($option) + public function setConcurrency($concurrency) { - return $this->options[$option]; + $this->concurrency = $concurrency; } /** - * Set Basic Authentication + * Set Cookie * - * @access public - * @param $username - * @param $password + * @param $key + * @param $value */ - public function setBasicAuthentication($username, $password = '') + #[\Override] + public function setCookie($key, $value) { - $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_BASIC); - $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + $this->cookies[$key] = $value; } /** - * Set Digest Authentication + * Set Cookies * - * @access public - * @param $username - * @param $password + * @param $cookies */ - public function setDigestAuthentication($username, $password = '') + #[\Override] + public function setCookies($cookies) { - $this->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); - $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); + foreach ($cookies as $key => $value) { + $this->cookies[$key] = $value; + } } /** - * Set Cookie + * Set Cookie String * - * @access public - * @param $key - * @param $value + * @param $string */ - public function setCookie($key, $value) + #[\Override] + public function setCookieString($string) { - $this->cookies[$key] = $value; - $this->setOpt(CURLOPT_COOKIE, str_replace('+', '%20', http_build_query($this->cookies, '', '; '))); + $this->setOpt(CURLOPT_COOKIE, $string); } /** * Set Cookie File * - * @access public - * @param $cookie_file + * @param $cookie_file */ + #[\Override] public function setCookieFile($cookie_file) { $this->setOpt(CURLOPT_COOKIEFILE, $cookie_file); @@ -354,9 +424,9 @@ public function setCookieFile($cookie_file) /** * Set Cookie Jar * - * @access public - * @param $cookie_jar + * @param $cookie_jar */ + #[\Override] public function setCookieJar($cookie_jar) { $this->setOpt(CURLOPT_COOKIEJAR, $cookie_jar); @@ -365,178 +435,380 @@ public function setCookieJar($cookie_jar) /** * Set Header * - * @access public - * @param $key - * @param $value + * Add extra header to include in the request. + * + * @param $key + * @param $value */ + #[\Override] public function setHeader($key, $value) { $this->headers[$key] = $value; + $this->updateHeaders(); + } + + /** + * Set Headers + * + * Add extra headers to include in the request. + * + * @param $headers + */ + #[\Override] + public function setHeaders($headers) + { + if (ArrayUtil::isArrayAssoc($headers)) { + foreach ($headers as $key => $value) { + $key = trim($key); + $value = trim($value); + $this->headers[$key] = $value; + } + } else { + foreach ($headers as $header) { + list($key, $value) = array_pad(explode(':', $header, 2), 2, ''); + $key = trim($key); + $value = trim($value); + $this->headers[$key] = $value; + } + } + + $this->updateHeaders(); } /** * Set JSON Decoder * - * @access public - * @param $function + * @param $mixed boolean|callable */ - public function setJsonDecoder($function) + #[\Override] + public function setJsonDecoder($mixed) { - if (is_callable($function)) { - $this->jsonDecoder = $function; + if ($mixed === false) { + $this->jsonDecoder = false; + } elseif (is_callable($mixed)) { + $this->jsonDecoder = $mixed; } } /** - * Set Opt + * Set XML Decoder * - * @access public - * @param $option - * @param $value + * @param $mixed boolean|callable */ - public function setOpt($option, $value) + #[\Override] + public function setXmlDecoder($mixed) { - $this->options[$option] = $value; + if ($mixed === false) { + $this->xmlDecoder = false; + } elseif (is_callable($mixed)) { + $this->xmlDecoder = $mixed; + } } /** - * Set Referer + * Set Proxies * - * @access public - * @param $referer + * Set proxies to tunnel requests through. When set, a random proxy will be + * used for the request. + * + * @param $proxies array - A list of HTTP proxies to tunnel requests + * through. May include port number. */ - public function setReferer($referer) + public function setProxies($proxies) { - $this->setReferrer($referer); + $this->proxies = $proxies; } /** - * Set Referrer + * Set Opt * - * @access public - * @param $referrer + * @param $option + * @param $value */ - public function setReferrer($referrer) + #[\Override] + public function setOpt($option, $value) { - $this->setOpt(CURLOPT_REFERER, $referrer); + $this->options[$option] = $value; + + // Make changing the url an instance-specific option. Set the value of + // existing instances when they have not already been set to avoid + // unexpectedly changing the request url after is has been specified. + if ($option === CURLOPT_URL) { + foreach ($this->queuedCurls as $curl_id => $curl) { + if ( + !isset($this->instanceSpecificOptions[$curl_id][$option]) || + $this->instanceSpecificOptions[$curl_id][$option] === null + ) { + $this->instanceSpecificOptions[$curl_id][$option] = $value; + } + } + } } /** - * Set Timeout + * Set Opts * - * @access public - * @param $seconds + * @param $options */ - public function setTimeout($seconds) + #[\Override] + public function setOpts($options) { - $this->setOpt(CURLOPT_TIMEOUT, $seconds); + foreach ($options as $option => $value) { + $this->setOpt($option, $value); + } } /** - * Set Url + * Set Rate Limit + * + * @param $rate_limit string (e.g. "60/1m"). + * @throws \UnexpectedValueException + */ + public function setRateLimit($rate_limit) + { + $rate_limit_pattern = + '/' . // delimiter + '^' . // assert start + '(\d+)' . // digit(s) + '\/' . // slash + '(\d+)?' . // digit(s), optional + '(s|m|h)' . // unit, s for seconds, m for minutes, h for hours + '$' . // assert end + '/' . // delimiter + 'i' . // case-insensitive matches + ''; + if (!preg_match($rate_limit_pattern, $rate_limit, $matches)) { + throw new \UnexpectedValueException( + 'rate limit must be formatted as $max_requests_per_interval/$interval(s|m|h) ' . + '(e.g. "60/1m" for a maximum of 60 requests per 1 minute)' + ); + } + + $max_requests_per_interval = (int)$matches['1']; + if ($matches['2'] === '') { + $interval = 1; + } else { + $interval = (int)$matches['2']; + } + $unit = strtolower($matches['3']); + + // Convert interval to seconds based on unit. + $interval_seconds = ''; + if ($unit === 's') { + $interval_seconds = $interval * 1; + } elseif ($unit === 'm') { + $interval_seconds = $interval * 60; + } elseif ($unit === 'h') { + $interval_seconds = $interval * 3600; + } + + $this->rateLimit = (string)$max_requests_per_interval . '/' . (string)$interval . $unit; + $this->rateLimitEnabled = true; + $this->maxRequestsPerInterval = $max_requests_per_interval; + $this->interval = $interval; + $this->intervalSeconds = $interval_seconds; + $this->unit = $unit; + } + + /** + * Set Retry + * + * Number of retries to attempt or decider callable. * - * @access public - * @param $url + * When using a number of retries to attempt, the maximum number of attempts + * for the request is $maximum_number_of_retries + 1. + * + * When using a callable decider, the request will be retried until the + * function returns a value which evaluates to false. + * + * @param $mixed */ - public function setURL($url) + #[\Override] + public function setRetry($mixed) { - $this->baseUrl = $url; + $this->retry = $mixed; } /** - * Set User Agent + * Set Url * - * @access public - * @param $user_agent + * @param $url + * @param $mixed_data */ - public function setUserAgent($user_agent) + #[\Override] + public function setUrl($url, $mixed_data = '') { - $this->setOpt(CURLOPT_USERAGENT, $user_agent); + $built_url = Url::buildUrl($url, $mixed_data); + + if ($this->baseUrl === null) { + $this->baseUrl = (string)new Url($built_url); + } else { + $this->baseUrl = (string)new Url($this->baseUrl, $built_url); + } + + $this->setOpt(CURLOPT_URL, $this->baseUrl); } /** * Start * - * @access public + * @throws \ErrorException */ public function start() { - foreach ($this->curls as $ch) { - $this->initHandle($ch); + if ($this->isStarted) { + return; } $this->isStarted = true; + $this->startTime = microtime(true); + $this->currentStartTime = microtime(true); + $this->currentRequestCount = 0; do { - curl_multi_select($this->multiCurl); - curl_multi_exec($this->multiCurl, $active); + while ( + count($this->queuedCurls) && + count($this->activeCurls) < $this->concurrency && + (!$this->rateLimitEnabled || $this->hasRequestQuota()) + ) { + $this->initHandle(); + } + + if ($this->rateLimitEnabled && !count($this->activeCurls) && !$this->hasRequestQuota()) { + $this->waitUntilRequestQuotaAvailable(); + } - while (!($info_array = curl_multi_info_read($this->multiCurl)) === false) { + if ($this->preferRequestTimeAccuracy) { + // Wait for activity on any curl_multi connection when curl_multi_select (libcurl) fails to correctly + // block. + // https://bugs.php.net/bug.php?id=63411 + // + // Also, use a shorter curl_multi_select() timeout instead the default of one second. This allows + // pending requests to have more accurate start times. Without a shorter timeout, it can be nearly a + // full second before available request quota is rechecked and pending requests can be initialized. + if (curl_multi_select($this->multiCurl, 0.2) === -1) { + usleep(100_000); + } + + curl_multi_exec($this->multiCurl, $active); + } else { + // Use multiple loops to get data off of the multi handler. Without this, the following error may appear + // intermittently on certain versions of PHP: + // curl_multi_exec(): supplied resource is not a valid cURL handle resource + + // Clear out the curl buffer. + do { + $status = curl_multi_exec($this->multiCurl, $active); + } while ($status === CURLM_CALL_MULTI_PERFORM); + + // Wait for more information and then get that information. + while ($active && $status === CURLM_OK) { + // Check if the network socket has some data. + if (curl_multi_select($this->multiCurl) !== -1) { + // Process the data for as long as the system tells us to keep getting it. + do { + $status = curl_multi_exec($this->multiCurl, $active); + } while ($status === CURLM_CALL_MULTI_PERFORM); + } + } + } + + while ( + (is_resource($this->multiCurl) || $this->multiCurl instanceof \CurlMultiHandle) && + (($info_array = curl_multi_info_read($this->multiCurl)) !== false) + ) { if ($info_array['msg'] === CURLMSG_DONE) { - foreach ($this->curls as $key => $ch) { - if ($ch->curl === $info_array['handle']) { - $ch->curlErrorCode = $info_array['result']; - $ch->exec($ch->curl); - curl_multi_remove_handle($this->multiCurl, $ch->curl); - unset($this->curls[$key]); - - // Close open file handles and reset the curl instance. - if (isset($this->curlFileHandles[$ch->id])) { - $ch->downloadComplete($this->curlFileHandles[$ch->id]); - unset($this->curlFileHandles[$ch->id]); + foreach ($this->activeCurls as $key => $curl) { + if ($curl->curl === $info_array['handle']) { + // Set the error code for multi handles using the "result" key in the array returned by + // curl_multi_info_read(). Using curl_errno() on a multi handle will incorrectly return 0 + // for errors. + $curl->curlErrorCode = $info_array['result']; + $curl->exec($curl->curl); + + if ($curl->attemptRetry()) { + // Remove completed handle before adding again in order to retry request. + curl_multi_remove_handle($this->multiCurl, $curl->curl); + + $curlm_error_code = curl_multi_add_handle($this->multiCurl, $curl->curl); + if ($curlm_error_code !== CURLM_OK) { + throw new \ErrorException( + 'cURL multi add handle error: ' . curl_multi_strerror($curlm_error_code) + ); + } + + $curl->call($curl->beforeSendCallback); + } else { + $curl->execDone(); + + // Remove completed instance from active curls. + unset($this->activeCurls[$key]); + + // Remove handle of the completed instance. + curl_multi_remove_handle($this->multiCurl, $curl->curl); + + // Clean up completed instance. + $curl->close(); } + break; } } } } - - if (!$active) { - $active = count($this->curls); - } - } while ($active > 0); + } while ($active || count($this->activeCurls) || count($this->queuedCurls)); $this->isStarted = false; + $this->stopTime = microtime(true); } /** - * Success - * - * @access public - * @param $callback + * Stop */ - public function success($callback) + #[\Override] + public function stop() { - $this->successFunction = $callback; + // Remove any queued curl requests. + while (count($this->queuedCurls)) { + $curl = array_pop($this->queuedCurls); + $curl->close(); + } + + // Attempt to stop active curl requests. + while (count($this->activeCurls)) { + // Remove instance from active curls. + $curl = array_pop($this->activeCurls); + + // Remove active curl handle. + curl_multi_remove_handle($this->multiCurl, $curl->curl); + + $curl->stop(); + } } /** * Unset Header * - * @access public - * @param $key + * Remove extra header previously set using Curl::setHeader(). + * + * @param $key */ + #[\Override] public function unsetHeader($key) { - $this->setHeader($key, ''); unset($this->headers[$key]); } /** - * Verbose - * - * @access public - * @param $on + * Set request time accuracy */ - public function verbose($on = true) + public function setRequestTimeAccuracy() { - $this->setOpt(CURLOPT_VERBOSE, $on); + $this->preferRequestTimeAccuracy = true; } /** * Destruct - * - * @access public */ public function __destruct() { @@ -544,45 +816,161 @@ public function __destruct() } /** - * Add Handle + * Update Headers + */ + private function updateHeaders() + { + foreach ($this->queuedCurls as $curl) { + $curl->setHeaders($this->headers); + } + } + + /** + * Queue Handle * - * @access private - * @param $curl + * @param $curl + */ + private function queueHandle($curl) + { + // Use sequential ids to allow for ordered post processing. + $curl->id = $this->nextCurlId++; + $curl->childOfMultiCurl = true; + $this->queuedCurls[$curl->id] = $curl; + + // Avoid overwriting any existing header. + if ($curl->getOpt(CURLOPT_HTTPHEADER) === null) { + $curl->setHeaders($this->headers); + } + } + + /** + * Init Handle + * + * @param $curl * @throws \ErrorException */ - private function addHandle($curl) + private function initHandle() { + $curl = array_shift($this->queuedCurls); + if ($curl === null) { + return; + } + + // Add instance to list of active curls. + $this->currentRequestCount += 1; + $this->activeCurls[$curl->id] = $curl; + + // Set callbacks if not already individually set. + if ($curl->beforeSendCallback === null) { + $curl->beforeSend($this->beforeSendCallback); + } + if ($curl->afterSendCallback === null) { + $curl->afterSend($this->afterSendCallback); + } + if ($curl->successCallback === null) { + $curl->success($this->successCallback); + } + if ($curl->errorCallback === null) { + $curl->error($this->errorCallback); + } + if ($curl->completeCallback === null) { + $curl->complete($this->completeCallback); + } + + // Set decoders if not already individually set. + if ($curl->jsonDecoder === null) { + $curl->setJsonDecoder($this->jsonDecoder); + } + if ($curl->xmlDecoder === null) { + $curl->setXmlDecoder($this->xmlDecoder); + } + + // Set instance-specific options on the Curl instance when present. + if (isset($this->instanceSpecificOptions[$curl->id])) { + $curl->setOpts($this->instanceSpecificOptions[$curl->id]); + } + + $curl->setRetry($this->retry); + $curl->setCookies($this->cookies); + + // Use a random proxy for the curl instance when proxies have been set + // and the curl instance doesn't already have a proxy set. + if (is_array($this->proxies) && $curl->getOpt(CURLOPT_PROXY) === null) { + $random_proxy = ArrayUtil::arrayRandom($this->proxies); + $curl->setProxy($random_proxy); + } + $curlm_error_code = curl_multi_add_handle($this->multiCurl, $curl->curl); - if (!($curlm_error_code === CURLM_OK)) { + if ($curlm_error_code !== CURLM_OK) { throw new \ErrorException('cURL multi add handle error: ' . curl_multi_strerror($curlm_error_code)); } - $curl->beforeSend($this->beforeSendFunction); - $curl->success($this->successFunction); - $curl->error($this->errorFunction); - $curl->complete($this->completeFunction); - $this->curls[] = $curl; - $curl->id = $this->nextCurlId++; - if ($this->isStarted) { - $this->initHandle($curl); + $curl->call($curl->beforeSendCallback); + } + + /** + * Has Request Quota + * + * Checks if there is any available quota to make additional requests while + * rate limiting is enabled. + */ + private function hasRequestQuota() + { + // Calculate if there's request quota since ratelimiting is enabled. + if ($this->rateLimitEnabled) { + // Determine if the limit of requests per interval has been reached. + if ($this->currentRequestCount >= $this->maxRequestsPerInterval) { + $micro_time = microtime(true); + $elapsed_seconds = $micro_time - $this->currentStartTime; + if ($elapsed_seconds <= $this->intervalSeconds) { + // Rate limit reached. + return false; + } else { + // Rate limit not reached. Rate limit interval has passed, + // reset counters. + $this->currentStartTime = $micro_time; + $this->currentRequestCount = 0; + } + } } + + return true; } /** - * Init Handle + * Wait Until Request Quota Available * - * @access private - * @param $curl + * Waits until there is available request quota available based on the rate limit. */ - private function initHandle($curl) + private function waitUntilRequestQuotaAvailable() { - foreach ($this->options as $option => $value) { - $curl->setOpt($option, $value); + $sleep_until = TimeUtil::getSleepUntilMicrotime( + $this->currentStartTime, + $this->intervalSeconds, + ); + + $current_microtime = microtime(true); + $sleep_seconds = TimeUtil::getSleepSecondsUntilMicrotime( + $sleep_until, + $current_microtime, + ); + + list($whole_seconds, $microseconds_remainder) = TimeUtil::getWholeAndRemainderSeconds($sleep_seconds); + + if ($whole_seconds >= 1) { + sleep($whole_seconds); } - foreach ($this->headers as $key => $value) { - $curl->setHeader($key, $value); + + if ($microseconds_remainder > 0) { + usleep($microseconds_remainder); } - $curl->setJsonDecoder($this->jsonDecoder); - $curl->call($curl->beforeSendFunction); + + $this->currentStartTime = microtime(true); + $this->currentRequestCount = 0; + } + + public function getActiveCurls() + { + return $this->activeCurls; } } diff --git a/src/Curl/StringUtil.php b/src/Curl/StringUtil.php new file mode 100644 index 0000000000..cc7c68be55 --- /dev/null +++ b/src/Curl/StringUtil.php @@ -0,0 +1,65 @@ +baseUrl = $base_url; + $this->relativeUrl = $relative_url; + } + + public function __toString(): string + { + return $this->absolutizeUrl(); + } + + /** + * Remove dot segments. + * + * Interpret and remove the special "." and ".." path segments from a referenced path. + * + * @param mixed $input + */ + public static function removeDotSegments($input) + { + // 1. The input buffer is initialized with the now-appended path + // components and the output buffer is initialized to the empty + // string. + $output = ''; + + // 2. While the input buffer is not empty, loop as follows: + while (!empty($input)) { + // A. If the input buffer begins with a prefix of "../" or "./", + // then remove that prefix from the input buffer; otherwise, + if (StringUtil::startsWith($input, '../')) { + $input = substr($input, 3); + } elseif (StringUtil::startsWith($input, './')) { + $input = substr($input, 2); + + // B. if the input buffer begins with a prefix of "/./" or "/.", + // where "." is a complete path segment, then replace that + // prefix with "/" in the input buffer; otherwise, + } elseif (StringUtil::startsWith($input, '/./')) { + $input = substr($input, 2); + } elseif ($input === '/.') { + $input = '/'; + + // C. if the input buffer begins with a prefix of "/../" or "/..", + // where ".." is a complete path segment, then replace that + // prefix with "/" in the input buffer and remove the last + // segment and its preceding "/" (if any) from the output + // buffer; otherwise, + } elseif (StringUtil::startsWith($input, '/../')) { + $input = substr($input, 3); + $output = substr_replace($output, '', StringUtil::reversePosition($output, '/')); + } elseif ($input === '/..') { + $input = '/'; + $output = substr_replace($output, '', StringUtil::reversePosition($output, '/')); + + // D. if the input buffer consists only of "." or "..", then remove + // that from the input buffer; otherwise, + } elseif ($input === '.' || $input === '..') { + $input = ''; + + // E. move the first path segment in the input buffer to the end of + // the output buffer, including the initial "/" character (if + // any) and any subsequent characters up to, but not including, + // the next "/" character or the end of the input buffer. + } elseif (!(($pos = StringUtil::position($input, '/', 1)) === false)) { + $output .= substr($input, 0, $pos); + $input = substr_replace($input, '', 0, $pos); + } else { + $output .= $input; + $input = ''; + } + } + + // 3. Finally, the output buffer is returned as the result of + // remove_dot_segments. + return $output . $input; + } + + /** + * Build Url + * + * @param $url + * @param $mixed_data + * @return string + */ + public static function buildUrl($url, $mixed_data = '') + { + $query_string = ''; + if (!empty($mixed_data)) { + $query_mark = strpos($url, '?') > 0 ? '&' : '?'; + if (is_string($mixed_data)) { + $query_string .= $query_mark . $mixed_data; + } elseif (is_array($mixed_data)) { + $query_string .= $query_mark . http_build_query($mixed_data, '', '&'); + } + } + return $url . $query_string; + } + + /** + * Absolutize url. + * + * Combine the base and relative url into an absolute url. + */ + private function absolutizeUrl() + { + $b = self::parseUrl($this->baseUrl); + if (!isset($b['path'])) { + $b['path'] = '/'; + } + if ($this->relativeUrl === null) { + return $this->unparseUrl($b); + } + $r = self::parseUrl($this->relativeUrl); + $r['authorized'] = isset($r['scheme']) || isset($r['host']) || isset($r['port']) + || isset($r['user']) || isset($r['pass']); + $target = []; + if (isset($r['scheme'])) { + $target['scheme'] = $r['scheme']; + $target['host'] = $r['host'] ?? null; + $target['port'] = $r['port'] ?? null; + $target['user'] = $r['user'] ?? null; + $target['pass'] = $r['pass'] ?? null; + $target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null; + $target['query'] = $r['query'] ?? null; + } else { + $target['scheme'] = $b['scheme'] ?? null; + if ($r['authorized']) { + $target['host'] = $r['host'] ?? null; + $target['port'] = $r['port'] ?? null; + $target['user'] = $r['user'] ?? null; + $target['pass'] = $r['pass'] ?? null; + $target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null; + $target['query'] = $r['query'] ?? null; + } else { + $target['host'] = $b['host'] ?? null; + $target['port'] = $b['port'] ?? null; + $target['user'] = $b['user'] ?? null; + $target['pass'] = $b['pass'] ?? null; + if (!isset($r['path']) || $r['path'] === '') { + $target['path'] = $b['path']; + $target['query'] = $r['query'] ?? $b['query'] ?? null; + } else { + if (StringUtil::startsWith($r['path'], '/')) { + $target['path'] = self::removeDotSegments($r['path']); + } else { + $base = StringUtil::characterReversePosition($b['path'], '/', true); + if ($base === false) { + $base = ''; + } + $target['path'] = self::removeDotSegments($base . '/' . $r['path']); + } + $target['query'] = $r['query'] ?? null; + } + } + } + if ($this->relativeUrl === '') { + $target['fragment'] = $b['fragment'] ?? null; + } else { + $target['fragment'] = $r['fragment'] ?? null; + } + $absolutized_url = $this->unparseUrl($target); + return $absolutized_url; + } + + /** + * Parse url. + * + * Parse url into components of a URI as specified by RFC 3986. + * + * @param mixed $url + */ + public static function parseUrl($url) + { + $parts = parse_url(http://github.com/(string) $url); + if (isset($parts['path'])) { + $parts['path'] = self::percentEncodeChars($parts['path']); + } + return $parts; + } + + /** + * Percent-encode characters. + * + * Percent-encode characters to represent a data octet in a component when + * that octet's corresponding character is outside the allowed set. + * + * @param mixed $chars + */ + private static function percentEncodeChars($chars) + { + // ALPHA = A-Z / a-z + $alpha = 'A-Za-z'; + + // DIGIT = 0-9 + $digit = '0-9'; + + // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + $unreserved = $alpha . $digit . preg_quote('-._~'); + + // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + // / "*" / "+" / "," / ";" / "=" / "#" + $sub_delims = preg_quote('!$&\'()*+,;=#'); + + // HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + $hexdig = $digit . 'A-F'; + // "The uppercase hexadecimal digits 'A' through 'F' are equivalent to + // the lowercase digits 'a' through 'f', respectively." + $hexdig .= 'a-f'; + + $pattern = '/(?:[^' . $unreserved . $sub_delims . preg_quote(':@%/?', '/') . ']++|%(?![' . $hexdig . ']{2}))/'; + $percent_encoded_chars = preg_replace_callback( + $pattern, + function ($matches) { + return rawurlencode($matches[0]); + }, + $chars + ); + return $percent_encoded_chars; + } + + /** + * Unparse url. + * + * Combine url components into a url. + * + * @param mixed $parsed_url + */ + private function unparseUrl($parsed_url) + { + $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; + $user = $parsed_url['user'] ?? ''; + $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; + $pass = ($user || $pass) ? $pass . '@' : ''; + $host = $parsed_url['host'] ?? ''; + $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; + $path = $parsed_url['path'] ?? ''; + $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; + $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; + $unparsed_url = $scheme . $user . $pass . $host . $port . $path . $query . $fragment; + return $unparsed_url; + } +} diff --git a/tests/.php-cs-fixer.php b/tests/.php-cs-fixer.php new file mode 100644 index 0000000000..b9014c2307 --- /dev/null +++ b/tests/.php-cs-fixer.php @@ -0,0 +1,88 @@ +in(dirname(__DIR__)); + +$config = new PhpCsFixer\Config(); +$config + ->setRiskyAllowed(true) + ->setRules([ + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'constant_case' => [ + 'case' => 'lower', + ], + 'elseif' => true, + 'encoding' => true, + 'general_phpdoc_annotation_remove' => [ + 'annotations' => [ + 'access', + 'author', + ], + 'case_sensitive' => false, + ], + 'is_null' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_leading_import_slash' => true, + 'no_unused_imports' => true, + 'ordered_imports' => [ + 'sort_algorithm' => 'alpha', + ], + 'phpdoc_add_missing_param_annotation' => [ + 'only_untyped' => false, + ], + 'phpdoc_align' => [ + 'align' => 'vertical', + ], + 'phpdoc_indent' => true, + 'phpdoc_line_span' => [ + 'const' => 'multi', + 'method' => 'multi', + 'property' => 'multi', + ], + 'phpdoc_order' => [ + 'order' => [ + 'param', + 'return', + 'throws', + 'see', + ], + ], + 'phpdoc_param_order' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => [ + 'groups' => [ + [ + 'deprecated', + 'expectedException', + 'param', + 'requires', + 'return', + 'see', + 'throws', + ], + ], + 'skip_unlisted_annotations' => false, + ], + 'phpdoc_trim' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types' => true, + 'phpdoc_types_order' => [ + 'null_adjustment' => 'always_last', + 'sort_algorithm' => 'alpha', + ], + 'short_scalar_cast' => true, + 'strict_comparison' => true, + 'strict_param' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => true, + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => false, + ], + ]) + ->setFinder($finder); + +return $config; diff --git a/tests/.pre-commit-config.yaml b/tests/.pre-commit-config.yaml new file mode 100644 index 0000000000..b0d2728d60 --- /dev/null +++ b/tests/.pre-commit-config.yaml @@ -0,0 +1,45 @@ +# Pre-commit git hooks. +# +# Usage: +# 1. Run one of: +# $ pip install pre-commit +# $ brew install pre-commit +# +# 2. Install hooks +# $ pre-commit install --config="tests/.pre-commit-config.yaml" +# +# 3. Optionally, enable automatic updates +# $ pre-commit autoupdate --config="tests/.pre-commit-config.yaml" +repos: +- repo: https://github.com/psf/black + rev: 25.11.0 + hooks: + - id: black + name: black + entry: black +- repo: https://github.com/pycqa/flake8 + rev: 7.3.0 + hooks: + - id: flake8 + args: ["--config", "tests/setup.cfg"] +- repo: https://github.com/pycqa/isort + rev: 7.0.0 + hooks: + - id: isort + name: isort + args: ["--force-single-line-imports", "--profile", "black"] +- repo: local + hooks: + - id: composer-validate + name: composer-validate + language: script + entry: scripts/pre-commit.sh + files: 'composer\.json' + pass_filenames: true +- repo: https://github.com/jazzband/pip-tools + rev: v7.5.2 + hooks: + - id: pip-compile + name: pip-compile make_release_requirements.in + files: ^(.*/)?make_release_requirements\.(in|txt)$ + entry: bash -c 'cd scripts/ && pip-compile --output-file="make_release_requirements.txt" "make_release_requirements.in"' diff --git a/tests/ContentRangeServer.php b/tests/ContentRangeServer.php new file mode 100644 index 0000000000..5be219a7d0 --- /dev/null +++ b/tests/ContentRangeServer.php @@ -0,0 +1,51 @@ +isValid()) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header('Content-Range: ' . $range->getContentRangeHeader()); + exit; + } + + $length = $range->getLength(); + + header('HTTP/1.1 206 Partial Content'); + header('Content-Length: ' . $length); + header('Content-Range: ' . $range->getContentRangeHeader()); + + $start = $range->getFirstBytePosition(); + if ($start > 0) { + fseek($fp, $start, SEEK_SET); + } + + $chunk_size = 4096; + while ($length) { + $read = $length > $chunk_size ? $chunk_size : $length; + $length -= $read; + echo fread($fp, $read); + } + } + + fclose($fp); + } +} diff --git a/tests/Helper.php b/tests/Helper.php new file mode 100644 index 0000000000..6be4669736 --- /dev/null +++ b/tests/Helper.php @@ -0,0 +1,209 @@ +testUrl = $port === null ? self::TEST_URL : $this->getTestUrl($port); + $this->curl = new Curl(); + $this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, false); + $this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, false); + } + + public function server($test, $request_method, $arg1 = null, $arg2 = null) + { + $this->curl->setHeader('X-DEBUG-TEST', $test); + $request_method = strtolower($request_method); + if ($arg1 !== null && $arg2 !== null) { + $this->curl->$request_method($this->testUrl, $arg1, $arg2); + } elseif ($arg1 !== null) { + $this->curl->$request_method($this->testUrl, $arg1); + } else { + $this->curl->$request_method($this->testUrl); + } + + $this->message = + 'error message: ' . $this->curl->errorMessage . "\n" . + 'curl error message: ' . $this->curl->curlErrorMessage . "\n" . + 'http error message: ' . $this->curl->httpErrorMessage . "\n" . + 'error code: ' . $this->curl->errorCode . "\n" . + 'curl error code: ' . $this->curl->curlErrorCode . "\n" . + 'raw response: ' . $this->curl->rawResponse . "\n" . + ''; + + return $this->curl->response; + } + + /* + * When chaining requests, the method must be forced, otherwise a + * previously forced method might be inherited. + * Especially, POSTs must be configured to not perform post-redirect-get. + */ + private function chainedRequest($request_method, $data) + { + if ($request_method === 'POST') { + $follow_303_with_post = true; + $this->server('request_method', $request_method, $data, $follow_303_with_post); + } else { + $this->server('request_method', $request_method, $data); + } + \PHPUnit\Framework\Assert::assertEquals($request_method, $this->curl->responseHeaders['X-REQUEST-METHOD']); + } + + public function chainRequests($first, $second, $data = []) + { + $this->chainedRequest($first, $data); + $this->chainedRequest($second, $data); + } + + public static function getTestUrl($port) + { + // Return url pointing to a test server running on the specified port. + // To avoid installing and configuring a web server for the tests, PHP's + // built-in development server is used. As each development server can + // only handle one request at a time (single-threaded) and some tests + // expect the server to handle requests simultaneously, multiple + // instances are run on different ports. With this setup, requests in + // the test can be made to the various port urls without having to be + // handled sequentially. + return 'http://127.0.0.1/' . $port . '/'; + } +} + +function create_png() +{ + // PNG image data, 1 x 1, 1-bit colormap, non-interlaced + ob_start(); + imagepng(imagecreatefromstring(base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', true))); + $raw_image = ob_get_contents(); + ob_end_clean(); + return $raw_image; +} + +function create_tmp_file($data) +{ + $tmp_file = tmpfile(); + fwrite($tmp_file, $data); + rewind($tmp_file); + return $tmp_file; +} + +function get_tmp_file_path() +{ + // Return temporary file path without creating file. + $tmp_file_path = + rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . + DIRECTORY_SEPARATOR . 'php-curl-class.' . uniqid((string) rand(), true); + return $tmp_file_path; +} + +function get_png() +{ + $tmp_filename = tempnam('/tmp', 'php-curl-class.'); + file_put_contents($tmp_filename, create_png()); + return $tmp_filename; +} + +function mime_type($file_path) +{ + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime_type = finfo_file($finfo, $file_path); + return $mime_type; +} + +function upload_file_to_server($upload_file_path) +{ + $upload_test = new Test(); + $upload_test->server('upload_response', 'POST', [ + 'image' => '@' . $upload_file_path, + ]); + $uploaded_file_path = $upload_test->curl->response->file_path; + + // Ensure files are not the same path. + assert($upload_file_path !== $uploaded_file_path); + + // Ensure file uploaded successfully. + assert(md5_file($upload_file_path) === $upload_test->curl->responseHeaders['ETag']); + + return $uploaded_file_path; +} + +function remove_file_from_server($uploaded_file_path) +{ + $download_test = new Test(); + + // Ensure file successfully removed. + assert($download_test->server('upload_cleanup', 'POST', [ + 'file_path' => $uploaded_file_path, + ]) === 'true'); + assert(file_exists($uploaded_file_path) === false); +} + +function get_curl_property_value($instance, $property_name) +{ + $reflector = new \ReflectionClass('\Curl\Curl'); + $property = $reflector->getProperty($property_name); + $property->setAccessible(true); + return $property->getValue($instance); +} + +function get_multi_curl_property_value($instance, $property_name) +{ + $reflector = new \ReflectionClass('\Curl\MultiCurl'); + $property = $reflector->getProperty($property_name); + $property->setAccessible(true); + return $property->getValue($instance); +} + +function get_request_stats($request_stats, $multi_curl) +{ + $messages = + ['total duration: ' . sprintf('%.6f', round($multi_curl->stopTime - $multi_curl->startTime, 6)) . "\n"]; + + foreach ($request_stats as $instance_id => &$value) { + $value['relative_start'] = sprintf('%.6f', round($value['start'] - $multi_curl->startTime, 6)); + $value['relative_stop'] = sprintf('%.6f', round($value['stop'] - $multi_curl->startTime, 6)); + $value['duration'] = (string)round($value['stop'] - $value['start'], 6); + + $messages[] = + $value['relative_start'] . ' - ' . 'request ' . $instance_id . ' start' . "\n" . + $value['relative_stop'] . ' - ' . 'request ' . $instance_id . ' complete' . "\n" . + $value['duration'] . ' - ' . 'request ' . $instance_id . ' duration' . "\n"; + + unset($value['start']); + unset($value['stop']); + } + + $request_stats['message'] = implode("\n", $messages); + + return $request_stats; +} + +function get_request_method($instance) +{ + $request_method = $instance->getOpt(CURLOPT_CUSTOMREQUEST); + + // POST requests have CURLOPT_CUSTOMREQUEST unset by default to allow + // post/redirect/get requests so infer that instance is a POST when the + // CURLOPT_POST option is enabled. + if ($request_method === null && $instance->getOpt(CURLOPT_POST) === true) { + $request_method = 'POST'; + } + + return $request_method; +} diff --git a/tests/PHPCurlClass/ArrayUtilTest.php b/tests/PHPCurlClass/ArrayUtilTest.php new file mode 100644 index 0000000000..f69bbda987 --- /dev/null +++ b/tests/PHPCurlClass/ArrayUtilTest.php @@ -0,0 +1,92 @@ +assertTrue(ArrayUtil::isArrayAssoc([ + 'foo' => 'wibble', + 'bar' => 'wubble', + 'baz' => 'wobble', + ])); + } + + public function testArrayIndexed() + { + $this->assertFalse(ArrayUtil::isArrayAssoc([ + 'wibble', + 'wubble', + 'wobble', + ])); + } + + public function testCaseInsensitiveArrayIsArrayAssoc() + { + $array = new CaseInsensitiveArray(); + $this->assertTrue(ArrayUtil::isArrayAssoc($array)); + } + + public function testArrayFlattenMultidimArray() + { + $data = [ + 'key-1' => 'value-1', + 'key-2' => 'value-2', + 'key-3' => [ + 'nested-key-1' => 'nested-value-1', + 'nested-key-2' => 'nested-value-2', + 'nested-key-3' => [ + 'nested-more-key-1' => 'nested-more-value-1', + 'nested-more-key-2' => 'nested-more-value-2', + ], + ], + ]; + + $this->assertEquals([ + 'key-1' => 'value-1', + 'key-2' => 'value-2', + 'key-3[nested-key-1]' => 'nested-value-1', + 'key-3[nested-key-2]' => 'nested-value-2', + 'key-3[nested-key-3][nested-more-key-1]' => 'nested-more-value-1', + 'key-3[nested-key-3][nested-more-key-2]' => 'nested-more-value-2', + ], ArrayUtil::arrayFlattenMultidim($data)); + } + + public function testArrayFlattenMultidimOrdering() + { + $data = [ + 'foo' => 'bar', + 'baz' => [ + 'qux' => [ + ], + 'wibble' => 'wobble', + ], + ]; + + $result = ArrayUtil::arrayFlattenMultidim($data); + + // Avoid using assertEquals() as it isn't strict about ordering: + // $this->assertEquals([ + // 'foo' => 'bar', + // 'baz[qux]' => '', + // 'baz[wibble]' => 'wobble', + // ], ArrayUtil::arrayFlattenMultidim($data)); + + $result_keys = array_keys($result); + $result_values = array_values($result); + + $this->assertEquals('foo', $result_keys[0]); + $this->assertEquals('baz[qux]', $result_keys[1]); + $this->assertEquals('baz[wibble]', $result_keys[2]); + + $this->assertEquals('bar', $result_values[0]); + $this->assertEquals('', $result_values[1]); + $this->assertEquals('wobble', $result_values[2]); + } +} diff --git a/tests/PHPCurlClass/Helper.php b/tests/PHPCurlClass/Helper.php deleted file mode 100644 index 25a6860b01..0000000000 --- a/tests/PHPCurlClass/Helper.php +++ /dev/null @@ -1,78 +0,0 @@ -curl = new Curl(); - $this->curl->setOpt(CURLOPT_SSL_VERIFYPEER, false); - $this->curl->setOpt(CURLOPT_SSL_VERIFYHOST, false); - } - - public function server($test, $request_method, $query_parameters = array(), $data = array()) - { - $this->curl->setHeader('X-DEBUG-TEST', $test); - $request_method = strtolower($request_method); - if (is_array($data) && empty($data)) { - $this->curl->$request_method(self::TEST_URL, $query_parameters); - } else { - $this->curl->$request_method(self::TEST_URL, $query_parameters, $data); - } - return $this->curl->response; - } -} - -function test($instance, $before, $after) -{ - $instance->server('request_method', $before); - \PHPUnit_Framework_Assert::assertEquals($before, $instance->curl->responseHeaders['X-REQUEST-METHOD']); - $instance->server('request_method', $after); - \PHPUnit_Framework_Assert::assertEquals($after, $instance->curl->responseHeaders['X-REQUEST-METHOD']); -} - -function create_png() -{ - // PNG image data, 1 x 1, 1-bit colormap, non-interlaced - ob_start(); - imagepng(imagecreatefromstring(base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'))); - $raw_image = ob_get_contents(); - ob_end_clean(); - return $raw_image; -} - -function create_tmp_file($data) -{ - $tmp_file = tmpfile(); - fwrite($tmp_file, $data); - rewind($tmp_file); - return $tmp_file; -} - -function get_png() -{ - $tmp_filename = tempnam('/tmp', 'php-curl-class.'); - file_put_contents($tmp_filename, create_png()); - return $tmp_filename; -} - -if (function_exists('finfo_open')) { - function mime_type($file_path) - { - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mime_type = finfo_file($finfo, $file_path); - finfo_close($finfo); - return $mime_type; - } -} else { - function mime_type($file_path) - { - $mime_type = mime_content_type($file_path); - return $mime_type; - } -} diff --git a/tests/PHPCurlClass/PHPCurlClassTest.php b/tests/PHPCurlClass/PHPCurlClassTest.php index 012369640d..4991d646ba 100644 --- a/tests/PHPCurlClass/PHPCurlClassTest.php +++ b/tests/PHPCurlClass/PHPCurlClassTest.php @@ -1,33 +1,29 @@ assertTrue(extension_loaded('curl')); - $this->assertTrue(extension_loaded('gd')); - } + private $skip_slow_tests; - public function testArrayAssociative() + protected function setUp(): void { - $this->assertTrue(Curl::is_array_assoc(array( - 'foo' => 'wibble', - 'bar' => 'wubble', - 'baz' => 'wobble', - ))); + $this->skip_slow_tests = in_array(getenv('PHP_CURL_CLASS_SKIP_SLOW_TESTS'), ['1', 'y', 'Y'], true); } - public function testArrayIndexed() + public function testExtensionsLoaded() { - $this->assertFalse(Curl::is_array_assoc(array( - 'wibble', - 'wubble', - 'wobble', - ))); + $this->assertTrue(extension_loaded('curl')); + $this->assertTrue(extension_loaded('gd')); + $this->assertTrue(extension_loaded('mbstring')); } public function testCaseInsensitiveArrayGet() @@ -45,7 +41,7 @@ public function testCaseInsensitiveArrayGet() public function testCaseInsensitiveArraySet() { $array = new CaseInsensitiveArray(); - foreach (array('FOO', 'FOo', 'Foo', 'fOO', 'fOo', 'foO', 'foo') as $key) { + foreach (['FOO', 'FOo', 'Foo', 'fOO', 'fOo', 'foO', 'foo'] as $key) { $value = mt_rand(); $array[$key] = $value; $this->assertCount(1, $array); @@ -64,12 +60,12 @@ public function testCaseInsensitiveArraySet() public function testBuildPostDataArgSeparator() { - $data = array( + $data = [ 'foo' => 'Hello', 'bar' => 'World', - ); + ]; - foreach (array(false, '&', '&') as $arg_separator) { + foreach ([false, '&', '&'] as $arg_separator) { if ($arg_separator) { ini_set('arg_separator.output', $arg_separator); } @@ -78,6 +74,24 @@ public function testBuildPostDataArgSeparator() } } + public function testBuildPostDataIntegerKey() + { + $curl = new Curl(); + $this->assertEquals('abc=foo&123=bar', $curl->buildPostData([ + 'abc' => 'foo', + '123' => 'bar', + ])); + } + + public function testBuildPostDataObject() + { + $data = new \stdClass(); + $data->{'abc'} = 'foo'; + $data->{'123'} = 'bar'; + $curl = new Curl(); + $this->assertEquals('abc=foo&123=bar', $curl->buildPostData($data)); + } + public function testUserAgent() { $php_version = preg_replace('/([\.\+\?\*\(\)\[\]\^\$\/])/', '\\\\\1', 'PHP/' . PHP_VERSION); @@ -85,9 +99,9 @@ public function testUserAgent() $curl_version = 'curl\/' . $curl_version['version']; $test = new Test(); - $user_agent = $test->server('server', 'GET', array('key' => 'HTTP_USER_AGENT')); - $this->assertRegExp('/' . $php_version . '/', $user_agent); - $this->assertRegExp('/' . $curl_version . '/', $user_agent); + $user_agent = $test->server('server', 'GET', ['key' => 'HTTP_USER_AGENT']); + $this->assertMatchesRegularExpression('/' . $php_version . '/', $user_agent); + $this->assertMatchesRegularExpression('/' . $curl_version . '/', $user_agent); } public function testGet() @@ -98,156 +112,168 @@ public function testGet() public function testUrl() { - $data = array('foo' => 'bar'); + $data = ['foo' => 'bar']; // curl -v --get --request GET "http://127.0.0.1:8000/" --data "foo=bar" $test = new Test(); $test->server('server', 'GET', $data); - $this->assertEquals(Test::TEST_URL, $test->curl->baseUrl); $this->assertEquals(Test::TEST_URL . '?' . http_build_query($data), $test->curl->url); // curl -v --request POST "http://127.0.0.1:8000/" --data "foo=bar" $test = new Test(); $test->server('server', 'POST', $data); - $this->assertEquals(Test::TEST_URL, $test->curl->baseUrl); $this->assertEquals(Test::TEST_URL, $test->curl->url); // curl -v --request PUT "http://127.0.0.1:8000/" --data "foo=bar" $test = new Test(); $test->server('server', 'PUT', $data); - $this->assertEquals(Test::TEST_URL, $test->curl->baseUrl); $this->assertEquals(Test::TEST_URL, $test->curl->url); // curl -v --request PATCH "http://127.0.0.1:8000/" --data "foo=bar" $test = new Test(); $test->server('server', 'PATCH', $data); - $this->assertEquals(Test::TEST_URL, $test->curl->baseUrl); + $this->assertEquals(Test::TEST_URL, $test->curl->url); + + // curl -v --request SEARCH "http://127.0.0.1:8000/" --data "foo=bar" + $test = new Test(); + $test->server('server', 'SEARCH', $data); $this->assertEquals(Test::TEST_URL, $test->curl->url); // curl -v --request DELETE "http://127.0.0.1:8000/?foo=bar" $test = new Test(); $test->server('server', 'DELETE', $data); - $this->assertEquals(Test::TEST_URL, $test->curl->baseUrl); $this->assertEquals(Test::TEST_URL . '?' . http_build_query($data), $test->curl->url); // curl -v --get --request HEAD --head "http://127.0.0.1:8000/" --data "foo=bar" $test = new Test(); $test->server('server', 'HEAD', $data); - $this->assertEquals(Test::TEST_URL, $test->curl->baseUrl); $this->assertEquals(Test::TEST_URL . '?' . http_build_query($data), $test->curl->url); // curl -v --get --request OPTIONS "http://127.0.0.1:8000/" --data "foo=bar" $test = new Test(); $test->server('server', 'OPTIONS', $data); - $this->assertEquals(Test::TEST_URL, $test->curl->baseUrl); $this->assertEquals(Test::TEST_URL . '?' . http_build_query($data), $test->curl->url); } public function testSetUrlInConstructor() { - $data = array('key' => 'value'); + $data = ['key' => 'value']; $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'delete_with_body'); - $curl->delete($data, array('wibble' => 'wubble')); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); + $curl->delete($data, ['wibble' => 'wubble']); $this->assertEquals('{"get":{"key":"value"},"delete":{"wibble":"wubble"}}', $curl->rawResponse); $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->delete($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); $this->assertEquals('key=value', $curl->response); $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->get($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); $this->assertEquals('key=value', $curl->response); $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->head($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); $this->assertEquals('HEAD /?key=value HTTP/1.1', $curl->requestHeaders['Request-Line']); $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->options($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); $this->assertEquals('key=value', $curl->response); $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'request_method'); $curl->patch($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); $this->assertEquals('PATCH', $curl->response); $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'post'); $curl->post($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); $this->assertEquals('key=value', $curl->response); $curl = new Curl(/service/test::TEST_URL); $curl->setHeader('X-DEBUG-TEST', 'put'); $curl->put($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); + $this->assertEquals('key=value', $curl->response); + + $curl = new Curl(/service/test::TEST_URL); + $curl->setHeader('X-DEBUG-TEST', 'search'); + $curl->search($data); $this->assertEquals('key=value', $curl->response); } public function testSetUrl() { - $data = array('key' => 'value'); + $data = ['key' => 'value']; $curl = new Curl(); - $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->setUrl(Test::TEST_URL); $curl->delete($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); - $this->assertEquals('key=value', $curl->response); + $this->assertEquals('DELETE /?key=value HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL . '?key=value', $curl->effectiveUrl); $curl = new Curl(); - $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->setUrl(Test::TEST_URL); $curl->get($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); - $this->assertEquals('key=value', $curl->response); + $this->assertEquals('GET /?key=value HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL . '?key=value', $curl->effectiveUrl); $curl = new Curl(); - $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->setUrl(Test::TEST_URL); $curl->head($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); $this->assertEquals('HEAD /?key=value HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL . '?key=value', $curl->effectiveUrl); $curl = new Curl(); - $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->setUrl(Test::TEST_URL); $curl->options($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); - $this->assertEquals('key=value', $curl->response); + $this->assertEquals('OPTIONS /?key=value HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL . '?key=value', $curl->effectiveUrl); $curl = new Curl(); - $curl->setHeader('X-DEBUG-TEST', 'request_method'); $curl->setUrl(Test::TEST_URL); $curl->patch($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); - $this->assertEquals('PATCH', $curl->response); + $this->assertEquals('PATCH / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); $curl = new Curl(); - $curl->setHeader('X-DEBUG-TEST', 'post'); $curl->setUrl(Test::TEST_URL); $curl->post($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); - $this->assertEquals('key=value', $curl->response); + $this->assertEquals('POST / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); $curl = new Curl(); - $curl->setHeader('X-DEBUG-TEST', 'put'); $curl->setUrl(Test::TEST_URL); $curl->put($data); - $this->assertEquals(Test::TEST_URL, $curl->baseUrl); - $this->assertEquals('key=value', $curl->response); + $this->assertEquals('PUT / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + + $curl = new Curl(); + $curl->setUrl(Test::TEST_URL); + $curl->search($data); + $this->assertEquals('SEARCH / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + } + + public function testEffectiveUrl() + { + $test = new Test(); + $test->server('redirect', 'GET'); + $this->assertEquals(Test::TEST_URL, $test->curl->effectiveUrl); + + $test = new Test(); + $test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true); + $test->server('redirect', 'GET'); + $this->assertEquals(Test::TEST_URL . '?redirect', $test->curl->effectiveUrl); + + $test = new Test(); + $test->server('get', 'GET'); + $this->assertEquals(Test::TEST_URL, $test->curl->effectiveUrl); + $test->server('get', 'GET', ['a' => '1', 'b' => '2']); + $this->assertEquals(Test::TEST_URL . '?a=1&b=2', $test->curl->effectiveUrl); } public function testPostRequestMethod() @@ -278,7 +304,7 @@ public function testPostContinueResponseHeader() 'Connection: keep-alive' . "\r\n" . "\r\n"; - $reflector = new ReflectionClass('\Curl\Curl'); + $reflector = new \ReflectionClass('\Curl\Curl'); $reflection_method = $reflector->getMethod('parseResponseHeaders'); $reflection_method->setAccessible(true); @@ -291,42 +317,47 @@ public function testPostContinueResponseHeader() public function testPostData() { $test = new Test(); - $this->assertEquals('key=value', $test->server('post', 'POST', array( + $this->assertEquals('key=value', $test->server('post', 'POST', [ 'key' => 'value', - ))); + ])); + } + + public function testPostDataEmptyJson() + { + $test = new Test(); + $test->curl->setHeader('Content-Type', 'application/json'); + $test->server('post_json', 'POST'); + $this->assertEquals('', $test->curl->response); + $this->assertEquals('', $test->curl->getOpt(CURLOPT_POSTFIELDS)); } public function testPostAssociativeArrayData() { + $data = [ + 'username' => 'myusername', + 'password' => 'mypassword', + 'more_data' => [ + 'param1' => 'something', + 'param2' => 'other thing', + 'param3' => 123, + 'param4' => 3.14, + ], + ]; + $test = new Test(); - $this->assertEquals( - 'username=myusername' . - '&password=mypassword' . - '&more_data%5Bparam1%5D=something' . - '&more_data%5Bparam2%5D=other%20thing' . - '&more_data%5Bparam3%5D=123' . - '&more_data%5Bparam4%5D=3.14', - $test->server('post_multidimensional', 'POST', array( - 'username' => 'myusername', - 'password' => 'mypassword', - 'more_data' => array( - 'param1' => 'something', - 'param2' => 'other thing', - 'param3' => 123, - 'param4' => 3.14, - ), - )) - ); + $test->curl->setDefaultJsonDecoder(true); + $response = $test->server('post_multidimensional', 'POST', $data); + $this->assertEquals($data, $response['post']); } public function testPostContentLength() { - $test_data = array( - array(false, 0), - array('', 0), - array(array(), 0), - array(null, 0), - ); + $test_data = [ + [false, 0], + ['', 0], + [[], 0], + [null, 0], + ]; foreach ($test_data as $data) { $test = new Test(); list($post_data, $expected_content_length) = $data; @@ -341,29 +372,105 @@ public function testPostContentLength() public function testPostMultidimensionalData() { - $test = new Test(); + $data = [ + 'key' => 'file', + 'file' => [ + 'wibble', + 'wubble', + 'wobble', + ], + ]; + $this->assertEquals( - 'key=file&file%5B%5D=wibble&file%5B%5D=wubble&file%5B%5D=wobble', - $test->server('post_multidimensional', 'POST', array( - 'key' => 'file', - 'file' => array( - 'wibble', - 'wubble', - 'wobble', - ), - )) + 'key=file&file[0]=wibble&file[1]=wubble&file[2]=wobble', + urldecode(http_build_query($data)) ); + + $test = new Test(); + $test->curl->setDefaultJsonDecoder(true); + $response = $test->server('post_multidimensional', 'POST', $data); + $this->assertEquals($data, $response['post']); + } + + public function testPostMultidimensionalDataWithFile() + { + $tests = []; + + $file_path_1 = \Helper\get_png(); + $tests[] = [ + 'file_path' => $file_path_1, + 'post_data_image' => '@' . $file_path_1, + ]; + + if (class_exists('CURLFile')) { + $file_path_2 = \Helper\get_png(); + $tests[] = [ + 'file_path' => $file_path_2, + 'post_data_image' => new \CURLFile($file_path_2), + ]; + } + + if (class_exists('CURLStringFile')) { + $file_path_3 = \Helper\get_png(); + $tests[] = [ + 'file_path' => $file_path_3, + 'post_data_image' => new \CURLStringFile(file_get_contents($file_path_3), 'image', 'image/png'), + ]; + } + + foreach ($tests as $test_data) { + $file_path = $test_data['file_path']; + $post_data_image = $test_data['post_data_image']; + + $test = new Test(); + + // Return associative for comparison. + $assoc = true; + $test->curl->setDefaultJsonDecoder($assoc); + + // Keep POST data separate from FILES data for comparison. + $post_data_without_file = [ + 'key' => 'value', + 'alpha' => [ + 'a' => '1', + 'b' => '2', + 'c' => '3', + ], + ]; + $post_data = $post_data_without_file; + $post_data['image'] = $post_data_image; + + $test->server('post_multidimensional_with_file', 'POST', $post_data); + + // Expect "Content-Type: multipart/form-data" in request headers. + preg_match( + '/^multipart\/form-data; boundary=/', + $test->curl->requestHeaders['Content-Type'], + $content_type + ); + $this->assertTrue(!empty($content_type)); + + // Expect received POST data to match POSTed data less the file. + $this->assertEquals($post_data_without_file, $test->curl->response['post']); + + // Expect POSTed files is received as $_FILES. + $this->assertTrue(isset($test->curl->response['files']['image']['tmp_name'])); + $this->assertEquals(0, $test->curl->response['files']['image']['error']); + + unlink($file_path); + $this->assertFalse(file_exists($file_path)); + } } public function testPostFilePathUpload() { - $file_path = Helper\get_png(); + $file_path = \Helper\get_png(); $test = new Test(); - $this->assertEquals('image/png', $test->server('post_file_path_upload', 'POST', array( + $this->assertEquals('image/png', $test->server('post_file_path_upload', 'POST', [ 'key' => 'image', 'image' => '@' . $file_path, - ))); + ])); unlink($file_path); $this->assertFalse(file_exists($file_path)); @@ -371,31 +478,67 @@ public function testPostFilePathUpload() public function testPostCurlFileUpload() { - if (class_exists('CURLFile')) { - $file_path = Helper\get_png(); + if (!class_exists('CURLFile')) { + $this->markTestSkipped(); + } - $test = new Test(); - $this->assertEquals('image/png', $test->server('post_file_path_upload', 'POST', array( - 'key' => 'image', - 'image' => new CURLFile($file_path), - ))); + $file_path = \Helper\get_png(); - unlink($file_path); - $this->assertFalse(file_exists($file_path)); + $test = new Test(); + $this->assertEquals('image/png', $test->server('post_file_path_upload', 'POST', [ + 'key' => 'image', + 'image' => new \CURLFile($file_path), + ])); + + unlink($file_path); + $this->assertFalse(file_exists($file_path)); + } + + public function testPostCurlStringFileUpload() + { + if (!class_exists('CURLStringFile')) { + $this->markTestSkipped(); } + + $file_path = \Helper\get_png(); + + $test = new Test(); + $this->assertEquals('image/png', $test->server('post_file_path_upload', 'POST', [ + 'key' => 'image', + 'image' => new \CURLStringFile(file_get_contents($file_path), 'image', 'image/png'), + ])); + + unlink($file_path); + $this->assertFalse(file_exists($file_path)); } public function testPostNonFilePathUpload() { $test = new Test(); - $test->server('post', 'POST', array( + $test->server('post', 'POST', [ 'foo' => 'bar', 'file' => '@not-a-file', - )); + ]); $this->assertFalse($test->curl->error); $this->assertEquals('foo=bar&file=%40not-a-file', $test->curl->response); } + public function testPostRedirectGet() + { + // Follow 303 redirection with GET + $test = new Test(); + $test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true); + $this->assertEquals('Redirected: GET', $test->server('post_redirect_get', 'POST')); + + // Follow 303 redirection with POST + $test = new Test(); + $test->curl->setOpt(CURLOPT_FOLLOWLOCATION, true); + $this->assertEquals('Redirected: POST', $test->server('post_redirect_get', 'POST', [], true)); + + // Ensure that it is possible to reuse an existing Curl object. + $this->assertEquals('Redirected: GET', $test->server('post_redirect_get', 'POST')); + } + public function testPutRequestMethod() { $test = new Test(); @@ -405,20 +548,20 @@ public function testPutRequestMethod() public function testPutData() { $test = new Test(); - $this->assertEquals('key=value', $test->server('put', 'PUT', array( + $this->assertEquals('key=value', $test->server('put', 'PUT', [ 'key' => 'value', - ))); + ])); $test = new Test(); - $this->assertEquals('{"key":"value"}', $test->server('put', 'PUT', json_encode(array( + $this->assertEquals('{"key":"value"}', $test->server('put', 'PUT', json_encode([ 'key' => 'value', - )))); + ]))); } public function testPutFileHandle() { - $png = Helper\create_png(); - $tmp_file = Helper\create_tmp_file($png); + $png = \Helper\create_png(); + $tmp_file = \Helper\create_tmp_file($png); $test = new Test(); $test->curl->setHeader('X-DEBUG-TEST', 'put_file_handle'); @@ -432,6 +575,32 @@ public function testPutFileHandle() $this->assertEquals('image/png', $test->curl->response); } + public function testMultipartFormDataContentType() + { + // Use a PUT request instead of a POST request so the request + // multipart/form-data is not automatically parsed and can be tested + // against. + $test = new Test(); + $test->curl->setHeader('Content-Type', 'multipart/form-data'); + $test->server('put', 'PUT', [ + 'foo' => 'bar', + ]); + + // Check the "expect" header value only when it is provided in the request. + if (isset($test->curl->requestHeaders['Expect'])) { + $this->assertEquals('100-continue', $test->curl->requestHeaders['Expect']); + } + + $this->assertStringStartsWith('multipart/form-data; boundary=', $test->curl->requestHeaders['Content-Type']); + + $expected_contains = "\r\n" . + 'Content-Disposition: form-data; name="foo"' . "\r\n" . + "\r\n" . + 'bar' . "\r\n" . + ''; + $this->assertStringContainsString($expected_contains, $test->curl->response); + } + public function testPatchRequestMethod() { $test = new Test(); @@ -441,24 +610,24 @@ public function testPatchRequestMethod() public function testPatchData() { $test = new Test(); - $this->assertEquals('key=value', $test->server('patch', 'PATCH', array( + $this->assertEquals('key=value', $test->server('patch', 'PATCH', [ 'key' => 'value', - ))); + ])); $test = new Test(); - $this->assertEquals('{"key":"value"}', $test->server('patch', 'PATCH', json_encode(array( + $this->assertEquals('{"key":"value"}', $test->server('patch', 'PATCH', json_encode([ 'key' => 'value', - )))); + ]))); } public function testPatchRequestMethodWithMultidimArray() { - $data = array( - 'data' => array( + $data = [ + 'data' => [ 'foo' => 'bar', 'wibble' => 'wubble', - ), - ); + ], + ]; $curl = new Curl(); $curl->setHeader('X-DEBUG-TEST', 'data_values'); @@ -476,10 +645,26 @@ public function testDeleteRequestMethod() public function testDeleteRequestBody() { $test = new Test(); - $test->server('delete_with_body', 'DELETE', array('foo' => 'bar'), array('wibble' => 'wubble')); + $test->server('delete_with_body', 'DELETE', ['foo' => 'bar'], ['wibble' => 'wubble']); $this->assertEquals('{"get":{"foo":"bar"},"delete":{"wibble":"wubble"}}', $test->curl->rawResponse); } + public function testDeleteContentLengthSetWithBody() + { + $request_body = 'a=1&b=2&c=3'; + $test = new Test(); + $test->server('request_method', 'DELETE', [], $request_body); + $this->assertEquals(strlen($request_body), $test->curl->requestHeaders['content-length']); + } + + public function testDeleteContentLengthUnsetWithoutBody() + { + $request_body = []; + $test = new Test(); + $test->server('request_method', 'DELETE', [], $request_body); + $this->assertFalse(isset($test->curl->requestHeaders['content-length'])); + } + public function testHeadRequestMethod() { $test = new Test(); @@ -495,30 +680,25 @@ public function testOptionsRequestMethod() $this->assertEquals('OPTIONS', $test->curl->responseHeaders['X-REQUEST-METHOD']); } - public function testDownload() + public function testDownloadToFile() { - // Upload a file. - $upload_file_path = Helper\get_png(); - $upload_test = new Test(); - $upload_test->server('upload_response', 'POST', array( - 'image' => '@' . $upload_file_path, - )); - $uploaded_file_path = $upload_test->curl->response->file_path; - $this->assertNotEquals($upload_file_path, $uploaded_file_path); - $this->assertEquals(md5_file($upload_file_path), $upload_test->curl->responseHeaders['ETag']); + // Create and upload a file. + $upload_file_path = \Helper\get_png(); + $uploaded_file_path = \Helper\upload_file_to_server($upload_file_path); // Download the file. $downloaded_file_path = tempnam('/tmp', 'php-curl-class.'); $download_test = new Test(); $download_test->curl->setHeader('X-DEBUG-TEST', 'download_response'); - $this->assertTrue($download_test->curl->download(Test::TEST_URL . '?' . http_build_query(array( + $this->assertTrue($download_test->curl->download(Test::TEST_URL . '?' . http_build_query([ 'file_path' => $uploaded_file_path, - )), $downloaded_file_path)); + ]), $downloaded_file_path)); $this->assertNotEquals($uploaded_file_path, $downloaded_file_path); $this->assertEquals(filesize($upload_file_path), filesize($downloaded_file_path)); $this->assertEquals(md5_file($upload_file_path), md5_file($downloaded_file_path)); $this->assertEquals(md5_file($upload_file_path), $download_test->curl->responseHeaders['ETag']); + $this->assertEquals($download_test->curl->downloadFileName, $downloaded_file_path . '.pccdownload'); // Ensure successive requests set the appropriate values. $this->assertEquals('GET', $download_test->server('request_method', 'GET')); @@ -526,53 +706,357 @@ public function testDownload() $this->assertFalse(is_bool($download_test->curl->rawResponse)); // Remove server file. - $this->assertEquals('true', $download_test->server('upload_cleanup', 'POST', array( - 'file_path' => $uploaded_file_path, - ))); + \Helper\remove_file_from_server($uploaded_file_path); unlink($upload_file_path); unlink($downloaded_file_path); $this->assertFalse(file_exists($upload_file_path)); - $this->assertFalse(file_exists($uploaded_file_path)); $this->assertFalse(file_exists($downloaded_file_path)); } public function testDownloadCallback() { - // Upload a file. - $upload_file_path = Helper\get_png(); - $upload_test = new Test(); - $upload_test->server('upload_response', 'POST', array( - 'image' => '@' . $upload_file_path, - )); - $uploaded_file_path = $upload_test->curl->response->file_path; + // Create and upload a file. + $upload_file_path = \Helper\get_png(); + $uploaded_file_path = \Helper\upload_file_to_server($upload_file_path); // Download the file. - $callback_called = false; + $download_callback_called = false; $curl = new Curl(); $curl->setHeader('X-DEBUG-TEST', 'download_response'); - $curl->download(Test::TEST_URL . '?' . http_build_query(array( + $curl->download(Test::TEST_URL . '?' . http_build_query([ 'file_path' => $uploaded_file_path, - )), function($instance, $fh) use (&$callback_called) { - PHPUnit_Framework_Assert::assertFalse($callback_called); - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue(is_resource($fh)); - PHPUnit_Framework_Assert::assertEquals('stream', get_resource_type($fh)); - PHPUnit_Framework_Assert::assertGreaterThan(0, strlen(stream_get_contents($fh))); - PHPUnit_Framework_Assert::assertEquals(0, strlen(stream_get_contents($fh))); - PHPUnit_Framework_Assert::assertTrue(fclose($fh)); - $callback_called = true; + ]), function ($instance, $fh) use (&$download_callback_called) { + \PHPUnit\Framework\Assert::assertFalse($download_callback_called); + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue(is_resource($fh)); + \PHPUnit\Framework\Assert::assertEquals('stream', get_resource_type($fh)); + \PHPUnit\Framework\Assert::assertGreaterThan(0, strlen(stream_get_contents($fh))); + \PHPUnit\Framework\Assert::assertEquals(0, strlen(stream_get_contents($fh))); + \PHPUnit\Framework\Assert::assertTrue(fclose($fh)); + $download_callback_called = true; }); - $this->assertTrue($callback_called); + $this->assertTrue($download_callback_called); // Remove server file. - $this->assertEquals('true', $upload_test->server('upload_cleanup', 'POST', array( - 'file_path' => $uploaded_file_path, - ))); + \Helper\remove_file_from_server($uploaded_file_path); unlink($upload_file_path); $this->assertFalse(file_exists($upload_file_path)); - $this->assertFalse(file_exists($uploaded_file_path)); + } + + public function testDownloadRange() + { + // Create and upload a file. + $filename = \Helper\get_png(); + $uploaded_file_path = \Helper\upload_file_to_server($filename); + + $filesize = filesize($filename); + + foreach ( + [ + false, + 0, + 1, + 2, + 3, + 5, + 10, + 25, + 50, + $filesize - 3, + $filesize - 2, + $filesize - 1, + + // A partial temporary file having the exact same file size as the complete source file should only + // occur under certain circumstances (almost never). When the download successfully completed, the + // temporary file should have been moved to the download destination save path. However, it is possible + // that a larger file download was interrupted after which the source file was updated and now has the + // exact same file size as the partial temporary. When resuming the download, the range is now + // unsatisfiable as the first byte position exceeds the available range. The entire file should be + // downloaded again. + $filesize - 0, + + // A partial temporary file having a larger file size than the complete source file should only occur + // under certain circumstances. This is possible when a download was interrupted after which the source + // file was updated with a smaller file. When resuming the download, the range is now unsatisfiable as + // the first byte position exceeds the the available range. The entire file should be downloaded again. + $filesize + 1, + $filesize + 2, + $filesize + 3, + + ] as $length + ) { + $source = Test::TEST_URL; + $destination = \Helper\get_tmp_file_path(); + + // Start with no file. + if ($length === false) { + $this->assertFalse(file_exists($destination)); + + // Start with $length bytes of file. + } else { + // Simulate resuming partially downloaded temporary file. + $partial_filename = $destination . '.pccdownload'; + + if ($length === 0) { + $partial_content = ''; + } else { + $file = fopen($filename, 'rb'); + $partial_content = fread($file, $length); + fclose($file); + } + + // Partial content size should be $length bytes large for testing resume download behavior. + if ($length <= $filesize) { + $this->assertEquals($length, strlen($partial_content)); + + // Partial content should not be larger than the original file size. + } else { + $this->assertEquals($filesize, strlen($partial_content)); + } + + file_put_contents($partial_filename, $partial_content); + $this->assertEquals(strlen($partial_content), strlen(file_get_contents($partial_filename))); + } + + // Download (the remaining bytes of) the file. + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'download_file_range'); + $curl->download($source . '?' . http_build_query([ + 'file_path' => $uploaded_file_path, + ]), $destination); + + clearstatcache(); + + $expected_bytes_downloaded = $filesize - min($length, $filesize); + $bytes_downloaded = $curl->responseHeaders['content-length']; + if ($length === false || $length === 0) { + $expected_http_status_code = 200; // 200 OK + $this->assertEquals($expected_bytes_downloaded, $bytes_downloaded); + } elseif ($length >= $filesize) { + $expected_http_status_code = 416; // 416 Requested Range Not Satisfiable + } else { + $expected_http_status_code = 206; // 206 Partial Content + $this->assertEquals($expected_bytes_downloaded, $bytes_downloaded); + } + $this->assertEquals($expected_http_status_code, $curl->httpStatusCode); + + if (!$curl->error) { + $this->assertEquals($filesize, filesize($destination)); + unlink($destination); + $this->assertFalse(file_exists($destination)); + } + } + + // Remove server file. + \Helper\remove_file_from_server($uploaded_file_path); + + unlink($filename); + $this->assertFalse(file_exists($filename)); + } + + public function testDownloadErrorDeleteTemporaryFile() + { + $destination = \Helper\get_tmp_file_path(); + + $test = new Test(); + $test->curl->setHeader('X-DEBUG-TEST', '404'); + $test->curl->download(Test::TEST_URL, $destination); + + $this->assertFalse(file_exists($test->curl->downloadFileName)); + $this->assertFalse(file_exists($destination)); + } + + public function testDownloadCallbackError() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $download_before_send_called = false; + $download_callback_called = false; + $curl = new Curl(); + $curl->beforeSend(function ($instance) use (&$download_before_send_called) { + \PHPUnit\Framework\Assert::assertFalse($download_before_send_called); + $download_before_send_called = true; + }); + $curl->download(Test::ERROR_URL, function ($instance, $fh) use (&$download_callback_called) { + $download_callback_called = true; + }); + $this->assertTrue($download_before_send_called); + $this->assertFalse($download_callback_called); + } + + public function testFastDownloadSuccessOnly() + { + // Create a local file. + $local_file_path = \Helper\get_png(); + + // Upload file to server. + $uploaded_server_file_path = \Helper\upload_file_to_server($local_file_path); + + // Download server file and save locally. + $url = Test::TEST_URL . '?' . http_build_query([ + 'file_path' => $uploaded_server_file_path, + ]); + $downloaded_local_file_path = \Helper\get_tmp_file_path(); + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'download_response'); + $curl->fastDownload($url, $downloaded_local_file_path); + + $this->assertEquals(md5_file($local_file_path), md5_file($downloaded_local_file_path)); + + // Remove server file. + \Helper\remove_file_from_server($uploaded_server_file_path); + + unlink($local_file_path); + $this->assertFalse(file_exists($local_file_path)); + + unlink($downloaded_local_file_path); + $this->assertFalse(file_exists($downloaded_local_file_path)); + } + + public function testFastDownloadFailOnly() + { + $url = Test::TEST_URL . '?failures=1'; + $downloaded_local_file_path = \Helper\get_tmp_file_path(); + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'retry'); + $response = $curl->fastDownload($url, $downloaded_local_file_path); + + $this->assertFalse($response); + } + + public function testFastDownloadSuccessFail() + { + $url = Test::TEST_URL . '?failures=0,1'; + $downloaded_local_file_path = \Helper\get_tmp_file_path(); + $cookie_jar = __DIR__ . '/cookiejar.txt'; + $connections = 1; + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'retry'); + $curl->setCookieFile($cookie_jar); + $curl->setCookieJar($cookie_jar); + $response = $curl->fastDownload($url, $downloaded_local_file_path, $connections); + + $this->assertFalse($response); + $this->assertTrue(unlink($cookie_jar)); + } + + public function testFastDownloadSuccessSuccessFail() + { + $url = Test::TEST_URL . '?failures=0,0,1'; + $downloaded_local_file_path = \Helper\get_tmp_file_path(); + $cookie_jar = __DIR__ . '/cookiejar.txt'; + $connections = 2; + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'retry'); + $curl->setCookieFile($cookie_jar); + $curl->setCookieJar($cookie_jar); + $response = $curl->fastDownload($url, $downloaded_local_file_path, $connections); + + $this->assertFalse($response); + $this->assertTrue(unlink($cookie_jar)); + } + + public function testMaxFilesize() + { + $tests = [ + [ + 'bytes' => 1, + 'max_filesize' => false, + 'expect_error' => false, + ], + [ + 'bytes' => 1, + 'max_filesize' => 1, + 'expect_error' => false, + ], + [ + 'bytes' => 1, + 'max_filesize' => 2, + 'expect_error' => false, + ], + [ + 'bytes' => 1, + 'max_filesize' => 0, + 'expect_error' => true, + ], + + [ + 'bytes' => 2, + 'max_filesize' => false, + 'expect_error' => false, + ], + [ + 'bytes' => 2, + 'max_filesize' => 2, + 'expect_error' => false, + ], + [ + 'bytes' => 2, + 'max_filesize' => 3, + 'expect_error' => false, + ], + [ + 'bytes' => 2, + 'max_filesize' => 1, + 'expect_error' => true, + ], + + [ + 'bytes' => 1000, + 'max_filesize' => false, + 'expect_error' => false, + ], + [ + 'bytes' => 1000, + 'max_filesize' => 1000, + 'expect_error' => false, + ], + [ + 'bytes' => 1000, + 'max_filesize' => 1001, + 'expect_error' => false, + ], + [ + 'bytes' => 1000, + 'max_filesize' => 999, + 'expect_error' => true, + ], + [ + 'bytes' => 1000, + 'max_filesize' => 0, + 'expect_error' => true, + ], + ]; + foreach ($tests as $test) { + $bytes = $test['bytes']; + $max_filesize = $test['max_filesize']; + $expect_error = $test['expect_error']; + + $test = new Test(); + if ($max_filesize !== false) { + $test->curl->setMaxFilesize($max_filesize); + } + $test->server('download_file_size', 'GET', [ + 'bytes' => $bytes, + ]); + + // Ensure exceeding download limit aborts the transfer and sets a CURLE_ABORTED_BY_CALLBACK error. + if ($expect_error) { + $this->assertTrue($test->curl->error); + $this->assertEquals(CURLE_ABORTED_BY_CALLBACK, $test->curl->errorCode); + } else { + $str = str_repeat('.', $bytes); + $this->assertEquals(md5($str), $test->curl->responseHeaders['etag']); + $this->assertEquals($str, $test->curl->response); + } + } } public function testBasicHttpAuth() @@ -592,12 +1076,6 @@ public function testBasicHttpAuth() public function testDigestHttpAuth() { - // Skip Digest Access Authentication test on HHVM. - // https://github.com/facebook/hhvm/issues/5201 - if (defined('HHVM_VERSION')) { - return; - } - $username = 'myusername'; $password = 'mypassword'; $invalid_password = 'anotherpassword'; @@ -624,84 +1102,97 @@ public function testReferrer() { $test = new Test(); $test->curl->setReferrer('myreferrer'); - $this->assertEquals('myreferrer', $test->server('server', 'GET', array( + $this->assertEquals('myreferrer', $test->server('server', 'GET', [ 'key' => 'HTTP_REFERER', - ))); + ])); $test = new Test(); $test->curl->setReferer('myreferer'); - $this->assertEquals('myreferer', $test->server('server', 'GET', array( + $this->assertEquals('myreferer', $test->server('server', 'GET', [ 'key' => 'HTTP_REFERER', - ))); + ])); } public function testResponseBody() { - foreach (array( + foreach ( + [ 'GET' => 'OK', 'POST' => 'OK', 'PUT' => 'OK', 'PATCH' => 'OK', + 'SEARCH' => 'OK', 'DELETE' => 'OK', 'HEAD' => '', 'OPTIONS' => 'OK', - ) as $request_method => $expected_response) { + ] as $request_method => $expected_response + ) { $curl = new Curl(); $curl->setHeader('X-DEBUG-TEST', 'response_body'); $this->assertEquals($expected_response, $curl->$request_method(Test::TEST_URL)); } } - public function testCookies() + public function testSetCookie() { $test = new Test(); $test->curl->setCookie('mycookie', 'yum'); - $this->assertEquals('yum', $test->server('cookie', 'GET', array( - 'key' => 'mycookie', - ))); + $test->server('setcookie', 'GET'); + $this->assertEquals('yum', $test->curl->responseCookies['mycookie']); + } + + public function testSetCookies() + { + $cookies = [ + 'mycookie' => 'yum', + 'fruit' => 'apple', + 'color' => 'red', + ]; + $test = new Test(); + $test->curl->setCookies($cookies); + $test->server('setcookie', 'GET'); + + $this->assertEquals('yum', $test->curl->responseCookies['mycookie']); + $this->assertEquals('apple', $test->curl->responseCookies['fruit']); + $this->assertEquals('red', $test->curl->responseCookies['color']); } - public function testCookieEncodingSpace() + public function testSetCookieEncodingSpace() { $curl = new Curl(); $curl->setCookie('cookie', 'Om nom nom nom'); - - $reflectionClass = new ReflectionClass('\Curl\Curl'); - $reflectionProperty = $reflectionClass->getProperty('options'); - $reflectionProperty->setAccessible(true); - $options = $reflectionProperty->getValue($curl); - $this->assertEquals('cookie=Om%20nom%20nom%20nom', $options[CURLOPT_COOKIE]); + $this->assertEquals('cookie=Om%20nom%20nom%20nom', $curl->getOpt(CURLOPT_COOKIE)); } - public function testMultipleCookies() + public function testSetMultipleCookies() { $curl = new Curl(); $curl->setCookie('cookie', 'Om nom nom nom'); $curl->setCookie('foo', 'bar'); - - $reflectionClass = new ReflectionClass('\Curl\Curl'); - $reflectionProperty = $reflectionClass->getProperty('options'); - $reflectionProperty->setAccessible(true); - $options = $reflectionProperty->getValue($curl); - $this->assertEquals('cookie=Om%20nom%20nom%20nom; foo=bar', $options[CURLOPT_COOKIE]); + $this->assertEquals('cookie=Om%20nom%20nom%20nom; foo=bar', $curl->getOpt(CURLOPT_COOKIE)); } - public function testCookieEncodingColon() + public function testSetCookieEncodingColon() { $curl = new Curl(); $curl->setCookie('JSESSIONID', '0000wd-PcsB3bZ-KzYGAqm_rKlm:17925chrl'); + $this->assertEquals('JSESSIONID=0000wd-PcsB3bZ-KzYGAqm_rKlm:17925chrl', $curl->getOpt(CURLOPT_COOKIE)); + } + + public function testSetCookieString() + { + $cookie_string = 'fruit=apple; color=red'; - $reflectionClass = new ReflectionClass('\Curl\Curl'); - $reflectionProperty = $reflectionClass->getProperty('options'); - $reflectionProperty->setAccessible(true); - $options = $reflectionProperty->getValue($curl); - $this->assertEquals('JSESSIONID=0000wd-PcsB3bZ-KzYGAqm_rKlm:17925chrl', $options[CURLOPT_COOKIE]); + $test = new Test(); + $test->curl->setCookieString($cookie_string); + $this->assertEquals($cookie_string, $test->curl->getOpt(CURLOPT_COOKIE)); + $this->assertEquals('fruit=apple&color=red', $test->server('cookie', 'GET')); } public function testCookieFile() { - $cookie_file = dirname(__FILE__) . '/cookies.txt'; - $cookie_data = implode("\t", array( + $cookie_file = __DIR__ . '/cookiefile.txt'; + $cookie_data = implode("\t", [ '127.0.0.1', // domain 'FALSE', // tailmatch '/', // path @@ -709,14 +1200,15 @@ public function testCookieFile() '0', // expires 'mycookie', // name 'yum', // value - )); + ]) . "\n"; file_put_contents($cookie_file, $cookie_data); $test = new Test(); $test->curl->setCookieFile($cookie_file); - $this->assertEquals('yum', $test->server('cookie', 'GET', array( + $this->assertEquals($cookie_data, file_get_contents($test->curl->getOpt(CURLOPT_COOKIEFILE))); + $this->assertEquals('yum', $test->server('cookie', 'GET', [ 'key' => 'mycookie', - ))); + ])); unlink($cookie_file); $this->assertFalse(file_exists($cookie_file)); @@ -724,16 +1216,16 @@ public function testCookieFile() public function testCookieJar() { - $cookie_file = dirname(__FILE__) . '/cookies.txt'; + $cookie_jar = __DIR__ . '/cookiejar.txt'; $test = new Test(); - $test->curl->setCookieJar($cookie_file); + $test->curl->setCookieJar($cookie_jar); $test->server('cookiejar', 'GET'); $test->curl->close(); - $this->assertTrue(!(strpos(file_get_contents($cookie_file), "\t" . 'mycookie' . "\t" . 'yum') === false)); - unlink($cookie_file); - $this->assertFalse(file_exists($cookie_file)); + $this->assertTrue(strpos(file_get_contents($cookie_jar), "\t" . 'mycookie' . "\t" . 'yum') !== false); + unlink($cookie_jar); + $this->assertFalse(file_exists($cookie_jar)); } public function testMultipleCookieResponse() @@ -741,14 +1233,21 @@ public function testMultipleCookieResponse() $test = new Test(); $test->server('multiple_cookie', 'GET'); $this->assertEquals('cookie1=scrumptious,cookie2=mouthwatering', $test->curl->responseHeaders['Set-Cookie']); + + $this->assertEquals('scrumptious', $test->curl->responseCookies['cookie1']); + $this->assertEquals('mouthwatering', $test->curl->responseCookies['cookie2']); } public function testDefaultTimeout() { - $test = new Test(); - $test->server('timeout', 'GET', array( + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $test = new Test('8001'); + $test->server('timeout', 'GET', [ 'seconds' => '31', - )); + ]); $this->assertTrue($test->curl->error); $this->assertTrue($test->curl->curlError); $this->assertEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->errorCode); @@ -758,11 +1257,15 @@ public function testDefaultTimeout() public function testTimeoutError() { - $test = new Test(); + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $test = new Test('8002'); $test->curl->setTimeout(5); - $test->server('timeout', 'GET', array( + $test->server('timeout', 'GET', [ 'seconds' => '10', - )); + ]); $this->assertTrue($test->curl->error); $this->assertTrue($test->curl->curlError); $this->assertEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->errorCode); @@ -772,27 +1275,42 @@ public function testTimeoutError() public function testTimeout() { - $test = new Test(); + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $test = new Test('8003'); $test->curl->setTimeout(10); - $test->server('timeout', 'GET', array( + $test->server('timeout', 'GET', [ 'seconds' => '5', - )); - $this->assertFalse($test->curl->error); - $this->assertFalse($test->curl->curlError); - $this->assertNotEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->errorCode); - $this->assertNotEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->curlErrorCode); - $this->assertFalse($test->curl->httpError); + ]); + + $this->assertFalse($test->curl->error, $test->message); + $this->assertFalse($test->curl->curlError, $test->message); + $this->assertNotEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->errorCode, $test->message); + $this->assertNotEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->curlErrorCode, $test->message); + $this->assertFalse($test->curl->httpError, $test->message); } public function testError() { - $test = new Test(); - $test->curl->setOpt(CURLOPT_CONNECTTIMEOUT_MS, 4000); + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $test = new Test('8004'); $test->curl->get(Test::ERROR_URL); $this->assertTrue($test->curl->error); $this->assertTrue($test->curl->curlError); - $this->assertEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->errorCode); - $this->assertEquals(CURLE_OPERATION_TIMEOUTED, $test->curl->curlErrorCode); + $possible_errors = [CURLE_SEND_ERROR, CURLE_OPERATION_TIMEOUTED, CURLE_COULDNT_CONNECT, CURLE_GOT_NOTHING]; + $this->assertTrue( + in_array($test->curl->errorCode, $possible_errors, true), + 'errorCode: ' . $test->curl->errorCode + ); + $this->assertTrue( + in_array($test->curl->curlErrorCode, $possible_errors, true), + 'curlErrorCode: ' . $test->curl->curlErrorCode + ); } public function testErrorMessage() @@ -808,11 +1326,7 @@ public function testRequestHeaderCaseSensitivity() $curl = new Curl(); $curl->setHeader('Content-Type', $content_type); - $reflector = new ReflectionClass('\Curl\Curl'); - $property = $reflector->getProperty('headers'); - $property->setAccessible(true); - $headers = $property->getValue($curl); - + $headers = \Helper\get_curl_property_value($curl, 'headers'); $this->assertEquals($content_type, $headers['Content-Type']); $this->assertEquals($content_type, $headers['content-type']); $this->assertEquals($content_type, $headers['CONTENT-TYPE']); @@ -825,9 +1339,9 @@ public function testResponseHeaders() $test->curl->setHeader('Content-Type', 'application/json'); $test->curl->setHeader('X-Requested-With', 'XMLHttpRequest'); $test->curl->setHeader('Accept', 'application/json'); - $this->assertEquals('application/json', $test->server('server', 'GET', array('key' => 'CONTENT_TYPE'))); - $this->assertEquals('XMLHttpRequest', $test->server('server', 'GET', array('key' => 'HTTP_X_REQUESTED_WITH'))); - $this->assertEquals('application/json', $test->server('server', 'GET', array('key' => 'HTTP_ACCEPT'))); + $this->assertEquals('application/json', $test->server('server', 'GET', ['key' => 'CONTENT_TYPE'])); + $this->assertEquals('XMLHttpRequest', $test->server('server', 'GET', ['key' => 'HTTP_X_REQUESTED_WITH'])); + $this->assertEquals('application/json', $test->server('server', 'GET', ['key' => 'HTTP_ACCEPT'])); } public function testResponseHeaderCaseSensitivity() @@ -872,7 +1386,7 @@ public function testHeaderOutOptional() $test_3->curl->setOpt(CURLINFO_HEADER_OUT, false); $test_3->curl->verbose(); $test_3->server('response_header', 'GET'); - $this->assertNull($test_3->curl->requestHeaders); + $this->assertEmpty($test_3->curl->requestHeaders); } public function testHeaderRedirect() @@ -894,24 +1408,26 @@ public function testRequestUrl() $test = new Test(); $this->assertFalse(substr($test->server('request_uri', 'PATCH'), -1) === '?'); $test = new Test(); + $this->assertFalse(substr($test->server('request_uri', 'SEARCH'), -1) === '?'); + $test = new Test(); $this->assertFalse(substr($test->server('request_uri', 'DELETE'), -1) === '?'); } public function testNestedData() { $test = new Test(); - $data = array( + $data = [ 'username' => 'myusername', 'password' => 'mypassword', - 'more_data' => array( + 'more_data' => [ 'param1' => 'something', 'param2' => 'other thing', - 'another' => array( + 'another' => [ 'extra' => 'level', 'because' => 'I need it', - ), - ), - ); + ], + ], + ]; $this->assertEquals(http_build_query($data), $test->server('post', 'POST', $data)); } @@ -925,21 +1441,26 @@ public function testPostStringUrlEncodedContentType() public function testPostArrayUrlEncodedContentType() { $test = new Test(); - $test->server('server', 'POST', array( + $test->server('server', 'POST', [ 'foo' => 'bar', - )); + ]); $this->assertEquals('application/x-www-form-urlencoded', $test->curl->requestHeaders['Content-Type']); } public function testPostFileFormDataContentType() { - $file_path = Helper\get_png(); + $file_path = \Helper\get_png(); $test = new Test(); - $test->server('server', 'POST', array( + $test->server('server', 'POST', [ 'image' => '@' . $file_path, - )); - $this->assertEquals('100-continue', $test->curl->requestHeaders['Expect']); + ]); + + // Check the "expect" header value only when it is provided in the request. + if (isset($test->curl->requestHeaders['Expect'])) { + $this->assertEquals('100-continue', $test->curl->requestHeaders['Expect']); + } + preg_match('/^multipart\/form-data; boundary=/', $test->curl->requestHeaders['Content-Type'], $content_type); $this->assertTrue(!empty($content_type)); @@ -950,16 +1471,21 @@ public function testPostFileFormDataContentType() public function testPostCurlFileFormDataContentType() { if (!class_exists('CURLFile')) { - return; + $this->markTestSkipped(); } - $file_path = Helper\get_png(); + $file_path = \Helper\get_png(); $test = new Test(); - $test->server('server', 'POST', array( - 'image' => new CURLFile($file_path), - )); - $this->assertEquals('100-continue', $test->curl->requestHeaders['Expect']); + $test->server('server', 'POST', [ + 'image' => new \CURLFile($file_path), + ]); + + // Check the "expect" header value only when it is provided in the request. + if (isset($test->curl->requestHeaders['Expect'])) { + $this->assertEquals('100-continue', $test->curl->requestHeaders['Expect']); + } + preg_match('/^multipart\/form-data; boundary=/', $test->curl->requestHeaders['Content-Type'], $content_type); $this->assertTrue(!empty($content_type)); @@ -967,45 +1493,50 @@ public function testPostCurlFileFormDataContentType() $this->assertFalse(file_exists($file_path)); } - public function testJSONRequest() + public function testJsonRequest() { foreach ( - array( - array( - array( + [ + [ + [ 'key' => 'value', - ), + ], '{"key":"value"}', - ), - array( - array( + ], + [ + [ 'key' => 'value', - 'strings' => array( + 'strings' => [ 'a', 'b', 'c', - ), - ), + ], + ], '{"key":"value","strings":["a","b","c"]}', - ), - ) as $test) { + ], + ] as $test + ) { list($data, $expected_response) = $test; $test = new Test(); $this->assertEquals($expected_response, $test->server('post_json', 'POST', json_encode($data))); - foreach (array( + foreach ( + [ 'Content-Type', 'content-type', - 'CONTENT-TYPE') as $key) { - foreach (array( + 'CONTENT-TYPE'] as $key + ) { + foreach ( + [ 'APPLICATION/JSON', 'APPLICATION/JSON; CHARSET=UTF-8', 'APPLICATION/JSON;CHARSET=UTF-8', 'application/json', 'application/json; charset=utf-8', 'application/json;charset=UTF-8', - ) as $value) { + ] as $value + ) { $test = new Test(); $test->curl->setHeader($key, $value); $this->assertEquals($expected_response, $test->server('post_json', 'POST', json_encode($data))); @@ -1018,25 +1549,29 @@ public function testJSONRequest() } } - public function testJSONResponse() + public function testJsonResponse() { - foreach (array( + foreach ( + [ 'Content-Type', 'content-type', - 'CONTENT-TYPE') as $key) { - foreach (array( + 'CONTENT-TYPE'] as $key + ) { + foreach ( + [ 'APPLICATION/JSON', 'APPLICATION/JSON; CHARSET=UTF-8', 'APPLICATION/JSON;CHARSET=UTF-8', 'application/json', 'application/json; charset=utf-8', 'application/json;charset=UTF-8', - ) as $value) { + ] as $value + ) { $test = new Test(); - $test->server('json_response', 'POST', array( + $test->server('json_response', 'POST', [ 'key' => $key, 'value' => $value, - )); + ]); $response = $test->curl->response; $this->assertNotNull($response); @@ -1047,7 +1582,7 @@ public function testJSONResponse() $this->assertTrue(is_float($response->float)); $this->assertEmpty($response->empty); $this->assertTrue(is_string($response->string)); - $this->assertEquals(json_encode(array( + $this->assertEquals(json_encode([ 'null' => null, 'true' => true, 'false' => false, @@ -1055,35 +1590,74 @@ public function testJSONResponse() 'float' => 3.14, 'empty' => '', 'string' => 'string', - )), $test->curl->rawResponse); + ]), $test->curl->rawResponse); } } } - public function testJSONDecoder() + /** + * @expectedException \ErrorException + */ + public function testJsonEncode() { - $data = array( - 'key' => 'Content-Type', - 'value' => 'application/json', - ); + $this->expectException(\ErrorException::class); + $data = [ + 'malformed' => pack('H*', 'c32e'), + ]; + + $test = new Test(); + $test->curl->setHeader('Content-Type', 'application/json'); + $test->server('post_json', 'POST', $data); + } + + public function testJsonDecoderOptions() + { + // Implicit default json decoder should return object. $test = new Test(); - $test->server('json_response', 'POST', $data); + $test->server('json_response', 'GET'); + $this->assertTrue(is_object($test->curl->response)); + + // Explicit default json decoder should return object. + $test = new Test(); + $test->curl->setDefaultJsonDecoder(); + $test->server('json_response', 'GET'); + $this->assertTrue(is_object($test->curl->response)); + + // Explicit default json decoder with options should return associative array as specified. + $assoc = true; + $depth = 512; + $options = 0; + $test = new Test(); + $test->curl->setDefaultJsonDecoder($assoc, $depth, $options); + $test->server('json_response', 'GET'); + $this->assertTrue(is_array($test->curl->response)); + } + + public function testJsonDecoder() + { + $test = new Test(); + $test->server('json_response', 'GET'); $this->assertTrue(is_object($test->curl->response)); $this->assertFalse(is_array($test->curl->response)); $test = new Test(); - $test->curl->setJsonDecoder(function($response) { + $test->curl->setJsonDecoder(function ($response) { return json_decode($response, true); }); - $test->server('json_response', 'POST', $data); + $test->server('json_response', 'GET'); $this->assertFalse(is_object($test->curl->response)); $this->assertTrue(is_array($test->curl->response)); + + $test = new Test(); + $test->curl->setJsonDecoder(false); + $test->server('json_response', 'GET'); + $this->assertTrue(is_string($test->curl->response)); } - public function testJSONContentTypeDetection() + public function testJsonContentTypeDetection() { - $json_content_types = array( + $json_content_types = [ 'application/alto-costmap+json', 'application/alto-costmapfilter+json', 'application/alto-directory+json', @@ -1136,19 +1710,17 @@ public function testJSONContentTypeDetection() 'application/x-json', 'text/json', 'text/x-json', - ); + ]; - $class = new ReflectionClass('\Curl\Curl'); - $property = $class->getProperty('jsonPattern'); - $property->setAccessible(true); - $json_pattern = $property->getValue(new Curl); + $curl = new Curl(); + $json_pattern = \Helper\get_curl_property_value($curl, 'jsonPattern'); foreach ($json_content_types as $json_content_type) { $message = '"' . $json_content_type . '" does not match pattern ' . $json_pattern; $this->assertEquals(1, preg_match($json_pattern, $json_content_type), $message); } - $not_json_content_types = array( + $not_json_content_types = [ 'application/1d-interleaved-parityfec', 'application/3gpdash-qoe-report+xml', 'application/3gpp-ims+xml', @@ -2217,7 +2789,7 @@ public function testJSONContentTypeDetection() 'application/yin+xml', 'application/zip', 'application/zlib', - ); + ]; foreach ($not_json_content_types as $json_content_type) { $message = '"' . $json_content_type . '" matches pattern ' . $json_pattern; @@ -2225,34 +2797,108 @@ public function testJSONContentTypeDetection() } } - public function testXMLResponse() + public function testXmlDecoderOptions() + { + // Implicit default xml decoder should return object. + $test = new Test(); + $test->server('xml_with_cdata_response', 'GET'); + $this->assertTrue(is_object($test->curl->response)); + $this->assertFalse(strpos($test->curl->response->saveXML(), 'curl->setDefaultXmlDecoder(); + $test->server('xml_with_cdata_response', 'GET'); + $this->assertTrue(is_object($test->curl->response)); + $this->assertFalse(strpos($test->curl->response->saveXML(), 'curl->setDefaultXmlDecoder($class_name, $options); + $test->server('xml_with_cdata_response', 'GET'); + $this->assertTrue(is_object($test->curl->response)); + $this->assertTrue(strpos($test->curl->response->saveXML(), 'server('xml_with_cdata_response', 'POST'); + $this->assertTrue(is_object($test->curl->response)); + $this->assertInstanceOf('SimpleXMLElement', $test->curl->response); + $this->assertFalse(strpos($test->curl->response->saveXML(), 'curl->setXmlDecoder(function ($response) { + return simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA); + }); + $test->server('xml_with_cdata_response', 'POST'); + $this->assertTrue(is_object($test->curl->response)); + $this->assertInstanceOf('SimpleXMLElement', $test->curl->response); + $this->assertTrue(strpos($test->curl->response->saveXML(), 'curl->setXmlDecoder(false); + $test->server('xml_with_cdata_response', 'POST'); + $this->assertTrue(is_string($test->curl->response)); + } + + public function testXmlContentTypeDetection() + { + $xml_content_types = [ + 'application/atom+xml', + 'application/rss+xml', + 'application/soap+xml', + 'application/xml', + 'text/xml', + ]; + + $curl = new Curl(); + $xml_pattern = \Helper\get_curl_property_value($curl, 'xmlPattern'); + + foreach ($xml_content_types as $xml_content_type) { + $message = '"' . $xml_content_type . '" does not match pattern ' . $xml_pattern; + $this->assertEquals(1, preg_match($xml_pattern, $xml_content_type), $message); + } + } + + public function testXmlResponse() { - foreach (array( + foreach ( + [ 'Content-Type', 'content-type', - 'CONTENT-TYPE') as $key) { - foreach (array( + 'CONTENT-TYPE'] as $key + ) { + foreach ( + [ 'application/atom+xml; charset=UTF-8', 'application/atom+xml;charset=UTF-8', 'application/rss+xml', 'application/rss+xml; charset=utf-8', 'application/rss+xml;charset=utf-8', + 'application/soap+xml', + 'application/soap+xml; charset=utf-8', + 'application/soap+xml;charset=utf-8', 'application/xml', 'application/xml; charset=utf-8', 'application/xml;charset=utf-8', 'text/xml', 'text/xml; charset=utf-8', 'text/xml;charset=utf-8', - ) as $value) { + ] as $value + ) { $test = new Test(); - $test->server('xml_response', 'POST', array( + $test->server('xml_response', 'POST', [ 'key' => $key, 'value' => $value, - )); + ]); $this->assertInstanceOf('SimpleXMLElement', $test->curl->response); - $doc = new DOMDocument(); + $doc = new \DOMDocument(); $doc->formatOutput = true; $rss = $doc->appendChild($doc->createElement('rss')); $rss->setAttribute('version', '2.0'); @@ -2273,47 +2919,112 @@ public function testXMLResponse() } } + public function testDefaultDecoder() + { + // Default. + $test = new Test(); + $test->server('download_file_size', 'GET'); + $this->assertTrue(is_string($test->curl->response)); + + // Callable. + $test = new Test(); + $test->curl->setDefaultDecoder(function ($response) { + return '123'; + }); + $test->server('download_file_size', 'GET'); + $this->assertEquals('123', $test->curl->response); + + // "json". + $test = new Test(); + $test->curl->setDefaultDecoder('json'); + $test->server('json_response', 'POST', [ + 'key' => 'Content-Type', + 'value' => 'application/but-not-json', + ]); + $this->assertInstanceOf('stdClass', $test->curl->response); + + // "xml". + $test = new Test(); + $test->curl->setDefaultDecoder('xml'); + $test->server('xml_response', 'POST', [ + 'key' => 'Content-Type', + 'value' => 'text/but-not-xml', + ]); + $this->assertInstanceOf('SimpleXMLElement', $test->curl->response); + + // False. + $test = new Test(); + $test->curl->setDefaultDecoder('json'); + $test->curl->setDefaultDecoder(false); + $test->server('json_response', 'POST', [ + 'key' => 'Content-Type', + 'value' => 'application/but-not-json', + ]); + $this->assertTrue(is_string($test->curl->response)); + } + + public function testEmptyResponse() { $response = "\r\n\r\n"; - $reflector = new ReflectionClass('\Curl\Curl'); + $reflector = new \ReflectionClass('\Curl\Curl'); $reflection_method = $reflector->getMethod('parseResponseHeaders'); $reflection_method->setAccessible(true); $curl = new Curl(); - $reflection_method->invoke($curl, $response); + $response_headers = $reflection_method->invoke($curl, $response); + $this->assertArrayHasKey('Status-Line', $response_headers); + } + + public function testMalformedResponseHeaders() + { + $response = + 'HTTP/1.0 403 Forbidden' . "\n" . + 'Cache-Control: no-cache' . "\n" . + 'Content-Type: text/html' . "\n" . + 'Strict-Transport-Security: max-age=0' . + "\r\n" . + "\n"; + + $reflector = new \ReflectionClass('\Curl\Curl'); + $reflection_method = $reflector->getMethod('parseResponseHeaders'); + $reflection_method->setAccessible(true); + + $curl = new Curl(); + $response_headers = $reflection_method->invoke($curl, $response); + $this->assertTrue($response_headers instanceof CaseInsensitiveArray); } public function testArrayToStringConversion() { $test = new Test(); - $test->server('post', 'POST', array( + $test->server('post', 'POST', [ 'foo' => 'bar', - 'baz' => array( - ), - )); + 'baz' => [ + ], + ]); $this->assertEquals('foo=bar&baz=', $test->curl->response); $test = new Test(); - $test->server('post', 'POST', array( + $test->server('post', 'POST', [ 'foo' => 'bar', - 'baz' => array( - 'qux' => array( - ), - ), - )); + 'baz' => [ + 'qux' => [ + ], + ], + ]); $this->assertEquals('foo=bar&baz[qux]=', urldecode($test->curl->response)); $test = new Test(); - $test->server('post', 'POST', array( + $test->server('post', 'POST', [ 'foo' => 'bar', - 'baz' => array( - 'qux' => array( - ), + 'baz' => [ + 'qux' => [ + ], 'wibble' => 'wobble', - ), - )); + ], + ]); $this->assertEquals('foo=bar&baz[qux]=&baz[wibble]=wobble', urldecode($test->curl->response)); } @@ -2328,34 +3039,50 @@ public function testSuccessCallback() $curl = $test->curl; $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->beforeSend(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($before_send_called); - PHPUnit_Framework_Assert::assertFalse($success_called); - PHPUnit_Framework_Assert::assertFalse($error_called); - PHPUnit_Framework_Assert::assertFalse($complete_called); + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($before_send_called); + \PHPUnit\Framework\Assert::assertFalse($success_called); + \PHPUnit\Framework\Assert::assertFalse($error_called); + \PHPUnit\Framework\Assert::assertFalse($complete_called); $before_send_called = true; }); $curl->success(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($before_send_called); - PHPUnit_Framework_Assert::assertFalse($success_called); - PHPUnit_Framework_Assert::assertFalse($error_called); - PHPUnit_Framework_Assert::assertFalse($complete_called); + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($before_send_called); + \PHPUnit\Framework\Assert::assertFalse($success_called); + \PHPUnit\Framework\Assert::assertFalse($error_called); + \PHPUnit\Framework\Assert::assertFalse($complete_called); $success_called = true; }); $curl->error(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { $error_called = true; }); $curl->complete(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($before_send_called); - PHPUnit_Framework_Assert::assertTrue($success_called); - PHPUnit_Framework_Assert::assertFalse($error_called); - PHPUnit_Framework_Assert::assertFalse($complete_called); + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($before_send_called); + \PHPUnit\Framework\Assert::assertTrue($success_called); + \PHPUnit\Framework\Assert::assertFalse($error_called); + \PHPUnit\Framework\Assert::assertFalse($complete_called); $complete_called = true; }); @@ -2369,6 +3096,10 @@ public function testSuccessCallback() public function testErrorCallback() { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + $before_send_called = false; $success_called = false; $error_called = false; @@ -2379,34 +3110,50 @@ public function testErrorCallback() $curl->setHeader('X-DEBUG-TEST', 'get'); $curl->setOpt(CURLOPT_CONNECTTIMEOUT_MS, 2000); $curl->beforeSend(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($before_send_called); - PHPUnit_Framework_Assert::assertFalse($success_called); - PHPUnit_Framework_Assert::assertFalse($error_called); - PHPUnit_Framework_Assert::assertFalse($complete_called); + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($before_send_called); + \PHPUnit\Framework\Assert::assertFalse($success_called); + \PHPUnit\Framework\Assert::assertFalse($error_called); + \PHPUnit\Framework\Assert::assertFalse($complete_called); $before_send_called = true; }); $curl->success(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { $success_called = true; }); $curl->error(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($before_send_called); - PHPUnit_Framework_Assert::assertFalse($success_called); - PHPUnit_Framework_Assert::assertFalse($error_called); - PHPUnit_Framework_Assert::assertFalse($complete_called); + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($before_send_called); + \PHPUnit\Framework\Assert::assertFalse($success_called); + \PHPUnit\Framework\Assert::assertFalse($error_called); + \PHPUnit\Framework\Assert::assertFalse($complete_called); $error_called = true; }); $curl->complete(function ($instance) use ( - &$before_send_called, &$success_called, &$error_called, &$complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($before_send_called); - PHPUnit_Framework_Assert::assertFalse($success_called); - PHPUnit_Framework_Assert::assertTrue($error_called); - PHPUnit_Framework_Assert::assertFalse($complete_called); + &$before_send_called, + &$success_called, + &$error_called, + &$complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($before_send_called); + \PHPUnit\Framework\Assert::assertFalse($success_called); + \PHPUnit\Framework\Assert::assertTrue($error_called); + \PHPUnit\Framework\Assert::assertFalse($complete_called); $complete_called = true; }); @@ -2420,20 +3167,38 @@ public function testErrorCallback() public function testClose() { - $test = new Test(); - $curl = $test->curl; - $curl->setHeader('X-DEBUG-TEST', 'post'); + $curl = new Curl(); $curl->post(Test::TEST_URL); - $this->assertTrue(is_resource($curl->curl)); + $this->assertTrue(is_object($curl->curl) || is_resource($curl->curl)); + $curl->close(); + $this->assertNull($curl->curl); + } + + public function testCookieJarAfterClose() + { + $cookie_jar = tempnam('/tmp', 'php-curl-class.'); + + $curl = new Curl(); + $curl->setCookieJar($cookie_jar); + $curl->get(Test::TEST_URL); $curl->close(); - $this->assertFalse(is_resource($curl->curl)); + $cookies = file_get_contents($cookie_jar); + $this->assertNotEmpty($cookies); } /** - * @expectedException PHPUnit_Framework_Error_Warning + * @requires PHPUnit >= 10 */ - public function testRequiredOptionCurlOptReturnTransferEmitsWarning() + #[RequiresPhpunit('>= 10')] + public function testRequiredOptionCurlOptReturnTransferEmitsWarningPHPUnit10Plus() { + set_error_handler(static function (int $errno, string $errstr): never { + restore_error_handler(); + throw new \Exception($errstr, $errno); + }, E_USER_WARNING); + + $this->expectExceptionMessage('CURLOPT_RETURNTRANSFER is a required option'); + $curl = new Curl(); $curl->setOpt(CURLOPT_RETURNTRANSFER, false); } @@ -2441,78 +3206,185 @@ public function testRequiredOptionCurlOptReturnTransferEmitsWarning() public function testRequestMethodSuccessiveGetRequests() { $test = new Test(); - Helper\test($test, 'GET', 'POST'); - Helper\test($test, 'GET', 'PUT'); - Helper\test($test, 'GET', 'PATCH'); - Helper\test($test, 'GET', 'DELETE'); - Helper\test($test, 'GET', 'HEAD'); - Helper\test($test, 'GET', 'OPTIONS'); + $test->chainRequests('GET', 'POST'); + $test->chainRequests('GET', 'PUT'); + $test->chainRequests('GET', 'PATCH'); + $test->chainRequests('GET', 'DELETE'); + $test->chainRequests('GET', 'HEAD'); + $test->chainRequests('GET', 'OPTIONS'); + $test->chainRequests('GET', 'SEARCH'); + $test->chainRequests('GET', 'GET'); + + $test = new Test(); + $test->chainRequests('GET', 'POST', ['a' => '1']); + $test->chainRequests('GET', 'PUT', ['b' => '22']); + $test->chainRequests('GET', 'PATCH', ['c' => '333']); + $test->chainRequests('GET', 'DELETE', ['d' => '4444']); + $test->chainRequests('GET', 'HEAD', ['e' => '55555']); + $test->chainRequests('GET', 'OPTIONS', ['f' => '666666']); + $test->chainRequests('GET', 'SEARCH', ['h' => '7777777']); + $test->chainRequests('GET', 'GET', ['g' => '88888888']); } public function testRequestMethodSuccessivePostRequests() { $test = new Test(); - Helper\test($test, 'POST', 'GET'); - Helper\test($test, 'POST', 'PUT'); - Helper\test($test, 'POST', 'PATCH'); - Helper\test($test, 'POST', 'DELETE'); - Helper\test($test, 'POST', 'HEAD'); - Helper\test($test, 'POST', 'OPTIONS'); + $test->chainRequests('POST', 'GET'); + $test->chainRequests('POST', 'PUT'); + $test->chainRequests('POST', 'PATCH'); + $test->chainRequests('POST', 'DELETE'); + $test->chainRequests('POST', 'HEAD'); + $test->chainRequests('POST', 'OPTIONS'); + $test->chainRequests('POST', 'SEARCH'); + $test->chainRequests('POST', 'POST'); + + $test = new Test(); + $test->chainRequests('POST', 'GET', ['a' => '1']); + $test->chainRequests('POST', 'PUT', ['b' => '22']); + $test->chainRequests('POST', 'PATCH', ['c' => '333']); + $test->chainRequests('POST', 'DELETE', ['d' => '4444']); + $test->chainRequests('POST', 'HEAD', ['e' => '55555']); + $test->chainRequests('POST', 'OPTIONS', ['f' => '666666']); + $test->chainRequests('POST', 'SEARCH', ['g' => '7777777']); + $test->chainRequests('POST', 'POST', ['g' => '88888888']); } public function testRequestMethodSuccessivePutRequests() { $test = new Test(); - Helper\test($test, 'PUT', 'GET'); - Helper\test($test, 'PUT', 'POST'); - Helper\test($test, 'PUT', 'PATCH'); - Helper\test($test, 'PUT', 'DELETE'); - Helper\test($test, 'PUT', 'HEAD'); - Helper\test($test, 'PUT', 'OPTIONS'); + $test->chainRequests('PUT', 'GET'); + $test->chainRequests('PUT', 'POST'); + $test->chainRequests('PUT', 'PATCH'); + $test->chainRequests('PUT', 'DELETE'); + $test->chainRequests('PUT', 'HEAD'); + $test->chainRequests('PUT', 'OPTIONS'); + $test->chainRequests('PUT', 'SEARCH'); + $test->chainRequests('PUT', 'PUT'); + + $test = new Test(); + $test->chainRequests('PUT', 'GET', ['a' => '1']); + $test->chainRequests('PUT', 'POST', ['b' => '22']); + $test->chainRequests('PUT', 'PATCH', ['c' => '333']); + $test->chainRequests('PUT', 'DELETE', ['d' => '4444']); + $test->chainRequests('PUT', 'HEAD', ['e' => '55555']); + $test->chainRequests('PUT', 'OPTIONS', ['f' => '666666']); + $test->chainRequests('PUT', 'SEARCH', ['f' => '7777777']); + $test->chainRequests('PUT', 'PUT', ['g' => '88888888']); } public function testRequestMethodSuccessivePatchRequests() { $test = new Test(); - Helper\test($test, 'PATCH', 'GET'); - Helper\test($test, 'PATCH', 'POST'); - Helper\test($test, 'PATCH', 'PUT'); - Helper\test($test, 'PATCH', 'DELETE'); - Helper\test($test, 'PATCH', 'HEAD'); - Helper\test($test, 'PATCH', 'OPTIONS'); + $test->chainRequests('PATCH', 'GET'); + $test->chainRequests('PATCH', 'POST'); + $test->chainRequests('PATCH', 'PUT'); + $test->chainRequests('PATCH', 'DELETE'); + $test->chainRequests('PATCH', 'HEAD'); + $test->chainRequests('PATCH', 'OPTIONS'); + $test->chainRequests('PATCH', 'SEARCH'); + $test->chainRequests('PATCH', 'PATCH'); + + $test = new Test(); + $test->chainRequests('PATCH', 'GET', ['a' => '1']); + $test->chainRequests('PATCH', 'POST', ['b' => '22']); + $test->chainRequests('PATCH', 'PUT', ['c' => '333']); + $test->chainRequests('PATCH', 'DELETE', ['d' => '4444']); + $test->chainRequests('PATCH', 'HEAD', ['e' => '55555']); + $test->chainRequests('PATCH', 'OPTIONS', ['f' => '666666']); + $test->chainRequests('PATCH', 'SEARCH', ['f' => '7777777']); + $test->chainRequests('PATCH', 'PATCH', ['g' => '88888888']); } public function testRequestMethodSuccessiveDeleteRequests() { $test = new Test(); - Helper\test($test, 'DELETE', 'GET'); - Helper\test($test, 'DELETE', 'POST'); - Helper\test($test, 'DELETE', 'PUT'); - Helper\test($test, 'DELETE', 'PATCH'); - Helper\test($test, 'DELETE', 'HEAD'); - Helper\test($test, 'DELETE', 'OPTIONS'); + $test->chainRequests('DELETE', 'GET'); + $test->chainRequests('DELETE', 'POST'); + $test->chainRequests('DELETE', 'PUT'); + $test->chainRequests('DELETE', 'PATCH'); + $test->chainRequests('DELETE', 'HEAD'); + $test->chainRequests('DELETE', 'OPTIONS'); + $test->chainRequests('DELETE', 'SEARCH'); + $test->chainRequests('DELETE', 'DELETE'); + + $test = new Test(); + $test->chainRequests('DELETE', 'GET', ['a' => '1']); + $test->chainRequests('DELETE', 'POST', ['b' => '22']); + $test->chainRequests('DELETE', 'PUT', ['c' => '333']); + $test->chainRequests('DELETE', 'PATCH', ['d' => '4444']); + $test->chainRequests('DELETE', 'HEAD', ['e' => '55555']); + $test->chainRequests('DELETE', 'OPTIONS', ['f' => '666666']); + $test->chainRequests('DELETE', 'SEARCH', ['f' => '7777777']); + $test->chainRequests('DELETE', 'DELETE', ['g' => '88888888']); } public function testRequestMethodSuccessiveHeadRequests() { $test = new Test(); - Helper\test($test, 'HEAD', 'GET'); - Helper\test($test, 'HEAD', 'POST'); - Helper\test($test, 'HEAD', 'PUT'); - Helper\test($test, 'HEAD', 'PATCH'); - Helper\test($test, 'HEAD', 'DELETE'); - Helper\test($test, 'HEAD', 'OPTIONS'); + $test->chainRequests('HEAD', 'GET'); + $test->chainRequests('HEAD', 'POST'); + $test->chainRequests('HEAD', 'PUT'); + $test->chainRequests('HEAD', 'PATCH'); + $test->chainRequests('HEAD', 'DELETE'); + $test->chainRequests('HEAD', 'OPTIONS'); + $test->chainRequests('HEAD', 'SEARCH'); + $test->chainRequests('HEAD', 'HEAD'); + + $test = new Test(); + $test->chainRequests('HEAD', 'GET', ['a' => '1']); + $test->chainRequests('HEAD', 'POST', ['b' => '22']); + $test->chainRequests('HEAD', 'PUT', ['c' => '333']); + $test->chainRequests('HEAD', 'PATCH', ['d' => '4444']); + $test->chainRequests('HEAD', 'DELETE', ['e' => '55555']); + $test->chainRequests('HEAD', 'OPTIONS', ['f' => '666666']); + $test->chainRequests('HEAD', 'SEARCH', ['g' => '7777777']); + $test->chainRequests('HEAD', 'HEAD', ['g' => '88888888']); } public function testRequestMethodSuccessiveOptionsRequests() { $test = new Test(); - Helper\test($test, 'OPTIONS', 'GET'); - Helper\test($test, 'OPTIONS', 'POST'); - Helper\test($test, 'OPTIONS', 'PUT'); - Helper\test($test, 'OPTIONS', 'PATCH'); - Helper\test($test, 'OPTIONS', 'DELETE'); - Helper\test($test, 'OPTIONS', 'HEAD'); + $test->chainRequests('OPTIONS', 'GET'); + $test->chainRequests('OPTIONS', 'POST'); + $test->chainRequests('OPTIONS', 'PUT'); + $test->chainRequests('OPTIONS', 'PATCH'); + $test->chainRequests('OPTIONS', 'DELETE'); + $test->chainRequests('OPTIONS', 'SEARCH'); + $test->chainRequests('OPTIONS', 'HEAD'); + $test->chainRequests('OPTIONS', 'OPTIONS'); + + $test = new Test(); + $test->chainRequests('OPTIONS', 'GET', ['a' => '1']); + $test->chainRequests('OPTIONS', 'POST', ['b' => '22']); + $test->chainRequests('OPTIONS', 'PUT', ['c' => '333']); + $test->chainRequests('OPTIONS', 'PATCH', ['d' => '4444']); + $test->chainRequests('OPTIONS', 'DELETE', ['e' => '55555']); + $test->chainRequests('OPTIONS', 'SEARCH', ['g' => '666666']); + $test->chainRequests('OPTIONS', 'HEAD', ['f' => '7777777']); + $test->chainRequests('OPTIONS', 'OPTIONS', ['g' => '88888888']); + } + + public function testRequestMethodSuccessiveSearchRequests() + { + $test = new Test(); + $test->chainRequests('SEARCH', 'GET'); + $test->chainRequests('SEARCH', 'POST'); + $test->chainRequests('SEARCH', 'PUT'); + $test->chainRequests('SEARCH', 'PATCH'); + $test->chainRequests('SEARCH', 'DELETE'); + $test->chainRequests('SEARCH', 'HEAD'); + $test->chainRequests('SEARCH', 'OPTIONS'); + $test->chainRequests('SEARCH', 'SEARCH'); + + $test = new Test(); + $test->chainRequests('SEARCH', 'GET', ['a' => '1']); + $test->chainRequests('SEARCH', 'POST', ['b' => '22']); + $test->chainRequests('SEARCH', 'PUT', ['c' => '333']); + $test->chainRequests('SEARCH', 'PATCH', ['d' => '4444']); + $test->chainRequests('SEARCH', 'DELETE', ['e' => '55555']); + $test->chainRequests('SEARCH', 'HEAD', ['f' => '666666']); + $test->chainRequests('SEARCH', 'OPTIONS', ['g' => '7777777']); + $test->chainRequests('SEARCH', 'SEARCH', ['g' => '88888888']); } public function testMemoryLeak() @@ -2525,7 +3397,12 @@ public function testMemoryLeak() } echo '{"before":' . memory_get_usage() . ','; $curl = new Curl(); - $curl->close(); + + // Unset the $curl object instead of calling $curl->close(). Calling + // unset($curl) should trigger the clean up: __destruct() which + // calls $curl->close(). + unset($curl); + echo '"after":' . memory_get_usage() . '}'; sleep(1); } @@ -2539,7 +3416,7 @@ public function testMemoryLeak() // indicate a memory leak. $max_memory_diff = 1000; foreach ($results as $i => $result) { - $memory_diff = $result['after'] - $result['before'];; + $memory_diff = $result['after'] - $result['before']; // Skip the first test to allow memory usage to settle. if ($i >= 1) { @@ -2550,11 +3427,6 @@ public function testMemoryLeak() public function testAlternativeStandardErrorOutput() { - // Skip test on HHVM due to "Segmentation fault". - if (defined('HHVM_VERSION')) { - return; - } - $buffer = fopen('php://memory', 'w+'); $curl = new Curl(); @@ -2567,4 +3439,1680 @@ public function testAlternativeStandardErrorOutput() $this->assertNotEmpty($stderr); } + + public function testTotalTime() + { + $test = new Test(); + $test->server('request_method', 'GET'); + $this->assertTrue(is_float($test->curl->totalTime)); + } + + public function testOptionSet() + { + // Skip this test on 8.0 and later: + // "ValueError: curl_setopt(): cURL option must not contain any null bytes" + if (version_compare(PHP_VERSION, '8.0.0', '>=')) { + $this->markTestSkipped(); + } + + $option = CURLOPT_ENCODING; + $value = 'gzip'; + $null = chr(0); + + // Ensure the option is stored when curl_setopt() succeeds. + $curl = new Curl(); + $success = $curl->setOpt($option, $value); + + $this->assertTrue($success); + $this->assertEquals($value, $curl->getOpt($option)); + + // Ensure the option is not stored when curl_setopt() fails. Make curl_setopt() return false and suppress + // errors. Triggers warning: "curl_setopt(): Curl option contains invalid characters (\0)". + $curl = new Curl(); + $success = @$curl->setOpt($option, $null); + + $this->assertFalse($success); + $this->assertNull($curl->getOpt($option)); + + // Ensure options following a Curl::setOpt() failure are not set when using Curl::setOpts(). + $options = [ + $option => $null, + CURLOPT_COOKIE => 'a=b', + ]; + $curl = new Curl(); + $success = @$curl->setOpts($options); + + $this->assertFalse($success); + $this->assertNull($curl->getOpt(CURLOPT_COOKIE)); + + // Ensure Curl::setOpts() returns true when all options are successfully set. + $options = [ + CURLOPT_COOKIE => 'a=b', + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_VERBOSE => true, + ]; + $curl = new Curl(); + $success = $curl->setOpts($options); + + $this->assertTrue($success); + $this->assertEquals('a=b', $curl->getOpt(CURLOPT_COOKIE)); + $this->assertTrue($curl->getOpt(CURLOPT_FOLLOWLOCATION)); + $this->assertTrue($curl->getOpt(CURLOPT_VERBOSE)); + } + + public function testBuildUrlArgs() + { + $tests = [ + [ + 'args' => [ + 'url' => 'https://www.example.com/', + 'mixed_data' => null, + ], + 'expected' => 'https://www.example.com/', + ], + [ + 'args' => [ + 'url' => 'https://www.example.com/', + 'mixed_data' => '', + ], + 'expected' => 'https://www.example.com/', + ], + [ + 'args' => [ + 'url' => 'https://www.example.com/', + 'mixed_data' => [], + ], + 'expected' => 'https://www.example.com/', + ], + [ + 'args' => [ + 'url' => 'https://www.example.com/', + 'mixed_data' => [ + 'a' => '1', + 'b' => '2', + 'c' => '3', + ], + ], + 'expected' => 'https://www.example.com/?a=1&b=2&c=3', + ], + [ + 'args' => [ + 'url' => 'https://www.example.com/?a=base', + 'mixed_data' => [ + 'b' => '2', + 'c' => '3', + ], + ], + 'expected' => 'https://www.example.com/?a=base&b=2&c=3', + ], + [ + 'args' => [ + 'url' => 'https://www.example.com/?a=base', + 'mixed_data' => 'b=2&c=3', + ], + 'expected' => 'https://www.example.com/?a=base&b=2&c=3', + ], + [ + 'args' => [ + 'url' => 'https://www.example.com/', + 'mixed_data' => 'user_ids=user_1,user_2', + ], + 'expected' => 'https://www.example.com/?user_ids=user_1,user_2', + ], + ]; + foreach ($tests as $test) { + $actual_url = Url::buildUrl($test['args']['url'], $test['args']['mixed_data']); + $this->assertEquals($test['expected'], $actual_url); + + $curl_2 = new Curl(); + $curl_2->setUrl($test['args']['url'], $test['args']['mixed_data']); + $this->assertEquals($test['expected'], $curl_2->url); + } + } + + public function testBuildUrlArgSeparator() + { + $base_url = 'https://www.example.com/path'; + $data = [ + 'arg' => 'value', + 'another' => 'one', + ]; + $expected_url = $base_url . '?arg=value&another=one'; + + foreach ([false, '&', '&'] as $arg_separator) { + if ($arg_separator) { + ini_set('arg_separator.output', $arg_separator); + } + + $actual_url = Url::buildUrl($base_url, $data); + $this->assertEquals($expected_url, $actual_url); + } + } + + public function testUnsetHeader() + { + $request_key = 'X-Request-Id'; + $request_value = '1'; + $data = [ + 'test' => 'server', + 'key' => 'HTTP_X_REQUEST_ID', + ]; + + $curl = new Curl(); + $curl->setHeader($request_key, $request_value); + $curl->get(Test::TEST_URL, $data); + $this->assertEquals($request_value, $curl->response); + + $curl = new Curl(); + $curl->setHeader($request_key, $request_value); + $curl->unsetHeader($request_key); + $curl->get(Test::TEST_URL, $data); + $this->assertEquals('', $curl->response); + } + + public function testRemoveHeader() + { + $curl = new Curl(); + $curl->get(Test::TEST_URL); + $this->assertEquals('127.0.0.1:8000', $curl->requestHeaders['host']); + + $curl = new Curl(); + $curl->removeHeader('HOST'); + $curl->get(Test::TEST_URL); + $this->assertEquals('', $curl->requestHeaders['host']); + } + + public function testGetInfo() + { + $test = new Test(); + $test->server('server', 'GET'); + $info = $test->curl->getInfo(); + + $expected_keys = [ + 'url', + 'content_type', + 'http_code', + 'header_size', + 'request_size', + 'filetime', + 'ssl_verify_result', + 'redirect_count', + 'total_time', + 'namelookup_time', + 'connect_time', + 'pretransfer_time', + 'size_upload', + 'size_download', + 'speed_download', + 'speed_upload', + 'download_content_length', + 'upload_content_length', + 'starttransfer_time', + 'redirect_time', + 'certinfo', + 'primary_ip', + 'primary_port', + 'local_ip', + 'local_port', + 'redirect_url', + 'request_header', + ]; + + foreach ($expected_keys as $key) { + $this->assertArrayHasKey($key, $info); + } + } + + public function testRetry() + { + $tests = [ + [ + 'maximum_number_of_retries' => null, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 1, + 'expect_success' => false, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 1, + 'expect_success' => true, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 2, + 'expect_success' => false, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 2, + 'failures' => 2, + 'expect_success' => true, + 'expect_attempts' => 3, + 'expect_retries' => 2, + ], + [ + 'maximum_number_of_retries' => 3, + 'failures' => 3, + 'expect_success' => true, + 'expect_attempts' => 4, + 'expect_retries' => 3, + ], + ]; + foreach ($tests as $test) { + $maximum_number_of_retries = $test['maximum_number_of_retries']; + $failures = $test['failures']; + $expect_success = $test['expect_success']; + $expect_attempts = $test['expect_attempts']; + $expect_retries = $test['expect_retries']; + + $test = new Test(); + $test->curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + + if ($maximum_number_of_retries !== null) { + $test->curl->setRetry($maximum_number_of_retries); + } + + $test->server('retry', 'GET', ['failures' => $failures]); + $this->assertEquals($expect_success, !$test->curl->error); + $this->assertEquals($expect_attempts, $test->curl->attempts); + $this->assertEquals($expect_retries, $test->curl->retries); + } + } + + public function testRetryCallable() + { + $tests = [ + [ + 'maximum_number_of_retries' => null, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 1, + 'expect_success' => false, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 1, + 'expect_success' => true, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 2, + 'expect_success' => false, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 2, + 'failures' => 2, + 'expect_success' => true, + 'expect_attempts' => 3, + 'expect_retries' => 2, + ], + [ + 'maximum_number_of_retries' => 3, + 'failures' => 3, + 'expect_success' => true, + 'expect_attempts' => 4, + 'expect_retries' => 3, + ], + ]; + foreach ($tests as $test) { + $maximum_number_of_retries = $test['maximum_number_of_retries']; + $failures = $test['failures']; + $expect_success = $test['expect_success']; + $expect_attempts = $test['expect_attempts']; + $expect_retries = $test['expect_retries']; + + $test = new Test(); + $test->curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + + if ($maximum_number_of_retries !== null) { + $test->curl->setRetry(function ($instance) use ($maximum_number_of_retries) { + $return = $instance->retries < $maximum_number_of_retries; + return $return; + }); + } + + $test->server('retry', 'GET', ['failures' => $failures]); + $this->assertEquals($expect_success, !$test->curl->error); + $this->assertEquals($expect_attempts, $test->curl->attempts); + $this->assertEquals($expect_retries, $test->curl->retries); + } + } + + public function testRelativeUrl() + { + $curl = new Curl(/service/test::TEST_URL . 'path/'); + $this->assertEquals('http://127.0.0.1:8000/path/', (string)$curl->url); + + $curl->get('test', [ + 'a' => '1', + 'b' => '2', + ]); + $this->assertEquals('http://127.0.0.1:8000/path/test?a=1&b=2', (string)$curl->url); + + $curl->get('/root', [ + 'c' => '3', + 'd' => '4', + ]); + $this->assertEquals('http://127.0.0.1:8000/root?c=3&d=4', (string)$curl->url); + + $tests = [ + [ + 'args' => [ + 'http://www.example.com/', + '/foo', + ], + 'expected' => 'http://www.example.com/foo', + ], + [ + 'args' => [ + 'http://www.example.com/', + '/foo/', + ], + 'expected' => 'http://www.example.com/foo/', + ], + [ + 'args' => [ + 'http://www.example.com/', + '/dir/page.html', + ], + 'expected' => 'http://www.example.com/dir/page.html', + ], + [ + 'args' => [ + 'http://www.example.com/dir1/page2.html', + '/dir/page.html', + ], + 'expected' => 'http://www.example.com/dir/page.html', + ], + [ + 'args' => [ + 'http://www.example.com/dir1/page2.html', + 'dir/page.html', + ], + 'expected' => 'http://www.example.com/dir1/dir/page.html', + ], + [ + 'args' => [ + 'http://www.example.com/dir1/dir3/page.html', + '../dir/page.html', + ], + 'expected' => 'http://www.example.com/dir1/dir/page.html', + ], + ]; + foreach ($tests as $test) { + $curl = new Curl(http://github.com/$test['args']['0']); + $curl->setUrl($test['args']['1']); + $this->assertEquals( + $test['expected'], + $curl->getOpt(CURLOPT_URL), + "Joint URLs: '{$test['args']['0']}', '{$test['args']['1']}'" + ); + + $curl = new Curl(http://github.com/$test['args']['0']); + $curl->setUrl($test['args']['1'], ['a' => '1', 'b' => '2']); + $this->assertEquals( + $test['expected'] . '?a=1&b=2', + $curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' with parameters a=1, b=2" + ); + + $curl = new Curl(); + $curl->setUrl($test['args']['0']); + $curl->setUrl($test['args']['1']); + $this->assertEquals( + $test['expected'], + $curl->getOpt(CURLOPT_URL), + "Joint URLs: '{$test['args']['0']}', '{$test['args']['1']}'" + ); + + $curl = new Curl(); + $curl->setUrl($test['args']['0'], ['a' => '1', 'b' => '2']); + $curl->setUrl($test['args']['1']); + $this->assertEquals( + $test['expected'], + $curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' with parameters a=1, b=2 and URL '{$test['args']['1']}'" + ); + + $curl = new Curl(); + $curl->setUrl($test['args']['0']); + $curl->setUrl($test['args']['1'], ['a' => '1', 'b' => '2']); + $this->assertEquals( + $test['expected'] . '?a=1&b=2', + $curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' and URL '{$test['args']['1']}' with parameters a=1, b=2" + ); + + $curl = new Curl(); + $curl->setUrl($test['args']['0'], ['a' => '1', 'b' => '2']); + $curl->setUrl($test['args']['1'], ['c' => '3', 'd' => '4']); + $this->assertEquals( + $test['expected'] . '?c=3&d=4', + $curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' with parameters a=1, b=2 " . + "and URL '{$test['args']['1']}' with parameters c=3, d=4" + ); + } + } + + public function testReset() + { + $test = new Test(); + + $original_user_agent = $test->server('server', 'GET', ['key' => 'HTTP_USER_AGENT']); + $this->assertNotEquals('New agent', $original_user_agent); + + $test->curl->setUserAgent('New agent'); + $user_agent = $test->server('server', 'GET', ['key' => 'HTTP_USER_AGENT']); + $this->assertEquals('New agent', $user_agent); + + $test->curl->reset(); + $user_agent = $test->server('server', 'GET', ['key' => 'HTTP_USER_AGENT']); + $this->assertEquals($original_user_agent, $user_agent); + } + + public function testMock() + { + $curl = $this->getMockBuilder('Curl\Curl') + ->getMock(); + + $curl->expects($this->once()) + ->method('getRawResponse') + ->willReturn('[]'); + + $this->assertEquals('[]', $curl->getRawResponse()); + } + + public function testProxySettings() + { + $curl = new Curl(); + $curl->setProxy('proxy.example.com', '1080', 'username', 'password'); + + $this->assertEquals('proxy.example.com', $curl->getOpt(CURLOPT_PROXY)); + $this->assertEquals('1080', $curl->getOpt(CURLOPT_PROXYPORT)); + $this->assertEquals('username:password', $curl->getOpt(CURLOPT_PROXYUSERPWD)); + + $curl->unsetProxy(); + $this->assertNull($curl->getOpt(CURLOPT_PROXY)); + } + + public function testSetProxyAuth() + { + $auth = CURLAUTH_BASIC; + + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_PROXYAUTH)); + $curl->setProxyAuth($auth); + $this->assertEquals($auth, $curl->getOpt(CURLOPT_PROXYAUTH)); + } + + public function testSetProxyType() + { + $type = CURLPROXY_SOCKS5; + + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_PROXYTYPE)); + $curl->setProxyType($type); + $this->assertEquals($type, $curl->getOpt(CURLOPT_PROXYTYPE)); + } + + public function testSetProxyTunnel() + { + $tunnel = true; + + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_HTTPPROXYTUNNEL)); + $curl->setProxyTunnel($tunnel); + $this->assertEquals($tunnel, $curl->getOpt(CURLOPT_HTTPPROXYTUNNEL)); + } + + public function testJsonSerializable() + { + if (!interface_exists('JsonSerializable')) { + $this->markTestSkipped(); + } + + $expected_response = '{"name":"Alice","email":"alice@example.com"}'; + + $user = new User('Alice', 'alice@example.com'); + $this->assertEquals($expected_response, json_encode($user)); + + $test = new Test(); + $test->curl->setHeader('Content-Type', 'application/json'); + $this->assertEquals($expected_response, $test->server('post_json', 'POST', $user)); + } + + public function testSetFile() + { + $file = STDOUT; + + $curl = new Curl(); + $curl->setFile($file); + $this->assertEquals($file, $curl->getOpt(CURLOPT_FILE)); + } + + public function testSetRange() + { + $range = '1000-'; + + $curl = new Curl(); + $curl->setRange($range); + $this->assertEquals($range, $curl->getOpt(CURLOPT_RANGE)); + } + + public function testDisableTimeout() + { + $curl = new Curl(); + $this->assertEquals(Curl::DEFAULT_TIMEOUT, $curl->getOpt(CURLOPT_TIMEOUT)); + $curl->disableTimeout(); + $this->assertNull($curl->getOpt(CURLOPT_TIMEOUT)); + } + + public function testSetHeadersAssociativeArray() + { + $curl = new Curl(); + $curl->setHeaders([ + ' Key1 ' => ' Value1 ', + ' Key2 ' => ' Value2', + ' Key3 ' => 'Value3 ', + ' Key4 ' => 'Value4', + ' Key5' => ' Value5 ', + ' Key6' => ' Value6', + ' Key7' => 'Value7 ', + ' Key8' => 'Value8', + 'Key9 ' => ' Value9 ', + 'Key10 ' => ' Value10', + 'Key11 ' => 'Value11 ', + 'Key12 ' => 'Value12', + 'Key13' => ' Value13 ', + 'Key14' => ' Value14', + 'Key15' => 'Value15 ', + 'Key16' => 'Value16', + ]); + + $this->assertEquals([ + 'Key1: Value1', + 'Key2: Value2', + 'Key3: Value3', + 'Key4: Value4', + 'Key5: Value5', + 'Key6: Value6', + 'Key7: Value7', + 'Key8: Value8', + 'Key9: Value9', + 'Key10: Value10', + 'Key11: Value11', + 'Key12: Value12', + 'Key13: Value13', + 'Key14: Value14', + 'Key15: Value15', + 'Key16: Value16', + ], $curl->getOpt(CURLOPT_HTTPHEADER)); + + $headers = \Helper\get_curl_property_value($curl, 'headers'); + $this->assertEquals('Value1', $headers['Key1']); + $this->assertEquals('Value2', $headers['Key2']); + $this->assertEquals('Value3', $headers['Key3']); + $this->assertEquals('Value4', $headers['Key4']); + $this->assertEquals('Value5', $headers['Key5']); + $this->assertEquals('Value6', $headers['Key6']); + $this->assertEquals('Value7', $headers['Key7']); + $this->assertEquals('Value8', $headers['Key8']); + $this->assertEquals('Value9', $headers['Key9']); + $this->assertEquals('Value10', $headers['Key10']); + $this->assertEquals('Value11', $headers['Key11']); + $this->assertEquals('Value12', $headers['Key12']); + $this->assertEquals('Value13', $headers['Key13']); + $this->assertEquals('Value14', $headers['Key14']); + $this->assertEquals('Value15', $headers['Key15']); + $this->assertEquals('Value16', $headers['Key16']); + } + + public function testSetHeadersIndexedArray() + { + $curl = new Curl(); + $curl->setHeaders([ + ' Key1 : Value1 ', + ' Key2 : Value2', + ' Key3 :Value3 ', + ' Key4 :Value4', + ' Key5: Value5 ', + ' Key6: Value6', + ' Key7:Value7 ', + ' Key8:Value8', + 'Key9 : Value9 ', + 'Key10 : Value10', + 'Key11 :Value11 ', + 'Key12 :Value12', + 'Key13: Value13 ', + 'Key14: Value14', + 'Key15:Value15 ', + 'Key16:Value16', + ]); + + $this->assertEquals([ + 'Key1: Value1', + 'Key2: Value2', + 'Key3: Value3', + 'Key4: Value4', + 'Key5: Value5', + 'Key6: Value6', + 'Key7: Value7', + 'Key8: Value8', + 'Key9: Value9', + 'Key10: Value10', + 'Key11: Value11', + 'Key12: Value12', + 'Key13: Value13', + 'Key14: Value14', + 'Key15: Value15', + 'Key16: Value16', + ], $curl->getOpt(CURLOPT_HTTPHEADER)); + + $headers = \Helper\get_curl_property_value($curl, 'headers'); + $this->assertEquals('Value1', $headers['Key1']); + $this->assertEquals('Value2', $headers['Key2']); + $this->assertEquals('Value3', $headers['Key3']); + $this->assertEquals('Value4', $headers['Key4']); + $this->assertEquals('Value5', $headers['Key5']); + $this->assertEquals('Value6', $headers['Key6']); + $this->assertEquals('Value7', $headers['Key7']); + $this->assertEquals('Value8', $headers['Key8']); + $this->assertEquals('Value9', $headers['Key9']); + $this->assertEquals('Value10', $headers['Key10']); + $this->assertEquals('Value11', $headers['Key11']); + $this->assertEquals('Value12', $headers['Key12']); + $this->assertEquals('Value13', $headers['Key13']); + $this->assertEquals('Value14', $headers['Key14']); + $this->assertEquals('Value15', $headers['Key15']); + $this->assertEquals('Value16', $headers['Key16']); + } + + public function testSetAutoReferer() + { + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_AUTOREFERER)); + $curl->setAutoReferer(true); + $this->assertTrue($curl->getOpt(CURLOPT_AUTOREFERER)); + } + + public function testSetAutoReferrer() + { + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_AUTOREFERER)); + $curl->setAutoReferrer(true); + $this->assertTrue($curl->getOpt(CURLOPT_AUTOREFERER)); + } + + public function testSetFollowLocation() + { + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_FOLLOWLOCATION)); + $curl->setFollowLocation(true); + $this->assertTrue($curl->getOpt(CURLOPT_FOLLOWLOCATION)); + } + + public function testSetForbidReuse() + { + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_FORBID_REUSE)); + $curl->setForbidReuse(true); + $this->assertTrue($curl->getOpt(CURLOPT_FORBID_REUSE)); + } + + public function testSetMaximumRedirects() + { + $curl = new Curl(); + $this->assertNull($curl->getOpt(CURLOPT_MAXREDIRS)); + $curl->setMaximumRedirects(3); + $this->assertEquals(3, $curl->getOpt(CURLOPT_MAXREDIRS)); + } + + public function testDiagnoseOutputGet() + { + // Test diagnose() with default parameters. + $test_1 = new Test(); + $test_1->server('error_message', 'GET'); + ob_start(); + $test_1->curl->diagnose(); + $test_1_output = ob_get_contents(); + ob_end_clean(); + + // Test diagnose() with return=true. + $test_2 = new Test(); + $test_2->server('error_message', 'GET'); + $test_2_output = $test_2->curl->diagnose(true); + + // Test diagnose() with return=false. + $test_3 = new Test(); + $test_3->server('error_message', 'GET'); + ob_start(); + $test_3->curl->diagnose(false); + $test_3_output = ob_get_contents(); + ob_end_clean(); + + foreach ( + [ + '--- Begin PHP Curl Class diagnostic output ---', + 'PHP Curl Class version: ' . Curl::VERSION, + 'PHP version: ' . PHP_VERSION, + 'CURLOPT_PROTOCOLS: 3 (CURLPROTO_HTTP | CURLPROTO_HTTPS)', + 'CURLOPT_REDIR_PROTOCOLS: 3 (CURLPROTO_HTTP | CURLPROTO_HTTPS)', + 'CURLOPT_HTTPGET: true', + 'Sent an HTTP GET request ', + 'Request contained no body.', + 'Received an HTTP status code of 401.', + 'Received an HTTP 401 error response with message "HTTP/1.1 401 Unauthorized".', + 'Received an empty response body (response="").', + '--- End PHP Curl Class diagnostic output ---', + ] as $expected_string + ) { + $this->assertStringContainsString($expected_string, $test_1_output); + $this->assertStringContainsString($expected_string, $test_2_output); + $this->assertStringContainsString($expected_string, $test_3_output); + } + } + + public function testDiagnoseOutputPost() + { + $test = new Test(); + $test->server('error_message', 'POST'); + $test_output = $test->curl->diagnose(true); + + foreach ( + [ + '--- Begin PHP Curl Class diagnostic output ---', + 'PHP Curl Class version: ' . Curl::VERSION, + 'PHP version: ' . PHP_VERSION, + 'CURLOPT_PROTOCOLS: 3 (CURLPROTO_HTTP | CURLPROTO_HTTPS)', + 'CURLOPT_REDIR_PROTOCOLS: 3 (CURLPROTO_HTTP | CURLPROTO_HTTPS)', + 'CURLOPT_POST: true', + 'Sent an HTTP POST request ', + 'Request contained no body.', + 'Received an HTTP status code of 401.', + 'Received an HTTP 401 error response with message "HTTP/1.1 401 Unauthorized".', + 'Received an empty response body (response="").', + '--- End PHP Curl Class diagnostic output ---', + ] as $expected_string + ) { + $this->assertStringContainsString($expected_string, $test_output); + } + } + + public function testDiagnoseAllowHeader() + { + $tests = [ + [ + 'http_method' => 'GET', + 'allow_header_name' => 'Allow', + 'allow_header_value' => 'POST, OPTIONS', + 'expected' => + 'Warning: An HTTP GET request was made, but only the following request types are allowed: ' . + 'POST, OPTIONS', + ], + [ + 'http_method' => 'GET', + 'allow_header_name' => 'allow', + 'allow_header_value' => 'OPTIONS, POST', + 'expected' => + 'Warning: An HTTP GET request was made, but only the following request types are allowed: ' . + 'OPTIONS, POST', + ], + [ + 'http_method' => 'POST', + 'allow_header_name' => 'allow', + 'allow_header_value' => 'GET, OPTIONS', + 'expected' => + 'Warning: An HTTP POST request was made, but only the following request types are allowed: ' . + 'GET, OPTIONS', + ], + [ + 'http_method' => 'POST', + 'allow_header_name' => 'allow', + 'allow_header_value' => 'GET,OPTIONS', + 'expected' => + 'Warning: An HTTP POST request was made, but only the following request types are allowed: ' . + 'GET, OPTIONS', + ], + [ + 'http_method' => 'POST', + 'allow_header_name' => 'ALLOW', + 'allow_header_value' => 'get,options', + 'expected' => + 'Warning: An HTTP POST request was made, but only the following request types are allowed: ' . + 'GET, OPTIONS', + ], + ]; + + foreach ($tests as $test_case) { + $test = new Test(); + $test->server('json_response', $test_case['http_method'], [ + 'key' => $test_case['allow_header_name'], + 'value' => $test_case['allow_header_value'], + ]); + + $test_output = $test->curl->diagnose(true); + $this->assertStringContainsString($test_case['expected'], $test_output); + } + } + + public function testDiagnoseBitmaskOptions() + { + $tests = [ + [ + 'CURLOPT_PROTOCOLS' => [ + 'option' => CURLOPT_PROTOCOLS, + 'bitmask' => CURLPROTO_HTTP | CURLPROTO_HTTPS, + 'expected' => 'CURLPROTO_HTTP | CURLPROTO_HTTPS', + ], + 'CURLOPT_REDIR_PROTOCOLS' => [ + 'option' => CURLOPT_REDIR_PROTOCOLS, + 'bitmask' => CURLPROTO_HTTPS | CURLPROTO_HTTP, + 'expected' => 'CURLPROTO_HTTP | CURLPROTO_HTTPS', + ], + ], + [ + 'CURLOPT_PROTOCOLS' => [ + 'option' => CURLOPT_PROTOCOLS, + 'bitmask' => CURLPROTO_TELNET | CURLPROTO_FTPS, + 'expected' => 'CURLPROTO_FTPS | CURLPROTO_TELNET', + ], + 'CURLOPT_REDIR_PROTOCOLS' => [ + 'option' => CURLOPT_REDIR_PROTOCOLS, + 'bitmask' => CURLPROTO_SCP | CURLPROTO_LDAP | CURLPROTO_LDAPS, + 'expected' => 'CURLPROTO_LDAP | CURLPROTO_LDAPS | CURLPROTO_SCP', + ], + ], + [ + 'CURLOPT_PROTOCOLS' => [ + 'option' => CURLOPT_PROTOCOLS, + 'bitmask' => CURLPROTO_ALL, + 'expected' => 'CURLPROTO_ALL', + ], + 'CURLOPT_REDIR_PROTOCOLS' => [ + 'option' => CURLOPT_REDIR_PROTOCOLS, + 'bitmask' => CURLPROTO_ALL, + 'expected' => 'CURLPROTO_ALL', + ], + ], + [ + 'CURLOPT_PROXYAUTH' => [ + 'option' => CURLOPT_PROXYAUTH, + 'bitmask' => CURLAUTH_BASIC, + 'expected' => 'CURLAUTH_BASIC', + ], + ], + [ + 'CURLOPT_PROXYAUTH' => [ + 'option' => CURLOPT_PROXYAUTH, + 'bitmask' => CURLAUTH_DIGEST | CURLAUTH_NTLM, + 'expected' => 'CURLAUTH_DIGEST | CURLAUTH_NTLM', + ], + ], + [ + 'CURLOPT_PROXYAUTH' => [ + 'option' => CURLOPT_PROXYAUTH, + 'bitmask' => CURLAUTH_ANY, + 'expected' => 'CURLAUTH_ANY', + ], + ], + [ + 'CURLOPT_PROXYAUTH' => [ + 'option' => CURLOPT_PROXYAUTH, + 'bitmask' => CURLAUTH_ANYSAFE, + 'expected' => 'CURLAUTH_ANYSAFE', + ], + ], + [ + 'CURLOPT_HTTPAUTH' => [ + 'option' => CURLOPT_HTTPAUTH, + 'bitmask' => CURLAUTH_BASIC, + 'expected' => 'CURLAUTH_BASIC', + ], + ], + [ + 'CURLOPT_HTTPAUTH' => [ + 'option' => CURLOPT_HTTPAUTH, + 'bitmask' => CURLAUTH_DIGEST | CURLAUTH_NTLM, + 'expected' => 'CURLAUTH_DIGEST | CURLAUTH_NTLM', + ], + ], + [ + 'CURLOPT_HTTPAUTH' => [ + 'option' => CURLOPT_HTTPAUTH, + 'bitmask' => CURLAUTH_ANY, + 'expected' => 'CURLAUTH_ANY', + ], + ], + [ + 'CURLOPT_HTTPAUTH' => [ + 'option' => CURLOPT_HTTPAUTH, + 'bitmask' => CURLAUTH_ANYSAFE, + 'expected' => 'CURLAUTH_ANYSAFE', + ], + ], + [ + 'CURLOPT_SSL_OPTIONS' => [ + 'option' => CURLOPT_SSL_OPTIONS, + 'bitmask' => CURLSSLOPT_ALLOW_BEAST | CURLSSLOPT_NO_REVOKE, + 'expected' => 'CURLSSLOPT_ALLOW_BEAST | CURLSSLOPT_NO_REVOKE', + ], + ], + [ + 'CURLOPT_SSH_AUTH_TYPES' => [ + 'option' => CURLOPT_SSH_AUTH_TYPES, + 'bitmask' => CURLSSH_AUTH_KEYBOARD | CURLSSH_AUTH_PASSWORD | CURLSSH_AUTH_PUBLICKEY, + 'expected' => 'CURLSSH_AUTH_KEYBOARD | CURLSSH_AUTH_PASSWORD | CURLSSH_AUTH_PUBLICKEY', + ], + ], + ]; + + if (defined('CURLOPT_PROXY_SSL_OPTIONS') && defined('CURLSSLOPT_NO_PARTIALCHAIN')) { + $tests[] = [ + 'CURLOPT_PROXY_SSL_OPTIONS' => [ + 'option' => CURLOPT_PROXY_SSL_OPTIONS, + 'bitmask' => CURLSSLOPT_ALLOW_BEAST | CURLSSLOPT_NO_REVOKE | CURLSSLOPT_NO_PARTIALCHAIN, + 'expected' => 'CURLSSLOPT_ALLOW_BEAST | CURLSSLOPT_NO_PARTIALCHAIN | CURLSSLOPT_NO_REVOKE', + ], + ]; + } + + foreach ($tests as $test_case) { + $test = new Test(); + foreach ($test_case as $option_name => $values) { + $test->curl->setOpt($values['option'], $values['bitmask']); + } + $test->server('json_response', 'GET'); + $test_output = $test->curl->diagnose(true); + + foreach ($test_case as $option_name => $values) { + $expected = $option_name . ': ' . $values['bitmask']; + if (isset($values['expected'])) { + $expected .= ' (' . $values['expected'] . ')'; + } + $expected .= "\n"; + $this->assertStringContainsString($expected, $test_output); + } + } + } + + public function testDiagnoseBitmaskOptionsNonPositive() + { + // CURLOPT_HTTPAUTH + // CURLAUTH_ANY: -17 + $test_1 = new Test(); + $test_1->curl->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_ANY); + $test_1->server('json_response', 'GET'); + $test_1_output = $test_1->curl->diagnose(true); + $this->assertStringContainsString('CURLOPT_HTTPAUTH: -17 (CURLAUTH_ANY)', $test_1_output); + + // CURLOPT_HTTPAUTH + // CURLAUTH_ANYSAFE: -18 + $test_2 = new Test(); + $test_2->curl->setOpt(CURLOPT_HTTPAUTH, CURLAUTH_ANYSAFE); + $test_2->server('json_response', 'GET'); + $test_2_output = $test_2->curl->diagnose(true); + $this->assertStringContainsString('CURLOPT_HTTPAUTH: -18 (CURLAUTH_ANYSAFE)', $test_2_output); + + // CURLOPT_PROTOCOLS + // CURLPROTO_ALL: -1 + $test_3 = new Test(); + $test_3->curl->setOpt(CURLOPT_PROTOCOLS, CURLPROTO_ALL); + $test_3->server('json_response', 'GET'); + $test_3_output = $test_3->curl->diagnose(true); + $this->assertStringContainsString('CURLOPT_PROTOCOLS: -1 (CURLPROTO_ALL)', $test_3_output); + + // CURLOPT_SSH_AUTH_TYPES + // CURLSSH_AUTH_ANY: -1 + $test_4 = new Test(); + $test_4->curl->setOpt(CURLOPT_SSH_AUTH_TYPES, CURLSSH_AUTH_ANY); + $test_4->server('json_response', 'GET'); + $test_4_output = $test_4->curl->diagnose(true); + $this->assertStringContainsString('CURLOPT_SSH_AUTH_TYPES: -1 (CURLSSH_AUTH_ANY)', $test_4_output); + + // CURLOPT_SSH_AUTH_TYPES + // CURLSSH_AUTH_DEFAULT: -1 + $test_5 = new Test(); + $test_5->curl->setOpt(CURLOPT_SSH_AUTH_TYPES, CURLSSH_AUTH_DEFAULT); + $test_5->server('json_response', 'GET'); + $test_5_output = $test_5->curl->diagnose(true); + $this->assertStringContainsString('CURLOPT_SSH_AUTH_TYPES: -1 (CURLSSH_AUTH_ANY)', $test_5_output); + } + + public function testDiagnoseErrorMessage() + { + $tests = [ + [ + 'body' => + json_encode([ + 'error' => [ + 'code' => 503, + 'message' => 'The service is currently unavailable.', + 'errors' => [ + [ + 'message' => 'The service is currently unavailable.', + 'domain' => 'global', + 'reason' => 'backendError', + ], + ], + 'status' => 'UNAVAILABLE', + ], + ], JSON_PRETTY_PRINT), + 'expects' => [ + 'Found 2 messages in response:', + 'code: 503', + 'message: The service is currently unavailable.', + ], + ], + [ + 'body' => + json_encode([ + 'errors' => [ + [ + 'id' => 'invalid_token', + 'message' => 'The access token is invalid', + ], + ], + ]), + 'expects' => [ + 'Found 1 message in response:', + 'message: The access token is invalid', + ], + ], + [ + 'body' => + json_encode([ + 'apiVersion' => '2.0', + 'error' => [ + 'code' => 404, + 'message' => 'File Not Found', + 'errors' => [ + [ + 'domain' => 'Calendar', + 'reason' => 'ResourceNotFoundException', + 'message' => 'File Not Found', + ], + ], + ], + ]), + 'expects' => [ + 'Found 2 messages in response:', + 'code: 404', + 'message: File Not Found', + ], + ], + ]; + foreach ($tests as $test_case) { + $test = new Test(); + $test->server('json_response', 'POST', [ + 'body' => $test_case['body'], + ]); + + $test_output = $test->curl->diagnose(true); + + foreach ($test_case['expects'] as $expect) { + $this->assertStringContainsString($expect, $test_output); + } + } + } + + public function testDiagnoseRequestHeadersEmptyWarning() + { + $expect = 'Warning: Request headers (Curl::requestHeaders) are expected to be empty'; + + $test_1 = new Test(); + $test_1->curl->verbose(); + $test_1->server('json_response', 'GET'); + $test_1_output = $test_1->curl->diagnose(true); + $this->assertStringContainsString($expect, $test_1_output); + + $test_2 = new Test(); + $test_2->curl->setOpt(CURLOPT_VERBOSE, true); + $test_2->curl->setOpt(CURLINFO_HEADER_OUT, false); + $test_2->server('json_response', 'GET'); + $test_2_output = $test_2->curl->diagnose(true); + $this->assertStringContainsString($expect, $test_2_output); + + $test_3 = new Test(); + $test_3->curl->setOpt(CURLOPT_VERBOSE, true); + $test_3->curl->setOpt(CURLINFO_HEADER_OUT, 0); + $test_3->server('json_response', 'GET'); + $test_3_output = $test_3->curl->diagnose(true); + $this->assertStringContainsString($expect, $test_3_output); + + $test_4 = new Test(); + $test_4->curl->setOpt(CURLOPT_VERBOSE, 1); + $test_4->curl->setOpt(CURLINFO_HEADER_OUT, false); + $test_4->server('json_response', 'GET'); + $test_4_output = $test_4->curl->diagnose(true); + $this->assertStringContainsString($expect, $test_4_output); + + $test_5 = new Test(); + $test_5->curl->setOpt(CURLOPT_VERBOSE, 1); + $test_5->curl->setOpt(CURLINFO_HEADER_OUT, 0); + $test_5->server('json_response', 'GET'); + $test_5_output = $test_5->curl->diagnose(true); + $this->assertStringContainsString($expect, $test_5_output); + } + + public function testDiagnoseContentTypeMissing() + { + $test = new Test(); + $test->server('json_response', 'POST', [ + 'remove-content-type-header' => '', + ]); + $test_output = $test->curl->diagnose(true); + + $this->assertFalse(isset($test->curl->responseHeaders['content-type'])); + $this->assertStringContainsString('Response did not set a content type', $test_output); + } + + public function testStopRequest() + { + $response_length_bytes = 1e6; // 1e6 = 1 megabyte + + $stop_request_early = function ($ch, $header) { + // Stop requests returning error responses early without downloading the + // full error response. + // + // Check the header for the status line starting with "HTTP/". + // Status-Line per RFC 2616: + // 6.1 Status-Line: + // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF + if (stripos($header, 'HTTP/') === 0) { + $status_line_parts = explode(' ', $header); + if (isset($status_line_parts['1'])) { + $http_status_code = $status_line_parts['1']; + $http_error = in_array((int) floor($http_status_code / 100), [4, 5], true); + if ($http_error) { + // Return true to stop receiving the response. + return true; + } + } + } + + // Return false to continue receiving the response. + return false; + }; + + // Verify that full response is fetched for an error. + $test_1 = new Test(); + $test_1->server('download_file_size', 'GET', [ + 'bytes' => $response_length_bytes, + 'http_response_code' => '500', + ]); + $this->assertEquals($response_length_bytes, $test_1->curl->responseHeaders['Content-Length']); + $this->assertEquals($response_length_bytes, $test_1->curl->getInfo(CURLINFO_SIZE_DOWNLOAD)); + $this->assertEquals($response_length_bytes, strlen($test_1->curl->rawResponse)); + $this->assertTrue($test_1->curl->error); + $this->assertFalse($test_1->curl->curlError); + $this->assertTrue($test_1->curl->httpError); + + // Verify that full response is not fetched for an error. + $test_2 = new Test(); + $test_2->curl->setStop($stop_request_early); + $test_2->server('download_file_size', 'GET', [ + 'bytes' => $response_length_bytes, + 'http_response_code' => '500', + ]); + $this->assertEquals($response_length_bytes, $test_2->curl->responseHeaders['Content-Length']); + $this->assertLessThan($response_length_bytes, $test_2->curl->getInfo(CURLINFO_SIZE_DOWNLOAD)); + $this->assertLessThan($response_length_bytes, strlen($test_2->curl->rawResponse)); + $this->assertTrue($test_2->curl->error); + $this->assertTrue($test_2->curl->curlError); + $this->assertTrue($test_2->curl->httpError); + + // Verify that full response is still fetched for a non-error. + $test_3 = new Test(); + $test_3->curl->setStop($stop_request_early); + $test_3->server('download_file_size', 'GET', [ + 'bytes' => $response_length_bytes, + 'http_response_code' => '200', + ]); + $this->assertEquals($response_length_bytes, $test_3->curl->responseHeaders['Content-Length']); + $this->assertEquals($response_length_bytes, $test_3->curl->getInfo(CURLINFO_SIZE_DOWNLOAD)); + $this->assertEquals($response_length_bytes, strlen($test_3->curl->rawResponse)); + $this->assertFalse($test_3->curl->error); + $this->assertFalse($test_3->curl->curlError); + $this->assertFalse($test_3->curl->httpError); + } + + public function testPostDataArray() + { + $data = ['key' => 'value']; + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'post'); + $curl->post(Test::TEST_URL, $data); + + $this->assertEquals('POST / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + $this->assertEquals('key=value', $curl->response); + } + + public function testPostDataArrayNullValues() + { + $data = [ + 'key1' => 'value1', + 'key2' => null, + 'key3' => 'value3', + ]; + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'post'); + $curl->post(Test::TEST_URL, $data); + + $this->assertEquals('key1=value1&key2=&key3=value3', $curl->response); + } + + public function testPostDataString() + { + $data = str_repeat('-', 100); + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'post_json'); + $curl->post(Test::TEST_URL, $data); + + $this->assertEquals('POST / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + $this->assertEquals($data, $curl->response); + } + + public function testPatchDataArray() + { + $data = ['key' => 'value']; + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'patch'); + $curl->patch(Test::TEST_URL, $data); + + $this->assertEquals('PATCH / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + } + + public function testPatchDataString() + { + $data = str_repeat('-', 100); + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'patch'); + $curl->patch(Test::TEST_URL, $data); + + $this->assertEquals('PATCH / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + $this->assertEquals($data, $curl->response); + } + + public function testPutDataArray() + { + $data = ['key' => 'value']; + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'put'); + $curl->put(Test::TEST_URL, $data); + + $this->assertEquals('PUT / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + $this->assertEquals('key=value', $curl->response); + } + + public function testPutDataString() + { + $data = str_repeat('-', 100); + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'put'); + $curl->put(Test::TEST_URL, $data); + + $this->assertEquals('PUT / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + } + + public function testSearchDataArray() + { + $data = ['key' => 'value']; + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'search'); + $curl->search(Test::TEST_URL, $data); + + $this->assertEquals('SEARCH / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + } + + public function testSearchDataString() + { + $data = str_repeat('-', 100); + + $curl = new Curl(); + $curl->setHeader('X-DEBUG-TEST', 'search'); + $curl->search(Test::TEST_URL, $data); + + $this->assertEquals('SEARCH / HTTP/1.1', $curl->requestHeaders['Request-Line']); + $this->assertEquals(Test::TEST_URL, $curl->url); + $this->assertEquals(Test::TEST_URL, $curl->effectiveUrl); + } + + public function testBeforeSendEachRequest() + { + // Ensure Curl::beforeSend() is called before each request including retries. + + $test = new Test(); + $test->curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $test->curl->setRetry(5); + + $before_send_call_count = 0; + $test->curl->beforeSend(function ($instance) use (&$before_send_call_count) { + $before_send_call_count += 1; + }); + + $test->server('retry', 'GET', ['failures' => 5]); + + $this->assertEquals(6, $before_send_call_count); + $this->assertEquals(6, $test->curl->attempts); + $this->assertEquals(5, $test->curl->retries); + $this->assertFalse($test->curl->error); + } + + public function testGzipDecoding() + { + $test = new Test(); + $test->server('json_response', 'POST', [ + 'key' => 'content-encoding', + 'value' => 'gzip', + 'body' => gzencode('hello'), + ]); + $this->assertEquals('hello', $test->curl->response); + } + + public function testGzipAlreadyDecodedWithHeader() + { + $test = new Test(); + + // Send header containing all supported encoding types by setting + // CURLOPT_ENCODING to an empty string. + $test->curl->setOpt(CURLOPT_ENCODING, ''); + + $test->server('json_response', 'POST', [ + 'key' => 'content-encoding', + 'value' => 'gzip', + 'body' => gzencode('hello'), + ]); + $this->assertEquals('hello', $test->curl->response); + } + + public function testGzipAlreadyDecodedWithoutHeader() + { + $test = new Test(); + + // Send header containing all supported encoding types by setting + // CURLOPT_ENCODING to an empty string. + $test->curl->setOpt(CURLOPT_ENCODING, ''); + + $test->server('json_response', 'POST', [ + 'body' => gzencode('hello'), + ]); + $this->assertEquals('hello', $test->curl->response); + } + + public function testGzipDecodingFailureWithoutWarning() + { + $test = new Test(); + $test->server('json_response', 'POST', [ + 'headers' => [ + 'content-type: text/html; charset=utf-8', + 'content-encoding: gzip', + ], + 'body' => 'not gzip-encoded', + ]); + $this->assertEquals('text/html; charset=utf-8', $test->curl->responseHeaders['content-type']); + $this->assertEquals('gzip', $test->curl->responseHeaders['content-encoding']); + $this->assertEquals('not gzip-encoded', $test->curl->response); + } + + public function testGzipDecodingNonStringResponseWithoutError() + { + $test = new Test(); + $test->curl->setDefaultDecoder(function () { + $response = new \stdClass(); + $response->{'abc'} = 'foo'; + $response->{'123'} = 'bar'; + return $response; + }); + $test->server('json_response', 'POST', [ + 'headers' => [ + 'content-type: text/html; charset=utf-8', + 'content-encoding: gzip', + ], + ]); + $this->assertEquals('text/html; charset=utf-8', $test->curl->responseHeaders['content-type']); + $this->assertEquals('gzip', $test->curl->responseHeaders['content-encoding']); + $this->assertEquals('foo', $test->curl->response->{'abc'}); + $this->assertEquals('bar', $test->curl->response->{'123'}); + } + + public function testGzipContentEncoding() + { + // [ + // [ + // 'Response header content-encoding: "gzip"', + // 'Response header content-encoding: "notgzip"', + // 'Response header content-encoding: "" (empty)', + // 'Response header without content-encoding', + // ], + // [ + // 'Content is valid gzip-encoded', + // 'Content is not valid gzip-encoded', + // 'Content is not gzip-encoded', + // ], + // ] + + $tests = [ + // Response header content-encoding: "gzip" + // Content is valid gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => 'gzip', + 'body' => gzencode('hello'), + ], + 'expect_response' => 'hello', + ], + + // Response header content-encoding: "gzip" + // Content is not valid gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => 'gzip', + 'body' => 'not-gzip-encoded', + ], + 'expect_response' => 'not-gzip-encoded', + ], + + // Response header content-encoding: "gzip" + // Content is not gzip-encoded + [ + 'data' => [ + 'headers' => [ + 'content-type: text/html; charset=utf-8', + 'content-encoding: gzip', + ], + 'body' => 'not gzip-encoded', + ], + 'expect_response' => 'not gzip-encoded', + ], + + // Response header content-encoding: "notgzip" + // Content is valid gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => 'notgzip', + 'body' => gzencode('hello'), + ], + 'expect_response' => 'hello', + ], + + // Response header content-encoding: "notgzip" + // Content is not valid gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => 'notgzip', + 'body' => base64_encode('not-valid-gzip-encoded'), + ], + 'expect_response' => base64_encode('not-valid-gzip-encoded'), + ], + + // Response header content-encoding: "notgzip" + // Content is not gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => 'notgzip', + 'body' => 'not-gzip-encoded', + ], + 'expect_response' => 'not-gzip-encoded', + ], + + // Response header content-encoding: "" (empty) + // Content is valid gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => '', + 'body' => gzencode('hello'), + ], + 'expect_response' => 'hello', + ], + + // Response header content-encoding: "" (empty) + // Content is not valid gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => '', + 'body' => base64_encode('not-valid-gzip-encoded'), + ], + 'expect_response' => base64_encode('not-valid-gzip-encoded'), + ], + + // Response header content-encoding: "" (empty) + // Content is not gzip-encoded + [ + 'data' => [ + 'key' => 'content-encoding', + 'value' => '', + 'body' => 'not-gzip-encoded', + ], + 'expect_response' => 'not-gzip-encoded', + ], + + // Response header without content-encoding + // Content is valid gzip-encoded + [ + 'data' => [ + 'body' => gzencode('hello'), + ], + 'expect_response' => 'hello', + ], + + // Response header without content-encoding + // Content is not valid gzip-encoded + [ + 'data' => [ + 'body' => base64_encode('not-valid-gzip-encoded'), + ], + 'expect_response' => base64_encode('not-valid-gzip-encoded'), + ], + + // Response header without content-encoding + // Content is not gzip-encoded + [ + 'data' => [ + 'body' => 'not-gzip-encoded', + ], + 'expect_response' => 'not-gzip-encoded', + ], + ]; + foreach ($tests as $test_data) { + $test = new Test(); + $test->server('json_response', 'POST', $test_data['data']); + $this->assertEquals($test_data['expect_response'], $test->curl->response); + } + } + + public function testAfterSendAttemptCount() + { + $test = new Test(); + $test->curl->setRetry(10); + $test->curl->afterSend(function ($instance) { + if ($instance->attempts < 5) { + $instance->error = true; + } else { + $instance->error = false; + } + }); + $test->server('json_response', 'GET'); + $this->assertEquals(5, $test->curl->attempts); + $this->assertEquals(4, $test->curl->retries); + $this->assertFalse($test->curl->error); + } + + public function testAfterSendResponseMessage() + { + $test = new Test(); + $test->curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $test->curl->setRetry(5); + $test->curl->afterSend(function ($instance) { + $instance->error = $instance->response->message !== '202 Accepted'; + }); + $test->server('retry', 'GET', ['failures' => 3]); + $this->assertEquals(4, $test->curl->attempts); + $this->assertEquals(3, $test->curl->retries); + $this->assertFalse($test->curl->error); + } } diff --git a/tests/PHPCurlClass/PHPMultiCurlClassTest.php b/tests/PHPCurlClass/PHPMultiCurlClassTest.php index 0a244f6c9e..088e283eb2 100644 --- a/tests/PHPCurlClass/PHPMultiCurlClassTest.php +++ b/tests/PHPCurlClass/PHPMultiCurlClassTest.php @@ -1,188 +1,441 @@ skip_slow_tests = in_array(getenv('PHP_CURL_CLASS_SKIP_SLOW_TESTS'), ['1', 'y', 'Y'], true); + } + public function testMultiCurlCallback() { $delete_before_send_called = false; + $delete_after_send_called = false; $delete_success_called = false; $delete_error_called = false; $delete_complete_called = false; $download_before_send_called = false; + $download_after_send_called = false; $download_success_called = false; $download_error_called = false; $download_complete_called = false; $get_before_send_called = false; + $get_after_send_called = false; $get_success_called = false; $get_error_called = false; $get_complete_called = false; $head_before_send_called = false; + $head_after_send_called = false; $head_success_called = false; $head_error_called = false; $head_complete_called = false; $options_before_send_called = false; + $options_after_send_called = false; $options_success_called = false; $options_error_called = false; $options_complete_called = false; $patch_before_send_called = false; + $patch_after_send_called = false; $patch_success_called = false; $patch_error_called = false; $patch_complete_called = false; $post_before_send_called = false; + $post_after_send_called = false; $post_success_called = false; $post_error_called = false; $post_complete_called = false; $put_before_send_called = false; + $put_after_send_called = false; $put_success_called = false; $put_error_called = false; $put_complete_called = false; + $search_before_send_called = false; + $search_after_send_called = false; + $search_success_called = false; + $search_error_called = false; + $search_complete_called = false; + $multi_curl = new MultiCurl(); $multi_curl->beforeSend(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called, - &$download_before_send_called, &$download_success_called, &$download_error_called, + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, &$download_complete_called, - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called, - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called, - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called, - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called, - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called, - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - $request_method = $instance->getOpt(CURLOPT_CUSTOMREQUEST); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); if ($request_method === 'DELETE') { - PHPUnit_Framework_Assert::assertFalse($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + \PHPUnit\Framework\Assert::assertFalse($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_before_send_called = true; } - if (isset($instance->download)) { - PHPUnit_Framework_Assert::assertFalse($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertFalse($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_before_send_called = true; } elseif ($request_method === 'GET') { - PHPUnit_Framework_Assert::assertFalse($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + \PHPUnit\Framework\Assert::assertFalse($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_before_send_called = true; } if ($request_method === 'HEAD') { - PHPUnit_Framework_Assert::assertFalse($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + \PHPUnit\Framework\Assert::assertFalse($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_before_send_called = true; } if ($request_method === 'OPTIONS') { - PHPUnit_Framework_Assert::assertFalse($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + \PHPUnit\Framework\Assert::assertFalse($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_before_send_called = true; } if ($request_method === 'PATCH') { - PHPUnit_Framework_Assert::assertFalse($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + \PHPUnit\Framework\Assert::assertFalse($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_before_send_called = true; } if ($request_method === 'POST') { - PHPUnit_Framework_Assert::assertFalse($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + \PHPUnit\Framework\Assert::assertFalse($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_before_send_called = true; } if ($request_method === 'PUT') { - PHPUnit_Framework_Assert::assertFalse($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + \PHPUnit\Framework\Assert::assertFalse($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_before_send_called = true; } + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertFalse($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_before_send_called = true; + } + }); + $multi_curl->afterSend(function ($instance) use ( + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called, + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); + if ($request_method === 'DELETE') { + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); + $delete_after_send_called = true; + } + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); + $download_after_send_called = true; + } elseif ($request_method === 'GET') { + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); + $get_after_send_called = true; + } + if ($request_method === 'HEAD') { + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); + $head_after_send_called = true; + } + if ($request_method === 'OPTIONS') { + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); + $options_after_send_called = true; + } + if ($request_method === 'PATCH') { + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); + $patch_after_send_called = true; + } + if ($request_method === 'POST') { + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); + $post_after_send_called = true; + } + if ($request_method === 'PUT') { + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); + $put_after_send_called = true; + } + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_after_send_called = true; + } }); $multi_curl->success(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called, - &$download_before_send_called, &$download_success_called, &$download_error_called, + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, &$download_complete_called, - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called, - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called, - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called, - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called, - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called, - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - $request_method = $instance->getOpt(CURLOPT_CUSTOMREQUEST); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); if ($request_method === 'DELETE') { - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_success_called = true; } - if (isset($instance->download)) { - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_success_called = true; } elseif ($request_method === 'GET') { - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_success_called = true; } if ($request_method === 'HEAD') { - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_success_called = true; } if ($request_method === 'OPTIONS') { - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_success_called = true; } if ($request_method === 'PATCH') { - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_success_called = true; } if ($request_method === 'POST') { - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_success_called = true; } if ($request_method === 'PUT') { - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_success_called = true; } + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_success_called = true; + } }); $multi_curl->error(function ($instance) use ( &$delete_error_called, @@ -192,7 +445,9 @@ public function testMultiCurlCallback() &$options_error_called, &$patch_error_called, &$post_error_called, - &$put_error_called) { + &$put_error_called, + &$search_error_called + ) { $delete_error_called = true; $download_error_called = true; $get_error_called = true; @@ -201,239 +456,500 @@ public function testMultiCurlCallback() $patch_error_called = true; $post_error_called = true; $put_error_called = true; + $search_error_called = true; }); $multi_curl->complete(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called, - &$download_before_send_called, &$download_success_called, &$download_error_called, + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, &$download_complete_called, - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called, - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called, - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called, - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called, - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called, - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - $request_method = $instance->getOpt(CURLOPT_CUSTOMREQUEST); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); if ($request_method === 'DELETE') { - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertTrue($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_complete_called = true; } - if (isset($instance->download)) { - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertTrue($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_complete_called = true; } elseif ($request_method === 'GET') { - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertTrue($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_complete_called = true; } if ($request_method === 'HEAD') { - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertTrue($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_complete_called = true; } if ($request_method === 'OPTIONS') { - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertTrue($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_complete_called = true; } if ($request_method === 'PATCH') { - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertTrue($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_complete_called = true; } if ($request_method === 'POST') { - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertTrue($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_complete_called = true; } if ($request_method === 'PUT') { - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertTrue($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_complete_called = true; } + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_complete_called = true; + } }); $multi_curl->addDelete(Test::TEST_URL); $download_file_path = tempnam('/tmp', 'php-curl-class.'); - $multi_curl->addDownload(Test::TEST_URL, $download_file_path)->download = true; + $multi_curl->addDownload(Test::TEST_URL, $download_file_path); $multi_curl->addGet(Test::TEST_URL); $multi_curl->addHead(Test::TEST_URL); $multi_curl->addOptions(Test::TEST_URL); $multi_curl->addPatch(Test::TEST_URL); $multi_curl->addPost(Test::TEST_URL); $multi_curl->addPut(Test::TEST_URL); + $multi_curl->addSearch(Test::TEST_URL); $multi_curl->start(); $this->assertTrue($delete_before_send_called); + $this->assertTrue($delete_after_send_called); $this->assertTrue($delete_success_called); $this->assertFalse($delete_error_called); $this->assertTrue($delete_complete_called); $this->assertTrue($download_before_send_called); + $this->assertTrue($download_after_send_called); $this->assertTrue($download_success_called); $this->assertFalse($download_error_called); $this->assertTrue($download_complete_called); $this->assertTrue(unlink($download_file_path)); $this->assertTrue($get_before_send_called); + $this->assertTrue($get_after_send_called); $this->assertTrue($get_success_called); $this->assertFalse($get_error_called); $this->assertTrue($get_complete_called); $this->assertTrue($head_before_send_called); + $this->assertTrue($head_after_send_called); $this->assertTrue($head_success_called); $this->assertFalse($head_error_called); $this->assertTrue($head_complete_called); $this->assertTrue($options_before_send_called); + $this->assertTrue($options_after_send_called); $this->assertTrue($options_success_called); $this->assertFalse($options_error_called); $this->assertTrue($options_complete_called); $this->assertTrue($patch_before_send_called); + $this->assertTrue($patch_after_send_called); $this->assertTrue($patch_success_called); $this->assertFalse($patch_error_called); $this->assertTrue($patch_complete_called); $this->assertTrue($post_before_send_called); + $this->assertTrue($post_after_send_called); $this->assertTrue($post_success_called); $this->assertFalse($post_error_called); $this->assertTrue($post_complete_called); $this->assertTrue($put_before_send_called); + $this->assertTrue($put_after_send_called); $this->assertTrue($put_success_called); $this->assertFalse($put_error_called); $this->assertTrue($put_complete_called); + + $this->assertTrue($search_before_send_called); + $this->assertTrue($search_after_send_called); + $this->assertTrue($search_success_called); + $this->assertFalse($search_error_called); + $this->assertTrue($search_complete_called); } public function testMultiCurlCallbackError() { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + $delete_before_send_called = false; + $delete_after_send_called = false; $delete_success_called = false; $delete_error_called = false; $delete_complete_called = false; $download_before_send_called = false; + $download_after_send_called = false; $download_success_called = false; $download_error_called = false; $download_complete_called = false; $get_before_send_called = false; + $get_after_send_called = false; $get_success_called = false; $get_error_called = false; $get_complete_called = false; $head_before_send_called = false; + $head_after_send_called = false; $head_success_called = false; $head_error_called = false; $head_complete_called = false; $options_before_send_called = false; + $options_after_send_called = false; $options_success_called = false; $options_error_called = false; $options_complete_called = false; $patch_before_send_called = false; + $patch_after_send_called = false; $patch_success_called = false; $patch_error_called = false; $patch_complete_called = false; $post_before_send_called = false; + $post_after_send_called = false; $post_success_called = false; $post_error_called = false; $post_complete_called = false; $put_before_send_called = false; + $put_after_send_called = false; $put_success_called = false; $put_error_called = false; $put_complete_called = false; + $search_before_send_called = false; + $search_after_send_called = false; + $search_success_called = false; + $search_error_called = false; + $search_complete_called = false; + $multi_curl = new MultiCurl(); $multi_curl->beforeSend(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called, - &$download_before_send_called, &$download_success_called, &$download_error_called, + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, &$download_complete_called, - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called, - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called, - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called, - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called, - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called, - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - $request_method = $instance->getOpt(CURLOPT_CUSTOMREQUEST); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); if ($request_method === 'DELETE') { - PHPUnit_Framework_Assert::assertFalse($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + \PHPUnit\Framework\Assert::assertFalse($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_before_send_called = true; } - if (isset($instance->download)) { - PHPUnit_Framework_Assert::assertFalse($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertFalse($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_before_send_called = true; } elseif ($request_method === 'GET') { - PHPUnit_Framework_Assert::assertFalse($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + \PHPUnit\Framework\Assert::assertFalse($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_before_send_called = true; } if ($request_method === 'HEAD') { - PHPUnit_Framework_Assert::assertFalse($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + \PHPUnit\Framework\Assert::assertFalse($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_before_send_called = true; } if ($request_method === 'OPTIONS') { - PHPUnit_Framework_Assert::assertFalse($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + \PHPUnit\Framework\Assert::assertFalse($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_before_send_called = true; } if ($request_method === 'PATCH') { - PHPUnit_Framework_Assert::assertFalse($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + \PHPUnit\Framework\Assert::assertFalse($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_before_send_called = true; } if ($request_method === 'POST') { - PHPUnit_Framework_Assert::assertFalse($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + \PHPUnit\Framework\Assert::assertFalse($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_before_send_called = true; } if ($request_method === 'PUT') { - PHPUnit_Framework_Assert::assertFalse($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + \PHPUnit\Framework\Assert::assertFalse($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_before_send_called = true; } + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertFalse($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_before_send_called = true; + } + }); + $multi_curl->afterSend(function ($instance) use ( + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called, + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); + if ($request_method === 'DELETE') { + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); + $delete_after_send_called = true; + } + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); + $download_after_send_called = true; + } elseif ($request_method === 'GET') { + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); + $get_after_send_called = true; + } + if ($request_method === 'HEAD') { + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); + $head_after_send_called = true; + } + if ($request_method === 'OPTIONS') { + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); + $options_after_send_called = true; + } + if ($request_method === 'PATCH') { + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); + $patch_after_send_called = true; + } + if ($request_method === 'POST') { + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); + $post_after_send_called = true; + } + if ($request_method === 'PUT') { + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); + $put_after_send_called = true; + } + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_after_send_called = true; + } }); $multi_curl->success(function ($instance) use ( &$delete_success_called, @@ -443,7 +959,9 @@ public function testMultiCurlCallbackError() &$options_success_called, &$patch_success_called, &$post_success_called, - &$put_success_called) { + &$put_success_called, + &$search_success_called + ) { $delete_success_called = true; $get_success_called = true; $head_success_called = true; @@ -451,200 +969,330 @@ public function testMultiCurlCallbackError() $patch_success_called = true; $post_success_called = true; $put_success_called = true; + $search_success_called = true; }); $multi_curl->error(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called, - &$download_before_send_called, &$download_success_called, &$download_error_called, + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, &$download_complete_called, - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called, - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called, - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called, - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called, - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called, - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - $request_method = $instance->getOpt(CURLOPT_CUSTOMREQUEST); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); if ($request_method === 'DELETE') { - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_error_called = true; } - if (isset($instance->download)) { - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_error_called = true; } elseif ($request_method === 'GET') { - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_error_called = true; } if ($request_method === 'HEAD') { - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_error_called = true; } if ($request_method === 'OPTIONS') { - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_error_called = true; } if ($request_method === 'PATCH') { - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_error_called = true; } if ($request_method === 'POST') { - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_error_called = true; } if ($request_method === 'PUT') { - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_error_called = true; } + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_error_called = true; + } }); $multi_curl->complete(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called, - &$download_before_send_called, &$download_success_called, &$download_error_called, + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called, + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, &$download_complete_called, - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called, - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called, - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called, - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called, - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called, - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - $request_method = $instance->getOpt(CURLOPT_CUSTOMREQUEST); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called, + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called, + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called, + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called, + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called, + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called, + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + $request_method = \Helper\get_request_method($instance); if ($request_method === 'DELETE') { - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertTrue($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertTrue($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_complete_called = true; } - if (isset($instance->download)) { - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertTrue($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + if ($instance->downloadFileName !== null) { + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertTrue($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_complete_called = true; } elseif ($request_method === 'GET') { - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertTrue($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertTrue($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_complete_called = true; } if ($request_method === 'HEAD') { - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertTrue($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertTrue($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_complete_called = true; } if ($request_method === 'OPTIONS') { - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertTrue($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertTrue($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_complete_called = true; } if ($request_method === 'PATCH') { - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertTrue($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertTrue($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_complete_called = true; } if ($request_method === 'POST') { - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertTrue($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertTrue($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_complete_called = true; } if ($request_method === 'PUT') { - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertTrue($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertTrue($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_complete_called = true; } - PHPUnit_Framework_Assert::assertTrue($instance->error); - PHPUnit_Framework_Assert::assertTrue($instance->curlError); - PHPUnit_Framework_Assert::assertFalse($instance->httpError); - PHPUnit_Framework_Assert::assertEquals(CURLE_OPERATION_TIMEOUTED, $instance->errorCode); - PHPUnit_Framework_Assert::assertEquals(CURLE_OPERATION_TIMEOUTED, $instance->curlErrorCode); + if ($request_method === 'SEARCH') { + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertTrue($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_complete_called = true; + } + \PHPUnit\Framework\Assert::assertTrue($instance->error); + \PHPUnit\Framework\Assert::assertTrue($instance->curlError); + \PHPUnit\Framework\Assert::assertFalse($instance->httpError); + $possible_errors = [ + CURLE_SEND_ERROR, CURLE_OPERATION_TIMEOUTED, CURLE_COULDNT_CONNECT, CURLE_GOT_NOTHING]; + \PHPUnit\Framework\Assert::assertTrue( + in_array($instance->errorCode, $possible_errors, true), + 'errorCode: ' . $instance->errorCode + ); + \PHPUnit\Framework\Assert::assertTrue( + in_array($instance->curlErrorCode, $possible_errors, true), + 'curlErrorCode: ' . $instance->curlErrorCode + ); }); $multi_curl->addDelete(Test::ERROR_URL); $download_file_path = tempnam('/tmp', 'php-curl-class.'); - $multi_curl->addDownload(Test::ERROR_URL, $download_file_path)->download = true; + $multi_curl->addDownload(Test::ERROR_URL, $download_file_path); $multi_curl->addGet(Test::ERROR_URL); $multi_curl->addHead(Test::ERROR_URL); $multi_curl->addOptions(Test::ERROR_URL); $multi_curl->addPatch(Test::ERROR_URL); $multi_curl->addPost(Test::ERROR_URL); $multi_curl->addPut(Test::ERROR_URL); + $multi_curl->addSearch(Test::ERROR_URL); $multi_curl->start(); $this->assertTrue($delete_before_send_called); + $this->assertTrue($delete_after_send_called); $this->assertFalse($delete_success_called); $this->assertTrue($delete_error_called); $this->assertTrue($delete_complete_called); $this->assertTrue($download_before_send_called); + $this->assertTrue($download_after_send_called); $this->assertFalse($download_success_called); $this->assertTrue($download_error_called); $this->assertTrue($download_complete_called); $this->assertTrue(unlink($download_file_path)); $this->assertTrue($get_before_send_called); + $this->assertTrue($get_after_send_called); $this->assertFalse($get_success_called); $this->assertTrue($get_error_called); $this->assertTrue($get_complete_called); $this->assertTrue($head_before_send_called); + $this->assertTrue($head_after_send_called); $this->assertFalse($head_success_called); $this->assertTrue($head_error_called); $this->assertTrue($head_complete_called); $this->assertTrue($options_before_send_called); + $this->assertTrue($options_after_send_called); $this->assertFalse($options_success_called); $this->assertTrue($options_error_called); $this->assertTrue($options_complete_called); $this->assertTrue($patch_before_send_called); + $this->assertTrue($patch_after_send_called); $this->assertFalse($patch_success_called); $this->assertTrue($patch_error_called); $this->assertTrue($patch_complete_called); $this->assertTrue($post_before_send_called); + $this->assertTrue($post_after_send_called); $this->assertFalse($post_success_called); $this->assertTrue($post_error_called); $this->assertTrue($post_complete_called); $this->assertTrue($put_before_send_called); + $this->assertTrue($put_after_send_called); $this->assertFalse($put_success_called); $this->assertTrue($put_error_called); $this->assertTrue($put_complete_called); + + $this->assertTrue($search_before_send_called); + $this->assertTrue($search_after_send_called); + $this->assertFalse($search_success_called); + $this->assertTrue($search_error_called); + $this->assertTrue($search_complete_called); } public function testCurlCallback() @@ -652,714 +1300,1445 @@ public function testCurlCallback() $multi_curl = new MultiCurl(); $delete_before_send_called = false; + $delete_after_send_called = false; $delete_success_called = false; $delete_error_called = false; $delete_complete_called = false; $delete = $multi_curl->addDelete(Test::TEST_URL); $delete->beforeSend(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_before_send_called = true; }); + $delete->afterSend(function ($instance) use ( + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); + $delete_after_send_called = true; + }); $delete->success(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_success_called = true; }); $delete->error(function ($instance) use ( - &$delete_error_called) { + &$delete_error_called + ) { $delete_error_called = true; }); $delete->complete(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertTrue($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_complete_called = true; }); $download_before_send_called = false; + $download_after_send_called = false; $download_success_called = false; $download_error_called = false; $download_complete_called = false; $download_file_path = tempnam('/tmp', 'php-curl-class.'); $download = $multi_curl->addDownload(Test::TEST_URL, $download_file_path); $download->beforeSend(function ($instance) use ( - &$download_before_send_called, &$download_success_called, &$download_error_called, - &$download_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_before_send_called = true; }); + $download->afterSend(function ($instance) use ( + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); + $download_after_send_called = true; + }); $download->success(function ($instance) use ( - &$download_before_send_called, &$download_success_called, &$download_error_called, - &$download_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_success_called = true; }); $download->error(function ($instance) use ( - &$download_error_called) { + &$download_error_called + ) { $download_error_called = true; }); $download->complete(function ($instance) use ( - &$download_before_send_called, &$download_success_called, &$download_error_called, - &$download_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertTrue($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_complete_called = true; }); $get_before_send_called = false; + $get_after_send_called = false; $get_success_called = false; $get_error_called = false; $get_complete_called = false; $get = $multi_curl->addGet(Test::TEST_URL); $get->beforeSend(function ($instance) use ( - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_before_send_called = true; }); + $get->afterSend(function ($instance) use ( + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); + $get_after_send_called = true; + }); $get->success(function ($instance) use ( - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_success_called = true; }); $get->error(function ($instance) use ( - &$get_error_called) { + &$get_error_called + ) { $get_error_called = true; }); $get->complete(function ($instance) use ( - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertTrue($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_complete_called = true; }); $head_before_send_called = false; + $head_after_send_called = false; $head_success_called = false; $head_error_called = false; $head_complete_called = false; $head = $multi_curl->addHead(Test::TEST_URL); $head->beforeSend(function ($instance) use ( - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_before_send_called = true; }); + $head->afterSend(function ($instance) use ( + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); + $head_after_send_called = true; + }); $head->success(function ($instance) use ( - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_success_called = true; }); $head->error(function ($instance) use ( - &$head_error_called) { + &$head_error_called + ) { $head_error_called = true; }); $head->complete(function ($instance) use ( - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertTrue($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_complete_called = true; }); $options_before_send_called = false; + $options_after_send_called = false; $options_success_called = false; $options_error_called = false; $options_complete_called = false; $options = $multi_curl->addOptions(Test::TEST_URL); $options->beforeSend(function ($instance) use ( - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_before_send_called = true; }); + $options->afterSend(function ($instance) use ( + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); + $options_after_send_called = true; + }); $options->success(function ($instance) use ( - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_success_called = true; }); $options->error(function ($instance) use ( - &$options_error_called) { + &$options_error_called + ) { $options_error_called = true; }); $options->complete(function ($instance) use ( - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertTrue($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_complete_called = true; }); $patch_before_send_called = false; + $patch_after_send_called = false; $patch_success_called = false; $patch_error_called = false; $patch_complete_called = false; $patch = $multi_curl->addPatch(Test::TEST_URL); $patch->beforeSend(function ($instance) use ( - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_before_send_called = true; }); + $patch->afterSend(function ($instance) use ( + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); + $patch_after_send_called = true; + }); $patch->success(function ($instance) use ( - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_success_called = true; }); $patch->error(function ($instance) use ( - &$patch_error_called) { + &$patch_error_called + ) { $patch_error_called = true; }); $patch->complete(function ($instance) use ( - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertTrue($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_complete_called = true; }); $post_before_send_called = false; + $post_after_send_called = false; $post_success_called = false; $post_error_called = false; $post_complete_called = false; $post = $multi_curl->addPost(Test::TEST_URL); $post->beforeSend(function ($instance) use ( - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_before_send_called = true; }); + $post->afterSend(function ($instance) use ( + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); + $post_after_send_called = true; + }); $post->success(function ($instance) use ( - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_success_called = true; }); $post->error(function ($instance) use ( - &$post_error_called) { + &$post_error_called + ) { $post_error_called = true; }); $post->complete(function ($instance) use ( - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertTrue($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_complete_called = true; }); $put_before_send_called = false; + $put_after_send_called = false; $put_success_called = false; $put_error_called = false; $put_complete_called = false; $put = $multi_curl->addPut(Test::TEST_URL); $put->beforeSend(function ($instance) use ( - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_before_send_called = true; }); + $put->afterSend(function ($instance) use ( + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); + $put_after_send_called = true; + }); $put->success(function ($instance) use ( - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_success_called = true; }); $put->error(function ($instance) use ( - &$put_error_called) { + &$put_error_called + ) { $put_error_called = true; }); $put->complete(function ($instance) use ( - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertTrue($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_complete_called = true; }); + $search_before_send_called = false; + $search_after_send_called = false; + $search_success_called = false; + $search_error_called = false; + $search_complete_called = false; + $search = $multi_curl->addSearch(Test::TEST_URL); + $search->beforeSend(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_before_send_called = true; + }); + $search->afterSend(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_after_send_called = true; + }); + $search->success(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_success_called = true; + }); + $search->error(function ($instance) use ( + &$search_error_called + ) { + $search_error_called = true; + }); + $search->complete(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_complete_called = true; + }); + $multi_curl->start(); $this->assertTrue($delete_before_send_called); + $this->assertTrue($delete_after_send_called); $this->assertTrue($delete_success_called); $this->assertFalse($delete_error_called); $this->assertTrue($delete_complete_called); $this->assertTrue($download_before_send_called); + $this->assertTrue($download_after_send_called); $this->assertTrue($download_success_called); $this->assertFalse($download_error_called); $this->assertTrue($download_complete_called); $this->assertTrue(unlink($download_file_path)); $this->assertTrue($get_before_send_called); + $this->assertTrue($get_after_send_called); $this->assertTrue($get_success_called); $this->assertFalse($get_error_called); $this->assertTrue($get_complete_called); $this->assertTrue($head_before_send_called); + $this->assertTrue($head_after_send_called); $this->assertTrue($head_success_called); $this->assertFalse($head_error_called); $this->assertTrue($head_complete_called); $this->assertTrue($options_before_send_called); + $this->assertTrue($options_after_send_called); $this->assertTrue($options_success_called); $this->assertFalse($options_error_called); $this->assertTrue($options_complete_called); $this->assertTrue($patch_before_send_called); + $this->assertTrue($patch_after_send_called); $this->assertTrue($patch_success_called); $this->assertFalse($patch_error_called); $this->assertTrue($patch_complete_called); $this->assertTrue($post_before_send_called); + $this->assertTrue($post_after_send_called); $this->assertTrue($post_success_called); $this->assertFalse($post_error_called); $this->assertTrue($post_complete_called); $this->assertTrue($put_before_send_called); + $this->assertTrue($put_after_send_called); $this->assertTrue($put_success_called); $this->assertFalse($put_error_called); $this->assertTrue($put_complete_called); + + $this->assertTrue($search_before_send_called); + $this->assertTrue($search_after_send_called); + $this->assertTrue($search_success_called); + $this->assertFalse($search_error_called); + $this->assertTrue($search_complete_called); } public function testCurlCallbackError() { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + $multi_curl = new MultiCurl(); $delete_before_send_called = false; + $delete_after_send_called = false; $delete_success_called = false; $delete_error_called = false; $delete_complete_called = false; $delete = $multi_curl->addDelete(Test::ERROR_URL); $delete->beforeSend(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_before_send_called = true; }); + $delete->afterSend(function ($instance) use ( + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); + $delete_after_send_called = true; + }); $delete->success(function ($instance) use ( - &$delete_success_called) { + &$delete_success_called + ) { $delete_success_called = true; }); $delete->error(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertFalse($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertFalse($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_error_called = true; }); $delete->complete(function ($instance) use ( - &$delete_before_send_called, &$delete_success_called, &$delete_error_called, &$delete_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($delete_before_send_called); - PHPUnit_Framework_Assert::assertFalse($delete_success_called); - PHPUnit_Framework_Assert::assertTrue($delete_error_called); - PHPUnit_Framework_Assert::assertFalse($delete_complete_called); + &$delete_before_send_called, + &$delete_after_send_called, + &$delete_success_called, + &$delete_error_called, + &$delete_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($delete_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($delete_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($delete_success_called); + \PHPUnit\Framework\Assert::assertTrue($delete_error_called); + \PHPUnit\Framework\Assert::assertFalse($delete_complete_called); $delete_complete_called = true; }); $download_before_send_called = false; + $download_after_send_called = false; $download_success_called = false; $download_error_called = false; $download_complete_called = false; $download_file_path = tempnam('/tmp', 'php-curl-class.'); $download = $multi_curl->addDownload(Test::ERROR_URL, $download_file_path); $download->beforeSend(function ($instance) use ( - &$download_before_send_called, &$download_success_called, &$download_error_called, - &$download_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_before_send_called = true; }); + $download->afterSend(function ($instance) use ( + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); + $download_after_send_called = true; + }); $download->success(function ($instance) use ( - &$download_success_called) { + &$download_success_called + ) { $download_success_called = true; }); $download->error(function ($instance) use ( - &$download_before_send_called, &$download_success_called, &$download_error_called, - &$download_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertFalse($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertFalse($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_error_called = true; }); $download->complete(function ($instance) use ( - &$download_before_send_called, &$download_success_called, &$download_error_called, - &$download_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($download_before_send_called); - PHPUnit_Framework_Assert::assertFalse($download_success_called); - PHPUnit_Framework_Assert::assertTrue($download_error_called); - PHPUnit_Framework_Assert::assertFalse($download_complete_called); + &$download_before_send_called, + &$download_after_send_called, + &$download_success_called, + &$download_error_called, + &$download_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($download_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($download_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($download_success_called); + \PHPUnit\Framework\Assert::assertTrue($download_error_called); + \PHPUnit\Framework\Assert::assertFalse($download_complete_called); $download_complete_called = true; }); $get_before_send_called = false; + $get_after_send_called = false; $get_success_called = false; $get_error_called = false; $get_complete_called = false; $get = $multi_curl->addGet(Test::ERROR_URL); $get->beforeSend(function ($instance) use ( - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_before_send_called = true; }); + $get->afterSend(function ($instance) use ( + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); + $get_after_send_called = true; + }); $get->success(function ($instance) use ( - &$get_success_called) { + &$get_success_called + ) { $get_success_called = true; }); $get->error(function ($instance) use ( - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertFalse($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertFalse($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_error_called = true; }); $get->complete(function ($instance) use ( - &$get_before_send_called, &$get_success_called, &$get_error_called, &$get_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($get_before_send_called); - PHPUnit_Framework_Assert::assertFalse($get_success_called); - PHPUnit_Framework_Assert::assertTrue($get_error_called); - PHPUnit_Framework_Assert::assertFalse($get_complete_called); + &$get_before_send_called, + &$get_after_send_called, + &$get_success_called, + &$get_error_called, + &$get_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($get_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($get_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($get_success_called); + \PHPUnit\Framework\Assert::assertTrue($get_error_called); + \PHPUnit\Framework\Assert::assertFalse($get_complete_called); $get_complete_called = true; }); $head_before_send_called = false; + $head_after_send_called = false; $head_success_called = false; $head_error_called = false; $head_complete_called = false; $head = $multi_curl->addHead(Test::ERROR_URL); $head->beforeSend(function ($instance) use ( - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_before_send_called = true; }); + $head->afterSend(function ($instance) use ( + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); + $head_after_send_called = true; + }); $head->success(function ($instance) use ( - &$head_success_called) { + &$head_success_called + ) { $head_success_called = true; }); $head->error(function ($instance) use ( - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertFalse($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertFalse($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_error_called = true; }); $head->complete(function ($instance) use ( - &$head_before_send_called, &$head_success_called, &$head_error_called, &$head_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($head_before_send_called); - PHPUnit_Framework_Assert::assertFalse($head_success_called); - PHPUnit_Framework_Assert::assertTrue($head_error_called); - PHPUnit_Framework_Assert::assertFalse($head_complete_called); + &$head_before_send_called, + &$head_after_send_called, + &$head_success_called, + &$head_error_called, + &$head_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($head_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($head_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($head_success_called); + \PHPUnit\Framework\Assert::assertTrue($head_error_called); + \PHPUnit\Framework\Assert::assertFalse($head_complete_called); $head_complete_called = true; }); $options_before_send_called = false; + $options_after_send_called = false; $options_success_called = false; $options_error_called = false; $options_complete_called = false; $options = $multi_curl->addOptions(Test::ERROR_URL); $options->beforeSend(function ($instance) use ( - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_before_send_called = true; }); + $options->afterSend(function ($instance) use ( + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); + $options_after_send_called = true; + }); $options->success(function ($instance) use ( - &$options_success_called) { + &$options_success_called + ) { $options_success_called = true; }); $options->error(function ($instance) use ( - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertFalse($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertFalse($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_error_called = true; }); $options->complete(function ($instance) use ( - &$options_before_send_called, &$options_success_called, &$options_error_called, &$options_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($options_before_send_called); - PHPUnit_Framework_Assert::assertFalse($options_success_called); - PHPUnit_Framework_Assert::assertTrue($options_error_called); - PHPUnit_Framework_Assert::assertFalse($options_complete_called); + &$options_before_send_called, + &$options_after_send_called, + &$options_success_called, + &$options_error_called, + &$options_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($options_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($options_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($options_success_called); + \PHPUnit\Framework\Assert::assertTrue($options_error_called); + \PHPUnit\Framework\Assert::assertFalse($options_complete_called); $options_complete_called = true; }); $patch_before_send_called = false; + $patch_after_send_called = false; $patch_success_called = false; $patch_error_called = false; $patch_complete_called = false; $patch = $multi_curl->addPatch(Test::ERROR_URL); $patch->beforeSend(function ($instance) use ( - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_before_send_called = true; }); + $patch->afterSend(function ($instance) use ( + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); + $patch_after_send_called = true; + }); $patch->success(function ($instance) use ( - &$patch_success_called) { + &$patch_success_called + ) { $patch_success_called = true; }); $patch->error(function ($instance) use ( - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertFalse($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertFalse($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_error_called = true; }); $patch->complete(function ($instance) use ( - &$patch_before_send_called, &$patch_success_called, &$patch_error_called, &$patch_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($patch_before_send_called); - PHPUnit_Framework_Assert::assertFalse($patch_success_called); - PHPUnit_Framework_Assert::assertTrue($patch_error_called); - PHPUnit_Framework_Assert::assertFalse($patch_complete_called); + &$patch_before_send_called, + &$patch_after_send_called, + &$patch_success_called, + &$patch_error_called, + &$patch_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($patch_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($patch_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($patch_success_called); + \PHPUnit\Framework\Assert::assertTrue($patch_error_called); + \PHPUnit\Framework\Assert::assertFalse($patch_complete_called); $patch_complete_called = true; }); $post_before_send_called = false; + $post_after_send_called = false; $post_success_called = false; $post_error_called = false; $post_complete_called = false; $post = $multi_curl->addPost(Test::ERROR_URL); $post->beforeSend(function ($instance) use ( - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_before_send_called = true; }); + $post->afterSend(function ($instance) use ( + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); + $post_after_send_called = true; + }); $post->success(function ($instance) use ( - &$post_success_called) { + &$post_success_called + ) { $post_sucess_called = true; }); $post->error(function ($instance) use ( - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertFalse($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertFalse($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_error_called = true; }); $post->complete(function ($instance) use ( - &$post_before_send_called, &$post_success_called, &$post_error_called, &$post_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($post_before_send_called); - PHPUnit_Framework_Assert::assertFalse($post_success_called); - PHPUnit_Framework_Assert::assertTrue($post_error_called); - PHPUnit_Framework_Assert::assertFalse($post_complete_called); + &$post_before_send_called, + &$post_after_send_called, + &$post_success_called, + &$post_error_called, + &$post_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($post_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($post_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($post_success_called); + \PHPUnit\Framework\Assert::assertTrue($post_error_called); + \PHPUnit\Framework\Assert::assertFalse($post_complete_called); $post_complete_called = true; }); $put_before_send_called = false; + $put_after_send_called = false; $put_success_called = false; $put_error_called = false; $put_complete_called = false; $put = $multi_curl->addPut(Test::ERROR_URL); $put->beforeSend(function ($instance) use ( - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertFalse($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_before_send_called = true; }); + $put->afterSend(function ($instance) use ( + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); + $put_after_send_called = true; + }); $put->success(function ($instance) use ( - &$put_success_called) { + &$put_success_called + ) { $put_success_called = true; }); $put->error(function ($instance) use ( - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertFalse($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertFalse($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_error_called = true; }); $put->complete(function ($instance) use ( - &$put_before_send_called, &$put_success_called, &$put_error_called, &$put_complete_called) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue($put_before_send_called); - PHPUnit_Framework_Assert::assertFalse($put_success_called); - PHPUnit_Framework_Assert::assertTrue($put_error_called); - PHPUnit_Framework_Assert::assertFalse($put_complete_called); + &$put_before_send_called, + &$put_after_send_called, + &$put_success_called, + &$put_error_called, + &$put_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($put_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($put_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($put_success_called); + \PHPUnit\Framework\Assert::assertTrue($put_error_called); + \PHPUnit\Framework\Assert::assertFalse($put_complete_called); $put_complete_called = true; }); + $search_before_send_called = false; + $search_after_send_called = false; + $search_success_called = false; + $search_error_called = false; + $search_complete_called = false; + $search = $multi_curl->addSearch(Test::ERROR_URL); + $search->beforeSend(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertFalse($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_before_send_called = true; + }); + $search->afterSend(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_after_send_called = true; + }); + $search->success(function ($instance) use ( + &$search_success_called + ) { + $search_success_called = true; + }); + $search->error(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertFalse($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_error_called = true; + }); + $search->complete(function ($instance) use ( + &$search_before_send_called, + &$search_after_send_called, + &$search_success_called, + &$search_error_called, + &$search_complete_called + ) { + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue($search_before_send_called); + \PHPUnit\Framework\Assert::assertTrue($search_after_send_called); + \PHPUnit\Framework\Assert::assertFalse($search_success_called); + \PHPUnit\Framework\Assert::assertTrue($search_error_called); + \PHPUnit\Framework\Assert::assertFalse($search_complete_called); + $search_complete_called = true; + }); + $multi_curl->start(); $this->assertTrue($delete_before_send_called); + $this->assertTrue($delete_after_send_called); $this->assertFalse($delete_success_called); $this->assertTrue($delete_error_called); $this->assertTrue($delete_complete_called); $this->assertTrue($download_before_send_called); + $this->assertTrue($download_after_send_called); $this->assertFalse($download_success_called); $this->assertTrue($download_error_called); $this->assertTrue($download_complete_called); $this->assertTrue(unlink($download_file_path)); $this->assertTrue($get_before_send_called); + $this->assertTrue($get_after_send_called); $this->assertFalse($get_success_called); $this->assertTrue($get_error_called); $this->assertTrue($get_complete_called); $this->assertTrue($head_before_send_called); + $this->assertTrue($head_after_send_called); $this->assertFalse($head_success_called); $this->assertTrue($head_error_called); $this->assertTrue($head_complete_called); $this->assertTrue($options_before_send_called); + $this->assertTrue($options_after_send_called); $this->assertFalse($options_success_called); $this->assertTrue($options_error_called); $this->assertTrue($options_complete_called); $this->assertTrue($patch_before_send_called); + $this->assertTrue($patch_after_send_called); $this->assertFalse($patch_success_called); $this->assertTrue($patch_error_called); $this->assertTrue($patch_complete_called); $this->assertTrue($post_before_send_called); + $this->assertTrue($post_after_send_called); $this->assertFalse($post_success_called); $this->assertTrue($post_error_called); $this->assertTrue($post_complete_called); $this->assertTrue($put_before_send_called); + $this->assertTrue($put_after_send_called); $this->assertFalse($put_success_called); $this->assertTrue($put_error_called); $this->assertTrue($put_complete_called); + + $this->assertTrue($search_before_send_called); + $this->assertTrue($search_after_send_called); + $this->assertFalse($search_success_called); + $this->assertTrue($search_error_called); + $this->assertTrue($search_complete_called); } public function testCurlCallbackOverride() { $multi_curl = new MultiCurl(); - $multi_curl->beforeSend(function() { - PHPUnit_Framework_Assert::assertFalse(true); + $multi_curl->beforeSend(function () { + \PHPUnit\Framework\Assert::assertFalse(true); }); - $multi_curl->success(function() { - PHPUnit_Framework_Assert::assertFalse(true); + $multi_curl->success(function () { + \PHPUnit\Framework\Assert::assertFalse(true); }); - $multi_curl->error(function() { - PHPUnit_Framework_Assert::assertFalse(true); + $multi_curl->error(function () { + \PHPUnit\Framework\Assert::assertFalse(true); }); - $multi_curl->complete(function() { - PHPUnit_Framework_Assert::assertFalse(true); + $multi_curl->complete(function () { + \PHPUnit\Framework\Assert::assertFalse(true); }); $delete_before_send_called = false; + $delete_after_send_called = false; $delete_success_called = false; $delete_error_called = false; $delete_complete_called = false; @@ -1367,6 +2746,9 @@ public function testCurlCallbackOverride() $delete->beforeSend(function ($instance) use (&$delete_before_send_called) { $delete_before_send_called = true; }); + $delete->afterSend(function ($instance) use (&$delete_after_send_called) { + $delete_after_send_called = true; + }); $delete->success(function ($instance) use (&$delete_success_called) { $delete_success_called = true; }); @@ -1378,6 +2760,7 @@ public function testCurlCallbackOverride() }); $download_before_send_called = false; + $download_after_send_called = false; $download_success_called = false; $download_error_called = false; $download_complete_called = false; @@ -1386,6 +2769,9 @@ public function testCurlCallbackOverride() $download->beforeSend(function ($instance) use (&$download_before_send_called) { $download_before_send_called = true; }); + $download->afterSend(function ($instance) use (&$download_after_send_called) { + $download_after_send_called = true; + }); $download->success(function ($instance) use (&$download_success_called) { $download_success_called = true; }); @@ -1397,6 +2783,7 @@ public function testCurlCallbackOverride() }); $get_before_send_called = false; + $get_after_send_called = false; $get_success_called = false; $get_error_called = false; $get_complete_called = false; @@ -1404,6 +2791,9 @@ public function testCurlCallbackOverride() $get->beforeSend(function ($instance) use (&$get_before_send_called) { $get_before_send_called = true; }); + $get->afterSend(function ($instance) use (&$get_after_send_called) { + $get_after_send_called = true; + }); $get->success(function ($instance) use (&$get_success_called) { $get_success_called = true; }); @@ -1415,6 +2805,7 @@ public function testCurlCallbackOverride() }); $head_before_send_called = false; + $head_after_send_called = false; $head_success_called = false; $head_error_called = false; $head_complete_called = false; @@ -1422,6 +2813,9 @@ public function testCurlCallbackOverride() $head->beforeSend(function ($instance) use (&$head_before_send_called) { $head_before_send_called = true; }); + $head->afterSend(function ($instance) use (&$head_after_send_called) { + $head_after_send_called = true; + }); $head->success(function ($instance) use (&$head_success_called) { $head_success_called = true; }); @@ -1433,6 +2827,7 @@ public function testCurlCallbackOverride() }); $options_before_send_called = false; + $options_after_send_called = false; $options_success_called = false; $options_error_called = false; $options_complete_called = false; @@ -1440,6 +2835,9 @@ public function testCurlCallbackOverride() $options->beforeSend(function ($instance) use (&$options_before_send_called) { $options_before_send_called = true; }); + $options->afterSend(function ($instance) use (&$options_after_send_called) { + $options_after_send_called = true; + }); $options->success(function ($instance) use (&$options_success_called) { $options_success_called = true; }); @@ -1451,6 +2849,7 @@ public function testCurlCallbackOverride() }); $patch_before_send_called = false; + $patch_after_send_called = false; $patch_success_called = false; $patch_error_called = false; $patch_complete_called = false; @@ -1458,6 +2857,9 @@ public function testCurlCallbackOverride() $patch->beforeSend(function ($instance) use (&$patch_before_send_called) { $patch_before_send_called = true; }); + $patch->afterSend(function ($instance) use (&$patch_after_send_called) { + $patch_after_send_called = true; + }); $patch->success(function ($instance) use (&$patch_success_called) { $patch_success_called = true; }); @@ -1469,6 +2871,7 @@ public function testCurlCallbackOverride() }); $post_before_send_called = false; + $post_after_send_called = false; $post_success_called = false; $post_error_called = false; $post_complete_called = false; @@ -1476,6 +2879,9 @@ public function testCurlCallbackOverride() $post->beforeSend(function ($instance) use (&$post_before_send_called) { $post_before_send_called = true; }); + $post->afterSend(function ($instance) use (&$post_after_send_called) { + $post_after_send_called = true; + }); $post->success(function ($instance) use (&$post_success_called) { $post_success_called = true; }); @@ -1487,6 +2893,7 @@ public function testCurlCallbackOverride() }); $put_before_send_called = false; + $put_after_send_called = false; $put_success_called = false; $put_error_called = false; $put_complete_called = false; @@ -1494,6 +2901,9 @@ public function testCurlCallbackOverride() $put->beforeSend(function ($instance) use (&$put_before_send_called) { $put_before_send_called = true; }); + $put->afterSend(function ($instance) use (&$put_after_send_called) { + $put_after_send_called = true; + }); $put->success(function ($instance) use (&$put_success_called) { $put_success_called = true; }); @@ -1504,497 +2914,3496 @@ public function testCurlCallbackOverride() $put_complete_called = true; }); + $search_before_send_called = false; + $search_after_send_called = false; + $search_success_called = false; + $search_error_called = false; + $search_complete_called = false; + $search = $multi_curl->addsearch(Test::TEST_URL); + $search->beforeSend(function ($instance) use (&$search_before_send_called) { + $search_before_send_called = true; + }); + $search->afterSend(function ($instance) use (&$search_after_send_called) { + $search_after_send_called = true; + }); + $search->success(function ($instance) use (&$search_success_called) { + $search_success_called = true; + }); + $search->error(function ($instance) use (&$search_error_called) { + $search_error_called = true; + }); + $search->complete(function ($instance) use (&$search_complete_called) { + $search_complete_called = true; + }); + $multi_curl->start(); $this->assertTrue($delete_before_send_called); + $this->assertTrue($delete_after_send_called); $this->assertTrue($delete_success_called); $this->assertFalse($delete_error_called); $this->assertTrue($delete_complete_called); $this->assertTrue($download_before_send_called); + $this->assertTrue($download_after_send_called); $this->assertTrue($download_success_called); $this->assertFalse($download_error_called); $this->assertTrue($download_complete_called); $this->assertTrue(unlink($download_file_path)); $this->assertTrue($get_before_send_called); + $this->assertTrue($get_after_send_called); $this->assertTrue($get_success_called); $this->assertFalse($get_error_called); $this->assertTrue($get_complete_called); $this->assertTrue($head_before_send_called); + $this->assertTrue($head_after_send_called); $this->assertTrue($head_success_called); $this->assertFalse($head_error_called); $this->assertTrue($head_complete_called); $this->assertTrue($options_before_send_called); + $this->assertTrue($options_after_send_called); $this->assertTrue($options_success_called); $this->assertFalse($options_error_called); $this->assertTrue($options_complete_called); $this->assertTrue($patch_before_send_called); + $this->assertTrue($patch_after_send_called); $this->assertTrue($patch_success_called); $this->assertFalse($patch_error_called); $this->assertTrue($patch_complete_called); $this->assertTrue($post_before_send_called); + $this->assertTrue($post_after_send_called); $this->assertTrue($post_success_called); $this->assertFalse($post_error_called); $this->assertTrue($post_complete_called); $this->assertTrue($put_before_send_called); + $this->assertTrue($put_after_send_called); $this->assertTrue($put_success_called); $this->assertFalse($put_error_called); $this->assertTrue($put_complete_called); + + $this->assertTrue($search_before_send_called); + $this->assertTrue($search_after_send_called); + $this->assertTrue($search_success_called); + $this->assertFalse($search_error_called); + $this->assertTrue($search_complete_called); } - public function testSetOptAndSetOptOverride() + public function testCurlCallbackAddedAfter() { - $multi_curl_user_agent = 'multi curl user agent'; - $curl_user_agent = 'curl user agent'; - $data = array('key' => 'HTTP_USER_AGENT'); - + $delete_before_send_called = false; + $delete_after_send_called = false; + $delete_success_called = false; + $delete_error_called = false; + $delete_complete_called = false; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'server'); - $multi_curl->setOpt(CURLOPT_USERAGENT, $multi_curl_user_agent); - - $get_1 = $multi_curl->addGet(Test::TEST_URL, $data); - $get_1->complete(function ($instance) use ($multi_curl_user_agent) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals($multi_curl_user_agent, $instance->getOpt(CURLOPT_USERAGENT)); - PHPUnit_Framework_Assert::assertEquals($multi_curl_user_agent, $instance->response); + $multi_curl->addDelete(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$delete_before_send_called) { + $delete_before_send_called = true; }); - - $get_2 = $multi_curl->addGet(Test::TEST_URL, $data); - $get_2->beforeSend(function ($instance) use ($curl_user_agent) { - $instance->setOpt(CURLOPT_USERAGENT, $curl_user_agent); + $multi_curl->afterSend(function ($instance) use (&$delete_after_send_called) { + $delete_after_send_called = true; + }); + $multi_curl->success(function ($instance) use (&$delete_success_called) { + $delete_success_called = true; }); - $get_2->complete(function ($instance) use ($curl_user_agent) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals($curl_user_agent, $instance->getOpt(CURLOPT_USERAGENT)); - PHPUnit_Framework_Assert::assertEquals($curl_user_agent, $instance->response); + $multi_curl->error(function ($instance) use (&$delete_error_called) { + $delete_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$delete_complete_called) { + $delete_complete_called = true; }); - $multi_curl->start(); + $this->assertTrue($delete_before_send_called); + $this->assertTrue($delete_after_send_called); + $this->assertTrue($delete_success_called); + $this->assertFalse($delete_error_called); + $this->assertTrue($delete_complete_called); - $this->assertEquals($multi_curl_user_agent, $multi_curl->getOpt(CURLOPT_USERAGENT)); - $this->assertEquals($multi_curl_user_agent, $get_1->getOpt(CURLOPT_USERAGENT)); - $this->assertEquals($multi_curl_user_agent, $get_1->response); - $this->assertEquals($curl_user_agent, $get_2->getOpt(CURLOPT_USERAGENT)); - $this->assertEquals($curl_user_agent, $get_2->response); - } - - public function testBasicHttpAuthSuccess() - { - $username1 = 'myusername'; - $password1 = 'mypassword'; - $username2 = 'myotherusername'; - $password2 = 'myotherpassword'; - + $download_before_send_called = false; + $download_after_send_called = false; + $download_success_called = false; + $download_error_called = false; + $download_complete_called = false; + $download_file_path = tempnam('/tmp', 'php-curl-class.'); $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'http_basic_auth'); - $multi_curl->setBasicAuthentication($username1, $password1); - - $get_1 = $multi_curl->addGet(Test::TEST_URL); - $get_1->complete(function ($instance) use ($username1, $password1) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals($username1, $instance->response->username); - PHPUnit_Framework_Assert::assertEquals($password1, $instance->response->password); + $multi_curl->addDownload(Test::TEST_URL, $download_file_path); + $multi_curl->beforeSend(function ($instance) use (&$download_before_send_called) { + $download_before_send_called = true; }); - - $get_2 = $multi_curl->addGet(Test::TEST_URL); - $get_2->beforeSend(function ($instance) use ($username2, $password2) { - $instance->setBasicAuthentication($username2, $password2); + $multi_curl->afterSend(function ($instance) use (&$download_after_send_called) { + $download_after_send_called = true; }); - $get_2->complete(function ($instance) use ($username2, $password2) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals($username2, $instance->response->username); - PHPUnit_Framework_Assert::assertEquals($password2, $instance->response->password); + $multi_curl->success(function ($instance) use (&$download_success_called) { + $download_success_called = true; }); - - $multi_curl->start(); - - $this->assertEquals(CURLAUTH_BASIC, $multi_curl->getOpt(CURLOPT_HTTPAUTH)); - $this->assertEquals(CURLAUTH_BASIC, $get_1->getOpt(CURLOPT_HTTPAUTH)); - $this->assertEquals($username1, $get_1->response->username); - $this->assertEquals($password1, $get_1->response->password); - $this->assertEquals(CURLAUTH_BASIC, $get_2->getOpt(CURLOPT_HTTPAUTH)); - $this->assertEquals($username2, $get_2->response->username); - $this->assertEquals($password2, $get_2->response->password); - } - - public function testDigestHttpAuthSuccess() - { - // Skip Digest Access Authentication test on HHVM. - // https://github.com/facebook/hhvm/issues/5201 - if (defined('HHVM_VERSION')) { - return; - } - - $username = 'myusername'; - $password = 'mypassword'; - $invalid_password = 'anotherpassword'; - - // Ensure that http digest returns canceled when not using any http digest authentication. - $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); - - $get_1 = $multi_curl->addGet(Test::TEST_URL); - $get_1->complete(function ($instance) { - PHPUnit_Framework_Assert::assertEquals('canceled', $instance->response); - PHPUnit_Framework_Assert::assertEquals(401, $instance->httpStatusCode); + $multi_curl->error(function ($instance) use (&$download_error_called) { + $download_error_called = true; }); - - $multi_curl->start(); - - // Ensure that http digest returns invalid when using incorrect http digest authentication. - $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); - $multi_curl->setDigestAuthentication($username, $invalid_password); - $this->assertEquals(CURLAUTH_DIGEST, $multi_curl->getOpt(CURLOPT_HTTPAUTH)); - - $get_1 = $multi_curl->addGet(Test::TEST_URL); - $get_1->complete(function ($instance) { - PHPUnit_Framework_Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); - PHPUnit_Framework_Assert::assertEquals('invalid', $instance->response); - PHPUnit_Framework_Assert::assertEquals(401, $instance->httpStatusCode); + $multi_curl->complete(function ($instance) use (&$download_complete_called) { + $download_complete_called = true; }); - $multi_curl->start(); + $this->assertTrue($download_before_send_called); + $this->assertTrue($download_after_send_called); + $this->assertTrue($download_success_called); + $this->assertFalse($download_error_called); + $this->assertTrue($download_complete_called); + $this->assertTrue(unlink($download_file_path)); - // Ensure that http digest returns valid when using correct http digest authentication. + $get_before_send_called = false; + $get_after_send_called = false; + $get_success_called = false; + $get_error_called = false; + $get_complete_called = false; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); - $multi_curl->setDigestAuthentication($username, $password); - $this->assertEquals(CURLAUTH_DIGEST, $multi_curl->getOpt(CURLOPT_HTTPAUTH)); - - $get_1 = $multi_curl->addGet(Test::TEST_URL); - $get_1->complete(function ($instance) { - PHPUnit_Framework_Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); - PHPUnit_Framework_Assert::assertEquals('valid', $instance->response); - PHPUnit_Framework_Assert::assertEquals(200, $instance->httpStatusCode); + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$get_before_send_called) { + $get_before_send_called = true; + }); + $multi_curl->afterSend(function ($instance) use (&$get_after_send_called) { + $get_after_send_called = true; + }); + $multi_curl->success(function ($instance) use (&$get_success_called) { + $get_success_called = true; + }); + $multi_curl->error(function ($instance) use (&$get_error_called) { + $get_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$get_complete_called) { + $get_complete_called = true; }); - $multi_curl->start(); + $this->assertTrue($get_before_send_called); + $this->assertTrue($get_after_send_called); + $this->assertTrue($get_success_called); + $this->assertFalse($get_error_called); + $this->assertTrue($get_complete_called); - // Ensure that http digest can return both invalid and valid when using - // incorrect and correct authentication in the same MultiCurl. + $head_before_send_called = false; + $head_after_send_called = false; + $head_success_called = false; + $head_error_called = false; + $head_complete_called = false; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); - $multi_curl->setDigestAuthentication($username, $password); - $this->assertEquals(CURLAUTH_DIGEST, $multi_curl->getOpt(CURLOPT_HTTPAUTH)); - - $get_1 = $multi_curl->addGet(Test::TEST_URL); - $get_1->beforeSend(function ($instance) use ($username, $invalid_password) { - $instance->setDigestAuthentication($username, $invalid_password); - PHPUnit_Framework_Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); + $multi_curl->addHead(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$head_before_send_called) { + $head_before_send_called = true; }); - $get_1->complete(function ($instance) { - PHPUnit_Framework_Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); - PHPUnit_Framework_Assert::assertEquals('invalid', $instance->response); - PHPUnit_Framework_Assert::assertEquals(401, $instance->httpStatusCode); + $multi_curl->afterSend(function ($instance) use (&$head_after_send_called) { + $head_after_send_called = true; }); - - $get_2 = $multi_curl->addGet(Test::TEST_URL); - $get_2->complete(function ($instance) { - PHPUnit_Framework_Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); - PHPUnit_Framework_Assert::assertEquals('valid', $instance->response); - PHPUnit_Framework_Assert::assertEquals(200, $instance->httpStatusCode); + $multi_curl->success(function ($instance) use (&$head_success_called) { + $head_success_called = true; + }); + $multi_curl->error(function ($instance) use (&$head_error_called) { + $head_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$head_complete_called) { + $head_complete_called = true; }); - $multi_curl->start(); - } - - public function testCookies() - { - $data = array('key' => 'mycookie'); + $this->assertTrue($head_before_send_called); + $this->assertTrue($head_after_send_called); + $this->assertTrue($head_success_called); + $this->assertFalse($head_error_called); + $this->assertTrue($head_complete_called); + $options_before_send_called = false; + $options_after_send_called = false; + $options_success_called = false; + $options_error_called = false; + $options_complete_called = false; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'cookie'); - $multi_curl->setCookie('mycookie', 'yum'); - - $get_1 = $multi_curl->addGet(Test::TEST_URL, $data); - $get_1->complete(function ($instance) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals('yum', $instance->response); + $multi_curl->addOptions(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$options_before_send_called) { + $options_before_send_called = true; }); - - $get_2 = $multi_curl->addGet(Test::TEST_URL, $data); - $get_2->beforeSend(function ($instance) { - $instance->setCookie('mycookie', 'yummy'); + $multi_curl->afterSend(function ($instance) use (&$options_after_send_called) { + $options_after_send_called = true; }); - $get_2->complete(function ($instance) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals('yummy', $instance->response); + $multi_curl->success(function ($instance) use (&$options_success_called) { + $options_success_called = true; + }); + $multi_curl->error(function ($instance) use (&$options_error_called) { + $options_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$options_complete_called) { + $options_complete_called = true; }); - $multi_curl->start(); + $this->assertTrue($options_before_send_called); + $this->assertTrue($options_after_send_called); + $this->assertTrue($options_success_called); + $this->assertFalse($options_error_called); + $this->assertTrue($options_complete_called); - $this->assertEquals('yum', $get_1->response); - $this->assertEquals('yummy', $get_2->response); - } - - public function testJSONDecoder() - { + $patch_before_send_called = false; + $patch_after_send_called = false; + $patch_success_called = false; + $patch_error_called = false; + $patch_complete_called = false; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'json_response'); - $multi_curl->setJsonDecoder(function($response) { - return 'foo'; + $multi_curl->addPatch(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$patch_before_send_called) { + $patch_before_send_called = true; }); - - $get_1 = $multi_curl->addGet(Test::TEST_URL); - $get_1->complete(function ($instance) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals('foo', $instance->response); + $multi_curl->afterSend(function ($instance) use (&$patch_after_send_called) { + $patch_after_send_called = true; }); - - $get_2 = $multi_curl->addGet(Test::TEST_URL); - $get_2->beforeSend(function ($instance) { - $instance->setJsonDecoder(function($response) { - return 'bar'; - }); + $multi_curl->success(function ($instance) use (&$patch_success_called) { + $patch_success_called = true; }); - $get_2->complete(function ($instance) { - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertEquals('bar', $instance->response); + $multi_curl->error(function ($instance) use (&$patch_error_called) { + $patch_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$patch_complete_called) { + $patch_complete_called = true; }); - $multi_curl->start(); - $this->assertEquals('foo', $get_1->response); + $this->assertTrue($patch_before_send_called); + $this->assertTrue($patch_after_send_called); + $this->assertTrue($patch_success_called); + $this->assertFalse($patch_error_called); + $this->assertTrue($patch_complete_called); + + $post_before_send_called = false; + $post_after_send_called = false; + $post_success_called = false; + $post_error_called = false; + $post_complete_called = false; + $multi_curl = new MultiCurl(); + $multi_curl->addPost(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$post_before_send_called) { + $post_before_send_called = true; + }); + $multi_curl->afterSend(function ($instance) use (&$post_after_send_called) { + $post_after_send_called = true; + }); + $multi_curl->success(function ($instance) use (&$post_success_called) { + $post_success_called = true; + }); + $multi_curl->error(function ($instance) use (&$post_error_called) { + $post_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$post_complete_called) { + $post_complete_called = true; + }); + $multi_curl->start(); + $this->assertTrue($post_before_send_called); + $this->assertTrue($post_after_send_called); + $this->assertTrue($post_success_called); + $this->assertFalse($post_error_called); + $this->assertTrue($post_complete_called); + + $put_before_send_called = false; + $put_after_send_called = false; + $put_success_called = false; + $put_error_called = false; + $put_complete_called = false; + $multi_curl = new MultiCurl(); + $multi_curl->addPut(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$put_before_send_called) { + $put_before_send_called = true; + }); + $multi_curl->afterSend(function ($instance) use (&$put_after_send_called) { + $put_after_send_called = true; + }); + $multi_curl->success(function ($instance) use (&$put_success_called) { + $put_success_called = true; + }); + $multi_curl->error(function ($instance) use (&$put_error_called) { + $put_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$put_complete_called) { + $put_complete_called = true; + }); + $multi_curl->start(); + $this->assertTrue($put_before_send_called); + $this->assertTrue($put_after_send_called); + $this->assertTrue($put_success_called); + $this->assertFalse($put_error_called); + $this->assertTrue($put_complete_called); + + $search_before_send_called = false; + $search_after_send_called = false; + $search_success_called = false; + $search_error_called = false; + $search_complete_called = false; + $multi_curl = new MultiCurl(); + $multi_curl->addSearch(Test::TEST_URL); + $multi_curl->beforeSend(function ($instance) use (&$search_before_send_called) { + $search_before_send_called = true; + }); + $multi_curl->afterSend(function ($instance) use (&$search_after_send_called) { + $search_after_send_called = true; + }); + $multi_curl->success(function ($instance) use (&$search_success_called) { + $search_success_called = true; + }); + $multi_curl->error(function ($instance) use (&$search_error_called) { + $search_error_called = true; + }); + $multi_curl->complete(function ($instance) use (&$search_complete_called) { + $search_complete_called = true; + }); + $multi_curl->start(); + $this->assertTrue($search_before_send_called); + $this->assertTrue($search_after_send_called); + $this->assertTrue($search_success_called); + $this->assertFalse($search_error_called); + $this->assertTrue($search_complete_called); + } + + public function testSetOptAndSetOptOverride() + { + $multi_curl_user_agent = 'multi curl user agent'; + $curl_user_agent_2 = 'curl user agent 2'; + $curl_user_agent_3 = 'curl user agent 3'; + $data = ['key' => 'HTTP_USER_AGENT']; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'server'); + + $multi_curl->setOpt(CURLOPT_USERAGENT, $multi_curl_user_agent); + + $get_1 = $multi_curl->addGet(Test::TEST_URL, $data); + $get_1->complete(function ($instance) use ($multi_curl_user_agent) { + \PHPUnit\Framework\Assert::assertEquals($multi_curl_user_agent, $instance->getOpt(CURLOPT_USERAGENT)); + \PHPUnit\Framework\Assert::assertEquals($multi_curl_user_agent, $instance->response); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL, $data); + $get_2->setOpt(CURLOPT_USERAGENT, $curl_user_agent_2); + $get_2->complete(function ($instance) use ($curl_user_agent_2) { + \PHPUnit\Framework\Assert::assertEquals($curl_user_agent_2, $instance->getOpt(CURLOPT_USERAGENT)); + \PHPUnit\Framework\Assert::assertEquals($curl_user_agent_2, $instance->response); + }); + + $get_3 = $multi_curl->addGet(Test::TEST_URL, $data); + $get_3->beforeSend(function ($instance) use ($curl_user_agent_3) { + $instance->setOpt(CURLOPT_USERAGENT, $curl_user_agent_3); + }); + $get_3->complete(function ($instance) use ($curl_user_agent_3) { + \PHPUnit\Framework\Assert::assertEquals($curl_user_agent_3, $instance->getOpt(CURLOPT_USERAGENT)); + \PHPUnit\Framework\Assert::assertEquals($curl_user_agent_3, $instance->response); + }); + + $multi_curl->start(); + } + + public function testSetHeaderAndOverride() + { + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('header-for-all-before', 'a'); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->setHeader('header-for-1st-request', '1'); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('a', $instance->requestHeaders['header-for-all-before']); + \PHPUnit\Framework\Assert::assertEquals('b', $instance->requestHeaders['header-for-all-after']); + \PHPUnit\Framework\Assert::assertEquals('1', $instance->requestHeaders['header-for-1st-request']); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->setHeader('header-for-2nd-request', '2'); + $get_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('a', $instance->requestHeaders['header-for-all-before']); + \PHPUnit\Framework\Assert::assertEquals('b', $instance->requestHeaders['header-for-all-after']); + \PHPUnit\Framework\Assert::assertEquals('2', $instance->requestHeaders['header-for-2nd-request']); + }); + + $multi_curl->setHeader('header-for-all-after', 'b'); + $multi_curl->start(); + } + + public function testBasicHttpAuthSuccess() + { + $username1 = 'myusername'; + $password1 = 'mypassword'; + $username2 = 'myotherusername'; + $password2 = 'myotherpassword'; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'http_basic_auth'); + $multi_curl->setBasicAuthentication($username1, $password1); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_BASIC, $instance->getOpt(CURLOPT_HTTPAUTH)); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->complete(function ($instance) use ($username1, $password1) { + \PHPUnit\Framework\Assert::assertEquals($username1, $instance->response->username); + \PHPUnit\Framework\Assert::assertEquals($password1, $instance->response->password); + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_BASIC, $instance->getOpt(CURLOPT_HTTPAUTH)); + }); + + $get_3 = $multi_curl->addGet(Test::TEST_URL); + $get_3->beforeSend(function ($instance) use ($username2, $password2) { + $instance->setBasicAuthentication($username2, $password2); + }); + $get_3->complete(function ($instance) use ($username2, $password2) { + \PHPUnit\Framework\Assert::assertEquals($username2, $instance->response->username); + \PHPUnit\Framework\Assert::assertEquals($password2, $instance->response->password); + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_BASIC, $instance->getOpt(CURLOPT_HTTPAUTH)); + }); + + $multi_curl->start(); + } + + public function testDigestHttpAuthSuccess() + { + $username = 'myusername'; + $password = 'mypassword'; + $invalid_password = 'anotherpassword'; + + // Ensure that http digest returns canceled when not using any http digest authentication. + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('canceled', $instance->response); + \PHPUnit\Framework\Assert::assertEquals(401, $instance->httpStatusCode); + }); + + $multi_curl->start(); + + // Ensure that http digest returns invalid when using incorrect http digest authentication. + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); + $multi_curl->setDigestAuthentication($username, $invalid_password); + $this->assertEquals(CURLAUTH_DIGEST, $multi_curl->getOpt(CURLOPT_HTTPAUTH)); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); + \PHPUnit\Framework\Assert::assertEquals('invalid', $instance->response); + \PHPUnit\Framework\Assert::assertEquals(401, $instance->httpStatusCode); + }); + + $multi_curl->start(); + + // Ensure that http digest returns valid when using correct http digest authentication. + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); + $multi_curl->setDigestAuthentication($username, $password); + $this->assertEquals(CURLAUTH_DIGEST, $multi_curl->getOpt(CURLOPT_HTTPAUTH)); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); + \PHPUnit\Framework\Assert::assertEquals('valid', $instance->response); + \PHPUnit\Framework\Assert::assertEquals(200, $instance->httpStatusCode); + }); + + $multi_curl->start(); + + // Ensure that http digest can return both invalid and valid when using + // incorrect and correct authentication in the same MultiCurl. + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'http_digest_auth'); + $multi_curl->setDigestAuthentication($username, $password); + $this->assertEquals(CURLAUTH_DIGEST, $multi_curl->getOpt(CURLOPT_HTTPAUTH)); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->beforeSend(function ($instance) use ($username, $invalid_password) { + $instance->setDigestAuthentication($username, $invalid_password); + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); + }); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); + \PHPUnit\Framework\Assert::assertEquals('invalid', $instance->response); + \PHPUnit\Framework\Assert::assertEquals(401, $instance->httpStatusCode); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals(CURLAUTH_DIGEST, $instance->getOpt(CURLOPT_HTTPAUTH)); + \PHPUnit\Framework\Assert::assertEquals('valid', $instance->response); + \PHPUnit\Framework\Assert::assertEquals(200, $instance->httpStatusCode); + }); + + $multi_curl->start(); + } + + public function testSetCookie() + { + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'setcookie'); + $multi_curl->setCookie('mycookie', 'yum'); + $multi_curl->setCookie('cookie-for-all-before', 'a'); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->setCookie('cookie-for-1st-request', '1'); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('yum', $instance->responseCookies['mycookie']); + \PHPUnit\Framework\Assert::assertEquals('a', $instance->responseCookies['cookie-for-all-before']); + \PHPUnit\Framework\Assert::assertEquals('b', $instance->responseCookies['cookie-for-all-after']); + \PHPUnit\Framework\Assert::assertEquals('1', $instance->responseCookies['cookie-for-1st-request']); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->setCookie('cookie-for-2nd-request', '2'); + $get_2->beforeSend(function ($instance) { + $instance->setCookie('mycookie', 'yummy'); + }); + $get_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('yummy', $instance->responseCookies['mycookie']); + \PHPUnit\Framework\Assert::assertEquals('a', $instance->responseCookies['cookie-for-all-before']); + \PHPUnit\Framework\Assert::assertEquals('b', $instance->responseCookies['cookie-for-all-after']); + \PHPUnit\Framework\Assert::assertEquals('2', $instance->responseCookies['cookie-for-2nd-request']); + }); + + $multi_curl->setCookie('cookie-for-all-after', 'b'); + $multi_curl->start(); + + $this->assertEquals('yum', $get_1->responseCookies['mycookie']); + $this->assertEquals('yummy', $get_2->responseCookies['mycookie']); + } + + public function testSetCookies() + { + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'setcookie'); + $multi_curl->setCookies([ + 'mycookie' => 'yum', + 'cookie-for-all-before' => 'a', + ]); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->setCookies([ + 'cookie-for-1st-request' => '1', + ]); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('yum', $instance->responseCookies['mycookie']); + \PHPUnit\Framework\Assert::assertEquals('a', $instance->responseCookies['cookie-for-all-before']); + \PHPUnit\Framework\Assert::assertEquals('b', $instance->responseCookies['cookie-for-all-after']); + \PHPUnit\Framework\Assert::assertEquals('1', $instance->responseCookies['cookie-for-1st-request']); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->setCookies([ + 'cookie-for-2nd-request' => '2', + ]); + $get_2->beforeSend(function ($instance) { + $instance->setCookies([ + 'mycookie' => 'yummy', + ]); + }); + $get_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('yummy', $instance->responseCookies['mycookie']); + \PHPUnit\Framework\Assert::assertEquals('a', $instance->responseCookies['cookie-for-all-before']); + \PHPUnit\Framework\Assert::assertEquals('b', $instance->responseCookies['cookie-for-all-after']); + \PHPUnit\Framework\Assert::assertEquals('2', $instance->responseCookies['cookie-for-2nd-request']); + }); + + $multi_curl->setCookies([ + 'cookie-for-all-after' => 'b', + ]); + $multi_curl->start(); + + $this->assertEquals('yum', $get_1->responseCookies['mycookie']); + $this->assertEquals('yummy', $get_2->responseCookies['mycookie']); + } + + public function testSetCookieStringMultiCurl() + { + $cookie_string = 'fruit=apple; color=red'; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'cookie'); + $multi_curl->setCookieString($cookie_string); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) use ($cookie_string) { + \PHPUnit\Framework\Assert::assertEquals('fruit=apple&color=red', $instance->response); + \PHPUnit\Framework\Assert::assertEquals($cookie_string, $instance->getOpt(CURLOPT_COOKIE)); + }); + + $multi_curl->start(); + } + + public function testSetCookieStringCurl() + { + $cookie_string = 'fruit=apple; color=red'; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'cookie'); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->setCookieString($cookie_string); + $get_1->complete(function ($instance) use ($cookie_string) { + \PHPUnit\Framework\Assert::assertEquals('fruit=apple&color=red', $instance->response); + \PHPUnit\Framework\Assert::assertEquals($cookie_string, $instance->getOpt(CURLOPT_COOKIE)); + }); + + $multi_curl->start(); + } + + public function testSetCookieString() + { + $cookie_string_1 = 'fruit=apple; color=red'; + $cookie_string_2 = 'fruit=banana; color=yellow'; + $cookie_string_3 = 'fruit=orange; color=orange'; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'cookie'); + $multi_curl->setCookieString($cookie_string_1); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) use ($cookie_string_1) { + \PHPUnit\Framework\Assert::assertEquals('fruit=apple&color=red', $instance->response); + \PHPUnit\Framework\Assert::assertEquals($cookie_string_1, $instance->getOpt(CURLOPT_COOKIE)); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->setCookieString($cookie_string_2); + $get_2->complete(function ($instance) use ($cookie_string_2) { + \PHPUnit\Framework\Assert::assertEquals('fruit=banana&color=yellow', $instance->response); + \PHPUnit\Framework\Assert::assertEquals($cookie_string_2, $instance->getOpt(CURLOPT_COOKIE)); + }); + + $get_3 = $multi_curl->addGet(Test::TEST_URL); + $get_3->setCookieString($cookie_string_3); + $get_3->complete(function ($instance) use ($cookie_string_3) { + \PHPUnit\Framework\Assert::assertEquals('fruit=orange&color=orange', $instance->response); + \PHPUnit\Framework\Assert::assertEquals($cookie_string_3, $instance->getOpt(CURLOPT_COOKIE)); + }); + + $multi_curl->start(); + } + + public function testJsonRequest() + { + foreach ( + [ + [ + [ + 'key' => 'value', + ], + '{"key":"value"}', + ], + [ + [ + 'key' => 'value', + 'strings' => [ + 'a', + 'b', + 'c', + ], + ], + '{"key":"value","strings":["a","b","c"]}', + ], + ] as $test + ) { + list($data, $expected_response) = $test; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'post_json'); + $multi_curl->complete(function ($instance) use ($expected_response, $data) { + \PHPUnit\Framework\Assert::assertEquals($expected_response, $instance->response); + }); + $multi_curl->addPost(Test::TEST_URL, json_encode($data)); + $multi_curl->start(); + + foreach ( + [ + 'Content-Type', + 'content-type', + 'CONTENT-TYPE'] as $key + ) { + foreach ( + [ + 'APPLICATION/JSON', + 'APPLICATION/JSON; CHARSET=UTF-8', + 'APPLICATION/JSON;CHARSET=UTF-8', + 'application/json', + 'application/json; charset=utf-8', + 'application/json;charset=UTF-8', + ] as $value + ) { + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'post_json'); + $multi_curl->setHeader($key, $value); + $multi_curl->complete(function ($instance) use ($expected_response, $data) { + \PHPUnit\Framework\Assert::assertEquals($expected_response, $instance->response); + }); + $multi_curl->addPost(Test::TEST_URL, json_encode($data)); + $multi_curl->start(); + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'post_json'); + $multi_curl->setHeader($key, $value); + $multi_curl->complete(function ($instance) use ($expected_response, $data) { + \PHPUnit\Framework\Assert::assertEquals($expected_response, $instance->response); + }); + $multi_curl->addPost(Test::TEST_URL, $data); + $multi_curl->start(); + } + } + } + } + + public function testJsonDecoder() + { + $data = [ + 'key' => 'Content-Type', + 'value' => 'application/json', + ]; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'json_response'); + $multi_curl->setJsonDecoder(function ($response) { + return 'first decoder'; + }); + + $post_1 = $multi_curl->addPost(Test::TEST_URL, $data); + $post_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('first decoder', $instance->response); + }); + + $post_2 = $multi_curl->addPost(Test::TEST_URL, $data); + $post_2->setJsonDecoder(function ($response) { + return 'second decoder'; + }); + $post_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('second decoder', $instance->response); + }); + + $multi_curl->start(); + $this->assertEquals('first decoder', $post_1->response); + $this->assertEquals('second decoder', $post_2->response); + + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'json_response'); + + $post_1 = $multi_curl->addPost(Test::TEST_URL, $data); + $post_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_object($instance->response)); + \PHPUnit\Framework\Assert::assertFalse(is_array($instance->response)); + }); + + $post_2 = $multi_curl->addPost(Test::TEST_URL, $data); + $post_2->setJsonDecoder(function ($response) { + return json_decode($response, true); + }); + $post_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertFalse(is_object($instance->response)); + \PHPUnit\Framework\Assert::assertTrue(is_array($instance->response)); + }); + + $post_3 = $multi_curl->addPost(Test::TEST_URL, $data); + $post_3->setJsonDecoder(false); + $post_3->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_string($instance->response)); + }); + + $multi_curl->start(); + + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'json_response'); + $multi_curl->setJsonDecoder(function ($response) { + return 'foo'; + }); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('foo', $instance->response); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->beforeSend(function ($instance) { + $instance->setJsonDecoder(function ($response) { + return 'bar'; + }); + }); + $get_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('bar', $instance->response); + }); + + $get_3 = $multi_curl->addGet(Test::TEST_URL); + $get_3->beforeSend(function ($instance) { + $instance->setJsonDecoder(false); + }); + $get_3->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_string($instance->response)); + }); + + $multi_curl->start(); + $this->assertEquals('foo', $get_1->response); + $this->assertEquals('bar', $get_2->response); + + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'json_response'); + $multi_curl->setJsonDecoder(false); + + $get_4 = $multi_curl->addGet(Test::TEST_URL); + $get_4->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_string($instance->response)); + }); + + $multi_curl->start(); + } + + public function testXMLDecoder() + { + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'xml_with_cdata_response'); + $multi_curl->setXmlDecoder(function ($response) { + return 'first decoder'; + }); + + $post_1 = $multi_curl->addPost(Test::TEST_URL); + $post_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('first decoder', $instance->response); + }); + + $post_2 = $multi_curl->addPost(Test::TEST_URL); + $post_2->setXmlDecoder(function ($response) { + return 'second decoder'; + }); + $post_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('second decoder', $instance->response); + }); + + $multi_curl->start(); + $this->assertEquals('first decoder', $post_1->response); + $this->assertEquals('second decoder', $post_2->response); + + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'xml_with_cdata_response'); + + $post_1 = $multi_curl->addPost(Test::TEST_URL); + $post_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_object($instance->response)); + \PHPUnit\Framework\Assert::assertInstanceOf('SimpleXMLElement', $instance->response); + \PHPUnit\Framework\Assert::assertFalse(strpos($instance->response->saveXML(), 'addPost(Test::TEST_URL); + $post_2->setXmlDecoder(function ($response) { + return simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA); + }); + $post_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_object($instance->response)); + \PHPUnit\Framework\Assert::assertInstanceOf('SimpleXMLElement', $instance->response); + \PHPUnit\Framework\Assert::assertTrue(strpos($instance->response->saveXML(), 'addPost(Test::TEST_URL); + $post_3->setXmlDecoder(false); + $post_3->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_string($instance->response)); + }); + + $multi_curl->start(); + + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'xml_with_cdata_response'); + $multi_curl->setXmlDecoder(function ($response) { + return 'foo'; + }); + + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_1->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('foo', $instance->response); + }); + + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->beforeSend(function ($instance) { + $instance->setXmlDecoder(function ($response) { + return 'bar'; + }); + }); + $get_2->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('bar', $instance->response); + }); + + $get_3 = $multi_curl->addGet(Test::TEST_URL); + $get_3->beforeSend(function ($instance) { + $instance->setXmlDecoder(false); + }); + $get_3->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_string($instance->response)); + }); + + $multi_curl->start(); + $this->assertEquals('foo', $get_1->response); $this->assertEquals('bar', $get_2->response); + + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'xml_with_cdata_response'); + $multi_curl->setXmlDecoder(false); + + $get_4 = $multi_curl->addGet(Test::TEST_URL); + $get_4->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertTrue(is_string($instance->response)); + }); + + $multi_curl->start(); + } + + public function testDownloadToFile() + { + // Create and upload a file. + $upload_file_path = \Helper\get_png(); + $uploaded_file_path = \Helper\upload_file_to_server($upload_file_path); + + // Download the file. + $downloaded_file_path = tempnam('/tmp', 'php-curl-class.'); + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'download_response'); + $multi_curl->addDownload(Test::TEST_URL . '?' . http_build_query([ + 'file_path' => $uploaded_file_path, + ]), $downloaded_file_path); + $multi_curl->complete(function ($instance) use ($upload_file_path, $downloaded_file_path) { + \PHPUnit\Framework\Assert::assertEquals(md5_file($upload_file_path), $instance->responseHeaders['ETag']); + \PHPUnit\Framework\Assert::assertEquals( + $instance->downloadFileName, + $downloaded_file_path . '.pccdownload' + ); + }); + $multi_curl->start(); + $this->assertNotEquals($uploaded_file_path, $downloaded_file_path); + + $this->assertEquals(filesize($upload_file_path), filesize($downloaded_file_path)); + $this->assertEquals(md5_file($upload_file_path), md5_file($downloaded_file_path)); + + // Remove server file. + \Helper\remove_file_from_server($uploaded_file_path); + + unlink($upload_file_path); + unlink($downloaded_file_path); + $this->assertFalse(file_exists($upload_file_path)); + $this->assertFalse(file_exists($downloaded_file_path)); + } + + public function testDownloadCallback() + { + // Create and upload a file. + $upload_file_path = \Helper\get_png(); + $uploaded_file_path = \Helper\upload_file_to_server($upload_file_path); + + // Download the file. + $download_callback_called = false; + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'download_response'); + $multi_curl->addDownload(Test::TEST_URL . '?' . http_build_query([ + 'file_path' => $uploaded_file_path, + ]), function ($instance, $fh) use (&$download_callback_called) { + \PHPUnit\Framework\Assert::assertFalse($download_callback_called); + \PHPUnit\Framework\Assert::assertInstanceOf('Curl\Curl', $instance); + \PHPUnit\Framework\Assert::assertTrue(is_resource($fh)); + \PHPUnit\Framework\Assert::assertEquals('stream', get_resource_type($fh)); + \PHPUnit\Framework\Assert::assertGreaterThan(0, strlen(stream_get_contents($fh))); + \PHPUnit\Framework\Assert::assertEquals(0, strlen(stream_get_contents($fh))); + \PHPUnit\Framework\Assert::assertTrue(fclose($fh)); + $download_callback_called = true; + }); + $multi_curl->start(); + $this->assertTrue($download_callback_called); + + // Remove server file. + \Helper\remove_file_from_server($uploaded_file_path); + + unlink($upload_file_path); + $this->assertFalse(file_exists($upload_file_path)); + } + + public function testDownloadRange() + { + // Create and upload a file. + $filename = \Helper\get_png(); + $uploaded_file_path = \Helper\upload_file_to_server($filename); + + $filesize = filesize($filename); + + foreach ( + [ + false, + 0, + 1, + 2, + 3, + 5, + 10, + 25, + 50, + $filesize - 3, + $filesize - 2, + $filesize - 1, + + // A partial temporary file having the exact same file size as the complete source file should only + // occur under certain circumstances (almost never). When the download successfully completed, the + // temporary file should have been moved to the download destination save path. However, it is possible + // that a larger file download was interrupted after which the source file was updated and now has the + // exact same file size as the partial temporary. When resuming the download, the range is now + // unsatisfiable as the first byte position exceeds the available range. The entire file should be + // downloaded again. + $filesize - 0, + + // A partial temporary file having a larger file size than the complete source file should only occur + // under certain circumstances. This is possible when a download was interrupted after which the source + // file was updated with a smaller file. When resuming the download, the range is now unsatisfiable as + // the first byte position exceeds the the available range. The entire file should be downloaded again. + $filesize + 1, + $filesize + 2, + $filesize + 3, + + ] as $length + ) { + $source = Test::TEST_URL; + $destination = \Helper\get_tmp_file_path(); + + // Start with no file. + if ($length === false) { + $this->assertFalse(file_exists($destination)); + + // Start with $length bytes of file. + } else { + // Simulate resuming partially downloaded temporary file. + $partial_filename = $destination . '.pccdownload'; + + if ($length === 0) { + $partial_content = ''; + } else { + $file = fopen($filename, 'rb'); + $partial_content = fread($file, $length); + fclose($file); + } + + // Partial content size should be $length bytes large for testing resume download behavior. + if ($length <= $filesize) { + $this->assertEquals($length, strlen($partial_content)); + + // Partial content should not be larger than the original file size. + } else { + $this->assertEquals($filesize, strlen($partial_content)); + } + + file_put_contents($partial_filename, $partial_content); + $this->assertEquals(strlen($partial_content), strlen(file_get_contents($partial_filename))); + } + + // Download (the remaining bytes of) the file. + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'download_file_range'); + $multi_curl->addDownload($source . '?' . http_build_query([ + 'file_path' => $uploaded_file_path, + ]), $destination); + + clearstatcache(); + + $instance_error = false; + $multi_curl->complete(function ($instance) use ($filesize, $length, $destination, &$instance_error) { + $expected_bytes_downloaded = $filesize - min($length, $filesize); + $bytes_downloaded = $instance->responseHeaders['content-length']; + if ($length === false || $length === 0) { + $expected_http_status_code = 200; // 200 OK + \PHPUnit\Framework\Assert::assertEquals($expected_bytes_downloaded, $bytes_downloaded); + } elseif ($length >= $filesize) { + $expected_http_status_code = 416; // 416 Requested Range Not Satisfiable + } else { + $expected_http_status_code = 206; // 206 Partial Content + \PHPUnit\Framework\Assert::assertEquals($expected_bytes_downloaded, $bytes_downloaded); + } + \PHPUnit\Framework\Assert::assertEquals($expected_http_status_code, $instance->httpStatusCode); + $instance_error = $instance->error; + }); + $multi_curl->start(); + + if (!$instance_error) { + $this->assertEquals($filesize, filesize($destination)); + unlink($destination); + $this->assertFalse(file_exists($destination)); + } + } + + // Remove server file. + \Helper\remove_file_from_server($uploaded_file_path); + + unlink($filename); + $this->assertFalse(file_exists($filename)); + } + + public function testDownloadErrorDeleteTemporaryFile() + { + $destination = \Helper\get_tmp_file_path(); + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', '404'); + $multi_curl->addDownload(Test::TEST_URL, $destination); + $multi_curl->complete(function ($instance) use ($destination) { + \PHPUnit\Framework\Assert::assertFalse(file_exists($instance->downloadFileName)); + \PHPUnit\Framework\Assert::assertFalse(file_exists($destination)); + }); + $multi_curl->start(); + } + + public function testDownloadCallbackError() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $download_before_send_called = false; + $download_after_send_called = false; + $download_callback_called = false; + $multi_curl = new MultiCurl(); + $multi_curl->beforeSend(function ($instance) use (&$download_before_send_called) { + \PHPUnit\Framework\Assert::assertFalse($download_before_send_called); + $download_before_send_called = true; + }); + $multi_curl->afterSend(function ($instance) use (&$download_after_send_called) { + \PHPUnit\Framework\Assert::assertFalse($download_after_send_called); + $download_after_send_called = true; + }); + $multi_curl->addDownload(Test::ERROR_URL, function ($instance, $fh) use (&$download_callback_called) { + $download_callback_called = true; + }); + $multi_curl->start(); + $this->assertTrue($download_before_send_called); + $this->assertTrue($download_after_send_called); + $this->assertFalse($download_callback_called); + } + + public function testSetUrlInConstructor() + { + $data = ['key' => 'value']; + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'delete_with_body'); + $multi_curl->addDelete($data, ['wibble' => 'wubble'])->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + '{"get":{"key":"value"},"delete":{"wibble":"wubble"}}', + $instance->rawResponse + ); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'get'); + $multi_curl->addDelete($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('key=value', $instance->response); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'get'); + $multi_curl->addGet($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('key=value', $instance->response); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'get'); + $multi_curl->addHead($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'HEAD /?key=value HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'get'); + $multi_curl->addOptions($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('key=value', $instance->response); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'request_method'); + $multi_curl->addPatch($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('PATCH', $instance->response); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'post'); + $multi_curl->addPost($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('key=value', $instance->response); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'put'); + $multi_curl->addPut($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('key=value', $instance->response); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setHeader('X-DEBUG-TEST', 'search'); + $multi_curl->addSearch($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('key=value', $instance->response); + }); + $multi_curl->start(); + } + + public function testSetUrl() + { + $data = ['key' => 'value']; + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addDelete($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'DELETE /?key=value HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL . '?key=value', $instance->effectiveUrl); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addGet($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'GET /?key=value HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL . '?key=value', $instance->effectiveUrl); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addHead($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'HEAD /?key=value HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL . '?key=value', $instance->effectiveUrl); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addOptions($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'OPTIONS /?key=value HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL . '?key=value', $instance->effectiveUrl); + }); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addPatch($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'PATCH / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); + }); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addPost($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'POST / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); + }); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addPut($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'PUT / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); + }); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl(Test::TEST_URL); + $multi_curl->addSearch($data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'SEARCH / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); + }); + } + + public function testAddRequestAfterStart() + { + $multi_curl = new MultiCurl(); + + $urls = []; + $copy_of_urls = []; + for ($i = 0; $i < 10; $i++) { + $url = Test::TEST_URL . '?' . md5((string) mt_rand()); + $urls[] = $url; + $copy_of_urls[] = $url; + } + + $urls_called = []; + $multi_curl->complete(function ($instance) use (&$multi_curl, &$urls, &$urls_called) { + $urls_called[] = $instance->url; + $next_url = array_pop($urls); + if ($next_url !== null) { + $multi_curl->addGet($next_url); + } + }); + + $multi_curl->addGet(array_pop($urls)); + $multi_curl->start(); + + $this->assertNotEmpty($copy_of_urls); + $this->assertNotEmpty($urls_called); + $this->assertEquals(count($copy_of_urls), count($urls_called)); + + foreach ($copy_of_urls as $url) { + $this->assertTrue(in_array($url, $urls_called, true)); + } + } + + public function testClose() + { + $multi_curl = new MultiCurl(); + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->start(); + $this->assertTrue(is_object($multi_curl->multiCurl) || is_resource($multi_curl->multiCurl)); + $multi_curl->close(); + $this->assertNull($multi_curl->multiCurl); + } + + public function testCookieJarAfterClose() + { + $cookie_jar = tempnam('/tmp', 'php-curl-class.'); + + $multi_curl = new MultiCurl(); + $multi_curl->setCookieJar($cookie_jar); + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->start(); + $multi_curl->close(); + $cookies = file_get_contents($cookie_jar); + $this->assertNotEmpty($cookies); + } + + public function testMultiPostRedirectGet() + { + // Deny the post-redirect-get and make a POST following the redirection. + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setFollowLocation(true); + $multi_curl->setHeader('X-DEBUG-TEST', 'post_redirect_get'); + $multi_curl->addPost([], true)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('Redirected: POST', $instance->response); + }); + $multi_curl->start(); + + // Allow the post-redirect-get and make a GET following the redirection. + $multi_curl = new MultiCurl(/service/test::TEST_URL); + $multi_curl->setFollowLocation(true); + $multi_curl->setHeader('X-DEBUG-TEST', 'post_redirect_get'); + $multi_curl->addPost([], false)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('Redirected: GET', $instance->response); + }); + $multi_curl->start(); + } + + public function testAlternativeStandardErrorOutput() + { + + $buffer = fopen('php://memory', 'w+'); + + $multi_curl = new MultiCurl(); + $multi_curl->verbose(true, $buffer); + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->start(); + + rewind($buffer); + $stderr = stream_get_contents($buffer); + fclose($buffer); + + $this->assertNotEmpty($stderr); + } + + public function testUnsetHeader() + { + $request_key = 'X-Request-Id'; + $request_value = '1'; + $data = [ + 'test' => 'server', + 'key' => 'HTTP_X_REQUEST_ID', + ]; + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader($request_key, $request_value); + $multi_curl->addGet(Test::TEST_URL, $data)->complete(function ($instance) use ($request_value) { + \PHPUnit\Framework\Assert::assertEquals($request_value, $instance->response); + }); + $multi_curl->start(); + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader($request_key, $request_value); + $multi_curl->unsetHeader($request_key); + $multi_curl->addGet(Test::TEST_URL, $data)->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals('', $instance->response); + }); + $multi_curl->start(); + } + + public function testAddCurl() + { + $curl = new Curl(); + $curl->setUrl(Test::TEST_URL); + $curl->setOpt(CURLOPT_CUSTOMREQUEST, 'GET'); + $curl->setOpt(CURLOPT_HTTPGET, true); + + $complete_called = false; + $multi_curl = new MultiCurl(); + $multi_curl->addCurl(http://github.com/$curl)->complete(function ($instance) use (&$complete_called) { + $complete_called = true; + }); + $multi_curl->start(); + $this->assertTrue($complete_called); + } + + public function testAddCurlHeaders() + { + $header_fields = [ + 'X-SOME-HEADER: some-value', + 'X-ANOTHER-HEADER: another-value', + ]; + + $curl = new Curl(); + $curl->setUrl(Test::TEST_URL); + $curl->setOpt(CURLOPT_HTTPHEADER, $header_fields); + + $this->assertEquals($header_fields, $curl->getOpt(CURLOPT_HTTPHEADER)); + + $multi_curl = new MultiCurl(); + $multi_curl->addCurl(http://github.com/$curl); + + $this->assertEquals($header_fields, $curl->getOpt(CURLOPT_HTTPHEADER)); + } + + public function testSequentialId() + { + $multi_curl = new MultiCurl(); + $multi_curl->complete(function ($instance) { + $sequential_id = $instance->getOpt(CURLOPT_POSTFIELDS); + \PHPUnit\Framework\Assert::assertEquals($sequential_id, $instance->id); + }); + + for ($i = 0; $i < 100; $i++) { + $multi_curl->addPost(Test::TEST_URL, $i); + } + + $multi_curl->start(); + } + + public function testAscendingNumericalOrder() + { + $counter = 0; + $multi_curl = new MultiCurl(); + $multi_curl->setConcurrency(1); + $multi_curl->complete(function ($instance) use (&$counter) { + $sequential_id = $instance->getOpt(CURLOPT_POSTFIELDS); + \PHPUnit\Framework\Assert::assertEquals($counter, $sequential_id); + $counter++; + }); + + for ($i = 0; $i < 100; $i++) { + $multi_curl->addPost(Test::TEST_URL, $i); + } + + $multi_curl->start(); + } + + public function testRetry() + { + $tests = [ + [ + 'maximum_number_of_retries' => null, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 1, + 'expect_success' => false, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 1, + 'expect_success' => true, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 2, + 'expect_success' => false, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 2, + 'failures' => 2, + 'expect_success' => true, + 'expect_attempts' => 3, + 'expect_retries' => 2, + ], + [ + 'maximum_number_of_retries' => 3, + 'failures' => 3, + 'expect_success' => true, + 'expect_attempts' => 4, + 'expect_retries' => 3, + ], + ]; + foreach ($tests as $test) { + $maximum_number_of_retries = $test['maximum_number_of_retries']; + $failures = $test['failures']; + $expect_success = $test['expect_success']; + $expect_attempts = $test['expect_attempts']; + $expect_retries = $test['expect_retries']; + + $multi_curl = new MultiCurl(); + $multi_curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $multi_curl->setHeader('X-DEBUG-TEST', 'retry'); + + if ($maximum_number_of_retries !== null) { + $multi_curl->setRetry($maximum_number_of_retries); + } + + $instance = $multi_curl->addGet(Test::TEST_URL, ['failures' => $failures]); + $multi_curl->start(); + + $this->assertEquals($expect_success, !$instance->error); + $this->assertEquals($expect_attempts, $instance->attempts); + $this->assertEquals($expect_retries, $instance->retries); + } + } + + public function testRetryCallable() + { + $tests = [ + [ + 'maximum_number_of_retries' => null, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 0, + 'expect_success' => true, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 0, + 'failures' => 1, + 'expect_success' => false, + 'expect_attempts' => 1, + 'expect_retries' => 0, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 1, + 'expect_success' => true, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 1, + 'failures' => 2, + 'expect_success' => false, + 'expect_attempts' => 2, + 'expect_retries' => 1, + ], + [ + 'maximum_number_of_retries' => 2, + 'failures' => 2, + 'expect_success' => true, + 'expect_attempts' => 3, + 'expect_retries' => 2, + ], + [ + 'maximum_number_of_retries' => 3, + 'failures' => 3, + 'expect_success' => true, + 'expect_attempts' => 4, + 'expect_retries' => 3, + ], + ]; + foreach ($tests as $test) { + $maximum_number_of_retries = $test['maximum_number_of_retries']; + $failures = $test['failures']; + $expect_success = $test['expect_success']; + $expect_attempts = $test['expect_attempts']; + $expect_retries = $test['expect_retries']; + + $multi_curl = new MultiCurl(); + $multi_curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $multi_curl->setHeader('X-DEBUG-TEST', 'retry'); + + if ($maximum_number_of_retries !== null) { + $multi_curl->setRetry(function ($instance) use ($maximum_number_of_retries) { + $return = $instance->retries < $maximum_number_of_retries; + return $return; + }); + } + + $instance = $multi_curl->addGet(Test::TEST_URL, ['failures' => $failures]); + $multi_curl->start(); + + $this->assertEquals($expect_success, !$instance->error); + $this->assertEquals($expect_attempts, $instance->attempts); + $this->assertEquals($expect_retries, $instance->retries); + } + } + + public function testRelativeUrl() + { + $multi_curl = new MultiCurl(/service/test::TEST_URL . 'path/'); + $this->assertEquals('http://127.0.0.1:8000/path/', (string)$multi_curl->baseUrl); + + $get_1 = $multi_curl->addGet('test', [ + 'a' => '1', + 'b' => '2', + ]); + $this->assertEquals('http://127.0.0.1:8000/path/test?a=1&b=2', (string)$get_1->url); + + $get_2 = $multi_curl->addGet('/root', [ + 'c' => '3', + 'd' => '4', + ]); + $this->assertEquals('http://127.0.0.1:8000/root?c=3&d=4', (string)$get_2->url); + + $tests = [ + [ + 'args' => [ + 'http://www.example.com/', + '/foo', + ], + 'expected' => 'http://www.example.com/foo', + ], + [ + 'args' => [ + 'http://www.example.com/', + '/foo/', + ], + 'expected' => 'http://www.example.com/foo/', + ], + [ + 'args' => [ + 'http://www.example.com/', + '/dir/page.html', + ], + 'expected' => 'http://www.example.com/dir/page.html', + ], + [ + 'args' => [ + 'http://www.example.com/dir1/page2.html', + '/dir/page.html', + ], + 'expected' => 'http://www.example.com/dir/page.html', + ], + [ + 'args' => [ + 'http://www.example.com/dir1/page2.html', + 'dir/page.html', + ], + 'expected' => 'http://www.example.com/dir1/dir/page.html', + ], + [ + 'args' => [ + 'http://www.example.com/dir1/dir3/page.html', + '../dir/page.html', + ], + 'expected' => 'http://www.example.com/dir1/dir/page.html', + ], + ]; + foreach ($tests as $test) { + $multi_curl = new MultiCurl(http://github.com/$test['args']['0']); + $multi_curl->setUrl($test['args']['1']); + $this->assertEquals( + $test['expected'], + $multi_curl->getOpt(CURLOPT_URL), + "Joint URLs: '{$test['args']['0']}', '{$test['args']['1']}'" + ); + + $multi_curl = new MultiCurl(http://github.com/$test['args']['0']); + $multi_curl->setUrl($test['args']['1'], ['a' => '1', 'b' => '2']); + $this->assertEquals( + $test['expected'] . '?a=1&b=2', + $multi_curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' with parameters a=1, b=2" + ); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl($test['args']['0']); + $multi_curl->setUrl($test['args']['1']); + $this->assertEquals( + $test['expected'], + $multi_curl->getOpt(CURLOPT_URL), + "Joint URLs: '{$test['args']['0']}', '{$test['args']['1']}'" + ); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl($test['args']['0'], ['a' => '1', 'b' => '2']); + $multi_curl->setUrl($test['args']['1']); + $this->assertEquals( + $test['expected'], + $multi_curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' with parameters a=1, b=2 and URL '{$test['args']['1']}'" + ); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl($test['args']['0']); + $multi_curl->setUrl($test['args']['1'], ['a' => '1', 'b' => '2']); + $this->assertEquals( + $test['expected'] . '?a=1&b=2', + $multi_curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' and URL '{$test['args']['1']}' with parameters a=1, b=2" + ); + + $multi_curl = new MultiCurl(); + $multi_curl->setUrl($test['args']['0'], ['a' => '1', 'b' => '2']); + $multi_curl->setUrl($test['args']['1'], ['c' => '3', 'd' => '4']); + $this->assertEquals( + $test['expected'] . '?c=3&d=4', + $multi_curl->getOpt(CURLOPT_URL), + "Joint URL '{$test['args']['0']}' with parameters a=1, b=2 " . + "and URL '{$test['args']['1']}' with parameters c=3, d=4" + ); + } + } + + public function testPostDataEmptyJson() + { + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'post_json'); + $multi_curl->setHeader('Content-Type', 'application/json'); + $multi_curl->addPost(Test::TEST_URL); + $post_complete_called = false; + $multi_curl->complete(function ($instance) use (&$post_complete_called) { + \PHPUnit\Framework\Assert::assertEquals('', $instance->response); + \PHPUnit\Framework\Assert::assertEquals('', $instance->getOpt(CURLOPT_POSTFIELDS)); + $post_complete_called = true; + }); + $multi_curl->start(); + $this->assertTrue($post_complete_called); + } + + public function testProxySettings() + { + $multi_curl = new MultiCurl(); + $multi_curl->setProxy('proxy.example.com', '1080', 'username', 'password'); + + $this->assertEquals('proxy.example.com', $multi_curl->getOpt(CURLOPT_PROXY)); + $this->assertEquals('1080', $multi_curl->getOpt(CURLOPT_PROXYPORT)); + $this->assertEquals('username:password', $multi_curl->getOpt(CURLOPT_PROXYUSERPWD)); + + $multi_curl->unsetProxy(); + $this->assertNull($multi_curl->getOpt(CURLOPT_PROXY)); + } + + public function testSetProxyAuth() + { + $auth = CURLAUTH_BASIC; + + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_PROXYAUTH)); + $multi_curl->setProxyAuth($auth); + $this->assertEquals($auth, $multi_curl->getOpt(CURLOPT_PROXYAUTH)); + } + + public function testSetProxyType() + { + $type = CURLPROXY_SOCKS5; + + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_PROXYTYPE)); + $multi_curl->setProxyType($type); + $this->assertEquals($type, $multi_curl->getOpt(CURLOPT_PROXYTYPE)); + } + + public function testSetProxyTunnel() + { + $tunnel = true; + + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_HTTPPROXYTUNNEL)); + $multi_curl->setProxyTunnel($tunnel); + $this->assertEquals($tunnel, $multi_curl->getOpt(CURLOPT_HTTPPROXYTUNNEL)); + } + + public function testSetProxiesRandomProxy() + { + $proxies = [ + 'example.com:80', + 'example.com:443', + 'example.com:1080', + 'example.com:3128', + 'example.com:8080', + ]; + + $multi_curl = new MultiCurl(); + $multi_curl->setProxies($proxies); + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_3 = $multi_curl->addGet(Test::TEST_URL); + + // Make MultiCurl::queuedCurls accessible and MultiCurl::initHandle() + // callable. + $reflector = new \ReflectionClass('\Curl\MultiCurl'); + $property = $reflector->getProperty('queuedCurls'); + $property->setAccessible(true); + $multi_curl_curls = $property->getValue($multi_curl); + $multi_curl_initHandle = $reflector->getMethod('initHandle'); + $multi_curl_initHandle->setAccessible(true); + + // Ensure we have the requests queued. + $this->assertCount(3, $multi_curl_curls); + + // Invoke MultiCurl::initHandle() so that proxies are set. + foreach ($multi_curl_curls as $curl) { + $multi_curl_initHandle->invoke($multi_curl, $curl); + } + + // Ensure requests are set to one of the random proxies. + $this->assertContains($get_1->getOpt(CURLOPT_PROXY), $proxies); + $this->assertContains($get_2->getOpt(CURLOPT_PROXY), $proxies); + $this->assertContains($get_3->getOpt(CURLOPT_PROXY), $proxies); + } + + public function testSetProxiesAlreadySet() + { + $proxies = [ + 'example.com:80', + 'example.com:443', + 'example.com:1080', + 'example.com:3128', + 'example.com:8080', + ]; + + $multi_curl = new MultiCurl(); + $multi_curl->setProxies($proxies); + $get_1 = $multi_curl->addGet(Test::TEST_URL); + $get_2 = $multi_curl->addGet(Test::TEST_URL); + $get_2->setProxy('example.com:9999'); + $get_3 = $multi_curl->addGet(Test::TEST_URL); + + // Make MultiCurl::queuedCurls accessible and MultiCurl::initHandle() + // callable. + $reflector = new \ReflectionClass('\Curl\MultiCurl'); + $property = $reflector->getProperty('queuedCurls'); + $property->setAccessible(true); + $multi_curl_curls = $property->getValue($multi_curl); + $multi_curl_initHandle = $reflector->getMethod('initHandle'); + $multi_curl_initHandle->setAccessible(true); + + // Ensure we have the requests queued. + $this->assertCount(3, $multi_curl_curls); + + // Invoke MultiCurl::initHandle() so that proxies are set. + foreach ($multi_curl_curls as $curl) { + $multi_curl_initHandle->invoke($multi_curl, $curl); + } + + // Ensure requests are set to one of the random proxies. + $this->assertContains($get_1->getOpt(CURLOPT_PROXY), $proxies); + $this->assertContains($get_3->getOpt(CURLOPT_PROXY), $proxies); + + // Ensure request with specific proxy is not set to one of the random proxies. + $this->assertNotContains($get_2->getOpt(CURLOPT_PROXY), $proxies); + } + + public function testSetFile() + { + $file = STDOUT; + + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_FILE)); + $multi_curl->setFile($file); + $this->assertEquals($file, $multi_curl->getOpt(CURLOPT_FILE)); + } + + public function testSetRange() + { + $range = '1000-'; + + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_RANGE)); + $multi_curl->setRange($range); + $this->assertEquals($range, $multi_curl->getOpt(CURLOPT_RANGE)); + } + + public function testDisableTimeout() + { + $multi_curl = new MultiCurl(); + $multi_curl->disableTimeout(); + $get = $multi_curl->addGet(Test::TEST_URL); + $get->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertNull($instance->getOpt(CURLOPT_TIMEOUT)); + }); + $multi_curl->start(); + } + + public function testSetRateLimitUnits() + { + foreach ( + [ + [ + 'rate_limit' => '1/s', + 'expected' => [ + 'rate_limit' => '1/1s', + 'max_requests' => '1', + 'interval' => '1', + 'unit' => 's', + 'interval_seconds' => '1', + ], + ], + [ + 'rate_limit' => '1/1s', + 'expected' => [ + 'rate_limit' => '1/1s', + 'max_requests' => '1', + 'interval' => '1', + 'unit' => 's', + 'interval_seconds' => '1', + ], + ], + [ + 'rate_limit' => '10/60s', + 'expected' => [ + 'rate_limit' => '10/60s', + 'max_requests' => '10', + 'interval' => '60', + 'unit' => 's', + 'interval_seconds' => '60', + ], + ], + [ + 'rate_limit' => '10/60S', + 'expected' => [ + 'rate_limit' => '10/60s', + 'max_requests' => '10', + 'interval' => '60', + 'unit' => 's', + 'interval_seconds' => '60', + ], + ], + [ + 'rate_limit' => '1/m', + 'expected' => [ + 'rate_limit' => '1/1m', + 'max_requests' => '1', + 'interval' => '1', + 'unit' => 'm', + 'interval_seconds' => '60', + ], + ], + [ + 'rate_limit' => '1/1m', + 'expected' => [ + 'rate_limit' => '1/1m', + 'max_requests' => '1', + 'interval' => '1', + 'unit' => 'm', + 'interval_seconds' => '60', + ], + ], + [ + 'rate_limit' => '5000/60m', + 'expected' => [ + 'rate_limit' => '5000/60m', + 'max_requests' => '5000', + 'interval' => '60', + 'unit' => 'm', + 'interval_seconds' => '3600', + ], + ], + [ + 'rate_limit' => '5000/60M', + 'expected' => [ + 'rate_limit' => '5000/60m', + 'max_requests' => '5000', + 'interval' => '60', + 'unit' => 'm', + 'interval_seconds' => '3600', + ], + ], + [ + 'rate_limit' => '1/h', + 'expected' => [ + 'rate_limit' => '1/1h', + 'max_requests' => '1', + 'interval' => '1', + 'unit' => 'h', + 'interval_seconds' => '3600', + ], + ], + [ + 'rate_limit' => '1/1h', + 'expected' => [ + 'rate_limit' => '1/1h', + 'max_requests' => '1', + 'interval' => '1', + 'unit' => 'h', + 'interval_seconds' => '3600', + ], + ], + [ + 'rate_limit' => '5000/1h', + 'expected' => [ + 'rate_limit' => '5000/1h', + 'max_requests' => '5000', + 'interval' => '1', + 'unit' => 'h', + 'interval_seconds' => '3600', + ], + ], + [ + 'rate_limit' => '100000/24h', + 'expected' => [ + 'rate_limit' => '100000/24h', + 'max_requests' => '100000', + 'interval' => '24', + 'unit' => 'h', + 'interval_seconds' => '86400', + ], + ], + [ + 'rate_limit' => '100000/24H', + 'expected' => [ + 'rate_limit' => '100000/24h', + 'max_requests' => '100000', + 'interval' => '24', + 'unit' => 'h', + 'interval_seconds' => '86400', + ], + ], + ] as $test + ) { + $multi_curl = new MultiCurl(); + $multi_curl->setRateLimit($test['rate_limit']); + + $this->assertEquals( + $test['expected']['rate_limit'], + \Helper\get_multi_curl_property_value($multi_curl, 'rateLimit') + ); + $this->assertEquals( + $test['expected']['max_requests'], + \Helper\get_multi_curl_property_value($multi_curl, 'maxRequestsPerInterval') + ); + $this->assertEquals( + $test['expected']['interval'], + \Helper\get_multi_curl_property_value($multi_curl, 'interval') + ); + $this->assertEquals( + $test['expected']['unit'], + \Helper\get_multi_curl_property_value($multi_curl, 'unit') + ); + $this->assertEquals( + $test['expected']['interval_seconds'], + \Helper\get_multi_curl_property_value($multi_curl, 'intervalSeconds') + ); + } + } + + public function testSetRateLimitPerSecond1() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0--| + // R1--| + // W---------------| + // R2--| + // R3--| + // W---------------| + // R4--| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=1'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=1'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=1'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=1'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=1'); + + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 1. + $this->assertGreaterThanOrEqual(0.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(1.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 1. + $this->assertGreaterThanOrEqual(0.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(1.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 6. + $this->assertGreaterThanOrEqual(5.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(6.5, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 6. + $this->assertGreaterThanOrEqual(5.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(6.5, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(11.5 + 1, $request_stats['4']['relative_stop']); + } + + public function testSetRateLimitPerSecond2() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0--| + // R1------| + // W-----------| + // R2--| + // R3------| + // W-----------| + // R4--| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=1'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=1'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=1'); + + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 1. + $this->assertGreaterThanOrEqual(0.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(1.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 2. + $this->assertGreaterThanOrEqual(1.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(2.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 6. + $this->assertGreaterThanOrEqual(5.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(6.5, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(7.5, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(11.5 + 1, $request_stats['4']['relative_stop']); + } + + public function testSetRateLimitPerSecond3() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0------| + // R1------------------| + // R2------| + // R3------------------| + // R4--| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=5'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=5'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=1'); + + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 2. + $this->assertGreaterThanOrEqual(1.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(2.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 5. + $this->assertGreaterThanOrEqual(4.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(5.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(7.5, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 10. + $this->assertGreaterThanOrEqual(9.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(10.5, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(11.5, $request_stats['4']['relative_stop']); + } + + public function testSetRateLimitPerSecond4() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0------| + // R1----------------------------------| + // R2----------| + // R3----------------------| + // R4--| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=9'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=3'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=6'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=1'); + + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start'], $request_stats['message']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start'], $request_stats['message']); + // Assert R0 ends around 2. + $this->assertGreaterThanOrEqual(1.8, $request_stats['0']['relative_stop'], $request_stats['message']); + $this->assertLessThanOrEqual(2.5, $request_stats['0']['relative_stop'], $request_stats['message']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start'], $request_stats['message']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start'], $request_stats['message']); + // Assert R1 ends around 9. + $this->assertGreaterThanOrEqual(8.8, $request_stats['1']['relative_stop'], $request_stats['message']); + $this->assertLessThanOrEqual(9.5, $request_stats['1']['relative_stop'], $request_stats['message']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start'], $request_stats['message']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start'], $request_stats['message']); + // Assert R2 ends around 8. + $this->assertGreaterThanOrEqual(7.8, $request_stats['2']['relative_stop'], $request_stats['message']); + $this->assertLessThanOrEqual(8.5 + 1, $request_stats['2']['relative_stop'], $request_stats['message']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start'], $request_stats['message']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start'], $request_stats['message']); + // Assert R3 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['3']['relative_stop'], $request_stats['message']); + $this->assertLessThanOrEqual(11.5 + 1, $request_stats['3']['relative_stop'], $request_stats['message']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start'], $request_stats['message']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start'], $request_stats['message']); + // Assert R4 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['4']['relative_stop'], $request_stats['message']); + $this->assertLessThanOrEqual(11.5 + 1, $request_stats['4']['relative_stop'], $request_stats['message']); + } + + public function testSetRateLimitPerSecond5() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0--------------------------| + // R1--------------------------| + // R2------| + // R3------| + // W-----------| + // R4------| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=7'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=7'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=2'); + + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(7.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(7.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(7.5 + 1, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(7.5 + 1, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['4']['relative_stop']); + } + + public function testSetRateLimitPerSecond6() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0--------------------------| + // R1--------------------------| + // R2--------------| + // R3------| + // W---| + // R4------| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=7'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=7'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=4'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=2'); + + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(7.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(7.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 9. + $this->assertGreaterThanOrEqual(8.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(9.5 + 1, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 7. + $this->assertGreaterThanOrEqual(6.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(7.5 + 1, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['4']['relative_stop']); + } + + public function testSetRateLimitPerSecond7() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0------| + // R1----------------------------------------------| + // R2----------------------| + // R3----------------------| + // R4------| + // R5------| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=12'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=6'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=6'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8006') . '?seconds=2'); + + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 2. + $this->assertGreaterThanOrEqual(1.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(2.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(12.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(11.5 + 1, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(11.5 + 1, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['4']['relative_stop']); + + // Assert R5 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['5']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['5']['relative_start']); + // Assert R5 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['5']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['5']['relative_stop']); } - public function testDownloadCallback() + public function testSetRateLimitPerSecond8() { - // Upload a file. - $upload_file_path = Helper\get_png(); - $upload_test = new Test(); - $upload_test->server('upload_response', 'POST', array( - 'image' => '@' . $upload_file_path, - )); - $uploaded_file_path = $upload_test->curl->response->file_path; + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0------------------------------| + // R1----------------------------------------------| + // R2--------------------------| + // R3--------------| + // R4--| + // R5------| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; - // Download the file. - $download_callback_called = false; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'download_response'); - $multi_curl->addDownload(Test::TEST_URL . '?' . http_build_query(array( - 'file_path' => $uploaded_file_path, - )), function($instance, $fh) use (&$download_callback_called) { - PHPUnit_Framework_Assert::assertFalse($download_callback_called); - PHPUnit_Framework_Assert::assertInstanceOf('Curl\Curl', $instance); - PHPUnit_Framework_Assert::assertTrue(is_resource($fh)); - PHPUnit_Framework_Assert::assertEquals('stream', get_resource_type($fh)); - PHPUnit_Framework_Assert::assertGreaterThan(0, strlen(stream_get_contents($fh))); - PHPUnit_Framework_Assert::assertEquals(0, strlen(stream_get_contents($fh))); - PHPUnit_Framework_Assert::assertTrue(fclose($fh)); - $download_callback_called = true; + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); }); - $multi_curl->start(); - $this->assertTrue($download_callback_called); - // Remove server file. - $this->assertEquals('true', $upload_test->server('upload_cleanup', 'POST', array( - 'file_path' => $uploaded_file_path, - ))); + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=8'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=12'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=7'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=4'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=1'); + $multi_curl->addGet(Test::getTestUrl('8006') . '?seconds=2'); - unlink($upload_file_path); - $this->assertFalse(file_exists($upload_file_path)); - $this->assertFalse(file_exists($uploaded_file_path)); + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 8. + $this->assertGreaterThanOrEqual(7.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(8.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(12.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 9. + $this->assertGreaterThanOrEqual(8.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(9.5 + 1, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 11. + $this->assertGreaterThanOrEqual(10.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(11.5, $request_stats['4']['relative_stop']); + + // Assert R5 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['5']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['5']['relative_start']); + // Assert R5 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['5']['relative_stop']); + $this->assertLessThanOrEqual(12.5, $request_stats['5']['relative_stop']); } - public function testDownloadCallbackError() + public function testSetRateLimitPerSecond9() { - $download_before_send_called = false; - $download_callback_called = false; + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + // R0----------------------------------------------| + // R1----------------------------------------------| + // R2--------------------------| + // R3--------------------------| + // R4------| + // R5------| + // 0 1 2 3 4 5 6 7 8 9 10 11 12 + + $request_stats = []; + $multi_curl = new MultiCurl(); - $multi_curl->beforeSend(function ($instance) use (&$download_before_send_called) { - PHPUnit_Framework_Assert::assertFalse($download_before_send_called); - $download_before_send_called = true; + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('2/5s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); }); - $multi_curl->addDownload(Test::ERROR_URL, function($instance, $fh) use (&$download_callback_called) { - $download_callback_called = true; + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=12'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=12'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=7'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=7'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=2'); + $multi_curl->addGet(Test::getTestUrl('8006') . '?seconds=2'); + $multi_curl->start(); - $this->assertTrue($download_before_send_called); - $this->assertFalse($download_callback_called); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R0 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['0']['relative_stop']); + $this->assertLessThanOrEqual(12.5, $request_stats['0']['relative_stop']); + + // Assert R1 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['1']['relative_start']); + // Assert R1 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['1']['relative_stop']); + $this->assertLessThanOrEqual(12.5, $request_stats['1']['relative_stop']); + + // Assert R2 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['2']['relative_start']); + // Assert R2 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['2']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['2']['relative_stop']); + + // Assert R3 starts around 5 and not before. + $this->assertGreaterThanOrEqual(5, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(5.5, $request_stats['3']['relative_start']); + // Assert R3 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['3']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['3']['relative_stop']); + + // Assert R4 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['4']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['4']['relative_start']); + // Assert R4 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['4']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['4']['relative_stop']); + + // Assert R5 starts around 10 and not before. + $this->assertGreaterThanOrEqual(10, $request_stats['5']['relative_start']); + $this->assertLessThanOrEqual(10.5, $request_stats['5']['relative_start']); + // Assert R5 ends around 12. + $this->assertGreaterThanOrEqual(11.8, $request_stats['5']['relative_stop']); + $this->assertLessThanOrEqual(12.5 + 1, $request_stats['5']['relative_stop']); } - public function testSetUrlInConstructor() + public function testSetRateLimitPerSecondOnePerSecond() { - $data = array('key' => 'value'); + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } - $multi_curl = new MultiCurl(/service/test::TEST_URL); - $multi_curl->setHeader('X-DEBUG-TEST', 'delete_with_body'); - $multi_curl->addDelete($data, array('wibble' => 'wubble'))->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('{"get":{"key":"value"},"delete":{"wibble":"wubble"}}', - $instance->rawResponse); + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setRateLimit('1/1s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8001')); + $multi_curl->addGet(Test::getTestUrl('8002')); + $multi_curl->addGet(Test::getTestUrl('8003')); + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); - $multi_curl = new MultiCurl(/service/test::TEST_URL); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->addDelete($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + + // Assert R1 starts around 1 and not before. + $this->assertGreaterThanOrEqual(1, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(1.5, $request_stats['1']['relative_start']); + + // Assert R2 starts around 2 and not before. + $this->assertGreaterThanOrEqual(2, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(2.5, $request_stats['2']['relative_start']); + } + + public function testSetRateLimitFivePerThirtySecond() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('5/30s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=15'); + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=25'); + $multi_curl->addGet(Test::getTestUrl('8006') . '?seconds=35'); + $multi_curl->addGet(Test::getTestUrl('8005') . '?seconds=45'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=20'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=10'); + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); - $multi_curl = new MultiCurl(/service/test::TEST_URL); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->addGet($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + // Assert R5 starts around 30 and not before. + $this->assertGreaterThanOrEqual(30, $request_stats['5']['relative_start']); + $this->assertLessThanOrEqual(30.5, $request_stats['5']['relative_start']); + } + + public function testSetRateLimitOnePerOneMinute() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('1/1m'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); + }); + + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=30'); + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=70'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=10'); + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // Assert R1 starts around 60 and not before. + $this->assertGreaterThanOrEqual(60, $request_stats['1']['relative_start']); + $this->assertLessThanOrEqual(60.5, $request_stats['1']['relative_start']); + // Assert R2 starts around 120 and not before. + $this->assertGreaterThanOrEqual(120, $request_stats['2']['relative_start']); + $this->assertLessThanOrEqual(120.5 + 1, $request_stats['2']['relative_start']); + } - $multi_curl = new MultiCurl(/service/test::TEST_URL); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->addHead($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals( - 'HEAD /?key=value HTTP/1.1', $instance->requestHeaders['Request-Line']); + public function testSetRateLimitThreePerOneMinute() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('3/1m'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); }); + + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=20'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=65'); + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=45'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=10'); + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); - $multi_curl = new MultiCurl(/service/test::TEST_URL); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->addOptions($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + // Assert R3 starts around 60 and not before. + $this->assertGreaterThanOrEqual(60, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(60.5, $request_stats['3']['relative_start']); + } + + public function testSetRateLimitThreePerSixtyFiveSeconds() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('3/65s'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); }); + + $multi_curl->addGet(Test::getTestUrl('8001') . '?seconds=5'); + $multi_curl->addGet(Test::getTestUrl('8002') . '?seconds=5'); + $multi_curl->addGet(Test::getTestUrl('8003') . '?seconds=5'); + $multi_curl->addGet(Test::getTestUrl('8004') . '?seconds=5'); + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); - $multi_curl = new MultiCurl(/service/test::TEST_URL); - $multi_curl->setHeader('X-DEBUG-TEST', 'request_method'); - $multi_curl->addPatch($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('PATCH', $instance->response); + // Assert R3 starts around 65 and not before. + $this->assertGreaterThanOrEqual(65, $request_stats['3']['relative_start']); + $this->assertLessThanOrEqual(65.5, $request_stats['3']['relative_start']); + } + + public function testSetRateLimitTenPerTwoMinutes() + { + if ($this->skip_slow_tests) { + $this->markTestSkipped(); + } + + $request_stats = []; + + $multi_curl = new MultiCurl(); + $multi_curl->setRequestTimeAccuracy(); + $multi_curl->setHeader('X-DEBUG-TEST', 'timeout'); + $multi_curl->setRateLimit('10/2m'); + $multi_curl->beforeSend(function ($instance) use (&$request_stats) { + $request_stats[$instance->id] = []; + $request_stats[$instance->id]['start'] = microtime(true); + }); + $multi_curl->complete(function ($instance) use (&$request_stats) { + $request_stats[$instance->id]['stop'] = microtime(true); }); + + for ($i = 0; $i <= 30; $i++) { + $multi_curl->addGet(Test::TEST_URL . '?seconds=1'); + } + $multi_curl->start(); + $request_stats = \Helper\get_request_stats($request_stats, $multi_curl); + + // 0-9 starts >= 0. + // 10-19 starts >= 120. + // 20-29 starts >= 240. + // 30-39 starts >= 360. + + // Assert R0 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['0']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['0']['relative_start']); + // Assert R9 starts around 0 and not before. + $this->assertGreaterThanOrEqual(0, $request_stats['9']['relative_start']); + $this->assertLessThanOrEqual(0.5, $request_stats['9']['relative_start']); + + // Assert R10 starts around 120 and not before. + $this->assertGreaterThanOrEqual(120, $request_stats['10']['relative_start']); + $this->assertLessThanOrEqual(120.5, $request_stats['10']['relative_start']); + // Assert R19 starts around 120 and not before. + $this->assertGreaterThanOrEqual(120, $request_stats['19']['relative_start']); + $this->assertLessThanOrEqual(120.5, $request_stats['19']['relative_start']); + + // Assert R20 starts around 240. Allow for some drift. + $this->assertGreaterThanOrEqual(239, $request_stats['20']['relative_start']); + $this->assertLessThanOrEqual(241, $request_stats['20']['relative_start']); + // Assert R29 starts around 240. Allow for some drift. + $this->assertGreaterThanOrEqual(239, $request_stats['29']['relative_start']); + $this->assertLessThanOrEqual(241, $request_stats['29']['relative_start']); + + // Assert R30 starts around 360. Allow for some drift. + $this->assertGreaterThanOrEqual(359, $request_stats['30']['relative_start']); + $this->assertLessThanOrEqual(361, $request_stats['30']['relative_start']); + } - $multi_curl = new MultiCurl(/service/test::TEST_URL); + public function testSetHeadersAssociativeArray() + { + $multi_curl = new MultiCurl(); + $multi_curl->setHeaders([ + ' Key1 ' => ' Value1 ', + ' Key2 ' => ' Value2', + ' Key3 ' => 'Value3 ', + ' Key4 ' => 'Value4', + ' Key5' => ' Value5 ', + ' Key6' => ' Value6', + ' Key7' => 'Value7 ', + ' Key8' => 'Value8', + 'Key9 ' => ' Value9 ', + 'Key10 ' => ' Value10', + 'Key11 ' => 'Value11 ', + 'Key12 ' => 'Value12', + 'Key13' => ' Value13 ', + 'Key14' => ' Value14', + 'Key15' => 'Value15 ', + 'Key16' => 'Value16', + ]); + $multi_curl->addGet(Test::TEST_URL); + + $curls = \Helper\get_multi_curl_property_value($multi_curl, 'queuedCurls'); + foreach ($curls as $curl) { + $this->assertEquals([ + 'Key1: Value1', + 'Key2: Value2', + 'Key3: Value3', + 'Key4: Value4', + 'Key5: Value5', + 'Key6: Value6', + 'Key7: Value7', + 'Key8: Value8', + 'Key9: Value9', + 'Key10: Value10', + 'Key11: Value11', + 'Key12: Value12', + 'Key13: Value13', + 'Key14: Value14', + 'Key15: Value15', + 'Key16: Value16', + ], $curl->getOpt(CURLOPT_HTTPHEADER)); + + $headers = \Helper\get_curl_property_value($curl, 'headers'); + $this->assertEquals('Value1', $headers['Key1']); + $this->assertEquals('Value2', $headers['Key2']); + $this->assertEquals('Value3', $headers['Key3']); + $this->assertEquals('Value4', $headers['Key4']); + $this->assertEquals('Value5', $headers['Key5']); + $this->assertEquals('Value6', $headers['Key6']); + $this->assertEquals('Value7', $headers['Key7']); + $this->assertEquals('Value8', $headers['Key8']); + $this->assertEquals('Value9', $headers['Key9']); + $this->assertEquals('Value10', $headers['Key10']); + $this->assertEquals('Value11', $headers['Key11']); + $this->assertEquals('Value12', $headers['Key12']); + $this->assertEquals('Value13', $headers['Key13']); + $this->assertEquals('Value14', $headers['Key14']); + $this->assertEquals('Value15', $headers['Key15']); + $this->assertEquals('Value16', $headers['Key16']); + } + } + + public function testSetHeadersIndexedArray() + { + $multi_curl = new MultiCurl(); + $multi_curl->setHeaders([ + ' Key1 : Value1 ', + ' Key2 : Value2', + ' Key3 :Value3 ', + ' Key4 :Value4', + ' Key5: Value5 ', + ' Key6: Value6', + ' Key7:Value7 ', + ' Key8:Value8', + 'Key9 : Value9 ', + 'Key10 : Value10', + 'Key11 :Value11 ', + 'Key12 :Value12', + 'Key13: Value13 ', + 'Key14: Value14', + 'Key15:Value15 ', + 'Key16:Value16', + ]); + $multi_curl->addGet(Test::TEST_URL); + + $curls = \Helper\get_multi_curl_property_value($multi_curl, 'queuedCurls'); + foreach ($curls as $curl) { + $this->assertEquals([ + 'Key1: Value1', + 'Key2: Value2', + 'Key3: Value3', + 'Key4: Value4', + 'Key5: Value5', + 'Key6: Value6', + 'Key7: Value7', + 'Key8: Value8', + 'Key9: Value9', + 'Key10: Value10', + 'Key11: Value11', + 'Key12: Value12', + 'Key13: Value13', + 'Key14: Value14', + 'Key15: Value15', + 'Key16: Value16', + ], $curl->getOpt(CURLOPT_HTTPHEADER)); + + $headers = \Helper\get_curl_property_value($curl, 'headers'); + $this->assertEquals('Value1', $headers['Key1']); + $this->assertEquals('Value2', $headers['Key2']); + $this->assertEquals('Value3', $headers['Key3']); + $this->assertEquals('Value4', $headers['Key4']); + $this->assertEquals('Value5', $headers['Key5']); + $this->assertEquals('Value6', $headers['Key6']); + $this->assertEquals('Value7', $headers['Key7']); + $this->assertEquals('Value8', $headers['Key8']); + $this->assertEquals('Value9', $headers['Key9']); + $this->assertEquals('Value10', $headers['Key10']); + $this->assertEquals('Value11', $headers['Key11']); + $this->assertEquals('Value12', $headers['Key12']); + $this->assertEquals('Value13', $headers['Key13']); + $this->assertEquals('Value14', $headers['Key14']); + $this->assertEquals('Value15', $headers['Key15']); + $this->assertEquals('Value16', $headers['Key16']); + } + } + + public function testSetAutoReferer() + { + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_AUTOREFERER)); + $multi_curl->setAutoReferer(true); + $this->assertTrue($multi_curl->getOpt(CURLOPT_AUTOREFERER)); + } + + public function testSetAutoReferrer() + { + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_AUTOREFERER)); + $multi_curl->setAutoReferrer(true); + $this->assertTrue($multi_curl->getOpt(CURLOPT_AUTOREFERER)); + } + + public function testSetFollowLocation() + { + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_FOLLOWLOCATION)); + $multi_curl->setFollowLocation(true); + $this->assertTrue($multi_curl->getOpt(CURLOPT_FOLLOWLOCATION)); + $multi_curl->setFollowLocation(false); + $this->assertFalse($multi_curl->getOpt(CURLOPT_FOLLOWLOCATION)); + } + + public function testSetForbidReuse() + { + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_FORBID_REUSE)); + $multi_curl->setForbidReuse(true); + $this->assertTrue($multi_curl->getOpt(CURLOPT_FORBID_REUSE)); + } + + public function testSetMaximumRedirects() + { + $multi_curl = new MultiCurl(); + $this->assertNull($multi_curl->getOpt(CURLOPT_MAXREDIRS)); + $multi_curl->setMaximumRedirects(3); + $this->assertEquals(3, $multi_curl->getOpt(CURLOPT_MAXREDIRS)); + } + + public function testPostDataArray() + { + $data = ['key' => 'value']; + + $multi_curl = new MultiCurl(); $multi_curl->setHeader('X-DEBUG-TEST', 'post'); - $multi_curl->addPost($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + $multi_curl->addPost(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'POST / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); }); $multi_curl->start(); + } - $multi_curl = new MultiCurl(/service/test::TEST_URL); - $multi_curl->setHeader('X-DEBUG-TEST', 'put'); - $multi_curl->addPut($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + public function testPostDataString() + { + $data = str_repeat('-', 100); + + $multi_curl = new MultiCurl(); + $multi_curl->setHeader('X-DEBUG-TEST', 'post_json'); + $multi_curl->addPost(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) use ($data) { + \PHPUnit\Framework\Assert::assertEquals( + 'POST / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); + \PHPUnit\Framework\Assert::assertEquals($data, $instance->response); }); $multi_curl->start(); } - public function testSetUrl() + public function testPatchDataArray() { - $data = array('key' => 'value'); + $data = ['key' => 'value']; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->setUrl(Test::TEST_URL); - $multi_curl->addDelete($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + $multi_curl->setHeader('X-DEBUG-TEST', 'patch'); + $multi_curl->addPatch(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'PATCH / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); }); $multi_curl->start(); + } + + public function testPatchDataString() + { + $data = str_repeat('-', 100); $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->setUrl(Test::TEST_URL); - $multi_curl->addGet($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + $multi_curl->setHeader('X-DEBUG-TEST', 'patch'); + $multi_curl->addPatch(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) use ($data) { + \PHPUnit\Framework\Assert::assertEquals( + 'PATCH / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); }); $multi_curl->start(); + } + + public function testPutDataArray() + { + $data = ['key' => 'value']; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->setUrl(Test::TEST_URL); - $multi_curl->addHead($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals( - 'HEAD /?key=value HTTP/1.1', $instance->requestHeaders['Request-Line']); + $multi_curl->setHeader('X-DEBUG-TEST', 'put'); + $multi_curl->addPut(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'PUT / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); + \PHPUnit\Framework\Assert::assertEquals('key=value', $instance->response); }); $multi_curl->start(); + } + + public function testPutDataString() + { + $data = str_repeat('-', 100); $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'get'); - $multi_curl->setUrl(Test::TEST_URL); - $multi_curl->addOptions($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + $multi_curl->setHeader('X-DEBUG-TEST', 'put'); + $multi_curl->addPut(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) use ($data) { + \PHPUnit\Framework\Assert::assertEquals( + 'PUT / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); }); + $multi_curl->start(); + } + + public function testSearchDataArray() + { + $data = ['key' => 'value']; $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'request_method'); - $multi_curl->setUrl(Test::TEST_URL); - $multi_curl->addPatch($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('PATCH', $instance->response); + $multi_curl->setHeader('X-DEBUG-TEST', 'search'); + $multi_curl->addSearch(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) { + \PHPUnit\Framework\Assert::assertEquals( + 'SEARCH / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); }); + $multi_curl->start(); + } + + public function testSearchDataString() + { + $data = str_repeat('-', 100); $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'post'); - $multi_curl->setUrl(Test::TEST_URL); - $multi_curl->addPost($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + $multi_curl->setHeader('X-DEBUG-TEST', 'search'); + $multi_curl->addSearch(Test::TEST_URL, $data); + $multi_curl->complete(function ($instance) use ($data) { + \PHPUnit\Framework\Assert::assertEquals( + 'SEARCH / HTTP/1.1', + $instance->requestHeaders['Request-Line'] + ); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->url); + \PHPUnit\Framework\Assert::assertEquals(Test::TEST_URL, $instance->effectiveUrl); }); + $multi_curl->start(); + } + public function testCurlStopActiveConcurrencyDefault() + { $multi_curl = new MultiCurl(); - $multi_curl->setHeader('X-DEBUG-TEST', 'put'); - $multi_curl->setUrl(Test::TEST_URL); - $multi_curl->addPut($data)->complete(function($instance) { - PHPUnit_Framework_Assert::assertEquals(Test::TEST_URL, $instance->baseUrl); - PHPUnit_Framework_Assert::assertEquals('key=value', $instance->response); + $request_count = 0; + $multi_curl->complete(function ($instance) use (&$request_count, $multi_curl) { + $request_count += 1; + $multi_curl->stop(); + }); + + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->addGet(Test::TEST_URL); + + $multi_curl->start(); + + $this->assertEquals(1, $request_count); + } + + public function testCurlStopActiveConcurrencyOne() + { + $multi_curl = new MultiCurl(); + $multi_curl->setConcurrency(1); + $request_count = 0; + $multi_curl->complete(function ($instance) use (&$request_count, $multi_curl) { + $request_count += 1; + $multi_curl->stop(); }); + + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->addGet(Test::TEST_URL); + + $multi_curl->start(); + + $this->assertEquals(1, $request_count); } - public function testAddRequestAfterStart() { + public function testCurlStopOnError() + { $multi_curl = new MultiCurl(); + $multi_curl->setConcurrency(1); + $request_count = 0; + $multi_curl->complete(function ($instance) use (&$request_count) { + $request_count += 1; + }); + $multi_curl->error(function ($instance) use ($multi_curl) { + $multi_curl->stop(); + }); - $urls = array(); - $copy_of_urls = array(); - for ($i = 0; $i < 10; $i++) { - $url = Test::TEST_URL . '?' . md5(mt_rand()); - $urls[] = $url; - $copy_of_urls[] = $url; - } + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->addGet(Test::TEST_URL); + $multi_curl->addGet(Test::ERROR_URL); + $multi_curl->addGet(Test::TEST_URL); - $urls_called = array(); - $multi_curl->complete(function ($instance) use (&$multi_curl, &$urls, &$urls_called) { - $urls_called[] = $instance->url; - $next_url = array_pop($urls); - if (!($next_url === null)) { - $multi_curl->addGet($next_url); - } + $multi_curl->start(); + + $this->assertEquals(3, $request_count); + } + + public function testBeforeSendEachRequest() + { + // Ensure MultiCurl::beforeSend() is called before each request including retries. + + $multi_curl = new MultiCurl(); + $multi_curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $multi_curl->setHeader('X-DEBUG-TEST', 'retry'); + $multi_curl->setRetry(5); + + $before_send_call_count = 0; + $multi_curl->beforeSend(function ($instance) use (&$before_send_call_count) { + $before_send_call_count += 1; }); - $multi_curl->addGet(array_pop($urls)); + $instance = $multi_curl->addGet(Test::TEST_URL, ['failures' => 5]); $multi_curl->start(); - $this->assertNotEmpty($copy_of_urls); - $this->assertNotEmpty($urls_called); - $this->assertEquals(count($copy_of_urls), count($urls_called)); + $this->assertEquals(6, $before_send_call_count); + $this->assertEquals(6, $instance->attempts); + $this->assertEquals(5, $instance->retries); + $this->assertFalse($instance->error); + } - foreach ($copy_of_urls as $url) { - $this->assertTrue(in_array($url, $urls_called, true)); - } + public function testAfterSendEachRequest() + { + // Ensure MultiCurl::afterSend() is called before each request including retries. + + $multi_curl = new MultiCurl(); + $multi_curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $multi_curl->setHeader('X-DEBUG-TEST', 'retry'); + $multi_curl->setRetry(5); + + $after_send_call_count = 0; + $multi_curl->afterSend(function ($instance) use (&$after_send_call_count) { + $after_send_call_count += 1; + }); + + $instance = $multi_curl->addGet(Test::TEST_URL, ['failures' => 5]); + $multi_curl->start(); + + $this->assertEquals(6, $after_send_call_count); + $this->assertEquals(6, $instance->attempts); + $this->assertEquals(5, $instance->retries); + $this->assertFalse($instance->error); } - public function testClose() + public function testAfterSendAttemptCount() { $multi_curl = new MultiCurl(); - $multi_curl->addGet(Test::TEST_URL); + $multi_curl->setRetry(10); + $multi_curl->afterSend(function ($instance) { + if ($instance->attempts < 5) { + $instance->error = true; + } else { + $instance->error = false; + } + }); + $multi_curl->setHeader('X-DEBUG-TEST', 'json_response'); + $instance = $multi_curl->addGet(Test::TEST_URL); $multi_curl->start(); - $this->assertTrue(is_resource($multi_curl->multiCurl)); - $multi_curl->close(); - $this->assertFalse(is_resource($multi_curl->multiCurl)); + $this->assertEquals(5, $instance->attempts); + $this->assertEquals(4, $instance->retries); + $this->assertFalse($instance->error); + } + + public function testAfterSendResponseMessage() + { + $multi_curl = new MultiCurl(); + $multi_curl->setOpt(CURLOPT_COOKIEJAR, '/dev/null'); + $multi_curl->setRetry(5); + $multi_curl->afterSend(function ($instance) { + $instance->error = $instance->response->message !== '202 Accepted'; + }); + $multi_curl->setHeader('X-DEBUG-TEST', 'retry'); + $instance = $multi_curl->addGet(Test::TEST_URL, ['failures' => 3]); + $multi_curl->start(); + $this->assertEquals(4, $instance->attempts); + $this->assertEquals(3, $instance->retries); + $this->assertFalse($instance->error); } } diff --git a/tests/PHPCurlClass/TimeUtilTest.php b/tests/PHPCurlClass/TimeUtilTest.php new file mode 100644 index 0000000000..a6ac1fb949 --- /dev/null +++ b/tests/PHPCurlClass/TimeUtilTest.php @@ -0,0 +1,60 @@ +assertEquals( + (float)1_750_000_060.123456, + $sleep_until_microtime, + ); + } + + public function testGetSleepSecondsUntilMicrotime() + { + $sleep_until_microtime = (float)1_750_000_060.123456; + $current_microtime = (float)1_750_000_044.999999; + + $sleep_seconds = TimeUtil::getSleepSecondsUntilMicrotime( + $sleep_until_microtime, + $current_microtime, + ); + + $this->assertEquals( + (float)15.123457, + $sleep_seconds, + ); + } + + public function testGetWholeAndRemainderSeconds() + { + $sleep_seconds = (float)15.123457; + + list($whole_seconds, $microseconds_remainder) = TimeUtil::getWholeAndRemainderSeconds( + $sleep_seconds, + ); + + $this->assertEquals(15, $whole_seconds); + $this->assertEquals(123457, $microseconds_remainder); + } +} diff --git a/tests/PHPCurlClass/UrlTest.php b/tests/PHPCurlClass/UrlTest.php new file mode 100644 index 0000000000..397793565e --- /dev/null +++ b/tests/PHPCurlClass/UrlTest.php @@ -0,0 +1,159 @@ +assertEquals($expected_url, $actual_url, "Joint URLs: '{$test[0]}', '{$test[1]}'"); + } + fclose($urls_file); + } + + public function testUrlInstances() + { + $a = new Url('https://developer.mozilla.org/', '/'); + $this->assertEquals('https://developer.mozilla.org/', $a); + + $b = new Url('https://developer.mozilla.org/'); + $this->assertEquals('https://developer.mozilla.org/', $b); + + $c = new Url($b, 'en-US/docs'); + $this->assertEquals('https://developer.mozilla.org/en-US/docs', $c); + + $d = new Url($b, '/en-US/docs'); + $this->assertEquals('https://developer.mozilla.org/en-US/docs', $d); + + $f = new Url($d, '/en-US/docs'); + $this->assertEquals('https://developer.mozilla.org/en-US/docs', $f); + + $g = new Url('https://developer.mozilla.org/fr-FR/toto', '/en-US/docs'); + $this->assertEquals('https://developer.mozilla.org/en-US/docs', $g); + + $h = new Url($a, '/en-US/docs'); + $this->assertEquals('https://developer.mozilla.org/en-US/docs', $h); + + $k = new Url('https://developers.mozilla.com/', 'http://www.example.com/'); + $this->assertEquals('http://www.example.com/', $k); + + $l = new Url($b, 'http://www.example.com/'); + $this->assertEquals('http://www.example.com/', $l); + } + + public function testRemoveDotSegments() + { + // TODO: Add tests using "Normal Examples" and "Abnormal Examples" from RFC 3986. + $tests = [ + [ + 'path' => '/a/b/c/./../../g', + 'expected' => '/a/g', + ], + [ + 'path' => 'mid/content=5/../6', + 'expected' => 'mid/6', + ], + ]; + foreach ($tests as $test) { + $actual_path = Url::removeDotSegments($test['path']); + $this->assertEquals($test['expected'], $actual_path); + } + } + + public function testUrlCyrillicChars() + { + $path_part = 'Банан-комнатный-саженцы-банана'; + $original_url = 'https://www.example.com/path/' . $path_part . '/page.html'; + $expected_url = 'https://www.example.com/path/' . rawurlencode($path_part) . '/page.html'; + $url = new Url($original_url); + $this->assertEquals($expected_url, $url); + } + + public function testParseUrlSyntaxComponents() + { + // RFC 3986 - Syntax Components. + // The following are two example URIs and their component parts: + // + // foo://example.com:8042/over/there?name=ferret#nose + // \_/ \______________/\_________/ \_________/ \__/ + // | | | | | + // scheme authority path query fragment + $input_url = 'foo://example.com:8042/over/there?name=ferret#nose'; + $expected_parts = [ + 'scheme' => 'foo', + 'host' => 'example.com', + 'port' => '8042', + 'path' => '/over/there', + 'query' => 'name=ferret', + 'fragment' => 'nose', + ]; + + $this->assertEquals($expected_parts, parse_url(http://github.com/$input_url)); + + $result = Url::parseUrl($input_url); + $this->assertEquals($expected_parts, $result); + } + + public function testParseUrlExample() + { + $input_url = 'http://username:password@hostname:9090/path?arg=value#anchor'; + $expected_parts = [ + 'scheme' => 'http', + 'host' => 'hostname', + 'port' => '9090', + 'user' => 'username', + 'pass' => 'password', + 'path' => '/path', + 'query' => 'arg=value', + 'fragment' => 'anchor', + ]; + + $this->assertEquals($expected_parts, parse_url(http://github.com/$input_url)); + + $result = Url::parseUrl($input_url); + $this->assertEquals($expected_parts, $result); + } + + public function testUrlIpv6NoPort() + { + $expected_url = 'http://[::1]/test'; + $actual_url = new Url($expected_url); + $this->assertEquals($expected_url, $actual_url); + } + + public function testUrlIpv6Port() + { + $expected_url = 'http://[::1]/test'; + $actual_url = new Url($expected_url); + $this->assertEquals($expected_url, $actual_url); + } + + public function testParseUrlSchemelessUrl() + { + $input_url = '10.1.2.43:8080/config/getconfig'; + $expected_parts = [ + 'host' => '10.1.2.43', + 'port' => '8080', + 'path' => '/config/getconfig', + ]; + + $this->assertEquals($expected_parts, parse_url(http://github.com/$input_url)); + + $result = Url::parseUrl($input_url); + $this->assertEquals($expected_parts, $result); + } +} diff --git a/tests/PHPCurlClass/server.php b/tests/PHPCurlClass/server.php deleted file mode 100644 index ebbb74f6d1..0000000000 --- a/tests/PHPCurlClass/server.php +++ /dev/null @@ -1,264 +0,0 @@ - $_SERVER['PHP_AUTH_USER'], - 'password' => $_SERVER['PHP_AUTH_PW'], - )); - exit; -} elseif ($test == 'http_digest_auth') { - $users = array( - 'myusername' => 'mypassword', - ); - - $realm = 'Restricted area'; - $qop = 'auth'; - $nonce = md5(uniqid()); - $opaque = md5(uniqid()); - if (empty($_SERVER['PHP_AUTH_DIGEST'])) { - header('HTTP/1.1 401 Unauthorized'); - header(sprintf( - 'WWW-Authenticate: Digest realm="%s", qop="%s", nonce="%s", opaque="%s"', $realm, $qop, $nonce, $opaque)); - echo 'canceled'; - exit; - } - - $data = array( - 'nonce' => '', - 'nc' => '', - 'cnonce' => '', - 'qop' => '', - 'username' => '', - 'uri' => '', - 'response' => '', - ); - preg_match_all('@(' . implode('|', array_keys($data)) . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', - $_SERVER['PHP_AUTH_DIGEST'], $matches, PREG_SET_ORDER); - foreach ($matches as $match) { - $data[$match['1']] = $match['3'] ? $match['3'] : $match['4']; - } - - $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]); - $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']); - $valid_response = md5( - $A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2); - - if (!($data['response'] === $valid_response)) { - header('HTTP/1.1 401 Unauthorized'); - echo 'invalid'; - exit; - } - - echo 'valid'; - exit; -} elseif ($test === 'get') { - echo http_build_query($_GET); - exit; -} elseif ($test === 'post') { - echo http_build_query($_POST); - exit; -} elseif ($test === 'post_json') { - echo $http_raw_post_data; - exit; -} elseif ($test === 'put') { - echo $http_raw_post_data; - exit; -} elseif ($test === 'patch') { - echo $http_raw_post_data; - exit; -} elseif ($test === 'post_multidimensional') { - echo $http_raw_post_data; - exit; -} elseif ($test === 'post_file_path_upload') { - echo Helper\mime_type($_FILES[$key]['tmp_name']); - exit; -} elseif ($test === 'put_file_handle') { - $tmp_filename = tempnam('/tmp', 'php-curl-class.'); - file_put_contents($tmp_filename, $http_raw_post_data); - echo Helper\mime_type($tmp_filename); - unlink($tmp_filename); - exit; -} elseif ($test === 'request_method') { - header('X-REQUEST-METHOD: ' . $request_method); - echo $request_method; - exit; -} elseif ($test === 'request_uri') { - echo $_SERVER['REQUEST_URI']; - exit; -} elseif ($test === 'cookiejar') { - setcookie('mycookie', 'yum'); - exit; -} elseif ($test === 'multiple_cookie') { - setcookie('cookie1', 'scrumptious'); - setcookie('cookie2', 'mouthwatering'); - exit; -} elseif ($test === 'response_header') { - header('Content-Type: application/json'); - header('ETag: ' . md5('worldpeace')); - exit; -} elseif ($test === 'response_body') { - echo 'OK'; - exit; -} elseif ($test === 'json_response') { - if ($request_method === 'POST') { - $key = $_POST['key']; - $value = $_POST['value']; - header($key . ': ' . $value); - } else { - header('Content-Type: application/json'); - } - echo json_encode(array( - 'null' => null, - 'true' => true, - 'false' => false, - 'integer' => 1, - 'float' => 3.14, - 'empty' => '', - 'string' => 'string', - )); - exit; -} elseif ($test === 'xml_response') { - $key = $_POST['key']; - $value = $_POST['value']; - header($key . ': ' . $value); - $doc = new DOMDocument(); - $doc->formatOutput = true; - $rss = $doc->appendChild($doc->createElement('rss')); - $rss->setAttribute('version', '2.0'); - $channel = $doc->createElement('channel'); - $title = $doc->createElement('title'); - $title->appendChild($doc->createTextNode('Title')); - $channel->appendChild($title); - $link = $doc->createElement('link'); - $link->appendChild($doc->createTextNode('Link')); - $channel->appendChild($link); - $description = $doc->createElement('description'); - $description->appendChild($doc->createTextNode('Description')); - $channel->appendChild($description); - $rss->appendChild($channel); - echo $doc->saveXML(); - exit; -} elseif ($test === 'upload_response') { - $tmp_filename = tempnam('/tmp', 'php-curl-class.'); - move_uploaded_file($_FILES['image']['tmp_name'], $tmp_filename); - header('Content-Type: application/json'); - header('ETag: ' . md5_file($tmp_filename)); - echo json_encode(array( - 'file_path' => $tmp_filename, - )); - exit; -} elseif ($test === 'upload_cleanup') { - $unsafe_file_path = $_POST['file_path']; - echo var_export(unlink($unsafe_file_path), true); - exit; -} elseif ($test === 'download_response') { - $unsafe_file_path = $_GET['file_path']; - header('Content-Type: image/png'); - header('Content-Disposition: attachment; filename="image.png"'); - header('Content-Length: ' . filesize($unsafe_file_path)); - header('ETag: ' . md5_file($unsafe_file_path)); - readfile($unsafe_file_path); - exit; -} elseif ($test === 'timeout') { - $unsafe_seconds = $_GET['seconds']; - $start = time(); - while (true) { - echo '.'; - ob_flush(); - flush(); - sleep(1); - $elapsed = time() - $start; - if ($elapsed >= $unsafe_seconds) { - break; - } - } - exit; -} elseif ($test === 'error_message') { - if (function_exists('http_response_code')) { - http_response_code(401); - } else { - header('HTTP/1.1 401 Unauthorized'); - } - exit; -} elseif ($test === 'redirect') { - if (!isset($_GET['redirect'])) { - header('Location: ?redirect'); - exit; - } - - echo 'OK'; - exit; -} elseif ($test === 'delete_with_body') { - header('Content-Type: application/json'); - echo json_encode(array( - 'get' => $_GET, - 'delete' => $_DELETE, - )); - exit; -} elseif ($test === 'data_values') { - header('Content-Type: application/json'); - echo json_encode($data_values); - exit; -} - -header('Content-Type: text/plain'); - -$data_mapping = array( - 'cookie' => '_COOKIE', - 'delete' => '_GET', - 'get' => '_GET', - 'patch' => '_PATCH', - 'post' => '_POST', - 'put' => '_PUT', - 'server' => '_SERVER', -); - -if (!empty($test)) { - $data = $$data_mapping[$test]; - $value = isset($data[$key]) ? $data[$key] : ''; - echo $value; -} diff --git a/tests/PHPCurlClass/urls.csv.gz b/tests/PHPCurlClass/urls.csv.gz new file mode 100644 index 0000000000..cc1972bae5 Binary files /dev/null and b/tests/PHPCurlClass/urls.csv.gz differ diff --git a/tests/RangeHeader.php b/tests/RangeHeader.php new file mode 100644 index 0000000000..4676c9f41f --- /dev/null +++ b/tests/RangeHeader.php @@ -0,0 +1,72 @@ +first_byte = isset($matches['1']) ? (int)$matches['1'] : null; + $this->last_byte = isset($matches['2']) ? (int)$matches['2'] : null; + + $this->filesize = filesize($file_path); + + // Start position begins after end of file. + if ($this->first_byte >= $this->filesize) { + $this->is_valid = false; + } + + // "If the last-byte-pos value is present, it MUST be greater than or equal to the first-byte-pos in that + // byte-range-spec, or the byte- range-spec is syntactically invalid." + if ($this->last_byte !== null && !($this->last_byte >= $this->first_byte)) { + $this->is_valid = false; + } + } + + public function getFirstBytePosition() + { + if ($this->first_byte === null) { + return $this->filesize - 1 - $this->last_byte; + } + + return $this->first_byte; + } + + public function getLastBytePosition() + { + if ($this->last_byte === null) { + return $this->filesize - 1; + } + + return $this->last_byte; + } + + public function getLength() + { + return $this->getLastBytePosition() - $this->getFirstBytePosition() + 1; + } + + public function getByteRangeSpec() + { + return $this->is_valid ? $this->getFirstBytePosition() . '-' . $this->getLastBytePosition() : '*'; + } + + public function getContentRangeHeader() + { + return 'bytes ' . $this->getByteRangeSpec() . '/' . $this->filesize; + } + + public function isValid() + { + return $this->is_valid; + } +} diff --git a/tests/User.php b/tests/User.php new file mode 100644 index 0000000000..3bb5660a5d --- /dev/null +++ b/tests/User.php @@ -0,0 +1,30 @@ +name = $name; + $this->email = $email; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return [ + 'name' => $this->name, + 'email' => $this->email, + ]; + } + } +} diff --git a/tests/before_script.sh b/tests/before_script.sh deleted file mode 100755 index 59d0da5ebc..0000000000 --- a/tests/before_script.sh +++ /dev/null @@ -1,58 +0,0 @@ -echo "TRAVIS_PHP_VERSION: ${TRAVIS_PHP_VERSION}" - -composer self-update -composer install --prefer-source --no-interaction - -if [[ "${TRAVIS_PHP_VERSION}" == "5.3" ]]; then - sudo add-apt-repository -y ppa:nginx/development - sudo apt-get update - sudo apt-get install -y nginx - sudo apt-get install -y php5-fpm - root="$(pwd)/tests/PHPCurlClass" - sudo tee /etc/nginx/sites-enabled/default <>> remove_dot_segments('https://www.example.com/baz/bux/') + 'https://www.example.com/baz/bux/' + >>> remove_dot_segments('https://www.example.com/some/file.ext') + 'https://www.example.com/some/file.ext' + """ + + parsed = urlparse(url) + new_path = posixpath.normpath(parsed.path) + if parsed.path.endswith("/"): + # Fix missing trailing slash. + # https://bugs.python.org/issue1707768 + new_path += "/" + if new_path.startswith("//"): + new_path = new_path[1:] + cleaned = parsed._replace(path=new_path) + return cleaned.geturl() + + +first_authorities = [ + "http://example.com@user:pass:7152", + "https://example.com/", +] +second_authorities = [ + "", + "https://www.example.org/", + "http://example.com@user:pass:1111", + "file://example.com", + "file://", +] +first_paths = [ + "", + "/", + "/foobar/bazz", + "foobar/bazz/", +] +second_paths = [ + "", + "/", + "/foo/bar", + "foo/bar/", + "./foo/../bar", + "foo/./.././bar", +] +first_queries = ["", "?a=1", "?a=647&b=s564"] +second_queries = ["", "?a=sdf", "?a=cvb&b=987"] +fragments = ["", "#foo", "#bar"] + +additional_tests = [ + { + "args": [ + "http://www.example.com/", + "", + ], + "expected": "http://www.example.com/", + }, + { + "args": [ + "http://www.example.com/", + "foo", + ], + "expected": "http://www.example.com/foo", + }, + { + "args": [ + "http://www.example.com/", + "/foo", + ], + "expected": "http://www.example.com/foo", + }, + { + "args": [ + "http://www.example.com/", + "/foo/", + ], + "expected": "http://www.example.com/foo/", + }, + { + "args": [ + "http://www.example.com/", + "/dir/page.html", + ], + "expected": "http://www.example.com/dir/page.html", + }, + { + "args": [ + "http://www.example.com/dir1/page2.html", + "/dir/page.html", + ], + "expected": "http://www.example.com/dir/page.html", + }, + { + "args": [ + "http://www.example.com/dir1/page2.html", + "dir/page.html", + ], + "expected": "http://www.example.com/dir1/dir/page.html", + }, + { + "args": [ + "http://www.example.com/dir1/dir3/page.html", + "../dir/page.html", + ], + "expected": "http://www.example.com/dir1/dir/page.html", + }, +] + +with open("urls.csv", "wt") as f: + csvwriter = csv.writer(f, quotechar='"', quoting=csv.QUOTE_ALL) + csvwriter.writerow(["first_url", "second_url", "expected"]) + for test in additional_tests: + csvwriter.writerow([test["args"][0], test["args"][1], test["expected"]]) + for first_domain, second_domain in product(first_authorities, second_authorities): + for first_path, second_path in product(first_paths, second_paths): + for first_query, second_query in product(first_queries, second_queries): + for first_fragment, second_fragment in product(fragments, fragments): + if not first_path.startswith("/"): + first_path = "/" + first_path + first_url = first_domain + first_path + first_query + first_fragment + if second_domain and not second_path.startswith("/"): + second_path = "/" + second_path + second_url = ( + second_domain + second_path + second_query + second_fragment + ) + if first_url != second_url: + expected_url = remove_dot_segments( + urljoin(first_url, second_url) + ) + csvwriter.writerow([first_url, second_url, expected_url]) diff --git a/tests/generate_urls.sh b/tests/generate_urls.sh new file mode 100755 index 0000000000..cfb11d3f09 --- /dev/null +++ b/tests/generate_urls.sh @@ -0,0 +1,10 @@ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +set -x + +python3 "generate_urls.py" && + ([[ -f "urls.csv.gz" ]] && rm --verbose "urls.csv.gz" || exit 0) && + gzip --verbose --best --no-name "urls.csv" && + gzip --verbose --test "urls.csv.gz" && + mv --verbose "urls.csv.gz" "PHPCurlClass/urls.csv.gz" diff --git a/tests/PHPCurlClass/index.php b/tests/index.php similarity index 100% rename from tests/PHPCurlClass/index.php rename to tests/index.php diff --git a/tests/phpstan-baseline.neon b/tests/phpstan-baseline.neon new file mode 100644 index 0000000000..cde552e0fc --- /dev/null +++ b/tests/phpstan-baseline.neon @@ -0,0 +1,13 @@ +parameters: + ignoreErrors: + - + message: '#^Class CurlHandle not found\.$#' + identifier: class.notFound + count: 2 + path: ../src/Curl/Curl.php + + - + message: '#^Class CurlMultiHandle not found\.$#' + identifier: class.notFound + count: 2 + path: ../src/Curl/MultiCurl.php diff --git a/tests/phpstan.neon b/tests/phpstan.neon new file mode 100644 index 0000000000..d933374d82 --- /dev/null +++ b/tests/phpstan.neon @@ -0,0 +1,16 @@ +includes: + - phpstan-baseline.neon + - phar://phpstan.phar/conf/bleedingEdge.neon + +parameters: + reportUnmatchedIgnoredErrors: false + + # TODO: Increase rule level to be more strict. + level: 3 + + # TODO: Remove all exclusions except vendor/ and fix related errors. + excludePaths: + - ../examples/* + - ../tests/* + - ../vendor/* + - ../www/* diff --git a/tests/phpunit.xml b/tests/phpunit.xml index a32aede7e9..ea668de18c 100644 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -1,7 +1,10 @@ - ./PHPCurlClass/ + ../tests/ + + + diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml new file mode 100644 index 0000000000..9515635b16 --- /dev/null +++ b/tests/psalm-baseline.xml @@ -0,0 +1,25 @@ + + + + + + + + + curl]]> + + + + + + + + + multiCurl]]> + multiCurl]]> + multiCurl]]> + multiCurl]]> + multiCurl]]> + + + diff --git a/tests/psalm.xml b/tests/psalm.xml new file mode 100644 index 0000000000..2a56ee25db --- /dev/null +++ b/tests/psalm.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/ruleset.xml b/tests/ruleset.xml new file mode 100644 index 0000000000..10ddbed48f --- /dev/null +++ b/tests/ruleset.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + ./examples/* + + + + + + + + + + + + 0 + + diff --git a/tests/run.sh b/tests/run.sh index ab99598dbe..e0b843252d 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,4 +1,66 @@ -php -S 127.0.0.1:8000 -t PHPCurlClass/ &> /dev/null & -pid="${!}" -phpunit --configuration phpunit.xml -kill "${pid}" +#!/usr/bin/env bash + +set -x + +warnings=() +errors=() + +if [[ "${CI}" == "true" ]]; then + composer self-update + + # Skip attempting to install psalm on future PHP releases to avoid the + # following error that would otherwise block the remaining tests from + # running: + # Your requirements could not be resolved to an installable set of packages. + # + # Problem 1 + # - Root composer.json requires vimeo/psalm >=5.26.1 -> satisfiable by vimeo/psalm[5.26.1, 6.0.0, ..., 6.8.8]. + # - vimeo/psalm 5.26.1 requires php ^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 -> your php version (8.5.0-dev) does not satisfy that requirement. + # - vimeo/psalm[6.0.0, ..., 6.5.0] require php ~8.1.17 || ~8.2.4 || ~8.3.0 || ~8.4.0 -> your php version (8.5.0-dev) does not satisfy that requirement. + # - vimeo/psalm[6.5.1, ..., 6.8.8] require php ~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 -> your php version (8.5.0-dev) does not satisfy that requirement. + # + # Error: Your requirements could not be resolved to an installable set of packages. + if ! "${CI_PHP_FUTURE_RELEASE}"; then + composer require --dev "vimeo/psalm:>=5.26.1" + fi + + composer install --prefer-source --no-interaction + if [[ "${?}" -ne 0 ]]; then + echo "❌ Error: composer install failed" + errors+=("composer install failed") + fi +fi + +# Use composer's phpunit and phpcs by adding composer bin directory to the path environment variable. +export PATH="${PWD}/vendor/bin:${PATH}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +source "set_vars.inc.sh" + +echo "CI_PHP_VERSION: ${CI_PHP_VERSION}" +echo "CI_PHP_FUTURE_RELEASE: ${CI_PHP_FUTURE_RELEASE}" +php -r "var_dump(phpversion());" +php -r "var_dump(curl_version());" + +source "run_syntax_check.sh" + +source "run_coding_standards_check.sh" + +source "run_phpunit.sh" + +source "run_static_analysis_check_phpstan.sh" + +source "run_static_analysis_check_psalm.sh" + +set +x + +source "display_warnings.inc.sh" +source "display_errors.inc.sh" + +if [[ "${CI_PHP_FUTURE_RELEASE}" != "true" ]]; then + exit "${#errors[@]}" +elif [[ "${#errors[@]}" -ne 0 ]]; then + echo "⚠️ One or more tests failed, but allowed as the CI_PHP_FUTURE_RELEASE flag is on for PHP version ${CI_PHP_VERSION}." +fi diff --git a/tests/run_coding_standards_check.sh b/tests/run_coding_standards_check.sh new file mode 100755 index 0000000000..7fdcde9925 --- /dev/null +++ b/tests/run_coding_standards_check.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +source "set_vars.inc.sh" + +# Run commands from the project root directory. +pushd .. + +# Enforce line ending consistency in php files. +crlf_file=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec grep --color=always --files-with-matches $'\r' {} \;) +if [[ ! -z "${crlf_file}" ]]; then + result="$(echo "${crlf_file}" | perl -pe 's/(.*)/CRLF line terminators found in \1/')" + echo "❌ ${result}" + errors+=("${result}") +fi + +# Enforce indentation character consistency in php files. +tab_char=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec grep --color=always --line-number -H --perl-regexp "\t" {} \;) +if [[ ! -z "${tab_char}" ]]; then + result="$(echo -e "${tab_char}" | perl -pe 's/^(.*)$/Tab character found in \1/')" + echo "❌ ${result}" + errors+=("${result}") +fi + +# Enforce indentation consistency in php files. +find_invalid_indentation() { + filename="${1}" + script=$(cat <<'EOF' + $file_name_color = '35'; // 35 = Magenta. + $separator_color = '36'; // 36 = Cyan. + $line_number_color = '32'; // 32 = Green. + + $filename = $argv['1']; + $lines = explode("\n", file_get_contents($filename)); + $line_number = 0; + foreach ($lines as $line) { + $line_number += 1; + $leading_space_count = strspn($line, ' '); + $remainder = $leading_space_count % 4; + if ($remainder !== 0) { + $trimmed_line = ltrim($line); + + // Allow doc comments. + if (substr($trimmed_line, 0, 1) === '*') { + continue; + } + + // Allow method chaining. + if (substr($trimmed_line, 0, 2) === '->') { + continue; + } + + $add_count = 4 - $remainder; + $remove_count = $remainder; + echo 'Invalid indentation found in ' . + "\033[" . $file_name_color . 'm' . $filename . "\033[0m" . + "\033[" . $separator_color . 'm' . ':' . "\033[0m" . + "\033[" . $line_number_color . 'm' . $line_number . "\033[0m" . + ' (' . $leading_space_count . ':+' . $add_count . '/-' . $remove_count . ')' . "\n"; + } + } +EOF +) + php --run "${script}" "${filename}" +} +export -f "find_invalid_indentation" +invalid_indentation=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec bash -c 'find_invalid_indentation "{}"' \;) +if [[ ! -z "${invalid_indentation}" ]]; then + echo "❌ ${invalid_indentation}" + errors+=("${invalid_indentation}") +fi + +# Prohibit trailing whitespace in php files. +trailing_whitespace=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec grep --color=always --extended-regexp --line-number -H " +$" {} \;) +if [[ ! -z "${trailing_whitespace}" ]]; then + result="$(echo -e "${trailing_whitespace}" | perl -pe 's/^(.*)$/Trailing whitespace found in \1/')" + echo "❌ ${result}" + errors+=("${result}") +fi + +# Require identical comparison operators (===, not ==) in php files. +equal=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec grep --color=always --extended-regexp --line-number -H "[^!=]==[^=]" {} \;) +if [[ ! -z "${equal}" ]]; then + result="$(echo -e "${equal}" | perl -pe 's/^(.*)$/Non-identical comparison operator found in \1/')" + echo "❌ ${result}" + errors+=("${result}") +fi + +# Require both braces on else statement line; "} else {" and not "}\nelse {". +elses=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec grep --color=always --line-number -H --perl-regexp '^(\s+)?else(\s+)?{' {} \;) +if [[ ! -z "${elses}" ]]; then + result="$(echo -e "${elses}" | perl -pe 's/^(.*)$/Found newline before "else" statement in \1/')" + echo "❌ ${result}" + errors+=("${result}") +fi + +# Run PHP_CodeSniffer. +# Determine which phpcs to use. +if [[ -f "vendor/bin/phpcs" ]]; then + phpcs_to_use="vendor/bin/phpcs" +else + phpcs_to_use="phpcs" +fi + +# Detect coding standard violations. +"${phpcs_to_use}" --version +"${phpcs_to_use}" \ + --extensions="php" \ + --ignore="*/vendor/*" \ + --standard="tests/ruleset.xml" \ + -p \ + -s \ + . +if [[ "${?}" -ne 0 ]]; then + echo "❌ Error: found PHP_CodeSniffer coding standard violation(s)" + errors+=("found PHP_CodeSniffer coding standard violation(s)") +fi + +# Run PHP-CS-Fixer. +vendor/bin/php-cs-fixer --version +vendor/bin/php-cs-fixer fix --ansi --config="tests/.php-cs-fixer.php" --diff --dry-run +if [[ "${?}" -ne 0 ]]; then + echo "❌ Error: found PHP-CS-Fixer coding standard violation(s)" + errors+=("found PHP-CS-Fixer coding standard violation(s)") +fi + +popd diff --git a/tests/run_phpunit.sh b/tests/run_phpunit.sh new file mode 100755 index 0000000000..46d1e1c037 --- /dev/null +++ b/tests/run_phpunit.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash + +remove_expectWarning() { + # Fix "Call to undefined method CurlTest\CurlTest::expectWarning()". + if sed v < /dev/null 2> /dev/null; then + sed -i"" -e "/->expectWarning(/d" "./PHPCurlClass/PHP"* + else + sed -i "" -e "/->expectWarning(/d" "./PHPCurlClass/PHP"* + fi +} + +replace_assertStringContainsString() { + # -->assertStringContainsString( + # +->assertContains( + find='->assertStringContainsString(' + replace='->assertContains(' + if sed v < /dev/null 2> /dev/null; then + sed -i"" -e "s/${find}/${replace}/" "./PHPCurlClass/PHP"* + else + sed -i "" -e "s/${find}/${replace}/" "./PHPCurlClass/PHP"* + fi +} + +replace_assertMatchesRegularExpression() { + # -->assertMatchesRegularExpression( + # +->assertRegExp( + find='->assertMatchesRegularExpression(' + replace='->assertRegExp(' + if sed v < /dev/null 2> /dev/null; then + sed -i"" -e "s/${find}/${replace}/" "./PHPCurlClass/PHP"* + else + sed -i "" -e "s/${find}/${replace}/" "./PHPCurlClass/PHP"* + fi +} + +phpunit_v6_5_shim() { + remove_expectWarning + replace_assertMatchesRegularExpression + replace_assertStringContainsString +} + +phpunit_v7_5_shim() { + remove_expectWarning + replace_assertMatchesRegularExpression +} + +phpunit_v8_5_shim() { + remove_expectWarning + replace_assertMatchesRegularExpression +} + +phpunit_v9_shim() { + replace_assertMatchesRegularExpression +} + +phpunit_v10_shim() { + remove_expectWarning +} + +phpunit_v11_shim() { + :; +} + +phpunit_v12_shim() { + remove_expectWarning +} + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +# Let test server know we should allow testing. +export PHP_CURL_CLASS_TEST_MODE_ENABLED="yes" + +# Start test servers. Run servers on different ports to allow simultaneous +# requests without blocking. +server_count=7 +pids=() +for i in $(seq 0 "$(echo "${server_count} - 1" | bc)"); do + port=8000 + (( port += $i )) + + php -S "127.0.0.1:${port}" server.php &> /dev/null & + pids+=("${!}") +done + +# Determine which phpunit to use. +if [[ -f "../vendor/bin/phpunit" ]]; then + phpunit_to_use="../vendor/bin/phpunit" +else + phpunit_to_use="phpunit" +fi + +phpunit_version="$("${phpunit_to_use}" --version | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+")" +echo "phpunit_version: ${phpunit_version}" + +extra_args="${@}" +if [[ "${phpunit_version}" == "6.5."* ]]; then + phpunit_v6_5_shim + phpunit_args=" --debug --verbose --fail-on-risky ${extra_args}" +elif [[ "${phpunit_version}" == "7.5."* ]]; then + phpunit_v7_5_shim + phpunit_args=" --debug --verbose --fail-on-risky ${extra_args}" +elif [[ "${phpunit_version}" == "8.5."* ]]; then + phpunit_v8_5_shim + phpunit_args=" --debug --verbose --fail-on-risky ${extra_args}" +elif [[ "${phpunit_version}" == "9."* ]]; then + phpunit_v9_shim + phpunit_args=" --debug --verbose --fail-on-risky ${extra_args}" +elif [[ "${phpunit_version}" == "10."* ]]; then + phpunit_v10_shim + phpunit_args=" --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warnings --fail-on-risky ${extra_args}" +elif [[ "${phpunit_version}" == "11."* ]]; then + phpunit_v11_shim + phpunit_args=" --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warnings --fail-on-risky ${extra_args}" +elif [[ "${phpunit_version}" == "12."* ]]; then + phpunit_v12_shim + phpunit_args=" --display-incomplete --display-skipped --display-deprecations --display-errors --display-notices --display-warnings --fail-on-risky ${extra_args}" +fi + +# Run tests. +"${phpunit_to_use}" --version +"${phpunit_to_use}" \ + --configuration "phpunit.xml" \ + ${phpunit_args} +if [[ "${?}" -ne 0 ]]; then + echo "❌ Error: phpunit command failed" + errors+=("phpunit command failed") +fi + +# Stop test servers. +for pid in "${pids[@]}"; do + kill "${pid}" &> /dev/null & +done diff --git a/tests/run_static_analysis_check_phpstan.sh b/tests/run_static_analysis_check_phpstan.sh new file mode 100755 index 0000000000..970a71d53f --- /dev/null +++ b/tests/run_static_analysis_check_phpstan.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +source "set_vars.inc.sh" + +# Run commands from the project root directory. +pushd .. + +set -x + +if true; then + + phpstan_args=(--ansi --configuration="tests/phpstan.neon") + + # Increase memory limit on local development. + if [[ "${CI}" != "true" ]]; then + phpstan_args+=(--memory-limit=256M) + fi + + vendor/bin/phpstan --version + vendor/bin/phpstan analyse "${phpstan_args[@]}" . + if [[ "${?}" -ne 0 ]]; then + echo "❌ Error: phpstan static analysis check failed" + errors+=("phpstan static analysis check failed") + fi +else + echo "⚠️ Skipped running phpstan check" + warnings+=("Skipped running phpstan check") +fi + +popd diff --git a/tests/run_static_analysis_check_psalm.sh b/tests/run_static_analysis_check_psalm.sh new file mode 100755 index 0000000000..2ed25a28ef --- /dev/null +++ b/tests/run_static_analysis_check_psalm.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +source "set_vars.inc.sh" + +# Run commands from the project root directory. +pushd .. + +set -x + +if [[ ! -f "vendor/bin/psalm" ]]; then + echo "⚠️ Skipped running psalm static analysis check" + warnings+=("Skipped running psalm static analysis check") +else + vendor/bin/psalm --version + vendor/bin/psalm --config="tests/psalm.xml" + + if [[ "${?}" -ne 0 ]]; then + echo "❌ Error: psalm static analysis check failed" + errors+=("psalm static analysis check failed") + fi +fi + +popd diff --git a/tests/run_syntax_check.sh b/tests/run_syntax_check.sh new file mode 100755 index 0000000000..01a98868ab --- /dev/null +++ b/tests/run_syntax_check.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +# Run commands from the project root directory. +pushd .. + +# Check syntax in php files. Use `xargs' over `find -exec' as xargs exits with a value of 1 when any command errors. +find . -type "f" -iname "*.php" ! -path "*/vendor/*" | xargs -L "1" php -l +if [[ "${?}" -ne 0 ]]; then + echo "❌ Error: php syntax checks failed" + errors+=("php syntax checks failed") +fi + +popd diff --git a/tests/script.sh b/tests/script.sh deleted file mode 100755 index 4e298bb03d..0000000000 --- a/tests/script.sh +++ /dev/null @@ -1,86 +0,0 @@ -# Check syntax in php files. -find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec php -l {} \; - -# Run tests. -phpunit --configuration tests/phpunit.xml -if [[ "${?}" -ne 0 ]]; then - exit 1 -fi - -# Enforce line ending consistency in php files. -crlf_file=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec grep --files-with-matches $'\r' {} \;) -if [[ ! -z "${crlf_file}" ]]; then - echo "${crlf_file}" | perl -pe 's/(.*)/CRLF line terminators found in \1/' - exit 1 -fi - -# Enforce indentation character consistency in php files. -tab_char=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec grep --line-number -H --perl-regexp "\t" {} \;) -if [[ ! -z "${tab_char}" ]]; then - echo -e "${tab_char}" | perl -pe 's/^(.*)$/Tab character found in \1/' - exit 1 -fi - -# Enforce indentation consistency in php files. -find_invalid_indentation() { - filename="${1}" - script=$(cat <<'EOF' - $filename = $argv['1']; - $lines = explode("\n", file_get_contents($filename)); - $line_number = 0; - foreach ($lines as $line) { - $line_number += 1; - $leading_space_count = strspn($line, ' '); - $remainder = $leading_space_count % 4; - if (!($remainder === 0)) { - // Allow doc comments. - if (substr(ltrim($line), 0, 1) === '*') { - continue; - } - $add_count = 4 - $remainder; - $remove_count = $remainder; - echo 'Invalid indentation found in ' . $filename . ':' . $line_number . - ' (' . $leading_space_count . ':+' . $add_count . '/-' . $remove_count . ')' . "\n"; - } - } -EOF -) - php --run "${script}" "${filename}" -} -# Skip hhvm "Notice: File could not be loaded: ..." -if [[ "${TRAVIS_PHP_VERSION}" != "hhvm" ]]; then - export -f "find_invalid_indentation" - invalid_indentation=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec bash -c 'find_invalid_indentation "{}"' \;) - if [[ ! -z "${invalid_indentation}" ]]; then - echo "${invalid_indentation}" - exit 1 - fi -fi - -# Prohibit trailing whitespace in php files. -trailing_whitespace=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec egrep --line-number -H " +$" {} \;) -if [[ ! -z "${trailing_whitespace}" ]]; then - echo -e "${trailing_whitespace}" | perl -pe 's/^(.*)$/Trailing whitespace found in \1/' - exit 1 -fi - -# Prohibit long lines in php files. -long_lines=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec awk '{print FILENAME":"NR" "length}' {} \; | awk '$2 > 120') -if [[ ! -z "${long_lines}" ]]; then - echo -e "${long_lines}" | perl -pe 's/^(.*)$/Long lines found in \1/' - exit 1 -fi - -# Prohibit @author in php files. -at_author=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec egrep --line-number -H "@author" {} \;) -if [[ ! -z "${at_author}" ]]; then - echo -e "${at_author}" | perl -pe 's/^(.*)$/\@author found in \1/' - exit 1 -fi - -# Prohibit screaming caps notation in php files. -caps=$(find . -type "f" -iname "*.php" ! -path "*/vendor/*" -exec egrep --color=always --line-number -H -e "FALSE[^']" -e "NULL" -e "TRUE" {} \;) -if [[ ! -z "${caps}" ]]; then - echo -e "${caps}" | perl -pe 's/^(.*)$/All caps found in \1/' - exit 1 -fi diff --git a/tests/server.php b/tests/server.php new file mode 100644 index 0000000000..ceb7da4414 --- /dev/null +++ b/tests/server.php @@ -0,0 +1,462 @@ + $_SERVER['PHP_AUTH_USER'], + 'password' => $_SERVER['PHP_AUTH_PW'], + ]); + exit; +} elseif ($test === 'http_digest_auth') { + $users = [ + 'myusername' => 'mypassword', + ]; + + $realm = 'Restricted area'; + $qop = 'auth'; + $nonce = md5(uniqid()); + $opaque = md5(uniqid()); + if (empty($_SERVER['PHP_AUTH_DIGEST'])) { + header('HTTP/1.1 401 Unauthorized'); + header(sprintf( + 'WWW-Authenticate: Digest realm="%s", qop="%s", nonce="%s", opaque="%s"', + $realm, + $qop, + $nonce, + $opaque + )); + echo 'canceled'; + exit; + } + + $data = [ + 'nonce' => '', + 'nc' => '', + 'cnonce' => '', + 'qop' => '', + 'username' => '', + 'uri' => '', + 'response' => '', + ]; + preg_match_all( + '@(' . implode('|', array_keys($data)) . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', + $_SERVER['PHP_AUTH_DIGEST'], + $matches, + PREG_SET_ORDER + ); + foreach ($matches as $match) { + $data[$match['1']] = $match['3'] ? $match['3'] : $match['4']; + } + + $A1 = md5($data['username'] . ':' . $realm . ':' . $users[$data['username']]); + $A2 = md5($_SERVER['REQUEST_METHOD'] . ':' . $data['uri']); + $valid_response = md5( + $A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2 + ); + + if ($data['response'] !== $valid_response) { + header('HTTP/1.1 401 Unauthorized'); + echo 'invalid'; + exit; + } + + echo 'valid'; + exit; +} elseif ($test === 'get') { + echo http_build_query($_GET); + exit; +} elseif ($test === 'post') { + echo http_build_query($_POST); + exit; +} elseif ($test === 'post_json') { + echo $http_raw_post_data; + exit; +} elseif ($test === 'put') { + echo $http_raw_post_data; + exit; +} elseif ($test === 'patch') { + echo $http_raw_post_data; + exit; +} elseif ($test === 'search') { + echo $http_raw_post_data; + exit; +} elseif ($test === 'post_multidimensional' || $test === 'post_multidimensional_with_file') { + header('Content-Type: application/json'); + echo json_encode([ + 'post' => $_POST, + 'files' => $_FILES, + ]); + exit; +} elseif ($test === 'post_file_path_upload') { + echo \Helper\mime_type($_FILES[$key]['tmp_name']); + exit; +} elseif ($test === 'put_file_handle') { + $tmp_filename = tempnam('/tmp', 'php-curl-class.'); + file_put_contents($tmp_filename, $http_raw_post_data); + echo \Helper\mime_type($tmp_filename); + unlink($tmp_filename); + exit; +} elseif ($test === 'request_method') { + header('X-REQUEST-METHOD: ' . $request_method); + echo $request_method; + exit; +} elseif ($test === 'request_uri') { + echo $_SERVER['REQUEST_URI']; + exit; +} elseif ($test === 'setcookie') { + foreach ($_COOKIE as $key => $value) { + setcookie($key, $value); + } + exit; +} elseif ($test === 'cookiejar') { + setcookie('mycookie', 'yum'); + exit; +} elseif ($test === 'multiple_cookie') { + setcookie('cookie1', 'scrumptious'); + setcookie('cookie2', 'mouthwatering'); + exit; +} elseif ($test === 'response_header') { + header('Content-Type: application/json'); + header('ETag: ' . md5('worldpeace')); + exit; +} elseif ($test === 'response_body') { + echo 'OK'; + exit; +} elseif ($test === 'json_response') { + if (isset($_POST['headers'])) { + foreach ($_POST['headers'] as $header) { + header($header); + } + } else { + if (isset($_POST['key'])) { + $key = $_POST['key']; + } elseif (isset($_GET['key'])) { + $key = $_GET['key']; + } else { + $key = 'Content-Type'; + } + + if (isset($_POST['value'])) { + $value = $_POST['value']; + } elseif (isset($_GET['value'])) { + $value = $_GET['value']; + } else { + $value = 'application/json'; + } + + header($key . ': ' . $value); + } + + if (isset($_POST['body'])) { + $body = $_POST['body']; + } else { + $body = json_encode([ + 'null' => null, + 'true' => true, + 'false' => false, + 'integer' => 1, + 'float' => 3.14, + 'empty' => '', + 'string' => 'string', + ]); + } + + if (isset($_POST['remove-content-type-header'])) { + header_remove('Content-Type'); + } + + echo $body; + exit; +} elseif ($test === 'xml_response') { + $key = $_POST['key']; + $value = $_POST['value']; + header($key . ': ' . $value); + $doc = new DOMDocument(); + $doc->formatOutput = true; + $rss = $doc->appendChild($doc->createElement('rss')); + $rss->setAttribute('version', '2.0'); + $channel = $doc->createElement('channel'); + $title = $doc->createElement('title'); + $title->appendChild($doc->createTextNode('Title')); + $channel->appendChild($title); + $link = $doc->createElement('link'); + $link->appendChild($doc->createTextNode('Link')); + $channel->appendChild($link); + $description = $doc->createElement('description'); + $description->appendChild($doc->createTextNode('Description')); + $channel->appendChild($description); + $rss->appendChild($channel); + echo $doc->saveXML(); + exit; +} elseif ($test === 'xml_with_cdata_response') { + header('Content-Type: text/xml'); + echo ' + + + + 1 + 33ee7e1eb504b6619c1b445ca1442c21 + <![CDATA[The Title]]> + + + + + 2 + b5c0b187fe309af0f4d35982fd961d7e + <![CDATA[Another Title]]> + + + + +'; + exit; +} elseif ($test === 'upload_response') { + $tmp_filename = tempnam('/tmp', 'php-curl-class.'); + move_uploaded_file($_FILES['image']['tmp_name'], $tmp_filename); + header('Content-Type: application/json'); + header('ETag: ' . md5_file($tmp_filename)); + echo json_encode([ + 'file_path' => $tmp_filename, + ]); + exit; +} elseif ($test === 'upload_cleanup') { + $unsafe_file_path = $_POST['file_path']; + echo var_export(unlink($unsafe_file_path), true); + exit; +} elseif ($test === 'download_response') { + $unsafe_file_path = $_GET['file_path']; + header('Content-Type: image/png'); + header('Content-Disposition: attachment; filename="image.png"'); + + if (!isset($_SERVER['HTTP_RANGE'])) { + header('ETag: ' . md5_file($unsafe_file_path)); + } + + $server = new ContentRangeServer\ContentRangeServer(); + $server->serve($unsafe_file_path); + exit; +} elseif ($test === 'download_file_size') { + if (isset($_GET['http_response_code'])) { + http_response_code((int) $_GET['http_response_code']); + } + $bytes = $_GET['bytes'] ?? 1234; + $str = str_repeat('.', (int) $bytes); + header('Content-Type: application/octet-stream'); + header('Content-Length: ' . strlen($str)); + header('ETag: ' . md5($str)); + echo $str; + exit; +} elseif ($test === 'download_file_range') { + $unsafe_file_path = $_GET['file_path']; + $server = new ContentRangeServer\ContentRangeServer(); + $server->serve($unsafe_file_path); + exit; +} elseif ($test === 'timeout') { + // Use --no-buffer to view loading indicator (e.g. + // curl --header "X-DEBUG-TEST: timeout" --include --no-buffer 127.0.0.1:8000/?seconds=3). + header('Content-Type: application/json'); + $unsafe_seconds = (int)$_GET['seconds']; + $start = microtime(true); + echo '{' . "\n"; + echo ' "loading": "'; + + $dots_printed = 0; + while (true) { + usleep(1_000_000 / 100); + + $elapsed = microtime(true) - $start; + $dots_to_print = floor($elapsed) - $dots_printed; + + if ($dots_to_print) { + echo str_repeat('.', (int) $dots_to_print); + $dots_printed += $dots_to_print; + } + + if ($elapsed >= $unsafe_seconds) { + break; + } + } + + echo '",' . "\n"; + echo ' "elapsed_seconds": "' . $elapsed . '",' . "\n"; + echo ' "server_port": "' . ((int)$_SERVER['SERVER_PORT']) . '",' . "\n"; + echo ' "server_start": "' . $server_start . '",' . "\n"; + echo ' "server_stop": "' . microtime(true) . '"' . "\n"; + echo '}' . "\n"; + exit; +} elseif ($test === 'error_message') { + // 401 Unauthorized. + http_response_code(401); + exit; +} elseif ($test === 'redirect') { + if (!isset($_GET['redirect'])) { + header('Location: ?redirect'); + exit; + } + + echo 'OK'; + exit; +} elseif ($test === 'delete_with_body') { + header('Content-Type: application/json'); + echo json_encode([ + 'get' => $_GET, + 'delete' => $_DELETE, + ]); + exit; +} elseif ($test === 'data_values') { + header('Content-Type: application/json'); + echo json_encode($data_values); + exit; +} elseif ($test === 'post_redirect_get') { + if (isset($_GET['redirect'])) { + echo 'Redirected: ' . $request_method; + } else { + if ($request_method === 'POST') { + // 303 See Other. + http_response_code(303); + + header('Location: ?redirect'); + } else { + echo 'Request method is ' . $request_method . ', but POST was expected'; + } + } + + exit; +} elseif ($test === 'retry') { + session_start(); + + if (isset($_SESSION['should_fail_entries'])) { + $should_fail_entries = $_SESSION['should_fail_entries']; + } else { + // Support specifying which requests fail and succeed (e.g. + // http://127.0.0.1:8000/?failures=1,1,0 to fail, fail, succeed). + if (strpos($_GET['failures'], ',') !== false) { + $should_fail_entries = explode(',', $_GET['failures']); + array_walk($should_fail_entries, function (&$value, $key) { + if ($value === '1') { + $value = true; + } elseif ($value === '0') { + $value = false; + } else { + $value = ''; + } + }); + + // Support specifying the number of failures before a success (e.g. + // http://127.0.0.1:8000/?failures=3). + } else { + $failure_count = (int)$_GET['failures']; + $should_fail_entries = array_fill(0, $failure_count, true); + $should_fail_entries[] = false; + } + } + + $should_fail = array_shift($should_fail_entries); + $_SESSION['should_fail_entries'] = $should_fail_entries; + + if ($should_fail) { + $message = '503 Service Unavailable'; + } else { + $message = '202 Accepted'; + } + + $response = json_encode([ + 'message' => $message, + 'remaining_should_fail_entries' => $_SESSION['should_fail_entries'], + ], JSON_PRETTY_PRINT); + + header('HTTP/1.1 ' . $message); + header('Content-Type: application/json'); + header('Content-Length: ' . strlen($response)); + echo $response; + exit; +} elseif ($test === '404') { + header('HTTP/1.1 404 Not Found'); + echo '404 Not Found'; + exit; +} + +header('Content-Type: text/plain'); + +$data_mapping = [ + 'cookie' => $_COOKIE, + 'delete' => $_GET, + 'get' => $_GET, + 'patch' => $_PATCH, + 'post' => $_POST, + 'put' => $_PUT, + 'server' => $_SERVER, +]; + +if (!empty($test)) { + $data = $data_mapping[$test]; + if (empty($key)) { + // Return all values when a key is not specified. + $value = http_build_query($data); + } else { + // Return individual value when a key is specified. + $value = $data[$key] ?? ''; + } + echo $value; +} diff --git a/tests/set_vars.inc.sh b/tests/set_vars.inc.sh new file mode 100644 index 0000000000..dc33f7b01e --- /dev/null +++ b/tests/set_vars.inc.sh @@ -0,0 +1,4 @@ +# Use installed version when variable not set. +if [[ -z "${CI_PHP_VERSION}" ]]; then + CI_PHP_VERSION="$(php -r "echo preg_replace('/^([0-9]+\.[0-9]+)\.[0-9]+/', '\$1', phpversion());")" +fi diff --git a/tests/setup.cfg b/tests/setup.cfg new file mode 100644 index 0000000000..7da1f9608e --- /dev/null +++ b/tests/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 100 diff --git a/tests/skip_slow_tests.sh b/tests/skip_slow_tests.sh new file mode 100755 index 0000000000..9ba74745ee --- /dev/null +++ b/tests/skip_slow_tests.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +# Let tests know we should skip slow tests. +export PHP_CURL_CLASS_SKIP_SLOW_TESTS="1" + +source "run.sh" diff --git a/tests/test_all.sh b/tests/test_all.sh new file mode 100755 index 0000000000..118e747ecc --- /dev/null +++ b/tests/test_all.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SCRIPT_DIR}" + +find . -type d -mindepth 2 -path "*/dockerfiles/*" | sort --reverse | while read directory; do + printf '=%.0s' {1..80} + echo -e "\nRunning ${directory}" + pushd "${directory}" + + bash "1_build.sh" + if [[ $? -ne 0 ]]; then + echo "Error: Build failed for ${directory}" + exit 1 + fi + + bash "2_start.sh" + if [[ $? -ne 0 ]]; then + echo "Error: Start failed for ${directory}" + exit 1 + fi + + bash "3_test.sh" + if [[ $? -ne 0 ]]; then + echo "Error: Test failed for ${directory}" + exit 1 + fi + + bash "4_stop.sh" + if [[ $? -ne 0 ]]; then + echo "Error: Stop failed for ${directory}" + exit 1 + fi + + popd +done diff --git a/www/Dockerfile b/www/Dockerfile new file mode 100644 index 0000000000..ae723cabfe --- /dev/null +++ b/www/Dockerfile @@ -0,0 +1,65 @@ +# Use phusion/baseimage with a specific version as base image. +# Pick a version from the releases page: +# https://github.com/phusion/baseimage-docker/releases +FROM phusion/baseimage:0.11 + +# Use baseimage-docker's init system. +CMD ["/sbin/my_init"] + +# BEGIN Build instructions ============================================================================================= + +# Install nginx mainline. +# "We recommend that in general you deploy the NGINX mainline branch at all times." - nginx.com +RUN add-apt-repository -y ppa:nginx/development +RUN apt-get update +RUN apt-get -y install nginx +RUN echo "nginx version: $(nginx -v)" +RUN echo '\ +server {\n\ + listen 80 default_server;\n\ + listen [::]:80 default_server;\n\ +\n\ + root /var/www;\n\ + index index.php;\n\ +\n\ + charset utf-8;\n\ +\n\ + server_name _;\n\ + server_tokens off;\n\ +\n\ + location / {\n\ + try_files $uri $uri/ =404;\n\ + }\n\ +\n\ + location ~ \.php$ {\n\ + include snippets/fastcgi-php.conf;\n\ + fastcgi_pass unix:/run/php/php7.3-fpm.sock;\n\ + }\n\ +}'\ +> /etc/nginx/sites-enabled/default +# Fix: "nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)". +RUN echo "daemon off;" >> /etc/nginx/nginx.conf + +# Install PHP. +RUN add-apt-repository ppa:ondrej/php +# Avoid "debconf: unable to initialize frontend: Dialog" by using DEBIAN_FRONTEND=noninteractive before install command. +RUN DEBIAN_FRONTEND=noninteractive apt-get -y install php7.3-fpm +RUN echo "php version: $(php -v)" + +# Add nginx daemon. +RUN mkdir /etc/service/nginx +RUN echo '#!/usr/bin/env bash\nnginx' > /etc/service/nginx/run +RUN chmod +x /etc/service/nginx/run + +# Add php-fpm daemon. +RUN mkdir /etc/service/php-fpm +RUN echo '#!/usr/bin/env bash\nservice php7.3-fpm start' > /etc/service/php-fpm/run +RUN chmod +x /etc/service/php-fpm/run + +# Add homepage. +ADD index.php /var/www/ + +# END Build instructions =============================================================================================== + +# Clean up APT when done. +RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/www/build.sh b/www/build.sh new file mode 100755 index 0000000000..d2ea314118 --- /dev/null +++ b/www/build.sh @@ -0,0 +1,3 @@ +docker build \ + --tag php-curl-class/php-curl-class:latest \ + . diff --git a/www/img/screencast.gif b/www/img/screencast.gif new file mode 100644 index 0000000000..9f58b50b4a Binary files /dev/null and b/www/img/screencast.gif differ diff --git a/www/index.php b/www/index.php new file mode 100644 index 0000000000..84fe67b2e1 --- /dev/null +++ b/www/index.php @@ -0,0 +1,293 @@ + + + + +PHP Curl Class + + + + + + + + + +

    PHP Curl Class

    +

    Easily send HTTP requests and integrate with web APIs

    + +
    +
    +$curl = new Curl();
    +$curl->get('https://www.example.com/');
    +
    +if ($curl->error) {
    +    echo 'Error: ' . $curl->errorMessage . "\n";
    +    $curl->diagnose();
    +} else {
    +    echo 'Success! Here is the response:' . "\n";
    +    var_dump($curl->response);
    +}
    +
    +    
    +
    + +

    + + https://github.com/php-curl-class/php-curl-class + +

    + +

    + + + + + + + + + + + + + + + +

    + +

    +    +

    + + + diff --git a/www/run.sh b/www/run.sh new file mode 100755 index 0000000000..50a8ee4e7b --- /dev/null +++ b/www/run.sh @@ -0,0 +1,5 @@ +docker run \ + --detach \ + --name www.phpcurlclass.com \ + -p 8001:80 \ + php-curl-class/php-curl-class:latest diff --git a/www/scripts/.gitignore b/www/scripts/.gitignore new file mode 100644 index 0000000000..504afef81f --- /dev/null +++ b/www/scripts/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/www/scripts/code.php b/www/scripts/code.php new file mode 100644 index 0000000000..c2f834d603 --- /dev/null +++ b/www/scripts/code.php @@ -0,0 +1,12 @@ +get('https://www.example.com/'); + +if ($curl->error) { + echo 'Error: ' . $curl->errorMessage . "\n"; + $curl->diagnose(); +} else { + echo 'Success! Here is the response:' . "\n"; + var_dump($curl->response); +} diff --git a/www/scripts/highlight_code.js b/www/scripts/highlight_code.js new file mode 100644 index 0000000000..f0f7b76abb --- /dev/null +++ b/www/scripts/highlight_code.js @@ -0,0 +1,11 @@ +const fs = require('fs'); +const Prism = require('prismjs'); +const loadLanguages = require('prismjs/components/'); +loadLanguages(['php']); + +const source = fs.readFileSync('code.php', 'utf8'); +const lines = source.split(/\n/); +const code = lines.slice(1).join('\n'); +const html = Prism.highlight(code, Prism.languages.php, 'php'); + +console.log(html); diff --git a/www/scripts/highlight_code.sh b/www/scripts/highlight_code.sh new file mode 100755 index 0000000000..0aacac7b64 --- /dev/null +++ b/www/scripts/highlight_code.sh @@ -0,0 +1,2 @@ +npm install prismjs +node highlight_code.js diff --git a/www/scripts/package.json b/www/scripts/package.json new file mode 100644 index 0000000000..43c0c0963d --- /dev/null +++ b/www/scripts/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "prismjs": "^1.30.0" + } +}