├── .editorconfig ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── boring-cyborg.yml ├── dependabot.yml ├── settings.yml └── workflows │ ├── ci.yml │ ├── craft-release.yaml │ ├── label-sponsors.yml │ └── set-milestone-on-pr.yaml ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── composer-require-checker.json ├── composer.json ├── composer.lock ├── etc └── qa │ ├── .phpunit.result.cache │ ├── composer-require-checker.json │ ├── phpcs.xml │ ├── phpstan.neon │ ├── phpunit.xml │ └── psalm.xml ├── infection.json.dist ├── src ├── Definition │ ├── AccessToken.php │ ├── ConsumerKey.php │ ├── ConsumerSecret.php │ ├── RequestToken.php │ └── TokenSecret.php ├── RequestSigning │ └── RequestSigner.php └── Signature │ ├── HmacMd5Signature.php │ ├── HmacSha1Signature.php │ ├── HmacSha256Signature.php │ ├── HmacSha384Signature.php │ ├── HmacSha512Signature.php │ ├── HmacSignature.php │ └── Signature.php └── var └── .gitkeep /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 4 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.json] 11 | indent_size = 2 12 | 13 | [Makefile] 14 | indent_style = tab 15 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @WyriHaximus 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: WyriHaximus 2 | -------------------------------------------------------------------------------- /.github/boring-cyborg.yml: -------------------------------------------------------------------------------- 1 | labelPRBasedOnFilePath: 2 | "Documentation 📚": 3 | - README.md 4 | - CONTRIBUTING.md 5 | "Dependencies 📦": 6 | - Dockerfile* 7 | - composer.* 8 | - package.json 9 | - package-lock.json 10 | - yarn.lock 11 | "Docker 🐳": 12 | - Dockerfile* 13 | - .docker/**/* 14 | "Image 🖼": 15 | - "**/*.gif" 16 | - "**/*.jpg" 17 | - "**/*.jpeg" 18 | - "**/*.png" 19 | - "**/*.webp" 20 | "CSS 👩‍🎨": 21 | - "**/*.css" 22 | "HTML 👷‍♀️": 23 | - "**/*.htm" 24 | - "**/*.html" 25 | "NEON 🦹‍♂️": 26 | - "**/*.neon" 27 | "MarkDown 📝": 28 | - "**/*.md" 29 | "YAML 🍄": 30 | - "**/*.yml" 31 | - "**/*.yaml" 32 | "JSON 👨‍💼": 33 | - "**/*.json" 34 | "Go 🐹": 35 | - "**/*.go" 36 | "JavaScript 🦏": 37 | - "**/*.js" 38 | - package.json 39 | - package-lock.json 40 | - yarn.lock 41 | "PHP 🐘": 42 | - "**/*.php" 43 | - composer.* 44 | "Configuration ⚙": 45 | - .github/* 46 | "CI 🚧": 47 | - .github/workflows/* 48 | - .scrutinizer.yml 49 | "Templates 🌲": 50 | - "**/*.twig" 51 | - "**/*.tpl" 52 | "Helm ☸": 53 | - .helm/**/* 54 | "Tests 🧪": 55 | - tests/**/* 56 | "Source 🔮": 57 | - src/**/* 58 | 59 | labelerFlags: 60 | labelOnPRUpdates: true 61 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "composer" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "Dependencies 📦" 9 | - "PHP 🐘" 10 | versioning-strategy: "widen" 11 | open-pull-requests-limit: 1 12 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | private: false 3 | has_issues: true 4 | has_wiki: false 5 | has_downloads: true 6 | default_branch: master 7 | allow_squash_merge: false 8 | allow_merge_commit: true 9 | allow_rebase_merge: false 10 | 11 | # Labels: define labels for Issues and Pull Requests 12 | labels: 13 | - name: "Dependencies 📦" 14 | color: 0025ff 15 | description: "Pull requests that update a dependency file" 16 | - name: "Image 🖼" 17 | color: 00ffff 18 | - name: "HTML 👷‍♀️" 19 | color: ffffff 20 | - name: "CSS 👩‍🎨" 21 | color: b3b3b3 22 | - name: "JavaScript 🦏" 23 | color: ffff00 24 | - name: "Go 🐹" 25 | color: 00ADD8 26 | - name: "JSON 👨‍💼" 27 | color: 00ADD8 28 | - name: "NEON 🦹‍♂️" 29 | color: CE3262 30 | - name: "MarkDown 📝" 31 | color: 000000 32 | - name: "YAML 🍄" 33 | color: ff1aff 34 | - name: "Templates 🌲" 35 | color: 009933 36 | - name: "Helm ☸" 37 | color: 091C84 38 | - name: "Tests 🧪" 39 | color: ffe6e6 40 | - name: "Source 🔮" 41 | color: e6ffe6 42 | - name: "Configuration ⚙" 43 | color: b3b3cc 44 | - name: "PHP 🐘" 45 | color: 8892BF 46 | description: "Hypertext Pre Processor" 47 | - name: "Docker 🐳" 48 | color: 0db7ed 49 | description: "Pull requests that relate to Docker" 50 | - name: "CI 🚧" 51 | color: ffff00 52 | - name: "Feature 🏗" 53 | color: 66ff99 54 | - name: "Documentation 📚" 55 | color: 6666ff 56 | - name: "Security 🕵️‍♀️" 57 | color: ff0000 58 | - name: "Hacktoberfest 🎃" 59 | color: 152347 60 | - name: "Bug 🐞" 61 | color: d73a4a 62 | description: "Something isn't working" 63 | oldname: bug 64 | - name: "Duplicate ♊" 65 | color: cfd3d7 66 | description: "This issue or pull request already exists" 67 | oldname: duplicate 68 | - name: "Enhancement ✨" 69 | color: a2eeef 70 | description: "New feature or request" 71 | oldname: enhancement 72 | - name: "Good First Issue" 73 | color: 7057ff 74 | description: "Good for newcomers" 75 | oldname: "good first issue" 76 | - name: "Help Wanted" 77 | color: 008672 78 | description: "Extra attention is needed" 79 | oldname: "help wanted" 80 | - name: Invalid 81 | color: e4e669 82 | description: "This doesn't seem right" 83 | oldname: invalid 84 | - name: "Question ❓" 85 | color: d876e3 86 | description: "Further information is requested" 87 | oldname: question 88 | - name: "Will not be fixed 🛑" 89 | color: ffffff 90 | description: "This will not be worked on" 91 | oldname: wontfix 92 | - name: "Sponsor Request ❤️" 93 | color: fedbf0 94 | description: "Issue/PR opened by sponsor" 95 | 96 | branches: 97 | - name: master 98 | protection: 99 | required_pull_request_reviews: 100 | required_approving_review_count: 1 101 | dismiss_stale_reviews: true 102 | require_code_owner_reviews: true 103 | # Required. Require status checks to pass before merging. Set to null to disable 104 | required_status_checks: 105 | # Required. Require branches to be up to date before merging. 106 | strict: true 107 | # Required. The list of status checks to require in order to merge into this branch 108 | contexts: [] 109 | # Required. Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable. 110 | enforce_admins: true 111 | # Required. Restrict who can push to this branch. Team and user restrictions are only available for organization-owned repositories. Set to null to disable. 112 | restrictions: 113 | apps: [] 114 | users: [] 115 | teams: [] 116 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: 5 | - 'main' 6 | - 'master' 7 | - 'refs/heads/v[0-9]+.[0-9]+.[0-9]+' 8 | pull_request: 9 | jobs: 10 | supported-versions-matrix: 11 | name: Supported Versions Matrix 12 | runs-on: ubuntu-latest 13 | needs: 14 | - lint-yaml 15 | - lint-json 16 | outputs: 17 | version: ${{ steps.supported-versions-matrix.outputs.version }} 18 | steps: 19 | - uses: actions/checkout@v2 20 | - id: supported-versions-matrix 21 | uses: WyriHaximus/github-action-composer-php-versions-in-range@v1 22 | supported-checks-matrix: 23 | name: Supported Checks Matrix 24 | runs-on: ubuntu-latest 25 | needs: 26 | - lint-yaml 27 | - composer-install 28 | outputs: 29 | check: ${{ steps.supported-checks-matrix.outputs.check }} 30 | steps: 31 | - uses: actions/checkout@v2 32 | - id: supported-checks-matrix 33 | name: Generate check 34 | run: | 35 | printf "Checks found: %s\r\n" $(make task-list-ci) 36 | printf "::set-output name=check::%s" $(make task-list-ci) 37 | composer-install: 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} 42 | composer: [lowest, current, highest] 43 | needs: 44 | - lint-yaml 45 | - lint-json 46 | - supported-versions-matrix 47 | runs-on: ubuntu-latest 48 | container: 49 | image: ghcr.io/wyrihaximusnet/php:${{ matrix.php }}-nts-buster-dev-root 50 | steps: 51 | - uses: actions/checkout@v2 52 | - name: Cache composer packages 53 | uses: actions/cache@v1 54 | with: 55 | path: ./vendor/ 56 | key: ${{ matrix.composer }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} 57 | - name: Install Dependencies 58 | run: composer update --prefer-lowest --no-progress --ansi --no-interaction --prefer-dist -o 59 | if: matrix.composer == 'lowest' 60 | - name: Install Dependencies 61 | run: composer install --ansi --no-progress --no-interaction --prefer-dist -o 62 | if: matrix.composer == 'current' 63 | - name: Install Dependencies 64 | run: composer update --ansi --no-progress --no-interaction --prefer-dist -o 65 | if: matrix.composer == 'highest' 66 | qa: 67 | name: Run ${{ matrix.check }} on PHP ${{ matrix.php }} with ${{ matrix.composer }} dependency preference (Linux) 68 | strategy: 69 | fail-fast: false 70 | matrix: 71 | php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} 72 | composer: [lowest, current, highest] 73 | check: ${{ fromJson(needs.supported-checks-matrix.outputs.check) }} 74 | needs: 75 | - lint-yaml 76 | - lint-json 77 | - composer-install 78 | - supported-checks-matrix 79 | - supported-versions-matrix 80 | runs-on: ubuntu-latest 81 | container: 82 | image: ghcr.io/wyrihaximusnet/php:${{ matrix.php }}-nts-buster-dev-root 83 | steps: 84 | - uses: actions/checkout@v2 85 | - name: Cache composer packages 86 | uses: actions/cache@v1 87 | with: 88 | path: ./vendor/ 89 | key: ${{ matrix.composer }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} 90 | - name: Install Dependencies 91 | run: (test -f vendor && true ) || composer update --prefer-lowest --no-progress --ansi --no-interaction --prefer-dist -o 92 | if: matrix.composer == 'lowest' 93 | - name: Install Dependencies 94 | run: (test -f vendor && true ) || composer install --ansi --no-progress --no-interaction --prefer-dist -o 95 | if: matrix.composer == 'current' 96 | - name: Install Dependencies 97 | run: (test -f vendor && true ) || composer update --ansi --no-progress --no-interaction --prefer-dist -o 98 | if: matrix.composer == 'highest' 99 | - name: Fetch Tags 100 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 101 | if: matrix.check == 'backward-compatibility-check' 102 | - run: make ${{ matrix.check }} 103 | env: 104 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 105 | COVERALLS_RUN_LOCALLY: ${{ secrets.COVERALLS_RUN_LOCALLY }} 106 | unittests-directly-on-os: 107 | name: Run unit tests on PHP ${{ matrix.php }} with ${{ matrix.composer }} dependency preference (${{ matrix.os }}) 108 | strategy: 109 | fail-fast: false 110 | matrix: 111 | os: [ubuntu-latest, windows-latest, macos-latest] 112 | php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} 113 | composer: [lowest, current, highest] 114 | needs: 115 | - lint-yaml 116 | - lint-json 117 | - composer-install 118 | - supported-versions-matrix 119 | runs-on: ${{ matrix.os }} 120 | steps: 121 | - uses: actions/checkout@v2 122 | - name: Setup PHP, extensions and composer with shivammathur/setup-php 123 | uses: shivammathur/setup-php@v2 124 | with: 125 | php-version: ${{ matrix.php }} 126 | coverage: xdebug, pcov 127 | extensions: intl, sodium 128 | - name: Cache composer packages 129 | uses: actions/cache@v1 130 | with: 131 | path: ./vendor/ 132 | key: ${{ matrix.composer }}-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }} 133 | - name: Install Dependencies 134 | run: (test -f vendor && true ) || composer update --prefer-lowest --no-progress --ansi --no-interaction --prefer-dist -o 135 | if: matrix.composer == 'lowest' 136 | - name: Install Dependencies 137 | run: (test -f vendor && true ) || composer install --ansi --no-progress --no-interaction --prefer-dist -o 138 | if: matrix.composer == 'current' 139 | - name: Install Dependencies 140 | run: (test -f vendor && true ) || composer update --ansi --no-progress --no-interaction --prefer-dist -o 141 | if: matrix.composer == 'highest' 142 | - name: Fetch Tags 143 | run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* || true 144 | if: matrix.check == 'backward-compatibility-check' 145 | - run: | 146 | ./vendor/bin/phpunit -c ./etc/qa/phpunit.xml 147 | env: 148 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 149 | COVERALLS_RUN_LOCALLY: ${{ secrets.COVERALLS_RUN_LOCALLY }} 150 | lint-yaml: 151 | name: Lint YAML 152 | runs-on: ubuntu-latest 153 | steps: 154 | - uses: actions/checkout@v2 155 | - name: yaml-lint 156 | uses: ibiqlik/action-yamllint@v3 157 | with: 158 | config_data: | 159 | extends: default 160 | ignore: | 161 | /.git/ 162 | rules: 163 | line-length: disable 164 | document-start: disable 165 | truthy: disable 166 | lint-json: 167 | name: Lint JSON 168 | runs-on: ubuntu-latest 169 | steps: 170 | - uses: actions/checkout@v2 171 | - name: json-syntax-check 172 | uses: limitusus/json-syntax-check@v1 173 | with: 174 | pattern: "\\.json$" 175 | check-mark: 176 | name: ✔️ 177 | needs: 178 | - lint-yaml 179 | - lint-json 180 | - qa 181 | - unittests-directly-on-os 182 | runs-on: ubuntu-latest 183 | steps: 184 | - run: echo "✔️" 185 | -------------------------------------------------------------------------------- /.github/workflows/craft-release.yaml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | env: 3 | MILESTONE: ${{ github.event.milestone.title }} 4 | on: 5 | milestone: 6 | types: 7 | - closed 8 | jobs: 9 | wait-for-status-checks: 10 | name: Wait for status checks 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - run: sleep 13 15 | - name: 'Wait for status checks' 16 | id: waitforstatuschecks 17 | uses: "WyriHaximus/github-action-wait-for-status@master" 18 | with: 19 | ignoreActions: "Wait for status checks" 20 | checkInterval: 5 21 | env: 22 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 23 | - id: generate-version-strategy 24 | if: steps.waitforstatuschecks.outputs.status != 'success' 25 | name: Fail 26 | run: exit 1 27 | generate-changelog: 28 | name: Generate Changelog 29 | needs: 30 | - wait-for-status-checks 31 | runs-on: ubuntu-latest 32 | outputs: 33 | changelog: ${{ steps.changelog.outputs.changelog }} 34 | steps: 35 | - name: Generate changelog 36 | uses: WyriHaximus/github-action-jwage-changelog-generator@v1 37 | id: changelog 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | milestone: ${{ env.MILESTONE }} 42 | - name: Show changelog 43 | run: echo "${CHANGELOG}" 44 | env: 45 | CHANGELOG: ${{ steps.changelog.outputs.changelog }} 46 | create-release: 47 | name: Create Release 48 | needs: 49 | - generate-changelog 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v2 53 | env: 54 | CHANGELOG: ${{ needs.generate-changelog.outputs.changelog }} 55 | - run: | 56 | echo -e "${MILESTONE_DESCRIPTION}\r\n\r\n${CHANGELOG}" > release-${{ env.MILESTONE }}-release-message.md 57 | cat release-${{ env.MILESTONE }}-release-message.md 58 | release_message=$(cat release-${{ env.MILESTONE }}-release-message.md) 59 | release_message="${release_message//'%'/'%25'}" 60 | release_message="${release_message//$'\n'/'%0A'}" 61 | release_message="${release_message//$'\r'/'%0D'}" 62 | echo "::set-output name=release_message::$release_message" 63 | id: releasemessage 64 | env: 65 | MILESTONE_DESCRIPTION: ${{ github.event.milestone.description }} 66 | CHANGELOG: ${{ needs.generate-changelog.outputs.changelog }} 67 | - name: Create Reference Release with Changelog 68 | uses: fleskesvor/create-release@feature/support-target-commitish 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | with: 72 | tag_name: ${{ env.MILESTONE }} 73 | release_name: ${{ env.MILESTONE }} 74 | body: ${{ steps.releasemessage.outputs.release_message }} 75 | draft: false 76 | prerelease: false 77 | -------------------------------------------------------------------------------- /.github/workflows/label-sponsors.yml: -------------------------------------------------------------------------------- 1 | name: Label sponsors ❤️ 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | issues: 7 | types: 8 | - opened 9 | jobs: 10 | sponsor-label: 11 | name: Label sponsors ❤️ 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: JasonEtco/is-sponsor-label-action@v1 15 | with: 16 | label: Sponsor Request ❤️ 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/set-milestone-on-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Set Milestone 2 | on: 3 | pull_request: 4 | types: 5 | - assigned 6 | - opened 7 | - synchronize 8 | - reopened 9 | - edited 10 | - ready_for_review 11 | - review_requested 12 | jobs: 13 | set-milestone: 14 | name: Set Milestone 15 | if: github.event.pull_request.milestone == null 16 | runs-on: ubuntu-latest 17 | outputs: 18 | check: ${{ steps.generate-checks-strategy.outputs.check }} 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: 'Get Previous tag' 22 | id: previoustag 23 | uses: "WyriHaximus/github-action-get-previous-tag@v1" 24 | env: 25 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 26 | - name: 'Get next versions' 27 | id: semvers 28 | uses: "WyriHaximus/github-action-next-semvers@v1" 29 | with: 30 | version: ${{ steps.previoustag.outputs.tag }} 31 | - name: 'Decide which version fits this PR' 32 | id: decidedversion 33 | run: | 34 | if [ "$(jq '.sender.id' -r ${GITHUB_EVENT_PATH})" = "49699333" ]; then 35 | printf "::set-output name=version::%s" "${PATCH}" 36 | exit 0 37 | fi 38 | 39 | composer install --no-progress --ansi --no-interaction --prefer-dist -o -q 40 | 41 | if ! (./vendor/bin/roave-backward-compatibility-check); then 42 | printf "::set-output name=version::%s" "${MAJOR}" 43 | exit 0 44 | fi 45 | 46 | printf "::set-output name=version::%s" "${MINOR}" 47 | env: 48 | MAJOR: ${{ steps.semvers.outputs.major }} 49 | MINOR: ${{ steps.semvers.outputs.minor }} 50 | PATCH: ${{ steps.semvers.outputs.patch }} 51 | - name: 'Get Milestones' 52 | uses: "WyriHaximus/github-action-get-milestones@master" 53 | id: milestones 54 | env: 55 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 56 | - run: printf "::set-output name=number::%s" $(printenv MILESTONES | jq --arg MILESTONE $(printenv MILESTONE) '.[] | select(.title == $MILESTONE) | .number') 57 | id: querymilestone 58 | env: 59 | MILESTONES: ${{ steps.milestones.outputs.milestones }} 60 | MILESTONE: ${{ steps.decidedversion.outputs.version }} 61 | - name: 'Create Milestone' 62 | if: steps.querymilestone.outputs.number == '' 63 | id: createmilestone 64 | uses: "WyriHaximus/github-action-create-milestone@0.1.0" 65 | with: 66 | title: ${{ steps.decidedversion.outputs.version }} 67 | env: 68 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 69 | - name: 'Select found or created Milestone' 70 | id: selectmilestone 71 | run: | 72 | if [ $(echo ${QUERY_NUMBER} | wc -c) -eq 1 ] ; then 73 | printf "::set-output name=number::%s" "${CREATED_NUMBER}" 74 | exit 0 75 | fi 76 | 77 | printf "::set-output name=number::%s" "${QUERY_NUMBER}" 78 | env: 79 | CREATED_NUMBER: ${{ steps.createmilestone.outputs.number }} 80 | QUERY_NUMBER: ${{ steps.querymilestone.outputs.number }} 81 | - name: 'Set Milestone' 82 | uses: "WyriHaximus/github-action-set-milestone@master" 83 | with: 84 | issue_number: ${{ github.event.pull_request.number }} 85 | milestone_number: ${{ steps.selectmilestone.outputs.number }} 86 | env: 87 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests are highly appreciated. Here's a quick guide. 4 | 5 | Fork, then clone the repo: 6 | 7 | git clone git@github.com:your-username/psr7-oauth1.git 8 | 9 | Set up your machine: 10 | 11 | composer install 12 | 13 | Make sure the tests pass: 14 | 15 | make unit 16 | 17 | Make your change. Add tests for your change. Make the tests pass: 18 | 19 | make unit 20 | 21 | Before committing and submitting your pull request make sure it passes PSR2 coding style, unit tests pass and pass on all supported PHP versions: 22 | 23 | make contrib 24 | 25 | Push to your fork and [submit a pull request][pr]. 26 | 27 | [pr]: https://help.github.com/articles/creating-a-pull-request/ 28 | 29 | At this point you're waiting on me. I like to at least comment on pull requests 30 | within a day or two. I may suggest some changes or improvements or alternatives. 31 | 32 | Some things that will increase the chance that your pull request is accepted: 33 | 34 | * Write tests. 35 | * Follow PSR2 (travis will also check for this). 36 | * Write a [good commit message][commit]. 37 | 38 | [commit]: http://chris.beams.io/posts/git-commit/ 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Cees-Jan Kiewiet & Beau Simensen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # set all to phony 2 | SHELL=bash 3 | 4 | .PHONY: * 5 | 6 | COMPOSER_CACHE_DIR=$(shell composer config --global cache-dir -q || echo ${HOME}/.composer-php/cache) 7 | COMPOSER_SHOW_EXTENSION_LIST=$(shell composer show -t | grep -o "\-\-\(ext-\).\+" | sort | uniq | cut -d- -f4- | tr -d '\n' | grep . | sed '/^$$/d' | xargs | sed -e 's/ /, /g' | tr -cd '[:alnum:],' | sed 's/.$$//') 8 | SLIM_DOCKER_IMAGE=$(shell php -r 'echo count(array_intersect(["gd", "vips"], explode(",", "${COMPOSER_SHOW_EXTENSION_LIST}"))) > 0 ? "" : "-slim";') 9 | 10 | ifneq ("$(wildcard /.you-are-in-a-wyrihaximus.net-php-docker-image)","") 11 | IN_DOCKER=TRUE 12 | else 13 | IN_DOCKER=FALSE 14 | endif 15 | 16 | ifeq ("$(IN_DOCKER)","TRUE") 17 | DOCKER_RUN:= 18 | else 19 | PHP_VERSION:=$(shell docker run --rm -v "`pwd`:`pwd`" jess/jq jq -r -c '.config.platform.php' "`pwd`/composer.json" | php -r "echo str_replace('|', '.', explode('.', implode('|', explode('.', stream_get_contents(STDIN), 2)), 2)[0]);") 20 | DOCKER_RUN:=docker run --rm -it \ 21 | -v "`pwd`:`pwd`" \ 22 | -v "${COMPOSER_CACHE_DIR}:/home/app/.composer/cache" \ 23 | -w "`pwd`" \ 24 | "ghcr.io/wyrihaximusnet/php:${PHP_VERSION}-nts-alpine${SLIM_DOCKER_IMAGE}-dev" 25 | endif 26 | 27 | ifneq (,$(findstring icrosoft,$(shell cat /proc/version))) 28 | THREADS=1 29 | else 30 | THREADS=$(shell nproc) 31 | endif 32 | 33 | all: ## Runs everything ### 34 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | grep -v "###" | awk 'BEGIN {FS = ":.*?## "}; {printf "%s\n", $$1}' | xargs --open-tty $(MAKE) 35 | 36 | syntax-php: ## Lint PHP syntax 37 | $(DOCKER_RUN) vendor/bin/parallel-lint --exclude vendor . 38 | 39 | cs-fix: ## Fix any automatically fixable code style issues 40 | $(DOCKER_RUN) vendor/bin/phpcbf --parallel=$(THREADS) --cache=./var/.phpcs.cache.json --standard=./etc/qa/phpcs.xml || $(DOCKER_RUN) vendor/bin/phpcbf --parallel=$(THREADS) --cache=./var/.phpcs.cache.json --standard=./etc/qa/phpcs.xml || $(DOCKER_RUN) vendor/bin/phpcbf --parallel=$(THREADS) --cache=./var/.phpcs.cache.json --standard=./etc/qa/phpcs.xml -vvvv 41 | 42 | cs: ## Check the code for code style issues 43 | $(DOCKER_RUN) vendor/bin/phpcs --parallel=$(THREADS) --cache=./var/.phpcs.cache.json --standard=./etc/qa/phpcs.xml 44 | 45 | stan: ## Run static analysis (PHPStan) 46 | $(DOCKER_RUN) vendor/bin/phpstan analyse src tests --level max --ansi -c ./etc/qa/phpstan.neon 47 | 48 | psalm: ## Run static analysis (Psalm) 49 | $(DOCKER_RUN) vendor/bin/psalm --threads=$(THREADS) --shepherd --stats --config=./etc/qa/psalm.xml 50 | 51 | unit-testing: ## Run tests 52 | $(DOCKER_RUN) vendor/bin/phpunit --colors=always -c ./etc/qa/phpunit.xml --coverage-text --coverage-html ./var/tests-unit-coverage-html --coverage-clover ./var/tests-unit-clover-coverage.xml 53 | $(DOCKER_RUN) test -n "$(COVERALLS_REPO_TOKEN)" && test -n "$(COVERALLS_RUN_LOCALLY)" && test -f ./var/tests-unit-clover-coverage.xml && vendor/bin/php-coveralls -v --coverage_clover ./build/logs/clover.xml --json_path ./var/tests-unit-clover-coverage-upload.json || true 54 | 55 | mutation-testing: ## Run mutation testing 56 | $(DOCKER_RUN) vendor/bin/infection --ansi --min-msi=100 --min-covered-msi=100 --threads=$(THREADS) --ignore-msi-with-no-mutations || (cat ./var/infection.log && false) 57 | 58 | composer-require-checker: ## Ensure we require every package used in this package directly 59 | $(DOCKER_RUN) vendor/bin/composer-require-checker --ignore-parse-errors --ansi -vvv --config-file=./etc/qa/composer-require-checker.json 60 | 61 | composer-unused: ## Ensure we don't require any package we don't use in this package directly 62 | $(DOCKER_RUN) composer unused --ansi 63 | 64 | composer-install: ## Install dependencies 65 | $(DOCKER_RUN) composer install --no-progress --ansi --no-interaction --prefer-dist -o 66 | 67 | backward-compatibility-check: ## Check code for backwards incompatible changes 68 | $(MAKE) backward-compatibility-check-raw || true 69 | 70 | backward-compatibility-check-raw: ## Check code for backwards incompatible changes, doesn't ignore the failure ### 71 | $(DOCKER_RUN) vendor/bin/roave-backward-compatibility-check 72 | 73 | shell: ## Provides Shell access in the expected environment ### 74 | $(DOCKER_RUN) ash 75 | 76 | task-list-ci: ## CI: Generate a JSON array of jobs to run, matches the commands run when running `make (|all)` ### 77 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | grep -v "###" | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%s\n", $$1}' | jq --raw-input --slurp -c 'split("\n")| .[0:-1]' 78 | 79 | help: ## Show this help ### 80 | @printf "\033[33mUsage:\033[0m\n make [target]\n\n\033[33mTargets:\033[0m\n" 81 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[32m%-32s\033[0m %s\n", $$1, $$2}' | tr -d '#' 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Client-side PSR-7 Oauth1 request signer for PHP `^8 || ^7.4` 2 | 3 | [![Continuous Integration](https://github.com/php-api-clients/psr7-oauth1/actions/workflows/ci.yml/badge.svg?event=push)](https://github.com/php-api-clients/psr7-oauth1/actions/workflows/ci.yml) 4 | [![Latest Stable Version](https://poser.pugx.org/api-clients/psr7-oauth1/v/stable.png)](https://packagist.org/packages/api-clients/psr7-oauth1) 5 | [![Total Downloads](https://poser.pugx.org/api-clients/psr7-oauth1/downloads.png)](https://packagist.org/packages/api-clients/psr7-oauth1/stats) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/php-api-clients/psr7-oauth1/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/php-api-clients/psr7-oauth1/?branch=master) 7 | [![License](https://poser.pugx.org/api-clients/psr7-oauth1/license.png)](https://packagist.org/packages/api-clients/psr7-oauth1) 8 | 9 | # Installation 10 | 11 | To install via [Composer](http://getcomposer.org/), use the command below, it will automatically detect the latest version and bind it with `^`. 12 | 13 | ```bash 14 | composer require api-clients/psr7-oauth1 15 | ``` 16 | 17 | In case you need to support `5.5+` as well in your package, we suggest you use the following command: 18 | 19 | ```bash 20 | composer require api-clients/psr7-oauth1:^1.0 21 | ``` 22 | 23 | However, since `1.0`, `2.0`, and `3.0` of this package are 100% compatible, we recommend you use the following command to support both: 24 | 25 | ```bash 26 | composer require "api-clients/psr7-oauth1:^2.0 || ^1.0" 27 | ``` 28 | 29 | # Example 30 | 31 | ```php 32 | withAccessToken( 43 | new Definition\AccessToken('token_key'), 44 | new Definition\TokenSecret('token_secret') 45 | )->sign($request); 46 | ``` 47 | 48 | # Suppported signatures 49 | 50 | All supported signatures are HMAC signatures. 51 | 52 | ## MD5 53 | 54 | Signs request with `HMAC-MD5`. Usage: 55 | 56 | ```php 57 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ../../src 10 | ../../tests 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /etc/qa/phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ergebnis: 3 | classesAllowedToBeExtended: 4 | - Exception 5 | - ApiClients\Tools\Psr7\Oauth1\Signature\HmacSignature 6 | - ApiClients\Tools\Psr7\Oauth1\Signature\Signature 7 | - ApiClients\Tests\Tools\Psr7\Oauth1\Signature\AbstractHmacSignatureTest 8 | 9 | includes: 10 | - ../../vendor/wyrihaximus/async-test-utilities/rules.neon 11 | -------------------------------------------------------------------------------- /etc/qa/phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ../../tests/ 6 | 7 | 8 | 9 | 10 | ../../src/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /etc/qa/psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 120, 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "logs": { 9 | "text": "./var/infection.log", 10 | "summary": "./var/infection-summary.log", 11 | "json": "./var/infection.json", 12 | "perMutator": "./var/infection-per-mutator.md" 13 | }, 14 | "mutators": { 15 | "@default": true, 16 | "InstanceOf_": { 17 | "ignore": [ 18 | "ApiClients\\Tools\\Psr7\\Oauth1\\RequestSigning\\RequestSigner::sign", 19 | "ApiClients\\Tools\\Psr7\\Oauth1\\Signature\\Signature::getKey" 20 | ] 21 | }, 22 | "UnwrapStrRepeat": false, 23 | "UnwrapStrShuffle": false 24 | }, 25 | "phpUnit": { 26 | "configDir": "./etc/qa/" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Definition/AccessToken.php: -------------------------------------------------------------------------------- 1 | accessToken; 19 | } 20 | 21 | /** 22 | * @deprecated Use accessToken property 23 | */ 24 | public function getToken(): string 25 | { 26 | return $this->accessToken; 27 | } 28 | 29 | public function __toString(): string 30 | { 31 | return $this->accessToken; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Definition/ConsumerKey.php: -------------------------------------------------------------------------------- 1 | consumerKey; 19 | } 20 | 21 | public function __toString(): string 22 | { 23 | return $this->consumerKey; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Definition/ConsumerSecret.php: -------------------------------------------------------------------------------- 1 | consumerSecret; 19 | } 20 | 21 | public function __toString(): string 22 | { 23 | return $this->consumerSecret; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Definition/RequestToken.php: -------------------------------------------------------------------------------- 1 | requestToken; 19 | } 20 | 21 | /** 22 | * @deprecated Use requestToken property 23 | */ 24 | public function getToken(): string 25 | { 26 | return $this->requestToken; 27 | } 28 | 29 | public function __toString(): string 30 | { 31 | return $this->requestToken; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Definition/TokenSecret.php: -------------------------------------------------------------------------------- 1 | tokenSecret; 19 | } 20 | 21 | public function __toString(): string 22 | { 23 | return $this->tokenSecret; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/RequestSigning/RequestSigner.php: -------------------------------------------------------------------------------- 1 | signature = $signature ?? new HmacSha1Signature($consumerSecret); 40 | } 41 | 42 | public function withAccessToken(AccessToken $accessToken, TokenSecret $tokenSecret): RequestSigner 43 | { 44 | $clone = clone $this; 45 | $clone->accessToken = $accessToken; 46 | $clone->signature = $clone->signature->withTokenSecret($tokenSecret); 47 | 48 | return $clone; 49 | } 50 | 51 | public function withoutAccessToken(): RequestSigner 52 | { 53 | $clone = clone $this; 54 | $clone->accessToken = null; 55 | $clone->signature = $clone->signature->withoutTokenSecret(); 56 | 57 | return $clone; 58 | } 59 | 60 | public function sign(RequestInterface $request): RequestInterface 61 | { 62 | $parameters = [ 63 | 'oauth_consumer_key' => (string) $this->consumerKey, 64 | 'oauth_nonce' => $this->generateNonce(), 65 | 'oauth_signature_method' => $this->signature->getMethod(), 66 | 'oauth_timestamp' => $this->generateTimestamp(), 67 | 'oauth_version' => '1.0', 68 | ]; 69 | 70 | if ($this->accessToken instanceof AccessToken) { 71 | $parameters['oauth_token'] = (string) $this->accessToken; 72 | } 73 | 74 | $parameters = $this->mergeSignatureParameter($request, $parameters); 75 | 76 | return $request->withHeader('Authorization', $this->generateAuthorizationHeader($parameters)); 77 | } 78 | 79 | /** 80 | * @param array $additionalParameters 81 | */ 82 | public function signToRequestAuthorization( 83 | RequestInterface $request, 84 | string $callbackUri, 85 | array $additionalParameters = [] 86 | ): RequestInterface { 87 | $parameters = [ 88 | 'oauth_consumer_key' => (string) $this->consumerKey, 89 | 'oauth_nonce' => $this->generateNonce(), 90 | 'oauth_signature_method' => $this->signature->getMethod(), 91 | 'oauth_timestamp' => $this->generateTimestamp(), 92 | 'oauth_version' => '1.0', 93 | 'oauth_callback' => $callbackUri, 94 | ]; 95 | 96 | $parameters = array_merge($parameters, $additionalParameters); 97 | 98 | $parameters = $this->mergeSignatureParameter($request, $parameters); 99 | 100 | return $request->withHeader('Authorization', $this->generateAuthorizationHeader($parameters)); 101 | } 102 | 103 | /** 104 | * @param array $parameters 105 | * 106 | * @return array 107 | */ 108 | private function mergeSignatureParameter(RequestInterface $request, array $parameters): array 109 | { 110 | $body = []; 111 | if ( 112 | $request->getMethod() === 'POST' && 113 | $request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded' 114 | ) { 115 | parse_str((string) $request->getBody(), $body); 116 | } 117 | 118 | $signature = $this->signature->sign($request->getUri(), array_merge($body, $parameters), $request->getMethod()); 119 | 120 | $parameters['oauth_signature'] = $signature; 121 | 122 | return $parameters; 123 | } 124 | 125 | private function generateTimestamp(): string 126 | { 127 | $dateTime = new DateTimeImmutable(); 128 | 129 | return $dateTime->format('U'); 130 | } 131 | 132 | private function generateNonce(int $length = self::DEFAULT_NONCE_LENGTH): string 133 | { 134 | $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 135 | 136 | return substr(str_shuffle(str_repeat($pool, self::NONCE_REPLICATION)), self::START, $length); 137 | } 138 | 139 | /** 140 | * @param array $parameters 141 | */ 142 | private function generateAuthorizationHeader(array $parameters): string 143 | { 144 | $header = 'OAuth '; 145 | foreach ($parameters as $key => $value) { 146 | $header .= rawurlencode($key) . '="' . rawurlencode($value) . '",'; 147 | } 148 | 149 | return rtrim($header, ','); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Signature/HmacMd5Signature.php: -------------------------------------------------------------------------------- 1 | getHashingAlgorithm())); 27 | } 28 | 29 | /** 30 | * @param array $parameters 31 | */ 32 | final public function sign(UriInterface $uri, array $parameters = [], string $method = 'POST'): string 33 | { 34 | $baseString = $this->generateBaseString($uri, $parameters, $method); 35 | 36 | return base64_encode($this->hash($baseString)); 37 | } 38 | 39 | /** 40 | * @param array $parameters 41 | */ 42 | private function generateBaseString(UriInterface $uri, array $parameters = [], string $method = 'POST'): string 43 | { 44 | $baseString = [rawurlencode($method)]; 45 | 46 | $scheme = $uri->getScheme(); 47 | $host = $uri->getHost(); 48 | $path = $uri->getPath(); 49 | $port = $uri->getPort(); 50 | 51 | $baseString[] = rawurlencode($port === null 52 | ? sprintf('%s://%s%s', $scheme, $host, $path) 53 | : sprintf('%s://%s:%d%s', $scheme, $host, $port, $path)); 54 | 55 | $data = []; 56 | 57 | parse_str($uri->getQuery(), $query); 58 | 59 | foreach (array_merge($query, $parameters) as $key => $value) { 60 | $data[rawurlencode($key)] = rawurlencode($value); 61 | } 62 | 63 | ksort($data); 64 | 65 | array_walk($data, static function (string &$value, string $key): void { 66 | $value = $key . '=' . $value; 67 | }); 68 | 69 | $baseString[] = rawurlencode(implode('&', $data)); 70 | 71 | return implode('&', $baseString); 72 | } 73 | 74 | private function hash(string $string): string 75 | { 76 | return hash_hmac($this->getHashingAlgorithm(), $string, $this->getKey(), true); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Signature/Signature.php: -------------------------------------------------------------------------------- 1 | tokenSecret = $tokenSecret; 25 | 26 | return $clone; 27 | } 28 | 29 | final public function withoutTokenSecret(): Signature 30 | { 31 | $clone = clone $this; 32 | $clone->tokenSecret = null; 33 | 34 | return $clone; 35 | } 36 | 37 | final protected function getKey(): string 38 | { 39 | $key = rawurlencode((string) $this->consumerSecret) . '&'; 40 | 41 | if ($this->tokenSecret instanceof TokenSecret) { 42 | $key .= rawurlencode((string) $this->tokenSecret); 43 | } 44 | 45 | return $key; 46 | } 47 | 48 | abstract public function getMethod(): string; 49 | 50 | /** 51 | * @param array $parameters 52 | */ 53 | abstract public function sign(UriInterface $uri, array $parameters = [], string $method = 'POST'): string; 54 | } 55 | -------------------------------------------------------------------------------- /var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/php-api-clients/psr7-oauth1/dcbe6547fc7cd5b37e0195d42ebd54a219a13828/var/.gitkeep --------------------------------------------------------------------------------