├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── Bug Report.yml │ ├── Feature Request.yml │ └── config.yml ├── dependabot.yml ├── stale.yml └── workflows │ └── semgrep.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── .phpcs.xml.dist ├── .shiprc ├── .version ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── build.sh ├── composer.json ├── opslevel.yml ├── phpstan.neon.dist ├── phpunit.xml.dist ├── psalm.xml.dist ├── public-signing-key.pub ├── rector.php ├── scoper.inc.php ├── src ├── Actions │ ├── Authentication.php │ ├── Base.php │ ├── Configuration.php │ ├── Sync.php │ ├── Tools.php │ └── Updates.php ├── Cache │ ├── WpObjectCacheItem.php │ └── WpObjectCachePool.php ├── Database.php ├── Filters │ ├── Authentication.php │ └── Base.php ├── Hooks.php ├── Http │ ├── Client.php │ ├── Factory.php │ ├── Message │ │ ├── MessageTrait.php │ │ ├── Request.php │ │ ├── RequestTrait.php │ │ ├── Response.php │ │ ├── ServerRequest.php │ │ ├── Stream.php │ │ ├── UploadedFile.php │ │ └── Uri.php │ └── MessageFactory │ │ ├── RequestFactory.php │ │ ├── ResponseFactory.php │ │ └── StreamFactory.php ├── Plugin.php └── Utilities │ ├── Render.php │ └── Sanitize.php ├── updates.json └── wpAuth0.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @auth0/project-dx-sdks-engineer-codeowner 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug Report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report a bug 2 | description: Have you found a bug or issue? Create a bug report for this library 3 | labels: ["bug"] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | **Please do not report security vulnerabilities here**. The [Responsible Disclosure Program](https://auth0.com/responsible-disclosure-policy) details the procedure for disclosing security issues. 10 | 11 | - type: checkboxes 12 | id: checklist 13 | attributes: 14 | label: Checklist 15 | options: 16 | - label: I have looked into the [Readme](https://github.com/auth0/wordpress#readme) and the [documentation](https://auth0.com/docs/customize/integrations/cms/wordpress-plugin), and have not found a suitable solution or answer. 17 | required: true 18 | - label: I have searched the [issues](https://github.com/auth0/wordpress/issues) and have not found a suitable solution or answer. 19 | required: true 20 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 21 | required: true 22 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 23 | required: true 24 | 25 | - type: textarea 26 | id: description 27 | attributes: 28 | label: Description 29 | description: Provide a clear and concise description of the issue, including what you expected to happen. 30 | validations: 31 | required: true 32 | 33 | - type: textarea 34 | id: reproduction 35 | attributes: 36 | label: Reproduction 37 | description: Detail the steps taken to reproduce this error, and whether this issue can be reproduced consistently or if it is intermittent. 38 | placeholder: | 39 | 1. Step 1... 40 | 2. Step 2... 41 | 3. ... 42 | validations: 43 | required: true 44 | 45 | - type: textarea 46 | id: additional-context 47 | attributes: 48 | label: Additional context 49 | description: Other libraries that might be involved, or any other relevant information you think would be useful. 50 | validations: 51 | required: false 52 | 53 | - type: input 54 | id: environment-version 55 | attributes: 56 | label: wp-auth0 version 57 | validations: 58 | required: true 59 | 60 | - type: input 61 | id: environment-wordpress-version 62 | attributes: 63 | label: WordPress version 64 | validations: 65 | required: true 66 | 67 | - type: input 68 | id: environment-php-version 69 | attributes: 70 | label: PHP version 71 | validations: 72 | required: true 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature Request.yml: -------------------------------------------------------------------------------- 1 | name: 🧩 Feature request 2 | description: Suggest an idea or a feature for this library 3 | labels: ["feature request"] 4 | 5 | body: 6 | - type: checkboxes 7 | id: checklist 8 | attributes: 9 | label: Checklist 10 | options: 11 | - label: I have looked into the [Readme](https://github.com/auth0/wordpress#readme) and the [documentation](https://auth0.com/docs/customize/integrations/cms/wordpress-plugin), and have not found a suitable solution or answer. 12 | required: true 13 | - label: I have searched the [issues](https://github.com/auth0/wordpress/issues) and have not found a suitable solution or answer. 14 | required: true 15 | - label: I have searched the [Auth0 Community](https://community.auth0.com) forums and have not found a suitable solution or answer. 16 | required: true 17 | - label: I agree to the terms within the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md). 18 | required: true 19 | 20 | - type: textarea 21 | id: description 22 | attributes: 23 | label: Describe the problem you'd like to have solved 24 | description: A clear and concise description of what the problem is. 25 | placeholder: I'm always frustrated when... 26 | validations: 27 | required: true 28 | 29 | - type: textarea 30 | id: ideal-solution 31 | attributes: 32 | label: Describe the ideal solution 33 | description: A clear and concise description of what you want to happen. 34 | validations: 35 | required: true 36 | 37 | - type: textarea 38 | id: alternatives-and-workarounds 39 | attributes: 40 | label: Alternatives and current workarounds 41 | description: A clear and concise description of any alternatives you've considered or any workarounds that are currently in place. 42 | validations: 43 | required: false 44 | 45 | - type: textarea 46 | id: additional-context 47 | attributes: 48 | label: Additional context 49 | description: Add any other context or screenshots about the feature request here. 50 | validations: 51 | required: false 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Auth0 Community 4 | url: https://community.auth0.com 5 | about: Discuss this SDK in the Auth0 Community forums 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 90 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | daysUntilClose: 7 8 | 9 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 10 | exemptLabels: [] 11 | 12 | # Set to true to ignore issues with an assignee (defaults to false) 13 | exemptAssignees: true 14 | 15 | # Label to use when marking as stale 16 | staleLabel: closed:stale 17 | 18 | # Comment to post when marking as stale. Set to `false` to disable 19 | markComment: > 20 | This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you have not received a response for our team (apologies for the delay) and this is still a blocker, please reply with additional information or just a ping. Thank you for your contribution! 🙇‍♂️ -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | name: Semgrep 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | push: 10 | branches: 11 | - 5.x 12 | schedule: 13 | - cron: "30 0 1,15 * *" 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 20 | cancel-in-progress: ${{ github.ref != 'refs/heads/5.x' }} 21 | 22 | jobs: 23 | check: 24 | name: Check for Vulnerabilities 25 | runs-on: ubuntu-latest 26 | 27 | container: 28 | image: returntocorp/semgrep 29 | 30 | steps: 31 | - if: github.actor == 'dependabot[bot]' || github.event_name == 'merge_group' 32 | run: exit 0 # Skip unnecessary test runs for dependabot and merge queues. Artifically flag as successful, as this is a required check for branch protection. 33 | 34 | - uses: actions/checkout@v4 35 | with: 36 | ref: ${{ github.event.pull_request.head.sha || github.ref }} 37 | 38 | - run: semgrep ci 39 | env: 40 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | build 3 | composer.phar 4 | composer.lock 5 | phpunit.xml 6 | vendor 7 | .idea 8 | .env 9 | .phpcs.xml 10 | phpcs.xml 11 | .phpunit.result.cache 12 | .DS_Store 13 | coverage 14 | TODO.txt 15 | composer.local.json 16 | .php-cs-fixer.cache 17 | composer.local.old 18 | pest.log 19 | private-signing-key.pem 20 | build.zip 21 | build.zip.sig 22 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 7 | ->setRules([ 8 | 'array_indentation' => true, 9 | 'array_push' => true, 10 | 'array_syntax' => ['syntax' => 'short'], 11 | 'assign_null_coalescing_to_coalesce_equal' => true, 12 | 'backtick_to_shell_exec' => true, 13 | 'binary_operator_spaces' => true, 14 | 'blank_line_after_namespace' => true, 15 | 'blank_line_after_opening_tag' => true, 16 | 'blank_line_before_statement' => true, 17 | 'blank_line_between_import_groups' => true, 18 | 'braces' => true, 19 | 'cast_spaces' => true, 20 | 'class_attributes_separation' => ['elements' => ['const' => 'one', 'method' => 'one', 'property' => 'one', 'trait_import' => 'one', 'case' => 'one']], 21 | 'class_definition' => ['multi_line_extends_each_single_line' => true, 'single_line' => true, 'single_item_single_line' => true, 'space_before_parenthesis' => false, 'inline_constructor_arguments' => false], 22 | 'class_reference_name_casing' => true, 23 | 'clean_namespace' => true, 24 | 'combine_consecutive_issets' => true, 25 | 'combine_consecutive_unsets' => true, 26 | 'combine_nested_dirname' => true, 27 | 'comment_to_phpdoc' => ['ignored_tags' => ['codeCoverageIgnoreStart', 'codeCoverageIgnoreEnd', 'phpstan-ignore-next-line']], 28 | 'compact_nullable_typehint' => true, 29 | 'concat_space' => ['spacing' => 'one'], 30 | 'constant_case' => ['case' => 'lower'], 31 | 'curly_braces_position' => ['control_structures_opening_brace' => 'same_line', 'functions_opening_brace' => 'next_line_unless_newline_at_signature_end', 'anonymous_functions_opening_brace' => 'same_line', 'classes_opening_brace' => 'next_line_unless_newline_at_signature_end', 'anonymous_classes_opening_brace' => 'same_line', 'allow_single_line_empty_anonymous_classes' => true, 'allow_single_line_anonymous_functions' => true], 32 | 'date_time_create_from_format_call' => true, 33 | 'date_time_immutable' => true, 34 | 'declare_equal_normalize' => ['space' => 'none'], 35 | 'declare_parentheses' => true, 36 | 'declare_strict_types' => true, 37 | 'dir_constant' => true, 38 | 'doctrine_annotation_array_assignment' => true, 39 | 'doctrine_annotation_braces' => true, 40 | 'doctrine_annotation_indentation' => true, 41 | 'doctrine_annotation_spaces' => true, 42 | 'echo_tag_syntax' => ['format' => 'long'], 43 | 'elseif' => true, 44 | 'empty_loop_body' => true, 45 | 'empty_loop_condition' => true, 46 | 'encoding' => true, 47 | 'ereg_to_preg' => true, 48 | 'error_suppression' => true, 49 | 'escape_implicit_backslashes' => true, 50 | 'explicit_indirect_variable' => true, 51 | 'explicit_string_variable' => true, 52 | 'final_class' => true, 53 | 'final_internal_class' => true, 54 | 'final_public_method_for_abstract_class' => true, 55 | 'fopen_flag_order' => true, 56 | 'fopen_flags' => true, 57 | 'full_opening_tag' => true, 58 | 'fully_qualified_strict_types' => true, 59 | 'function_declaration' => true, 60 | 'function_to_constant' => true, 61 | 'function_typehint_space' => true, 62 | 'general_phpdoc_annotation_remove' => true, 63 | 'general_phpdoc_tag_rename' => true, 64 | 'get_class_to_class_keyword' => true, 65 | 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true], 66 | 'group_import' => true, 67 | 'heredoc_indentation' => true, 68 | 'heredoc_to_nowdoc' => true, 69 | 'implode_call' => true, 70 | 'include' => true, 71 | 'increment_style' => ['style' => 'pre'], 72 | 'indentation_type' => true, 73 | 'integer_literal_case' => true, 74 | 'is_null' => true, 75 | 'lambda_not_used_import' => true, 76 | 'line_ending' => true, 77 | 'linebreak_after_opening_tag' => true, 78 | 'list_syntax' => ['syntax' => 'short'], 79 | 'logical_operators' => true, 80 | 'lowercase_cast' => true, 81 | 'lowercase_keywords' => true, 82 | 'lowercase_static_reference' => true, 83 | 'magic_constant_casing' => true, 84 | 'magic_method_casing' => true, 85 | 'mb_str_functions' => false, 86 | 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline', 'after_heredoc' => true], 87 | 'method_chaining_indentation' => true, 88 | 'modernize_strpos' => true, 89 | 'modernize_types_casting' => true, 90 | 'multiline_comment_opening_closing' => true, 91 | 'multiline_whitespace_before_semicolons' => true, 92 | 'native_function_casing' => true, 93 | 'native_function_invocation' => true, 94 | 'native_function_type_declaration_casing' => true, 95 | 'new_with_braces' => true, 96 | 'no_alias_functions' => true, 97 | 'no_alias_language_construct_call' => true, 98 | 'no_alternative_syntax' => true, 99 | 'no_binary_string' => true, 100 | 'no_blank_lines_after_class_opening' => true, 101 | 'no_blank_lines_after_phpdoc' => true, 102 | 'no_break_comment' => true, 103 | 'no_closing_tag' => true, 104 | 'no_empty_comment' => true, 105 | 'no_empty_phpdoc' => true, 106 | 'no_empty_statement' => true, 107 | 'no_extra_blank_lines' => true, 108 | 'no_homoglyph_names' => true, 109 | 'no_leading_import_slash' => true, 110 | 'no_leading_namespace_whitespace' => true, 111 | 'no_mixed_echo_print' => true, 112 | 'no_multiline_whitespace_around_double_arrow' => true, 113 | 'no_multiple_statements_per_line' => true, 114 | 'no_php4_constructor' => true, 115 | 'no_short_bool_cast' => true, 116 | 'no_singleline_whitespace_before_semicolons' => true, 117 | 'no_space_around_double_colon' => true, 118 | 'no_spaces_after_function_name' => true, 119 | 'no_spaces_around_offset' => true, 120 | 'no_spaces_inside_parenthesis' => true, 121 | 'no_superfluous_elseif' => true, 122 | 'no_trailing_comma_in_singleline' => true, 123 | 'no_trailing_whitespace_in_comment' => true, 124 | 'no_trailing_whitespace_in_string' => true, 125 | 'no_trailing_whitespace' => true, 126 | 'no_unneeded_control_parentheses' => true, 127 | 'no_unneeded_curly_braces' => true, 128 | 'no_unneeded_final_method' => true, 129 | 'no_unneeded_import_alias' => true, 130 | 'no_unreachable_default_argument_value' => true, 131 | 'no_unset_cast' => true, 132 | 'no_unused_imports' => true, 133 | 'no_useless_concat_operator' => true, 134 | 'no_useless_else' => true, 135 | 'no_useless_nullsafe_operator' => true, 136 | 'no_useless_return' => true, 137 | 'no_useless_sprintf' => true, 138 | 'no_whitespace_before_comma_in_array' => true, 139 | 'no_whitespace_in_blank_line' => true, 140 | 'non_printable_character' => true, 141 | 'normalize_index_brace' => true, 142 | 'not_operator_with_successor_space' => true, 143 | 'nullable_type_declaration_for_default_null_value' => true, 144 | 'object_operator_without_whitespace' => true, 145 | 'octal_notation' => true, 146 | 'operator_linebreak' => true, 147 | 'ordered_class_elements' => ['sort_algorithm' => 'alpha', 'order' => ['use_trait', 'case', 'constant', 'constant_private', 'constant_protected', 'constant_public', 'property_private', 'property_private_readonly', 'property_private_static', 'property_protected', 'property_protected_readonly', 'property_protected_static', 'property_public', 'property_public_readonly', 'property_public_static', 'property_static', 'protected', 'construct', 'destruct', 'magic', 'method', 'public', 'method_public', 'method_abstract', 'method_public_abstract', 'method_public_abstract_static', 'method_public_static', 'method_static', 'method_private', 'method_private_abstract', 'method_private_abstract_static', 'method_private_static', 'method_protected', 'method_protected_abstract', 'method_protected_abstract_static', 'method_protected_static', 'phpunit', 'private', 'property']], 148 | 'ordered_imports' => ['sort_algorithm' => 'alpha', 'imports_order' => ['const', 'class', 'function']], 149 | 'ordered_interfaces' => true, 150 | 'ordered_traits' => true, 151 | 'php_unit_fqcn_annotation' => true, 152 | 'phpdoc_add_missing_param_annotation' => ['only_untyped' => false], 153 | 'phpdoc_align' => ['align' => 'vertical'], 154 | 'phpdoc_indent' => true, 155 | 'phpdoc_inline_tag_normalizer' => true, 156 | 'phpdoc_line_span' => true, 157 | 'phpdoc_no_access' => true, 158 | 'phpdoc_no_empty_return' => true, 159 | 'phpdoc_no_package' => true, 160 | 'phpdoc_no_useless_inheritdoc' => true, 161 | 'phpdoc_order_by_value' => true, 162 | 'phpdoc_order' => true, 163 | 'phpdoc_return_self_reference' => ['replacements' => ['this' => 'self']], 164 | 'phpdoc_scalar' => true, 165 | 'phpdoc_separation' => true, 166 | 'phpdoc_single_line_var_spacing' => true, 167 | 'phpdoc_summary' => true, 168 | 'phpdoc_tag_type' => true, 169 | 'phpdoc_to_comment' => ['ignored_tags' => ['var']], 170 | 'phpdoc_trim_consecutive_blank_line_separation' => true, 171 | 'phpdoc_trim' => true, 172 | 'phpdoc_types_order' => true, 173 | 'phpdoc_types' => true, 174 | 'phpdoc_var_annotation_correct_order' => true, 175 | 'phpdoc_var_without_name' => true, 176 | 'pow_to_exponentiation' => true, 177 | 'protected_to_private' => true, 178 | 'psr_autoloading' => true, 179 | 'random_api_migration' => true, 180 | 'regular_callable_call' => true, 181 | 'return_assignment' => true, 182 | 'return_type_declaration' => ['space_before' => 'none'], 183 | 'return_type_declaration' => true, 184 | 'self_accessor' => true, 185 | 'self_static_accessor' => true, 186 | 'semicolon_after_instruction' => true, 187 | 'set_type_to_cast' => true, 188 | 'short_scalar_cast' => true, 189 | 'simple_to_complex_string_variable' => true, 190 | 'simplified_if_return' => true, 191 | 'single_blank_line_at_eof' => true, 192 | 'single_blank_line_before_namespace' => true, 193 | 'single_class_element_per_statement' => true, 194 | 'single_line_after_imports' => true, 195 | 'single_line_comment_spacing' => true, 196 | 'single_line_comment_style' => ['comment_types' => ['hash']], 197 | 'single_line_throw' => true, 198 | 'single_quote' => true, 199 | 'single_space_after_construct' => true, 200 | 'single_space_around_construct' => true, 201 | 'single_trait_insert_per_statement' => true, 202 | 'space_after_semicolon' => true, 203 | 'standardize_increment' => true, 204 | 'standardize_not_equals' => true, 205 | 'statement_indentation' => true, 206 | 'static_lambda' => true, 207 | 'strict_comparison' => true, 208 | 'strict_param' => true, 209 | 'string_length_to_empty' => true, 210 | 'string_line_ending' => true, 211 | 'switch_case_semicolon_to_colon' => true, 212 | 'switch_case_space' => true, 213 | 'switch_continue_to_break' => true, 214 | 'ternary_operator_spaces' => true, 215 | 'ternary_to_elvis_operator' => true, 216 | 'ternary_to_null_coalescing' => true, 217 | 'trailing_comma_in_multiline' => ['after_heredoc' => true, 'elements' => ['arguments', 'arrays', 'match', 'parameters']], 218 | 'trim_array_spaces' => true, 219 | 'types_spaces' => ['space' => 'single', 'space_multiple_catch' => 'single'], 220 | 'unary_operator_spaces' => true, 221 | 'use_arrow_functions' => true, 222 | 'visibility_required' => true, 223 | 'void_return' => true, 224 | 'whitespace_after_comma_in_array' => true, 225 | 'yoda_style' => true, 226 | ]) 227 | ->setFinder( 228 | PhpCsFixer\Finder::create() 229 | ->exclude('vendor') 230 | ->in([__DIR__ . '/src/']), 231 | ); 232 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.shiprc: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "wpAuth0.php": [], 4 | ".version": [] 5 | }, 6 | "prefixVersion": false 7 | } 8 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 5.2.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [5.3.0](https://github.com/auth0/wp-auth0/tree/5.3.0) (2025-05-16) 4 | 5 | ### Fixed 6 | 7 | - Security fix: Resolve CVE-2025-47275 8 | 9 | ## [5.2.1](https://github.com/auth0/wp-auth0/tree/5.2.1) (2024-06-03) 10 | 11 | ### Fixed 12 | 13 | - Resolves an issue in which the fallback URI secret isn't shown. [\#903](https://github.com/auth0/wordpress/pull/903) ([HPiirainen](https://github.com/HPiirainen)) 14 | - Resolves a compatibility issue with changes in WordPress 6.5 causing invalidated sessions. ([evansims](https://github.com/evansims)) 15 | 16 | ## [5.2.0](https://github.com/auth0/wp-auth0/tree/5.2.0) (2023-12-11) 17 | 18 | ### Added 19 | 20 | - feat(SDK-4734): Implement support for Back-Channel Logout [\#882](https://github.com/auth0/wordpress/pull/882) ([evansims](https://github.com/evansims)) 21 | 22 | > **Note** 23 | > ¹ To use this feature, an Auth0 tenant must have support for it enabled. 24 | 25 | ## [5.1.0](https://github.com/auth0/wp-auth0/tree/5.1.0) (2023-07-24) 26 | 27 | ### Added 28 | 29 | - Organization Name support was added for Authentication API and token handling ¹ 30 | 31 | ### Updated 32 | 33 | - Bumped tested WordPress version to forthcoming 6.3.0 release. 34 | - Bumped `auth0-php` dependency version range to `^8.7`. 35 | - Updated telemetry to indicate `wordpress` package (previously `wp-auth0`.) 36 | 37 | > **Note** 38 | > ¹ To use this feature, an Auth0 tenant must have support for it enabled. This feature is not yet available to all tenants. 39 | 40 | ## [5.0.1](https://github.com/auth0/wp-auth0/tree/5.0.1) (2022-12-12) 41 | 42 | ### Fixed 43 | 44 | - Resolves an issue that sometimes prevented the plugin from being activated on WordPress 6 45 | 46 | ## [5.0.0](https://github.com/auth0/wp-auth0/tree/5.0.0) (2022-10-28) 47 | 48 | Introducing V5 of WP-Auth0 ("Login by Auth0"), a major redesign and upgrade to our WordPress integration plugin. V5 includes many new features and changes: 49 | 50 | - [WordPress 6](https://wordpress.org/support/wordpress-version/version-6-0/) and [PHP 8](https://www.php.net/releases/8.0/en.php) support 51 | - Integration with the [Auth0-PHP SDK](https://github.com/auth0/auth0-php), and access to its entire API (including Management API calls) 52 | High-performance background sync using [WordPress' Cron](https://developer.wordpress.org/plugins/cron/) feature 53 | - "Flexible identifier" support, allowing users to sign in using multiple connection types without requiring extra configuration 54 | - Expanded control over how sign-ins without matching existing WordPress accounts are handled 55 | - Enhanced session pairing between WordPress and Auth0, including session invalidation, access token refresh, and more. 56 | 57 | V5 represents a major step forward for our WordPress plugin, and we're excited to see what you build with it! 58 | 59 | It's important to note, if you wrote custom theme code or plugins for your WordPress site that targeted previous versions of the plugin, you may need to adjust those themes or plugins to adapt to the new version. 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Auth0, Inc. (https://auth0.com) 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![WordPress by Auth0](https://cdn.auth0.com/website/sdks/banners/wp-auth0-banner.png) 2 | 3 | WordPress Plugin for [Auth0](https://auth0.com) Authentication 4 | 5 | [![License](https://img.shields.io/packagist/l/auth0/auth0-php)](https://doge.mit-license.org/) 6 | 7 | :rocket: [Getting Started](#getting-started) - :computer: [SDK Usage](#sdk-usage) - 📆 [Support Policy](#support-policy) - :speech_balloon: [Feedback](#feedback) 8 | 9 | ## Overview 10 | 11 | The Auth0 WordPress plugin replaces the standard WordPress login flow with a new authentication process using Auth0's Universal Login experience. This enables you to secure your WordPress site with Auth0's advanced features, such as MFA, SSO, Passwordless, PassKey, and so on. 12 | 13 | > [!IMPORTANT] 14 | > This plugin is **NOT** a SDK (Software Development Kit.) It's APIs are internal and not intended for developers to extend directly. We do not support altering the plugin's behavior or integrating it in any way beyond what is outlined in this README. If you're looking to build a more extensive integration, please create a solution using the [Auth0-PHP SDK](https://github.com/auth0/auth0-php) instead. 15 | 16 | > [!WARNING] 17 | > v4 of the plugin is no longer supported as of June 2023. We are no longer providing new features or bugfixes for that release. Please upgrade to v5 as soon as possible. 18 | 19 | ## Getting Started 20 | 21 | ### Requirements 22 | 23 | - PHP 8.1+ 24 | - [Most recent version of WordPress](https://wordpress.org/news/category/releases/) 25 | - Database credentials with table creation permissions 26 | 27 | > Please review our [support policy](#support-policy) on specific PHP and WordPress versions and when they may exit support in the future. 28 | 29 | ### Installation 30 | 31 | 54 | 55 | #### Composer 56 | 57 | The plugin supports installation through [Composer](https://getcomposer.org/), and is [WPackagist](https://wpackagist.org/) compatible. This approach is preferred when using [Bedrock](https://roots.io/bedrock/), but will work with virtually any WordPress installation. 58 | 59 | For [Bedrock](https://roots.io/bedrock/) installations, you'll usually run this command from the root WordPress installation directory, but check the documentation the project's maintainers provide for the best guidance. 60 | 61 | For standard WordPress installations, this command can be run from the `wp-content/plugins` sub-directory. 62 | 63 | ``` 64 | composer require symfony/http-client nyholm/psr7 auth0/wordpress:^5.0 65 | ``` 66 | 67 |

68 | Note on Composer Dependencies 69 | 70 | When installed with Composer, the plugin depends on the presence of [PSR-18](https://packagist.org/providers/psr/http-client-implementation) and [PSR-17](https://packagist.org/providers/psr/http-factory-implementation) library implementations. The `require` command above includes two such libraries (`symfony/http-client` and `nyholm/psr7`) that satisfy these requirements, but you can use any other compatible libraries that you prefer. Visit Packagist for a list of [PSR-18](https://packagist.org/providers/psr/http-client-implementation) and [PSR-17](https://packagist.org/providers/psr/http-factory-implementation) providers. 71 | 72 | If you are using Bedrock or another Composer-based configuration, you can try installing `auth0/wordpress` without any other dependencies, as the implementations may be satisfied by other already installed packages. 73 | 74 | > **Note** PHP Standards Recommendations (PSRs) are standards for PHP libraries and applications that enable greater interoperability and choice. You can learn more about them and the PHP-FIG organization that maintains them [here](https://www.php-fig.org/). 75 | 76 |

77 | 78 | 90 | 91 | ### Activation 92 | 93 | After installation, you must activate the plugin within your WordPress site: 94 | 95 | 1. Open your WordPress Dashboard. 96 | 2. Select 'Plugins' from the sidebar, and then 'Installed Plugins.' 97 | 3. Choose 'Activate' underneath the plugin's name. 98 | 99 | ### Configure Auth0 100 | 101 | 1. Sign into Auth0. If you don't have an account, [it's free to create one](https://auth0.com/signup). 102 | 2. [Open 'Applications' from your Auth0 Dashboard](https://manage.auth0.com/#/applications/create), and select 'Create Application.' 103 | 3. Choose 'Regular Web Application' and then 'Create.' 104 | 4. From the newly created application's page, select the Settings tab. 105 | 106 | Please prepare the following information: 107 | 108 | - Note the **Domain**, **Client ID**, and **Client Secret**, available from the newly created Application's Settings page. You will need these to configure the plugin in the next step. 109 | - From your WordPress Dashboard's General Settings page, note your **WordPress Address** and **Site Address** URLs. We recommend you read our guidance on [common WordPress URL issues](#common-wordpress-url-issues). 110 | 111 | Continue configuring your Auth0 application from its Settings page: 112 | 113 | - **Allowed Callback URLs** should include the URL to your WordPress site's `wp-login.php`. 114 | - In most (but not all) cases, this will be your WordPress Address with `/wp-login.php` appended. 115 | - Please ensure your site is configured never to cache this URL, or you may see an "invalid state" error during login. 116 | - **Allowed Web Origins** should include both your WordPress Address and Site Address URLs. 117 | - **Allowed Logout URLs** should consist of your WordPress Address. 118 | 119 |

120 | Common WordPress URL Issues 121 | 122 | - These must be the URLs your visitors will use to access your WordPress site. If you are using a reverse proxy, you may need to manually configure your WordPress Address and Site Address URLs to match the URL you use to access your site. 123 | - Make sure these URLs match your site's configured protocol. When using a reverse proxy, you may need to update these to reflect serving over SSL/HTTPS. 124 |

125 | 126 |

127 | Troubleshooting 128 | 129 | If you're encountering issues, start by checking that your Auth0 Application is setup like so: 130 | 131 | - **Application Type** must be set to **Regular Web Application**. 132 | - **Token Endpoint Authentication Method** must be set to **Post**. 133 | - **Allowed Origins (CORS)** should be blank. 134 | 135 | Scroll down and expand the "Advanced Settings" panel, then: 136 | 137 | - Under **OAuth**: 138 | - Ensure that **JsonWebToken Signature Algorithm** is set to **RS256**. 139 | - Check that **OIDC Conformant** is enabled. 140 | - Under **Grant Types**: 141 | - Ensure that **Implicit**, **Authorization Code**, and **Client Credentials** are enabled. 142 | - You may also want to enable **Refresh Token**. 143 | 144 |

145 | 146 | ### Configure the Plugin 147 | 148 | Upon activating the Auth0 plugin, you will find a new "Auth0" section in the sidebar of your WordPress Dashboard. This section enables you to configure the plugin in a variety of ways. 149 | 150 | For the plugin to operate, at a minimum, you will need to configure the Domain, Client ID, and Client Secret fields. These are available from the Auth0 Application you created in the previous step. Once configured, select the "Enable Authentication" option to have the plugin begin handling authentication for you. 151 | 152 | We recommend testing on a staging/development site using a separate Auth0 Application before putting the plugin live on your production site. 153 | 154 | ### Configure WordPress 155 | 156 | #### Plugin Database Tables 157 | 158 | The plugin uses dedicated database tables to guarantee high performance. When the plugin is activated, it will use the database credentials you have configured for WordPress to create these tables. 159 | 160 | Please ensure your configured credentials have appropriate privileges to create new tables. 161 | 162 | #### Cron Configuration 163 | 164 | The plugin uses WordPress' [background task manager](https://developer.wordpress.org/plugins/cron/) to perform important periodic tasks. Proper synchronization between WordPress and Auth0 relies on this. 165 | 166 | By default, WordPress' task manager runs on every page load, which is inadvisable for production sites. For best performance and reliability, please ensure you have configured WordPress to use a [cron job](https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/) to run these tasks periodically instead. 167 | 168 | ## SDK Usage 169 | 170 | The plugin is built on top of [Auth0-PHP](https://github.com/auth0/auth0-PHP) — Auth0's full-featured PHP SDK for Authentication and Management APIs. 171 | 172 | For custom WordPress development, please do not extend the plugin's classes themselves, as this is not supported. Nearly all of the plugin's APIs are considered `internal` and will change over time, most likely breaking any custom extension built upon them. 173 | 174 | Instead, please take advantage of the full PHP SDK that the plugin is built upon. You can use the plugin's `getSdk()` method to retrieve a configured instance of the SDK, ready for use. This method can be called from the plugin's global `wpAuth0()` helper, which returns the WordPress plugin itself. 175 | 176 | ```php 177 | getSdk(); // Returns an instanceof Auth0\SDK\Auth0 181 | ``` 182 | 183 | Please direct questions about developing with the Auth0-PHP SDK to the [Auth0 Community](https://community.auth0.com), and issues or feature requests to [it's respective repository](https://github.com/auth0/auth0-PHP). Documentations and examples on working with the Auth0-PHP SDKs are also available from [its repository](https://github.com/auth0/auth0-PHP). 184 | 185 | ## Support Policy 186 | 187 | - Our PHP version support window mirrors the [PHP release support schedule](https://www.php.net/supported-versions.php). Our support for PHP versions ends when they stop receiving security fixes. 188 | - As Automattic's stated policy is "security patches are backported when possible, but this is not guaranteed," we only support [the latest release](https://wordpress.org/news/category/releases/) marked as ["actively supported"](https://endoflife.date/wordpress) by Automattic. 189 | 190 | | Plugin Version | WordPress Version | PHP Version | Support Ends | 191 | | -------------- | ----------------- | ----------- | ------------ | 192 | | 5 | 6 | 8.3 | Nov 2026 | 193 | | | | 8.2 | Dec 2025 | 194 | | | | 8.1 | Nov 2024 | 195 | 196 | Composer and WordPress do not offer upgrades to incompatible versions. Therefore, we regularly deprecate support within the plugin for PHP or WordPress versions that have reached end-of-life. These deprecations are not considered breaking changes and will not result in a major version bump. 197 | 198 | Sites running unsupported versions of PHP or WordPress will continue to function but will not receive updates until their environment is upgraded. For your security, please ensure your PHP runtime and WordPress remain up to date. 199 | 200 | ## Feedback 201 | 202 | ### Contributing 203 | 204 | We appreciate feedback and contribution to this repo! Before you get started, please see the following: 205 | 206 | - [Auth0's general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md) 207 | - [Auth0's code of conduct guidelines](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md) 208 | 209 | ### Raise an issue 210 | 211 | To provide feedback or report a bug, [please raise an issue on our issue tracker](https://github.com/auth0/wp-auth0/issues). 212 | 213 | ### Vulnerability Reporting 214 | 215 | Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues. 216 | 217 | --- 218 | 219 |

220 | 221 | 222 | 223 | Auth0 Logo 224 | 225 |

226 | 227 |

Auth0 is an easy-to-implement, adaptable authentication and authorization platform.
228 | To learn more checkout Why Auth0?

229 | 230 |

This project is licensed under the MIT license. See the LICENSE file for more info.

231 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is used to build and package the plugin for distribution. 4 | # It will clean up the environment, execute Composer, prefix dependencies, finalize the build, archive the build, and sign the archive. 5 | 6 | function trim() { 7 | local str="$*" 8 | str="${str#"${str%%[![:space:]]*}"}" 9 | str="${str%"${str##*[![:space:]]}"}" 10 | echo "${str}" 11 | } 12 | 13 | function semver { 14 | local SEMVER_REGEX='^([0-9]+\.){2}(\*|[0-9]+)(-.*)?$' 15 | local version=$(trim $1) 16 | 17 | if [[ "$version" =~ $SEMVER_REGEX ]]; then 18 | if [ "$#" -eq 2 ]; then 19 | local major=${BASH_REMATCH[0]} 20 | local minor=${BASH_REMATCH[1]} 21 | local patch=${BASH_REMATCH[2]} 22 | local suffix=${BASH_REMATCH[3]} 23 | eval "$2=(\"$major\" \"$minor\" \"$patch\" \"$suffix\")" 24 | fi 25 | else 26 | echo "Error: version '$version' does not match the semver 'X.Y.Z' format." 27 | exit 1 28 | fi 29 | } 30 | 31 | printf 'What version are you building? ' 32 | read version 33 | echo "Version: $version" 34 | semver "$version" version 35 | filename="Auth0_WordPress_${version}.zip" 36 | 37 | echo "# Cleaning up environment..." 38 | rm -f build.zip 39 | rm -f build.zip.sig 40 | rm -rf build 41 | rm -rf vendor 42 | rm composer.lock 43 | 44 | echo "# Executing Composer..." 45 | composer update --no-plugins 46 | 47 | echo "# Prefixing Dependencies..." 48 | vendor/bin/php-scoper add-prefix --force 49 | 50 | echo "# Finalizing Build..." 51 | cd build 52 | composer update --no-dev --optimize-autoloader --no-plugins 53 | rm composer.json 54 | rm composer.lock 55 | cd .. 56 | 57 | echo "# Archiving Build..." 58 | zip -vr ${filename} build/ -x "*.DS_Store" 59 | 60 | echo "# Signing Build..." 61 | openssl dgst -sign private-signing-key.pem -sha256 -out ${filename}.sig -binary ${filename} 62 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth0/wordpress", 3 | "description": "WordPress Plugin for Auth0", 4 | "license": "MIT", 5 | "type": "wordpress-plugin", 6 | "keywords": [ 7 | "auth0", 8 | "authentication", 9 | "authorization", 10 | "login", 11 | "auth", 12 | "jwt", 13 | "json web token", 14 | "jwk", 15 | "json web key", 16 | "oauth", 17 | "openid", 18 | "secure", 19 | "protect", 20 | "api" 21 | ], 22 | "authors": [ 23 | { 24 | "name": "Auth0", 25 | "email": "support@auth0.com", 26 | "homepage": "https://auth0.com/" 27 | } 28 | ], 29 | "support": { 30 | "issues": "https://github.com/auth0/wp-auth0/issues", 31 | "forum": "https://community.auth0.com/tags/wordpress", 32 | "source": "https://github.com/auth0/wp-auth0", 33 | "docs": "https://auth0.com/docs/customize/integrations/cms/wordpress-plugin" 34 | }, 35 | "require": { 36 | "php": "^8.1", 37 | "ext-json": "*", 38 | "ext-openssl": "*", 39 | "auth0/auth0-php": "^8.14", 40 | "psr/cache": "^3.0" 41 | }, 42 | "require-dev": { 43 | "humbug/php-scoper": "^0.18", 44 | "buggregator/trap": "^1", 45 | "ergebnis/composer-normalize": "^2", 46 | "friendsofphp/php-cs-fixer": "^3", 47 | "hyperf/event": "^2", 48 | "mockery/mockery": "^1", 49 | "nyholm/psr7": "^1", 50 | "pestphp/pest": "^2", 51 | "phpstan/phpstan": "^1", 52 | "phpstan/phpstan-strict-rules": "^1", 53 | "psr-mock/http": "^1", 54 | "rector/rector": "0.17.0", 55 | "symfony/cache": "^6", 56 | "szepeviktor/phpstan-wordpress": "^1", 57 | "vimeo/psalm": "^5", 58 | "wikimedia/composer-merge-plugin": "^2" 59 | }, 60 | "prefer-stable": true, 61 | "autoload": { 62 | "psr-4": { 63 | "Auth0\\WordPress\\": "src/" 64 | } 65 | }, 66 | "autoload-dev": { 67 | "psr-4": { 68 | "Auth0\\Tests\\": "tests/" 69 | } 70 | }, 71 | "config": { 72 | "allow-plugins": { 73 | "ergebnis/composer-normalize": true, 74 | "pestphp/pest-plugin": true, 75 | "php-http/discovery": false, 76 | "wikimedia/composer-merge-plugin": true 77 | }, 78 | "optimize-autoloader": true, 79 | "preferred-install": "dist", 80 | "process-timeout": 0, 81 | "sort-packages": true 82 | }, 83 | "extra": { 84 | "merge-plugin": { 85 | "ignore-duplicates": false, 86 | "include": [ 87 | "composer.local.json" 88 | ], 89 | "merge-dev": true, 90 | "merge-extra": false, 91 | "merge-extra-deep": false, 92 | "merge-scripts": false, 93 | "recurse": true, 94 | "replace": true 95 | } 96 | }, 97 | "scripts": { 98 | "build": "./build.sh", 99 | "pest": "@php vendor/bin/pest --order-by random --fail-on-risky --parallel --no-progress", 100 | "pest:coverage": "@php vendor/bin/pest --order-by random --fail-on-risky --coverage --parallel --no-progress", 101 | "pest:debug": "@php vendor/bin/pest --log-events-verbose-text pest.log --display-errors --fail-on-risky --no-progress", 102 | "pest:profile": "@php vendor/bin/pest --profile", 103 | "phpcs": "@php vendor/bin/php-cs-fixer fix --dry-run --diff", 104 | "phpcs:fix": "@php vendor/bin/php-cs-fixer fix", 105 | "phpstan": "@php vendor/bin/phpstan analyze", 106 | "psalm": "@php vendor/bin/psalm", 107 | "psalm:fix": "@php vendor/bin/psalter --issues=all", 108 | "rector": "@php vendor/bin/rector process src --dry-run", 109 | "rector:fix": "@php vendor/bin/rector process src", 110 | "test": [ 111 | "@pest", 112 | "@phpstan", 113 | "@psalm", 114 | "@rector", 115 | "@phpcs" 116 | ] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /opslevel.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | repository: 4 | owner: dx_sdks 5 | tags: 6 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-strict-rules/rules.neon 3 | - vendor/szepeviktor/phpstan-wordpress/extension.neon 4 | 5 | parameters: 6 | level: max 7 | 8 | paths: 9 | - src 10 | - wpAuth0.php 11 | 12 | ignoreErrors: 13 | - '#Constructor of class (.*) has an unused parameter (.*).#' 14 | - '#Method (.*) has parameter (.*) with no value type specified in iterable type array.#' 15 | - '#no value type specified in iterable type array.#' 16 | 17 | reportUnmatchedIgnoredErrors: false 18 | treatPhpDocTypesAsCertain: false 19 | checkGenericClassInNonGenericObjectType: false 20 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | tests/Unit 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ./src/ 20 | 21 | 22 | ./src/Events/ 23 | ./src/Exceptions/ 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /psalm.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public-signing-key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwXT8DjGHLtj7cW4vsNQL 3 | vrGyzmUaIcmvXjB9ajTOJw8/46AUXh/NTeMnJR5qFmFnvION+iYqhgsXui3s80nA 4 | R5GjO/ceKeqATF8LPGpATgpBViwq2G1wD52VoxCOtj5B+0ITwa94tnt5u3/06TOe 5 | VSdlYpf5pUYZhvRASaKasRCH3q5RAjlQWniAZV236W7S2MLAV3/wv3lrgFklj2QV 6 | +Q6c7Qij4t3ZAUt5StfBQuem05o3I5jWxoMkC5tNRU6FJ81cBv7jXoHl8tW9v3QP 7 | EMToXfZzIgBJZRh811TuYw1ZlEYe5vNz/v4batN+6CnC6byOktcBj0H+DljwtBcN 8 | NCb7+6CgzlTGsaY5Ode4YnaPHoeCJZy51/MFZelRHhBMwpqflxL5WO0WNa/WzU2o 9 | NAi3OXqSvSS4QTLUAxA104VJXeT62SY9JvEZRX+fYh+BkXiiEn00alrgN27pZumR 10 | AEtiryDySD/VlmWAph0vOp1LQmpdWhDRRJS+Di82wjlBNPwaMg3nCRQnMnh2GagM 11 | OZtQ2TBpXdjpvBsoR86qZurcD2S4jGeEmVbjhDhLs7NSWUWTzks+zmf0AzoRDrjf 12 | gy78UqciVM3FC/RGvjWbWGLxirnP89xspwWBwcnbYDed3YFC9y5Kciqe3E+sZq0R 13 | OcHN4UlU1ChXtKdfMD9AYlMCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 236 | __DIR__ . '/config', 237 | __DIR__ . '/src', 238 | ]); 239 | 240 | $rectorConfig->ruleWithConfiguration( 241 | RenameFunctionRector::class, 242 | [ 243 | 'chop' => 'rtrim', 244 | 'doubleval' => 'floatval', 245 | 'fputs' => 'fwrite', 246 | 'gzputs' => 'gzwrites', 247 | 'ini_alter' => 'ini_set', 248 | 'is_double' => 'is_float', 249 | 'is_integer' => 'is_int', 250 | 'is_long' => 'is_int', 251 | 'is_real' => 'is_float', 252 | 'is_writeable' => 'is_writable', 253 | 'join' => 'implode', 254 | 'key_exists' => 'array_key_exists', 255 | 'mbstrcut' => 'mb_strcut', 256 | 'mbstrlen' => 'mb_strlen', 257 | 'mbstrpos' => 'mb_strpos', 258 | 'mbstrrpos' => 'mb_strrpos', 259 | 'mbsubstr' => 'mb_substr', 260 | 'pos' => 'current', 261 | 'sizeof' => 'count', 262 | 'split' => 'explode', 263 | 'strchr' => 'strstr', 264 | ], 265 | ); 266 | 267 | $rectorConfig->ruleWithConfiguration( 268 | StaticCallToFuncCallRector::class, 269 | [ 270 | new StaticCallToFuncCall('Nette\\Utils\\Strings', 'contains', 'str_contains'), 271 | new StaticCallToFuncCall('Nette\\Utils\\Strings', 'endsWith', 'str_ends_with'), 272 | new StaticCallToFuncCall('Nette\\Utils\\Strings', 'startsWith', 'str_starts_with'), 273 | ], 274 | ); 275 | 276 | $rectorConfig->ruleWithConfiguration( 277 | ArgumentAdderRector::class, 278 | [new ArgumentAdder('Nette\\Utils\\Strings', 'replace', 2, 'replacement', '')], 279 | ); 280 | 281 | $rectorConfig->ruleWithConfiguration( 282 | RenameFunctionRector::class, 283 | [ 284 | 'pg_clientencoding' => 'pg_client_encoding', 285 | 'pg_cmdtuples' => 'pg_affected_rows', 286 | 'pg_errormessage' => 'pg_last_error', 287 | 'pg_fieldisnull' => 'pg_field_is_null', 288 | 'pg_fieldname' => 'pg_field_name', 289 | 'pg_fieldnum' => 'pg_field_num', 290 | 'pg_fieldprtlen' => 'pg_field_prtlen', 291 | 'pg_fieldsize' => 'pg_field_size', 292 | 'pg_fieldtype' => 'pg_field_type', 293 | 'pg_freeresult' => 'pg_free_result', 294 | 'pg_getlastoid' => 'pg_last_oid', 295 | 'pg_loclose' => 'pg_lo_close', 296 | 'pg_locreate' => 'pg_lo_create', 297 | 'pg_loexport' => 'pg_lo_export', 298 | 'pg_loimport' => 'pg_lo_import', 299 | 'pg_loopen' => 'pg_lo_open', 300 | 'pg_loread' => 'pg_lo_read', 301 | 'pg_loreadall' => 'pg_lo_read_all', 302 | 'pg_lounlink' => 'pg_lo_unlink', 303 | 'pg_lowrite' => 'pg_lo_write', 304 | 'pg_numfields' => 'pg_num_fields', 305 | 'pg_numrows' => 'pg_num_rows', 306 | 'pg_result' => 'pg_fetch_result', 307 | 'pg_setclientencoding' => 'pg_set_client_encoding' 308 | ], 309 | ); 310 | 311 | $rectorConfig->ruleWithConfiguration( 312 | FunctionArgumentDefaultValueReplacerRector::class, 313 | [ 314 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'), 315 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'), 316 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '', '!='), 317 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, '!', '!='), 318 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'g', 'gt'), 319 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'l', 'lt'), 320 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'gte', 'ge'), 321 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'lte', 'le'), 322 | new ReplaceFuncCallArgumentDefaultValue('version_compare', 2, 'n', 'ne') 323 | ], 324 | ); 325 | 326 | $rectorConfig->ruleWithConfiguration( 327 | FuncCallToConstFetchRector::class, 328 | [ 329 | 'php_sapi_name' => 'PHP_SAPI', 330 | 'pi' => 'M_PI' 331 | ], 332 | ); 333 | 334 | $rectorConfig->rules([ 335 | AbsolutizeRequireAndIncludePathRector::class, 336 | // ActionInjectionToConstructorInjectionRector::class, 337 | AddArrayDefaultToArrayPropertyRector::class, 338 | AddArrowFunctionReturnTypeRector::class, 339 | AddClosureReturnTypeRector::class, 340 | // AddFalseDefaultToBoolPropertyRector::class, 341 | AddMethodCallBasedStrictParamTypeRector::class, 342 | AddParamBasedOnParentClassMethodRector::class, 343 | AddParamTypeBasedOnPHPUnitDataProviderRector::class, 344 | AddParamTypeSplFixedArrayRector::class, 345 | // AddPregQuoteDelimiterRector::class, 346 | AddReturnTypeDeclarationBasedOnParentClassMethodRector::class, 347 | AddReturnTypeDeclarationFromYieldsRector::class, 348 | AddVoidReturnTypeWhereNoReturnRector::class, 349 | AndAssignsToSeparateLinesRector::class, 350 | ArrayKeyExistsTernaryThenValueToCoalescingRector::class, 351 | // ArrayKeysAndInArrayToArrayKeyExistsRector::class, 352 | ArrayMergeOfNonArraysToSimpleArrayRector::class, 353 | // ArrayShapeFromConstantArrayReturnRector::class, 354 | BinarySwitchToIfElseRector::class, 355 | BooleanNotIdenticalToNotIdenticalRector::class, 356 | BoolvalToTypeCastRector::class, 357 | CallableThisArrayToAnonymousFunctionRector::class, 358 | CallUserFuncArrayToVariadicRector::class, 359 | CallUserFuncToMethodCallRector::class, 360 | CallUserFuncWithArrowFunctionToInlineRector::class, 361 | CatchExceptionNameMatchingTypeRector::class, 362 | ChangeArrayPushToArrayAssignRector::class, 363 | // ChangeGlobalVariablesToPropertiesRector::class, 364 | ChangeIfElseValueAssignToEarlyReturnRector::class, 365 | ChangeNestedForeachIfsToEarlyContinueRector::class, 366 | ChangeNestedIfsToEarlyReturnRector::class, 367 | ChangeOrIfContinueToMultiContinueRector::class, 368 | // ChangeReadOnlyPropertyWithDefaultValueToConstantRector::class, 369 | // ChangeReadOnlyVariableWithDefaultValueToConstantRector::class, 370 | ChangeSwitchToMatchRector::class, 371 | ClassOnObjectRector::class, 372 | ClassOnThisVariableObjectRector::class, 373 | ClassPropertyAssignToConstructorPromotionRector::class, 374 | CombinedAssignRector::class, 375 | CombineIfRector::class, 376 | CommonNotEqualRector::class, 377 | CompactToVariablesRector::class, 378 | CompleteDynamicPropertiesRector::class, 379 | ConsecutiveNullCompareReturnsToNullCoalesceQueueRector::class, 380 | ConsistentImplodeRector::class, 381 | // ConsistentPregDelimiterRector::class, 382 | CountArrayToEmptyArrayComparisonRector::class, 383 | EmptyOnNullableObjectToInstanceOfRector::class, 384 | EncapsedStringsToSprintfRector::class, 385 | ExplicitBoolCompareRector::class, 386 | // ExplicitMethodCallOverMagicGetSetRector::class, 387 | FinalizeClassesWithoutChildrenRector::class, 388 | FinalPrivateToPrivateVisibilityRector::class, 389 | FlipTypeControlToUseExclusiveTypeRector::class, 390 | FloatvalToTypeCastRector::class, 391 | ForeachItemsAssignToEmptyArrayToAssignRector::class, 392 | ForeachToInArrayRector::class, 393 | ForRepeatedCountToOwnVariableRector::class, 394 | // ForToForeachRector::class, 395 | FuncGetArgsToVariadicParamRector::class, 396 | GetClassToInstanceOfRector::class, 397 | GetDebugTypeRector::class, 398 | InlineArrayReturnAssignRector::class, 399 | InlineConstructorDefaultToPropertyRector::class, 400 | InlineIfToExplicitIfRector::class, 401 | InlineIsAInstanceOfRector::class, 402 | IntvalToTypeCastRector::class, 403 | IsAWithStringWithThirdArgumentRector::class, 404 | IssetOnPropertyObjectToPropertyExistsRector::class, 405 | JoinStringConcatRector::class, 406 | LogicalToBooleanRector::class, 407 | MakeInheritedMethodVisibilitySameAsParentRector::class, 408 | // MultipleClassFileToPsr4ClassesRector::class, 409 | // NarrowUnionTypeDocRector::class, 410 | NewlineBeforeNewAssignSetRector::class, 411 | NewStaticToNewSelfRector::class, 412 | // NormalizeNamespaceByPSR4ComposerAutoloadRector::class, 413 | NullableCompareToNullRector::class, 414 | OptionalParametersAfterRequiredRector::class, 415 | // ParamAnnotationIncorrectNullableRector::class, 416 | ParamTypeByMethodCallTypeRector::class, 417 | ParamTypeByParentCallTypeRector::class, 418 | ParamTypeFromStrictTypedPropertyRector::class, 419 | // Php8ResourceReturnToObjectRector::class, 420 | PostIncDecToPreIncDecRector::class, 421 | PrivatizeFinalClassMethodRector::class, 422 | PrivatizeFinalClassPropertyRector::class, 423 | PropertyTypeFromStrictSetterGetterRector::class, 424 | RemoveAlwaysElseRector::class, 425 | // RemoveAlwaysTrueConditionSetInConstructorRector::class, 426 | RemoveAndTrueRector::class, 427 | RemoveDeadConditionAboveReturnRector::class, 428 | RemoveDeadContinueRector::class, 429 | RemoveDeadIfForeachForRector::class, 430 | RemoveDeadLoopRector::class, 431 | RemoveDeadReturnRector::class, 432 | RemoveDeadStmtRector::class, 433 | RemoveDeadTryCatchRector::class, 434 | RemoveDeadZeroAndOneOperationRector::class, 435 | // RemoveDelegatingParentCallRector::class, 436 | RemoveDoubleAssignRector::class, 437 | // RemoveDoubleUnderscoreInMethodNameRector::class, 438 | RemoveDuplicatedArrayKeyRector::class, 439 | RemoveDuplicatedCaseInSwitchRector::class, 440 | // RemoveDuplicatedIfReturnRector::class, 441 | // RemoveDuplicatedInstanceOfRector::class, 442 | RemoveEmptyClassMethodRector::class, 443 | // RemoveEmptyMethodCallRector::class, 444 | // RemoveEmptyTestMethodRector::class, 445 | RemoveExtraParametersRector::class, 446 | RemoveFinalFromConstRector::class, 447 | RemoveJustPropertyFetchForAssignRector::class, 448 | // RemoveJustVariableAssignRector::class, 449 | // RemoveLastReturnRector::class, 450 | // RemoveNonExistingVarAnnotationRector::class, 451 | RemoveNullPropertyInitializationRector::class, 452 | RemoveParentCallWithoutParentRector::class, 453 | RemoveSoleValueSprintfRector::class, 454 | RemoveUnreachableStatementRector::class, 455 | RemoveUnusedConstructorParamRector::class, 456 | RemoveUnusedForeachKeyRector::class, 457 | RemoveUnusedNonEmptyArrayBeforeForeachRector::class, 458 | RemoveUnusedPrivateClassConstantRector::class, 459 | RemoveUnusedPrivateMethodParameterRector::class, 460 | RemoveUnusedPrivatePropertyRector::class, 461 | RemoveUnusedPromotedPropertyRector::class, 462 | RemoveUnusedVariableAssignRector::class, 463 | RemoveUnusedVariableInCatchRector::class, 464 | RemoveUselessReturnTagRector::class, 465 | RemoveUselessVarTagRector::class, 466 | RenameForeachValueVariableToMatchExprVariableRector::class, 467 | RenameForeachValueVariableToMatchMethodCallReturnTypeRector::class, 468 | ReplaceMultipleBooleanNotRector::class, 469 | ReturnAnnotationIncorrectNullableRector::class, 470 | // ReturnBinaryAndToEarlyReturnRector::class, 471 | ReturnBinaryOrToEarlyReturnRector::class, 472 | ReturnEarlyIfVariableRector::class, 473 | ReturnNeverTypeRector::class, 474 | ReturnTypeFromReturnDirectArrayRector::class, 475 | ReturnTypeFromReturnNewRector::class, 476 | ReturnTypeFromStrictBoolReturnExprRector::class, 477 | ReturnTypeFromStrictConstantReturnRector::class, 478 | ReturnTypeFromStrictNativeCallRector::class, 479 | ReturnTypeFromStrictNewArrayRector::class, 480 | ReturnTypeFromStrictScalarReturnExprRector::class, 481 | ReturnTypeFromStrictTernaryRector::class, 482 | ReturnTypeFromStrictTypedCallRector::class, 483 | ReturnTypeFromStrictTypedPropertyRector::class, 484 | SeparateMultiUseImportsRector::class, 485 | SetStateToStaticRector::class, 486 | SetTypeToCastRector::class, 487 | ShortenElseIfRector::class, 488 | SimplifyArraySearchRector::class, 489 | SimplifyBoolIdenticalTrueRector::class, 490 | SimplifyConditionsRector::class, 491 | SimplifyDeMorganBinaryRector::class, 492 | SimplifyEmptyArrayCheckRector::class, 493 | SimplifyEmptyCheckOnEmptyArrayRector::class, 494 | // SimplifyForeachToArrayFilterRector::class, 495 | SimplifyForeachToCoalescingRector::class, 496 | SimplifyFuncGetArgsCountRector::class, 497 | SimplifyIfElseToTernaryRector::class, 498 | SimplifyIfElseWithSameContentRector::class, 499 | // SimplifyIfExactValueReturnValueRector::class, 500 | SimplifyIfNotNullReturnRector::class, 501 | SimplifyIfNullableReturnRector::class, 502 | SimplifyIfReturnBoolRector::class, 503 | SimplifyInArrayValuesRector::class, 504 | SimplifyMirrorAssignRector::class, 505 | SimplifyRegexPatternRector::class, 506 | SimplifyStrposLowerRector::class, 507 | SimplifyTautologyTernaryRector::class, 508 | // SimplifyUselessLastVariableAssignRector::class, 509 | SimplifyUselessVariableRector::class, 510 | SingleInArrayToCompareRector::class, 511 | SingularSwitchToIfRector::class, 512 | SplitDoubleAssignRector::class, 513 | SplitGroupedClassConstantsRector::class, 514 | SplitGroupedPropertiesRector::class, 515 | // SplitListAssignToSeparateLineRector::class, 516 | StaticArrowFunctionRector::class, 517 | StaticClosureRector::class, 518 | StrContainsRector::class, 519 | StrEndsWithRector::class, 520 | StrictArraySearchRector::class, 521 | StringableForToStringRector::class, 522 | StrlenZeroToIdenticalEmptyStringRector::class, 523 | StrStartsWithRector::class, 524 | StrvalToTypeCastRector::class, 525 | SwitchNegatedTernaryRector::class, 526 | SymplifyQuoteEscapeRector::class, 527 | TernaryConditionVariableAssignmentRector::class, 528 | TernaryEmptyArrayArrayDimFetchToCoalesceRector::class, 529 | TernaryFalseExpressionToIfRector::class, 530 | TernaryToBooleanOrFalseToBooleanAndRector::class, 531 | ThrowWithPreviousExceptionRector::class, 532 | // TokenGetAllToObjectRector::class, 533 | TypedPropertyFromAssignsRector::class, 534 | TypedPropertyFromStrictConstructorRector::class, 535 | TypedPropertyFromStrictGetterMethodReturnTypeRector::class, 536 | TypedPropertyFromStrictSetUpRector::class, 537 | UnnecessaryTernaryExpressionRector::class, 538 | UnSpreadOperatorRector::class, 539 | UnusedForeachValueToArrayKeysRector::class, 540 | UnwrapFutureCompatibleIfPhpVersionRector::class, 541 | UnwrapSprintfOneArgumentRector::class, 542 | UseClassKeywordForClassNameResolutionRector::class, 543 | UseIdenticalOverEqualWithSameTypeRector::class, 544 | UseIncrementAssignRector::class, 545 | // VarAnnotationIncorrectNullableRector::class, 546 | // VarConstantCommentRector::class, 547 | VarToPublicPropertyRector::class, 548 | VersionCompareFuncCallToConstantRector::class, 549 | WrapEncapsedVariableInCurlyBracesRector::class, 550 | ]); 551 | }; 552 | -------------------------------------------------------------------------------- /scoper.inc.php: -------------------------------------------------------------------------------- 1 | 'Auth0\\WordPress\\Vendor', 9 | 10 | 'finders' => [ 11 | Finder::create() 12 | ->files() 13 | ->ignoreVCS(true) 14 | ->notName('/.*\\.dist|Makefile|scoper.inc.php|rector.php|opslevel.yml|build.sh|public-signing-key.pub|composer.json|composer.lock/') 15 | ->exclude([ 16 | 'doc', 17 | 'test', 18 | 'test_old', 19 | 'tests', 20 | 'Tests', 21 | 'vendor-bin', 22 | ]) 23 | ->in(['vendor', '.']), 24 | 25 | Finder::create()->append([ 26 | 'composer.json', 27 | ]), 28 | ], 29 | 30 | 'exclude-namespaces' => [ 31 | '/^Auth0\\\\WordPress\\\\/', 32 | '/^Auth0\\\\WordPress/', 33 | '/^Psr\\\/', 34 | ], 35 | 36 | 'expose-global-constants' => false, 37 | 'expose-global-classes' => false, 38 | 'expose-global-functions' => false, 39 | 40 | 'patchers' => [], 41 | ]; 42 | -------------------------------------------------------------------------------- /src/Actions/Authentication.php: -------------------------------------------------------------------------------- 1 | |string> 24 | */ 25 | protected array $registry = [ 26 | 'init' => 'onInit', 27 | // 'rest_api_init' => 'onInit', 28 | // 'admin_init' => 'onInit', 29 | // 'shutdown' => 'onShutdown', 30 | 'send_headers' => 'onShutdown', 31 | 32 | 'auth_cookie_expiration' => ['onAuthCookieAssignExpiration', 3], 33 | 'auth_cookie_malformed' => ['onAuthCookieMalformed', 2], 34 | 'auth_cookie_expired' => 'onAuthCookieExpired', 35 | 'auth_cookie_bad_username' => 'onAuthCookieBadUsername', 36 | 'auth_cookie_bad_session_token' => 'onAuthCookieBadSessionToken', 37 | 'auth_cookie_bad_hash' => 'onAuthCookieBadHash', 38 | 39 | 'login_form_login' => 'onLogin', 40 | 'auth0_login_callback' => 'onLogin', 41 | 42 | 'login_form_logout' => 'onLogout', 43 | 'auth0_logout' => 'onLogout', 44 | 'auth0_token_exchange_failed' => 'onExchangeFailed', 45 | 46 | 'before_signup_header' => 'onRegistration', 47 | 48 | 'edit_user_created_user' => ['onCreatedUser', 2], 49 | 'deleted_user' => 'onDeletedUser', 50 | 'profile_update' => ['onUpdatedUser', 2], 51 | ]; 52 | 53 | public function createAccountConnection(WP_User $wpUser, string $connection): void 54 | { 55 | $network = get_current_network_id(); 56 | $blog = get_current_blog_id(); 57 | $cacheKey = 'auth0_account_' . hash('sha256', $connection . '::' . $network . '!' . $blog); 58 | 59 | $found = false; 60 | wp_cache_get($cacheKey, '', false, $found); 61 | 62 | if (! $found && false === get_transient($cacheKey)) { 63 | $database = $this->getPlugin()->database(); 64 | $table = $database->getTableName(Database::CONST_TABLE_ACCOUNTS); 65 | $found = null; 66 | 67 | $this->prepDatabase(Database::CONST_TABLE_ACCOUNTS); 68 | 69 | $found = $database->selectRow('*', $table, 'WHERE `user` = %d AND `site` = %d AND `blog` = %d AND `auth0` = "%s" LIMIT 1', [$wpUser->ID, $network, $blog, $connection]); 70 | 71 | if (null === $found) { 72 | set_transient($cacheKey, $wpUser->ID, 120); 73 | wp_cache_set($cacheKey, $found, 120); 74 | 75 | $database->insertRow($table, [ 76 | 'user' => $wpUser->ID, 77 | 'site' => $network, 78 | 'blog' => $blog, 79 | 'auth0' => $connection, 80 | ], [ 81 | '%d', 82 | '%d', 83 | '%d', 84 | '%s', 85 | ]); 86 | } 87 | } 88 | } 89 | 90 | public function deleteAccountConnections(int $userId): ?array 91 | { 92 | $database = $this->getPlugin()->database(); 93 | $table = $database->getTableName(Database::CONST_TABLE_ACCOUNTS); 94 | $network = get_current_network_id(); 95 | $blog = get_current_blog_id(); 96 | 97 | $this->prepDatabase(Database::CONST_TABLE_ACCOUNTS); 98 | 99 | $connections = $database->selectResults('auth0', $table, 'WHERE `site` = %d AND `blog` = %d AND `user` = "%s" LIMIT 1', [$network, $blog, $userId]); 100 | 101 | if ($connections) { 102 | $database->deleteRow($table, ['user' => $userId, 'site' => $network, 'blog' => $blog], ['%d', '%s', '%s']); 103 | wp_cache_flush(); 104 | 105 | return $connections; 106 | } 107 | 108 | return null; 109 | } 110 | 111 | public function getAccountByConnection(string $connection): ?WP_User 112 | { 113 | $network = get_current_network_id(); 114 | $blog = get_current_blog_id(); 115 | $cacheKey = 'auth0_account_' . hash('sha256', $connection . '::' . $network . '!' . $blog); 116 | 117 | $found = false; 118 | $user = wp_cache_get($cacheKey, '', false, $found); 119 | 120 | if ($found) { 121 | $found = $user; 122 | } 123 | 124 | if (! $found) { 125 | $found = get_transient($cacheKey); 126 | 127 | if (false === $found) { 128 | $database = $this->getPlugin()->database(); 129 | $table = $database->getTableName(Database::CONST_TABLE_ACCOUNTS); 130 | 131 | $this->prepDatabase(Database::CONST_TABLE_ACCOUNTS); 132 | $found = $database->selectRow('user', $table, 'WHERE `site` = %d AND `blog` = %d AND `auth0` = "%s" LIMIT 1', [$network, $blog, $connection]); 133 | 134 | if (null === $found) { 135 | return null; 136 | } 137 | 138 | $found = $found->user; 139 | } 140 | } 141 | 142 | if ($found) { 143 | set_transient($cacheKey, $found, 120); 144 | wp_cache_set($cacheKey, $found, 120); 145 | 146 | $user = get_user_by('ID', $found); 147 | } 148 | 149 | if (false === $user) { 150 | return null; 151 | } 152 | 153 | return $user; 154 | } 155 | 156 | public function getAccountConnections(int $userId): ?array 157 | { 158 | $database = $this->getPlugin()->database(); 159 | $table = $database->getTableName(Database::CONST_TABLE_ACCOUNTS); 160 | $network = get_current_network_id(); 161 | $blog = get_current_blog_id(); 162 | 163 | $this->prepDatabase(Database::CONST_TABLE_ACCOUNTS); 164 | 165 | $connections = $database->selectResults('auth0', $table, 'WHERE `site` = %d AND `blog` = %d AND `user` = "%s" LIMIT 1', [$network, $blog, $userId]); 166 | 167 | if ($connections) { 168 | return $connections; 169 | } 170 | 171 | return null; 172 | } 173 | 174 | /** 175 | * Fires when 'auth_cookie_expiration' is triggered by WordPress. 176 | * 177 | * @link https://developer.wordpress.org/reference/hooks/auth_cookie_expiration/ 178 | * 179 | * @param int $length 180 | * @param int $user_id 181 | * @param bool $remember 182 | */ 183 | public function onAuthCookieAssignExpiration(int $length, int $user_id, bool $remember): int 184 | { 185 | if ($remember) { 186 | $ttl = $this->getPlugin()->getOptionInteger('cookies', 'ttl') ?? 0; 187 | 188 | return $ttl > 0 ? $ttl : $length; 189 | } 190 | 191 | return $length; 192 | } 193 | 194 | /** 195 | * Fires when 'auth_cookie_bad_hash' is triggered by WordPress. 196 | * 197 | * @link https://developer.wordpress.org/reference/hooks/auth_cookie_bad_hash/ 198 | * 199 | * @param array $cookieElements 200 | */ 201 | public function onAuthCookieBadHash(array $cookieElements): void 202 | { 203 | $this->getSdk() 204 | ->clear(); 205 | } 206 | 207 | /** 208 | * Fires when 'auth_cookie_bad_session_token' is triggered by WordPress. 209 | * 210 | * @link https://developer.wordpress.org/reference/hooks/auth_cookie_bad_session_token/ 211 | * 212 | * @param array $cookieElements 213 | */ 214 | public function onAuthCookieBadSessionToken(array $cookieElements): void 215 | { 216 | $this->getSdk() 217 | ->clear(); 218 | } 219 | 220 | /** 221 | * Fires when 'auth_cookie_bad_username' is triggered by WordPress. 222 | * 223 | * @link https://developer.wordpress.org/reference/hooks/auth_cookie_bad_username/ 224 | * 225 | * @param array $cookieElements 226 | */ 227 | public function onAuthCookieBadUsername(array $cookieElements): void 228 | { 229 | $this->getSdk() 230 | ->clear(); 231 | } 232 | 233 | /** 234 | * Fires when 'auth_cookie_expired' is triggered by WordPress. 235 | * 236 | * @link https://developer.wordpress.org/reference/hooks/auth_cookie_expired/ 237 | * 238 | * @param array $cookieElements 239 | */ 240 | public function onAuthCookieExpired(array $cookieElements): void 241 | { 242 | $this->getSdk() 243 | ->clear(); 244 | } 245 | 246 | /** 247 | * Fires when 'auth_cookie_malformed' is triggered by WordPress. 248 | * 249 | * @link https://developer.wordpress.org/reference/hooks/auth_cookie_malformed/ 250 | * 251 | * @param string $cookie 252 | * @param ?string $scheme 253 | */ 254 | public function onAuthCookieMalformed(string $cookie, ?string $scheme = null): void 255 | { 256 | if ('' === $cookie) { 257 | return; 258 | } 259 | 260 | $this->getSdk() 261 | ->clear(); 262 | } 263 | 264 | /** 265 | * Note that this ONLY fires for users created via WordPress' UI, like the "Add New" button from the Admin -> Users page. 266 | * 267 | * @param mixed $userId 268 | * @param null|mixed $notify 269 | */ 270 | public function onCreatedUser($userId, $notify = null): void 271 | { 272 | if (! is_int($userId)) { 273 | return; 274 | } 275 | 276 | $network = get_current_network_id(); 277 | $blog = get_current_blog_id(); 278 | $database = $this->getPlugin()->database(); 279 | $table = $database->getTableName(Database::CONST_TABLE_SYNC); 280 | 281 | $this->prepDatabase(Database::CONST_TABLE_SYNC); 282 | 283 | $payload = json_encode([ 284 | 'event' => 'wp_user_created', 285 | 'user' => $userId, 286 | ], JSON_THROW_ON_ERROR); 287 | $checksum = hash('sha256', $payload); 288 | 289 | $dupe = $database->selectRow('id', $table, 'WHERE `hashsum` = "%s";', [$checksum]); 290 | 291 | if (! $dupe) { 292 | $database->insertRow($table, [ 293 | 'site' => $network, 294 | 'blog' => $blog, 295 | 'created' => time(), 296 | 'payload' => $payload, 297 | 'hashsum' => hash('sha256', $payload), 298 | 'locked' => 0, 299 | ], [ 300 | '%d', 301 | '%d', 302 | '%d', 303 | '%s', 304 | '%s', 305 | '%d', 306 | ]); 307 | } 308 | } 309 | 310 | public function onDeletedUser($userId): void 311 | { 312 | $connections = $this->deleteAccountConnections($userId); 313 | 314 | if (null !== $connections && [] !== $connections) { 315 | $network = get_current_network_id(); 316 | $blog = get_current_blog_id(); 317 | $database = $this->getPlugin()->database(); 318 | $table = $database->getTableName(Database::CONST_TABLE_SYNC); 319 | 320 | $this->prepDatabase(Database::CONST_TABLE_SYNC); 321 | 322 | foreach ($connections as $connection) { 323 | $payload = json_encode([ 324 | 'event' => 'wp_user_deleted', 325 | 'user' => $userId, 326 | 'connection' => $connection->auth0, 327 | ], JSON_THROW_ON_ERROR); 328 | $checksum = hash('sha256', $payload); 329 | 330 | $dupe = $database->selectRow('id', $table, 'WHERE `hashsum` = "%s";', [$checksum]); 331 | 332 | if (! $dupe) { 333 | $database->insertRow($table, [ 334 | 'site' => $network, 335 | 'blog' => $blog, 336 | 'created' => time(), 337 | 'payload' => $payload, 338 | 'hashsum' => hash('sha256', $payload), 339 | 'locked' => 0, 340 | ], [ 341 | '%d', 342 | '%d', 343 | '%d', 344 | '%s', 345 | '%s', 346 | '%d', 347 | ]); 348 | } 349 | } 350 | } 351 | } 352 | 353 | public function onInit(): void 354 | { 355 | if (! $this->getPlugin()->isEnabled()) { 356 | return; 357 | } 358 | 359 | if (! $this->getPlugin()->isReady()) { 360 | return; 361 | } 362 | 363 | if (! is_user_logged_in()) { 364 | return; 365 | } 366 | 367 | $session = $this->getSdk()->getCredentials(); 368 | $expired = $session?->accessTokenExpired ?? true; 369 | 370 | if (! $expired && (wp_is_json_request() || wp_is_rest_endpoint())) { 371 | return; 372 | } 373 | 374 | $wordpress = wp_get_current_user(); 375 | 376 | // Paired sessions enforced 377 | if (2 !== $this->getPlugin()->getOption('authentication', 'pair_sessions', 0)) { 378 | // ... for all but admins? 379 | if (0 === $this->getPlugin()->getOption('authentication', 'pair_sessions', 0) && is_admin()) { 380 | return; 381 | } 382 | 383 | // Is an Auth0 session available? 384 | if (! is_object($session) && 0 !== $wordpress->ID) { 385 | wp_logout(); 386 | 387 | return; 388 | } 389 | 390 | // Is an WP session available? 391 | if (is_object($session) && 0 === $wordpress->ID) { 392 | $this->getSdk()->clear(); 393 | 394 | return; 395 | } 396 | 397 | if (is_object($session)) { 398 | // Verify the WordPress user signed in is linked to the Auth0 Connection 'sub'. 399 | $sub = $session->user['sub'] ?? null; 400 | 401 | if (null !== $sub) { 402 | $match = $this->getAccountByConnection($sub); 403 | 404 | if (! $match instanceof WP_User || $match->ID !== $wordpress->ID) { 405 | $this->getSdk()->clear(); 406 | wp_logout(); 407 | 408 | return; 409 | } 410 | } 411 | 412 | // Verify that the Auth0 token cookie has not expired 413 | if ($expired && 'true' === $this->getPlugin()->getOption('sessions', 'refresh_tokens')) { 414 | try { 415 | // Token has expired, attempt to refresh it. 416 | $this->getSdk()->renew(); 417 | 418 | return; 419 | } catch (StateException) { 420 | // Refresh failed. 421 | } 422 | 423 | // Invalidation authentication state. 424 | $this->getSdk()->clear(); 425 | wp_logout(); 426 | 427 | return; 428 | } 429 | } 430 | } 431 | } 432 | 433 | public function onLogin(): void 434 | { 435 | if (! $this->getPlugin()->isEnabled()) { 436 | return; 437 | } 438 | 439 | if (! $this->getPlugin()->isReady()) { 440 | return; 441 | } 442 | 443 | if (isset($_GET['auth0_fb'])) { 444 | $incomingFallbackRequest = Sanitize::string($_GET['auth0_fb']); 445 | $fallbackSecret = $this->getPlugin()->getOptionString('authentication', 'fallback_secret'); 446 | 447 | if ($incomingFallbackRequest === $fallbackSecret) { 448 | return; 449 | } 450 | 451 | // Ignore invalid requests; continue as normal. 452 | } 453 | 454 | if (isset($_GET['auth0_bcl'], $_POST['logout_token'])) { 455 | $incomingBackchannelLogoutRequest = Sanitize::string($_GET['auth0_bcl']); 456 | $backchannelLogoutSecret = $this->getPlugin()->getOptionString('authentication', 'backchannel_logout_secret'); 457 | 458 | if ($incomingBackchannelLogoutRequest === $backchannelLogoutSecret) { 459 | $logoutToken = Sanitize::string($_POST['logout_token']); 460 | 461 | try { 462 | $this->getSdk()->handleBackchannelLogout($logoutToken); 463 | exit(); 464 | } catch (Throwable) { 465 | } 466 | } 467 | 468 | // Ignore invalid requests; continue as normal. 469 | } 470 | 471 | // Don't allow caching of this route 472 | nocache_headers(); 473 | 474 | // Check if authentication flow parameters are present (?code and ?state) 475 | $code = $this->getSdk()->getRequestParameter('code'); 476 | $state = $this->getSdk()->getRequestParameter('state'); 477 | $exchangeParameters = null !== $code && null !== $state; 478 | 479 | // Check if authentication flow error parameter is present (?error) 480 | $error = $this->getSdk() 481 | ->getRequestParameter('error'); 482 | 483 | // Are token exchange parameters present? 484 | if ($exchangeParameters) { 485 | try { 486 | // Attempt completion of the authentication flow using 487 | $this->getSdk() 488 | ->exchange( 489 | code: sanitize_text_field($code), 490 | state: sanitize_text_field($state), 491 | ); 492 | } catch (Throwable $throwable) { 493 | // Exchange failed; throw an error 494 | try { 495 | error_log($throwable->getMessage()); 496 | } catch (Throwable) { 497 | } 498 | 499 | do_action('auth0_token_exchange_failed', $throwable); 500 | return; 501 | } 502 | 503 | $session = $this->getSdk() 504 | ->getCredentials(); 505 | 506 | // Do we indeed have a session now? 507 | if (null !== $session) { 508 | $sub = sanitize_text_field($session->user['sub'] ?? ''); 509 | $email = sanitize_email($session->user['email'] ?? ''); 510 | $verified = $session->user['email_verified'] ?? null; 511 | 512 | if ('' === $email) { 513 | $email = null; 514 | $verified = null; 515 | } 516 | 517 | $wpUser = $this->resolveIdentity(sub: $sub, email: $email, verified: $verified); 518 | 519 | if ($wpUser instanceof WP_User) { 520 | if ('' !== $sub) { 521 | $this->createAccountConnection($wpUser, $sub); 522 | } 523 | 524 | if (null !== $email && true === $verified && $email !== $wpUser->user_email) { 525 | $this->removeAction('profile_update'); 526 | $this->setAccountEmail($wpUser, $email); 527 | $this->addAction('profile_update'); 528 | } 529 | 530 | wp_set_current_user($wpUser->ID); 531 | wp_set_auth_cookie($wpUser->ID, true); 532 | do_action('wp_login', $wpUser->user_login, $wpUser); 533 | wp_redirect('/'); 534 | exit; 535 | } 536 | } 537 | } 538 | 539 | if (null !== $error) { 540 | wp_redirect('/'); 541 | exit; 542 | } 543 | 544 | if ($exchangeParameters && null === $error && (0 !== wp_get_current_user()->ID || null !== $this->getSdk()->getCredentials())) { 545 | wp_redirect('/'); 546 | exit; 547 | } 548 | 549 | wp_redirect($this->getSdk()->login()); 550 | exit; 551 | } 552 | 553 | public function onExchangeFailed(Throwable $_) 554 | { 555 | // Custom hook ('auth0_token_exchange_failed') to register when token exchange fails. 556 | wp_redirect('/'); 557 | exit; 558 | } 559 | 560 | public function onLogout(): never 561 | { 562 | wp_logout(); 563 | wp_redirect($this->getSdk()->logout(get_site_url())); 564 | exit; 565 | } 566 | 567 | public function onRegistration(): never 568 | { 569 | // Block registration attempts from the API? 570 | exit; 571 | } 572 | 573 | public function onShutdown(): void 574 | { 575 | if (! is_user_logged_in() || wp_is_json_request() || wp_is_rest_endpoint()) { 576 | return; 577 | } 578 | 579 | if ('false' !== $this->getPlugin()->getOption('sessions', 'rolling_sessions')) { 580 | $store = $this->getSdk()->configuration()->getSessionStorage(); 581 | 582 | /** 583 | * @var CookieStore $store 584 | */ 585 | $store->setState(true); 586 | 587 | wp_set_auth_cookie(get_current_user_id(), true); 588 | } 589 | } 590 | 591 | public function onUpdatedUser($userId, $previousUserData = null): void 592 | { 593 | if (! is_int($userId)) { 594 | return; 595 | } 596 | 597 | $network = get_current_network_id(); 598 | $blog = get_current_blog_id(); 599 | $database = $this->getPlugin()->database(); 600 | $table = $database->getTableName(Database::CONST_TABLE_SYNC); 601 | 602 | $this->prepDatabase(Database::CONST_TABLE_SYNC); 603 | 604 | $payload = json_encode([ 605 | 'event' => 'wp_user_updated', 606 | 'user' => $userId, 607 | ], JSON_THROW_ON_ERROR); 608 | $checksum = hash('sha256', $payload); 609 | 610 | $dupe = $database->selectRow('id', $table, 'WHERE `hashsum` = "%s";', [$checksum]); 611 | 612 | if (! $dupe) { 613 | $database->insertRow($table, [ 614 | 'site' => $network, 615 | 'blog' => $blog, 616 | 'created' => time(), 617 | 'payload' => $payload, 618 | 'hashsum' => hash('sha256', $payload), 619 | 'locked' => 0, 620 | ], [ 621 | '%d', 622 | '%d', 623 | '%d', 624 | '%s', 625 | '%s', 626 | '%d', 627 | ]); 628 | } 629 | } 630 | 631 | public function setAccountEmail(WP_User $wpUser, string $email): ?WP_User 632 | { 633 | if ($wpUser->user_email !== $email) { 634 | $wpUser->user_email = $email; 635 | $status = wp_update_user($wpUser); 636 | 637 | if ($status instanceof WP_Error) { 638 | return null; 639 | } 640 | } 641 | 642 | return $wpUser; 643 | } 644 | 645 | private function prepDatabase(string $databaseName) 646 | { 647 | $cacheKey = 'auth0_db_check_' . hash('sha256', $databaseName); 648 | 649 | $found = false; 650 | wp_cache_get($cacheKey, '', false, $found); 651 | 652 | if (! $found && false === get_transient($cacheKey)) { 653 | set_transient($cacheKey, true, 1800); 654 | wp_cache_set($cacheKey, true, 1800); 655 | 656 | return $this->getPlugin()->database()->createTable($databaseName); 657 | } 658 | } 659 | 660 | private function resolveIdentity( 661 | ?string $sub = null, 662 | ?string $email = null, 663 | ?bool $verified = null, 664 | ): ?WP_User { 665 | $email = sanitize_email(filter_var($email ?? '', FILTER_SANITIZE_EMAIL, FILTER_NULL_ON_FAILURE) ?? ''); 666 | 667 | if (null !== $sub) { 668 | $sub = sanitize_text_field($sub); 669 | $found = $this->getAccountByConnection($sub); 670 | 671 | if ($found instanceof WP_User) { 672 | return $found; 673 | } 674 | } 675 | 676 | // If an email is not marked as verified by the connection, dismiss it. 677 | if (true !== $verified || '' === $email) { 678 | $email = null; 679 | } 680 | 681 | if (null !== $email) { 682 | $found = get_user_by('email', $email); 683 | 684 | if ($found instanceof WP_User) { 685 | // Are we allowed to match loosely by email? 686 | if ('strict' !== $this->getPlugin()->getOption('accounts', 'matching')) { 687 | return $found; 688 | } 689 | 690 | // Are administrators allowed to bypass the check as a failsafe for configuration issues? 691 | if (0 === $this->getPlugin()->getOption('authentication', 'pair_sessions', 0)) { 692 | $roles = $found->roles; 693 | 694 | if (in_array('administrator', $roles, true)) { 695 | return $found; 696 | } 697 | } 698 | 699 | return null; 700 | } 701 | } 702 | 703 | if ('create' === $this->getPlugin()->getOption('accounts', 'missing')) { 704 | $username = (null !== $email) ? explode('@', $email, 2)[0] : explode('|', $sub ?? '', 2)[1]; 705 | $user = wp_create_user($username, wp_generate_password(random_int(12, 123), true, true), $email ?? ''); 706 | 707 | if (! $user instanceof WP_Error) { 708 | $user = get_user_by('ID', $user); 709 | 710 | if ($user instanceof WP_User) { 711 | $role = $this->getPlugin()->getOptionString('accounts', 'default_role'); 712 | 713 | if (is_string($role) && ! in_array($role, $user->roles, true)) { 714 | $user->set_role($role); 715 | wp_update_user($user); 716 | } 717 | 718 | return $user; 719 | } 720 | } 721 | } 722 | 723 | return null; 724 | } 725 | } 726 | -------------------------------------------------------------------------------- /src/Actions/Base.php: -------------------------------------------------------------------------------- 1 | |string> 20 | */ 21 | protected array $registry = []; 22 | 23 | public function __construct(private Plugin $plugin) 24 | { 25 | } 26 | 27 | final public function addAction(string $event, $method = null): ?Hooks 28 | { 29 | $callback = null; 30 | $method ??= $this->registry[$event] ?? null; 31 | $arguments = 1; 32 | 33 | if (null !== $method) { 34 | if (is_string($method)) { 35 | $callback = $method; 36 | } 37 | 38 | if (is_array($method) && count($method) >= 1 && is_string($method[0]) && is_numeric($method[1])) { 39 | $callback = $method[0]; 40 | $arguments = (int) $method[1]; 41 | } 42 | 43 | if (null !== $callback) { 44 | return $this->plugin->actions() 45 | ->add($event, $this, $callback, $this->getPriority($event), $arguments); 46 | } 47 | } 48 | 49 | return null; 50 | } 51 | 52 | final public function getPlugin(): Plugin 53 | { 54 | return $this->plugin; 55 | } 56 | 57 | final public function getPriority(string $event, int $default = 10, string $prefix = 'AUTH0_ACTION_PRIORITY'): int 58 | { 59 | $noramlized = strtoupper($prefix . '_' . $event); 60 | 61 | if (defined($noramlized)) { 62 | $constant = constant($noramlized); 63 | 64 | if (is_numeric($constant)) { 65 | return (int) $constant; 66 | } 67 | } 68 | 69 | return $default; 70 | } 71 | 72 | final public function getSdk(): Auth0 73 | { 74 | return $this->plugin->getSdk(); 75 | } 76 | 77 | final public function isPluginEnabled(): bool 78 | { 79 | return $this->plugin 80 | ->isEnabled(); 81 | } 82 | 83 | final public function isPluginReady(): bool 84 | { 85 | return $this->plugin 86 | ->isReady(); 87 | } 88 | 89 | final public function register(): self 90 | { 91 | foreach ($this->registry as $event => $method) { 92 | $this->addAction($event, $method); 93 | } 94 | 95 | return $this; 96 | } 97 | 98 | final public function removeAction(string $event, $method = null): ?Hooks 99 | { 100 | $callback = null; 101 | $method ??= $this->registry[$event] ?? null; 102 | $arguments = 1; 103 | 104 | if (null !== $method) { 105 | if (is_string($method)) { 106 | $callback = $method; 107 | } 108 | 109 | if (is_array($method) && count($method) >= 1 && is_string($method[0]) && is_numeric($method[1])) { 110 | $callback = $method[0]; 111 | $arguments = (int) $method[1]; 112 | } 113 | 114 | if (null !== $callback) { 115 | return $this->plugin->actions() 116 | ->remove($event, $this, $callback, $this->getPriority($event), $arguments); 117 | } 118 | } 119 | 120 | return null; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Actions/Sync.php: -------------------------------------------------------------------------------- 1 | |string> 38 | */ 39 | protected array $registry = [ 40 | self::CONST_JOB_BACKGROUND_SYNC => 'onBackgroundSync', 41 | self::CONST_JOB_BACKGROUND_MAINTENANCE => 'onBackgroundMaintenance', 42 | 'cron_schedules' => 'updateCronSchedule', 43 | ]; 44 | 45 | /** 46 | * In the event of an issue during the WP account deletion hooks, connections might be left in the accounts table that point to missing WP accounts. 47 | * This clears out those 'orphaned' connections for re-use by other WP accounts, or for use in creating a new WP account. 48 | */ 49 | public function cleanupOrphanedConnections(): void 50 | { 51 | $database = $this->getPlugin()->database(); 52 | $table = $database->getTableName(Database::CONST_TABLE_ACCOUNTS); 53 | $network = get_current_network_id(); 54 | $blog = get_current_blog_id(); 55 | 56 | $this->getPlugin()->database()->createTable(Database::CONST_TABLE_ACCOUNTS); 57 | 58 | $users = $database->selectDistinctResults('user', $table, 'WHERE `site` = %d AND `blog` = %d', [$network, $blog]); 59 | if (! is_array($users)) { 60 | return; 61 | } 62 | 63 | if ([] === $users) { 64 | return; 65 | } 66 | 67 | foreach ($users as $user) { 68 | $found = get_user_by('ID', $user->user); 69 | 70 | if (! $found) { 71 | $this->authentication()->deleteAccountConnections((int) $user->user); 72 | } 73 | } 74 | } 75 | 76 | public function eventUserCreated(string $dbConnection, array $event): void 77 | { 78 | if (isset($event['user'])) { 79 | $user = $event['user'] ?? null; 80 | 81 | if (null === $user) { 82 | return; 83 | } 84 | 85 | $user = get_user_by('ID', $user); 86 | 87 | if ($user) { 88 | $exists = $this->getResults($this->getSdk()->management()->usersByEmail()->get($user->user_email)); 89 | 90 | if (! is_array($exists) || [] === $exists) { 91 | $dbConnectionName = $this->getDatabaseName($dbConnection); 92 | 93 | $response = $this->getSdk()->management()->users()->create($dbConnectionName, [ 94 | 'name' => $user->display_name, 95 | 'nickname' => $user->nickname, 96 | 'given_name' => $user->user_firstname, 97 | 'family_name' => $user->user_lastname, 98 | 'email' => $user->user_email, 99 | 'password' => wp_generate_password(random_int(12, 123), true, true), 100 | ]); 101 | 102 | $response = $this->getResults($response, 201); 103 | 104 | if (null !== $response) { 105 | // Trigger a password change email to let them set their password 106 | $this->getSdk()->management()->tickets()->createPasswordChange([ 107 | 'user_id' => $response['user_id'], 108 | ]); 109 | 110 | $this->authentication()->createAccountConnection($user, $response['user_id']); 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | public function eventUserDeleted(string $dbConnection, array $event): void 118 | { 119 | if (isset($event['user'])) { 120 | $user = $event['user'] ?? null; 121 | $connection = $event['connection'] ?? null; 122 | 123 | if (null !== $user && null !== $connection) { 124 | // Verify that the connection has not been claimed by another account already 125 | $wpUser = $this->authentication()->getAccountByConnection($connection); 126 | 127 | if (! $wpUser instanceof WP_User) { 128 | // Determine if the Auth0 counterpart account still exists 129 | $api = $this->getResults($this->getSdk()->management()->users()->get($connection)); 130 | 131 | if (null !== $api) { 132 | // Delete the Auth0 counterpart account 133 | $this->getSdk()->management()->users()->delete($connection); 134 | } 135 | } 136 | } 137 | } 138 | } 139 | 140 | public function eventUserUpdated(string $dbConnection, array $event): void 141 | { 142 | if (isset($event['user'])) { 143 | $user = $event['user'] ?? null; 144 | $connection = $event['connection'] ?? null; 145 | 146 | if (null === $user && null === $connection) { 147 | return; 148 | } 149 | 150 | $user = get_user_by('ID', $user); 151 | 152 | if (! $user) { 153 | return; 154 | } 155 | 156 | $connections = $this->authentication()->getAccountConnections($user->ID); 157 | 158 | if (null !== $connections) { 159 | foreach ($connections as $connection) { 160 | $api = $this->getResults($this->getSdk()->management()->users()->get($connection->auth0)); 161 | 162 | if (null !== $api) { 163 | $connectionId = $api['user_id'] ?? null; 164 | 165 | if (null === $connectionId) { 166 | continue; 167 | } 168 | 169 | $currentEmail = $api['email'] ?? ''; 170 | 171 | $this->getSdk()->management()->users()->update($connectionId, [ 172 | 'name' => $user->display_name, 173 | 'nickname' => $user->nickname, 174 | 'given_name' => $user->user_firstname, 175 | 'family_name' => $user->user_lastname, 176 | 'email' => $user->user_email, 177 | ]); 178 | 179 | if ($user->user_email !== $currentEmail) { 180 | $this->getSdk()->management()->tickets()->createEmailVerification($connectionId); 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | 188 | public function getDatabaseName(?string $dbConnection): ?string 189 | { 190 | static $dbConnectionName = []; 191 | 192 | if (isset($dbConnectionName[$dbConnection])) { 193 | return $dbConnectionName[$dbConnectionName]; 194 | } 195 | 196 | if (null !== $dbConnection) { 197 | $response = $this->getResults($this->getSdk()->management()->connections()->get($dbConnection)); 198 | 199 | if ($response) { 200 | $dbConnectionName[$dbConnection] = $response['name']; 201 | 202 | return $response['name'] ?? $dbConnection; 203 | } 204 | } 205 | 206 | return null; 207 | } 208 | 209 | public function onBackgroundMaintenance(): void 210 | { 211 | $this->cleanupOrphanedConnections(); 212 | } 213 | 214 | public function onBackgroundSync(): void 215 | { 216 | $database = $this->getPlugin()->database(); 217 | $table = $database->getTableName(Database::CONST_TABLE_SYNC); 218 | $network = get_current_network_id(); 219 | $blog = get_current_blog_id(); 220 | 221 | $this->getPlugin()->database()->createTable(Database::CONST_TABLE_SYNC); 222 | 223 | $queue = $database->selectResults('*', $table, 'WHERE `site` = %d AND `blog` = %d ORDER BY created LIMIT 10', [$network, $blog]); 224 | 225 | $enabledEvents = [ 226 | 'wp_user_created' => $this->getPlugin()->getOptionBoolean('sync_events', 'user_creation') ?? true, 227 | 'wp_user_deleted' => $this->getPlugin()->getOptionBoolean('sync_events', 'user_deletion') ?? true, 228 | 'wp_user_updated' => $this->getPlugin()->getOptionBoolean('sync_events', 'user_updates') ?? true, 229 | ]; 230 | 231 | $dbConnection = $this->getPlugin()->getOptionString('sync', 'database'); 232 | 233 | foreach ($queue as $singleQueue) { 234 | if (null !== $dbConnection) { 235 | $payload = json_decode($singleQueue->payload, true, 512, JSON_THROW_ON_ERROR); 236 | 237 | if (isset($payload['event'])) { 238 | if ('wp_user_created' === $payload['event'] && $enabledEvents['wp_user_created']) { 239 | $this->eventUserCreated($dbConnection, $payload); 240 | } 241 | 242 | if ('wp_user_deleted' === $payload['event'] && $enabledEvents['wp_user_deleted']) { 243 | $this->eventUserDeleted($dbConnection, $payload); 244 | } 245 | 246 | if ('wp_user_updated' === $payload['event'] && $enabledEvents['wp_user_updated']) { 247 | $this->eventUserUpdated($dbConnection, $payload); 248 | } 249 | } 250 | } 251 | 252 | $database->deleteRow($table, ['id' => $singleQueue->id], ['%d']); 253 | } 254 | } 255 | 256 | /** 257 | * @param mixed $schedules 258 | * 259 | * @return mixed[] 260 | */ 261 | public function updateCronSchedule($schedules): array 262 | { 263 | $schedules[self::CONST_SCHEDULE_BACKGROUND_SYNC] = ['interval' => $this->getPlugin()->getOptionInteger('sync', 'schedule') ?? 3600, 'display' => 'Plugin Configuration']; 264 | 265 | $schedules[self::CONST_SCHEDULE_BACKGROUND_MAINTENANCE] = ['interval' => 300, 'display' => 'Every 5 Minutes']; 266 | 267 | return $schedules; 268 | } 269 | 270 | private function authentication(): Authentication 271 | { 272 | return $this->getPlugin()->getClassInstance(Authentication::class); 273 | } 274 | 275 | private function getResults(ResponseInterface $response, int $expectedStatusCode = 200): ?array 276 | { 277 | if (HttpResponse::wasSuccessful($response, $expectedStatusCode)) { 278 | return HttpResponse::decodeContent($response); 279 | } 280 | 281 | return null; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/Actions/Tools.php: -------------------------------------------------------------------------------- 1 | 'doUpdateCheck', 14 | 'transient_update_plugins' => 'doUpdateCheck', 15 | ]; 16 | 17 | public function doUpdateCheck($plugins) 18 | { 19 | // trap($plugins); 20 | 21 | if (! is_object($plugins)) { 22 | return $plugins; 23 | } 24 | 25 | if (! isset($plugins->response) || ! is_array($plugins->response)) { 26 | $plugins->response = []; 27 | } 28 | 29 | // $plugins->response['auth0/wpAuth0.php'] = (object) [ 30 | // 'slug' => 'auth0', 31 | // 'new_version' => '5.9', 32 | // 'url' => 'https://github.com/auth0/wordpress', 33 | // 'package' => 'https://github.com/auth0/wirdoress/archive/0.2.zip', 34 | // ]; 35 | 36 | return $plugins; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Cache/WpObjectCacheItem.php: -------------------------------------------------------------------------------- 1 | expires; 26 | } 27 | 28 | /** 29 | * @param null|DateInterval|int $time 30 | */ 31 | public function expiresAfter(DateInterval | int | null $time): static 32 | { 33 | if (null === $time) { 34 | $this->expires = null; 35 | 36 | return $this; 37 | } 38 | 39 | if ($time instanceof DateInterval) { 40 | $dateTime = new DateTimeImmutable(); 41 | $dateTime->add($time); 42 | $this->expires = $dateTime->getTimestamp(); 43 | 44 | return $this; 45 | } 46 | 47 | $this->expires = time() + $time; 48 | 49 | return $this; 50 | } 51 | 52 | public function expiresAt(?DateTimeInterface $expiration): static 53 | { 54 | if ($expiration instanceof DateTimeInterface) { 55 | $this->expires = $expiration->getTimestamp(); 56 | 57 | return $this; 58 | } 59 | 60 | $this->expires = $expiration; 61 | 62 | return $this; 63 | } 64 | 65 | public function get(): mixed 66 | { 67 | return $this->value; 68 | } 69 | 70 | public function getKey(): string 71 | { 72 | return $this->key; 73 | } 74 | 75 | public function isHit(): bool 76 | { 77 | return $this->is_hit; 78 | } 79 | 80 | public function set(mixed $value): static 81 | { 82 | $this->value = $value; 83 | $this->is_hit = true; 84 | 85 | return $this; 86 | } 87 | 88 | public static function miss(string $key): self 89 | { 90 | return new self($key, null, false); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Cache/WpObjectCachePool.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private array $deferred = []; 27 | 28 | public function __construct( 29 | private string $group = 'auth0', 30 | ) { 31 | } 32 | 33 | public function __destruct() 34 | { 35 | $this->commit(); 36 | } 37 | 38 | public function clear(): bool 39 | { 40 | $this->deferred = []; 41 | 42 | return wp_cache_flush(); 43 | } 44 | 45 | public function commit(): bool 46 | { 47 | $success = true; 48 | 49 | foreach (array_keys($this->deferred) as $singleDeferred) { 50 | $item = $this->wpGetItemDeferred((string) $singleDeferred); 51 | 52 | if ($item instanceof CacheItemInterface && ! $this->save($item)) { 53 | $success = false; 54 | } 55 | } 56 | 57 | $this->deferred = []; 58 | 59 | return $success; 60 | } 61 | 62 | public function deleteItem(string $key): bool 63 | { 64 | return $this->wpDeleteItem($key); 65 | } 66 | 67 | public function deleteItems(array $keys): bool 68 | { 69 | $deleted = true; 70 | 71 | foreach ($keys as $key) { 72 | if (! $this->wpDeleteItem($key)) { 73 | $deleted = false; 74 | } 75 | } 76 | 77 | return $deleted; 78 | } 79 | 80 | public function getItem(string $key): CacheItemInterface 81 | { 82 | return $this->wpGetItem($key); 83 | } 84 | 85 | /** 86 | * @param string[] $keys 87 | * 88 | * @return CacheItemInterface[] 89 | */ 90 | public function getItems(array $keys = []): iterable 91 | { 92 | if ([] === $keys) { 93 | return []; 94 | } 95 | 96 | $results = wp_cache_get_multiple($keys, $this->group); 97 | $items = []; 98 | 99 | foreach ($results as $key => $value) { 100 | $key = (string) $key; 101 | $items[$key] = $this->wpCreateItem($key, $value); 102 | } 103 | 104 | return $items; 105 | } 106 | 107 | public function hasItem(string $key): bool 108 | { 109 | return $this->getItem($key) 110 | ->isHit(); 111 | } 112 | 113 | public function save(CacheItemInterface $item): bool 114 | { 115 | if (! $item instanceof WpObjectCacheItem) { 116 | return false; 117 | } 118 | 119 | $value = serialize($item->get()); 120 | $key = $item->getKey(); 121 | $expires = $item->expirationTimestamp(); 122 | $ttl = 0; 123 | 124 | if (null !== $expires) { 125 | if ($expires <= time()) { 126 | return $this->wpDeleteItem($key); 127 | } 128 | 129 | $ttl = $expires - time(); 130 | } 131 | 132 | return wp_cache_set($key, $value, $this->group, $ttl); 133 | } 134 | 135 | public function saveDeferred(CacheItemInterface $item): bool 136 | { 137 | if (! $item instanceof WpObjectCacheItem) { 138 | return false; 139 | } 140 | 141 | $this->deferred[$item->getKey()] = [ 142 | 'item' => $item, 143 | 'expiration' => $item->expirationTimestamp(), 144 | ]; 145 | 146 | return true; 147 | } 148 | 149 | private function wpCreateItem(string $key, mixed $value): CacheItemInterface 150 | { 151 | if (! is_string($value)) { 152 | return WpObjectCacheItem::miss($key); 153 | } 154 | 155 | $value = unserialize($value); 156 | 157 | if (false === $value || 'b:0;' !== $value) { 158 | return WpObjectCacheItem::miss($key); 159 | } 160 | 161 | return new WpObjectCacheItem($key, $value, true); 162 | } 163 | 164 | private function wpDeleteItem(string $key): bool 165 | { 166 | return wp_cache_delete($key, $this->group); 167 | } 168 | 169 | private function wpGetItem(string $key): CacheItemInterface 170 | { 171 | $value = wp_cache_get($key, $this->group, true); 172 | 173 | if (false === $value) { 174 | return WpObjectCacheItem::miss($key); 175 | } 176 | 177 | return $this->wpCreateItem($key, $value); 178 | } 179 | 180 | private function wpGetItemDeferred(string $key): ?CacheItemInterface 181 | { 182 | if (! isset($this->deferred[$key])) { 183 | return null; 184 | } 185 | 186 | $deferred = $this->deferred[$key]; 187 | $item = clone $deferred['item']; 188 | $expires = $deferred['expiration']; 189 | 190 | if (null !== $expires && $expires <= time()) { 191 | unset($this->deferred[$key]); 192 | 193 | return null; 194 | } 195 | 196 | return $item; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Database.php: -------------------------------------------------------------------------------- 1 | createTableAccounts(); 35 | } 36 | 37 | if (self::CONST_TABLE_SYNC === $table) { 38 | return $this->createTableSync(); 39 | } 40 | } 41 | 42 | public function deleteRow( 43 | string $table, 44 | array $where, 45 | array $format, 46 | ): int | bool { 47 | return $this->getWpdb()->delete($table, $where, $format); 48 | } 49 | 50 | public function getTableName( 51 | string $table, 52 | ): string { 53 | return $this->getWpdb()->prefix . 'auth0_' . $table; 54 | } 55 | 56 | public function insertRow( 57 | string $table, 58 | array $data, 59 | array $formats, 60 | ): int | bool { 61 | try { 62 | return $this->getWpdb()->insert($table, $data, $formats); 63 | } catch (Throwable) { 64 | return false; 65 | } 66 | } 67 | 68 | public function selectDistinctResults( 69 | string $select, 70 | string $from, 71 | string $query, 72 | array $args = [], 73 | ): array | object | null { 74 | $query = $this->getWpdb()->prepare($query, ...$args); 75 | 76 | return $this->getWpdb()->get_results(sprintf('SELECT DISTINCT %s FROM %s ', $select, $from) . $query); 77 | } 78 | 79 | public function selectResults( 80 | string $select, 81 | string $from, 82 | string $query, 83 | array $args = [], 84 | ): array | object | null { 85 | $query = $this->getWpdb()->prepare($query, ...$args); 86 | 87 | return $this->getWpdb()->get_results(sprintf('SELECT %s FROM %s ', $select, $from) . $query); 88 | } 89 | 90 | public function selectRow( 91 | string $select, 92 | string $from, 93 | string $query, 94 | array $args = [], 95 | ): array | object | null { 96 | $query = $this->getWpdb()->prepare($query, ...$args); 97 | 98 | return $this->getWpdb()->get_row(sprintf('SELECT %s FROM %s ', $select, $from) . $query); 99 | } 100 | 101 | private function createTableAccounts() 102 | { 103 | $charset = $this->getWpdb()->get_charset_collate(); 104 | $table = $this->getTableName(self::CONST_TABLE_ACCOUNTS); 105 | 106 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 107 | 108 | return maybe_create_table( 109 | $table, 110 | sprintf('CREATE TABLE %s ( 111 | id BIGINT NOT NULL AUTO_INCREMENT, 112 | site TINYINT NOT NULL, 113 | blog BIGINT NOT NULL, 114 | user BIGINT NOT NULL, 115 | auth0 TEXT NOT NULL, 116 | PRIMARY KEY (id) 117 | )' . $charset . ';', $table), 118 | ); 119 | } 120 | 121 | private function createTableSync() 122 | { 123 | $charset = $this->getWpdb()->get_charset_collate(); 124 | $table = $this->getTableName(self::CONST_TABLE_SYNC); 125 | 126 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 127 | 128 | return maybe_create_table( 129 | $table, 130 | sprintf('CREATE TABLE %s ( 131 | id BIGINT NOT NULL AUTO_INCREMENT, 132 | site TINYINT NOT NULL, 133 | blog BIGINT NOT NULL, 134 | created INT(11) NOT NULL, 135 | payload TEXT NOT NULL, 136 | hashsum VARCHAR(64) NOT NULL UNIQUE, 137 | locked INT(1) NOT NULL, 138 | PRIMARY KEY (id) 139 | )' . $charset . ';', $table), 140 | ); 141 | } 142 | 143 | private function getWpdb(): object 144 | { 145 | global $wpdb; 146 | 147 | return $wpdb; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Filters/Authentication.php: -------------------------------------------------------------------------------- 1 | |string> 20 | */ 21 | protected array $registry = []; 22 | 23 | public function __construct(private Plugin $plugin) 24 | { 25 | } 26 | 27 | final public function getPlugin(): Plugin 28 | { 29 | return $this->plugin; 30 | } 31 | 32 | final public function getPriority(string $event, int $default = 10, string $prefix = 'AUTH0_FILTER_PRIORITY'): int 33 | { 34 | $noramlized = strtoupper($prefix . '_' . $event); 35 | 36 | if (defined($noramlized)) { 37 | $constant = constant($noramlized); 38 | 39 | if (is_numeric($constant)) { 40 | return (int) $constant; 41 | } 42 | } 43 | 44 | return $default; 45 | } 46 | 47 | final public function getSdk(): Auth0 48 | { 49 | return $this->plugin->getSdk(); 50 | } 51 | 52 | final public function register(): self 53 | { 54 | foreach ($this->registry as $event => $method) { 55 | $callback = null; 56 | $arguments = 1; 57 | 58 | if (is_string($method)) { 59 | $callback = $method; 60 | } 61 | 62 | if (is_array($method) && count($method) >= 1 && is_string($method[0]) && is_numeric($method[1])) { 63 | $callback = $method[0]; 64 | $arguments = (int) $method[1]; 65 | } 66 | 67 | if (null !== $callback) { 68 | $this->plugin->filters() 69 | ->add($event, $this, $callback, $this->getPriority($event), $arguments); 70 | } 71 | } 72 | 73 | return $this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Hooks.php: -------------------------------------------------------------------------------- 1 | hookType) { 36 | add_action($hook, $callback, $priority, $arguments); 37 | } 38 | 39 | if (self::CONST_ACTION_FILTER === $this->hookType) { 40 | add_filter($hook, $callback, $priority, $arguments); 41 | } 42 | 43 | return $this; 44 | } 45 | 46 | public function remove(string $hook, object $class, string $method, int $priority = 10, int $arguments = 1): self 47 | { 48 | $callback = [$class, $method]; 49 | 50 | /** 51 | * @var callable $callback 52 | */ 53 | if (self::CONST_ACTION_HOOK === $this->hookType) { 54 | remove_action($hook, $callback, $priority, $arguments); 55 | } 56 | 57 | if (self::CONST_ACTION_FILTER === $this->hookType) { 58 | remove_filter($hook, $callback, $priority, $arguments); 59 | } 60 | 61 | return $this; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Http/Client.php: -------------------------------------------------------------------------------- 1 | getBody() 28 | ->rewind(); 29 | 30 | $destinationUri = (string) $request->getUri(); 31 | $arguments = $this->getArguments($request); 32 | 33 | $this->setupTelemetry(); 34 | 35 | $responseData = wp_remote_request($destinationUri, $arguments); 36 | 37 | // error_log($arguments['method'] . ' -> ' . $destinationUri); 38 | // error_log(str_repeat(' ', strlen($arguments['method']) + 4) . json_encode($responseData)); 39 | 40 | $code = wp_remote_retrieve_response_code($responseData); 41 | $code = is_numeric($code) ? (int) $code : 400; 42 | 43 | $reason = wp_remote_retrieve_response_message($responseData); 44 | $headers = wp_remote_retrieve_headers($responseData); 45 | $headers = is_array($headers) ? $headers : iterator_to_array($headers); 46 | 47 | $body = wp_remote_retrieve_body($responseData); 48 | 49 | $response = $this->responseFactory->createResponse($code, $reason); 50 | $response = $response->withBody(Stream::create($body)); 51 | 52 | foreach ($headers as $header => $value) { 53 | $response = $response->withHeader($header, $value); 54 | } 55 | 56 | $response->getBody() 57 | ->rewind(); 58 | 59 | return $response; 60 | } 61 | 62 | /** 63 | * @param RequestInterface $request 64 | * 65 | * @return mixed[] 66 | */ 67 | private function getArguments(RequestInterface $request): array 68 | { 69 | return array_merge($this->options, [ 70 | 'method' => $request->getMethod(), 71 | 'httpversion' => $request->getProtocolVersion(), 72 | 'headers' => $this->getHeaders($request), 73 | 'body' => (string) $request->getBody(), 74 | ]); 75 | } 76 | 77 | /** 78 | * @param RequestInterface $request 79 | * 80 | * @return string[] 81 | */ 82 | private function getHeaders(RequestInterface $request): array 83 | { 84 | $headers = []; 85 | 86 | foreach (array_keys($request->getHeaders()) as $header) { 87 | $headers[$header] = $request->getHeaderLine($header); 88 | } 89 | 90 | return $headers; 91 | } 92 | 93 | /** 94 | * Configure the HTTP telemetry for Auth0 API calls. 95 | * 96 | * @psalm-suppress UnresolvableInclude,UndefinedConstant 97 | */ 98 | private function setupTelemetry(): void 99 | { 100 | $wp_version = '5.0.0'; 101 | 102 | if (! $this->telemetrySet) { 103 | return; 104 | } 105 | 106 | require ABSPATH . WPINC . '/version.php'; 107 | 108 | /** @var string $wp_version */ 109 | if ('' === $wp_version) { 110 | try { 111 | $core = get_site_transient('update_core'); 112 | 113 | /** @var object $core */ 114 | if (property_exists($core, 'version_checked')) { 115 | $wp_version = $core->version_checked; 116 | } 117 | } catch (Throwable) { 118 | // Silently ignore if unavailable. 119 | } 120 | } 121 | 122 | if (! isset($wp_version) || false === $wp_version) { 123 | $wp_version = '0.0.0'; 124 | } 125 | 126 | HttpTelemetry::setEnvProperty('WordPress', $wp_version); 127 | HttpTelemetry::setPackage('wordpress', WP_AUTH0_VERSION); 128 | 129 | $this->telemetrySet = true; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Http/Factory.php: -------------------------------------------------------------------------------- 1 | Map of lowercase header name => original name at registration 18 | */ 19 | private array $headerNames = []; 20 | 21 | /** 22 | * @var array Map of all registered headers, as original name => array of values 23 | */ 24 | private array $headers = []; 25 | 26 | private string $protocol = '1.1'; 27 | 28 | private StreamInterface | null $stream = null; 29 | 30 | public function getBody(): StreamInterface 31 | { 32 | if (null === $this->stream) { 33 | $this->stream = Stream::create(''); 34 | } 35 | 36 | return $this->stream; 37 | } 38 | 39 | /** 40 | * @param string $name 41 | * 42 | * @return string[] 43 | * 44 | * @psalm-return array 45 | */ 46 | public function getHeader($name): array 47 | { 48 | $normalized = $this->normalizeHeaderKey($name); 49 | 50 | if (! isset($this->headerNames[$normalized])) { 51 | return []; 52 | } 53 | 54 | return $this->headers[$this->headerNames[$normalized]]; 55 | } 56 | 57 | /** 58 | * @param string $name 59 | */ 60 | public function getHeaderLine($name): string 61 | { 62 | return implode(', ', $this->getHeader($name)); 63 | } 64 | 65 | /** 66 | * @return string[][] 67 | */ 68 | public function getHeaders(): array 69 | { 70 | return $this->headers; 71 | } 72 | 73 | public function getProtocolVersion(): string 74 | { 75 | return $this->protocol; 76 | } 77 | 78 | /** 79 | * @param string $name 80 | */ 81 | public function hasHeader($name): bool 82 | { 83 | return isset($this->headerNames[strtr( 84 | $name, 85 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 86 | 'abcdefghijklmnopqrstuvwxyz', 87 | )]); 88 | } 89 | 90 | /** 91 | * @param string $name 92 | * @param string|string[] $value 93 | */ 94 | public function withAddedHeader($name, $value): static 95 | { 96 | if ('' === trim($name)) { 97 | throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); 98 | } 99 | 100 | $new = clone $this; 101 | $new->setHeaders([ 102 | $name => $value, 103 | ]); 104 | 105 | return $new; 106 | } 107 | 108 | public function withBody(StreamInterface $body): static 109 | { 110 | if ($body === $this->stream) { 111 | return $this; 112 | } 113 | 114 | $new = clone $this; 115 | $new->stream = $body; 116 | 117 | return $new; 118 | } 119 | 120 | /** 121 | * @param string $name 122 | * @param string|string[] $value 123 | */ 124 | public function withHeader($name, $value): static 125 | { 126 | $value = $this->sanitizeHeader($name, $value); 127 | $normalized = $this->normalizeHeaderKey($name); 128 | 129 | $new = clone $this; 130 | 131 | if (isset($new->headerNames[$normalized])) { 132 | unset($new->headers[$new->headerNames[$normalized]]); 133 | } 134 | 135 | $new->headerNames[$normalized] = $name; 136 | $new->headers[$name] = $value; 137 | 138 | return $new; 139 | } 140 | 141 | /** 142 | * @param string $name 143 | */ 144 | public function withoutHeader($name): static 145 | { 146 | $this->normalizeHeaderKey($name); 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * @param string $version 153 | */ 154 | public function withProtocolVersion($version): static 155 | { 156 | if ($this->protocol === $version) { 157 | return $this; 158 | } 159 | 160 | $new = clone $this; 161 | $new->protocol = $version; 162 | 163 | return $new; 164 | } 165 | 166 | private function normalizeHeaderKey(string $header): string 167 | { 168 | return strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); 169 | } 170 | 171 | /** 172 | * @param string $header 173 | * @param string|string[] $values 174 | * 175 | * @return string[] 176 | */ 177 | private function sanitizeHeader($header, $values): array 178 | { 179 | if (1 !== preg_match("#^[!\\#$%&'*+.^_`|~0-9A-Za-z-]+$#", $header)) { 180 | throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); 181 | } 182 | 183 | if (! is_array($values)) { 184 | if ((! is_numeric($values) && ! is_string($values)) || 1 !== preg_match( 185 | "@^[ \t\x21-\x7E\x80-\xFF]*$@", 186 | $values, 187 | )) { 188 | throw new InvalidArgumentException('Header values must be RFC 7230 compatible strings.'); 189 | } 190 | 191 | return [trim($values, " \t")]; 192 | } 193 | 194 | if (empty($values)) { 195 | throw new InvalidArgumentException('Header values must be a string or an array of strings, empty array given.'); 196 | } 197 | 198 | $returnValues = []; 199 | 200 | foreach ($values as $value) { 201 | if ((! is_numeric($value) && ! is_string($value)) || 1 !== preg_match( 202 | "@^[ \t\x21-\x7E\x80-\xFF]*$@", 203 | $value, 204 | )) { 205 | throw new InvalidArgumentException('Header values must be RFC 7230 compatible strings.'); 206 | } 207 | 208 | $returnValues[] = trim($value, " \t"); 209 | } 210 | 211 | return $returnValues; 212 | } 213 | 214 | private function setHeaders(array $headers): void 215 | { 216 | foreach ($headers as $header => $value) { 217 | if (is_int($header)) { 218 | $header = (string) $header; 219 | } 220 | 221 | $value = $this->sanitizeHeader($header, $value); 222 | $normalized = $this->normalizeHeaderKey($header); 223 | 224 | if (isset($this->headerNames[$normalized])) { 225 | $header = $this->headerNames[$normalized]; 226 | $this->headers[$header] = array_merge($this->headers[$header], $value); 227 | } else { 228 | $this->headerNames[$normalized] = $header; 229 | $this->headers[$header] = $value; 230 | } 231 | } 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/Http/Message/Request.php: -------------------------------------------------------------------------------- 1 | method = strtoupper($method); 27 | $this->uri = $uri; 28 | $this->setHeaders($headers); 29 | $this->protocol = $version; 30 | 31 | if (! $this->hasHeader('Host')) { 32 | $this->updateHostFromUri(); 33 | } 34 | 35 | if ('' === $body) { 36 | return; 37 | } 38 | 39 | if (null === $body) { 40 | return; 41 | } 42 | 43 | $this->stream = Stream::create($body); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/Message/RequestTrait.php: -------------------------------------------------------------------------------- 1 | method; 22 | } 23 | 24 | public function getRequestTarget(): string 25 | { 26 | if (null !== $this->requestTarget) { 27 | return $this->requestTarget; 28 | } 29 | 30 | $target = $this->uri->getPath(); 31 | 32 | if ('' === $target) { 33 | $target = '/'; 34 | } 35 | 36 | $query = $this->uri->getQuery(); 37 | 38 | if ('' !== $query) { 39 | $target .= '?' . $query; 40 | } 41 | 42 | return $target; 43 | } 44 | 45 | public function getUri(): \Psr\Http\Message\UriInterface 46 | { 47 | return $this->uri; 48 | } 49 | 50 | /** 51 | * @param string $method 52 | */ 53 | public function withMethod($method): static 54 | { 55 | $new = clone $this; 56 | $new->method = $method; 57 | 58 | return $new; 59 | } 60 | 61 | public function withRequestTarget(mixed $requestTarget): static 62 | { 63 | if (! is_string($requestTarget)) { 64 | throw new InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); 65 | } 66 | 67 | if (preg_match('#\s#', $requestTarget)) { 68 | throw new InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); 69 | } 70 | 71 | $new = clone $this; 72 | $new->requestTarget = $requestTarget; 73 | 74 | return $new; 75 | } 76 | 77 | /** 78 | * @param \Psr\Http\Message\UriInterface $uri 79 | * @param bool $preserveHost 80 | */ 81 | public function withUri(\Psr\Http\Message\UriInterface $uri, $preserveHost = false): static 82 | { 83 | if ($this->uri === $uri) { 84 | return $this; 85 | } 86 | 87 | $new = clone $this; 88 | $new->uri = $uri; 89 | 90 | if (! $preserveHost || ! $this->hasHeader('Host')) { 91 | $new->updateHostFromUri(); 92 | } 93 | 94 | return $new; 95 | } 96 | 97 | private function updateHostFromUri(): void 98 | { 99 | $host = $this->uri->getHost(); 100 | 101 | if ('' === $host) { 102 | return; 103 | } 104 | 105 | $port = $this->uri->getPort(); 106 | 107 | if (null !== $port) { 108 | $host .= ':' . $port; 109 | } 110 | 111 | if (isset($this->headerNames['host'])) { 112 | $header = $this->headerNames['host']; 113 | } else { 114 | $this->headerNames['host'] = 'Host'; 115 | $header = 'Host'; 116 | } 117 | 118 | $this->headers = [ 119 | $header => [$host], 120 | ] + $this->headers; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Http/Message/Response.php: -------------------------------------------------------------------------------- 1 | 'Continue', 19 | 101 => 'Switching Protocols', 20 | 102 => 'Processing', 21 | 200 => 'OK', 22 | 201 => 'Created', 23 | 202 => 'Accepted', 24 | 203 => 'Non-Authoritative Information', 25 | 204 => 'No Content', 26 | 205 => 'Reset Content', 27 | 206 => 'Partial Content', 28 | 207 => 'Multi-status', 29 | 208 => 'Already Reported', 30 | 300 => 'Multiple Choices', 31 | 301 => 'Moved Permanently', 32 | 302 => 'Found', 33 | 303 => 'See Other', 34 | 304 => 'Not Modified', 35 | 305 => 'Use Proxy', 36 | 306 => 'Switch Proxy', 37 | 307 => 'Temporary Redirect', 38 | 400 => 'Bad Request', 39 | 401 => 'Unauthorized', 40 | 402 => 'Payment Required', 41 | 403 => 'Forbidden', 42 | 404 => 'Not Found', 43 | 405 => 'Method Not Allowed', 44 | 406 => 'Not Acceptable', 45 | 407 => 'Proxy Authentication Required', 46 | 408 => 'Request Time-out', 47 | 409 => 'Conflict', 48 | 410 => 'Gone', 49 | 411 => 'Length Required', 50 | 412 => 'Precondition Failed', 51 | 413 => 'Request Entity Too Large', 52 | 414 => 'Request-URI Too Large', 53 | 415 => 'Unsupported Media Type', 54 | 416 => 'Requested range not satisfiable', 55 | 417 => 'Expectation Failed', 56 | 418 => "I'm a teapot", 57 | 422 => 'Unprocessable Entity', 58 | 423 => 'Locked', 59 | 424 => 'Failed Dependency', 60 | 425 => 'Unordered Collection', 61 | 426 => 'Upgrade Required', 62 | 428 => 'Precondition Required', 63 | 429 => 'Too Many Requests', 64 | 431 => 'Request Header Fields Too Large', 65 | 451 => 'Unavailable For Legal Reasons', 66 | 500 => 'Internal Server Error', 67 | 501 => 'Not Implemented', 68 | 502 => 'Bad Gateway', 69 | 503 => 'Service Unavailable', 70 | 504 => 'Gateway Time-out', 71 | 505 => 'HTTP Version not supported', 72 | 506 => 'Variant Also Negotiates', 73 | 507 => 'Insufficient Storage', 74 | 508 => 'Loop Detected', 75 | 511 => 'Network Authentication Required', 76 | ]; 77 | 78 | private string $reasonPhrase = ''; 79 | 80 | public function __construct( 81 | private int $statusCode = 200, 82 | array $headers = [], 83 | string | StreamInterface | null $body = null, 84 | string $version = '1.1', 85 | string $reason = '', 86 | ) { 87 | if ('' !== $body && null !== $body) { 88 | $this->stream = Stream::create($body); 89 | } 90 | 91 | $this->setHeaders($headers); 92 | 93 | if ('' === $reason && isset(self::PHRASES[$this->statusCode])) { 94 | $this->reasonPhrase = self::PHRASES[$statusCode]; 95 | } 96 | 97 | $this->protocol = $version; 98 | } 99 | 100 | public function getReasonPhrase(): string 101 | { 102 | return $this->reasonPhrase; 103 | } 104 | 105 | public function getStatusCode(): int 106 | { 107 | return $this->statusCode; 108 | } 109 | 110 | public function withStatus($code, $reasonPhrase = ''): ResponseInterface 111 | { 112 | if ($code < 100 || $code > 599) { 113 | throw new InvalidArgumentException(sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); 114 | } 115 | 116 | $phrase = self::PHRASES[$code] ?? ''; 117 | $phrase = '' !== $reasonPhrase ? $reasonPhrase : $phrase; 118 | 119 | $new = clone $this; 120 | $new->statusCode = $code; 121 | $new->reasonPhrase = $phrase; 122 | 123 | return $new; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Http/Message/ServerRequest.php: -------------------------------------------------------------------------------- 1 | $headers Request headers 39 | * @param null|StreamInterface|string $body 40 | * @param string $version Protocol version 41 | * @param array $serverParams Typically the $_SERVER superglobal 42 | */ 43 | public function __construct( 44 | string $method, 45 | string | UriInterface $uri, 46 | array $headers = [], 47 | string | StreamInterface | null $body = null, 48 | string $version = '1.1', 49 | private array $serverParams = [], 50 | ) { 51 | if (! $uri instanceof UriInterface) { 52 | $uri = new Uri($uri); 53 | } 54 | 55 | $this->method = $method; 56 | $this->uri = $uri; 57 | $this->setHeaders($headers); 58 | $this->protocol = $version; 59 | 60 | if (! $this->hasHeader('Host')) { 61 | $this->updateHostFromUri(); 62 | } 63 | 64 | if (null === $body) { 65 | return; 66 | } 67 | 68 | if ('' === $body) { 69 | return; 70 | } 71 | 72 | $this->stream = Stream::create($body); 73 | } 74 | 75 | public function getAttribute($name, $default = null): mixed 76 | { 77 | if (! array_key_exists($name, $this->attributes)) { 78 | return $default; 79 | } 80 | 81 | return $this->attributes[$name]; 82 | } 83 | 84 | /** 85 | * @return mixed[] 86 | */ 87 | public function getAttributes(): array 88 | { 89 | return $this->attributes; 90 | } 91 | 92 | /** 93 | * @return mixed[] 94 | */ 95 | public function getCookieParams(): array 96 | { 97 | return $this->cookieParams; 98 | } 99 | 100 | public function getParsedBody(): array | object | null 101 | { 102 | return $this->parsedBody; 103 | } 104 | 105 | /** 106 | * @return mixed[] 107 | */ 108 | public function getQueryParams(): array 109 | { 110 | return $this->queryParams; 111 | } 112 | 113 | /** 114 | * @return mixed[] 115 | */ 116 | public function getServerParams(): array 117 | { 118 | return $this->serverParams; 119 | } 120 | 121 | /** 122 | * @return UploadedFileInterface[] 123 | */ 124 | public function getUploadedFiles(): array 125 | { 126 | return $this->uploadedFiles; 127 | } 128 | 129 | public function withAttribute($name, $value): ServerRequestInterface 130 | { 131 | $new = clone $this; 132 | $new->attributes[$name] = $value; 133 | 134 | return $new; 135 | } 136 | 137 | public function withCookieParams(array $cookies): static 138 | { 139 | $new = clone $this; 140 | $new->cookieParams = $cookies; 141 | 142 | return $new; 143 | } 144 | 145 | public function withoutAttribute($name): ServerRequestInterface 146 | { 147 | if (! array_key_exists($name, $this->attributes)) { 148 | return $this; 149 | } 150 | 151 | $new = clone $this; 152 | unset($new->attributes[$name]); 153 | 154 | return $new; 155 | } 156 | 157 | public function withParsedBody($data): static 158 | { 159 | if (null !== $data && [] !== $data && is_object($data)) { 160 | throw new InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null'); 161 | } 162 | 163 | $new = clone $this; 164 | $new->parsedBody = $data; 165 | 166 | return $new; 167 | } 168 | 169 | public function withQueryParams(array $query): static 170 | { 171 | $new = clone $this; 172 | $new->queryParams = $query; 173 | 174 | return $new; 175 | } 176 | 177 | public function withUploadedFiles(array $uploadedFiles): static 178 | { 179 | $new = clone $this; 180 | $new->uploadedFiles = $uploadedFiles; 181 | 182 | return $new; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Http/Message/Stream.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'r' => true, 23 | 'w+' => true, 24 | 'r+' => true, 25 | 'x+' => true, 26 | 'c+' => true, 27 | 'rb' => true, 28 | 'w+b' => true, 29 | 'r+b' => true, 30 | 'x+b' => true, 31 | 'c+b' => true, 32 | 'rt' => true, 33 | 'w+t' => true, 34 | 'r+t' => true, 35 | 'x+t' => true, 36 | 'c+t' => true, 37 | 'a+' => true, 38 | ], 39 | 'write' => [ 40 | 'w' => true, 41 | 'w+' => true, 42 | 'rw' => true, 43 | 'r+' => true, 44 | 'x+' => true, 45 | 'c+' => true, 46 | 'wb' => true, 47 | 'w+b' => true, 48 | 'r+b' => true, 49 | 'x+b' => true, 50 | 'c+b' => true, 51 | 'w+t' => true, 52 | 'r+t' => true, 53 | 'x+t' => true, 54 | 'c+t' => true, 55 | 'a' => true, 56 | 'a+' => true, 57 | ], 58 | ]; 59 | 60 | private ?bool $readable = null; 61 | 62 | private ?bool $seekable = null; 63 | 64 | private ?int $size = null; 65 | 66 | /** 67 | * @var null|resource A resource reference 68 | */ 69 | private mixed $stream = null; 70 | 71 | private ?bool $writable = null; 72 | 73 | public function __destruct() 74 | { 75 | $this->close(); 76 | } 77 | 78 | public function __toString(): string 79 | { 80 | if ($this->seekable) { 81 | $this->seek(0); 82 | } 83 | 84 | return $this->getContents(); 85 | } 86 | 87 | public function close(): void 88 | { 89 | $stream = $this->stream; 90 | 91 | /** @var null|closed-resource|resource $stream */ 92 | if (is_resource($stream)) { 93 | fclose($stream); 94 | } 95 | 96 | $this->detach(); 97 | } 98 | 99 | public function detach(): mixed 100 | { 101 | if (null === $this->stream) { 102 | return null; 103 | } 104 | 105 | $result = $this->stream; 106 | unset($this->stream); 107 | 108 | $this->size = null; 109 | $this->readable = false; 110 | $this->seekable = false; 111 | $this->writable = false; 112 | $this->seekable = false; 113 | 114 | return $result; 115 | } 116 | 117 | public function eof(): bool 118 | { 119 | if (null === $this->stream) { 120 | throw new RuntimeException('Stream is detached'); 121 | } 122 | 123 | return feof($this->stream); 124 | } 125 | 126 | public function getContents(): string 127 | { 128 | if (null === $this->stream) { 129 | throw new RuntimeException('Stream is detached'); 130 | } 131 | 132 | $contents = stream_get_contents($this->stream); 133 | 134 | if (false === $contents) { 135 | throw new RuntimeException('Unable to read stream contents'); 136 | } 137 | 138 | return $contents; 139 | } 140 | 141 | public function getMetadata($key = null): mixed 142 | { 143 | if (null === $this->stream) { 144 | return $key ? null : []; 145 | } 146 | 147 | $meta = stream_get_meta_data($this->stream); 148 | 149 | if (null === $key) { 150 | return $meta; 151 | } 152 | 153 | return $meta[$key] ?? null; 154 | } 155 | 156 | public function getSize(): ?int 157 | { 158 | if (null !== $this->size) { 159 | return $this->size; 160 | } 161 | 162 | if (null === $this->stream) { 163 | return null; 164 | } 165 | 166 | clearstatcache(true, $this->getUri()); 167 | $stats = fstat($this->stream); 168 | 169 | if (isset($stats['size'])) { 170 | $this->size = $stats['size']; 171 | 172 | return $this->size; 173 | } 174 | 175 | return null; 176 | } 177 | 178 | public function isReadable(): bool 179 | { 180 | return $this->readable ?? false; 181 | } 182 | 183 | public function isSeekable(): bool 184 | { 185 | return $this->seekable ?? false; 186 | } 187 | 188 | public function isWritable(): bool 189 | { 190 | return $this->writable ?? false; 191 | } 192 | 193 | public function read($length): string 194 | { 195 | if (null === $this->stream) { 196 | throw new RuntimeException('Stream is detached'); 197 | } 198 | 199 | if (! $this->readable) { 200 | throw new RuntimeException('Cannot read from non-readable stream'); 201 | } 202 | 203 | if ($length < 0) { 204 | throw new RuntimeException('Length parameter cannot be negative'); 205 | } 206 | 207 | if (0 === $length) { 208 | return ''; 209 | } 210 | 211 | $string = fread($this->stream, $length); 212 | 213 | if (false === $string) { 214 | throw new RuntimeException('Unable to read from stream'); 215 | } 216 | 217 | return $string; 218 | } 219 | 220 | public function rewind(): void 221 | { 222 | $this->seek(0); 223 | } 224 | 225 | public function seek($offset, $whence = SEEK_SET): void 226 | { 227 | if (null === $this->stream) { 228 | throw new RuntimeException('Stream is detached'); 229 | } 230 | 231 | if (! $this->seekable) { 232 | throw new RuntimeException('Stream is not seekable'); 233 | } 234 | 235 | if (-1 === fseek($this->stream, $offset, $whence)) { 236 | throw new RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . var_export($whence, true)); 237 | } 238 | } 239 | 240 | public function tell(): int 241 | { 242 | if (null === $this->stream) { 243 | throw new RuntimeException('Stream is detached'); 244 | } 245 | 246 | $result = ftell($this->stream); 247 | 248 | if (false === $result) { 249 | throw new RuntimeException('Unable to determine stream position'); 250 | } 251 | 252 | return $result; 253 | } 254 | 255 | public function write($string): int 256 | { 257 | if (null === $this->stream) { 258 | throw new RuntimeException('Stream is detached'); 259 | } 260 | 261 | if (! $this->writable) { 262 | throw new RuntimeException('Cannot write to a non-writable stream'); 263 | } 264 | 265 | $this->size = null; 266 | $result = fwrite($this->stream, $string); 267 | 268 | if (false === $result) { 269 | throw new RuntimeException('Unable to write to stream'); 270 | } 271 | 272 | return $result; 273 | } 274 | 275 | /** 276 | * @param resource|StreamInterface|string $body 277 | */ 278 | public static function create(mixed $body = ''): StreamInterface 279 | { 280 | if ($body instanceof StreamInterface) { 281 | return $body; 282 | } 283 | 284 | if (is_string($body)) { 285 | $resource = fopen('php://temp', 'rw+b'); 286 | fwrite($resource, $body); 287 | $body = $resource; 288 | } 289 | 290 | if (is_resource($body)) { 291 | $self = new self(); 292 | $self->stream = $body; 293 | $meta = stream_get_meta_data($self->stream); 294 | $self->seekable = $meta['seekable'] && 0 === fseek($self->stream, 0, SEEK_CUR); 295 | $self->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); 296 | $self->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); 297 | 298 | return $self; 299 | } 300 | 301 | throw new InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.'); 302 | } 303 | 304 | private function getUri(): string 305 | { 306 | $uri = $this->getMetadata('uri'); 307 | 308 | if (! is_string($uri)) { 309 | return ''; 310 | } 311 | 312 | return $uri; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/Http/Message/UploadedFile.php: -------------------------------------------------------------------------------- 1 | 1, 20 | UPLOAD_ERR_INI_SIZE => 1, 21 | UPLOAD_ERR_FORM_SIZE => 1, 22 | UPLOAD_ERR_PARTIAL => 1, 23 | UPLOAD_ERR_NO_FILE => 1, 24 | UPLOAD_ERR_NO_TMP_DIR => 1, 25 | UPLOAD_ERR_CANT_WRITE => 1, 26 | UPLOAD_ERR_EXTENSION => 1, 27 | ]; 28 | 29 | private int $error; 30 | 31 | private ?string $file = null; 32 | 33 | private bool $moved = false; 34 | 35 | private ?StreamInterface $stream = null; 36 | 37 | /** 38 | * @param StreamInterface|string $stream 39 | * @param int $size 40 | * @param int $errorStatus 41 | * @param ?string $clientFilename 42 | * @param ?string $clientMediaType 43 | */ 44 | public function __construct( 45 | string | StreamInterface $stream, 46 | private int $size, 47 | int $errorStatus, 48 | private ?string $clientFilename = null, 49 | private ?string $clientMediaType = null, 50 | ) { 51 | if (! isset(self::ERRORS[$errorStatus])) { 52 | throw new InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.'); 53 | } 54 | 55 | $this->error = $errorStatus; 56 | 57 | if (UPLOAD_ERR_OK === $this->error) { 58 | if (is_string($stream) && '' !== $stream) { 59 | $this->file = $stream; 60 | } elseif ($stream instanceof StreamInterface) { 61 | $this->stream = $stream; 62 | } else { 63 | throw new InvalidArgumentException('Invalid stream or file provided for UploadedFile'); 64 | } 65 | } 66 | } 67 | 68 | public function getClientFilename(): ?string 69 | { 70 | return $this->clientFilename; 71 | } 72 | 73 | public function getClientMediaType(): ?string 74 | { 75 | return $this->clientMediaType; 76 | } 77 | 78 | public function getError(): int 79 | { 80 | return $this->error; 81 | } 82 | 83 | public function getSize(): int 84 | { 85 | return $this->size; 86 | } 87 | 88 | public function getStream(): StreamInterface 89 | { 90 | $this->validateActive(); 91 | 92 | if ($this->stream instanceof StreamInterface) { 93 | return $this->stream; 94 | } 95 | 96 | $resource = fopen($this->file, 'rb'); 97 | 98 | if (false === $resource) { 99 | throw new RuntimeException(sprintf('The file "%s" cannot be opened', $this->file)); 100 | } 101 | 102 | return Stream::create($resource); 103 | } 104 | 105 | public function moveTo($targetPath): void 106 | { 107 | $this->validateActive(); 108 | 109 | if ('' === $targetPath) { 110 | throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string'); 111 | } 112 | 113 | if (null !== $this->file) { 114 | $this->moved = PHP_SAPI === 'cli' ? rename($this->file, $targetPath) : move_uploaded_file( 115 | $this->file, 116 | $targetPath, 117 | ); 118 | 119 | if (! $this->moved) { 120 | throw new RuntimeException(sprintf('Uploaded file could not be moved to "%s"', $targetPath)); 121 | } 122 | } else { 123 | $stream = $this->getStream(); 124 | 125 | if ($stream->isSeekable()) { 126 | $stream->rewind(); 127 | } 128 | 129 | $resource = fopen($targetPath, 'wb'); 130 | 131 | if (false === $resource) { 132 | throw new RuntimeException(sprintf('The file "%s" cannot be opened', $targetPath)); 133 | } 134 | 135 | $dest = Stream::create($resource); 136 | 137 | while (! $stream->eof()) { 138 | if (0 === $dest->write($stream->read(1_048_576))) { 139 | break; 140 | } 141 | } 142 | 143 | $this->moved = true; 144 | } 145 | } 146 | 147 | private function validateActive(): void 148 | { 149 | if (UPLOAD_ERR_OK !== $this->error) { 150 | throw new RuntimeException('Cannot retrieve stream due to upload error'); 151 | } 152 | 153 | if ($this->moved) { 154 | throw new RuntimeException('Cannot retrieve stream after it has already been moved'); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Http/Message/Uri.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private const SCHEMES = [ 27 | 'http' => 80, 28 | 'https' => 443, 29 | ]; 30 | 31 | private string $fragment = ''; 32 | 33 | private string $host = ''; 34 | 35 | private string $path = ''; 36 | 37 | private ?int $port = null; 38 | 39 | private string $query = ''; 40 | 41 | private string $scheme = ''; 42 | 43 | private string $userInfo = ''; 44 | 45 | public function __construct(string $uri = '') 46 | { 47 | if ('' !== $uri) { 48 | $parts = parse_url($uri); 49 | 50 | if (false === $parts) { 51 | throw new InvalidArgumentException(sprintf('Unable to parse URI: "%s"', $uri)); 52 | } 53 | 54 | $this->scheme = isset($parts['scheme']) ? strtr( 55 | $parts['scheme'], 56 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 57 | 'abcdefghijklmnopqrstuvwxyz', 58 | ) : ''; 59 | $this->userInfo = $parts['user'] ?? ''; 60 | $this->host = isset($parts['host']) ? strtr( 61 | $parts['host'], 62 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 63 | 'abcdefghijklmnopqrstuvwxyz', 64 | ) : ''; 65 | $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; 66 | $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; 67 | $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; 68 | $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; 69 | 70 | if (isset($parts['pass'])) { 71 | $this->userInfo .= ':' . $parts['pass']; 72 | } 73 | } 74 | } 75 | 76 | public function __toString(): string 77 | { 78 | return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); 79 | } 80 | 81 | public function getAuthority(): string 82 | { 83 | if ('' === $this->host) { 84 | return ''; 85 | } 86 | 87 | $authority = $this->host; 88 | 89 | if ('' !== $this->userInfo) { 90 | $authority = $this->userInfo . '@' . $authority; 91 | } 92 | 93 | if (null !== $this->port) { 94 | $authority .= ':' . $this->port; 95 | } 96 | 97 | return $authority; 98 | } 99 | 100 | public function getFragment(): string 101 | { 102 | return $this->fragment; 103 | } 104 | 105 | public function getHost(): string 106 | { 107 | return $this->host; 108 | } 109 | 110 | public function getPath(): string 111 | { 112 | return $this->path; 113 | } 114 | 115 | public function getPort(): ?int 116 | { 117 | return $this->port; 118 | } 119 | 120 | public function getQuery(): string 121 | { 122 | return $this->query; 123 | } 124 | 125 | public function getScheme(): string 126 | { 127 | return $this->scheme; 128 | } 129 | 130 | public function getUserInfo(): string 131 | { 132 | return $this->userInfo; 133 | } 134 | 135 | public function withFragment($fragment): UriInterface 136 | { 137 | $fragment = $this->filterQueryAndFragment($fragment); 138 | 139 | if ($this->fragment === $fragment) { 140 | return $this; 141 | } 142 | 143 | $new = clone $this; 144 | $new->fragment = $fragment; 145 | 146 | return $new; 147 | } 148 | 149 | public function withHost($host): UriInterface 150 | { 151 | $host = strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); 152 | 153 | if ($this->host === $host) { 154 | return $this; 155 | } 156 | 157 | $new = clone $this; 158 | $new->host = $host; 159 | 160 | return $new; 161 | } 162 | 163 | public function withPath($path): UriInterface 164 | { 165 | $path = $this->filterPath($path); 166 | 167 | if ($this->path === $path) { 168 | return $this; 169 | } 170 | 171 | $new = clone $this; 172 | $new->path = $path; 173 | 174 | return $new; 175 | } 176 | 177 | public function withPort($port): UriInterface 178 | { 179 | $port = $this->filterPort($port); 180 | 181 | if ($this->port === $port) { 182 | return $this; 183 | } 184 | 185 | $new = clone $this; 186 | $new->port = $port; 187 | 188 | return $new; 189 | } 190 | 191 | public function withQuery($query): UriInterface 192 | { 193 | $query = $this->filterQueryAndFragment($query); 194 | 195 | if ($this->query === $query) { 196 | return $this; 197 | } 198 | 199 | $new = clone $this; 200 | $new->query = $query; 201 | 202 | return $new; 203 | } 204 | 205 | public function withScheme($scheme): UriInterface 206 | { 207 | $scheme = strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); 208 | 209 | if ($this->scheme === $scheme) { 210 | return $this; 211 | } 212 | 213 | $new = clone $this; 214 | $new->scheme = $scheme; 215 | $new->port = $new->filterPort($new->port); 216 | 217 | return $new; 218 | } 219 | 220 | public function withUserInfo($user, $password = null): UriInterface 221 | { 222 | $info = $user; 223 | 224 | if (null !== $password && '' !== $password) { 225 | $info .= ':' . $password; 226 | } 227 | 228 | if ($this->userInfo === $info) { 229 | return $this; 230 | } 231 | 232 | $new = clone $this; 233 | $new->userInfo = $info; 234 | 235 | return $new; 236 | } 237 | 238 | private function filterPath(string $path): ?string 239 | { 240 | return preg_replace_callback( 241 | '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', 242 | static fn (array $match): string => self::rawurlencodeMatchZero($match), 243 | $path, 244 | ); 245 | } 246 | 247 | /** 248 | * @param null|int $port 249 | */ 250 | private function filterPort(?int $port): ?int 251 | { 252 | if (null === $port) { 253 | return null; 254 | } 255 | 256 | if ($port < 0 || $port > 0xFFFF) { 257 | throw new InvalidArgumentException(sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); 258 | } 259 | 260 | return self::isNonStandardPort($this->scheme, $port) ? $port : null; 261 | } 262 | 263 | private function filterQueryAndFragment(string $str): ?string 264 | { 265 | return preg_replace_callback( 266 | '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', 267 | static fn (array $match): string => self::rawurlencodeMatchZero($match), 268 | $str, 269 | ); 270 | } 271 | 272 | private static function createUriString( 273 | string $scheme, 274 | string $authority, 275 | string $path, 276 | string $query, 277 | string $fragment, 278 | ): string { 279 | $uri = ''; 280 | 281 | if ('' !== $scheme) { 282 | $uri .= $scheme . ':'; 283 | } 284 | 285 | if ('' !== $authority) { 286 | $uri .= '//' . $authority; 287 | } 288 | 289 | if ('' !== $path) { 290 | if ('/' !== $path[0]) { 291 | if ('' !== $authority) { 292 | $path = '/' . $path; 293 | } 294 | } elseif (isset($path[1]) && '/' === $path[1]) { 295 | if ('' === $authority) { 296 | $path = '/' . ltrim($path, '/'); 297 | } 298 | } 299 | 300 | $uri .= $path; 301 | } 302 | 303 | if ('' !== $query) { 304 | $uri .= '?' . $query; 305 | } 306 | 307 | if ('' !== $fragment) { 308 | $uri .= '#' . $fragment; 309 | } 310 | 311 | return $uri; 312 | } 313 | 314 | private static function isNonStandardPort(string $scheme, int $port): bool 315 | { 316 | return ! isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme]; 317 | } 318 | 319 | private static function rawurlencodeMatchZero(array $match): string 320 | { 321 | return rawurlencode($match[0]); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Http/MessageFactory/RequestFactory.php: -------------------------------------------------------------------------------- 1 | > 25 | */ 26 | private const ACTIONS = [AuthenticationActions::class, ConfigurationActions::class, SyncActions::class, ToolsActions::class, UpdatesActions::class]; 27 | 28 | /** 29 | * @var array> 30 | */ 31 | private const FILTERS = [AuthenticationFilters::class]; 32 | 33 | /** 34 | * @var mixed[] 35 | */ 36 | private array $registry = []; 37 | 38 | public function __construct( 39 | private ?Auth0 $auth0, 40 | private ?SdkConfiguration $sdkConfiguration, 41 | ) { 42 | } 43 | 44 | /** 45 | * Returns a singleton instance of Hooks configured for working with actions. 46 | */ 47 | public function actions(): Hooks 48 | { 49 | static $instance = null; 50 | 51 | if (null === $instance) { 52 | $instance = new Hooks(Hooks::CONST_ACTION_HOOK); 53 | } 54 | 55 | return $instance; 56 | } 57 | 58 | /** 59 | * Returns a singleton instance of Database. 60 | */ 61 | public function database(): Database 62 | { 63 | static $instance = null; 64 | 65 | if (null === $instance) { 66 | $instance = new Database(); 67 | } 68 | 69 | return $instance; 70 | } 71 | 72 | /** 73 | * Returns a singleton instance of Hooks configured for working with filters. 74 | */ 75 | public function filters(): Hooks 76 | { 77 | static $instance = null; 78 | 79 | if (null === $instance) { 80 | $instance = new Hooks(Hooks::CONST_ACTION_FILTER); 81 | } 82 | 83 | return $instance; 84 | } 85 | 86 | public function getClassInstance(string $class): mixed 87 | { 88 | if (! array_key_exists($class, $this->registry)) { 89 | $this->registry[$class] = new $class($this); 90 | } 91 | 92 | return $this->registry[$class]; 93 | } 94 | 95 | /** 96 | * Returns a singleton instance of SdkConfiguration. 97 | */ 98 | public function getConfiguration(): SdkConfiguration 99 | { 100 | $this->sdkConfiguration ??= $this->importConfiguration(); 101 | 102 | return $this->sdkConfiguration; 103 | } 104 | 105 | /** 106 | * @psalm-param 0|null $default 107 | * 108 | * @param string $group 109 | * @param string $key 110 | * @param ?int $default 111 | * @param string $prefix 112 | */ 113 | public function getOption(string $group, string $key, ?int $default = null, string $prefix = 'auth0_'): mixed 114 | { 115 | $options = get_option($prefix . $group, []); 116 | 117 | if (is_array($options) && isset($options[$key])) { 118 | return $options[$key]; 119 | } 120 | 121 | return $default; 122 | } 123 | 124 | public function getOptionBoolean(string $group, string $key, string $prefix = 'auth0_'): ?bool 125 | { 126 | $result = $this->getOption($group, $key, null, $prefix); 127 | 128 | if (is_string($result)) { 129 | return 'true' === $result || '1' === $result; 130 | } 131 | 132 | return null; 133 | } 134 | 135 | public function getOptionInteger(string $group, string $key, string $prefix = 'auth0_'): ?int 136 | { 137 | $result = $this->getOption($group, $key, null, $prefix); 138 | 139 | if (is_int($result)) { 140 | return $result; 141 | } 142 | 143 | if (is_numeric($result)) { 144 | return (int) $result; 145 | } 146 | 147 | return null; 148 | } 149 | 150 | public function getOptionString(string $group, string $key, string $prefix = 'auth0_'): ?string 151 | { 152 | $result = $this->getOption($group, $key, null, $prefix); 153 | 154 | if (is_string($result)) { 155 | return $result; 156 | } 157 | 158 | return null; 159 | } 160 | 161 | /** 162 | * Returns a singleton instance of the Auth0 SDK. 163 | */ 164 | public function getSdk(): Auth0 165 | { 166 | $this->auth0 ??= new Auth0($this->getConfiguration()); 167 | 168 | return $this->auth0; 169 | } 170 | 171 | /** 172 | * Returns true if the plugin has been enabled. 173 | */ 174 | public function isEnabled(): bool 175 | { 176 | return 'true' === $this->getOptionString('state', 'enable'); 177 | } 178 | 179 | /** 180 | * Returns true if the plugin has a minimum viable configuration. 181 | */ 182 | public function isReady(): bool 183 | { 184 | try { 185 | $config = $this->getConfiguration(); 186 | } catch (Throwable) { 187 | return false; 188 | } 189 | 190 | if (! $config->hasClientId()) { 191 | return false; 192 | } 193 | 194 | if ('' === (string) $config->getClientId()) { 195 | return false; 196 | } 197 | 198 | if (! $config->hasClientSecret()) { 199 | return false; 200 | } 201 | 202 | if ('' === (string) $config->getClientSecret()) { 203 | return false; 204 | } 205 | 206 | if (! $config->hasDomain()) { 207 | return false; 208 | } 209 | 210 | if ('' === $config->getDomain()) { 211 | return false; 212 | } 213 | 214 | if (! $config->hasCookieSecret()) { 215 | return false; 216 | } 217 | 218 | return '' !== (string) $config->getCookieSecret(); 219 | } 220 | 221 | /** 222 | * Main plugin functionality. 223 | */ 224 | public function run(): self 225 | { 226 | foreach (self::FILTERS as $filter) { 227 | $callback = [$this->getClassInstance($filter), 'register']; 228 | 229 | /** 230 | * @var callable $callback 231 | */ 232 | $callback(); 233 | } 234 | 235 | foreach (self::ACTIONS as $action) { 236 | $callback = [$this->getClassInstance($action), 'register']; 237 | 238 | /** 239 | * @var callable $callback 240 | */ 241 | $callback(); 242 | } 243 | 244 | return $this; 245 | } 246 | 247 | /** 248 | * Assign a Auth0\SDK\Configuration\SdkConfiguration instance for the plugin to use. 249 | * 250 | * @param SdkConfiguration $sdkConfiguration 251 | */ 252 | public function setConfiguration(SdkConfiguration $sdkConfiguration): self 253 | { 254 | $this->sdkConfiguration = $sdkConfiguration; 255 | 256 | return $this; 257 | } 258 | 259 | /** 260 | * Assign a Auth0\SDK\Auth0 instance for the plugin to use. 261 | * 262 | * @param Auth0 $auth0 263 | */ 264 | public function setSdk(Auth0 $auth0): self 265 | { 266 | $this->auth0 = $auth0; 267 | 268 | return $this; 269 | } 270 | 271 | /** 272 | * Import configuration settings from database. 273 | */ 274 | private function importConfiguration(): SdkConfiguration 275 | { 276 | $audiences = $this->getOptionString('client_advanced', 'apis') ?? ''; 277 | $organizations = $this->getOptionString('client_advanced', 'organizations') ?? ''; 278 | $caching = $this->getOption('tokens', 'caching'); 279 | 280 | $audiences = array_filter(array_values(array_unique(explode("\n", trim($audiences))))); 281 | $organizations = array_filter(array_values(array_unique(explode("\n", trim($organizations))))); 282 | $secure = $this->getOptionBoolean('cookies', 'secure') ?? is_ssl(); 283 | $expires = $this->getOptionInteger('cookies', 'ttl') ?? 0; 284 | 285 | if (defined('DOING_CRON')) { 286 | // When invoked from a WP_Cron task, just use a minimum configuration for those needs (namely, no sessions invoked.) 287 | $sdkConfiguration = new SdkConfiguration( 288 | strategy: SdkConfiguration::STRATEGY_NONE, 289 | httpRequestFactory: Factory::getRequestFactory(), 290 | httpResponseFactory: Factory::getResponseFactory(), 291 | httpStreamFactory: Factory::getStreamFactory(), 292 | httpClient: Factory::getClient(), 293 | domain: $this->getOptionString('client', 'domain'), 294 | clientId: $this->getOptionString('client', 'id'), 295 | clientSecret: $this->getOptionString('client', 'secret'), 296 | audience: [] !== $audiences ? $audiences : null, 297 | organization: [] !== $organizations ? $organizations : null, 298 | ); 299 | } else { 300 | $sdkConfiguration = new SdkConfiguration( 301 | strategy: SdkConfiguration::STRATEGY_REGULAR, 302 | httpRequestFactory: Factory::getRequestFactory(), 303 | httpResponseFactory: Factory::getResponseFactory(), 304 | httpStreamFactory: Factory::getStreamFactory(), 305 | httpClient: Factory::getClient(), 306 | domain: $this->getOptionString('client', 'domain'), 307 | clientId: $this->getOptionString('client', 'id'), 308 | clientSecret: $this->getOptionString('client', 'secret'), 309 | customDomain: $this->getOptionString('client_advanced', 'custom_domain'), 310 | audience: [] !== $audiences ? $audiences : null, 311 | organization: [] !== $organizations ? $organizations : null, 312 | cookieSecret: $this->getOptionString('cookies', 'secret'), 313 | cookieDomain: $this->getOptionString('cookies', 'domain'), 314 | cookiePath: $this->getOptionString('cookies', 'path') ?? '/', 315 | cookieExpires: $expires, 316 | cookieSecure: (bool) $secure, 317 | cookieSameSite: $this->getOptionString('cookies', 'samesite'), 318 | redirectUri: get_site_url(null, 'wp-login.php'), 319 | ); 320 | } 321 | 322 | if ('disable' !== $caching) { 323 | $wpObjectCachePool = new WpObjectCachePool(); 324 | $sdkConfiguration->setTokenCache($wpObjectCachePool); 325 | $sdkConfiguration->setBackchannelLogoutCache($wpObjectCachePool); 326 | } 327 | 328 | return $sdkConfiguration; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/Utilities/Render.php: -------------------------------------------------------------------------------- 1 | $select 33 | * @param string $element 34 | * @param string $name 35 | * @param null|bool|int|string $value 36 | * @param string $type 37 | * @param string $description 38 | * @param string $placeholder 39 | * @param ?bool $disabled 40 | */ 41 | public static function option( 42 | string $element, 43 | string $name, 44 | string | int | bool | null $value, 45 | string $type = 'text', 46 | string $description = '', 47 | string $placeholder = '', 48 | ?array $select = null, 49 | ?bool $disabled = null, 50 | ): void { 51 | if ('' !== $placeholder) { 52 | $placeholder = ' placeholder="' . $placeholder . '"'; 53 | } 54 | 55 | $disabledString = ''; 56 | 57 | if (null !== $disabled && $disabled) { 58 | $disabledString = ' disabled'; 59 | } 60 | 61 | if (null !== $select && count($select) >= 1) { 62 | if (true === $disabled) { 63 | echo ''; 64 | echo ''; 67 | } 68 | 69 | foreach ($select as $optVal => $optText) { 70 | $selected = ''; 71 | 72 | if ($optVal === $value) { 73 | $selected = ' selected'; 74 | } 75 | 76 | echo ''; 77 | } 78 | 79 | echo ''; 80 | 81 | if ('' !== $description) { 82 | echo '

' . $description . '

'; 83 | } 84 | 85 | return; 86 | } 87 | 88 | if (in_array($type, self::TREAT_AS_TEXT, true)) { 89 | echo ''; 90 | 91 | if ('' !== $description) { 92 | echo '

' . $description . '

'; 93 | } 94 | 95 | return; 96 | } 97 | 98 | if ('textarea' === $type) { 99 | echo ''; 100 | 101 | if ('' !== $description) { 102 | echo '

' . $description . '

'; 103 | } 104 | 105 | return; 106 | } 107 | 108 | if ('boolean' === $type) { 109 | echo ' ' . $description; 113 | } 114 | } 115 | 116 | public static function pageBegin(string $title, ?string $action = 'options.php'): void 117 | { 118 | echo '
'; 119 | echo '

' . $title . '

'; 120 | 121 | if (null !== $action) { 122 | echo '
'; 123 | } 124 | } 125 | 126 | public static function pageEnd(): void 127 | { 128 | echo '
'; 129 | echo '
'; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Utilities/Sanitize.php: -------------------------------------------------------------------------------- 1 | $max) { 133 | return $max; 134 | } 135 | 136 | return $int; 137 | } 138 | 139 | public static function string(string $string): ?string 140 | { 141 | $string = trim(sanitize_text_field($string)); 142 | 143 | if ('' === $string) { 144 | return null; 145 | } 146 | 147 | return $string; 148 | } 149 | 150 | public static function textarea(string $string): ?string 151 | { 152 | $string = trim(sanitize_textarea_field($string)); 153 | 154 | if ('' === $string) { 155 | return null; 156 | } 157 | 158 | return $string; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "5.3.0": { 3 | "download": "Auth0_WordPress_5.3.0.zip" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /wpAuth0.php: -------------------------------------------------------------------------------- 1 | bin2hex(random_bytes(64)) 48 | ]); 49 | } 50 | 51 | $backchannelLogout = get_option('auth0_backchannel_logout', []); 52 | 53 | if (! is_array($backchannelLogout) || [] === $backchannelLogout || ! isset($backchannelLogout['secret'])) { 54 | add_option('auth0_backchannel_logout', [ 55 | 'secret' => bin2hex(random_bytes(64)) 56 | ]); 57 | } 58 | 59 | $authentication = get_option('auth0_authentication', []); 60 | 61 | if (! is_array($authentication) || [] === $authentication || ! isset($authentication['fallback_secret'])) { 62 | add_option('auth0_authentication', [ 63 | 'fallback_secret' => bin2hex(random_bytes(64)) 64 | ]); 65 | } 66 | } 67 | ); 68 | 69 | // Run plugin functions 70 | wpAuth0()->run(); 71 | 72 | /** 73 | * Return a configured singleton of the Auth0 WP plugin. 74 | * 75 | * @param null|Plugin $plugin Optional. An existing instance of Auth0\WordPress\Plugin to use. 76 | * @param null|Auth0\SDK\Auth0 $sdk Optional. An existing instance of Auth0\SDK\Auth0 to use. 77 | * @param null|Auth0\SDK\Configuration\SdkConfiguration $configuration Optional. An existing instance of Auth0\SDK\Configuration\SdkConfiguration to use. 78 | */ 79 | function wpAuth0( 80 | ?Plugin $plugin = null, 81 | ?Sdk $sdk = null, 82 | ?Configuration $configuration = null, 83 | ): Plugin { 84 | static $instance = null; 85 | 86 | $instance ??= $instance ?? $plugin ?? new Plugin($sdk, $configuration); 87 | 88 | return $instance; 89 | } 90 | --------------------------------------------------------------------------------