├── composer.json ├── screenshots ├── widget.png ├── settings_auth.png └── settings_not_auth.png ├── .gitignore ├── vendor ├── firebase │ └── php-jwt │ │ ├── src │ │ ├── ExpiredException.php │ │ ├── BeforeValidException.php │ │ ├── SignatureInvalidException.php │ │ ├── Key.php │ │ ├── CachedKeySet.php │ │ ├── JWK.php │ │ └── JWT.php │ │ ├── composer.json │ │ ├── LICENSE │ │ ├── CHANGELOG.md │ │ └── README.md ├── composer │ ├── autoload_namespaces.php │ ├── autoload_psr4.php │ ├── autoload_classmap.php │ ├── platform_check.php │ ├── LICENSE │ ├── autoload_static.php │ ├── installed.php │ ├── autoload_real.php │ ├── installed.json │ ├── InstalledVersions.php │ └── ClassLoader.php └── autoload.php ├── phpunit.xml.dist ├── .github └── workflows │ └── label-ai-generated-prs.yml ├── test ├── ValidatorTest.php ├── UserTest.php ├── SnippetTest.php ├── SnippetSettingsTest.php ├── SecureModeCalculatorTest.php └── IntercomSettingsPageTest.php ├── .circleci └── config.yml ├── readme.md ├── composer.lock ├── readme.txt └── bootstrap.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "firebase/php-jwt": "6.4" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /screenshots/widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intercom/intercom-wordpress/HEAD/screenshots/widget.png -------------------------------------------------------------------------------- /screenshots/settings_auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intercom/intercom-wordpress/HEAD/screenshots/settings_auth.png -------------------------------------------------------------------------------- /screenshots/settings_not_auth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intercom/intercom-wordpress/HEAD/screenshots/settings_not_auth.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swo 2 | *.swp 3 | intercom-wordpress.zip 4 | .idea 5 | .vscode 6 | .DS_Store 7 | .phpunit.result.cache 8 | .phpunit.cache 9 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/src/ExpiredException.php: -------------------------------------------------------------------------------- 1 | array($vendorDir . '/firebase/php-jwt/src'), 10 | ); 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./test 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /vendor/composer/autoload_classmap.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /.github/workflows/label-ai-generated-prs.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/label-ai-generated-prs.yml 2 | name: Label AI-generated PRs 3 | 4 | on: 5 | pull_request: 6 | types: [opened, edited, synchronize] # run when the body changes too 7 | 8 | jobs: 9 | call-label-ai-prs: 10 | uses: intercom/github-action-workflows/.github/workflows/label-ai-prs.yml@main 11 | secrets: inherit -------------------------------------------------------------------------------- /test/ValidatorTest.php: -------------------------------------------------------------------------------- 1 | ", "", $x); 11 | }; 12 | $validator = new Validator(array("app_id" => "foo 26 | 29 | 30 | HTML; 31 | 32 | $this->assertEquals($expectedHtml, $snippet->html()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/src/Key.php: -------------------------------------------------------------------------------- 1 | keyMaterial = $keyMaterial; 44 | $this->algorithm = $algorithm; 45 | } 46 | 47 | /** 48 | * Return the algorithm valid for this key 49 | * 50 | * @return string 51 | */ 52 | public function getAlgorithm(): string 53 | { 54 | return $this->algorithm; 55 | } 56 | 57 | /** 58 | * @return string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate 59 | */ 60 | public function getKeyMaterial() 61 | { 62 | return $this->keyMaterial; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Intercom / WordPress 2 | 3 | [![Build Status](https://travis-ci.org/intercom/intercom-wordpress.svg?branch=master)](https://travis-ci.org/intercom/intercom-wordpress) 4 | 5 | # Compatibility 6 | 7 | Requires PHP 7.2 or higher. 8 | 9 | # Local Testing 10 | 11 | Running tests requires [phpunit](https://phpunit.de/). 12 | 13 | ```php 14 | INTERCOM_PLUGIN_TEST=1 phpunit 15 | ``` 16 | 17 | # Usage 18 | 19 | Installing this plugin provides a new Intercom settings page. 20 | Authenticate with Intercom to retrieve your app_id and Identity Verification secret. 21 | 22 | 23 | Once authenticated, the Intercom widget will automatically appear on your site. 24 | 25 | 26 | 27 | NB: This plugin injects a Javascript snippet on your website frontend containing dynamic user data. Some caching solutions will cache entire pages and should not be used with this plugin. Doing so may cause conversations to be delivered to the wrong user. 28 | 29 | # Pass custom data attributes to the Intercom Messenger 30 | 31 | Using the [add_filter](https://developer.wordpress.org/reference/functions/add_filter) method in your WordPress theme or custom plugin you can pass [custom data attributes](https://www.intercom.com/help/en/articles/179-create-and-track-custom-data-attributes-cdas) to the Intercom Messenger (see example below): 32 | 33 | ```php 34 | add_filter( 'intercom_settings', function( $settings ) { 35 | $settings['customer_type'] = $customer_type; 36 | return $settings; 37 | } ); 38 | ``` 39 | 40 | 41 | # Users 42 | 43 | If a `$current_user` is present, we use their email and ID as an identifier in the widget. 44 | We recommend enabling [Identity Verification](https://docs.intercom.com/configure-intercom-for-your-product-or-site/staying-secure/enable-identity-verification-on-your-web-product) in the settings page. 45 | 46 | # Contributing 47 | 48 | * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet. 49 | * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it. 50 | * Fork the project. 51 | * Start a feature/bugfix branch. 52 | * Commit and push until you are happy with your contribution. 53 | * Make sure to add tests for it. This is important so we don't break it in a future version unintentionally. 54 | -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "name": "firebase/php-jwt", 5 | "version": "v6.4.0", 6 | "version_normalized": "6.4.0.0", 7 | "source": { 8 | "type": "git", 9 | "url": "https://github.com/firebase/php-jwt.git", 10 | "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224" 11 | }, 12 | "dist": { 13 | "type": "zip", 14 | "url": "https://api.github.com/repos/firebase/php-jwt/zipball/4dd1e007f22a927ac77da5a3fbb067b42d3bc224", 15 | "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224", 16 | "shasum": "" 17 | }, 18 | "require": { 19 | "php": "^7.1||^8.0" 20 | }, 21 | "require-dev": { 22 | "guzzlehttp/guzzle": "^6.5||^7.4", 23 | "phpspec/prophecy-phpunit": "^1.1", 24 | "phpunit/phpunit": "^7.5||^9.5", 25 | "psr/cache": "^1.0||^2.0", 26 | "psr/http-client": "^1.0", 27 | "psr/http-factory": "^1.0" 28 | }, 29 | "suggest": { 30 | "ext-sodium": "Support EdDSA (Ed25519) signatures", 31 | "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" 32 | }, 33 | "time": "2023-02-09T21:01:23+00:00", 34 | "type": "library", 35 | "installation-source": "dist", 36 | "autoload": { 37 | "psr-4": { 38 | "Firebase\\JWT\\": "src" 39 | } 40 | }, 41 | "notification-url": "https://packagist.org/downloads/", 42 | "license": [ 43 | "BSD-3-Clause" 44 | ], 45 | "authors": [ 46 | { 47 | "name": "Neuman Vong", 48 | "email": "neuman+pear@twilio.com", 49 | "role": "Developer" 50 | }, 51 | { 52 | "name": "Anant Narayanan", 53 | "email": "anant@php.net", 54 | "role": "Developer" 55 | } 56 | ], 57 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 58 | "homepage": "https://github.com/firebase/php-jwt", 59 | "keywords": [ 60 | "jwt", 61 | "php" 62 | ], 63 | "support": { 64 | "issues": "https://github.com/firebase/php-jwt/issues", 65 | "source": "https://github.com/firebase/php-jwt/tree/v6.4.0" 66 | }, 67 | "install-path": "../firebase/php-jwt" 68 | } 69 | ], 70 | "dev": true, 71 | "dev-package-names": [] 72 | } 73 | -------------------------------------------------------------------------------- /test/SnippetSettingsTest.php: -------------------------------------------------------------------------------- 1 | "bar")); 29 | $this->assertEquals("{\"app_id\":\"bar\",\"installation_type\":\"wordpress\",\"installation_version\":\"" . INTERCOM_PLUGIN_VERSION . "\"}", $snippet_settings->json()); 30 | } 31 | public function testJSONRenderingWithIdentityVerification() 32 | { 33 | $snippet_settings = new IntercomSnippetSettings(array("app_id" => "bar"), "s3cre7", new FakeWordPressUserForSnippetTest()); 34 | $jwt_data = array( 35 | "user_id" => "foo@bar.com", 36 | "email" => "foo@bar.com", 37 | "exp" => TimeProvider::getCurrentTime() + 3600 38 | ); 39 | $jwt = JWT::encode($jwt_data, "s3cre7", 'HS256'); 40 | $this->assertEquals('{"app_id":"bar","intercom_user_jwt":"'.$jwt.'","installation_type":"wordpress","installation_version":"' . INTERCOM_PLUGIN_VERSION . '"}', $snippet_settings->json()); 41 | } 42 | public function testJSONRenderingWithIdentityVerificationAndNoSecret() 43 | { 44 | $snippet_settings = new IntercomSnippetSettings(array("app_id" => "bar"), NULL, new FakeWordPressUserForSnippetTest()); 45 | $this->assertEquals("{\"app_id\":\"bar\",\"email\":\"foo@bar.com\",\"installation_type\":\"wordpress\",\"installation_version\":\"" . INTERCOM_PLUGIN_VERSION . "\"}", $snippet_settings->json()); 46 | } 47 | public function testInstallationType() 48 | { 49 | $snippet_settings = new IntercomSnippetSettings(array("app_id" => "bar")); 50 | $this->assertEquals("{\"app_id\":\"bar\",\"installation_type\":\"wordpress\",\"installation_version\":\"" . INTERCOM_PLUGIN_VERSION . "\"}", $snippet_settings->json()); 51 | } 52 | public function testIclLanguageConstant() 53 | { 54 | define('ICL_LANGUAGE_CODE', 'fr'); 55 | $snippet_settings = new IntercomSnippetSettings(array("app_id" => "bar")); 56 | $this->assertEquals("{\"app_id\":\"bar\",\"language_override\":\"fr\",\"installation_type\":\"wordpress\",\"installation_version\":\"" . INTERCOM_PLUGIN_VERSION . "\"}", $snippet_settings->json()); 57 | } 58 | 59 | public function testAppId() 60 | { 61 | $snippet_settings = new IntercomSnippetSettings(array("app_id" => "bar")); 62 | $this->assertEquals("bar", $snippet_settings->appId()); 63 | } 64 | 65 | public function testValidation() 66 | { 67 | $this->expectException(\Exception::class); 68 | $snippet = new IntercomSnippetSettings(array("foo" => "bar")); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "f0ba7c25cd025ed8dd9b1c80e1eb2ef8", 8 | "packages": [ 9 | { 10 | "name": "firebase/php-jwt", 11 | "version": "v6.4.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/firebase/php-jwt.git", 15 | "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/firebase/php-jwt/zipball/4dd1e007f22a927ac77da5a3fbb067b42d3bc224", 20 | "reference": "4dd1e007f22a927ac77da5a3fbb067b42d3bc224", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": "^7.1||^8.0" 25 | }, 26 | "require-dev": { 27 | "guzzlehttp/guzzle": "^6.5||^7.4", 28 | "phpspec/prophecy-phpunit": "^1.1", 29 | "phpunit/phpunit": "^7.5||^9.5", 30 | "psr/cache": "^1.0||^2.0", 31 | "psr/http-client": "^1.0", 32 | "psr/http-factory": "^1.0" 33 | }, 34 | "suggest": { 35 | "ext-sodium": "Support EdDSA (Ed25519) signatures", 36 | "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" 37 | }, 38 | "type": "library", 39 | "autoload": { 40 | "psr-4": { 41 | "Firebase\\JWT\\": "src" 42 | } 43 | }, 44 | "notification-url": "https://packagist.org/downloads/", 45 | "license": [ 46 | "BSD-3-Clause" 47 | ], 48 | "authors": [ 49 | { 50 | "name": "Neuman Vong", 51 | "email": "neuman+pear@twilio.com", 52 | "role": "Developer" 53 | }, 54 | { 55 | "name": "Anant Narayanan", 56 | "email": "anant@php.net", 57 | "role": "Developer" 58 | } 59 | ], 60 | "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", 61 | "homepage": "https://github.com/firebase/php-jwt", 62 | "keywords": [ 63 | "jwt", 64 | "php" 65 | ], 66 | "support": { 67 | "issues": "https://github.com/firebase/php-jwt/issues", 68 | "source": "https://github.com/firebase/php-jwt/tree/v6.4.0" 69 | }, 70 | "time": "2023-02-09T21:01:23+00:00" 71 | } 72 | ], 73 | "packages-dev": [], 74 | "aliases": [], 75 | "minimum-stability": "stable", 76 | "stability-flags": {}, 77 | "prefer-stable": false, 78 | "prefer-lowest": false, 79 | "platform": {}, 80 | "platform-dev": {}, 81 | "plugin-api-version": "2.6.0" 82 | } 83 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Intercom === 2 | Contributors: bobintercom, jacopointercom 3 | Tags: intercom, ai, customer, chat 4 | Requires at least: 4.7.0 5 | Tested up to: 6.7.2 6 | Requires PHP: 7.2 7 | License: Apache 2.0 8 | Stable tag: 3.0.2 9 | 10 | Official Intercom WordPress plugin: Engage visitors in real time, power growth with AI, and convert leads into loyal customers. 11 | 12 | == Description == 13 | 14 | [Intercom](https://www.intercom.com/) is a next-generation customer communications platform that combines powerful live chat, proactive messaging, and advanced AI solutions — like our Fin AI chatbot — to help businesses instantly connect with customers. 15 | 16 | By installing the Intercom WordPress plugin, you can seamlessly add the Messenger to your site, track both logged-in users and visitors, and engage them right away. With Intercom’s industry-leading AI at your fingertips, you’ll deliver fast, personalized support and drive growth more effectively than ever before. 17 | 18 | == Installation == 19 | 20 | Installing Intercom on your WordPress site takes just a few minutes. 21 | 22 | You can find full instructions on signing up and installing Intercom using the WordPress plugin [here](https://www.intercom.com/help/en/articles/173-install-intercom-on-your-wordpress-site). 23 | 24 | If you’re already an Intercom customer, you can also find instructions in the in-app [setup guide](https://app.intercom.com/a/apps/_/platform/guide) or [app store](https://app.intercom.com/a/apps/_/appstore?app_package_code=wordpress&search=wordpress). 25 | 26 | The first thing you’ll need to do is install and activate the plugin - you must be using WordPress v4.9.0 or higher and have the ability to install plugins in order to use this method. 27 | 28 | Note: This plugin injects a Javascript snippet on your website frontend containing dynamic user data. Some caching solutions will cache entire pages and should not be used with this plugin. Doing so may cause conversations to be delivered to the wrong user. 29 | 30 | == Screenshots == 31 | 1. Plugin settings authenticate with Intercom settings_not_auth.png 32 | 2. Plugin settings successfully authenticated with Intercom settings_auth.png 33 | 3. Intercom widget used by customers to communicate with the business widget.png 34 | == Changelog == 35 | = 3.0.2 = 36 | 37 | https://github.com/intercom/intercom-wordpress/pull/131 38 | * Updated version attribute to avoid it getting sent in the user data. 39 | 40 | = 3.0.1 = 41 | 42 | https://github.com/intercom/intercom-wordpress/pull/130 43 | * Added version tracking to help Intercom provide better support by identifying which plugin version is in use. 44 | 45 | https://github.com/intercom/intercom-wordpress/pull/88 46 | * Loads Intercom messenger last to ensure JQuery is present. 47 | 48 | = 3.0.0 = 49 | 50 | https://github.com/intercom/intercom-wordpress/pull/127 51 | * Replaced user_hash with intercom_user_jwt https://www.intercom.com/help/en/articles/10589769-authenticating-users-in-the-messenger-with-json-web-tokens-jwts. 52 | * Updated readme to follow guidelines. 53 | * Added missing tests. 54 | 55 | == Upgrade Notice == 56 | = 3.0.1 = 57 | Updated version attribute name to avoid setting it as part of user data. 58 | 59 | = 3.0.1 = 60 | Help Intercom provide better support by sharing the plugin version is in use. 61 | 62 | = 3.0.0 = 63 | Upgrade the security of your messenger with the introduction of JWT - https://www.intercom.com/help/en/articles/10589769-authenticating-users-in-the-messenger-with-json-web-tokens-jwts 64 | -------------------------------------------------------------------------------- /test/SecureModeCalculatorTest.php: -------------------------------------------------------------------------------- 1 | "abcdef", "email" => "test@intercom.io"); 24 | $calculator = new MessengerSecurityCalculator($data, "s3cre7"); 25 | $jwt_data = array( 26 | "user_id" => "test@intercom.io", 27 | "email" => "test@intercom.io", 28 | "exp" => TimeProvider::getCurrentTime() + 3600 29 | ); 30 | $jwt = JWT::encode($jwt_data, "s3cre7", 'HS256'); 31 | $this->assertEquals( 32 | array( 33 | "app_id" => "abcdef", 34 | "intercom_user_jwt" => $jwt 35 | ), 36 | $calculator->messengerSecurityComponent() 37 | ); 38 | } 39 | 40 | public function testUserIdEmailJWT() 41 | { 42 | $data = array("app_id" => "abcdef", "user_id" => "abcdef", "email" => "test@intercom.io"); 43 | $calculator = new MessengerSecurityCalculator($data, "s3cre7"); 44 | $jwt_data = array( 45 | "user_id" => "abcdef", 46 | "email" => "test@intercom.io", 47 | "exp" => TimeProvider::getCurrentTime() + 3600 48 | ); 49 | $jwt = JWT::encode($jwt_data, "s3cre7", 'HS256'); 50 | $this->assertEquals( 51 | array( 52 | "app_id" => "abcdef", 53 | "intercom_user_jwt" => $jwt 54 | ), 55 | $calculator->messengerSecurityComponent() 56 | ); 57 | } 58 | 59 | public function testUserIdEmailNameJWT() 60 | { 61 | $data = array("app_id" => "abcdef", "user_id" => "abcdef", "email" => "test@intercom.io", "name" => "John Doe"); 62 | $calculator = new MessengerSecurityCalculator($data, "s3cre7"); 63 | $jwt_data = array( 64 | "user_id" => "abcdef", 65 | "email" => "test@intercom.io", 66 | "name" => "John Doe", 67 | "exp" => TimeProvider::getCurrentTime() + 3600 68 | ); 69 | $jwt = JWT::encode($jwt_data, "s3cre7", 'HS256'); 70 | $this->assertEquals( 71 | array( 72 | "app_id" => "abcdef", 73 | "intercom_user_jwt" => $jwt 74 | ), 75 | $calculator->messengerSecurityComponent() 76 | ); 77 | } 78 | 79 | public function testEmpty() 80 | { 81 | $data = array("app_id" => "abcdef"); 82 | $calculator = new MessengerSecurityCalculator($data, "s3cre7"); 83 | $this->assertEquals( 84 | array( 85 | "app_id" => "abcdef", 86 | ), 87 | $calculator->messengerSecurityComponent() 88 | ); 89 | } 90 | 91 | public function testExtraJWTData() 92 | { 93 | putenv('INTERCOM_PLUGIN_TEST_JWT_DATA=' . json_encode(array("custom_data" => "custom_value"))); 94 | 95 | $data = array("app_id" => "abcdef", "user_id" => "abcdef", "email" => "test@intercom.io", "name" => "John Doe"); 96 | $calculator = new MessengerSecurityCalculator($data, "s3cre7"); 97 | $jwt_data = array( 98 | "user_id" => "abcdef", 99 | "email" => "test@intercom.io", 100 | "name" => "John Doe", 101 | "custom_data" => "custom_value", 102 | "exp" => TimeProvider::getCurrentTime() + 3600 103 | ); 104 | $jwt = JWT::encode($jwt_data, "s3cre7", 'HS256'); 105 | $this->assertEquals( 106 | array( 107 | "app_id" => "abcdef", 108 | "intercom_user_jwt" => $jwt 109 | ), 110 | $calculator->messengerSecurityComponent() 111 | ); 112 | putenv('INTERCOM_PLUGIN_TEST_JWT_DATA='); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [6.4.0](https://github.com/firebase/php-jwt/compare/v6.3.2...v6.4.0) (2023-02-08) 4 | 5 | 6 | ### Features 7 | 8 | * add support for W3C ES256K ([#462](https://github.com/firebase/php-jwt/issues/462)) ([213924f](https://github.com/firebase/php-jwt/commit/213924f51936291fbbca99158b11bd4ae56c2c95)) 9 | * improve caching by only decoding jwks when necessary ([#486](https://github.com/firebase/php-jwt/issues/486)) ([78d3ed1](https://github.com/firebase/php-jwt/commit/78d3ed1073553f7d0bbffa6c2010009a0d483d5c)) 10 | 11 | ## [6.3.2](https://github.com/firebase/php-jwt/compare/v6.3.1...v6.3.2) (2022-11-01) 12 | 13 | 14 | ### Bug Fixes 15 | 16 | * check kid before using as array index ([bad1b04](https://github.com/firebase/php-jwt/commit/bad1b040d0c736bbf86814c6b5ae614f517cf7bd)) 17 | 18 | ## [6.3.1](https://github.com/firebase/php-jwt/compare/v6.3.0...v6.3.1) (2022-11-01) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * casing of GET for PSR compat ([#451](https://github.com/firebase/php-jwt/issues/451)) ([60b52b7](https://github.com/firebase/php-jwt/commit/60b52b71978790eafcf3b95cfbd83db0439e8d22)) 24 | * string interpolation format for php 8.2 ([#446](https://github.com/firebase/php-jwt/issues/446)) ([2e07d8a](https://github.com/firebase/php-jwt/commit/2e07d8a1524d12b69b110ad649f17461d068b8f2)) 25 | 26 | ## 6.3.0 / 2022-07-15 27 | 28 | - Added ES256 support to JWK parsing ([#399](https://github.com/firebase/php-jwt/pull/399)) 29 | - Fixed potential caching error in `CachedKeySet` by caching jwks as strings ([#435](https://github.com/firebase/php-jwt/pull/435)) 30 | 31 | ## 6.2.0 / 2022-05-14 32 | 33 | - Added `CachedKeySet` ([#397](https://github.com/firebase/php-jwt/pull/397)) 34 | - Added `$defaultAlg` parameter to `JWT::parseKey` and `JWT::parseKeySet` ([#426](https://github.com/firebase/php-jwt/pull/426)). 35 | 36 | ## 6.1.0 / 2022-03-23 37 | 38 | - Drop support for PHP 5.3, 5.4, 5.5, 5.6, and 7.0 39 | - Add parameter typing and return types where possible 40 | 41 | ## 6.0.0 / 2022-01-24 42 | 43 | - **Backwards-Compatibility Breaking Changes**: See the [Release Notes](https://github.com/firebase/php-jwt/releases/tag/v6.0.0) for more information. 44 | - New Key object to prevent key/algorithm type confusion (#365) 45 | - Add JWK support (#273) 46 | - Add ES256 support (#256) 47 | - Add ES384 support (#324) 48 | - Add Ed25519 support (#343) 49 | 50 | ## 5.0.0 / 2017-06-26 51 | - Support RS384 and RS512. 52 | See [#117](https://github.com/firebase/php-jwt/pull/117). Thanks [@joostfaassen](https://github.com/joostfaassen)! 53 | - Add an example for RS256 openssl. 54 | See [#125](https://github.com/firebase/php-jwt/pull/125). Thanks [@akeeman](https://github.com/akeeman)! 55 | - Detect invalid Base64 encoding in signature. 56 | See [#162](https://github.com/firebase/php-jwt/pull/162). Thanks [@psignoret](https://github.com/psignoret)! 57 | - Update `JWT::verify` to handle OpenSSL errors. 58 | See [#159](https://github.com/firebase/php-jwt/pull/159). Thanks [@bshaffer](https://github.com/bshaffer)! 59 | - Add `array` type hinting to `decode` method 60 | See [#101](https://github.com/firebase/php-jwt/pull/101). Thanks [@hywak](https://github.com/hywak)! 61 | - Add all JSON error types. 62 | See [#110](https://github.com/firebase/php-jwt/pull/110). Thanks [@gbalduzzi](https://github.com/gbalduzzi)! 63 | - Bugfix 'kid' not in given key list. 64 | See [#129](https://github.com/firebase/php-jwt/pull/129). Thanks [@stampycode](https://github.com/stampycode)! 65 | - Miscellaneous cleanup, documentation and test fixes. 66 | See [#107](https://github.com/firebase/php-jwt/pull/107), [#115](https://github.com/firebase/php-jwt/pull/115), 67 | [#160](https://github.com/firebase/php-jwt/pull/160), [#161](https://github.com/firebase/php-jwt/pull/161), and 68 | [#165](https://github.com/firebase/php-jwt/pull/165). Thanks [@akeeman](https://github.com/akeeman), 69 | [@chinedufn](https://github.com/chinedufn), and [@bshaffer](https://github.com/bshaffer)! 70 | 71 | ## 4.0.0 / 2016-07-17 72 | - Add support for late static binding. See [#88](https://github.com/firebase/php-jwt/pull/88) for details. Thanks to [@chappy84](https://github.com/chappy84)! 73 | - Use static `$timestamp` instead of `time()` to improve unit testing. See [#93](https://github.com/firebase/php-jwt/pull/93) for details. Thanks to [@josephmcdermott](https://github.com/josephmcdermott)! 74 | - Fixes to exceptions classes. See [#81](https://github.com/firebase/php-jwt/pull/81) for details. Thanks to [@Maks3w](https://github.com/Maks3w)! 75 | - Fixes to PHPDoc. See [#76](https://github.com/firebase/php-jwt/pull/76) for details. Thanks to [@akeeman](https://github.com/akeeman)! 76 | 77 | ## 3.0.0 / 2015-07-22 78 | - Minimum PHP version updated from `5.2.0` to `5.3.0`. 79 | - Add `\Firebase\JWT` namespace. See 80 | [#59](https://github.com/firebase/php-jwt/pull/59) for details. Thanks to 81 | [@Dashron](https://github.com/Dashron)! 82 | - Require a non-empty key to decode and verify a JWT. See 83 | [#60](https://github.com/firebase/php-jwt/pull/60) for details. Thanks to 84 | [@sjones608](https://github.com/sjones608)! 85 | - Cleaner documentation blocks in the code. See 86 | [#62](https://github.com/firebase/php-jwt/pull/62) for details. Thanks to 87 | [@johanderuijter](https://github.com/johanderuijter)! 88 | 89 | ## 2.2.0 / 2015-06-22 90 | - Add support for adding custom, optional JWT headers to `JWT::encode()`. See 91 | [#53](https://github.com/firebase/php-jwt/pull/53/files) for details. Thanks to 92 | [@mcocaro](https://github.com/mcocaro)! 93 | 94 | ## 2.1.0 / 2015-05-20 95 | - Add support for adding a leeway to `JWT:decode()` that accounts for clock skew 96 | between signing and verifying entities. Thanks to [@lcabral](https://github.com/lcabral)! 97 | - Add support for passing an object implementing the `ArrayAccess` interface for 98 | `$keys` argument in `JWT::decode()`. Thanks to [@aztech-dev](https://github.com/aztech-dev)! 99 | 100 | ## 2.0.0 / 2015-04-01 101 | - **Note**: It is strongly recommended that you update to > v2.0.0 to address 102 | known security vulnerabilities in prior versions when both symmetric and 103 | asymmetric keys are used together. 104 | - Update signature for `JWT::decode(...)` to require an array of supported 105 | algorithms to use when verifying token signatures. 106 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/src/CachedKeySet.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class CachedKeySet implements ArrayAccess 20 | { 21 | /** 22 | * @var string 23 | */ 24 | private $jwksUri; 25 | /** 26 | * @var ClientInterface 27 | */ 28 | private $httpClient; 29 | /** 30 | * @var RequestFactoryInterface 31 | */ 32 | private $httpFactory; 33 | /** 34 | * @var CacheItemPoolInterface 35 | */ 36 | private $cache; 37 | /** 38 | * @var ?int 39 | */ 40 | private $expiresAfter; 41 | /** 42 | * @var ?CacheItemInterface 43 | */ 44 | private $cacheItem; 45 | /** 46 | * @var array> 47 | */ 48 | private $keySet; 49 | /** 50 | * @var string 51 | */ 52 | private $cacheKey; 53 | /** 54 | * @var string 55 | */ 56 | private $cacheKeyPrefix = 'jwks'; 57 | /** 58 | * @var int 59 | */ 60 | private $maxKeyLength = 64; 61 | /** 62 | * @var bool 63 | */ 64 | private $rateLimit; 65 | /** 66 | * @var string 67 | */ 68 | private $rateLimitCacheKey; 69 | /** 70 | * @var int 71 | */ 72 | private $maxCallsPerMinute = 10; 73 | /** 74 | * @var string|null 75 | */ 76 | private $defaultAlg; 77 | 78 | public function __construct( 79 | string $jwksUri, 80 | ClientInterface $httpClient, 81 | RequestFactoryInterface $httpFactory, 82 | CacheItemPoolInterface $cache, 83 | int $expiresAfter = null, 84 | bool $rateLimit = false, 85 | string $defaultAlg = null 86 | ) { 87 | $this->jwksUri = $jwksUri; 88 | $this->httpClient = $httpClient; 89 | $this->httpFactory = $httpFactory; 90 | $this->cache = $cache; 91 | $this->expiresAfter = $expiresAfter; 92 | $this->rateLimit = $rateLimit; 93 | $this->defaultAlg = $defaultAlg; 94 | $this->setCacheKeys(); 95 | } 96 | 97 | /** 98 | * @param string $keyId 99 | * @return Key 100 | */ 101 | public function offsetGet($keyId): Key 102 | { 103 | if (!$this->keyIdExists($keyId)) { 104 | throw new OutOfBoundsException('Key ID not found'); 105 | } 106 | return JWK::parseKey($this->keySet[$keyId], $this->defaultAlg); 107 | } 108 | 109 | /** 110 | * @param string $keyId 111 | * @return bool 112 | */ 113 | public function offsetExists($keyId): bool 114 | { 115 | return $this->keyIdExists($keyId); 116 | } 117 | 118 | /** 119 | * @param string $offset 120 | * @param Key $value 121 | */ 122 | public function offsetSet($offset, $value): void 123 | { 124 | throw new LogicException('Method not implemented'); 125 | } 126 | 127 | /** 128 | * @param string $offset 129 | */ 130 | public function offsetUnset($offset): void 131 | { 132 | throw new LogicException('Method not implemented'); 133 | } 134 | 135 | /** 136 | * @return array 137 | */ 138 | private function formatJwksForCache(string $jwks): array 139 | { 140 | $jwks = json_decode($jwks, true); 141 | 142 | if (!isset($jwks['keys'])) { 143 | throw new UnexpectedValueException('"keys" member must exist in the JWK Set'); 144 | } 145 | 146 | if (empty($jwks['keys'])) { 147 | throw new InvalidArgumentException('JWK Set did not contain any keys'); 148 | } 149 | 150 | $keys = []; 151 | foreach ($jwks['keys'] as $k => $v) { 152 | $kid = isset($v['kid']) ? $v['kid'] : $k; 153 | $keys[(string) $kid] = $v; 154 | } 155 | 156 | return $keys; 157 | } 158 | 159 | private function keyIdExists(string $keyId): bool 160 | { 161 | if (null === $this->keySet) { 162 | $item = $this->getCacheItem(); 163 | // Try to load keys from cache 164 | if ($item->isHit()) { 165 | // item found! retrieve it 166 | $this->keySet = $item->get(); 167 | // If the cached item is a string, the JWKS response was cached (previous behavior). 168 | // Parse this into expected format array instead. 169 | if (\is_string($this->keySet)) { 170 | $this->keySet = $this->formatJwksForCache($this->keySet); 171 | } 172 | } 173 | } 174 | 175 | if (!isset($this->keySet[$keyId])) { 176 | if ($this->rateLimitExceeded()) { 177 | return false; 178 | } 179 | $request = $this->httpFactory->createRequest('GET', $this->jwksUri); 180 | $jwksResponse = $this->httpClient->sendRequest($request); 181 | $this->keySet = $this->formatJwksForCache((string) $jwksResponse->getBody()); 182 | 183 | if (!isset($this->keySet[$keyId])) { 184 | return false; 185 | } 186 | 187 | $item = $this->getCacheItem(); 188 | $item->set($this->keySet); 189 | if ($this->expiresAfter) { 190 | $item->expiresAfter($this->expiresAfter); 191 | } 192 | $this->cache->save($item); 193 | } 194 | 195 | return true; 196 | } 197 | 198 | private function rateLimitExceeded(): bool 199 | { 200 | if (!$this->rateLimit) { 201 | return false; 202 | } 203 | 204 | $cacheItem = $this->cache->getItem($this->rateLimitCacheKey); 205 | if (!$cacheItem->isHit()) { 206 | $cacheItem->expiresAfter(1); // # of calls are cached each minute 207 | } 208 | 209 | $callsPerMinute = (int) $cacheItem->get(); 210 | if (++$callsPerMinute > $this->maxCallsPerMinute) { 211 | return true; 212 | } 213 | $cacheItem->set($callsPerMinute); 214 | $this->cache->save($cacheItem); 215 | return false; 216 | } 217 | 218 | private function getCacheItem(): CacheItemInterface 219 | { 220 | if (\is_null($this->cacheItem)) { 221 | $this->cacheItem = $this->cache->getItem($this->cacheKey); 222 | } 223 | 224 | return $this->cacheItem; 225 | } 226 | 227 | private function setCacheKeys(): void 228 | { 229 | if (empty($this->jwksUri)) { 230 | throw new RuntimeException('JWKS URI is empty'); 231 | } 232 | 233 | // ensure we do not have illegal characters 234 | $key = preg_replace('|[^a-zA-Z0-9_\.!]|', '', $this->jwksUri); 235 | 236 | // add prefix 237 | $key = $this->cacheKeyPrefix . $key; 238 | 239 | // Hash keys if they exceed $maxKeyLength of 64 240 | if (\strlen($key) > $this->maxKeyLength) { 241 | $key = substr(hash('sha256', $key), 0, $this->maxKeyLength); 242 | } 243 | 244 | $this->cacheKey = $key; 245 | 246 | if ($this->rateLimit) { 247 | // add prefix 248 | $rateLimitKey = $this->cacheKeyPrefix . 'ratelimit' . $key; 249 | 250 | // Hash keys if they exceed $maxKeyLength of 64 251 | if (\strlen($rateLimitKey) > $this->maxKeyLength) { 252 | $rateLimitKey = substr(hash('sha256', $rateLimitKey), 0, $this->maxKeyLength); 253 | } 254 | 255 | $this->rateLimitCacheKey = $rateLimitKey; 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /test/IntercomSettingsPageTest.php: -------------------------------------------------------------------------------- 1 | originalGet = $_GET; 17 | 18 | // Default settings 19 | $this->settings = [ 20 | 'app_id' => 'test_app_id', 21 | 'secret' => 'test_secret', 22 | 'identity_verification' => false 23 | ]; 24 | 25 | // Create the IntercomSettingsPage instance 26 | $this->intercomSettingsPage = new IntercomSettingsPage($this->settings); 27 | } 28 | 29 | protected function tearDown(): void 30 | { 31 | // Restore original $_GET 32 | $_GET = $this->originalGet; 33 | 34 | parent::tearDown(); 35 | } 36 | 37 | /** 38 | * Test the constructor properly initializes the class 39 | */ 40 | public function testConstructor() 41 | { 42 | $settings = [ 43 | 'app_id' => 'test_app_id', 44 | 'secret' => 'test_secret', 45 | 'identity_verification' => true 46 | ]; 47 | 48 | $intercomSettingsPage = new IntercomSettingsPage($settings); 49 | 50 | // Use reflection to access private properties 51 | $reflection = new ReflectionClass($intercomSettingsPage); 52 | $settingsProperty = $reflection->getProperty('settings'); 53 | $settingsProperty->setAccessible(true); 54 | $stylesProperty = $reflection->getProperty('styles'); 55 | $stylesProperty->setAccessible(true); 56 | 57 | $this->assertEquals($settings, $settingsProperty->getValue($intercomSettingsPage)); 58 | $this->assertNotEmpty($stylesProperty->getValue($intercomSettingsPage)); 59 | } 60 | 61 | /** 62 | * Test the dismissibleMessage method 63 | */ 64 | public function testDismissibleMessage() 65 | { 66 | $message = "Test message"; 67 | $result = $this->intercomSettingsPage->dismissibleMessage($message); 68 | 69 | $this->assertStringContainsString('
', $result); 70 | $this->assertStringContainsString('

