├── LICENSE ├── composer.json ├── composer.lock ├── includes ├── Block_Invocation.php ├── Block_Recognizer.php ├── Calling_Reflection.php ├── Database.php ├── Dependencies.php ├── Exception.php ├── File_Locator.php ├── Hook_Invocation.php ├── Hook_Wrapper.php ├── Incrementor.php ├── Invocation.php ├── Invocation_Watcher.php ├── Output_Annotator.php ├── Plugin.php ├── Server_Timing_Headers.php ├── Shortcode_Invocation.php ├── Widget_Invocation.php └── Wrapped_Callback.php ├── js └── identify-node-sources.js └── origination.php /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google/wp-origination", 3 | "type": "wordpress-plugin", 4 | "description": "WordPress plugin to determine the origin of where things come from in WordPress whether slow code, inefficient queries, or bad markup.", 5 | "keywords": [ 6 | "wordpress", 7 | "performance" 8 | ], 9 | "homepage": "https://github.com/GoogleChromeLabs/wp-origination", 10 | "license": "Apache-2.0", 11 | "authors": [ 12 | { 13 | "name": "Weston Ruter", 14 | "email": "westonruter@google.com", 15 | "homepage": "https://weston.ruter.net/" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.0" 20 | }, 21 | "require-dev": { 22 | "dealerdirect/phpcodesniffer-composer-installer": "0.7.0", 23 | "phpcompatibility/php-compatibility": "9.3.1", 24 | "phpunit/phpunit": "^6", 25 | "wp-coding-standards/wpcs": "2.1.1", 26 | "xwp/wp-dev-lib": "1.3.0" 27 | }, 28 | "config": { 29 | "platform": { 30 | "php": "7.0" 31 | }, 32 | "allow-plugins": { 33 | "dealerdirect/phpcodesniffer-composer-installer": true 34 | } 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Google\\WP_Origination\\": "includes" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Google\\WP_Origination\\Tests\\PHPUnit\\Framework\\": "tests/phpunit/framework" 44 | } 45 | }, 46 | "prefer-stable": true, 47 | "scripts": { 48 | "lint": [ 49 | "@phplint", 50 | "@phpcs" 51 | ], 52 | "dist": "php bin/create-wordpress-plugin-zip.php", 53 | "phpcbf": "phpcbf", 54 | "phpcs": "phpcs", 55 | "phplint": "git ls-files -z -- '*.php' | xargs -0 -n 1 -P 4 php -l", 56 | "test": "phpunit --config=phpunit.xml.dist" 57 | }, 58 | "support": { 59 | "issues": "https://github.com/GoogleChromeLabs/wp-origination/issues" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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": "b6dabbb715ac5dd50716868522bb7cec", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "dealerdirect/phpcodesniffer-composer-installer", 12 | "version": "v0.7.0", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", 16 | "reference": "e8d808670b8f882188368faaf1144448c169c0b7" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e8d808670b8f882188368faaf1144448c169c0b7", 21 | "reference": "e8d808670b8f882188368faaf1144448c169c0b7", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "composer-plugin-api": "^1.0 || ^2.0", 26 | "php": ">=5.3", 27 | "squizlabs/php_codesniffer": "^2 || ^3 || 4.0.x-dev" 28 | }, 29 | "require-dev": { 30 | "composer/composer": "*", 31 | "phpcompatibility/php-compatibility": "^9.0", 32 | "sensiolabs/security-checker": "^4.1.0" 33 | }, 34 | "type": "composer-plugin", 35 | "extra": { 36 | "class": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "authors": [ 48 | { 49 | "name": "Franck Nijhof", 50 | "email": "franck.nijhof@dealerdirect.com", 51 | "homepage": "http://www.frenck.nl", 52 | "role": "Developer / IT Manager" 53 | } 54 | ], 55 | "description": "PHP_CodeSniffer Standards Composer Installer Plugin", 56 | "homepage": "http://www.dealerdirect.com", 57 | "keywords": [ 58 | "PHPCodeSniffer", 59 | "PHP_CodeSniffer", 60 | "code quality", 61 | "codesniffer", 62 | "composer", 63 | "installer", 64 | "phpcs", 65 | "plugin", 66 | "qa", 67 | "quality", 68 | "standard", 69 | "standards", 70 | "style guide", 71 | "stylecheck", 72 | "tests" 73 | ], 74 | "support": { 75 | "issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues", 76 | "source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer" 77 | }, 78 | "time": "2020-06-25T14:57:39+00:00" 79 | }, 80 | { 81 | "name": "doctrine/instantiator", 82 | "version": "1.0.5", 83 | "source": { 84 | "type": "git", 85 | "url": "https://github.com/doctrine/instantiator.git", 86 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 87 | }, 88 | "dist": { 89 | "type": "zip", 90 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 91 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 92 | "shasum": "" 93 | }, 94 | "require": { 95 | "php": ">=5.3,<8.0-DEV" 96 | }, 97 | "require-dev": { 98 | "athletic/athletic": "~0.1.8", 99 | "ext-pdo": "*", 100 | "ext-phar": "*", 101 | "phpunit/phpunit": "~4.0", 102 | "squizlabs/php_codesniffer": "~2.0" 103 | }, 104 | "type": "library", 105 | "extra": { 106 | "branch-alias": { 107 | "dev-master": "1.0.x-dev" 108 | } 109 | }, 110 | "autoload": { 111 | "psr-4": { 112 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 113 | } 114 | }, 115 | "notification-url": "https://packagist.org/downloads/", 116 | "license": [ 117 | "MIT" 118 | ], 119 | "authors": [ 120 | { 121 | "name": "Marco Pivetta", 122 | "email": "ocramius@gmail.com", 123 | "homepage": "http://ocramius.github.com/" 124 | } 125 | ], 126 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 127 | "homepage": "https://github.com/doctrine/instantiator", 128 | "keywords": [ 129 | "constructor", 130 | "instantiate" 131 | ], 132 | "support": { 133 | "issues": "https://github.com/doctrine/instantiator/issues", 134 | "source": "https://github.com/doctrine/instantiator/tree/master" 135 | }, 136 | "time": "2015-06-14T21:17:01+00:00" 137 | }, 138 | { 139 | "name": "myclabs/deep-copy", 140 | "version": "1.7.0", 141 | "source": { 142 | "type": "git", 143 | "url": "https://github.com/myclabs/DeepCopy.git", 144 | "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" 145 | }, 146 | "dist": { 147 | "type": "zip", 148 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", 149 | "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", 150 | "shasum": "" 151 | }, 152 | "require": { 153 | "php": "^5.6 || ^7.0" 154 | }, 155 | "require-dev": { 156 | "doctrine/collections": "^1.0", 157 | "doctrine/common": "^2.6", 158 | "phpunit/phpunit": "^4.1" 159 | }, 160 | "type": "library", 161 | "autoload": { 162 | "files": [ 163 | "src/DeepCopy/deep_copy.php" 164 | ], 165 | "psr-4": { 166 | "DeepCopy\\": "src/DeepCopy/" 167 | } 168 | }, 169 | "notification-url": "https://packagist.org/downloads/", 170 | "license": [ 171 | "MIT" 172 | ], 173 | "description": "Create deep copies (clones) of your objects", 174 | "keywords": [ 175 | "clone", 176 | "copy", 177 | "duplicate", 178 | "object", 179 | "object graph" 180 | ], 181 | "support": { 182 | "issues": "https://github.com/myclabs/DeepCopy/issues", 183 | "source": "https://github.com/myclabs/DeepCopy/tree/1.x" 184 | }, 185 | "time": "2017-10-19T19:58:43+00:00" 186 | }, 187 | { 188 | "name": "phar-io/manifest", 189 | "version": "1.0.1", 190 | "source": { 191 | "type": "git", 192 | "url": "https://github.com/phar-io/manifest.git", 193 | "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" 194 | }, 195 | "dist": { 196 | "type": "zip", 197 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", 198 | "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", 199 | "shasum": "" 200 | }, 201 | "require": { 202 | "ext-dom": "*", 203 | "ext-phar": "*", 204 | "phar-io/version": "^1.0.1", 205 | "php": "^5.6 || ^7.0" 206 | }, 207 | "type": "library", 208 | "extra": { 209 | "branch-alias": { 210 | "dev-master": "1.0.x-dev" 211 | } 212 | }, 213 | "autoload": { 214 | "classmap": [ 215 | "src/" 216 | ] 217 | }, 218 | "notification-url": "https://packagist.org/downloads/", 219 | "license": [ 220 | "BSD-3-Clause" 221 | ], 222 | "authors": [ 223 | { 224 | "name": "Arne Blankerts", 225 | "email": "arne@blankerts.de", 226 | "role": "Developer" 227 | }, 228 | { 229 | "name": "Sebastian Heuer", 230 | "email": "sebastian@phpeople.de", 231 | "role": "Developer" 232 | }, 233 | { 234 | "name": "Sebastian Bergmann", 235 | "email": "sebastian@phpunit.de", 236 | "role": "Developer" 237 | } 238 | ], 239 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 240 | "support": { 241 | "issues": "https://github.com/phar-io/manifest/issues", 242 | "source": "https://github.com/phar-io/manifest/tree/master" 243 | }, 244 | "time": "2017-03-05T18:14:27+00:00" 245 | }, 246 | { 247 | "name": "phar-io/version", 248 | "version": "1.0.1", 249 | "source": { 250 | "type": "git", 251 | "url": "https://github.com/phar-io/version.git", 252 | "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" 253 | }, 254 | "dist": { 255 | "type": "zip", 256 | "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", 257 | "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", 258 | "shasum": "" 259 | }, 260 | "require": { 261 | "php": "^5.6 || ^7.0" 262 | }, 263 | "type": "library", 264 | "autoload": { 265 | "classmap": [ 266 | "src/" 267 | ] 268 | }, 269 | "notification-url": "https://packagist.org/downloads/", 270 | "license": [ 271 | "BSD-3-Clause" 272 | ], 273 | "authors": [ 274 | { 275 | "name": "Arne Blankerts", 276 | "email": "arne@blankerts.de", 277 | "role": "Developer" 278 | }, 279 | { 280 | "name": "Sebastian Heuer", 281 | "email": "sebastian@phpeople.de", 282 | "role": "Developer" 283 | }, 284 | { 285 | "name": "Sebastian Bergmann", 286 | "email": "sebastian@phpunit.de", 287 | "role": "Developer" 288 | } 289 | ], 290 | "description": "Library for handling version information and constraints", 291 | "support": { 292 | "issues": "https://github.com/phar-io/version/issues", 293 | "source": "https://github.com/phar-io/version/tree/master" 294 | }, 295 | "time": "2017-03-05T17:38:23+00:00" 296 | }, 297 | { 298 | "name": "phpcompatibility/php-compatibility", 299 | "version": "9.3.1", 300 | "source": { 301 | "type": "git", 302 | "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", 303 | "reference": "9999344e47e7af6b00e1a898eacc4e4368fb7196" 304 | }, 305 | "dist": { 306 | "type": "zip", 307 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9999344e47e7af6b00e1a898eacc4e4368fb7196", 308 | "reference": "9999344e47e7af6b00e1a898eacc4e4368fb7196", 309 | "shasum": "" 310 | }, 311 | "require": { 312 | "php": ">=5.3", 313 | "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" 314 | }, 315 | "conflict": { 316 | "squizlabs/php_codesniffer": "2.6.2" 317 | }, 318 | "require-dev": { 319 | "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" 320 | }, 321 | "suggest": { 322 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", 323 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 324 | }, 325 | "type": "phpcodesniffer-standard", 326 | "notification-url": "https://packagist.org/downloads/", 327 | "license": [ 328 | "LGPL-3.0-or-later" 329 | ], 330 | "authors": [ 331 | { 332 | "name": "Wim Godden", 333 | "homepage": "https://github.com/wimg", 334 | "role": "lead" 335 | }, 336 | { 337 | "name": "Juliette Reinders Folmer", 338 | "homepage": "https://github.com/jrfnl", 339 | "role": "lead" 340 | }, 341 | { 342 | "name": "Contributors", 343 | "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" 344 | } 345 | ], 346 | "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", 347 | "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", 348 | "keywords": [ 349 | "compatibility", 350 | "phpcs", 351 | "standards" 352 | ], 353 | "support": { 354 | "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", 355 | "source": "https://github.com/PHPCompatibility/PHPCompatibility" 356 | }, 357 | "time": "2019-09-05T18:36:49+00:00" 358 | }, 359 | { 360 | "name": "phpdocumentor/reflection-common", 361 | "version": "1.0.1", 362 | "source": { 363 | "type": "git", 364 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 365 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" 366 | }, 367 | "dist": { 368 | "type": "zip", 369 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 370 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 371 | "shasum": "" 372 | }, 373 | "require": { 374 | "php": ">=5.5" 375 | }, 376 | "require-dev": { 377 | "phpunit/phpunit": "^4.6" 378 | }, 379 | "type": "library", 380 | "extra": { 381 | "branch-alias": { 382 | "dev-master": "1.0.x-dev" 383 | } 384 | }, 385 | "autoload": { 386 | "psr-4": { 387 | "phpDocumentor\\Reflection\\": [ 388 | "src" 389 | ] 390 | } 391 | }, 392 | "notification-url": "https://packagist.org/downloads/", 393 | "license": [ 394 | "MIT" 395 | ], 396 | "authors": [ 397 | { 398 | "name": "Jaap van Otterdijk", 399 | "email": "opensource@ijaap.nl" 400 | } 401 | ], 402 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 403 | "homepage": "http://www.phpdoc.org", 404 | "keywords": [ 405 | "FQSEN", 406 | "phpDocumentor", 407 | "phpdoc", 408 | "reflection", 409 | "static analysis" 410 | ], 411 | "support": { 412 | "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", 413 | "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master" 414 | }, 415 | "time": "2017-09-11T18:02:19+00:00" 416 | }, 417 | { 418 | "name": "phpdocumentor/reflection-docblock", 419 | "version": "4.3.4", 420 | "source": { 421 | "type": "git", 422 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 423 | "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" 424 | }, 425 | "dist": { 426 | "type": "zip", 427 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", 428 | "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", 429 | "shasum": "" 430 | }, 431 | "require": { 432 | "php": "^7.0", 433 | "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", 434 | "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", 435 | "webmozart/assert": "^1.0" 436 | }, 437 | "require-dev": { 438 | "doctrine/instantiator": "^1.0.5", 439 | "mockery/mockery": "^1.0", 440 | "phpdocumentor/type-resolver": "0.4.*", 441 | "phpunit/phpunit": "^6.4" 442 | }, 443 | "type": "library", 444 | "extra": { 445 | "branch-alias": { 446 | "dev-master": "4.x-dev" 447 | } 448 | }, 449 | "autoload": { 450 | "psr-4": { 451 | "phpDocumentor\\Reflection\\": [ 452 | "src/" 453 | ] 454 | } 455 | }, 456 | "notification-url": "https://packagist.org/downloads/", 457 | "license": [ 458 | "MIT" 459 | ], 460 | "authors": [ 461 | { 462 | "name": "Mike van Riel", 463 | "email": "me@mikevanriel.com" 464 | } 465 | ], 466 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 467 | "support": { 468 | "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", 469 | "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x" 470 | }, 471 | "time": "2019-12-28T18:55:12+00:00" 472 | }, 473 | { 474 | "name": "phpdocumentor/type-resolver", 475 | "version": "0.5.1", 476 | "source": { 477 | "type": "git", 478 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 479 | "reference": "cf842904952e64e703800d094cdf34e715a8a3ae" 480 | }, 481 | "dist": { 482 | "type": "zip", 483 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/cf842904952e64e703800d094cdf34e715a8a3ae", 484 | "reference": "cf842904952e64e703800d094cdf34e715a8a3ae", 485 | "shasum": "" 486 | }, 487 | "require": { 488 | "php": "^7.0", 489 | "phpdocumentor/reflection-common": "^1.0" 490 | }, 491 | "require-dev": { 492 | "mockery/mockery": "^1.0", 493 | "phpunit/phpunit": "^6.4" 494 | }, 495 | "type": "library", 496 | "extra": { 497 | "branch-alias": { 498 | "dev-master": "1.0.x-dev" 499 | } 500 | }, 501 | "autoload": { 502 | "psr-4": { 503 | "phpDocumentor\\Reflection\\": "src" 504 | } 505 | }, 506 | "notification-url": "https://packagist.org/downloads/", 507 | "license": [ 508 | "MIT" 509 | ], 510 | "authors": [ 511 | { 512 | "name": "Mike van Riel", 513 | "email": "me@mikevanriel.com" 514 | } 515 | ], 516 | "support": { 517 | "issues": "https://github.com/phpDocumentor/TypeResolver/issues", 518 | "source": "https://github.com/phpDocumentor/TypeResolver/tree/master" 519 | }, 520 | "time": "2017-12-30T13:23:38+00:00" 521 | }, 522 | { 523 | "name": "phpspec/prophecy", 524 | "version": "v1.10.3", 525 | "source": { 526 | "type": "git", 527 | "url": "https://github.com/phpspec/prophecy.git", 528 | "reference": "451c3cd1418cf640de218914901e51b064abb093" 529 | }, 530 | "dist": { 531 | "type": "zip", 532 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", 533 | "reference": "451c3cd1418cf640de218914901e51b064abb093", 534 | "shasum": "" 535 | }, 536 | "require": { 537 | "doctrine/instantiator": "^1.0.2", 538 | "php": "^5.3|^7.0", 539 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 540 | "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", 541 | "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" 542 | }, 543 | "require-dev": { 544 | "phpspec/phpspec": "^2.5 || ^3.2", 545 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 546 | }, 547 | "type": "library", 548 | "extra": { 549 | "branch-alias": { 550 | "dev-master": "1.10.x-dev" 551 | } 552 | }, 553 | "autoload": { 554 | "psr-4": { 555 | "Prophecy\\": "src/Prophecy" 556 | } 557 | }, 558 | "notification-url": "https://packagist.org/downloads/", 559 | "license": [ 560 | "MIT" 561 | ], 562 | "authors": [ 563 | { 564 | "name": "Konstantin Kudryashov", 565 | "email": "ever.zet@gmail.com", 566 | "homepage": "http://everzet.com" 567 | }, 568 | { 569 | "name": "Marcello Duarte", 570 | "email": "marcello.duarte@gmail.com" 571 | } 572 | ], 573 | "description": "Highly opinionated mocking framework for PHP 5.3+", 574 | "homepage": "https://github.com/phpspec/prophecy", 575 | "keywords": [ 576 | "Double", 577 | "Dummy", 578 | "fake", 579 | "mock", 580 | "spy", 581 | "stub" 582 | ], 583 | "support": { 584 | "issues": "https://github.com/phpspec/prophecy/issues", 585 | "source": "https://github.com/phpspec/prophecy/tree/v1.10.3" 586 | }, 587 | "time": "2020-03-05T15:02:03+00:00" 588 | }, 589 | { 590 | "name": "phpunit/php-code-coverage", 591 | "version": "5.3.2", 592 | "source": { 593 | "type": "git", 594 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 595 | "reference": "c89677919c5dd6d3b3852f230a663118762218ac" 596 | }, 597 | "dist": { 598 | "type": "zip", 599 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c89677919c5dd6d3b3852f230a663118762218ac", 600 | "reference": "c89677919c5dd6d3b3852f230a663118762218ac", 601 | "shasum": "" 602 | }, 603 | "require": { 604 | "ext-dom": "*", 605 | "ext-xmlwriter": "*", 606 | "php": "^7.0", 607 | "phpunit/php-file-iterator": "^1.4.2", 608 | "phpunit/php-text-template": "^1.2.1", 609 | "phpunit/php-token-stream": "^2.0.1", 610 | "sebastian/code-unit-reverse-lookup": "^1.0.1", 611 | "sebastian/environment": "^3.0", 612 | "sebastian/version": "^2.0.1", 613 | "theseer/tokenizer": "^1.1" 614 | }, 615 | "require-dev": { 616 | "phpunit/phpunit": "^6.0" 617 | }, 618 | "suggest": { 619 | "ext-xdebug": "^2.5.5" 620 | }, 621 | "type": "library", 622 | "extra": { 623 | "branch-alias": { 624 | "dev-master": "5.3.x-dev" 625 | } 626 | }, 627 | "autoload": { 628 | "classmap": [ 629 | "src/" 630 | ] 631 | }, 632 | "notification-url": "https://packagist.org/downloads/", 633 | "license": [ 634 | "BSD-3-Clause" 635 | ], 636 | "authors": [ 637 | { 638 | "name": "Sebastian Bergmann", 639 | "email": "sebastian@phpunit.de", 640 | "role": "lead" 641 | } 642 | ], 643 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 644 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 645 | "keywords": [ 646 | "coverage", 647 | "testing", 648 | "xunit" 649 | ], 650 | "support": { 651 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 652 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/5.3" 653 | }, 654 | "time": "2018-04-06T15:36:58+00:00" 655 | }, 656 | { 657 | "name": "phpunit/php-file-iterator", 658 | "version": "1.4.5", 659 | "source": { 660 | "type": "git", 661 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 662 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" 663 | }, 664 | "dist": { 665 | "type": "zip", 666 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", 667 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", 668 | "shasum": "" 669 | }, 670 | "require": { 671 | "php": ">=5.3.3" 672 | }, 673 | "type": "library", 674 | "extra": { 675 | "branch-alias": { 676 | "dev-master": "1.4.x-dev" 677 | } 678 | }, 679 | "autoload": { 680 | "classmap": [ 681 | "src/" 682 | ] 683 | }, 684 | "notification-url": "https://packagist.org/downloads/", 685 | "license": [ 686 | "BSD-3-Clause" 687 | ], 688 | "authors": [ 689 | { 690 | "name": "Sebastian Bergmann", 691 | "email": "sb@sebastian-bergmann.de", 692 | "role": "lead" 693 | } 694 | ], 695 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 696 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 697 | "keywords": [ 698 | "filesystem", 699 | "iterator" 700 | ], 701 | "support": { 702 | "irc": "irc://irc.freenode.net/phpunit", 703 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 704 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5" 705 | }, 706 | "time": "2017-11-27T13:52:08+00:00" 707 | }, 708 | { 709 | "name": "phpunit/php-text-template", 710 | "version": "1.2.1", 711 | "source": { 712 | "type": "git", 713 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 714 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 715 | }, 716 | "dist": { 717 | "type": "zip", 718 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 719 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 720 | "shasum": "" 721 | }, 722 | "require": { 723 | "php": ">=5.3.3" 724 | }, 725 | "type": "library", 726 | "autoload": { 727 | "classmap": [ 728 | "src/" 729 | ] 730 | }, 731 | "notification-url": "https://packagist.org/downloads/", 732 | "license": [ 733 | "BSD-3-Clause" 734 | ], 735 | "authors": [ 736 | { 737 | "name": "Sebastian Bergmann", 738 | "email": "sebastian@phpunit.de", 739 | "role": "lead" 740 | } 741 | ], 742 | "description": "Simple template engine.", 743 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 744 | "keywords": [ 745 | "template" 746 | ], 747 | "support": { 748 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 749 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" 750 | }, 751 | "time": "2015-06-21T13:50:34+00:00" 752 | }, 753 | { 754 | "name": "phpunit/php-timer", 755 | "version": "1.0.9", 756 | "source": { 757 | "type": "git", 758 | "url": "https://github.com/sebastianbergmann/php-timer.git", 759 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" 760 | }, 761 | "dist": { 762 | "type": "zip", 763 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 764 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 765 | "shasum": "" 766 | }, 767 | "require": { 768 | "php": "^5.3.3 || ^7.0" 769 | }, 770 | "require-dev": { 771 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 772 | }, 773 | "type": "library", 774 | "extra": { 775 | "branch-alias": { 776 | "dev-master": "1.0-dev" 777 | } 778 | }, 779 | "autoload": { 780 | "classmap": [ 781 | "src/" 782 | ] 783 | }, 784 | "notification-url": "https://packagist.org/downloads/", 785 | "license": [ 786 | "BSD-3-Clause" 787 | ], 788 | "authors": [ 789 | { 790 | "name": "Sebastian Bergmann", 791 | "email": "sb@sebastian-bergmann.de", 792 | "role": "lead" 793 | } 794 | ], 795 | "description": "Utility class for timing", 796 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 797 | "keywords": [ 798 | "timer" 799 | ], 800 | "support": { 801 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 802 | "source": "https://github.com/sebastianbergmann/php-timer/tree/master" 803 | }, 804 | "time": "2017-02-26T11:10:40+00:00" 805 | }, 806 | { 807 | "name": "phpunit/php-token-stream", 808 | "version": "2.0.2", 809 | "source": { 810 | "type": "git", 811 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 812 | "reference": "791198a2c6254db10131eecfe8c06670700904db" 813 | }, 814 | "dist": { 815 | "type": "zip", 816 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", 817 | "reference": "791198a2c6254db10131eecfe8c06670700904db", 818 | "shasum": "" 819 | }, 820 | "require": { 821 | "ext-tokenizer": "*", 822 | "php": "^7.0" 823 | }, 824 | "require-dev": { 825 | "phpunit/phpunit": "^6.2.4" 826 | }, 827 | "type": "library", 828 | "extra": { 829 | "branch-alias": { 830 | "dev-master": "2.0-dev" 831 | } 832 | }, 833 | "autoload": { 834 | "classmap": [ 835 | "src/" 836 | ] 837 | }, 838 | "notification-url": "https://packagist.org/downloads/", 839 | "license": [ 840 | "BSD-3-Clause" 841 | ], 842 | "authors": [ 843 | { 844 | "name": "Sebastian Bergmann", 845 | "email": "sebastian@phpunit.de" 846 | } 847 | ], 848 | "description": "Wrapper around PHP's tokenizer extension.", 849 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 850 | "keywords": [ 851 | "tokenizer" 852 | ], 853 | "support": { 854 | "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", 855 | "source": "https://github.com/sebastianbergmann/php-token-stream/tree/master" 856 | }, 857 | "abandoned": true, 858 | "time": "2017-11-27T05:48:46+00:00" 859 | }, 860 | { 861 | "name": "phpunit/phpunit", 862 | "version": "6.5.14", 863 | "source": { 864 | "type": "git", 865 | "url": "https://github.com/sebastianbergmann/phpunit.git", 866 | "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7" 867 | }, 868 | "dist": { 869 | "type": "zip", 870 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bac23fe7ff13dbdb461481f706f0e9fe746334b7", 871 | "reference": "bac23fe7ff13dbdb461481f706f0e9fe746334b7", 872 | "shasum": "" 873 | }, 874 | "require": { 875 | "ext-dom": "*", 876 | "ext-json": "*", 877 | "ext-libxml": "*", 878 | "ext-mbstring": "*", 879 | "ext-xml": "*", 880 | "myclabs/deep-copy": "^1.6.1", 881 | "phar-io/manifest": "^1.0.1", 882 | "phar-io/version": "^1.0", 883 | "php": "^7.0", 884 | "phpspec/prophecy": "^1.7", 885 | "phpunit/php-code-coverage": "^5.3", 886 | "phpunit/php-file-iterator": "^1.4.3", 887 | "phpunit/php-text-template": "^1.2.1", 888 | "phpunit/php-timer": "^1.0.9", 889 | "phpunit/phpunit-mock-objects": "^5.0.9", 890 | "sebastian/comparator": "^2.1", 891 | "sebastian/diff": "^2.0", 892 | "sebastian/environment": "^3.1", 893 | "sebastian/exporter": "^3.1", 894 | "sebastian/global-state": "^2.0", 895 | "sebastian/object-enumerator": "^3.0.3", 896 | "sebastian/resource-operations": "^1.0", 897 | "sebastian/version": "^2.0.1" 898 | }, 899 | "conflict": { 900 | "phpdocumentor/reflection-docblock": "3.0.2", 901 | "phpunit/dbunit": "<3.0" 902 | }, 903 | "require-dev": { 904 | "ext-pdo": "*" 905 | }, 906 | "suggest": { 907 | "ext-xdebug": "*", 908 | "phpunit/php-invoker": "^1.1" 909 | }, 910 | "bin": [ 911 | "phpunit" 912 | ], 913 | "type": "library", 914 | "extra": { 915 | "branch-alias": { 916 | "dev-master": "6.5.x-dev" 917 | } 918 | }, 919 | "autoload": { 920 | "classmap": [ 921 | "src/" 922 | ] 923 | }, 924 | "notification-url": "https://packagist.org/downloads/", 925 | "license": [ 926 | "BSD-3-Clause" 927 | ], 928 | "authors": [ 929 | { 930 | "name": "Sebastian Bergmann", 931 | "email": "sebastian@phpunit.de", 932 | "role": "lead" 933 | } 934 | ], 935 | "description": "The PHP Unit Testing framework.", 936 | "homepage": "https://phpunit.de/", 937 | "keywords": [ 938 | "phpunit", 939 | "testing", 940 | "xunit" 941 | ], 942 | "support": { 943 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 944 | "source": "https://github.com/sebastianbergmann/phpunit/tree/6.5.14" 945 | }, 946 | "time": "2019-02-01T05:22:47+00:00" 947 | }, 948 | { 949 | "name": "phpunit/phpunit-mock-objects", 950 | "version": "5.0.10", 951 | "source": { 952 | "type": "git", 953 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 954 | "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f" 955 | }, 956 | "dist": { 957 | "type": "zip", 958 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/cd1cf05c553ecfec36b170070573e540b67d3f1f", 959 | "reference": "cd1cf05c553ecfec36b170070573e540b67d3f1f", 960 | "shasum": "" 961 | }, 962 | "require": { 963 | "doctrine/instantiator": "^1.0.5", 964 | "php": "^7.0", 965 | "phpunit/php-text-template": "^1.2.1", 966 | "sebastian/exporter": "^3.1" 967 | }, 968 | "conflict": { 969 | "phpunit/phpunit": "<6.0" 970 | }, 971 | "require-dev": { 972 | "phpunit/phpunit": "^6.5.11" 973 | }, 974 | "suggest": { 975 | "ext-soap": "*" 976 | }, 977 | "type": "library", 978 | "extra": { 979 | "branch-alias": { 980 | "dev-master": "5.0.x-dev" 981 | } 982 | }, 983 | "autoload": { 984 | "classmap": [ 985 | "src/" 986 | ] 987 | }, 988 | "notification-url": "https://packagist.org/downloads/", 989 | "license": [ 990 | "BSD-3-Clause" 991 | ], 992 | "authors": [ 993 | { 994 | "name": "Sebastian Bergmann", 995 | "email": "sebastian@phpunit.de", 996 | "role": "lead" 997 | } 998 | ], 999 | "description": "Mock Object library for PHPUnit", 1000 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 1001 | "keywords": [ 1002 | "mock", 1003 | "xunit" 1004 | ], 1005 | "support": { 1006 | "issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues", 1007 | "source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/5.0.10" 1008 | }, 1009 | "abandoned": true, 1010 | "time": "2018-08-09T05:50:03+00:00" 1011 | }, 1012 | { 1013 | "name": "sebastian/code-unit-reverse-lookup", 1014 | "version": "1.0.2", 1015 | "source": { 1016 | "type": "git", 1017 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1018 | "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" 1019 | }, 1020 | "dist": { 1021 | "type": "zip", 1022 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", 1023 | "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", 1024 | "shasum": "" 1025 | }, 1026 | "require": { 1027 | "php": ">=5.6" 1028 | }, 1029 | "require-dev": { 1030 | "phpunit/phpunit": "^8.5" 1031 | }, 1032 | "type": "library", 1033 | "extra": { 1034 | "branch-alias": { 1035 | "dev-master": "1.0.x-dev" 1036 | } 1037 | }, 1038 | "autoload": { 1039 | "classmap": [ 1040 | "src/" 1041 | ] 1042 | }, 1043 | "notification-url": "https://packagist.org/downloads/", 1044 | "license": [ 1045 | "BSD-3-Clause" 1046 | ], 1047 | "authors": [ 1048 | { 1049 | "name": "Sebastian Bergmann", 1050 | "email": "sebastian@phpunit.de" 1051 | } 1052 | ], 1053 | "description": "Looks up which function or method a line of code belongs to", 1054 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1055 | "support": { 1056 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 1057 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" 1058 | }, 1059 | "funding": [ 1060 | { 1061 | "url": "https://github.com/sebastianbergmann", 1062 | "type": "github" 1063 | } 1064 | ], 1065 | "time": "2020-11-30T08:15:22+00:00" 1066 | }, 1067 | { 1068 | "name": "sebastian/comparator", 1069 | "version": "2.1.3", 1070 | "source": { 1071 | "type": "git", 1072 | "url": "https://github.com/sebastianbergmann/comparator.git", 1073 | "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" 1074 | }, 1075 | "dist": { 1076 | "type": "zip", 1077 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", 1078 | "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", 1079 | "shasum": "" 1080 | }, 1081 | "require": { 1082 | "php": "^7.0", 1083 | "sebastian/diff": "^2.0 || ^3.0", 1084 | "sebastian/exporter": "^3.1" 1085 | }, 1086 | "require-dev": { 1087 | "phpunit/phpunit": "^6.4" 1088 | }, 1089 | "type": "library", 1090 | "extra": { 1091 | "branch-alias": { 1092 | "dev-master": "2.1.x-dev" 1093 | } 1094 | }, 1095 | "autoload": { 1096 | "classmap": [ 1097 | "src/" 1098 | ] 1099 | }, 1100 | "notification-url": "https://packagist.org/downloads/", 1101 | "license": [ 1102 | "BSD-3-Clause" 1103 | ], 1104 | "authors": [ 1105 | { 1106 | "name": "Jeff Welch", 1107 | "email": "whatthejeff@gmail.com" 1108 | }, 1109 | { 1110 | "name": "Volker Dusch", 1111 | "email": "github@wallbash.com" 1112 | }, 1113 | { 1114 | "name": "Bernhard Schussek", 1115 | "email": "bschussek@2bepublished.at" 1116 | }, 1117 | { 1118 | "name": "Sebastian Bergmann", 1119 | "email": "sebastian@phpunit.de" 1120 | } 1121 | ], 1122 | "description": "Provides the functionality to compare PHP values for equality", 1123 | "homepage": "https://github.com/sebastianbergmann/comparator", 1124 | "keywords": [ 1125 | "comparator", 1126 | "compare", 1127 | "equality" 1128 | ], 1129 | "support": { 1130 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 1131 | "source": "https://github.com/sebastianbergmann/comparator/tree/master" 1132 | }, 1133 | "time": "2018-02-01T13:46:46+00:00" 1134 | }, 1135 | { 1136 | "name": "sebastian/diff", 1137 | "version": "2.0.1", 1138 | "source": { 1139 | "type": "git", 1140 | "url": "https://github.com/sebastianbergmann/diff.git", 1141 | "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" 1142 | }, 1143 | "dist": { 1144 | "type": "zip", 1145 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", 1146 | "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", 1147 | "shasum": "" 1148 | }, 1149 | "require": { 1150 | "php": "^7.0" 1151 | }, 1152 | "require-dev": { 1153 | "phpunit/phpunit": "^6.2" 1154 | }, 1155 | "type": "library", 1156 | "extra": { 1157 | "branch-alias": { 1158 | "dev-master": "2.0-dev" 1159 | } 1160 | }, 1161 | "autoload": { 1162 | "classmap": [ 1163 | "src/" 1164 | ] 1165 | }, 1166 | "notification-url": "https://packagist.org/downloads/", 1167 | "license": [ 1168 | "BSD-3-Clause" 1169 | ], 1170 | "authors": [ 1171 | { 1172 | "name": "Kore Nordmann", 1173 | "email": "mail@kore-nordmann.de" 1174 | }, 1175 | { 1176 | "name": "Sebastian Bergmann", 1177 | "email": "sebastian@phpunit.de" 1178 | } 1179 | ], 1180 | "description": "Diff implementation", 1181 | "homepage": "https://github.com/sebastianbergmann/diff", 1182 | "keywords": [ 1183 | "diff" 1184 | ], 1185 | "support": { 1186 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1187 | "source": "https://github.com/sebastianbergmann/diff/tree/master" 1188 | }, 1189 | "time": "2017-08-03T08:09:46+00:00" 1190 | }, 1191 | { 1192 | "name": "sebastian/environment", 1193 | "version": "3.1.0", 1194 | "source": { 1195 | "type": "git", 1196 | "url": "https://github.com/sebastianbergmann/environment.git", 1197 | "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" 1198 | }, 1199 | "dist": { 1200 | "type": "zip", 1201 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", 1202 | "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", 1203 | "shasum": "" 1204 | }, 1205 | "require": { 1206 | "php": "^7.0" 1207 | }, 1208 | "require-dev": { 1209 | "phpunit/phpunit": "^6.1" 1210 | }, 1211 | "type": "library", 1212 | "extra": { 1213 | "branch-alias": { 1214 | "dev-master": "3.1.x-dev" 1215 | } 1216 | }, 1217 | "autoload": { 1218 | "classmap": [ 1219 | "src/" 1220 | ] 1221 | }, 1222 | "notification-url": "https://packagist.org/downloads/", 1223 | "license": [ 1224 | "BSD-3-Clause" 1225 | ], 1226 | "authors": [ 1227 | { 1228 | "name": "Sebastian Bergmann", 1229 | "email": "sebastian@phpunit.de" 1230 | } 1231 | ], 1232 | "description": "Provides functionality to handle HHVM/PHP environments", 1233 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1234 | "keywords": [ 1235 | "Xdebug", 1236 | "environment", 1237 | "hhvm" 1238 | ], 1239 | "support": { 1240 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1241 | "source": "https://github.com/sebastianbergmann/environment/tree/master" 1242 | }, 1243 | "time": "2017-07-01T08:51:00+00:00" 1244 | }, 1245 | { 1246 | "name": "sebastian/exporter", 1247 | "version": "3.1.4", 1248 | "source": { 1249 | "type": "git", 1250 | "url": "https://github.com/sebastianbergmann/exporter.git", 1251 | "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db" 1252 | }, 1253 | "dist": { 1254 | "type": "zip", 1255 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", 1256 | "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db", 1257 | "shasum": "" 1258 | }, 1259 | "require": { 1260 | "php": ">=7.0", 1261 | "sebastian/recursion-context": "^3.0" 1262 | }, 1263 | "require-dev": { 1264 | "ext-mbstring": "*", 1265 | "phpunit/phpunit": "^8.5" 1266 | }, 1267 | "type": "library", 1268 | "extra": { 1269 | "branch-alias": { 1270 | "dev-master": "3.1.x-dev" 1271 | } 1272 | }, 1273 | "autoload": { 1274 | "classmap": [ 1275 | "src/" 1276 | ] 1277 | }, 1278 | "notification-url": "https://packagist.org/downloads/", 1279 | "license": [ 1280 | "BSD-3-Clause" 1281 | ], 1282 | "authors": [ 1283 | { 1284 | "name": "Sebastian Bergmann", 1285 | "email": "sebastian@phpunit.de" 1286 | }, 1287 | { 1288 | "name": "Jeff Welch", 1289 | "email": "whatthejeff@gmail.com" 1290 | }, 1291 | { 1292 | "name": "Volker Dusch", 1293 | "email": "github@wallbash.com" 1294 | }, 1295 | { 1296 | "name": "Adam Harvey", 1297 | "email": "aharvey@php.net" 1298 | }, 1299 | { 1300 | "name": "Bernhard Schussek", 1301 | "email": "bschussek@gmail.com" 1302 | } 1303 | ], 1304 | "description": "Provides the functionality to export PHP variables for visualization", 1305 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1306 | "keywords": [ 1307 | "export", 1308 | "exporter" 1309 | ], 1310 | "support": { 1311 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1312 | "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.4" 1313 | }, 1314 | "funding": [ 1315 | { 1316 | "url": "https://github.com/sebastianbergmann", 1317 | "type": "github" 1318 | } 1319 | ], 1320 | "time": "2021-11-11T13:51:24+00:00" 1321 | }, 1322 | { 1323 | "name": "sebastian/global-state", 1324 | "version": "2.0.0", 1325 | "source": { 1326 | "type": "git", 1327 | "url": "https://github.com/sebastianbergmann/global-state.git", 1328 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" 1329 | }, 1330 | "dist": { 1331 | "type": "zip", 1332 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1333 | "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", 1334 | "shasum": "" 1335 | }, 1336 | "require": { 1337 | "php": "^7.0" 1338 | }, 1339 | "require-dev": { 1340 | "phpunit/phpunit": "^6.0" 1341 | }, 1342 | "suggest": { 1343 | "ext-uopz": "*" 1344 | }, 1345 | "type": "library", 1346 | "extra": { 1347 | "branch-alias": { 1348 | "dev-master": "2.0-dev" 1349 | } 1350 | }, 1351 | "autoload": { 1352 | "classmap": [ 1353 | "src/" 1354 | ] 1355 | }, 1356 | "notification-url": "https://packagist.org/downloads/", 1357 | "license": [ 1358 | "BSD-3-Clause" 1359 | ], 1360 | "authors": [ 1361 | { 1362 | "name": "Sebastian Bergmann", 1363 | "email": "sebastian@phpunit.de" 1364 | } 1365 | ], 1366 | "description": "Snapshotting of global state", 1367 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1368 | "keywords": [ 1369 | "global state" 1370 | ], 1371 | "support": { 1372 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1373 | "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0" 1374 | }, 1375 | "time": "2017-04-27T15:39:26+00:00" 1376 | }, 1377 | { 1378 | "name": "sebastian/object-enumerator", 1379 | "version": "3.0.4", 1380 | "source": { 1381 | "type": "git", 1382 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1383 | "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" 1384 | }, 1385 | "dist": { 1386 | "type": "zip", 1387 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", 1388 | "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", 1389 | "shasum": "" 1390 | }, 1391 | "require": { 1392 | "php": ">=7.0", 1393 | "sebastian/object-reflector": "^1.1.1", 1394 | "sebastian/recursion-context": "^3.0" 1395 | }, 1396 | "require-dev": { 1397 | "phpunit/phpunit": "^6.0" 1398 | }, 1399 | "type": "library", 1400 | "extra": { 1401 | "branch-alias": { 1402 | "dev-master": "3.0.x-dev" 1403 | } 1404 | }, 1405 | "autoload": { 1406 | "classmap": [ 1407 | "src/" 1408 | ] 1409 | }, 1410 | "notification-url": "https://packagist.org/downloads/", 1411 | "license": [ 1412 | "BSD-3-Clause" 1413 | ], 1414 | "authors": [ 1415 | { 1416 | "name": "Sebastian Bergmann", 1417 | "email": "sebastian@phpunit.de" 1418 | } 1419 | ], 1420 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1421 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1422 | "support": { 1423 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1424 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" 1425 | }, 1426 | "funding": [ 1427 | { 1428 | "url": "https://github.com/sebastianbergmann", 1429 | "type": "github" 1430 | } 1431 | ], 1432 | "time": "2020-11-30T07:40:27+00:00" 1433 | }, 1434 | { 1435 | "name": "sebastian/object-reflector", 1436 | "version": "1.1.2", 1437 | "source": { 1438 | "type": "git", 1439 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1440 | "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" 1441 | }, 1442 | "dist": { 1443 | "type": "zip", 1444 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", 1445 | "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", 1446 | "shasum": "" 1447 | }, 1448 | "require": { 1449 | "php": ">=7.0" 1450 | }, 1451 | "require-dev": { 1452 | "phpunit/phpunit": "^6.0" 1453 | }, 1454 | "type": "library", 1455 | "extra": { 1456 | "branch-alias": { 1457 | "dev-master": "1.1-dev" 1458 | } 1459 | }, 1460 | "autoload": { 1461 | "classmap": [ 1462 | "src/" 1463 | ] 1464 | }, 1465 | "notification-url": "https://packagist.org/downloads/", 1466 | "license": [ 1467 | "BSD-3-Clause" 1468 | ], 1469 | "authors": [ 1470 | { 1471 | "name": "Sebastian Bergmann", 1472 | "email": "sebastian@phpunit.de" 1473 | } 1474 | ], 1475 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1476 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1477 | "support": { 1478 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1479 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" 1480 | }, 1481 | "funding": [ 1482 | { 1483 | "url": "https://github.com/sebastianbergmann", 1484 | "type": "github" 1485 | } 1486 | ], 1487 | "time": "2020-11-30T07:37:18+00:00" 1488 | }, 1489 | { 1490 | "name": "sebastian/recursion-context", 1491 | "version": "3.0.1", 1492 | "source": { 1493 | "type": "git", 1494 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1495 | "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" 1496 | }, 1497 | "dist": { 1498 | "type": "zip", 1499 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", 1500 | "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", 1501 | "shasum": "" 1502 | }, 1503 | "require": { 1504 | "php": ">=7.0" 1505 | }, 1506 | "require-dev": { 1507 | "phpunit/phpunit": "^6.0" 1508 | }, 1509 | "type": "library", 1510 | "extra": { 1511 | "branch-alias": { 1512 | "dev-master": "3.0.x-dev" 1513 | } 1514 | }, 1515 | "autoload": { 1516 | "classmap": [ 1517 | "src/" 1518 | ] 1519 | }, 1520 | "notification-url": "https://packagist.org/downloads/", 1521 | "license": [ 1522 | "BSD-3-Clause" 1523 | ], 1524 | "authors": [ 1525 | { 1526 | "name": "Sebastian Bergmann", 1527 | "email": "sebastian@phpunit.de" 1528 | }, 1529 | { 1530 | "name": "Jeff Welch", 1531 | "email": "whatthejeff@gmail.com" 1532 | }, 1533 | { 1534 | "name": "Adam Harvey", 1535 | "email": "aharvey@php.net" 1536 | } 1537 | ], 1538 | "description": "Provides functionality to recursively process PHP variables", 1539 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1540 | "support": { 1541 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1542 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" 1543 | }, 1544 | "funding": [ 1545 | { 1546 | "url": "https://github.com/sebastianbergmann", 1547 | "type": "github" 1548 | } 1549 | ], 1550 | "time": "2020-11-30T07:34:24+00:00" 1551 | }, 1552 | { 1553 | "name": "sebastian/resource-operations", 1554 | "version": "1.0.0", 1555 | "source": { 1556 | "type": "git", 1557 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1558 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" 1559 | }, 1560 | "dist": { 1561 | "type": "zip", 1562 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1563 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1564 | "shasum": "" 1565 | }, 1566 | "require": { 1567 | "php": ">=5.6.0" 1568 | }, 1569 | "type": "library", 1570 | "extra": { 1571 | "branch-alias": { 1572 | "dev-master": "1.0.x-dev" 1573 | } 1574 | }, 1575 | "autoload": { 1576 | "classmap": [ 1577 | "src/" 1578 | ] 1579 | }, 1580 | "notification-url": "https://packagist.org/downloads/", 1581 | "license": [ 1582 | "BSD-3-Clause" 1583 | ], 1584 | "authors": [ 1585 | { 1586 | "name": "Sebastian Bergmann", 1587 | "email": "sebastian@phpunit.de" 1588 | } 1589 | ], 1590 | "description": "Provides a list of PHP built-in functions that operate on resources", 1591 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1592 | "support": { 1593 | "issues": "https://github.com/sebastianbergmann/resource-operations/issues", 1594 | "source": "https://github.com/sebastianbergmann/resource-operations/tree/master" 1595 | }, 1596 | "time": "2015-07-28T20:34:47+00:00" 1597 | }, 1598 | { 1599 | "name": "sebastian/version", 1600 | "version": "2.0.1", 1601 | "source": { 1602 | "type": "git", 1603 | "url": "https://github.com/sebastianbergmann/version.git", 1604 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1605 | }, 1606 | "dist": { 1607 | "type": "zip", 1608 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1609 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1610 | "shasum": "" 1611 | }, 1612 | "require": { 1613 | "php": ">=5.6" 1614 | }, 1615 | "type": "library", 1616 | "extra": { 1617 | "branch-alias": { 1618 | "dev-master": "2.0.x-dev" 1619 | } 1620 | }, 1621 | "autoload": { 1622 | "classmap": [ 1623 | "src/" 1624 | ] 1625 | }, 1626 | "notification-url": "https://packagist.org/downloads/", 1627 | "license": [ 1628 | "BSD-3-Clause" 1629 | ], 1630 | "authors": [ 1631 | { 1632 | "name": "Sebastian Bergmann", 1633 | "email": "sebastian@phpunit.de", 1634 | "role": "lead" 1635 | } 1636 | ], 1637 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1638 | "homepage": "https://github.com/sebastianbergmann/version", 1639 | "support": { 1640 | "issues": "https://github.com/sebastianbergmann/version/issues", 1641 | "source": "https://github.com/sebastianbergmann/version/tree/master" 1642 | }, 1643 | "time": "2016-10-03T07:35:21+00:00" 1644 | }, 1645 | { 1646 | "name": "squizlabs/php_codesniffer", 1647 | "version": "3.6.2", 1648 | "source": { 1649 | "type": "git", 1650 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 1651 | "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a" 1652 | }, 1653 | "dist": { 1654 | "type": "zip", 1655 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/5e4e71592f69da17871dba6e80dd51bce74a351a", 1656 | "reference": "5e4e71592f69da17871dba6e80dd51bce74a351a", 1657 | "shasum": "" 1658 | }, 1659 | "require": { 1660 | "ext-simplexml": "*", 1661 | "ext-tokenizer": "*", 1662 | "ext-xmlwriter": "*", 1663 | "php": ">=5.4.0" 1664 | }, 1665 | "require-dev": { 1666 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 1667 | }, 1668 | "bin": [ 1669 | "bin/phpcs", 1670 | "bin/phpcbf" 1671 | ], 1672 | "type": "library", 1673 | "extra": { 1674 | "branch-alias": { 1675 | "dev-master": "3.x-dev" 1676 | } 1677 | }, 1678 | "notification-url": "https://packagist.org/downloads/", 1679 | "license": [ 1680 | "BSD-3-Clause" 1681 | ], 1682 | "authors": [ 1683 | { 1684 | "name": "Greg Sherwood", 1685 | "role": "lead" 1686 | } 1687 | ], 1688 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 1689 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 1690 | "keywords": [ 1691 | "phpcs", 1692 | "standards" 1693 | ], 1694 | "support": { 1695 | "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", 1696 | "source": "https://github.com/squizlabs/PHP_CodeSniffer", 1697 | "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" 1698 | }, 1699 | "time": "2021-12-12T21:44:58+00:00" 1700 | }, 1701 | { 1702 | "name": "symfony/polyfill-ctype", 1703 | "version": "v1.19.0", 1704 | "source": { 1705 | "type": "git", 1706 | "url": "https://github.com/symfony/polyfill-ctype.git", 1707 | "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b" 1708 | }, 1709 | "dist": { 1710 | "type": "zip", 1711 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/aed596913b70fae57be53d86faa2e9ef85a2297b", 1712 | "reference": "aed596913b70fae57be53d86faa2e9ef85a2297b", 1713 | "shasum": "" 1714 | }, 1715 | "require": { 1716 | "php": ">=5.3.3" 1717 | }, 1718 | "suggest": { 1719 | "ext-ctype": "For best performance" 1720 | }, 1721 | "type": "library", 1722 | "extra": { 1723 | "branch-alias": { 1724 | "dev-main": "1.19-dev" 1725 | }, 1726 | "thanks": { 1727 | "name": "symfony/polyfill", 1728 | "url": "https://github.com/symfony/polyfill" 1729 | } 1730 | }, 1731 | "autoload": { 1732 | "files": [ 1733 | "bootstrap.php" 1734 | ], 1735 | "psr-4": { 1736 | "Symfony\\Polyfill\\Ctype\\": "" 1737 | } 1738 | }, 1739 | "notification-url": "https://packagist.org/downloads/", 1740 | "license": [ 1741 | "MIT" 1742 | ], 1743 | "authors": [ 1744 | { 1745 | "name": "Gert de Pagter", 1746 | "email": "BackEndTea@gmail.com" 1747 | }, 1748 | { 1749 | "name": "Symfony Community", 1750 | "homepage": "https://symfony.com/contributors" 1751 | } 1752 | ], 1753 | "description": "Symfony polyfill for ctype functions", 1754 | "homepage": "https://symfony.com", 1755 | "keywords": [ 1756 | "compatibility", 1757 | "ctype", 1758 | "polyfill", 1759 | "portable" 1760 | ], 1761 | "support": { 1762 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.19.0" 1763 | }, 1764 | "funding": [ 1765 | { 1766 | "url": "https://symfony.com/sponsor", 1767 | "type": "custom" 1768 | }, 1769 | { 1770 | "url": "https://github.com/fabpot", 1771 | "type": "github" 1772 | }, 1773 | { 1774 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1775 | "type": "tidelift" 1776 | } 1777 | ], 1778 | "time": "2020-10-23T09:01:57+00:00" 1779 | }, 1780 | { 1781 | "name": "theseer/tokenizer", 1782 | "version": "1.1.3", 1783 | "source": { 1784 | "type": "git", 1785 | "url": "https://github.com/theseer/tokenizer.git", 1786 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" 1787 | }, 1788 | "dist": { 1789 | "type": "zip", 1790 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1791 | "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", 1792 | "shasum": "" 1793 | }, 1794 | "require": { 1795 | "ext-dom": "*", 1796 | "ext-tokenizer": "*", 1797 | "ext-xmlwriter": "*", 1798 | "php": "^7.0" 1799 | }, 1800 | "type": "library", 1801 | "autoload": { 1802 | "classmap": [ 1803 | "src/" 1804 | ] 1805 | }, 1806 | "notification-url": "https://packagist.org/downloads/", 1807 | "license": [ 1808 | "BSD-3-Clause" 1809 | ], 1810 | "authors": [ 1811 | { 1812 | "name": "Arne Blankerts", 1813 | "email": "arne@blankerts.de", 1814 | "role": "Developer" 1815 | } 1816 | ], 1817 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1818 | "support": { 1819 | "issues": "https://github.com/theseer/tokenizer/issues", 1820 | "source": "https://github.com/theseer/tokenizer/tree/master" 1821 | }, 1822 | "time": "2019-06-13T22:48:21+00:00" 1823 | }, 1824 | { 1825 | "name": "webmozart/assert", 1826 | "version": "1.9.1", 1827 | "source": { 1828 | "type": "git", 1829 | "url": "https://github.com/webmozarts/assert.git", 1830 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" 1831 | }, 1832 | "dist": { 1833 | "type": "zip", 1834 | "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", 1835 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", 1836 | "shasum": "" 1837 | }, 1838 | "require": { 1839 | "php": "^5.3.3 || ^7.0 || ^8.0", 1840 | "symfony/polyfill-ctype": "^1.8" 1841 | }, 1842 | "conflict": { 1843 | "phpstan/phpstan": "<0.12.20", 1844 | "vimeo/psalm": "<3.9.1" 1845 | }, 1846 | "require-dev": { 1847 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 1848 | }, 1849 | "type": "library", 1850 | "autoload": { 1851 | "psr-4": { 1852 | "Webmozart\\Assert\\": "src/" 1853 | } 1854 | }, 1855 | "notification-url": "https://packagist.org/downloads/", 1856 | "license": [ 1857 | "MIT" 1858 | ], 1859 | "authors": [ 1860 | { 1861 | "name": "Bernhard Schussek", 1862 | "email": "bschussek@gmail.com" 1863 | } 1864 | ], 1865 | "description": "Assertions to validate method input/output with nice error messages.", 1866 | "keywords": [ 1867 | "assert", 1868 | "check", 1869 | "validate" 1870 | ], 1871 | "support": { 1872 | "issues": "https://github.com/webmozarts/assert/issues", 1873 | "source": "https://github.com/webmozarts/assert/tree/1.9.1" 1874 | }, 1875 | "time": "2020-07-08T17:02:28+00:00" 1876 | }, 1877 | { 1878 | "name": "wp-coding-standards/wpcs", 1879 | "version": "2.1.1", 1880 | "source": { 1881 | "type": "git", 1882 | "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", 1883 | "reference": "bd9c33152115e6741e3510ff7189605b35167908" 1884 | }, 1885 | "dist": { 1886 | "type": "zip", 1887 | "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/bd9c33152115e6741e3510ff7189605b35167908", 1888 | "reference": "bd9c33152115e6741e3510ff7189605b35167908", 1889 | "shasum": "" 1890 | }, 1891 | "require": { 1892 | "php": ">=5.4", 1893 | "squizlabs/php_codesniffer": "^3.3.1" 1894 | }, 1895 | "require-dev": { 1896 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", 1897 | "phpcompatibility/php-compatibility": "^9.0", 1898 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 1899 | }, 1900 | "suggest": { 1901 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." 1902 | }, 1903 | "type": "phpcodesniffer-standard", 1904 | "notification-url": "https://packagist.org/downloads/", 1905 | "license": [ 1906 | "MIT" 1907 | ], 1908 | "authors": [ 1909 | { 1910 | "name": "Contributors", 1911 | "homepage": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/graphs/contributors" 1912 | } 1913 | ], 1914 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 1915 | "keywords": [ 1916 | "phpcs", 1917 | "standards", 1918 | "wordpress" 1919 | ], 1920 | "support": { 1921 | "issues": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/issues", 1922 | "source": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards", 1923 | "wiki": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki" 1924 | }, 1925 | "time": "2019-05-21T02:50:00+00:00" 1926 | }, 1927 | { 1928 | "name": "xwp/wp-dev-lib", 1929 | "version": "1.3.0", 1930 | "source": { 1931 | "type": "git", 1932 | "url": "https://github.com/xwp/wp-dev-lib.git", 1933 | "reference": "e0205d733c2cabccfd05ec281ae43872fc42c221" 1934 | }, 1935 | "dist": { 1936 | "type": "zip", 1937 | "url": "https://api.github.com/repos/xwp/wp-dev-lib/zipball/e0205d733c2cabccfd05ec281ae43872fc42c221", 1938 | "reference": "e0205d733c2cabccfd05ec281ae43872fc42c221", 1939 | "shasum": "" 1940 | }, 1941 | "type": "library", 1942 | "notification-url": "https://packagist.org/downloads/", 1943 | "license": [ 1944 | "MIT" 1945 | ], 1946 | "authors": [ 1947 | { 1948 | "name": "Weston Ruter", 1949 | "email": "weston@xwp.co", 1950 | "homepage": "https://weston.ruter.net" 1951 | }, 1952 | { 1953 | "name": "XWP", 1954 | "email": "engage@xwp.co", 1955 | "homepage": "https://xwp.co" 1956 | } 1957 | ], 1958 | "description": "Common code used during development of WordPress plugins and themes", 1959 | "homepage": "https://github.com/xwp/wp-dev-lib", 1960 | "keywords": [ 1961 | "wordpress" 1962 | ], 1963 | "support": { 1964 | "issues": "https://github.com/xwp/wp-dev-lib/issues", 1965 | "source": "https://github.com/xwp/wp-dev-lib" 1966 | }, 1967 | "time": "2019-10-22T09:03:59+00:00" 1968 | } 1969 | ], 1970 | "aliases": [], 1971 | "minimum-stability": "stable", 1972 | "stability-flags": [], 1973 | "prefer-stable": true, 1974 | "prefer-lowest": false, 1975 | "platform": { 1976 | "php": ">=7.0" 1977 | }, 1978 | "platform-dev": [], 1979 | "platform-overrides": { 1980 | "php": "7.0" 1981 | }, 1982 | "plugin-api-version": "2.3.0" 1983 | } 1984 | -------------------------------------------------------------------------------- /includes/Block_Invocation.php: -------------------------------------------------------------------------------- 1 | 'block', 57 | 'name' => $this->name, 58 | 'dynamic' => true, 59 | 'attributes' => $this->attributes, 60 | ], 61 | $data 62 | ); 63 | 64 | return $data; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /includes/Block_Recognizer.php: -------------------------------------------------------------------------------- 1 | 'core', 51 | ]; 52 | } 53 | 54 | if ( isset( $this->cached_namespace_sources[ $namespace ] ) ) { 55 | return $this->cached_namespace_sources[ $namespace ]; 56 | } 57 | 58 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 59 | 60 | // Look among plugins. 61 | foreach ( array_keys( get_plugins( $this->plugin_folder ) ) as $plugin_name ) { 62 | $plugin_slug = str_replace( '.php', '', strtok( $plugin_name, '/' ) ); 63 | if ( $plugin_slug === $namespace ) { 64 | $this->cached_namespace_sources[ $namespace ] = [ 65 | 'type' => 'plugin', 66 | 'name' => $plugin_name, 67 | ]; 68 | return $this->cached_namespace_sources[ $namespace ]; 69 | } 70 | } 71 | 72 | // Look among themes. 73 | foreach ( wp_get_themes() as $theme ) { 74 | if ( $theme->get_stylesheet() === $namespace ) { 75 | $this->cached_namespace_sources[ $namespace ] = [ 76 | 'type' => 'theme', 77 | 'name' => $theme->get_stylesheet(), 78 | ]; 79 | return $this->cached_namespace_sources[ $namespace ]; 80 | } 81 | } 82 | 83 | // Look among mu-plugins. 84 | foreach ( array_keys( get_mu_plugins() ) as $mu_plugin_name ) { 85 | $mu_plugin_slug = str_replace( '.php', '', $mu_plugin_name ); 86 | if ( $mu_plugin_slug === $namespace ) { 87 | $this->cached_namespace_sources[ $namespace ] = [ 88 | 'type' => 'mu-plugin', 89 | 'name' => $mu_plugin_slug, 90 | ]; 91 | return $this->cached_namespace_sources[ $namespace ]; 92 | } 93 | } 94 | 95 | return null; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /includes/Calling_Reflection.php: -------------------------------------------------------------------------------- 1 | callback_reflection = new ReflectionMethod( $exploded_callback[0], $exploded_callback[1] ); 45 | } else { 46 | $this->callback_reflection = new ReflectionFunction( $callback ); 47 | } 48 | } elseif ( is_array( $callback ) && isset( $callback[0], $callback[1] ) && method_exists( $callback[0], $callback[1] ) ) { 49 | $reflection = new ReflectionMethod( $callback[0], $callback[1] ); 50 | 51 | // Handle the special case of the class being a widget, in which case the display_callback method should 52 | // actually map to the underling widget method. It is the display_callback in the end that is wrapped. 53 | if ( 'WP_Widget' === $reflection->getDeclaringClass()->getName() && 'display_callback' === $reflection->getName() ) { 54 | $reflection = new ReflectionMethod( $callback[0], 'widget' ); 55 | } 56 | 57 | $this->callback_reflection = $reflection; 58 | } elseif ( is_object( $callback ) && ( 'Closure' === get_class( $callback ) ) ) { 59 | $this->callback_reflection = new ReflectionFunction( $callback ); 60 | } elseif ( is_object( $callback ) && method_exists( $callback, '__invoke' ) ) { 61 | $this->callback_reflection = new ReflectionMethod( $callback, '__invoke' ); 62 | } else { 63 | throw new Exception( 'Unrecognized callable.' ); 64 | } 65 | } 66 | 67 | /** 68 | * Get callback reflection. 69 | * 70 | * @return ReflectionFunction|ReflectionMethod 71 | */ 72 | public function get_callback_reflection() { 73 | return $this->callback_reflection; 74 | } 75 | 76 | /** 77 | * Get normalized name. 78 | * 79 | * @return string Normalized name. 80 | */ 81 | public function get_name() { 82 | $name = $this->callback_reflection->getName(); 83 | if ( $this->callback_reflection instanceof ReflectionMethod ) { 84 | $name = $this->callback_reflection->getDeclaringClass()->getName() . '::' . $name; 85 | } 86 | return $name; 87 | } 88 | 89 | /** 90 | * Get file name. 91 | * 92 | * @return string File name where called method is defined. 93 | */ 94 | public function get_file_name() { 95 | return $this->callback_reflection->getFileName(); 96 | } 97 | 98 | /** 99 | * Get namespace for the called method/function. 100 | * 101 | * @return string Namespace. 102 | */ 103 | public function get_namespace_name() { 104 | if ( $this->callback_reflection instanceof ReflectionMethod ) { 105 | return $this->callback_reflection->getDeclaringClass()->getNamespaceName(); 106 | } else { 107 | return $this->callback_reflection->getNamespaceName(); 108 | } 109 | } 110 | 111 | /** 112 | * Determine whether the given reflection method/function has params passed by reference. 113 | * 114 | * @return bool Whether there are parameters passed by reference. 115 | */ 116 | public function has_parameters_passed_by_reference() { 117 | foreach ( $this->callback_reflection->getParameters() as $parameter ) { 118 | if ( $parameter->isPassedByReference() ) { 119 | return true; 120 | } 121 | } 122 | return false; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /includes/Database.php: -------------------------------------------------------------------------------- 1 | wpdb = $wpdb; 43 | } 44 | 45 | /** 46 | * Get the WordPress DB. 47 | * 48 | * @return wpdb|object DB. 49 | */ 50 | public function get_wpdb() { 51 | return $this->wpdb; 52 | } 53 | 54 | /** 55 | * Get index of the most recently-made query. 56 | * 57 | * @return int Index for the 58 | */ 59 | public function get_latest_query_index() { 60 | if ( 0 === $this->wpdb->num_queries ) { 61 | return 0; 62 | } 63 | return $this->wpdb->num_queries - 1; 64 | } 65 | 66 | /** 67 | * Get a query by index. 68 | * 69 | * This depends on the SAVEQUERIES constant being set. 70 | * 71 | * @param int $index Query index for array item in `$wpdb->queries`. 72 | * @return null|array { 73 | * Query information, or null if no query exists for the index. 74 | * 75 | * @type string $sql SQL. 76 | * @type float $duration Duration the query took to run. 77 | * @type string[] $backtrace Backtrace of PHP calls for the query. 78 | * @type float $timestamp Timestamp for the query. 79 | * } 80 | */ 81 | public function get_query_by_index( $index ) { 82 | if ( ! isset( $this->wpdb->queries[ $index ] ) ) { 83 | return null; 84 | } 85 | 86 | $query = $this->wpdb->queries[ $index ]; 87 | 88 | // Purge references to the hook wrapper from the query call stack. 89 | $search = sprintf( '%1$s->%2$s\{closure}, call_user_func_array, ', Hook_Wrapper::class, __NAMESPACE__ ); 90 | $backtrace = explode( ', ', str_replace( $search, '', $query[2] ) ); 91 | 92 | return [ 93 | 'sql' => $query[0], 94 | 'duration' => floatval( $query[1] ), 95 | 'backtrace' => $backtrace, 96 | 'timestamp' => isset( $query[3] ) ? floatval( $query[3] ) : null, 97 | ]; 98 | } 99 | 100 | /** 101 | * Identify the queries that were made during the invocation. 102 | * 103 | * @param Invocation $invocation Invocation. 104 | * @return int[] Query indices associated with the hook. 105 | */ 106 | public function identify_invocation_queries( Invocation $invocation ) { 107 | 108 | // Short-circuit if queries are not being saved (aka if SAVEQUERIES is not defined). 109 | if ( empty( $this->wpdb->queries ) ) { 110 | return []; 111 | } 112 | 113 | $latest_query_index = $this->get_latest_query_index(); 114 | 115 | // If no queries have been made during the hook invocation, short-circuit. 116 | if ( $latest_query_index === $invocation->get_before_query_index() ) { 117 | return []; 118 | } 119 | 120 | $query_indices = []; 121 | foreach ( range( $invocation->get_before_query_index(), $latest_query_index ) as $query_index ) { 122 | 123 | // Flag this query as being associated with this hook instance. 124 | if ( ! isset( $this->sourced_query_indices[ $query_index ] ) ) { 125 | $query_indices[] = $query_index; 126 | 127 | $this->sourced_query_indices[ $query_index ] = true; 128 | } 129 | } 130 | 131 | return $query_indices; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /includes/Dependencies.php: -------------------------------------------------------------------------------- 1 | get_dependency_registry( $type ); 67 | if ( $dependencies && isset( $dependencies->queue ) ) { 68 | return $dependencies->queue; 69 | } 70 | return []; 71 | } 72 | 73 | /** 74 | * Get the invocations that result in a dependency being enqueued (either directly or via another dependency). 75 | * 76 | * @param Invocation_Watcher $invocation_watcher Invocation watcher. 77 | * @param string $type Type (e.g. 'wp_scripts', 'wp_styles'). 78 | * @param string $handle Dependency handle. 79 | * @return Invocation[] Invocations. 80 | */ 81 | public function get_dependency_enqueueing_invocations( Invocation_Watcher $invocation_watcher, $type, $handle ) { 82 | $enqueueing_invocations = []; 83 | foreach ( $invocation_watcher->get_invocations() as $invocation ) { 84 | // @todo This should be be improved, perhaps a method that we can pass $type. 85 | if ( 'wp_scripts' === $type ) { 86 | $enqueued_handles = $invocation->get_enqueued_scripts(); 87 | } elseif ( 'wp_styles' === $type ) { 88 | $enqueued_handles = $invocation->get_enqueued_styles(); 89 | } else { 90 | $enqueued_handles = []; 91 | } 92 | 93 | $is_enqueued = false; 94 | if ( in_array( $handle, $enqueued_handles, true ) ) { 95 | $is_enqueued = true; 96 | } else { 97 | foreach ( $enqueued_handles as $invocation_enqueue_handle ) { 98 | $registry = $this->get_dependency_registry( $type ); 99 | if ( $registry && in_array( $handle, $this->get_dependencies( $registry, $invocation_enqueue_handle ), true ) ) { 100 | $is_enqueued = true; 101 | break; 102 | } 103 | } 104 | } 105 | if ( $is_enqueued ) { 106 | // @todo In reality we can just break here because we will only detect the first invocation that enqueues a dependency. 107 | $enqueueing_invocations[] = $invocation; 108 | } 109 | } 110 | return $enqueueing_invocations; 111 | } 112 | 113 | /** 114 | * Get the dependencies for a given $handle. 115 | * 116 | * @see WP_Dependencies::all_deps() Which isn't used because it has side effects. 117 | * 118 | * @param WP_Dependencies $dependencies Dependencies. 119 | * @param string $handle Dependency handle. 120 | * @param int $max_depth Maximum depth to look. Guards against infinite recursion. 121 | * @return string[] Handles. 122 | */ 123 | public function get_dependencies( WP_Dependencies $dependencies, $handle, $max_depth = 50 ) { 124 | if ( $max_depth < 0 || ! isset( $dependencies->registered[ $handle ] ) ) { 125 | return []; 126 | } 127 | 128 | $dependency_handles = $dependencies->registered[ $handle ]->deps; 129 | if ( empty( $dependency_handles ) ) { 130 | return []; 131 | } 132 | 133 | $max_depth--; 134 | return array_merge( 135 | $dependency_handles, 136 | // Wow, this _is_ PHP (5.6)! 137 | ...array_map( 138 | function( $dependency_handle ) use ( $dependencies, $max_depth ) { 139 | return $this->get_dependencies( $dependencies, $dependency_handle, $max_depth ); 140 | }, 141 | $dependency_handles 142 | ) 143 | ); 144 | } 145 | 146 | /** 147 | * Identify the scripts that were enqueued during the hook's invocation. 148 | * 149 | * @param Invocation $invocation Invocation. 150 | * 151 | * @return string[] Script handles. 152 | */ 153 | public function identify_enqueued_scripts( Invocation $invocation ) { 154 | $before_script_handles = $invocation->get_before_scripts_queue(); 155 | $after_script_handles = $this->get_dependency_queue( 'wp_scripts' ); 156 | 157 | $enqueued_handles = []; 158 | foreach ( array_diff( $after_script_handles, $before_script_handles ) as $enqueued_script ) { 159 | 160 | // Flag this script handle as being associated with this hook callback invocation. 161 | if ( ! isset( $this->sourced_script_enqueues[ $enqueued_script ] ) ) { 162 | $enqueued_handles[] = $enqueued_script; 163 | 164 | $this->sourced_script_enqueues[ $enqueued_script ] = true; 165 | } 166 | } 167 | return $enqueued_handles; 168 | } 169 | 170 | /** 171 | * Identify the styles that were enqueued during the hook's invocation. 172 | * 173 | * @param Invocation $invocation Invocation. 174 | * 175 | * @return string[] Style handles. 176 | */ 177 | public function identify_enqueued_styles( Invocation $invocation ) { 178 | $before_style_handles = $invocation->get_before_styles_queue(); 179 | $after_style_handles = $this->get_dependency_queue( 'wp_styles' ); 180 | 181 | $enqueued_handles = []; 182 | foreach ( array_diff( $after_style_handles, $before_style_handles ) as $enqueued_style ) { 183 | 184 | // Flag this style handle as being associated with this hook callback invocation. 185 | if ( ! isset( $this->sourced_style_enqueues[ $enqueued_style ] ) ) { 186 | $enqueued_handles[] = $enqueued_style; 187 | 188 | $this->sourced_style_enqueues[ $enqueued_style ] = true; 189 | } 190 | } 191 | return $enqueued_handles; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /includes/Exception.php: -------------------------------------------------------------------------------- 1 | core_directory = trailingslashit( wp_normalize_path( ABSPATH ) ); 59 | $this->current_theme = wp_get_theme(); 60 | $this->plugins_directories[] = trailingslashit( wp_normalize_path( WP_PLUGIN_DIR ) ); 61 | $this->mu_plugins_directory = trailingslashit( wp_normalize_path( WPMU_PLUGIN_DIR ) ); 62 | } 63 | 64 | /** 65 | * Identify the location for a given file. 66 | * 67 | * @param string $file File. 68 | * @return array|null { 69 | * Location information, or null if no location could be identified. 70 | * 71 | * @var string $type The type of location, either core, plugin, mu-plugin, or theme. 72 | * @var string $name The name of the entity, such as 'twentyseventeen' or 'amp/amp.php'. 73 | * @var \WP_Theme|array|null $data Additional data about the entity, such as the theme object or plugin data. 74 | * } 75 | */ 76 | public function identify( $file ) { 77 | 78 | if ( isset( $this->cached_file_locations[ $file ] ) ) { 79 | return $this->cached_file_locations[ $file ]; 80 | } 81 | 82 | $file = wp_normalize_path( $file ); 83 | $slug_pattern = '(?P[^/]+)'; 84 | 85 | if ( preg_match( ':' . preg_quote( $this->core_directory, ':' ) . '(wp-admin|wp-includes)/:s', $file, $matches ) ) { 86 | $this->cached_file_locations[ $file ] = [ 87 | 'type' => 'core', 88 | 'name' => $matches[1], 89 | 'data' => null, 90 | ]; 91 | return $this->cached_file_locations[ $file ]; 92 | } 93 | 94 | // Identify child theme file. 95 | if ( $this->current_theme->exists() && preg_match( ':' . preg_quote( $this->current_theme->get_stylesheet_directory(), ':' ) . '/:s', $file ) ) { 96 | $this->cached_file_locations[ $file ] = [ 97 | 'type' => 'theme', 98 | 'name' => $this->current_theme->get_stylesheet(), 99 | 'data' => $this->current_theme, 100 | ]; 101 | return $this->cached_file_locations[ $file ]; 102 | } 103 | 104 | // Identify parent theme file. 105 | if ( $this->current_theme->parent() && preg_match( ':' . preg_quote( $this->current_theme->get_template_directory(), ':' ) . '/:s', $file ) ) { 106 | $this->cached_file_locations[ $file ] = [ 107 | 'type' => 'theme', 108 | 'name' => $this->current_theme->get_template(), 109 | 'data' => $this->current_theme->parent(), 110 | ]; 111 | return $this->cached_file_locations[ $file ]; 112 | } 113 | 114 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 115 | 116 | $plugin_dir = null; 117 | $plugin_matches = null; 118 | foreach ( $this->plugins_directories as $plugin_directory ) { 119 | if ( preg_match( ':' . preg_quote( $plugin_directory, ':' ) . $slug_pattern . '(?P/.+$)?:s', $file, $matches ) ) { 120 | $plugin_matches = $matches; 121 | $plugin_dir = $plugin_directory . '/' . $matches['root_slug'] . '/'; 122 | break; 123 | } 124 | unset( $matches ); 125 | } 126 | 127 | if ( $plugin_dir && $plugin_matches ) { 128 | // Fallback slug is the path segment under the plugins directory. 129 | $slug = $plugin_matches['root_slug']; 130 | 131 | $data = null; 132 | 133 | // Try getting the plugin data from the file itself if it is in a plugin directory. 134 | if ( empty( $plugin_matches['rel_path'] ) || 0 === substr_count( trim( $plugin_matches['rel_path'], '/' ), '/' ) ) { 135 | $data = get_plugin_data( $file ); 136 | } 137 | 138 | // If the file is itself a plugin file, then the slug includes the rel_path under the root_slug. 139 | if ( ! empty( $data['Name'] ) && ! empty( $plugin_matches['rel_path'] ) ) { 140 | $slug .= $plugin_matches['rel_path']; 141 | } 142 | 143 | // If the file is not a plugin file, try looking for {slug}/{slug}.php. 144 | if ( empty( $data['Name'] ) && file_exists( $plugin_dir . $plugin_matches['root_slug'] . '.php' ) ) { 145 | $slug = $plugin_matches['root_slug'] . '/' . $plugin_matches['root_slug'] . '.php'; 146 | $data = get_plugin_data( $plugin_dir . $plugin_matches['root_slug'] . '.php' ); 147 | } 148 | 149 | // Otherwise, grab the first plugin file located in the plugin directory. 150 | if ( empty( $data['Name'] ) ) { 151 | $plugins = get_plugins( '/' . $plugin_matches['root_slug'] ); 152 | if ( ! empty( $plugins ) ) { 153 | $key = key( $plugins ); 154 | $data = $plugins[ $key ]; 155 | $slug = $plugin_matches['root_slug'] . '/' . $key; 156 | } 157 | } 158 | 159 | // Failed to locate the plugin. 160 | if ( empty( $data['Name'] ) ) { 161 | $data = null; 162 | } 163 | 164 | $this->cached_file_locations[ $file ] = [ 165 | 'type' => 'plugin', 166 | 'name' => $slug, 167 | 'data' => $data, 168 | ]; 169 | return $this->cached_file_locations[ $file ]; 170 | } 171 | 172 | if ( preg_match( ':' . preg_quote( $this->mu_plugins_directory, ':' ) . $slug_pattern . ':s', $file, $matches ) ) { 173 | $this->cached_file_locations[ $file ] = [ 174 | 'type' => 'mu-plugin', 175 | 'name' => $matches['root_slug'], 176 | 'data' => get_plugin_data( $file ), // This is a best guess as $file may not actually be the plugin file. 177 | ]; 178 | return $this->cached_file_locations[ $file ]; 179 | } 180 | 181 | return null; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /includes/Hook_Invocation.php: -------------------------------------------------------------------------------- 1 | name ) > 0; 39 | } 40 | 41 | /** 42 | * Whether a filter modified the value. 43 | * 44 | * @var null|bool 45 | */ 46 | public $value_modified = null; 47 | 48 | /** 49 | * Whether this invocation is expected to produce output (an action) vs a filter. 50 | * 51 | * @return bool Whether output is expected. 52 | */ 53 | public function can_output() { 54 | return $this->is_action(); 55 | } 56 | 57 | /** 58 | * Get data for exporting. 59 | * 60 | * @return array Data. 61 | */ 62 | public function data() { 63 | $data = parent::data(); 64 | $index = $data['index']; 65 | unset( $data['index'] ); 66 | 67 | $data = array_merge( 68 | compact( 'index' ), 69 | [ 70 | 'type' => $this->is_action() ? 'action' : 'filter', 71 | 'name' => $this->name, 72 | 'priority' => $this->priority, 73 | ], 74 | $data 75 | ); 76 | 77 | if ( 'filter' === $data['type'] ) { 78 | $data['value_modified'] = $this->value_modified; 79 | } 80 | 81 | return $data; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /includes/Hook_Wrapper.php: -------------------------------------------------------------------------------- 1 | before_each_callback = $before_each_callback; 63 | $this->after_each_callback = $after_each_callback; 64 | $this->before_all_callback = $before_all_callback; 65 | $this->after_all_callback = $after_all_callback; 66 | } 67 | 68 | /** 69 | * Add all hook to wrap hook callbacks. 70 | */ 71 | public function add_all_hook() { 72 | add_action( 'all', [ $this, 'wrap_hook_callbacks' ] ); 73 | } 74 | 75 | /** 76 | * Remove all hook to wrap hook callbacks. 77 | */ 78 | public function remove_all_hook() { 79 | remove_action( 'all', [ $this, 'wrap_hook_callbacks' ] ); 80 | } 81 | 82 | /** 83 | * Wrap filter/action callback functions for a given hook to capture performance data. 84 | * 85 | * Wrapped callback functions are reset to their original functions after invocation. 86 | * This runs at the 'all' action. 87 | * 88 | * @global \WP_Hook[] $wp_filter 89 | * 90 | * @param string $name Hook name for action or filter. 91 | * 92 | * @return void 93 | */ 94 | public function wrap_hook_callbacks( $name ) { 95 | global $wp_filter; 96 | 97 | // Short-circuit the 'all' hook if there aren't any callbacks added. 98 | if ( ! isset( $wp_filter[ $name ] ) ) { 99 | return; 100 | } 101 | 102 | // Run callback before all. 103 | if ( $this->before_all_callback ) { 104 | call_user_func( $this->before_all_callback, $name ); 105 | } 106 | 107 | $priorities = array_keys( $wp_filter[ $name ]->callbacks ); 108 | foreach ( $priorities as $priority ) { 109 | // @todo If $priority is PHP_INT_MAX, consider moving/merging them to PHP_INT_MAX - 1. 110 | foreach ( $wp_filter[ $name ]->callbacks[ $priority ] as &$callback ) { 111 | $function = $callback['function']; 112 | 113 | try { 114 | $source = new Calling_Reflection( $function ); 115 | } catch ( \Exception $e ) { 116 | // Skip if the source callback function cannot be determined by chance. 117 | // @todo This should perform a callback to communicate this case. 118 | continue; 119 | } 120 | 121 | // Prevent wrapping our own hooks. 122 | if ( in_array( $source->get_namespace_name(), $this->ignored_callback_namespaces, true ) ) { 123 | continue; 124 | } 125 | 126 | /* 127 | * A current limitation with wrapping callbacks is that the wrapped function cannot have 128 | * any parameters passed by reference. Without this the result is: 129 | * 130 | * > PHP Warning: Parameter 1 to wp_default_styles() expected to be a reference, value given. 131 | */ 132 | if ( $source->has_parameters_passed_by_reference() ) { 133 | // @todo This should perform a callback to communicate this case. 134 | continue; 135 | } 136 | 137 | $callback['function'] = function() use ( &$callback, $name, $priority, $function, $source ) { 138 | // Restore the original callback function after this wrapped callback function is invoked. 139 | $callback['function'] = $function; 140 | 141 | $hook_args = func_get_args(); 142 | 143 | // @todo Optionally capture debug backtrace? 144 | $reflection = $source->get_callback_reflection(); 145 | $source_file = $source->get_file_name(); 146 | $function_name = $source->get_name(); 147 | $is_filter = ! did_action( $name ); 148 | $value_modified = null; 149 | $context = compact( 'name', 'function', 'function_name', 'reflection', 'source_file', 'priority', 'hook_args', 'is_filter' ); 150 | if ( $this->before_each_callback ) { 151 | call_user_func( $this->before_each_callback, $context ); 152 | } 153 | $exception = null; 154 | try { 155 | $return = call_user_func_array( $function, $hook_args ); 156 | if ( $is_filter && isset( $hook_args[0] ) ) { 157 | $value_modified = ( $return !== $hook_args[0] ); 158 | } 159 | } catch ( \Exception $e ) { 160 | $exception = $e; 161 | $return = null; 162 | } 163 | if ( $this->after_each_callback ) { 164 | $context['return'] = $return; 165 | if ( $is_filter ) { 166 | $context['value_modified'] = $value_modified; 167 | } 168 | 169 | $return_override = call_user_func( $this->after_each_callback, $context ); 170 | 171 | // Give the opportunity for the after_callback to override the (filtered) hook response, e.g. to add annotations. 172 | if ( isset( $return_override ) ) { 173 | $return = $return_override; 174 | } 175 | } 176 | if ( $exception ) { 177 | throw $exception; 178 | } 179 | 180 | return $return; 181 | }; 182 | } 183 | } 184 | 185 | // Run callback after all. 186 | if ( $this->after_all_callback ) { 187 | $after_all_priority = max( $priorities ) + 1; 188 | $after_all_callback = function () use ( $name, &$after_all_callback, $after_all_priority ) { 189 | remove_filter( $name, $after_all_callback, $after_all_priority ); // Remove self. 190 | return call_user_func_array( $this->after_all_callback, array_merge( [ $name ], func_get_args() ) ); 191 | }; 192 | add_filter( $name, $after_all_callback, $after_all_priority, 2 ); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /includes/Incrementor.php: -------------------------------------------------------------------------------- 1 | current_value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /includes/Invocation.php: -------------------------------------------------------------------------------- 1 | queue, $before_styles_queue ) or call identify_enqueued_styles? Would not be final, however. 171 | * @var string[] 172 | */ 173 | protected $enqueued_styles = []; 174 | 175 | /** 176 | * The indices of the queries in $wpdb->queries that this hook was responsible for. 177 | * 178 | * @var int[] 179 | */ 180 | public $own_query_indices = []; 181 | 182 | /** 183 | * Constructor. 184 | * 185 | * @param Invocation_Watcher $watcher Watcher. 186 | * @param Incrementor $incrementor Incrementor. 187 | * @param Database $database Database. 188 | * @param File_Locator $file_locator File locator. 189 | * @param Dependencies $dependencies Dependencies. 190 | * @param array $args Arguments which are assigned to properties. 191 | */ 192 | public function __construct( Invocation_Watcher $watcher, Incrementor $incrementor, Database $database, File_Locator $file_locator, Dependencies $dependencies, $args ) { 193 | foreach ( $args as $key => $value ) { 194 | if ( property_exists( $this, $key ) ) { 195 | $this->$key = $value; 196 | } 197 | } 198 | $this->index = $incrementor->next(); 199 | $this->invocation_watcher = $watcher; 200 | $this->database = $database; 201 | $this->file_locator = $file_locator; 202 | $this->dependencies = $dependencies; 203 | $this->start_time = microtime( true ); 204 | 205 | $this->before_query_index = $this->database->get_latest_query_index(); 206 | 207 | // @todo Better to have some multi-dimensional array structure here? 208 | $this->before_scripts_queue = $this->dependencies->get_dependency_queue( 'wp_scripts' ); 209 | $this->before_styles_queue = $this->dependencies->get_dependency_queue( 'wp_styles' ); 210 | } 211 | 212 | /** 213 | * Whether this invocation is expected to produce output. 214 | * 215 | * @todo This is perhaps not relevant in the base class. 216 | * 217 | * @return bool Whether output is expected. 218 | */ 219 | public function can_output() { 220 | return true; 221 | } 222 | 223 | /** 224 | * Number of queries before function started. 225 | * 226 | * @return int Query index. 227 | */ 228 | public function get_before_query_index() { 229 | return $this->before_query_index; 230 | } 231 | 232 | /** 233 | * Number of queries after function started. 234 | * 235 | * The after_query_index won't be set if method is invoked before finalize() is called. 236 | * 237 | * @return int Query index. 238 | */ 239 | public function get_after_query_index() { 240 | return isset( $this->after_query_index ) ? $this->after_query_index : $this->database->get_latest_query_index(); 241 | } 242 | 243 | /** 244 | * Get the script handles enqueued before the hook callback was invoked. 245 | * 246 | * @todo Combine into get_before_dependencies_queue? 247 | * @return string[] Script handles. 248 | */ 249 | public function get_before_scripts_queue() { 250 | return $this->before_scripts_queue; 251 | } 252 | 253 | /** 254 | * Get the enqueued script handles during invocation (excluding child invocations). 255 | * 256 | * @throws \Exception If called prior to being finalized. 257 | * @return string[] Script handles. 258 | */ 259 | public function get_enqueued_scripts() { 260 | if ( ! $this->finalized ) { 261 | throw new \Exception( 'Not finalized.' ); 262 | } 263 | return $this->enqueued_scripts; 264 | } 265 | 266 | /** 267 | * Get the style handles enqueued before the hook callback was invoked. 268 | * 269 | * @return string[] Style handles. 270 | */ 271 | public function get_before_styles_queue() { 272 | return $this->before_styles_queue; 273 | } 274 | 275 | /** 276 | * Get the enqueued style handles during invocation (excluding child invocations). 277 | * 278 | * @throws \Exception If called prior to being finalized. 279 | * @return string[] Style handles. 280 | */ 281 | public function get_enqueued_styles() { 282 | if ( ! $this->finalized ) { 283 | throw new \Exception( 'Not finalized.' ); 284 | } 285 | return $this->enqueued_styles; 286 | } 287 | 288 | /** 289 | * Finalize the invocation. 290 | * 291 | * @throws \Exception If the invocation was already finalized. 292 | * @param array $args Additional args to merge. 293 | */ 294 | public function finalize( $args = array() ) { 295 | foreach ( $args as $key => $value ) { 296 | if ( property_exists( $this, $key ) ) { 297 | $this->$key = $value; 298 | } 299 | } 300 | if ( $this->finalized ) { 301 | throw new \Exception( 'Already finalized.' ); 302 | } 303 | $this->end_time = microtime( true ); 304 | 305 | // Flag the queries that were used during this hook. 306 | $this->after_query_index = $this->database->get_latest_query_index(); 307 | 308 | $this->own_query_indices = $this->database->identify_invocation_queries( $this ); 309 | 310 | // Capture the scripts and styles that were enqueued by this hook. 311 | $this->enqueued_scripts = $this->dependencies->identify_enqueued_scripts( $this ); 312 | $this->enqueued_styles = $this->dependencies->identify_enqueued_styles( $this ); 313 | 314 | $this->finalized = true; 315 | 316 | // These are no longer needed after calling identify_queued_scripts and identify_queued_styles, and they just take up memory. 317 | unset( $this->before_scripts_queue ); 318 | unset( $this->before_styles_queue ); 319 | } 320 | 321 | /** 322 | * Get the duration of the hook callback invocation. 323 | * 324 | * @param bool $own_time Whether to exclude children invocations from the total time. 325 | * @return float Duration. 326 | */ 327 | public function duration( $own_time = true ) { 328 | // The end_time won't be set if method is invoked before finalize() is called. 329 | $end_time = isset( $this->end_time ) ? $this->end_time : microtime( true ); 330 | 331 | $duration = $end_time - $this->start_time; 332 | 333 | if ( $own_time ) { 334 | foreach ( $this->children as $invocation ) { 335 | $duration -= $invocation->duration( false ); 336 | } 337 | } 338 | return $duration; 339 | } 340 | 341 | /** 342 | * Get the indices for the queries that were made for this hook. 343 | * 344 | * @param bool $own Whether to only return query indices for this invocation and not the children. 345 | * @return array Query indices. 346 | */ 347 | public function get_query_indices( $own = true ) { 348 | $after_query_index = $this->get_after_query_index(); 349 | 350 | if ( $this->get_before_query_index() === $after_query_index ) { 351 | return []; 352 | } 353 | 354 | $query_indices = $this->own_query_indices; 355 | 356 | if ( $own ) { 357 | return $query_indices; 358 | } 359 | 360 | // Recursively gather all children query indices. 361 | $query_indices = array_merge( 362 | $query_indices, 363 | ...array_map( 364 | function ( Invocation $invocation ) { 365 | return $invocation->get_query_indices( false ); 366 | }, 367 | $this->children 368 | ) 369 | ); 370 | 371 | sort( $query_indices ); 372 | return $query_indices; 373 | } 374 | 375 | /** 376 | * Get the queries made during the hook callback invocation. 377 | * 378 | * @param bool $own Whether to only return queries from this invocation and not the children. 379 | * @return array|null Queries or null if no queries are being saved (SAVEQUERIES). 380 | */ 381 | public function queries( $own = true ) { 382 | return array_filter( 383 | array_map( 384 | function( $query_index ) { 385 | return $this->database->get_query_by_index( $query_index ); 386 | }, 387 | $this->get_query_indices( $own ) 388 | ) 389 | ); 390 | } 391 | 392 | /** 393 | * Get the location of the file. 394 | * 395 | * @return array|null { 396 | * Location information, or null if no location could be identified. 397 | * 398 | * @var string $type The type of location, either core, plugin, mu-plugin, or theme. 399 | * @var string $name The name of the entity, such as 'twentyseventeen' or 'amp/amp.php'. 400 | * @var \WP_Theme|array|null $data Additional data about the entity, such as the theme object or plugin data. 401 | * } 402 | */ 403 | public function file_location() { 404 | return $this->file_locator->identify( $this->source_file ); 405 | } 406 | 407 | /** 408 | * Get annotation data. 409 | * 410 | * @return array Data. 411 | */ 412 | public function data() { 413 | $data = [ 414 | 'index' => $this->index, 415 | 'function' => $this->function_name, 416 | 'own_time' => $this->duration( true ), 417 | 'source' => [], 418 | 'parent' => $this->parent ? $this->parent->index : null, 419 | 'children' => array_map( 420 | function( Invocation $invocation ) { 421 | return $invocation->index; 422 | }, 423 | $this->children 424 | ), 425 | ]; 426 | 427 | if ( ! empty( $this->enqueued_scripts ) ) { 428 | $data['enqueued_scripts'] = $this->enqueued_scripts; 429 | } 430 | if ( ! empty( $this->enqueued_styles ) ) { 431 | $data['enqueued_styles'] = $this->enqueued_styles; 432 | } 433 | 434 | if ( $this->source_file ) { 435 | $data['source']['file'] = $this->source_file; 436 | 437 | $file_location = $this->file_location(); 438 | if ( $file_location ) { 439 | $data['source']['type'] = $file_location['type']; 440 | $data['source']['name'] = $file_location['name']; 441 | } 442 | } elseif ( $this->reflection ) { 443 | $data['source']['type'] = 'php'; 444 | $data['source']['name'] = $this->reflection->getExtensionName(); 445 | } 446 | 447 | return $data; 448 | } 449 | } 450 | -------------------------------------------------------------------------------- /includes/Invocation_Watcher.php: -------------------------------------------------------------------------------- 1 | file_locator = $file_locator; 123 | $this->output_annotator = $output_annotator; 124 | $this->dependencies = $dependencies; 125 | $this->database = $database; 126 | $this->incrementor = $incrementor; 127 | $this->hook_wrapper = $hook_wrapper; 128 | } 129 | 130 | /** 131 | * Start watching. 132 | */ 133 | public function start() { 134 | $this->hook_wrapper->before_each_callback = [ $this, 'before_each_hook_callback' ]; 135 | $this->hook_wrapper->after_each_callback = [ $this, 'after_each_hook_callback' ]; 136 | $this->hook_wrapper->before_all_callback = [ $this, 'before_all_hook_callbacks' ]; 137 | $this->hook_wrapper->after_all_callback = [ $this, 'after_all_hook_callbacks' ]; 138 | $this->hook_wrapper->add_all_hook(); 139 | 140 | add_action( 'template_redirect', [ $this, 'wrap_shortcode_callbacks' ] ); 141 | add_action( 'template_redirect', [ $this, 'wrap_block_render_callbacks' ] ); 142 | add_action( 'template_redirect', [ $this, 'wrap_widget_callbacks' ] ); 143 | } 144 | 145 | /** 146 | * Determine whether queries can be shown. 147 | * 148 | * @return bool Whether to show queries. 149 | */ 150 | public function can_show_queries() { 151 | return ! empty( $this->can_show_queries_callback ) && call_user_func( $this->can_show_queries_callback ); 152 | } 153 | 154 | /** 155 | * Push new invocation onto the stack. 156 | * 157 | * @param Invocation $invocation Pushed invocation. 158 | * @return int The number of elements in the stack. 159 | */ 160 | public function push_invocation_stack( Invocation $invocation ) { 161 | $this->invocations[ $invocation->index ] = $invocation; 162 | return array_push( $this->invocation_stack, $invocation ); 163 | } 164 | 165 | /** 166 | * Get an invocation by its index. 167 | * 168 | * @param int $index Invocation index. 169 | * @return Invocation|null Invocation with the given ID or null if not defined. 170 | */ 171 | public function get_invocation_by_index( $index ) { 172 | if ( isset( $this->invocations[ $index ] ) ) { 173 | return $this->invocations[ $index ]; 174 | } 175 | return null; 176 | } 177 | 178 | /** 179 | * Get invocations. 180 | * 181 | * @yield Invocation 182 | */ 183 | public function get_invocations() { 184 | foreach ( $this->invocations as $index => $invocation ) { 185 | yield $index => $invocation; 186 | } 187 | } 188 | 189 | /** 190 | * Pop the top invocation off the stack. 191 | * 192 | * @return Invocation Popped invocation. 193 | */ 194 | public function pop_invocation_stack() { 195 | return array_pop( $this->invocation_stack ); 196 | } 197 | 198 | /** 199 | * Get the parent invocation based on the stack. 200 | * 201 | * @return Invocation|null Parent invocation. 202 | */ 203 | public function get_parent_invocation() { 204 | $parent = null; 205 | if ( ! empty( $this->invocation_stack ) ) { 206 | $parent = $this->invocation_stack[ count( $this->invocation_stack ) - 1 ]; 207 | } 208 | return $parent; 209 | } 210 | 211 | /** 212 | * Determine whether a hook is a filter. 213 | * 214 | * @param string $name Hook name. 215 | * @return bool Whether filter. 216 | */ 217 | protected function is_filter( $name ) { 218 | return ! did_action( $name ); 219 | } 220 | 221 | /** 222 | * Add a filter to run at the very end to wrap the output with the annotations, if the filter is annotatable. 223 | * 224 | * @param string $name Name. 225 | */ 226 | public function before_all_hook_callbacks( $name ) { 227 | if ( $this->is_filter( $name ) ) { 228 | $this->pending_filter_invocations_stack[] = []; 229 | } 230 | } 231 | 232 | /** 233 | * Before hook. 234 | * 235 | * @todo This should use Wrapped_Callback. 236 | * 237 | * @param array $args { 238 | * Args. 239 | * 240 | * @var string $name Name. 241 | * @var callable $function Function. 242 | * @var int $priority Priority. 243 | * @var array $hook_args Hook args. 244 | * } 245 | */ 246 | public function before_each_hook_callback( $args ) { 247 | $parent = $this->get_parent_invocation(); 248 | 249 | $args['parent'] = $parent; 250 | 251 | $invocation = new Hook_Invocation( $this, $this->incrementor, $this->database, $this->file_locator, $this->dependencies, $args ); 252 | 253 | if ( $parent ) { 254 | $parent->children[] = $invocation; 255 | } 256 | 257 | $this->push_invocation_stack( $invocation ); 258 | 259 | // @todo There needs to be a callback to be given an $invocation and for us to determine whether or not to render given $args. 260 | if ( $invocation->can_output() ) { 261 | echo $this->output_annotator->get_before_invocation_placeholder_annotation( $invocation ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 262 | } 263 | } 264 | 265 | /** 266 | * After hook. 267 | * 268 | * @todo This should use Wrapped_Callback. 269 | * 270 | * @throws \Exception If the stack was empty, which should not happen. 271 | * 272 | * @param array $args { 273 | * Args. 274 | * 275 | * @var string $name Name. 276 | * @var callable $function Function. 277 | * @var int $priority Priority. 278 | * @var array $hook_args Hook args. 279 | * @var bool $value_modified Whether the value was modified. 280 | * @var mixed $return The returned value when filtering. 281 | * } 282 | */ 283 | public function after_each_hook_callback( $args ) { 284 | $invocation = $this->pop_invocation_stack(); 285 | if ( ! $invocation ) { 286 | throw new \Exception( 'Stack was empty' ); 287 | } 288 | 289 | $value_modified = ! empty( $args['value_modified'] ); 290 | $invocation->finalize( 291 | compact( 'value_modified' ) 292 | ); 293 | 294 | if ( $invocation instanceof Hook_Invocation && ! $invocation->is_action() ) { 295 | $this->pending_filter_invocations_stack[ count( $this->pending_filter_invocations_stack ) - 1 ][] = $invocation; 296 | } 297 | 298 | // @todo There needs to be a callback to be given an $invocation and for us to determine whether or not to render given $args. 299 | // @todo $this->output_annotator->should_annotate( $invocation, $args ) 300 | if ( $invocation->can_output() ) { 301 | echo $this->output_annotator->get_after_invocation_placeholder_annotation( $invocation ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 302 | } 303 | } 304 | 305 | /** 306 | * After all filter hook callbacks. 307 | * 308 | * @param string $name Hook name. 309 | * @param mixed $value Filtered value. 310 | * @return mixed Filtered value, with wrapped annotations if filter is annotatable. 311 | */ 312 | public function after_all_hook_callbacks( $name, $value = null ) { 313 | if ( ! $this->is_filter( $name ) ) { 314 | return $value; 315 | } 316 | $pending_invocations = array_pop( $this->pending_filter_invocations_stack ); 317 | assert( is_array( $pending_invocations ) ); 318 | foreach ( $pending_invocations as $invocation ) { 319 | assert( $invocation instanceof Hook_Invocation ); 320 | assert( ! $invocation->is_action() ); 321 | assert( $name === $invocation->name ); 322 | if ( $invocation->value_modified && in_array( $name, $this->annotatable_filters, true ) ) { 323 | $value = $this->output_annotator->get_before_invocation_placeholder_annotation( $invocation ) . $value . $this->output_annotator->get_after_invocation_placeholder_annotation( $invocation ); 324 | } 325 | } 326 | return $value; 327 | } 328 | 329 | /** 330 | * Wrap each shortcode callback to capture the invocation. 331 | * 332 | * This function must be called after all shortcodes are added, such as at template_redirect. 333 | * Note that overriding and wrapping the callback is done instead of using the 'do_shortcode_tag' filter 334 | * because the former method allows us to capture the stylesheets that were enqueued when it was called. 335 | * 336 | * @global array $shortcode_tags 337 | */ 338 | public function wrap_shortcode_callbacks() { 339 | global $shortcode_tags; 340 | 341 | foreach ( array_keys( $shortcode_tags ) as $tag ) { 342 | $callback = $shortcode_tags[ $tag ]; 343 | 344 | $shortcode_tags[ $tag ] = new Wrapped_Callback( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 345 | $callback, 346 | $this, 347 | function( $invocation_args, $func_args ) use ( $tag ) { 348 | return new Shortcode_Invocation( 349 | $this, 350 | $this->incrementor, 351 | $this->database, 352 | $this->file_locator, 353 | $this->dependencies, 354 | array_merge( 355 | $invocation_args, 356 | compact( 'tag' ), 357 | [ 358 | 'attributes' => isset( $func_args[0] ) ? $func_args[0] : [], 359 | 'content' => isset( $func_args[1] ) ? $func_args[1] : null, 360 | ] 361 | ) 362 | ); 363 | }, 364 | function ( Shortcode_Invocation $invocation, $func_args ) { 365 | $return = call_user_func_array( $invocation->function, $func_args ); 366 | return $this->output_annotator->get_before_invocation_placeholder_annotation( $invocation ) . $return . $this->output_annotator->get_after_invocation_placeholder_annotation( $invocation ); 367 | } 368 | ); 369 | } 370 | } 371 | 372 | /** 373 | * Wrap each block render callback to capture the invocation. 374 | * 375 | * This only applies to dynamic blocks. For static blocks, the 'render_block' filter is used and which is handled 376 | * by the Output_Annotator, since static blocks do not involve invocations. 377 | * 378 | * This function must be called after all blocks are registered, such as at template_redirect. 379 | * Note that overriding and wrapping the callback is done instead of exclusively using the 'render_block' filter 380 | * because the former method allows us to capture the stylesheets that were enqueued when it was called. 381 | * 382 | * @see \Google\WP_Origination\Output_Annotator::add_static_block_annotation() 383 | */ 384 | public function wrap_block_render_callbacks() { 385 | foreach ( \WP_Block_Type_Registry::get_instance()->get_all_registered() as $block_type ) { 386 | if ( ! $block_type->is_dynamic() ) { 387 | continue; 388 | } 389 | 390 | $block_type->render_callback = new Wrapped_Callback( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 391 | $block_type->render_callback, 392 | $this, 393 | function( $invocation_args, $func_args ) use ( $block_type ) { 394 | return new Block_Invocation( 395 | $this, 396 | $this->incrementor, 397 | $this->database, 398 | $this->file_locator, 399 | $this->dependencies, 400 | array_merge( 401 | $invocation_args, 402 | [ 403 | 'name' => $block_type->name, 404 | 'attributes' => isset( $func_args[0] ) ? $func_args[0] : [], 405 | 'content' => isset( $func_args[1] ) ? $func_args[1] : null, 406 | ] 407 | ) 408 | ); 409 | }, 410 | function ( Block_Invocation $invocation, $func_args ) { 411 | $return = call_user_func_array( $invocation->function, $func_args ); 412 | return $this->output_annotator->get_before_invocation_placeholder_annotation( $invocation ) . $return . $this->output_annotator->get_after_invocation_placeholder_annotation( $invocation ); 413 | } 414 | ); 415 | } 416 | } 417 | 418 | /** 419 | * Wrap each widget render callback to capture the invocation. 420 | * 421 | * @global array $wp_registered_widgets 422 | */ 423 | public function wrap_widget_callbacks() { 424 | global $wp_registered_widgets; 425 | foreach ( $wp_registered_widgets as $widget_id => &$registered_widget ) { 426 | $function = $registered_widget['callback']; 427 | 428 | $registered_widget['callback'] = new Wrapped_Callback( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 429 | $function, 430 | $this, 431 | function( $invocation_args, $func_args ) use ( $function ) { 432 | 433 | $widget = null; 434 | if ( is_array( $function ) && isset( $function[0] ) && $function[0] instanceof \WP_Widget ) { 435 | $widget = $function[0]; 436 | } 437 | 438 | /* 439 | * Note that $widget_args should only contain 'number' if it is a multi-widget. 440 | * There is also a possibility that additional params could be registered for non-multi widgets 441 | * when more than 4 arguments to wp_register_sidebar_widget(), but this is probably unlikely 442 | * and it is even less likely to be necessary for our purposes here. 443 | */ 444 | $number = isset( $func_args[1]['number'] ) ? $func_args[1]['number'] : null; 445 | 446 | $id = isset( $func_args[0]['widget_id'] ) ? $func_args[0]['widget_id'] : null; 447 | $name = isset( $func_args[0]['widget_name'] ) ? $func_args[0]['widget_name'] : null; 448 | 449 | return new Widget_Invocation( 450 | $this, 451 | $this->incrementor, 452 | $this->database, 453 | $this->file_locator, 454 | $this->dependencies, 455 | array_merge( 456 | $invocation_args, 457 | compact( 'widget', 'number', 'id', 'name' ) 458 | ) 459 | ); 460 | }, 461 | function ( Widget_Invocation $invocation, $func_args ) use ( $function ) { 462 | // @todo This could also augment the $invocation with the widget $instance data. 463 | echo $this->output_annotator->get_before_invocation_placeholder_annotation( $invocation ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 464 | $return = call_user_func_array( $function, $func_args ); 465 | echo $this->output_annotator->get_after_invocation_placeholder_annotation( $invocation ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped 466 | return $return; 467 | } 468 | ); 469 | } 470 | } 471 | } 472 | 473 | -------------------------------------------------------------------------------- /includes/Output_Annotator.php: -------------------------------------------------------------------------------- 1 | $value ) { 143 | $this->$key = $value; 144 | } 145 | $this->dependencies = $dependencies; 146 | $this->incrementor = $incrementor; 147 | $this->block_recognizer = $block_recognizer; 148 | } 149 | 150 | /** 151 | * Set invocation watcher. 152 | * 153 | * @param Invocation_Watcher $invocation_watcher Invocation watcher. 154 | */ 155 | public function set_invocation_watcher( Invocation_Watcher $invocation_watcher ) { 156 | $this->invocation_watcher = $invocation_watcher; 157 | } 158 | 159 | /** 160 | * Get placeholder annotation pattern. 161 | * 162 | * Pattern assumes that regex delimiter will be '#'. 163 | * 164 | * Note that placeholder annotations do not include self-closing annotations because those are only added at the end 165 | * in `\Google\WP_Origination\Output_Annotator::finish()` if it is determined that they have not already been annotated. 166 | * 167 | * @return string Pattern. 168 | */ 169 | public function get_placeholder_annotation_pattern() { 170 | return sprintf( 171 | '', 172 | static::INVOCATION_ANNOTATION_PLACEHOLDER_TAG . '|' . static::DEPENDENCY_ANNOTATION_PLACEHOLDER_TAG . '|' . static::BLOCK_ANNOTATION_PLACEHOLDER_TAG . '|' . static::OEMBED_ANNOTATION_PLACEHOLDER_TAG 173 | ); 174 | } 175 | 176 | /** 177 | * Start. 178 | * 179 | * @param bool $lock_buffer Whether buffer is locked (can be flushed/erased/cancelled). 180 | */ 181 | public function start( $lock_buffer = true ) { 182 | 183 | // Output buffer so that Server-Timing headers can be sent, and prevent plugins from flushing it. 184 | ob_start( 185 | [ $this, 'finish' ], 186 | null, 187 | $lock_buffer ? 0 : PHP_OUTPUT_HANDLER_STDFLAGS 188 | ); 189 | 190 | // Prevent PHP Notice: ob_end_flush(): failed to send buffer. 191 | remove_action( 'shutdown', 'wp_ob_end_flush_all', 1 ); 192 | 193 | /* 194 | * Note that the PHP_INT_MAX-1 priority is used to prevent type annotations from being corrupted by user filters. 195 | * The filter annotations will still appear inside of any filter annotations because they get added at PHP_INT_MAX. 196 | */ 197 | $priority = PHP_INT_MAX - 1; // One less than max so that \Google\WP_Origination\Invocation_Watcher::after_all_hook_callbacks() can run after. 198 | add_filter( 'script_loader_tag', [ $this, 'add_enqueued_script_annotation' ], $priority, 2 ); 199 | add_filter( 'style_loader_tag', [ $this, 'add_enqueued_style_annotation' ], $priority, 2 ); 200 | add_filter( 'render_block', [ $this, 'add_static_block_annotation' ], $priority, 2 ); 201 | add_filter( 'embed_handler_html', [ $this, 'add_oembed_annotation' ], $priority, 3 ); 202 | add_filter( 'embed_oembed_html', [ $this, 'add_oembed_annotation' ], $priority, 3 ); 203 | } 204 | 205 | /** 206 | * Print annotation placeholder before an printing invoked callback. 207 | * 208 | * @param Invocation $invocation Invocation. 209 | * @return string Before placeholder annotation HTML comment. 210 | */ 211 | public function get_before_invocation_placeholder_annotation( Invocation $invocation ) { 212 | return sprintf( '', static::INVOCATION_ANNOTATION_PLACEHOLDER_TAG, $invocation->index ); 213 | } 214 | 215 | /** 216 | * Print annotation placeholder after an printing invoked callback. 217 | * 218 | * @param Invocation $invocation Invocation. 219 | * @return string After placeholder annotation HTML comment. 220 | */ 221 | public function get_after_invocation_placeholder_annotation( Invocation $invocation ) { 222 | return sprintf( '', static::INVOCATION_ANNOTATION_PLACEHOLDER_TAG, $invocation->index ); 223 | } 224 | 225 | /** 226 | * Add annotation for an enqueued dependency that was printed. 227 | * 228 | * @param string $tag HTML tag. 229 | * @param string $handle Handle. 230 | * @param string $type Type, such as 'enqueued_script' or 'enqueued_style'. 231 | * @param string $registry Registry name, such as 'wp_scripts' or 'wp_styles'. 232 | * @return string HTML tag with annotation. 233 | */ 234 | public function add_enqueued_dependency_annotation( $tag, $handle, $type, $registry ) { 235 | // Abort if filter has been applied without passing all required arguments. 236 | if ( ! $handle ) { 237 | return $tag; 238 | } 239 | 240 | /* 241 | * Abort if this is a stylesheet that has conditional comments, as adding comments will cause them to be nested, 242 | * which is not allowed for comments. Also, styles that are inside conditional comments are mostly pointless 243 | * to identify the source of since they area dead and won't impact the page for any modern browsers. 244 | */ 245 | if ( 'wp_styles' === $registry && wp_styles()->get_data( $handle, 'conditional' ) ) { 246 | return $tag; 247 | } 248 | 249 | $index = $this->incrementor->next(); 250 | 251 | $this->pending_dependency_annotations[ $index ] = compact( 'handle', 'type', 'registry' ); 252 | 253 | return ( 254 | sprintf( '', static::DEPENDENCY_ANNOTATION_PLACEHOLDER_TAG, $index ) 255 | . $tag 256 | . sprintf( '', static::DEPENDENCY_ANNOTATION_PLACEHOLDER_TAG, $index ) 257 | ); 258 | } 259 | 260 | /** 261 | * Add annotation for an enqueued script that was printed. 262 | * 263 | * @param string $tag Script tag. 264 | * @param string $handle Handle. 265 | * @return string Script tag. 266 | */ 267 | public function add_enqueued_script_annotation( $tag, $handle = null ) { 268 | return $this->add_enqueued_dependency_annotation( $tag, $handle, 'enqueued_script', 'wp_scripts' ); 269 | } 270 | 271 | /** 272 | * Add annotation for an enqueued style that was printed. 273 | * 274 | * @param string $tag Style tag. 275 | * @param string $handle Handle. 276 | * @return string Style tag. 277 | */ 278 | public function add_enqueued_style_annotation( $tag, $handle = null ) { 279 | return $this->add_enqueued_dependency_annotation( $tag, $handle, 'enqueued_style', 'wp_styles' ); 280 | } 281 | 282 | /** 283 | * Annotate static block. 284 | * 285 | * @param string $block_content The block content about to be appended. 286 | * @param array $block The full block, including name and attributes. 287 | * @return string Block content. 288 | */ 289 | public function add_static_block_annotation( $block_content, $block ) { 290 | $is_registered = \WP_Block_Type_Registry::get_instance()->is_registered( $block['blockName'] ); 291 | 292 | if ( empty( $block['blockName'] ) ) { 293 | return $block_content; 294 | } 295 | 296 | // Skip annotating dynamic blocks since they'll be annotated separately. 297 | if ( $is_registered && \WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] )->is_dynamic() ) { 298 | return $block_content; 299 | } 300 | 301 | $index = $this->incrementor->next(); 302 | 303 | unset( $block['innerHTML'], $block['innerBlocks'], $block['innerContent'] ); 304 | $this->pending_block_annotations[ $index ] = $block; 305 | 306 | // @todo How do we determine the source for static blocks that lack render callbacks? Try using the namespace to see if it matches a plugin slug? 307 | return ( 308 | sprintf( '', static::BLOCK_ANNOTATION_PLACEHOLDER_TAG, $index ) 309 | . $block_content 310 | . sprintf( '', static::BLOCK_ANNOTATION_PLACEHOLDER_TAG, $index ) 311 | ); 312 | } 313 | 314 | /** 315 | * Annotate oEmbed responses. 316 | * 317 | * @param string $output Embed output. 318 | * @param string $url URL. 319 | * @param array $attributes Attributes. 320 | * 321 | * @return string Output. 322 | */ 323 | public function add_oembed_annotation( $output, $url, $attributes ) { 324 | $index = $this->incrementor->next(); 325 | 326 | $this->pending_oembed_annotations[ $index ] = array_merge( 327 | compact( 'url', 'attributes' ), 328 | [ 329 | 'internal' => current_filter() === 'embed_handler_html', 330 | ] 331 | ); 332 | 333 | return ( 334 | sprintf( '', static::OEMBED_ANNOTATION_PLACEHOLDER_TAG, $index ) 335 | . $output 336 | . sprintf( '', static::OEMBED_ANNOTATION_PLACEHOLDER_TAG, $index ) 337 | ); 338 | } 339 | 340 | /** 341 | * Purge annotations in start tag. 342 | * 343 | * @param array $start_tag_matches Start tag matches. 344 | * @return string Start tag. 345 | */ 346 | public function purge_annotations_in_start_tag( $start_tag_matches ) { 347 | $attributes = preg_replace_callback( 348 | '#' . static::get_placeholder_annotation_pattern() . '#', 349 | '__return_empty_string', 350 | $start_tag_matches['attrs'] 351 | ); 352 | 353 | return '<' . $start_tag_matches['name'] . $attributes . '>'; 354 | } 355 | 356 | /** 357 | * Hydrate an placeholder annotation. 358 | * 359 | * @param int $index Index. 360 | * @param string $type Type. 361 | * @param bool $closing Closing. 362 | * @return string Hydrated annotation. 363 | */ 364 | public function hydrate_placeholder_annotation( $index, $type, $closing ) { 365 | if ( self::DEPENDENCY_ANNOTATION_PLACEHOLDER_TAG === $type ) { 366 | return $this->hydrate_dependency_placeholder_annotation( $index, $closing ); 367 | } elseif ( self::INVOCATION_ANNOTATION_PLACEHOLDER_TAG === $type ) { 368 | return $this->hydrate_invocation_placeholder_annotation( $index, $closing ); 369 | } elseif ( self::BLOCK_ANNOTATION_PLACEHOLDER_TAG === $type ) { 370 | return $this->hydrate_block_placeholder_annotation( $index, $closing ); 371 | } elseif ( self::OEMBED_ANNOTATION_PLACEHOLDER_TAG === $type ) { 372 | return $this->hydrate_oembed_placeholder_annotation( $index, $closing ); 373 | } 374 | return ''; 375 | } 376 | 377 | /** 378 | * Hydrate an dependency placeholder annotation. 379 | * 380 | * @param int $index Index. 381 | * @param bool $closing Closing. 382 | * @return string Hydrated annotation. 383 | */ 384 | protected function hydrate_dependency_placeholder_annotation( $index, $closing ) { 385 | if ( ! isset( $this->pending_dependency_annotations[ $index ] ) ) { 386 | return ''; 387 | } 388 | 389 | // Determine invocations for this dependency and store for the closing annotation comment. 390 | if ( ! isset( $this->pending_dependency_annotations[ $index ]['invocations'] ) ) { 391 | $this->pending_dependency_annotations[ $index ]['invocations'] = wp_list_pluck( 392 | $this->dependencies->get_dependency_enqueueing_invocations( 393 | $this->invocation_watcher, 394 | $this->pending_dependency_annotations[ $index ]['registry'], 395 | $this->pending_dependency_annotations[ $index ]['handle'] 396 | ), 397 | 'index' 398 | ); 399 | } 400 | 401 | // Remove annotation entirely if there are no invocations (which shouldn't happen). 402 | if ( empty( $this->pending_dependency_annotations[ $index ]['invocations'] ) ) { 403 | unset( $this->pending_dependency_annotations[ $index ] ); 404 | return ''; 405 | } 406 | 407 | $data = compact( 'index' ); 408 | if ( ! $closing ) { 409 | $data = array_merge( 410 | $data, 411 | [ 412 | 'type' => $this->pending_dependency_annotations[ $index ]['type'], 413 | 'invocations' => $this->pending_dependency_annotations[ $index ]['invocations'], 414 | ] 415 | ); 416 | } 417 | 418 | return $this->get_annotation_comment( $data, $closing ? self::CLOSE_ANNOTATION : self::OPEN_ANNOTATION ); 419 | } 420 | 421 | /** 422 | * Hydrate a (static) block placeholder annotation. 423 | * 424 | * Note that dynamic blocks are annotated as invocations. 425 | * 426 | * @param int $index Index. 427 | * @param bool $closing Closing. 428 | * @return string Hydrated annotation. 429 | */ 430 | protected function hydrate_block_placeholder_annotation( $index, $closing ) { 431 | if ( ! isset( $this->pending_block_annotations[ $index ] ) ) { 432 | return ''; 433 | } 434 | 435 | $block = $this->pending_block_annotations[ $index ]; 436 | 437 | $data = compact( 'index' ); 438 | if ( ! $closing ) { 439 | $data = array_merge( 440 | $data, 441 | [ 442 | 'type' => 'block', 443 | 'name' => $block['blockName'], 444 | 'dynamic' => false, 445 | ] 446 | ); 447 | if ( ! empty( $block['attrs'] ) ) { 448 | $data['attributes'] = $block['attrs']; 449 | } 450 | 451 | $source = $this->block_recognizer->identify( strtok( $block['blockName'], '/' ) ); 452 | if ( $source ) { 453 | $data['source'] = $source; 454 | } 455 | } 456 | 457 | return $this->get_annotation_comment( $data, $closing ? self::CLOSE_ANNOTATION : self::OPEN_ANNOTATION ); 458 | } 459 | 460 | /** 461 | * Hydrate an oEmbed placeholder annotation. 462 | * 463 | * @param int $index Index. 464 | * @param bool $closing Closing. 465 | * @return string Hydrated annotation. 466 | */ 467 | protected function hydrate_oembed_placeholder_annotation( $index, $closing ) { 468 | if ( ! isset( $this->pending_oembed_annotations[ $index ] ) ) { 469 | return ''; 470 | } 471 | 472 | $data = compact( 'index' ); 473 | if ( ! $closing ) { 474 | $data = array_merge( 475 | $data, 476 | [ 'type' => 'oembed' ], 477 | $this->pending_oembed_annotations[ $index ] 478 | ); 479 | } 480 | 481 | return $this->get_annotation_comment( $data, $closing ? self::CLOSE_ANNOTATION : self::OPEN_ANNOTATION ); 482 | } 483 | 484 | /** 485 | * Hydrate a dependency placeholder annotation. 486 | * 487 | * @param int $index Index. 488 | * @param bool $closing Closing. 489 | * @return string Hydrated annotation. 490 | */ 491 | protected function hydrate_invocation_placeholder_annotation( $index, $closing ) { 492 | $invocation = $this->invocation_watcher->get_invocation_by_index( $index ); 493 | if ( ! $invocation ) { 494 | return ''; 495 | } 496 | 497 | $data = compact( 'index' ); 498 | if ( ! $closing ) { 499 | $data = array_merge( $data, $invocation->data() ); 500 | 501 | // Include queries if requested. 502 | if ( $this->show_queries ) { 503 | $queries = $invocation->queries( true ); 504 | if ( ! empty( $queries ) ) { 505 | $data['queries'] = $queries; 506 | } 507 | } 508 | } 509 | 510 | return $this->get_annotation_comment( $data, $closing ? self::CLOSE_ANNOTATION : self::OPEN_ANNOTATION ); 511 | } 512 | 513 | /** 514 | * Get annotation comment. 515 | * 516 | * @param array $data Data. 517 | * @param int $type Comment type. Either OPEN_ANNOTATION, CLOSE_ANNOTATION, EMPTY_ANNOTATION. 518 | * @return string HTML comment. 519 | */ 520 | public function get_annotation_comment( array $data, $type = self::OPEN_ANNOTATION ) { 521 | 522 | if ( ! in_array( $type, [ self::OPEN_ANNOTATION, self::CLOSE_ANNOTATION, self::SELF_CLOSING_ANNOTATION ], true ) ) { 523 | _doing_it_wrong( __METHOD__, esc_html__( 'Wrong annotation type.', 'origination' ), '0.1' ); 524 | } 525 | 526 | // Escape double-hyphens in comment content. 527 | $json = str_replace( 528 | '--', 529 | '\u2D\u2D', 530 | wp_json_encode( $data, JSON_UNESCAPED_SLASHES ) 531 | ); 532 | 533 | return sprintf( 534 | '', 535 | self::CLOSE_ANNOTATION === $type ? '/' : '', 536 | static::ANNOTATION_TAG, 537 | $json, 538 | self::SELF_CLOSING_ANNOTATION === $type ? '/' : '' 539 | ); 540 | } 541 | 542 | /** 543 | * Parse data out of origination annotation comment text. 544 | * 545 | * @param string|\DOMComment $comment Comment. 546 | * @return null|array { 547 | * Parsed comment. Returns null on parse error. 548 | * 549 | * @type bool $closing Closing. 550 | * @type array $data Data. 551 | * } 552 | */ 553 | public function parse_annotation_comment( $comment ) { 554 | if ( $comment instanceof \DOMComment ) { 555 | $comment = $comment->nodeValue; 556 | } 557 | $pattern = sprintf( 558 | '#^ (?P/)?%s (?P{.+}) (?P/)?$#s', 559 | preg_quote( static::ANNOTATION_TAG, '#' ) 560 | ); 561 | if ( ! preg_match( $pattern, $comment, $matches ) ) { 562 | return null; 563 | } 564 | $closing = ! empty( $matches['closing'] ); 565 | $self_closing = ! empty( $matches['self_closing'] ); 566 | $data = json_decode( $matches['json'], true ); 567 | if ( ! is_array( $data ) ) { 568 | return null; 569 | } 570 | return compact( 'closing', 'data', 'self_closing' ); 571 | } 572 | 573 | /** 574 | * Get the annotation stack for a given DOM node. 575 | * 576 | * @param \DOMNode $node Target DOM node. 577 | * @return array[] Stack of annotation comment data. 578 | * @throws \Exception If comments are found to be malformed. 579 | */ 580 | public function get_node_annotation_stack( \DOMNode $node ) { 581 | $stack = []; 582 | $xpath = new \DOMXPath( $node->ownerDocument ); 583 | 584 | $open_prefix = sprintf( ' %s ', self::ANNOTATION_TAG ); 585 | $close_prefix = sprintf( ' /%s ', self::ANNOTATION_TAG ); 586 | 587 | $expr = sprintf( 588 | 'preceding::comment()[ starts-with( ., "%s" ) or starts-with( ., "%s" ) ]', 589 | $open_prefix, 590 | $close_prefix 591 | ); 592 | 593 | foreach ( $xpath->query( $expr, $node ) as $comment ) { 594 | $parsed_comment = $this->parse_annotation_comment( $comment ); 595 | if ( ! is_array( $parsed_comment ) ) { 596 | continue; 597 | } 598 | 599 | if ( $parsed_comment['closing'] ) { 600 | $popped = array_pop( $stack ); 601 | if ( $popped['index'] !== $parsed_comment['data']['index'] ) { 602 | throw new \Exception( sprintf( 'Comment stack mismatch: saw closing comment %1$d but expected %2$d.', $parsed_comment['data']['index'], $popped['index'] ) ); 603 | } 604 | } else { 605 | array_push( $stack, $parsed_comment['data'] ); 606 | } 607 | } 608 | return $stack; 609 | } 610 | 611 | /** 612 | * Finalize annotations. 613 | * 614 | * Given this HTML in the buffer: 615 | * 616 | * data-hello=world data-second="A>B">. 617 | * 618 | * The returned string should be: 619 | * 620 | * . 621 | * 622 | * @param string $buffer Buffer. 623 | * @return string Processed buffer. 624 | */ 625 | public function finish( $buffer ) { 626 | $placeholder_annotation_pattern = static::get_placeholder_annotation_pattern(); 627 | 628 | $this->show_queries = call_user_func( $this->can_show_queries_callback ); 629 | 630 | // Make sure that all open invocations get closed, which can happen when an exit is done in a hook callback. 631 | while ( true ) { 632 | $invocation = $this->invocation_watcher->pop_invocation_stack(); 633 | if ( ! $invocation ) { 634 | break; 635 | } 636 | if ( $invocation->can_output() ) { 637 | $buffer .= $this->get_after_invocation_placeholder_annotation( $invocation ); 638 | } 639 | } 640 | 641 | // Match all start tags that have attributes. 642 | $pattern = join( 643 | '', 644 | [ 645 | '#<', 646 | '(?P[a-zA-Z0-9_\-]+)', 647 | '(?P\s', 648 | '(?:' . $placeholder_annotation_pattern . '|[^<>"\']+|"[^"]*+"|\'[^\']*+\')*+', // Attribute tokens, plus annotations. 649 | ')>#s', 650 | ] 651 | ); 652 | 653 | $buffer = preg_replace_callback( 654 | $pattern, 655 | [ $this, 'purge_annotations_in_start_tag' ], 656 | $buffer 657 | ); 658 | 659 | if ( ! preg_match_all( '#' . $placeholder_annotation_pattern . '#', $buffer, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER ) ) { 660 | return $buffer; 661 | } 662 | 663 | // Determine all invocations that have been annotated. 664 | $closing_pos = 1; 665 | $type_pos = 2; 666 | $index_pos = 3; 667 | foreach ( $matches as $match ) { 668 | $type = $match[ $type_pos ][0]; 669 | if ( self::INVOCATION_ANNOTATION_PLACEHOLDER_TAG === $type ) { 670 | $index = $match[ $index_pos ][0]; 671 | $this->invocation_watcher->get_invocation_by_index( $index )->annotated = true; 672 | } 673 | } 674 | 675 | // Now hydrate the matching placeholder annotations. 676 | $offset_differential = 0; 677 | while ( ! empty( $matches ) ) { 678 | $match = array_shift( $matches ); 679 | $length = strlen( $match[0][0] ); 680 | $offset = $match[0][1]; 681 | 682 | $hydrated_annotation = $this->hydrate_placeholder_annotation( 683 | intval( $match[ $index_pos ][0] ), 684 | $match[ $type_pos ][0], 685 | ! empty( $match[ $closing_pos ][0] ) 686 | ); 687 | 688 | // Splice the hydrated annotation into the buffer to replace the placeholder annotation. 689 | $buffer = substr_replace( 690 | $buffer, 691 | $hydrated_annotation, 692 | $offset + $offset_differential, 693 | $length 694 | ); 695 | 696 | // Update the offset differential based on the difference in length of the hydration. 697 | $offset_differential += ( strlen( $hydrated_annotation ) - $length ); 698 | } 699 | 700 | // Finally, amend the response with all remaining invocations that have not been annotated. These do not wrap any output. 701 | foreach ( $this->invocation_watcher->get_invocations() as $invocation ) { 702 | if ( ! $invocation->annotated ) { 703 | $invocation->annotated = true; 704 | 705 | $buffer .= $this->get_annotation_comment( $invocation->data(), self::SELF_CLOSING_ANNOTATION ); 706 | } 707 | } 708 | 709 | return $buffer; 710 | } 711 | 712 | } 713 | -------------------------------------------------------------------------------- /includes/Plugin.php: -------------------------------------------------------------------------------- 1 | start(); 130 | 131 | return true; 132 | } 133 | 134 | /** 135 | * Plugin constructor. 136 | * 137 | * @since 0.1.0 138 | * 139 | * @param string $main_file Absolute path to the plugin main file. This is primarily useful for plugins that subclass this class. 140 | */ 141 | public function __construct( $main_file ) { 142 | $this->main_file = $main_file; 143 | } 144 | 145 | /** 146 | * Gets the plugin basename, which consists of the plugin directory name and main file name. 147 | * 148 | * @since 0.1.0 149 | * 150 | * @return string Plugin basename. 151 | */ 152 | public function basename() { 153 | return plugin_basename( $this->main_file ); 154 | } 155 | 156 | /** 157 | * Gets the absolute path for a path relative to the plugin directory. 158 | * 159 | * @since 0.1.0 160 | * 161 | * @param string $relative_path Optional. Relative path. Default '/'. 162 | * @return string Absolute path. 163 | */ 164 | public function path( $relative_path = '/' ) { 165 | return plugin_dir_path( $this->main_file ) . ltrim( $relative_path, '/' ); 166 | } 167 | 168 | /** 169 | * Gets the full URL for a path relative to the plugin directory. 170 | * 171 | * @since 0.1.0 172 | * 173 | * @param string $relative_path Optional. Relative path. Default '/'. 174 | * @return string Full URL. 175 | */ 176 | public function url( $relative_path = '/' ) { 177 | return plugin_dir_url( $this->main_file ) . ltrim( $relative_path, '/' ); 178 | } 179 | 180 | /** 181 | * Determine whether origination should run for the current request. 182 | * 183 | * @return bool 184 | */ 185 | public function should_run() { 186 | return ( 187 | defined( 'WP_DEBUG' ) 188 | && 189 | WP_DEBUG 190 | && 191 | isset( $_GET['origination'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.NoNonceVerification 192 | ); 193 | } 194 | 195 | /** 196 | * Construct classes used by the plugin. 197 | * 198 | * @global \wpdb $wpdb 199 | */ 200 | public function init() { 201 | global $wpdb; 202 | 203 | $this->incrementor = new Incrementor(); 204 | 205 | $this->file_locator = new File_Locator(); 206 | 207 | $this->block_recognizer = new Block_Recognizer(); 208 | 209 | $this->dependencies = new Dependencies(); 210 | 211 | // @todo Pass options for verbosity, which filters to wrap, whether to output annotations that have no output, etc. 212 | $this->output_annotator = new Output_Annotator( 213 | $this->dependencies, 214 | $this->incrementor, 215 | $this->block_recognizer, 216 | [ 217 | 'can_show_queries_callback' => function() { 218 | return current_user_can( $this->show_queries_cap ); 219 | }, 220 | ] 221 | ); 222 | 223 | $this->database = new Database( $wpdb ); 224 | 225 | $this->hook_wrapper = new Hook_Wrapper(); 226 | 227 | $this->invocation_watcher = new Invocation_Watcher( 228 | $this->file_locator, 229 | $this->output_annotator, 230 | $this->dependencies, 231 | $this->database, 232 | $this->incrementor, 233 | $this->hook_wrapper 234 | ); 235 | 236 | $this->output_annotator->set_invocation_watcher( $this->invocation_watcher ); 237 | 238 | $this->server_timing_headers = new Server_Timing_Headers( $this->invocation_watcher ); 239 | } 240 | 241 | /** 242 | * Run. 243 | */ 244 | public function start() { 245 | $wp_debug = defined( 'WP_DEBUG' ) && WP_DEBUG; 246 | 247 | if ( ! isset( $_GET['origination'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.NoNonceVerification 248 | return; 249 | } 250 | 251 | if ( ! $wp_debug ) { 252 | wp_die( 253 | esc_html__( 'WP_DEBUG must currently be enabled to generate Origination data.', 'origination' ), 254 | esc_html__( 'WP_DEBUG Required', 'origination' ), 255 | array( 256 | 'response' => 400, 257 | ) 258 | ); 259 | } 260 | 261 | $this->init(); 262 | 263 | $this->invocation_watcher->start(); 264 | $this->output_annotator->start(); 265 | add_action( 'shutdown', [ $this->server_timing_headers, 'send' ] ); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /includes/Server_Timing_Headers.php: -------------------------------------------------------------------------------- 1 | invocation_watcher = $invocation_watcher; 32 | } 33 | 34 | /** 35 | * Send the headers for the processed invocations on the invocation watcher. 36 | */ 37 | public function send() { 38 | $entity_timings = []; 39 | 40 | foreach ( $this->invocation_watcher->get_invocations() as $invocation ) { 41 | $hook_duration = $invocation->duration( true ); 42 | 43 | $file_location = $invocation->file_location(); 44 | if ( $file_location ) { 45 | $entity_key = sprintf( '%s:%s', $file_location['type'], $file_location['name'] ); 46 | } else { 47 | $entity_key = sprintf( '%s:%s', 'php', $invocation->reflection->getExtensionName() ); 48 | } 49 | 50 | if ( ! isset( $entity_timings[ $entity_key ] ) ) { 51 | $entity_timings[ $entity_key ] = 0.0; 52 | } 53 | $entity_timings[ $entity_key ] += $hook_duration; 54 | } 55 | 56 | $round_to_fourth_precision = function( $timing ) { 57 | return round( $timing, 4 ); 58 | }; 59 | 60 | foreach ( array_map( $round_to_fourth_precision, $entity_timings ) as $entity => $timing ) { 61 | $value = strtok( $entity, ':' ); 62 | $value .= sprintf( ';desc="%s"', $entity ); 63 | $value .= sprintf( ';dur=%f', $timing * 1000 ); 64 | header( sprintf( 'Server-Timing: %s', $value ), false ); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /includes/Shortcode_Invocation.php: -------------------------------------------------------------------------------- 1 | 'shortcode', 57 | 'tag' => $this->tag, 58 | 'attributes' => $this->attributes, 59 | ], 60 | $data 61 | ); 62 | 63 | return $data; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /includes/Widget_Invocation.php: -------------------------------------------------------------------------------- 1 | 'widget', 69 | 'id_base' => $this->widget ? $this->widget->id_base : null, 70 | 'number' => $this->number, 71 | 'id' => $this->id, 72 | 'name' => $this->name, 73 | ], 74 | $data 75 | ); 76 | 77 | if ( $this->widget instanceof \WP_Widget && $this->number ) { 78 | $instances = $this->widget->get_settings(); 79 | if ( isset( $instances[ $this->number ] ) ) { 80 | $data['instance'] = $instances[ $this->number ]; 81 | } 82 | } 83 | 84 | return $data; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /includes/Wrapped_Callback.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 60 | $this->invocation_watcher = $invocation_watcher; 61 | $this->invocation_creator = $invocation_creator; 62 | $this->invocation_invoker = $invocation_invoker; 63 | } 64 | 65 | /** 66 | * Invoke wrapped callback. 67 | * 68 | * @return mixed 69 | */ 70 | public function __invoke() { 71 | $parent = $this->invocation_watcher->get_parent_invocation(); 72 | 73 | try { 74 | $source = new Calling_Reflection( $this->callback ); 75 | $context = [ 76 | 'source_file' => $source->get_file_name(), 77 | 'reflection' => $source->get_callback_reflection(), 78 | 'function_name' => $source->get_name(), 79 | ]; 80 | } catch ( \Exception $e ) { 81 | $context['exception'] = $e->getMessage(); 82 | } 83 | 84 | $invocation_args = array_merge( 85 | compact( 'parent' ), 86 | [ 'function' => $this->callback ], 87 | $context 88 | ); 89 | 90 | $invocation = call_user_func( $this->invocation_creator, $invocation_args, func_get_args() ); 91 | 92 | if ( $parent ) { 93 | $parent->children[] = $invocation; 94 | } 95 | 96 | $this->invocation_watcher->push_invocation_stack( $invocation ); 97 | 98 | $return = call_user_func( $this->invocation_invoker, $invocation, func_get_args() ); 99 | 100 | $this->invocation_watcher->pop_invocation_stack(); 101 | $invocation->finalize(); 102 | 103 | return $return; 104 | } 105 | 106 | /** 107 | * Offset set. 108 | * 109 | * @param mixed $offset Offset. 110 | * @param mixed $value Value. 111 | */ 112 | public function offsetSet( $offset, $value ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid 113 | if ( ! is_array( $this->callback ) ) { 114 | return; 115 | } 116 | if ( is_null( $offset ) ) { 117 | $this->callback[] = $value; 118 | } else { 119 | $this->callback[ $offset ] = $value; 120 | } 121 | } 122 | 123 | /** 124 | * Offset exists. 125 | * 126 | * @param mixed $offset Offset. 127 | * @return bool Exists. 128 | */ 129 | public function offsetExists( $offset ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid 130 | if ( ! is_array( $this->callback ) ) { 131 | return false; 132 | } 133 | return isset( $this->callback[ $offset ] ); 134 | } 135 | 136 | /** 137 | * Offset unset. 138 | * 139 | * @param mixed $offset Offset. 140 | */ 141 | public function offsetUnset( $offset ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid 142 | if ( ! is_array( $this->callback ) ) { 143 | return; 144 | } 145 | unset( $this->callback[ $offset ] ); 146 | } 147 | 148 | /** 149 | * Offset get. 150 | * 151 | * @param mixed $offset Offset. 152 | * @return mixed|null Value. 153 | */ 154 | public function offsetGet( $offset ) { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid 155 | if ( is_array( $this->callback ) && isset( $this->callback[ $offset ] ) ) { 156 | return $this->callback[ $offset ]; 157 | } 158 | return null; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /js/identify-node-sources.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For a given DOM node, obtain the stack trace of annotations to reveal the sources for where the node came from. 3 | * 4 | * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 5 | * @copyright 2019 Google LLC 6 | * 7 | * Copyright 2019 Google LLC 8 | * 9 | * Licensed under the Apache License, Version 2.0 (the "License"); 10 | * you may not use this file except in compliance with the License. 11 | * You may obtain a copy of the License at 12 | * 13 | * https://www.apache.org/licenses/LICENSE-2.0 14 | * 15 | * Unless required by applicable law or agreed to in writing, software 16 | * distributed under the License is distributed on an "AS IS" BASIS, 17 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | * See the License for the specific language governing permissions and 19 | * limitations under the License. 20 | * 21 | * @param {Node} node DOM Node. 22 | * @returns {Object[]} Annotations. 23 | */ 24 | export default function identifyNodeSources( node ) { 25 | const openCommentPrefix = ' origination '; 26 | const closeCommentPrefix = ' /origination '; 27 | 28 | const invocations = {}; 29 | const expression = ` 30 | preceding::comment()[ 31 | starts-with( ., "${openCommentPrefix}" ) 32 | or 33 | starts-with( ., "${closeCommentPrefix}" ) 34 | ]`; 35 | const xPathResult = document.evaluate( expression, node, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ); 36 | const annotationStack = []; 37 | const rawAnnotationData = []; 38 | 39 | // Capture the data for the annotations. 40 | for ( let i = 0; i < xPathResult.snapshotLength; i++ ) { 41 | let commentText = xPathResult.snapshotItem( i ).nodeValue; 42 | 43 | const isOpen = commentText.startsWith( openCommentPrefix ); 44 | const isSelfClosing = commentText.endsWith( '/' ); 45 | 46 | commentText = commentText.substr( isOpen ? openCommentPrefix.length : closeCommentPrefix.length ); 47 | if ( isSelfClosing ) { 48 | commentText = commentText.replace( /\/$/, '' ); 49 | } 50 | 51 | const data = JSON.parse( commentText ); 52 | if ( typeof data !== 'object' || ! data.index ) { 53 | throw new Error( `Missing index: ${commentText}` ); 54 | } 55 | if ( isOpen ) { 56 | invocations[ data.index ] = data; 57 | } 58 | rawAnnotationData.push( { commentText, isOpen, isSelfClosing, data } ); 59 | } 60 | 61 | // Construct the final call stack. 62 | for ( const annotation of rawAnnotationData ) { 63 | const { isOpen, data, commentText, isSelfClosing } = annotation; 64 | 65 | if ( isOpen ) { 66 | if ( data.invocations ) { 67 | for ( const index of data.invocations ) { 68 | if ( ! invocations[ index ] ) { 69 | throw new Error( `No existing invocation index for: ${index}.` ); 70 | } 71 | annotationStack.push( invocations[ index ] ); 72 | } 73 | } else { 74 | annotationStack.push( data ); 75 | } 76 | 77 | // Turn around and pop right away when self-closing. 78 | if ( isSelfClosing ) { 79 | annotationStack.pop(); 80 | } 81 | } else { 82 | if ( ! invocations[ data.index ] ) { 83 | throw new Error( `Unable to find invocation data for index: ${data.index}` ); 84 | } 85 | Object.assign( data, invocations[ data.index ] ); 86 | 87 | if ( data.invocations ) { 88 | for ( const index of [...data.invocations].reverse() ) { 89 | const popped = annotationStack.pop(); 90 | if ( index !== popped.index ) { 91 | throw new Error( `Unexpected closing annotation comment for ref: ${commentText}. Expected index: ${index}.` ); 92 | } 93 | } 94 | } else { 95 | const popped = annotationStack.pop(); 96 | if ( data.index !== popped.index ) { 97 | throw new Error( `Unexpected closing annotation comment: ${commentText}. Expected index: ${popped.index}.` ); 98 | } 99 | } 100 | } 101 | } 102 | return annotationStack; 103 | } 104 | -------------------------------------------------------------------------------- /origination.php: -------------------------------------------------------------------------------- 1 | add( 47 | 'php_version', 48 | sprintf( 49 | /* translators: 1: required PHP version, 2: currently used PHP version */ 50 | __( 'Plugin requires at least PHP version %1$s; your site is currently running on PHP %2$s.', 'origination' ), 51 | '7.0', 52 | phpversion() 53 | ) 54 | ); 55 | } 56 | 57 | if ( version_compare( get_bloginfo( 'version' ), '5.2', '<' ) ) { 58 | $load_errors->add( 59 | 'wp_version', 60 | sprintf( 61 | /* translators: 1: required WordPress version, 2: currently used WordPress version */ 62 | __( 'Plugin requires at least WordPress version %1$s; your site is currently running on WordPress %2$s.', 'origination' ), 63 | '5.2', 64 | get_bloginfo( 'version' ) 65 | ) 66 | ); 67 | } 68 | 69 | // DIST_REMOVED. 70 | if ( ! file_exists( __DIR__ . '/vendor/autoload.php' ) ) { 71 | $load_errors->add( 72 | 'composer_install', 73 | sprintf( 74 | /* translators: %s is composer install command */ 75 | __( 'Plugin appears to be running from source and requires %s to complete the plugin\'s installation.', 'origination' ), 76 | 'composer install' 77 | ) 78 | ); 79 | } 80 | 81 | if ( ! empty( $load_errors->errors ) ) { 82 | $_google_wp_origination_load_errors = $load_errors; 83 | add_action( 'admin_notices', '_google_wp_show_dependency_errors_admin_notice' ); 84 | if ( defined( 'WP_CLI' ) && WP_CLI ) { 85 | $messages = array( __( 'Origination plugin unable to load.', 'origination' ) ); 86 | foreach ( array_keys( $load_errors->errors ) as $error_code ) { 87 | $messages[] = $load_errors->get_error_message( $error_code ); 88 | } 89 | $message = implode( ' ', $messages ); 90 | $message = str_replace( array( '', '' ), '`', $message ); 91 | WP_CLI::warning( $message ); 92 | } 93 | } else { 94 | require_once __DIR__ . '/vendor/autoload.php'; 95 | call_user_func( array( 'Google\\WP_Origination\\Plugin', 'load' ), WP_ORIGINATION_PLUGIN_FILE ); 96 | } 97 | } 98 | 99 | /** 100 | * Displays an admin notice about why the plugin is unable to load. 101 | * 102 | * @since 0.1.0 103 | * @global WP_Error $_google_wp_origination_load_errors 104 | */ 105 | function _google_wp_show_dependency_errors_admin_notice() { 106 | global $_google_wp_origination_load_errors; 107 | ?> 108 |
109 |

110 | 111 | errors ) as $error_code ) : ?> 112 | get_error_message( $error_code ) ); ?> 113 | 114 |

115 |
116 |