' . $message . '

', $result); 71 | $this->assertStringContainsString(' 123 |
124 | END; 125 | } 126 | 127 | public function getAuthUrl() { 128 | return "https://wordpress_auth.intercom.io/confirm?state=".get_site_url()."::".wp_create_nonce('intercom-oauth'); 129 | } 130 | 131 | public function htmlUnclosed() 132 | { 133 | $settings = $this->getSettings(); 134 | $styles = $this->getStyles(); 135 | $app_id = WordPressEscaper::escAttr($settings['app_id']); 136 | $secret = WordPressEscaper::escAttr($settings['secret']); 137 | $auth_url = $this->getAuthUrl(); 138 | $dismissable_message = ''; 139 | if (isset($_GET['appId'])) { 140 | // Copying app_id from setup guide 141 | $app_id = WordPressEscaper::escAttr($_GET['appId']); 142 | $dismissable_message = $this->dismissibleMessage("We've copied your new Intercom app id below. click to save changes and then close this window to finish signing up for Intercom."); 143 | } 144 | if (isset($_GET['saved'])) { 145 | $dismissable_message = $this->dismissibleMessage("Your app id has been successfully saved. You can now close this window to finish signing up for Intercom."); 146 | } 147 | if (isset($_GET['authenticated'])) { 148 | $dismissable_message = $this->dismissibleMessage('You successfully authenticated with Intercom'); 149 | } 150 | 151 | return << 154 | 159 | 160 |
161 | $dismissable_message 162 | 163 |
164 |
165 |
166 | 167 |
168 |
169 |
Get started with Intercom
170 | 171 |
172 | Chat with visitors to your website in real-time, capture them as leads, and convert them to customers. Install Intercom on your WordPress site in a couple of clicks. 173 |
174 | 175 |
176 | 177 | 178 | 179 |
180 |
181 | 182 |
Intercom setup
183 |
Intercom app ID saved
184 |
185 |
Intercom has been installed
186 | 187 |
188 |
189 | Intercom is now set up and ready to go. You can now chat with your existing and potential new customers, send them targeted messages, and get feedback. 190 |
191 |
192 | Click here to access your Intercom Team Inbox. 193 |
194 |
195 | Need help? Visit our documentation for best practices, tips, and much more. 196 |
197 |
198 |
199 | 200 |
201 |
202 | Learn more about our products : Messages, Articles and Inbox. 203 |
204 |
205 | 206 | 207 | 208 | 209 | 213 | 214 | 215 |
210 | 211 | 212 |
216 | 217 | END; 218 | } 219 | 220 | public function htmlClosed() 221 | { 222 | $settings = $this->getSettings(); 223 | $styles = $this->getStyles(); 224 | $auth_url = $this->getAuthUrl(); 225 | $secret = WordPressEscaper::escAttr($settings['secret']); 226 | $app_id = WordPressEscaper::escAttr($settings['app_id']); 227 | $auth_url_identity_verification = ""; 228 | if (empty($secret) && !empty($app_id)) { 229 | $auth_url_identity_verification = $auth_url.'&enable_identity_verification=1'; 230 | } 231 | return << 233 |
234 | 237 |

Identity Verification ensures that conversations between you and your users are kept private.
238 |
239 | Learn more about Identity Verification 240 |

241 |
242 |
If the Intercom application associated with your WordPress is incorrect, please click here to reconnect with Intercom, to choose a new application.
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 | 252 | END; 253 | } 254 | 255 | public function html() 256 | { 257 | return $this->htmlUnclosed() . $this->htmlClosed(); 258 | } 259 | 260 | public function setStyles($settings) { 261 | $styles = array(); 262 | $app_id = WordPressEscaper::escAttr($settings['app_id']); 263 | $secret = WordPressEscaper::escAttr($settings['secret']); 264 | $identity_verification = WordPressEscaper::escAttr($settings['identity_verification']); 265 | 266 | // Use Case : Identity Verification enabled : checkbox checked and disabled 267 | if($identity_verification) { 268 | $styles['identity_verification_state'] = 'checked disabled'; 269 | } else { 270 | $styles['identity_verification_state'] = ''; 271 | } 272 | 273 | // Use Case : app_id here but Identity Verification disabled 274 | if (empty($secret) && !empty($app_id)) { 275 | $styles['app_secret_row_style'] = 'display: none;'; 276 | $styles['app_secret_link_style'] = ''; 277 | } else { 278 | $styles['app_secret_row_style'] = ''; 279 | $styles['app_secret_link_style'] = 'display: none;'; 280 | } 281 | 282 | // Copying appId from Intercom Setup Guide for validation 283 | if (isset($_GET['appId'])) { 284 | $app_id = WordPressEscaper::escAttr($_GET['appId']); 285 | $styles['app_id_state'] = 'readonly'; 286 | $styles['app_id_class'] = "cta__email"; 287 | $styles['button_submit_style'] = ''; 288 | $styles['app_id_copy_hidden'] = 'display: none;'; 289 | $styles['app_id_copy_title'] = ''; 290 | $styles['identity_verification_state'] = 'disabled'; # Prevent from sending POST data about identity_verification when using app_id form 291 | } else { 292 | $styles['app_id_class'] = ""; 293 | $styles['button_submit_style'] = 'display: none;'; 294 | $styles['app_id_copy_title'] = 'display: none;'; 295 | $styles['app_id_state'] = 'disabled'; # Prevent from sending POST data about app_id when using identity_verification form 296 | $styles['app_id_copy_hidden'] = ''; 297 | } 298 | 299 | //Use Case App_id successfully copied 300 | if (isset($_GET['saved'])) { 301 | $styles['app_id_copy_hidden'] = 'display: none;'; 302 | $styles['app_id_saved_title'] = ''; 303 | } else { 304 | $styles['app_id_saved_title'] = 'display: none;'; 305 | } 306 | 307 | // Display 'connect with intercom' button if no app_id provided (copied from setup guide or from Oauth) 308 | if (empty($app_id)) { 309 | $styles['app_id_row_style'] = 'display: none;'; 310 | $styles['app_id_link_style'] = ''; 311 | } else { 312 | $styles['app_id_row_style'] = ''; 313 | $styles['app_id_link_style'] = 'display: none;'; 314 | } 315 | return $styles; 316 | } 317 | 318 | private function getSettings() 319 | { 320 | return $this->settings; 321 | } 322 | 323 | private function getStyles() 324 | { 325 | return $this->styles; 326 | } 327 | } 328 | 329 | class IntercomSnippet 330 | { 331 | private $snippet_settings = ""; 332 | 333 | public function __construct($snippet_settings) 334 | { 335 | $this->snippet_settings = $snippet_settings; 336 | } 337 | public function html() 338 | { 339 | return $this->shutdown_on_logout() . $this->source(); 340 | } 341 | 342 | private function shutdown_on_logout() 343 | { 344 | return << 346 | document.onreadystatechange = function () { 347 | if (document.readyState == "complete") { 348 | var logout_link = document.querySelectorAll('a[href*="wp-login.php?action=logout"]'); 349 | if (logout_link) { 350 | for(var i=0; i < logout_link.length; i++) { 351 | logout_link[i].addEventListener( "click", function() { 352 | Intercom('shutdown'); 353 | }); 354 | } 355 | } 356 | } 357 | }; 358 | 359 | 360 | HTML; 361 | } 362 | 363 | private function source() 364 | { 365 | $snippet_json = $this->snippet_settings->json(); 366 | $app_id = $this->snippet_settings->appId(); 367 | 368 | return << 370 | window.intercomSettings = $snippet_json; 371 | 372 | 373 | HTML; 374 | } 375 | } 376 | 377 | class IntercomSnippetSettings 378 | { 379 | private $raw_data = array(); 380 | private $secret = NULL; 381 | private $wordpress_user = NULL; 382 | 383 | public function __construct($raw_data, $secret = NULL, $wordpress_user = NULL, $constants = array('ICL_LANGUAGE_CODE' => 'language_override')) 384 | { 385 | $this->raw_data = $this->validateRawData($raw_data); 386 | $this->secret = $secret; 387 | $this->wordpress_user = $wordpress_user; 388 | $this->constants = $constants; 389 | } 390 | 391 | public function json() 392 | { 393 | return json_encode($this->getRawData()); 394 | } 395 | 396 | public function appId() 397 | { 398 | $raw_data = $this->getRawData(); 399 | return $raw_data["app_id"]; 400 | } 401 | 402 | private function getRawData() 403 | { 404 | $user = new IntercomUser($this->wordpress_user, $this->raw_data); 405 | $messengerSecurityCalculator = new MessengerSecurityCalculator($user->buildSettings(), $this->secret); 406 | $settings = $messengerSecurityCalculator->messengerSecurityComponent(); 407 | $result = $this->mergeConstants(apply_filters("intercom_settings", $settings)); 408 | $result['installation_type'] = 'wordpress'; 409 | $result['installation_version'] = INTERCOM_PLUGIN_VERSION; 410 | return $result; 411 | } 412 | 413 | private function mergeConstants($settings) { 414 | foreach($this->constants as $key => $value) { 415 | if (defined($key)) { 416 | $const_val = WordPressEscaper::escJS(constant($key)); 417 | $settings = array_merge($settings, array($value => $const_val)); 418 | } 419 | } 420 | return $settings; 421 | } 422 | 423 | private function validateRawData($raw_data) 424 | { 425 | if (!array_key_exists("app_id", $raw_data)) { 426 | throw new Exception("app_id is required"); 427 | } 428 | return $raw_data; 429 | } 430 | } 431 | 432 | if (getenv('INTERCOM_PLUGIN_TEST') == '1' && !function_exists('apply_filters')) { 433 | function apply_filters($key, $value) { 434 | if ($key == "intercom_sensitive_attributes") { 435 | $extra_data_key = 'INTERCOM_PLUGIN_TEST_JWT_DATA'; 436 | } elseif ($key == "intercom_settings") { 437 | $extra_data_key = 'INTERCOM_PLUGIN_TEST_SETTINGS'; 438 | } 439 | 440 | $extra_data = getenv($extra_data_key); 441 | if ($extra_data) { 442 | $extra_data = json_decode($extra_data, true); 443 | return array_merge($value, $extra_data); 444 | } 445 | 446 | return $value; 447 | } 448 | } 449 | 450 | class WordPressEscaper 451 | { 452 | public static function escAttr($value) 453 | { 454 | if (function_exists('esc_attr')) { 455 | return esc_attr($value); 456 | } else { 457 | if (getenv('INTERCOM_PLUGIN_TEST') == '1') { 458 | return $value; 459 | } 460 | } 461 | } 462 | 463 | public static function escJS($value) 464 | { 465 | if (function_exists('esc_js')) { 466 | return esc_js($value); 467 | } else { 468 | if (getenv('INTERCOM_PLUGIN_TEST') == '1') { 469 | return $value; 470 | } 471 | } 472 | } 473 | } 474 | 475 | class IntercomUser 476 | { 477 | private $wordpress_user = NULL; 478 | private $settings = array(); 479 | 480 | public function __construct($wordpress_user, $settings) 481 | { 482 | $this->wordpress_user = $wordpress_user; 483 | $this->settings = $settings; 484 | } 485 | 486 | public function buildSettings() 487 | { 488 | if (empty($this->wordpress_user)) 489 | { 490 | return $this->settings; 491 | } 492 | if (!empty($this->wordpress_user->user_email)) 493 | { 494 | $this->settings["email"] = WordPressEscaper::escJS($this->wordpress_user->user_email); 495 | } 496 | if (!empty($this->wordpress_user->ID)) 497 | { 498 | $this->settings["user_id"] = WordPressEscaper::escJS($this->wordpress_user->ID); 499 | } 500 | if (!empty($this->wordpress_user->display_name)) 501 | { 502 | $this->settings["name"] = WordPressEscaper::escJS($this->wordpress_user->display_name); 503 | } 504 | return $this->settings; 505 | } 506 | } 507 | 508 | class Validator 509 | { 510 | private $inputs = array(); 511 | private $validation; 512 | 513 | public function __construct($inputs, $validation) 514 | { 515 | $this->input = $inputs; 516 | $this->validation = $validation; 517 | } 518 | 519 | public function validAppId() 520 | { 521 | return $this->validate($this->input["app_id"]); 522 | } 523 | 524 | public function validSecret() 525 | { 526 | return $this->validate($this->input["secret"]); 527 | } 528 | 529 | private function validate($x) 530 | { 531 | return call_user_func($this->validation, $x); 532 | } 533 | } 534 | 535 | if (getenv('INTERCOM_PLUGIN_TEST') != '1') { 536 | if (!defined('ABSPATH')) exit; 537 | } 538 | 539 | function add_intercom_snippet() 540 | { 541 | $options = get_option('intercom'); 542 | $snippet_settings = new IntercomSnippetSettings( 543 | array("app_id" => WordPressEscaper::escJS($options['app_id'])), 544 | WordPressEscaper::escJS($options['secret']), 545 | wp_get_current_user() 546 | ); 547 | $snippet = new IntercomSnippet($snippet_settings); 548 | echo $snippet->html(); 549 | } 550 | 551 | function add_intercom_settings_page() 552 | { 553 | add_options_page( 554 | 'Intercom Settings', 555 | 'Intercom', 556 | 'manage_options', 557 | 'intercom', 558 | 'render_intercom_options_page' 559 | ); 560 | } 561 | 562 | function render_intercom_options_page() 563 | { 564 | if (!current_user_can('manage_options')) 565 | { 566 | wp_die('You do not have sufficient permissions to access Intercom settings'); 567 | } 568 | $options = get_option('intercom'); 569 | $settings_page = new IntercomSettingsPage(array("app_id" => $options['app_id'], "secret" => $options['secret'])); 570 | echo $settings_page->htmlUnclosed(); 571 | wp_nonce_field('intercom-update'); 572 | echo $settings_page->htmlClosed(); 573 | } 574 | 575 | function intercom_settings() { 576 | register_setting('intercom', 'intercom'); 577 | if (isset($_GET['state']) && wp_verify_nonce($_GET[ 'state'], 'intercom-oauth') && current_user_can('manage_options') && isset($_GET['app_id']) && isset($_GET['secret']) ) { 578 | $validator = new Validator($_GET, function($x) { return wp_kses(trim($x), array()); }); 579 | update_option("intercom", array("app_id" => $validator->validAppId(), "secret" => $validator->validSecret())); 580 | $redirect_to = 'options-general.php?page=intercom&authenticated=1'; 581 | wp_safe_redirect(admin_url($redirect_to)); 582 | } 583 | if (current_user_can('manage_options') && isset($_POST['app_id']) && isset($_POST[ '_wpnonce']) && wp_verify_nonce($_POST[ '_wpnonce'],'intercom-update')) { 584 | $options = array(); 585 | $options["app_id"] = WordPressEscaper::escAttr($_POST['app_id']); 586 | update_option("intercom", $options); 587 | wp_safe_redirect(admin_url('options-general.php?page=intercom&saved=1')); 588 | } 589 | } 590 | 591 | if (getenv('INTERCOM_PLUGIN_TEST') != '1') { 592 | add_action('admin_menu', 'add_intercom_settings_page'); 593 | add_action('network_admin_menu', 'add_intercom_settings_page'); 594 | add_action('admin_init', 'intercom_settings'); 595 | // Add in priority to load the snippet last 596 | add_action('wp_footer', 'add_intercom_snippet', 999); 597 | } 598 | -------------------------------------------------------------------------------- /vendor/firebase/php-jwt/src/JWT.php: -------------------------------------------------------------------------------- 1 | 24 | * @author Anant Narayanan 25 | * @license http://opensource.org/licenses/BSD-3-Clause 3-clause BSD 26 | * @link https://github.com/firebase/php-jwt 27 | */ 28 | class JWT 29 | { 30 | private const ASN1_INTEGER = 0x02; 31 | private const ASN1_SEQUENCE = 0x10; 32 | private const ASN1_BIT_STRING = 0x03; 33 | 34 | /** 35 | * When checking nbf, iat or expiration times, 36 | * we want to provide some extra leeway time to 37 | * account for clock skew. 38 | * 39 | * @var int 40 | */ 41 | public static $leeway = 0; 42 | 43 | /** 44 | * Allow the current timestamp to be specified. 45 | * Useful for fixing a value within unit testing. 46 | * Will default to PHP time() value if null. 47 | * 48 | * @var ?int 49 | */ 50 | public static $timestamp = null; 51 | 52 | /** 53 | * @var array 54 | */ 55 | public static $supported_algs = [ 56 | 'ES384' => ['openssl', 'SHA384'], 57 | 'ES256' => ['openssl', 'SHA256'], 58 | 'ES256K' => ['openssl', 'SHA256'], 59 | 'HS256' => ['hash_hmac', 'SHA256'], 60 | 'HS384' => ['hash_hmac', 'SHA384'], 61 | 'HS512' => ['hash_hmac', 'SHA512'], 62 | 'RS256' => ['openssl', 'SHA256'], 63 | 'RS384' => ['openssl', 'SHA384'], 64 | 'RS512' => ['openssl', 'SHA512'], 65 | 'EdDSA' => ['sodium_crypto', 'EdDSA'], 66 | ]; 67 | 68 | /** 69 | * Decodes a JWT string into a PHP object. 70 | * 71 | * @param string $jwt The JWT 72 | * @param Key|array $keyOrKeyArray The Key or associative array of key IDs (kid) to Key objects. 73 | * If the algorithm used is asymmetric, this is the public key 74 | * Each Key object contains an algorithm and matching key. 75 | * Supported algorithms are 'ES384','ES256', 'HS256', 'HS384', 76 | * 'HS512', 'RS256', 'RS384', and 'RS512' 77 | * 78 | * @return stdClass The JWT's payload as a PHP object 79 | * 80 | * @throws InvalidArgumentException Provided key/key-array was empty or malformed 81 | * @throws DomainException Provided JWT is malformed 82 | * @throws UnexpectedValueException Provided JWT was invalid 83 | * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed 84 | * @throws BeforeValidException Provided JWT is trying to be used before it's eligible as defined by 'nbf' 85 | * @throws BeforeValidException Provided JWT is trying to be used before it's been created as defined by 'iat' 86 | * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim 87 | * 88 | * @uses jsonDecode 89 | * @uses urlsafeB64Decode 90 | */ 91 | public static function decode( 92 | string $jwt, 93 | $keyOrKeyArray 94 | ): stdClass { 95 | // Validate JWT 96 | $timestamp = \is_null(static::$timestamp) ? \time() : static::$timestamp; 97 | 98 | if (empty($keyOrKeyArray)) { 99 | throw new InvalidArgumentException('Key may not be empty'); 100 | } 101 | $tks = \explode('.', $jwt); 102 | if (\count($tks) !== 3) { 103 | throw new UnexpectedValueException('Wrong number of segments'); 104 | } 105 | list($headb64, $bodyb64, $cryptob64) = $tks; 106 | $headerRaw = static::urlsafeB64Decode($headb64); 107 | if (null === ($header = static::jsonDecode($headerRaw))) { 108 | throw new UnexpectedValueException('Invalid header encoding'); 109 | } 110 | $payloadRaw = static::urlsafeB64Decode($bodyb64); 111 | if (null === ($payload = static::jsonDecode($payloadRaw))) { 112 | throw new UnexpectedValueException('Invalid claims encoding'); 113 | } 114 | if (\is_array($payload)) { 115 | // prevent PHP Fatal Error in edge-cases when payload is empty array 116 | $payload = (object) $payload; 117 | } 118 | if (!$payload instanceof stdClass) { 119 | throw new UnexpectedValueException('Payload must be a JSON object'); 120 | } 121 | $sig = static::urlsafeB64Decode($cryptob64); 122 | if (empty($header->alg)) { 123 | throw new UnexpectedValueException('Empty algorithm'); 124 | } 125 | if (empty(static::$supported_algs[$header->alg])) { 126 | throw new UnexpectedValueException('Algorithm not supported'); 127 | } 128 | 129 | $key = self::getKey($keyOrKeyArray, property_exists($header, 'kid') ? $header->kid : null); 130 | 131 | // Check the algorithm 132 | if (!self::constantTimeEquals($key->getAlgorithm(), $header->alg)) { 133 | // See issue #351 134 | throw new UnexpectedValueException('Incorrect key for this algorithm'); 135 | } 136 | if (\in_array($header->alg, ['ES256', 'ES256K', 'ES384'], true)) { 137 | // OpenSSL expects an ASN.1 DER sequence for ES256/ES256K/ES384 signatures 138 | $sig = self::signatureToDER($sig); 139 | } 140 | if (!self::verify("{$headb64}.{$bodyb64}", $sig, $key->getKeyMaterial(), $header->alg)) { 141 | throw new SignatureInvalidException('Signature verification failed'); 142 | } 143 | 144 | // Check the nbf if it is defined. This is the time that the 145 | // token can actually be used. If it's not yet that time, abort. 146 | if (isset($payload->nbf) && $payload->nbf > ($timestamp + static::$leeway)) { 147 | throw new BeforeValidException( 148 | 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->nbf) 149 | ); 150 | } 151 | 152 | // Check that this token has been created before 'now'. This prevents 153 | // using tokens that have been created for later use (and haven't 154 | // correctly used the nbf claim). 155 | if (isset($payload->iat) && $payload->iat > ($timestamp + static::$leeway)) { 156 | throw new BeforeValidException( 157 | 'Cannot handle token prior to ' . \date(DateTime::ISO8601, $payload->iat) 158 | ); 159 | } 160 | 161 | // Check if this token has expired. 162 | if (isset($payload->exp) && ($timestamp - static::$leeway) >= $payload->exp) { 163 | throw new ExpiredException('Expired token'); 164 | } 165 | 166 | return $payload; 167 | } 168 | 169 | /** 170 | * Converts and signs a PHP array into a JWT string. 171 | * 172 | * @param array $payload PHP array 173 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. 174 | * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', 175 | * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' 176 | * @param string $keyId 177 | * @param array $head An array with header elements to attach 178 | * 179 | * @return string A signed JWT 180 | * 181 | * @uses jsonEncode 182 | * @uses urlsafeB64Encode 183 | */ 184 | public static function encode( 185 | array $payload, 186 | $key, 187 | string $alg, 188 | string $keyId = null, 189 | array $head = null 190 | ): string { 191 | $header = ['typ' => 'JWT', 'alg' => $alg]; 192 | if ($keyId !== null) { 193 | $header['kid'] = $keyId; 194 | } 195 | if (isset($head) && \is_array($head)) { 196 | $header = \array_merge($head, $header); 197 | } 198 | $segments = []; 199 | $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header)); 200 | $segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload)); 201 | $signing_input = \implode('.', $segments); 202 | 203 | $signature = static::sign($signing_input, $key, $alg); 204 | $segments[] = static::urlsafeB64Encode($signature); 205 | 206 | return \implode('.', $segments); 207 | } 208 | 209 | /** 210 | * Sign a string with a given key and algorithm. 211 | * 212 | * @param string $msg The message to sign 213 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $key The secret key. 214 | * @param string $alg Supported algorithms are 'ES384','ES256', 'ES256K', 'HS256', 215 | * 'HS384', 'HS512', 'RS256', 'RS384', and 'RS512' 216 | * 217 | * @return string An encrypted message 218 | * 219 | * @throws DomainException Unsupported algorithm or bad key was specified 220 | */ 221 | public static function sign( 222 | string $msg, 223 | $key, 224 | string $alg 225 | ): string { 226 | if (empty(static::$supported_algs[$alg])) { 227 | throw new DomainException('Algorithm not supported'); 228 | } 229 | list($function, $algorithm) = static::$supported_algs[$alg]; 230 | switch ($function) { 231 | case 'hash_hmac': 232 | if (!\is_string($key)) { 233 | throw new InvalidArgumentException('key must be a string when using hmac'); 234 | } 235 | return \hash_hmac($algorithm, $msg, $key, true); 236 | case 'openssl': 237 | $signature = ''; 238 | $success = \openssl_sign($msg, $signature, $key, $algorithm); // @phpstan-ignore-line 239 | if (!$success) { 240 | throw new DomainException('OpenSSL unable to sign data'); 241 | } 242 | if ($alg === 'ES256' || $alg === 'ES256K') { 243 | $signature = self::signatureFromDER($signature, 256); 244 | } elseif ($alg === 'ES384') { 245 | $signature = self::signatureFromDER($signature, 384); 246 | } 247 | return $signature; 248 | case 'sodium_crypto': 249 | if (!\function_exists('sodium_crypto_sign_detached')) { 250 | throw new DomainException('libsodium is not available'); 251 | } 252 | if (!\is_string($key)) { 253 | throw new InvalidArgumentException('key must be a string when using EdDSA'); 254 | } 255 | try { 256 | // The last non-empty line is used as the key. 257 | $lines = array_filter(explode("\n", $key)); 258 | $key = base64_decode((string) end($lines)); 259 | if (\strlen($key) === 0) { 260 | throw new DomainException('Key cannot be empty string'); 261 | } 262 | return sodium_crypto_sign_detached($msg, $key); 263 | } catch (Exception $e) { 264 | throw new DomainException($e->getMessage(), 0, $e); 265 | } 266 | } 267 | 268 | throw new DomainException('Algorithm not supported'); 269 | } 270 | 271 | /** 272 | * Verify a signature with the message, key and method. Not all methods 273 | * are symmetric, so we must have a separate verify and sign method. 274 | * 275 | * @param string $msg The original message (header and body) 276 | * @param string $signature The original signature 277 | * @param string|resource|OpenSSLAsymmetricKey|OpenSSLCertificate $keyMaterial For HS*, a string key works. for RS*, must be an instance of OpenSSLAsymmetricKey 278 | * @param string $alg The algorithm 279 | * 280 | * @return bool 281 | * 282 | * @throws DomainException Invalid Algorithm, bad key, or OpenSSL failure 283 | */ 284 | private static function verify( 285 | string $msg, 286 | string $signature, 287 | $keyMaterial, 288 | string $alg 289 | ): bool { 290 | if (empty(static::$supported_algs[$alg])) { 291 | throw new DomainException('Algorithm not supported'); 292 | } 293 | 294 | list($function, $algorithm) = static::$supported_algs[$alg]; 295 | switch ($function) { 296 | case 'openssl': 297 | $success = \openssl_verify($msg, $signature, $keyMaterial, $algorithm); // @phpstan-ignore-line 298 | if ($success === 1) { 299 | return true; 300 | } 301 | if ($success === 0) { 302 | return false; 303 | } 304 | // returns 1 on success, 0 on failure, -1 on error. 305 | throw new DomainException( 306 | 'OpenSSL error: ' . \openssl_error_string() 307 | ); 308 | case 'sodium_crypto': 309 | if (!\function_exists('sodium_crypto_sign_verify_detached')) { 310 | throw new DomainException('libsodium is not available'); 311 | } 312 | if (!\is_string($keyMaterial)) { 313 | throw new InvalidArgumentException('key must be a string when using EdDSA'); 314 | } 315 | try { 316 | // The last non-empty line is used as the key. 317 | $lines = array_filter(explode("\n", $keyMaterial)); 318 | $key = base64_decode((string) end($lines)); 319 | if (\strlen($key) === 0) { 320 | throw new DomainException('Key cannot be empty string'); 321 | } 322 | if (\strlen($signature) === 0) { 323 | throw new DomainException('Signature cannot be empty string'); 324 | } 325 | return sodium_crypto_sign_verify_detached($signature, $msg, $key); 326 | } catch (Exception $e) { 327 | throw new DomainException($e->getMessage(), 0, $e); 328 | } 329 | case 'hash_hmac': 330 | default: 331 | if (!\is_string($keyMaterial)) { 332 | throw new InvalidArgumentException('key must be a string when using hmac'); 333 | } 334 | $hash = \hash_hmac($algorithm, $msg, $keyMaterial, true); 335 | return self::constantTimeEquals($hash, $signature); 336 | } 337 | } 338 | 339 | /** 340 | * Decode a JSON string into a PHP object. 341 | * 342 | * @param string $input JSON string 343 | * 344 | * @return mixed The decoded JSON string 345 | * 346 | * @throws DomainException Provided string was invalid JSON 347 | */ 348 | public static function jsonDecode(string $input) 349 | { 350 | $obj = \json_decode($input, false, 512, JSON_BIGINT_AS_STRING); 351 | 352 | if ($errno = \json_last_error()) { 353 | self::handleJsonError($errno); 354 | } elseif ($obj === null && $input !== 'null') { 355 | throw new DomainException('Null result with non-null input'); 356 | } 357 | return $obj; 358 | } 359 | 360 | /** 361 | * Encode a PHP array into a JSON string. 362 | * 363 | * @param array $input A PHP array 364 | * 365 | * @return string JSON representation of the PHP array 366 | * 367 | * @throws DomainException Provided object could not be encoded to valid JSON 368 | */ 369 | public static function jsonEncode(array $input): string 370 | { 371 | if (PHP_VERSION_ID >= 50400) { 372 | $json = \json_encode($input, \JSON_UNESCAPED_SLASHES); 373 | } else { 374 | // PHP 5.3 only 375 | $json = \json_encode($input); 376 | } 377 | if ($errno = \json_last_error()) { 378 | self::handleJsonError($errno); 379 | } elseif ($json === 'null' && $input !== null) { 380 | throw new DomainException('Null result with non-null input'); 381 | } 382 | if ($json === false) { 383 | throw new DomainException('Provided object could not be encoded to valid JSON'); 384 | } 385 | return $json; 386 | } 387 | 388 | /** 389 | * Decode a string with URL-safe Base64. 390 | * 391 | * @param string $input A Base64 encoded string 392 | * 393 | * @return string A decoded string 394 | * 395 | * @throws InvalidArgumentException invalid base64 characters 396 | */ 397 | public static function urlsafeB64Decode(string $input): string 398 | { 399 | $remainder = \strlen($input) % 4; 400 | if ($remainder) { 401 | $padlen = 4 - $remainder; 402 | $input .= \str_repeat('=', $padlen); 403 | } 404 | return \base64_decode(\strtr($input, '-_', '+/')); 405 | } 406 | 407 | /** 408 | * Encode a string with URL-safe Base64. 409 | * 410 | * @param string $input The string you want encoded 411 | * 412 | * @return string The base64 encode of what you passed in 413 | */ 414 | public static function urlsafeB64Encode(string $input): string 415 | { 416 | return \str_replace('=', '', \strtr(\base64_encode($input), '+/', '-_')); 417 | } 418 | 419 | 420 | /** 421 | * Determine if an algorithm has been provided for each Key 422 | * 423 | * @param Key|ArrayAccess|array $keyOrKeyArray 424 | * @param string|null $kid 425 | * 426 | * @throws UnexpectedValueException 427 | * 428 | * @return Key 429 | */ 430 | private static function getKey( 431 | $keyOrKeyArray, 432 | ?string $kid 433 | ): Key { 434 | if ($keyOrKeyArray instanceof Key) { 435 | return $keyOrKeyArray; 436 | } 437 | 438 | if (empty($kid)) { 439 | throw new UnexpectedValueException('"kid" empty, unable to lookup correct key'); 440 | } 441 | 442 | if ($keyOrKeyArray instanceof CachedKeySet) { 443 | // Skip "isset" check, as this will automatically refresh if not set 444 | return $keyOrKeyArray[$kid]; 445 | } 446 | 447 | if (!isset($keyOrKeyArray[$kid])) { 448 | throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key'); 449 | } 450 | 451 | return $keyOrKeyArray[$kid]; 452 | } 453 | 454 | /** 455 | * @param string $left The string of known length to compare against 456 | * @param string $right The user-supplied string 457 | * @return bool 458 | */ 459 | public static function constantTimeEquals(string $left, string $right): bool 460 | { 461 | if (\function_exists('hash_equals')) { 462 | return \hash_equals($left, $right); 463 | } 464 | $len = \min(self::safeStrlen($left), self::safeStrlen($right)); 465 | 466 | $status = 0; 467 | for ($i = 0; $i < $len; $i++) { 468 | $status |= (\ord($left[$i]) ^ \ord($right[$i])); 469 | } 470 | $status |= (self::safeStrlen($left) ^ self::safeStrlen($right)); 471 | 472 | return ($status === 0); 473 | } 474 | 475 | /** 476 | * Helper method to create a JSON error. 477 | * 478 | * @param int $errno An error number from json_last_error() 479 | * 480 | * @throws DomainException 481 | * 482 | * @return void 483 | */ 484 | private static function handleJsonError(int $errno): void 485 | { 486 | $messages = [ 487 | JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', 488 | JSON_ERROR_STATE_MISMATCH => 'Invalid or malformed JSON', 489 | JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', 490 | JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', 491 | JSON_ERROR_UTF8 => 'Malformed UTF-8 characters' //PHP >= 5.3.3 492 | ]; 493 | throw new DomainException( 494 | isset($messages[$errno]) 495 | ? $messages[$errno] 496 | : 'Unknown JSON error: ' . $errno 497 | ); 498 | } 499 | 500 | /** 501 | * Get the number of bytes in cryptographic strings. 502 | * 503 | * @param string $str 504 | * 505 | * @return int 506 | */ 507 | private static function safeStrlen(string $str): int 508 | { 509 | if (\function_exists('mb_strlen')) { 510 | return \mb_strlen($str, '8bit'); 511 | } 512 | return \strlen($str); 513 | } 514 | 515 | /** 516 | * Convert an ECDSA signature to an ASN.1 DER sequence 517 | * 518 | * @param string $sig The ECDSA signature to convert 519 | * @return string The encoded DER object 520 | */ 521 | private static function signatureToDER(string $sig): string 522 | { 523 | // Separate the signature into r-value and s-value 524 | $length = max(1, (int) (\strlen($sig) / 2)); 525 | list($r, $s) = \str_split($sig, $length); 526 | 527 | // Trim leading zeros 528 | $r = \ltrim($r, "\x00"); 529 | $s = \ltrim($s, "\x00"); 530 | 531 | // Convert r-value and s-value from unsigned big-endian integers to 532 | // signed two's complement 533 | if (\ord($r[0]) > 0x7f) { 534 | $r = "\x00" . $r; 535 | } 536 | if (\ord($s[0]) > 0x7f) { 537 | $s = "\x00" . $s; 538 | } 539 | 540 | return self::encodeDER( 541 | self::ASN1_SEQUENCE, 542 | self::encodeDER(self::ASN1_INTEGER, $r) . 543 | self::encodeDER(self::ASN1_INTEGER, $s) 544 | ); 545 | } 546 | 547 | /** 548 | * Encodes a value into a DER object. 549 | * 550 | * @param int $type DER tag 551 | * @param string $value the value to encode 552 | * 553 | * @return string the encoded object 554 | */ 555 | private static function encodeDER(int $type, string $value): string 556 | { 557 | $tag_header = 0; 558 | if ($type === self::ASN1_SEQUENCE) { 559 | $tag_header |= 0x20; 560 | } 561 | 562 | // Type 563 | $der = \chr($tag_header | $type); 564 | 565 | // Length 566 | $der .= \chr(\strlen($value)); 567 | 568 | return $der . $value; 569 | } 570 | 571 | /** 572 | * Encodes signature from a DER object. 573 | * 574 | * @param string $der binary signature in DER format 575 | * @param int $keySize the number of bits in the key 576 | * 577 | * @return string the signature 578 | */ 579 | private static function signatureFromDER(string $der, int $keySize): string 580 | { 581 | // OpenSSL returns the ECDSA signatures as a binary ASN.1 DER SEQUENCE 582 | list($offset, $_) = self::readDER($der); 583 | list($offset, $r) = self::readDER($der, $offset); 584 | list($offset, $s) = self::readDER($der, $offset); 585 | 586 | // Convert r-value and s-value from signed two's compliment to unsigned 587 | // big-endian integers 588 | $r = \ltrim($r, "\x00"); 589 | $s = \ltrim($s, "\x00"); 590 | 591 | // Pad out r and s so that they are $keySize bits long 592 | $r = \str_pad($r, $keySize / 8, "\x00", STR_PAD_LEFT); 593 | $s = \str_pad($s, $keySize / 8, "\x00", STR_PAD_LEFT); 594 | 595 | return $r . $s; 596 | } 597 | 598 | /** 599 | * Reads binary DER-encoded data and decodes into a single object 600 | * 601 | * @param string $der the binary data in DER format 602 | * @param int $offset the offset of the data stream containing the object 603 | * to decode 604 | * 605 | * @return array{int, string|null} the new offset and the decoded object 606 | */ 607 | private static function readDER(string $der, int $offset = 0): array 608 | { 609 | $pos = $offset; 610 | $size = \strlen($der); 611 | $constructed = (\ord($der[$pos]) >> 5) & 0x01; 612 | $type = \ord($der[$pos++]) & 0x1f; 613 | 614 | // Length 615 | $len = \ord($der[$pos++]); 616 | if ($len & 0x80) { 617 | $n = $len & 0x1f; 618 | $len = 0; 619 | while ($n-- && $pos < $size) { 620 | $len = ($len << 8) | \ord($der[$pos++]); 621 | } 622 | } 623 | 624 | // Value 625 | if ($type === self::ASN1_BIT_STRING) { 626 | $pos++; // Skip the first contents octet (padding indicator) 627 | $data = \substr($der, $pos, $len - 1); 628 | $pos += $len - 1; 629 | } elseif (!$constructed) { 630 | $data = \substr($der, $pos, $len); 631 | $pos += $len; 632 | } else { 633 | $data = null; 634 | } 635 | 636 | return [$pos, $data]; 637 | } 638 | } 639 | --------------------------------------------------------------------------------