├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ └── gardening.yml ├── .gitignore ├── .phpcs.xml.dist ├── .travis.yml ├── LICENSE ├── bin ├── deploy-to-plugin-directory.sh └── install-wp-tests.sh ├── composer.json ├── composer.lock ├── concat-css.php ├── concat-js.php ├── cssmin ├── Colors.php ├── Minifier.php ├── Utils.php └── cssmin.php ├── dependency-path-mapping.php ├── page-optimize.php ├── phpunit.xml.dist ├── readme.txt ├── service.php ├── settings.php ├── tests ├── bootstrap.php ├── data │ └── url-to-file-mapping │ │ ├── content │ │ ├── exists │ │ └── plugins │ │ │ └── exists │ │ ├── plugins │ │ └── exists │ │ └── site │ │ ├── content │ │ ├── exists │ │ └── plugins │ │ │ └── exists │ │ ├── exists │ │ └── plugins │ │ └── exists └── test_URL_to_file_mapping.php └── utils.php /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # WordPress Coding Standards 5 | # https://make.wordpress.org/core/handbook/coding-standards/ 6 | 7 | root = true 8 | 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = tab 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | [*.yml] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | #### Steps to reproduce 4 | 5 | 1. Starting at URL: 6 | 2. 7 | 3. 8 | 4. 9 | 10 | #### What I expected 11 | 12 | #### What happened instead 13 | 14 | #### Browser / OS version 15 | 16 | #### Screenshot / Video 17 | 18 | #### Context / Source 19 | 20 | 26 | 27 | 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Helps us improve our product! 3 | labels: "Needs triage, [Type] Bug" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ### Thanks for contributing! 9 | 10 | Please write a clear title, then fill in the fields below and submit. 11 | 12 | Please **do not** link to image hosting services such as Cloudup, Droplr, Imgur, etc… 13 | Instead, directly embed screenshot(s) or recording(s) in any of the text areas below: click, then drag and drop. 14 | 15 | If you have any questions, please ask in: `#dotcom-triage-alerts`. 16 | - type: markdown 17 | attributes: 18 | value: | 19 | --- 20 | ## Core Information 21 | - type: textarea 22 | id: summary 23 | attributes: 24 | label: Quick summary 25 | - type: textarea 26 | id: steps 27 | attributes: 28 | label: Steps to reproduce 29 | placeholder: | 30 | 1. Start at `site-domain.com/blog`. 31 | 2. Click on any blog post. 32 | 3. Click on the 'Like' button. 33 | 4. ... 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: expected 38 | attributes: 39 | label: What you expected to happen 40 | placeholder: | 41 | e.g. The post should be liked. 42 | validations: 43 | required: true 44 | - type: textarea 45 | id: actual 46 | attributes: 47 | label: What actually happened 48 | placeholder: | 49 | e.g. Clicking the button does nothing visibly. 50 | validations: 51 | required: true 52 | - type: dropdown 53 | id: users-affected 54 | attributes: 55 | label: Impact 56 | description: Approximately how many users are impacted? 57 | options: 58 | - One 59 | - Some (< 50%) 60 | - Most (> 50%) 61 | - All 62 | validations: 63 | required: true 64 | - type: dropdown 65 | id: workarounds 66 | attributes: 67 | label: Available workarounds? 68 | options: 69 | - No and the platform is unusable 70 | - No but the platform is still usable 71 | - Yes, difficult to implement 72 | - Yes, easy to implement 73 | - There is no user impact 74 | validations: 75 | required: true 76 | 77 | - type: markdown 78 | attributes: 79 | value: | 80 |
81 | 82 | ## Optional Information 83 | 84 | The following section is optional. 85 | - type: textarea 86 | id: logs 87 | attributes: 88 | label: Logs or notes 89 | placeholder: | 90 | Add any information that may be relevant, such as: 91 | - Browser/Platform 92 | - Theme 93 | - Logs/Errors 94 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for Page Optimize! 3 | title: "Feature Request:" 4 | labels: ["[Type] Enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please, be as descriptive as possible. Issues lacking detail, or for any other reason than to request a feature, may be closed without action. 10 | 11 | - type: textarea 12 | id: what 13 | attributes: 14 | label: What 15 | description: Add a concise description of the feature being requested. 16 | placeholder: eg. I would like a new dropdown at ... 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: why 22 | attributes: 23 | label: Why 24 | description: Add a description of the problem this feature solves. 25 | placeholder: | 26 | eg. This will solve my accessibility needs. 27 | validations: 28 | required: true 29 | 30 | - type: textarea 31 | id: how 32 | attributes: 33 | label: How 34 | description: If applicable, add screenshots, mockup, animations and/or videos to help illustrate how the feature could be done. 35 | -------------------------------------------------------------------------------- /.github/workflows/gardening.yml: -------------------------------------------------------------------------------- 1 | # Repo gardening. Automate some of the triage tasks in the repo. 2 | name: Repo Gardening 3 | 4 | on: 5 | pull_request_target: # When a PR is opened, edited, updated, closed, or a label is added. 6 | types: [opened, reopened, synchronize, edited, labeled, closed] 7 | issues: # For auto-triage of issues. 8 | types: [opened, labeled, reopened, edited, closed] 9 | issue_comment: # To gather support references in issue comments. 10 | types: [created] 11 | concurrency: 12 | # For pull_request_target, cancel any concurrent jobs with the same type (e.g. "opened", "labeled") and branch. 13 | # Don't cancel any for other events, accomplished by grouping on the unique run_id. 14 | group: gardening-${{ github.event_name }}-${{ github.event.action }}-${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.ref || github.run_id }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | repo-gardening: 19 | name: 'Automated repo gardening.' 20 | runs-on: ubuntu-latest 21 | if: github.event_name == 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name 22 | timeout-minutes: 10 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v3 27 | 28 | - name: Setup Node 29 | uses: actions/setup-node@v3 30 | with: 31 | node-version: lts/* 32 | 33 | - name: Wait for prior instances of the workflow to finish 34 | uses: softprops/turnstyle@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: 'Automate triage (add labels, clean labels, ...).' 39 | uses: automattic/action-repo-gardening@trunk 40 | with: 41 | github_token: ${{ secrets.GITHUB_TOKEN }} 42 | slack_token: ${{ secrets.SLACK_TOKEN }} 43 | slack_team_channel: ${{ secrets.SLACK_TEAM_CHANNEL }} 44 | slack_he_triage_channel: ${{ secrets.SLACK_HE_TRIAGE_CHANNEL }} 45 | slack_quality_channel: ${{ secrets.SLACK_QUALITY_CHANNEL }} 46 | tasks: 'assignIssues,flagOss,gatherSupportReferences,replyToCustomersReminder' 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/dotorg-svn-deploy 2 | .DS_Store 3 | vendor 4 | .idea 5 | -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins. 4 | 5 | 6 | . 7 | /vendor/ 8 | /node_modules/ 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 48 | 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: trusty 3 | 4 | language: php 5 | 6 | notifications: 7 | email: 8 | on_success: never 9 | on_failure: change 10 | 11 | branches: 12 | only: 13 | - master 14 | 15 | cache: 16 | directories: 17 | - $HOME/.composer/cache 18 | 19 | jobs: 20 | include: 21 | - php: 7.2 22 | env: WP_VERSION=latest 23 | - php: 7.1 24 | env: WP_VERSION=latest 25 | - php: 7.0 26 | env: WP_VERSION=latest 27 | - php: 5.6 28 | env: WP_TRAVISCI=phpcs 29 | 30 | before_script: 31 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 32 | - | 33 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then 34 | phpenv config-rm xdebug.ini 35 | else 36 | echo "xdebug.ini does not exist" 37 | fi 38 | - | 39 | if [[ ! -z "$WP_VERSION" ]] ; then 40 | bash bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 41 | composer global require "phpunit/phpunit=4.8.*|5.7.*" 42 | fi 43 | - | 44 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then 45 | composer global require dealerdirect/phpcodesniffer-composer-installer wp-coding-standards/wpcs phpcompatibility/phpcompatibility-wp:* 46 | composer install 47 | fi 48 | 49 | script: 50 | - | 51 | if [[ ! -z "$WP_VERSION" ]] ; then 52 | phpunit 53 | WP_MULTISITE=1 phpunit 54 | fi 55 | - | 56 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then 57 | phpcs 58 | fi 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /bin/deploy-to-plugin-directory.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Deploys the latest plugin version to the .org plugin directory 4 | # 5 | # Before deploying, this script checks to make sure the new version... 6 | # - Is greater than the previous version 7 | # - Matches "Stable version:" in readme.txt 8 | # - TODO: Is not more than one digit different than the previous version (just a warning) 9 | # - Contains new changes 10 | # - Is not yet tagged in the plugin directory SVN 11 | # 12 | # When deploying, this script... 13 | # - Adds all new files to SVN 14 | # - Removes all deleted files from SVN 15 | # 16 | 17 | set -Eeuo pipefail 18 | trap "cleanup" ERR INT EXIT 19 | 20 | function cleanup { 21 | EXIT_CODE=$? 22 | echo -e "\nCleaning up before exit.\n" 23 | rm -rf "$PLUGIN_UPDATE_LOCATION" 24 | echo -e "Done!\n" 25 | # Avoids loop where trap will fire again: 26 | trap "" ERR INT EXIT 27 | exit $EXIT_CODE 28 | } 29 | 30 | function wp_plugin_version { 31 | echo "$( < "$1" grep -E "[[:blank:]]*Version:[[:blank:]]*[0-9]" | cut -d: -f2 | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" )" 32 | } 33 | 34 | # Set up colors for shell output. 35 | RED=`tput setaf 1` 36 | GREEN=`tput setaf 2` 37 | YELLOW=`tput setaf 3` 38 | RESET=`tput sgr0` 39 | 40 | # Get script parent directory. https://stackoverflow.com/a/246128 41 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 42 | 43 | PLUGIN_PATH="$( realpath "$SCRIPT_DIR/.." )" 44 | PLUGIN_UPDATE_LOCATION="$SCRIPT_DIR/dotorg-svn-deploy" 45 | 46 | # Get the production version from the mu-plugins loader file. 47 | NEW_LOADER_FILE="$PLUGIN_PATH/page-optimize.php" 48 | NEW_VERSION="$( wp_plugin_version "$NEW_LOADER_FILE" )" 49 | 50 | GIT_CURRENT_BRANCH="$( git branch | grep "* " | cut -c 3- )" 51 | if [[ "master" != $GIT_CURRENT_BRANCH ]] ; then 52 | echo -e "${RED}Please switch to the master branch before deploying.$RESET" 53 | exit 1 54 | fi 55 | 56 | GIT_MASTER_REMOTE="$( git rev-parse --abbrev-ref --symbolic-full-name @{u} | cut -d/ -f 1 )" 57 | GIT_MASTER_LOCAL_COMMIT="$( git rev-parse master )" 58 | GIT_MASTER_REMOTE_COMMIT="$( git ls-remote $GIT_MASTER_REMOTE master | cut -f1 )" 59 | if [[ $GIT_MASTER_LOCAL_COMMIT != $GIT_MASTER_REMOTE_COMMIT ]] ; then 60 | echo -e "${RED}Local master does not match $GIT_MASTER_REMOTE/master.$RESET" 61 | exit 1 62 | fi 63 | 64 | GIT_TAG="v$NEW_VERSION" 65 | if ! git rev-parse "$GIT_TAG" >/dev/null 2>&1; then 66 | echo -e "${RED}Please create and push '$GIT_TAG' for the new release.$RESET" 67 | exit 1 68 | fi 69 | 70 | if ! git ls-remote --exit-code --tags "$GIT_MASTER_REMOTE" "$GIT_TAG" 2>&1 >/dev/null; then 71 | echo -e "${RED}Please push the '$GIT_TAG' to the '$GIT_MASTER_REMOTE' git remote.$RESET" 72 | exit 1 73 | fi 74 | 75 | GIT_TAG_COMMIT="$( git ls-remote origin $GIT_TAG | cut -f1 )" 76 | if [[ $GIT_TAG_COMMIT != $GIT_MASTER_LOCAL_COMMIT ]] ; then 77 | echo -e "${RED}The '$GIT_TAG' tag does not match master.$RESET" 78 | exit 1 79 | fi 80 | 81 | TRUNK="$PLUGIN_UPDATE_LOCATION/trunk" 82 | DOTORG_PLUGIN_URL="http://plugins.svn.wordpress.org/page-optimize" 83 | 84 | echo -e "Checking out the .org plugin repo...\n" 85 | mkdir "$PLUGIN_UPDATE_LOCATION" 86 | cd "$PLUGIN_UPDATE_LOCATION" 87 | # Only copies trunk to the directory. 88 | svn co -q "$DOTORG_PLUGIN_URL/trunk" 89 | 90 | if [ ! -d "trunk" ] ; then 91 | echo -e "${RED}Couldn't clone the SVN repository.${RESET}\n" 92 | exit 1 93 | fi 94 | 95 | # Grab the current version from the plugin loader file before applying the update. 96 | OLD_LOADER_FILE="$TRUNK/page-optimize.php" 97 | if [[ -e "$OLD_LOADER_FILE" ]]; then 98 | OLD_VERSION="$( wp_plugin_version "$OLD_LOADER_FILE" )" 99 | else 100 | OLD_VERSION='None' 101 | fi 102 | 103 | # Rsync is faster than cp, and it can handle deleting files in the target 104 | # directory. We also exclude hidden files so that SVN continues working. 105 | rsync -a --delete --exclude=".*" --exclude="bin" "$PLUGIN_PATH/" "$TRUNK/" 106 | 107 | cd "$TRUNK" 108 | 109 | CHANGES=`svn status` 110 | if [ -z "$CHANGES" ] ; then 111 | echo -e "No changes between .org and this working copy. Exiting.\n" 112 | exit 1 113 | fi 114 | 115 | echo -e "Adding/removing files from svn (if applicable)...\n" 116 | 117 | # Adds all unversioned, but not ignored files. 118 | svn st | { grep '^\?' || test $? = 1; } | sed 's/^\? *//' | xargs -I% svn add % 119 | 120 | # Removes all files which have been deleted. 121 | svn st | { grep '^\!' || test $? = 1; } | sed 's/! *//' | xargs -I% svn rm % 122 | 123 | echo -e "\nThese files have changed since the last update:\n" 124 | svn st 125 | echo -e "\n" 126 | 127 | NEW_VERSION_URL="$DOTORG_PLUGIN_URL/tags/$NEW_VERSION" 128 | # This value should contain 404 if the version does NOT exist. 129 | NEW_VERSION_RESPONSE=`curl --write-out %{http_code} --silent --output /dev/null "$NEW_VERSION_URL"` 130 | 131 | README_FILE="$TRUNK/readme.txt" 132 | STABLE_VERSION="$( < "$README_FILE" grep -E "[[:blank:]]*Stable tag:[[:blank:]]*[0-9]" | cut -d: -f2 | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" )" 133 | 134 | SHOULD_STOP=false 135 | if [[ $NEW_VERSION_RESPONSE != "404" ]] ; then 136 | echo -e "${RED}The new version ($NEW_VERSION) has already been tagged. Please update the version before committing.${RESET}\n" 137 | SHOULD_STOP=true 138 | fi 139 | 140 | if [[ $STABLE_VERSION != $NEW_VERSION ]] ; then 141 | echo -e "${RED}The new version ($NEW_VERSION) does not match the stable tag ($STABLE_VERSION) in readme.txt.${RESET}\n" 142 | SHOULD_STOP=true 143 | fi 144 | 145 | echo -e "Version info:\n\tPrevious Version: $OLD_VERSION\n\tNew Version (defined in plugin loader): $NEW_VERSION\n\tStable Tag (defined in readme.txt): $STABLE_VERSION\n" 146 | 147 | if [ "$SHOULD_STOP" = true ] ; then 148 | echo -e "Exiting -- please fix the errors shown above ${RED}in GitHub${RESET} before continuing.\n" 149 | exit 1 150 | fi 151 | 152 | echo -e "The new version ($NEW_VERSION) has not been created yet. Everything looks good!\n" 153 | 154 | read -p "Are you sure you want to submit an update? (type y if you're sure): " -r 155 | echo 156 | if [[ $REPLY = 'y' ]] 157 | then 158 | cd "$TRUNK" 159 | echo -e "Enter your .org username for the SVN commit:" 160 | read USERNAME 161 | MESSAGE="Update Page Optimize to $NEW_VERSION" 162 | svn ci --username "$USERNAME" -m "$MESSAGE" 163 | svn cp --username "$USERNAME" -m "$MESSAGE" "$DOTORG_PLUGIN_URL/trunk" "$DOTORG_PLUGIN_URL/tags/$NEW_VERSION" 164 | echo -e "Committed and tagged the plugin successfully!\n" 165 | else 166 | echo -e "Did not commit the changes.\n" 167 | fi 168 | 169 | exit 0 170 | -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" 5 | exit 1 6 | fi 7 | 8 | DB_NAME=$1 9 | DB_USER=$2 10 | DB_PASS=$3 11 | DB_HOST=${4-localhost} 12 | WP_VERSION=${5-latest} 13 | SKIP_DB_CREATE=${6-false} 14 | 15 | TMPDIR=${TMPDIR-/tmp} 16 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") 17 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} 18 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} 19 | 20 | download() { 21 | if [ `which curl` ]; then 22 | curl -s "$1" > "$2"; 23 | elif [ `which wget` ]; then 24 | wget -nv -O "$2" "$1" 25 | fi 26 | } 27 | 28 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then 29 | WP_BRANCH=${WP_VERSION%\-*} 30 | WP_TESTS_TAG="branches/$WP_BRANCH" 31 | 32 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then 33 | WP_TESTS_TAG="branches/$WP_VERSION" 34 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then 35 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 36 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 37 | WP_TESTS_TAG="tags/${WP_VERSION%??}" 38 | else 39 | WP_TESTS_TAG="tags/$WP_VERSION" 40 | fi 41 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 42 | WP_TESTS_TAG="trunk" 43 | else 44 | # http serves a single offer, whereas https serves multiple. we only want one 45 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 46 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 47 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 48 | if [[ -z "$LATEST_VERSION" ]]; then 49 | echo "Latest WordPress version could not be found" 50 | exit 1 51 | fi 52 | WP_TESTS_TAG="tags/$LATEST_VERSION" 53 | fi 54 | set -ex 55 | 56 | install_wp() { 57 | 58 | if [ -d $WP_CORE_DIR ]; then 59 | return; 60 | fi 61 | 62 | mkdir -p $WP_CORE_DIR 63 | 64 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 65 | mkdir -p $TMPDIR/wordpress-nightly 66 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip 67 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ 68 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR 69 | else 70 | if [ $WP_VERSION == 'latest' ]; then 71 | local ARCHIVE_NAME='latest' 72 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then 73 | # https serves multiple offers, whereas http serves single. 74 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json 75 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then 76 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x 77 | LATEST_VERSION=${WP_VERSION%??} 78 | else 79 | # otherwise, scan the releases and get the most up to date minor version of the major release 80 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` 81 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) 82 | fi 83 | if [[ -z "$LATEST_VERSION" ]]; then 84 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 85 | else 86 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION" 87 | fi 88 | else 89 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 90 | fi 91 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz 92 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR 93 | fi 94 | 95 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 96 | } 97 | 98 | install_test_suite() { 99 | # portable in-place argument for both GNU sed and Mac OSX sed 100 | if [[ $(uname -s) == 'Darwin' ]]; then 101 | local ioption='-i.bak' 102 | else 103 | local ioption='-i' 104 | fi 105 | 106 | # set up testing suite if it doesn't yet exist 107 | if [ ! -d $WP_TESTS_DIR ]; then 108 | # set up testing suite 109 | mkdir -p $WP_TESTS_DIR 110 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 111 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 112 | fi 113 | 114 | if [ ! -f wp-tests-config.php ]; then 115 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 116 | # remove all forward slashes in the end 117 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 118 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 119 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 120 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 121 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 122 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 123 | fi 124 | 125 | } 126 | 127 | install_db() { 128 | 129 | if [ ${SKIP_DB_CREATE} = "true" ]; then 130 | return 0 131 | fi 132 | 133 | # parse DB_HOST for port or socket references 134 | local PARTS=(${DB_HOST//\:/ }) 135 | local DB_HOSTNAME=${PARTS[0]}; 136 | local DB_SOCK_OR_PORT=${PARTS[1]}; 137 | local EXTRA="" 138 | 139 | if ! [ -z $DB_HOSTNAME ] ; then 140 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 141 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 142 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 143 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 144 | elif ! [ -z $DB_HOSTNAME ] ; then 145 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 146 | fi 147 | fi 148 | 149 | # create database 150 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 151 | } 152 | 153 | install_wp 154 | install_test_suite 155 | install_db 156 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2", 4 | "phpcompatibility/phpcompatibility-wp": "*", 5 | "wp-coding-standards/wpcs": "^2.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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": "cfc0ae38ab4c4c74164edeab1a7beccd", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "dealerdirect/phpcodesniffer-composer-installer", 12 | "version": "v0.6.2", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", 16 | "reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/8001af8eb107fbfcedc31a8b51e20b07d85b457a", 21 | "reference": "8001af8eb107fbfcedc31a8b51e20b07d85b457a", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "composer-plugin-api": "^1.0", 26 | "php": "^5.3|^7", 27 | "squizlabs/php_codesniffer": "^2|^3" 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 | "time": "2020-01-29T20:22:20+00:00" 75 | }, 76 | { 77 | "name": "phpcompatibility/php-compatibility", 78 | "version": "9.3.5", 79 | "source": { 80 | "type": "git", 81 | "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", 82 | "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" 83 | }, 84 | "dist": { 85 | "type": "zip", 86 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", 87 | "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", 88 | "shasum": "" 89 | }, 90 | "require": { 91 | "php": ">=5.3", 92 | "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" 93 | }, 94 | "conflict": { 95 | "squizlabs/php_codesniffer": "2.6.2" 96 | }, 97 | "require-dev": { 98 | "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" 99 | }, 100 | "suggest": { 101 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", 102 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 103 | }, 104 | "type": "phpcodesniffer-standard", 105 | "notification-url": "https://packagist.org/downloads/", 106 | "license": [ 107 | "LGPL-3.0-or-later" 108 | ], 109 | "authors": [ 110 | { 111 | "name": "Wim Godden", 112 | "homepage": "https://github.com/wimg", 113 | "role": "lead" 114 | }, 115 | { 116 | "name": "Juliette Reinders Folmer", 117 | "homepage": "https://github.com/jrfnl", 118 | "role": "lead" 119 | }, 120 | { 121 | "name": "Contributors", 122 | "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" 123 | } 124 | ], 125 | "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", 126 | "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", 127 | "keywords": [ 128 | "compatibility", 129 | "phpcs", 130 | "standards" 131 | ], 132 | "time": "2019-12-27T09:44:58+00:00" 133 | }, 134 | { 135 | "name": "phpcompatibility/phpcompatibility-paragonie", 136 | "version": "1.3.0", 137 | "source": { 138 | "type": "git", 139 | "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", 140 | "reference": "b862bc32f7e860d0b164b199bd995e690b4b191c" 141 | }, 142 | "dist": { 143 | "type": "zip", 144 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/b862bc32f7e860d0b164b199bd995e690b4b191c", 145 | "reference": "b862bc32f7e860d0b164b199bd995e690b4b191c", 146 | "shasum": "" 147 | }, 148 | "require": { 149 | "phpcompatibility/php-compatibility": "^9.0" 150 | }, 151 | "require-dev": { 152 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5", 153 | "paragonie/random_compat": "dev-master", 154 | "paragonie/sodium_compat": "dev-master" 155 | }, 156 | "suggest": { 157 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", 158 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 159 | }, 160 | "type": "phpcodesniffer-standard", 161 | "notification-url": "https://packagist.org/downloads/", 162 | "license": [ 163 | "LGPL-3.0-or-later" 164 | ], 165 | "authors": [ 166 | { 167 | "name": "Wim Godden", 168 | "role": "lead" 169 | }, 170 | { 171 | "name": "Juliette Reinders Folmer", 172 | "role": "lead" 173 | } 174 | ], 175 | "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", 176 | "homepage": "http://phpcompatibility.com/", 177 | "keywords": [ 178 | "compatibility", 179 | "paragonie", 180 | "phpcs", 181 | "polyfill", 182 | "standards" 183 | ], 184 | "time": "2019-11-04T15:17:54+00:00" 185 | }, 186 | { 187 | "name": "phpcompatibility/phpcompatibility-wp", 188 | "version": "2.1.0", 189 | "source": { 190 | "type": "git", 191 | "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", 192 | "reference": "41bef18ba688af638b7310666db28e1ea9158b2f" 193 | }, 194 | "dist": { 195 | "type": "zip", 196 | "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/41bef18ba688af638b7310666db28e1ea9158b2f", 197 | "reference": "41bef18ba688af638b7310666db28e1ea9158b2f", 198 | "shasum": "" 199 | }, 200 | "require": { 201 | "phpcompatibility/php-compatibility": "^9.0", 202 | "phpcompatibility/phpcompatibility-paragonie": "^1.0" 203 | }, 204 | "require-dev": { 205 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5" 206 | }, 207 | "suggest": { 208 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHP_CodeSniffer 'installed_paths' automatically.", 209 | "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." 210 | }, 211 | "type": "phpcodesniffer-standard", 212 | "notification-url": "https://packagist.org/downloads/", 213 | "license": [ 214 | "LGPL-3.0-or-later" 215 | ], 216 | "authors": [ 217 | { 218 | "name": "Wim Godden", 219 | "role": "lead" 220 | }, 221 | { 222 | "name": "Juliette Reinders Folmer", 223 | "role": "lead" 224 | } 225 | ], 226 | "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", 227 | "homepage": "http://phpcompatibility.com/", 228 | "keywords": [ 229 | "compatibility", 230 | "phpcs", 231 | "standards", 232 | "wordpress" 233 | ], 234 | "time": "2019-08-28T14:22:28+00:00" 235 | }, 236 | { 237 | "name": "squizlabs/php_codesniffer", 238 | "version": "3.5.4", 239 | "source": { 240 | "type": "git", 241 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 242 | "reference": "dceec07328401de6211037abbb18bda423677e26" 243 | }, 244 | "dist": { 245 | "type": "zip", 246 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dceec07328401de6211037abbb18bda423677e26", 247 | "reference": "dceec07328401de6211037abbb18bda423677e26", 248 | "shasum": "" 249 | }, 250 | "require": { 251 | "ext-simplexml": "*", 252 | "ext-tokenizer": "*", 253 | "ext-xmlwriter": "*", 254 | "php": ">=5.4.0" 255 | }, 256 | "require-dev": { 257 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 258 | }, 259 | "bin": [ 260 | "bin/phpcs", 261 | "bin/phpcbf" 262 | ], 263 | "type": "library", 264 | "extra": { 265 | "branch-alias": { 266 | "dev-master": "3.x-dev" 267 | } 268 | }, 269 | "notification-url": "https://packagist.org/downloads/", 270 | "license": [ 271 | "BSD-3-Clause" 272 | ], 273 | "authors": [ 274 | { 275 | "name": "Greg Sherwood", 276 | "role": "lead" 277 | } 278 | ], 279 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 280 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 281 | "keywords": [ 282 | "phpcs", 283 | "standards" 284 | ], 285 | "time": "2020-01-30T22:20:29+00:00" 286 | }, 287 | { 288 | "name": "wp-coding-standards/wpcs", 289 | "version": "2.2.1", 290 | "source": { 291 | "type": "git", 292 | "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", 293 | "reference": "b5a453203114cc2284b1a614c4953456fbe4f546" 294 | }, 295 | "dist": { 296 | "type": "zip", 297 | "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/b5a453203114cc2284b1a614c4953456fbe4f546", 298 | "reference": "b5a453203114cc2284b1a614c4953456fbe4f546", 299 | "shasum": "" 300 | }, 301 | "require": { 302 | "php": ">=5.4", 303 | "squizlabs/php_codesniffer": "^3.3.1" 304 | }, 305 | "require-dev": { 306 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", 307 | "phpcompatibility/php-compatibility": "^9.0", 308 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 309 | }, 310 | "suggest": { 311 | "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." 312 | }, 313 | "type": "phpcodesniffer-standard", 314 | "notification-url": "https://packagist.org/downloads/", 315 | "license": [ 316 | "MIT" 317 | ], 318 | "authors": [ 319 | { 320 | "name": "Contributors", 321 | "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" 322 | } 323 | ], 324 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 325 | "keywords": [ 326 | "phpcs", 327 | "standards", 328 | "wordpress" 329 | ], 330 | "time": "2020-02-04T02:52:06+00:00" 331 | } 332 | ], 333 | "aliases": [], 334 | "minimum-stability": "stable", 335 | "stability-flags": [], 336 | "prefer-stable": false, 337 | "prefer-lowest": false, 338 | "platform": [], 339 | "platform-dev": [] 340 | } 341 | -------------------------------------------------------------------------------- /concat-css.php: -------------------------------------------------------------------------------- 1 | old_styles = new WP_Styles(); 19 | } else { 20 | $this->old_styles = $styles; 21 | } 22 | 23 | // Unset all the object properties except our private copy of the styles object. 24 | // We have to unset everything so that the overload methods talk to $this->old_styles->whatever 25 | // instead of $this->whatever. 26 | foreach ( array_keys( get_object_vars( $this ) ) as $key ) { 27 | if ( 'old_styles' === $key ) { 28 | continue; 29 | } 30 | unset( $this->$key ); 31 | } 32 | 33 | $this->dependency_path_mapping = new Page_Optimize_Dependency_Path_Mapping( 34 | apply_filters( 'page_optimize_site_url', $this->base_url ) 35 | ); 36 | } 37 | 38 | function do_items( $handles = false, $group = false ) { 39 | $handles = false === $handles ? $this->queue : (array) $handles; 40 | $stylesheets = array(); 41 | $siteurl = apply_filters( 'page_optimize_site_url', $this->base_url ); 42 | 43 | $this->all_deps( $handles ); 44 | 45 | $stylesheet_group_index = 0; 46 | // Merge CSS into a single file 47 | $concat_group = 'concat'; 48 | // Concat group on top (first array element gets processed earlier) 49 | $stylesheets[ $concat_group ] = array(); 50 | 51 | foreach ( $this->to_do as $key => $handle ) { 52 | $obj = $this->registered[ $handle ]; 53 | $obj->src = apply_filters( 'style_loader_src', $obj->src, $obj->handle ); 54 | 55 | // Core is kind of broken and returns "true" for src of "colors" handle 56 | // http://core.trac.wordpress.org/attachment/ticket/16827/colors-hacked-fixed.diff 57 | // http://core.trac.wordpress.org/ticket/20729 58 | $css_url = $obj->src; 59 | if ( 'colors' == $obj->handle && true === $css_url ) { 60 | $css_url = wp_style_loader_src( $css_url, $obj->handle ); 61 | } 62 | 63 | $css_url_parsed = parse_url( $obj->src ); 64 | $extra = $obj->extra; 65 | 66 | // Don't concat by default 67 | $do_concat = false; 68 | 69 | // Only try to concat static css files 70 | if ( false !== strpos( $css_url_parsed['path'], '.css' ) ) { 71 | $do_concat = true; 72 | } else { 73 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 74 | echo sprintf( "\n\n", esc_html( $handle ), esc_html( $obj->src ) ); 75 | } 76 | } 77 | 78 | // Don't try to concat styles which are loaded conditionally (like IE stuff) 79 | if ( isset( $extra['conditional'] ) ) { 80 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 81 | echo sprintf( "\n\n", esc_html( $handle ) ); 82 | } 83 | $do_concat = false; 84 | } 85 | 86 | // Don't concat rtl stuff for now until concat supports it correctly 87 | if ( $do_concat && 'rtl' === $this->text_direction && ! empty( $extra['rtl'] ) ) { 88 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 89 | echo sprintf( "\n\n", esc_html( $handle ) ); 90 | } 91 | $do_concat = false; 92 | } 93 | 94 | // Don't try to concat externally hosted scripts 95 | $is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $css_url ); 96 | if ( $do_concat && ! $is_internal_uri ) { 97 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 98 | echo sprintf( "\n\n", esc_html( $handle ), esc_url( $css_url ) ); 99 | } 100 | $do_concat = false; 101 | } 102 | 103 | if ( $do_concat ) { 104 | // Resolve paths and concat styles that exist in the filesystem 105 | $css_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $css_url ); 106 | if ( false === $css_realpath ) { 107 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 108 | echo sprintf( "\n\n", esc_html( $handle ), esc_html( $css_realpath ) ); 109 | } 110 | $do_concat = false; 111 | } 112 | } 113 | 114 | // Skip concating CSS from exclusion list 115 | $exclude_list = page_optimize_css_exclude_list(); 116 | foreach ( $exclude_list as $exclude ) { 117 | if ( $do_concat && $handle === $exclude ) { 118 | $do_concat = false; 119 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 120 | echo sprintf( "\n\n", esc_html( $handle ) ); 121 | } 122 | } 123 | } 124 | 125 | // Allow plugins to disable concatenation of certain stylesheets. 126 | if ( $do_concat && ! apply_filters( 'css_do_concat', $do_concat, $handle ) ) { 127 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 128 | echo sprintf( "\n\n", esc_html( $handle ) ); 129 | } 130 | } 131 | $do_concat = apply_filters( 'css_do_concat', $do_concat, $handle ); 132 | 133 | if ( true === $do_concat ) { 134 | $media = $obj->args; 135 | if ( empty( $media ) ) { 136 | $media = 'all'; 137 | } 138 | 139 | $stylesheets[ $concat_group ][ $media ][ $handle ] = $css_url_parsed['path']; 140 | $this->done[] = $handle; 141 | } else { 142 | $stylesheet_group_index ++; 143 | $stylesheets[ $stylesheet_group_index ]['noconcat'][] = $handle; 144 | $stylesheet_group_index ++; 145 | } 146 | unset( $this->to_do[ $key ] ); 147 | } 148 | 149 | foreach ( $stylesheets as $idx => $stylesheets_group ) { 150 | foreach ( $stylesheets_group as $media => $css ) { 151 | if ( 'noconcat' == $media ) { 152 | foreach ( $css as $handle ) { 153 | if ( $this->do_item( $handle, $group ) ) { 154 | $this->done[] = $handle; 155 | } 156 | } 157 | continue; 158 | } elseif ( count( $css ) > 1 ) { 159 | $fs_paths = array(); 160 | foreach ( $css as $css_uri_path ) { 161 | $fs_paths[] = $this->dependency_path_mapping->uri_path_to_fs_path( $css_uri_path ); 162 | } 163 | 164 | $mtime = max( array_map( 'filemtime', $fs_paths ) ); 165 | if ( page_optimize_use_concat_base_dir() ) { 166 | $path_str = implode( ',', array_map( 'page_optimize_remove_concat_base_prefix', $fs_paths ) ); 167 | } else { 168 | $path_str = implode( ',', $css ); 169 | } 170 | $path_str = "$path_str?m=$mtime"; 171 | 172 | if ( $this->allow_gzip_compression ) { 173 | $path_64 = base64_encode( gzcompress( $path_str ) ); 174 | if ( strlen( $path_str ) > ( strlen( $path_64 ) + 1 ) ) { 175 | $path_str = '-' . $path_64; 176 | } 177 | } 178 | 179 | $href = $siteurl . "/_static/??" . $path_str; 180 | } else { 181 | $href = Page_Optimize_Utils::cache_bust_mtime( current( $css ), $siteurl ); 182 | } 183 | 184 | $handles = array_keys( $css ); 185 | $css_id = "$media-css-" . md5( $href ); 186 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 187 | echo apply_filters( 'page_optimize_style_loader_tag', "\n", $handles, $href, $media ); 188 | } else { 189 | echo apply_filters( 'page_optimize_style_loader_tag', "\n", $handles, $href, $media ); 190 | } 191 | array_map( array( $this, 'print_inline_style' ), array_keys( $css ) ); 192 | } 193 | } 194 | 195 | return $this->done; 196 | } 197 | 198 | function __isset( $key ) { 199 | return isset( $this->old_styles->$key ); 200 | } 201 | 202 | function __unset( $key ) { 203 | unset( $this->old_styles->$key ); 204 | } 205 | 206 | function &__get( $key ) { 207 | return $this->old_styles->$key; 208 | } 209 | 210 | function __set( $key, $value ) { 211 | $this->old_styles->$key = $value; 212 | } 213 | } 214 | 215 | function page_optimize_css_concat_init() { 216 | global $wp_styles; 217 | 218 | $wp_styles = new Page_Optimize_CSS_Concat( $wp_styles ); 219 | $wp_styles->allow_gzip_compression = ALLOW_GZIP_COMPRESSION; 220 | } 221 | 222 | if ( page_optimize_should_concat_css() ) { 223 | add_action( 'init', 'page_optimize_css_concat_init' ); 224 | } 225 | -------------------------------------------------------------------------------- /concat-js.php: -------------------------------------------------------------------------------- 1 | old_scripts = new WP_Scripts(); 19 | } else { 20 | $this->old_scripts = $scripts; 21 | } 22 | 23 | // Unset all the object properties except our private copy of the scripts object. 24 | // We have to unset everything so that the overload methods talk to $this->old_scripts->whatever 25 | // instead of $this->whatever. 26 | foreach ( array_keys( get_object_vars( $this ) ) as $key ) { 27 | if ( 'old_scripts' === $key ) { 28 | continue; 29 | } 30 | unset( $this->$key ); 31 | } 32 | 33 | $this->dependency_path_mapping = new Page_Optimize_Dependency_Path_Mapping( 34 | apply_filters( 'page_optimize_site_url', $this->base_url ) 35 | ); 36 | } 37 | 38 | protected function has_inline_content( $handle ) { 39 | $before_output = $this->get_data( $handle, 'before' ); 40 | if ( ! empty( $before_output ) ) { 41 | return true; 42 | } 43 | 44 | $after_output = $this->get_data( $handle, 'after' ); 45 | if ( ! empty( $after_output ) ) { 46 | return true; 47 | } 48 | 49 | // JavaScript translations 50 | $has_translations = ! empty( $this->registered[ $handle ]->textdomain ); 51 | if ( $has_translations ) { 52 | return true; 53 | } 54 | 55 | return false; 56 | } 57 | 58 | function do_items( $handles = false, $group = false ) { 59 | $handles = false === $handles ? $this->queue : (array) $handles; 60 | $javascripts = array(); 61 | $siteurl = apply_filters( 'page_optimize_site_url', $this->base_url ); 62 | $this->all_deps( $handles ); 63 | $level = 0; 64 | 65 | $using_strict = false; 66 | foreach ( $this->to_do as $key => $handle ) { 67 | $script_is_strict = false; 68 | if ( in_array( $handle, $this->done ) || ! isset( $this->registered[ $handle ] ) ) { 69 | continue; 70 | } 71 | 72 | if ( 0 === $group && $this->groups[ $handle ] > 0 ) { 73 | $this->in_footer[] = $handle; 74 | unset( $this->to_do[ $key ] ); 75 | continue; 76 | } 77 | 78 | if ( ! $this->registered[ $handle ]->src ) { // Defines a group. 79 | if ( $this->has_inline_content( $handle ) ) { 80 | $level ++; 81 | $javascripts[ $level ]['type'] = 'do_item'; 82 | $javascripts[ $level ]['handle'] = $handle; 83 | $level ++; 84 | unset( $this->to_do[ $key ] ); 85 | } else { 86 | // if there are localized items, echo them 87 | $this->print_extra_script( $handle ); 88 | $this->done[] = $handle; 89 | } 90 | continue; 91 | } 92 | 93 | if ( false === $group && in_array( $handle, $this->in_footer, true ) ) { 94 | $this->in_footer = array_diff( $this->in_footer, (array) $handle ); 95 | } 96 | 97 | $obj = $this->registered[ $handle ]; 98 | $js_url = $obj->src; 99 | $js_url_parsed = parse_url( $js_url ); 100 | 101 | // Don't concat by default 102 | $do_concat = false; 103 | 104 | // Only try to concat static js files 105 | if ( false !== strpos( $js_url_parsed['path'], '.js' ) ) { 106 | $do_concat = page_optimize_should_concat_js(); 107 | } else { 108 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 109 | echo sprintf( "\n\n", esc_html( $handle ), esc_html( $obj->src ) ); 110 | } 111 | } 112 | 113 | // Don't try to concat externally hosted scripts 114 | $is_internal_uri = $this->dependency_path_mapping->is_internal_uri( $js_url ); 115 | if ( $do_concat && ! $is_internal_uri ) { 116 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 117 | echo sprintf( "\n\n", esc_html( $handle ), esc_url( $js_url ) ); 118 | } 119 | $do_concat = false; 120 | } 121 | 122 | if ( $do_concat ) { 123 | // Resolve paths and concat scripts that exist in the filesystem 124 | $js_realpath = $this->dependency_path_mapping->dependency_src_to_fs_path( $js_url ); 125 | if ( false === $js_realpath ) { 126 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 127 | echo sprintf( "\n\n", esc_html( $handle ), esc_html( $js_realpath ) ); 128 | } 129 | $do_concat = false; 130 | } 131 | } 132 | 133 | if ( $do_concat && $this->has_inline_content( $handle ) ) { 134 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 135 | echo sprintf( "\n\n", esc_html( $handle ) ); 136 | } 137 | $do_concat = false; 138 | } 139 | 140 | // Skip core scripts that use Strict Mode 141 | if ( $do_concat && ( 'react' === $handle || 'react-dom' === $handle ) ) { 142 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 143 | echo sprintf( "\n\n", esc_html( $handle ) ); 144 | } 145 | $do_concat = false; 146 | $script_is_strict = true; 147 | } else if ( $do_concat && preg_match_all( '/^[\',"]use strict[\',"];/Uims', file_get_contents( $js_realpath ), $matches ) ) { 148 | // Skip third-party scripts that use Strict Mode 149 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 150 | echo sprintf( "\n\n", esc_html( $handle ) ); 151 | } 152 | $do_concat = false; 153 | $script_is_strict = true; 154 | } else { 155 | $script_is_strict = false; 156 | } 157 | 158 | // Skip concating scripts from exclusion list 159 | $exclude_list = page_optimize_js_exclude_list(); 160 | foreach ( $exclude_list as $exclude ) { 161 | if ( $do_concat && $handle === $exclude ) { 162 | $do_concat = false; 163 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 164 | echo sprintf( "\n\n", esc_html( $handle ) ); 165 | } 166 | } 167 | } 168 | 169 | // Allow plugins to disable concatenation of certain scripts. 170 | if ( $do_concat && ! apply_filters( 'js_do_concat', $do_concat, $handle ) ) { 171 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 172 | echo sprintf( "\n\n", esc_html( $handle ) ); 173 | } 174 | } 175 | $do_concat = apply_filters( 'js_do_concat', $do_concat, $handle ); 176 | 177 | if ( true === $do_concat ) { 178 | if ( ! isset( $javascripts[ $level ] ) ) { 179 | $javascripts[ $level ]['type'] = 'concat'; 180 | } 181 | 182 | $javascripts[ $level ]['paths'][] = $js_url_parsed['path']; 183 | $javascripts[ $level ]['handles'][] = $handle; 184 | 185 | } else { 186 | $level ++; 187 | $javascripts[ $level ]['type'] = 'do_item'; 188 | $javascripts[ $level ]['handle'] = $handle; 189 | $level ++; 190 | } 191 | unset( $this->to_do[ $key ] ); 192 | 193 | if ( $using_strict !== $script_is_strict ) { 194 | if ( $script_is_strict ) { 195 | $using_strict = true; 196 | $strict_count = 0; 197 | } else { 198 | $using_strict = false; 199 | } 200 | } 201 | 202 | if ( $script_is_strict ) { 203 | $strict_count ++; 204 | } 205 | } 206 | 207 | if ( empty( $javascripts ) ) { 208 | return $this->done; 209 | } 210 | 211 | foreach ( $javascripts as $js_array ) { 212 | if ( 'do_item' == $js_array['type'] ) { 213 | if ( $this->do_item( $js_array['handle'], $group ) ) { 214 | $this->done[] = $js_array['handle']; 215 | } 216 | } else if ( 'concat' == $js_array['type'] ) { 217 | array_map( array( $this, 'print_extra_script' ), $js_array['handles'] ); 218 | 219 | if ( isset( $js_array['paths'] ) && count( $js_array['paths'] ) > 1 ) { 220 | $fs_paths = array(); 221 | foreach ( $js_array['paths'] as $js_url ) { 222 | $fs_paths[] = $this->dependency_path_mapping->uri_path_to_fs_path( $js_url ); 223 | } 224 | 225 | $mtime = max( array_map( 'filemtime', $fs_paths ) ); 226 | if ( page_optimize_use_concat_base_dir() ) { 227 | $path_str = implode( ',', array_map( 'page_optimize_remove_concat_base_prefix', $fs_paths ) ); 228 | } else { 229 | $path_str = implode( ',', $js_array['paths'] ); 230 | } 231 | $path_str = "$path_str?m=$mtime"; 232 | 233 | if ( $this->allow_gzip_compression ) { 234 | $path_64 = base64_encode( gzcompress( $path_str ) ); 235 | if ( strlen( $path_str ) > ( strlen( $path_64 ) + 1 ) ) { 236 | $path_str = '-' . $path_64; 237 | } 238 | } 239 | 240 | $href = $siteurl . "/_static/??" . $path_str; 241 | } elseif ( isset( $js_array['paths'] ) && is_array( $js_array['paths'] ) ) { 242 | $href = Page_Optimize_Utils::cache_bust_mtime( $js_array['paths'][0], $siteurl ); 243 | } 244 | 245 | $this->done = array_merge( $this->done, $js_array['handles'] ); 246 | 247 | // Print before/after scripts from wp_inline_scripts() and concatenated script tag 248 | if ( isset( $js_array['extras']['before'] ) ) { 249 | foreach ( $js_array['extras']['before'] as $inline_before ) { 250 | echo $inline_before; 251 | } 252 | } 253 | 254 | if ( isset( $href ) ) { 255 | $handles = implode( ',', $js_array['handles'] ); 256 | 257 | $load_mode = page_optimize_load_mode_js(); 258 | 259 | if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 260 | $tag = "\n"; 261 | } else { 262 | $tag = "\n"; 263 | } 264 | 265 | if ( is_array( $js_array['handles'] ) && count( $js_array['handles'] ) === 1 ) { 266 | // Because we have a single script, let's apply the `script_loader_tag` filter as core does in `do_item()`. 267 | // That way, we interfere less with plugin and theme script filtering. For example, without this filter, 268 | // there is a case where we block the TwentyTwenty theme from adding async/defer attributes. 269 | // https://github.com/Automattic/page-optimize/pull/44 270 | $tag = apply_filters( 'script_loader_tag', $tag, $js_array['handles'][0], $href ); 271 | } 272 | 273 | echo $tag; 274 | } 275 | 276 | if ( isset( $js_array['extras']['after'] ) ) { 277 | foreach ( $js_array['extras']['after'] as $inline_after ) { 278 | echo $inline_after; 279 | } 280 | } 281 | } 282 | } 283 | 284 | do_action( 'js_concat_did_items', $javascripts ); 285 | 286 | return $this->done; 287 | } 288 | 289 | function __isset( $key ) { 290 | return isset( $this->old_scripts->$key ); 291 | } 292 | 293 | function __unset( $key ) { 294 | unset( $this->old_scripts->$key ); 295 | } 296 | 297 | function &__get( $key ) { 298 | return $this->old_scripts->$key; 299 | } 300 | 301 | function __set( $key, $value ) { 302 | $this->old_scripts->$key = $value; 303 | } 304 | } 305 | 306 | function page_optimize_js_concat_init() { 307 | global $wp_scripts; 308 | 309 | $wp_scripts = new Page_Optimize_JS_Concat( $wp_scripts ); 310 | $wp_scripts->allow_gzip_compression = ALLOW_GZIP_COMPRESSION; 311 | } 312 | 313 | if ( ! is_admin() && ( page_optimize_should_concat_js() || page_optimize_load_mode_js() ) ) { 314 | add_action( 'init', 'page_optimize_js_concat_init' ); 315 | } 316 | -------------------------------------------------------------------------------- /cssmin/Colors.php: -------------------------------------------------------------------------------- 1 | 'azure', 12 | '#f5f5dc' => 'beige', 13 | '#ffe4c4' => 'bisque', 14 | '#a52a2a' => 'brown', 15 | '#ff7f50' => 'coral', 16 | '#ffd700' => 'gold', 17 | '#808080' => 'gray', 18 | '#008000' => 'green', 19 | '#4b0082' => 'indigo', 20 | '#fffff0' => 'ivory', 21 | '#f0e68c' => 'khaki', 22 | '#faf0e6' => 'linen', 23 | '#800000' => 'maroon', 24 | '#000080' => 'navy', 25 | '#fdf5e6' => 'oldlace', 26 | '#808000' => 'olive', 27 | '#ffa500' => 'orange', 28 | '#da70d6' => 'orchid', 29 | '#cd853f' => 'peru', 30 | '#ffc0cb' => 'pink', 31 | '#dda0dd' => 'plum', 32 | '#800080' => 'purple', 33 | '#f00' => 'red', 34 | '#fa8072' => 'salmon', 35 | '#a0522d' => 'sienna', 36 | '#c0c0c0' => 'silver', 37 | '#fffafa' => 'snow', 38 | '#d2b48c' => 'tan', 39 | '#008080' => 'teal', 40 | '#ff6347' => 'tomato', 41 | '#ee82ee' => 'violet', 42 | '#f5deb3' => 'wheat' 43 | ); 44 | } 45 | 46 | public static function getNamedToHexMap() 47 | { 48 | // Named colors longer than hex counterpart 49 | return array( 50 | 'aliceblue' => '#f0f8ff', 51 | 'antiquewhite' => '#faebd7', 52 | 'aquamarine' => '#7fffd4', 53 | 'black' => '#000', 54 | 'blanchedalmond' => '#ffebcd', 55 | 'blueviolet' => '#8a2be2', 56 | 'burlywood' => '#deb887', 57 | 'cadetblue' => '#5f9ea0', 58 | 'chartreuse' => '#7fff00', 59 | 'chocolate' => '#d2691e', 60 | 'cornflowerblue' => '#6495ed', 61 | 'cornsilk' => '#fff8dc', 62 | 'darkblue' => '#00008b', 63 | 'darkcyan' => '#008b8b', 64 | 'darkgoldenrod' => '#b8860b', 65 | 'darkgray' => '#a9a9a9', 66 | 'darkgreen' => '#006400', 67 | 'darkgrey' => '#a9a9a9', 68 | 'darkkhaki' => '#bdb76b', 69 | 'darkmagenta' => '#8b008b', 70 | 'darkolivegreen' => '#556b2f', 71 | 'darkorange' => '#ff8c00', 72 | 'darkorchid' => '#9932cc', 73 | 'darksalmon' => '#e9967a', 74 | 'darkseagreen' => '#8fbc8f', 75 | 'darkslateblue' => '#483d8b', 76 | 'darkslategray' => '#2f4f4f', 77 | 'darkslategrey' => '#2f4f4f', 78 | 'darkturquoise' => '#00ced1', 79 | 'darkviolet' => '#9400d3', 80 | 'deeppink' => '#ff1493', 81 | 'deepskyblue' => '#00bfff', 82 | 'dodgerblue' => '#1e90ff', 83 | 'firebrick' => '#b22222', 84 | 'floralwhite' => '#fffaf0', 85 | 'forestgreen' => '#228b22', 86 | 'fuchsia' => '#f0f', 87 | 'gainsboro' => '#dcdcdc', 88 | 'ghostwhite' => '#f8f8ff', 89 | 'goldenrod' => '#daa520', 90 | 'greenyellow' => '#adff2f', 91 | 'honeydew' => '#f0fff0', 92 | 'indianred' => '#cd5c5c', 93 | 'lavender' => '#e6e6fa', 94 | 'lavenderblush' => '#fff0f5', 95 | 'lawngreen' => '#7cfc00', 96 | 'lemonchiffon' => '#fffacd', 97 | 'lightblue' => '#add8e6', 98 | 'lightcoral' => '#f08080', 99 | 'lightcyan' => '#e0ffff', 100 | 'lightgoldenrodyellow' => '#fafad2', 101 | 'lightgray' => '#d3d3d3', 102 | 'lightgreen' => '#90ee90', 103 | 'lightgrey' => '#d3d3d3', 104 | 'lightpink' => '#ffb6c1', 105 | 'lightsalmon' => '#ffa07a', 106 | 'lightseagreen' => '#20b2aa', 107 | 'lightskyblue' => '#87cefa', 108 | 'lightslategray' => '#778899', 109 | 'lightslategrey' => '#778899', 110 | 'lightsteelblue' => '#b0c4de', 111 | 'lightyellow' => '#ffffe0', 112 | 'limegreen' => '#32cd32', 113 | 'mediumaquamarine' => '#66cdaa', 114 | 'mediumblue' => '#0000cd', 115 | 'mediumorchid' => '#ba55d3', 116 | 'mediumpurple' => '#9370db', 117 | 'mediumseagreen' => '#3cb371', 118 | 'mediumslateblue' => '#7b68ee', 119 | 'mediumspringgreen' => '#00fa9a', 120 | 'mediumturquoise' => '#48d1cc', 121 | 'mediumvioletred' => '#c71585', 122 | 'midnightblue' => '#191970', 123 | 'mintcream' => '#f5fffa', 124 | 'mistyrose' => '#ffe4e1', 125 | 'moccasin' => '#ffe4b5', 126 | 'navajowhite' => '#ffdead', 127 | 'olivedrab' => '#6b8e23', 128 | 'orangered' => '#ff4500', 129 | 'palegoldenrod' => '#eee8aa', 130 | 'palegreen' => '#98fb98', 131 | 'paleturquoise' => '#afeeee', 132 | 'palevioletred' => '#db7093', 133 | 'papayawhip' => '#ffefd5', 134 | 'peachpuff' => '#ffdab9', 135 | 'powderblue' => '#b0e0e6', 136 | 'rebeccapurple' => '#663399', 137 | 'rosybrown' => '#bc8f8f', 138 | 'royalblue' => '#4169e1', 139 | 'saddlebrown' => '#8b4513', 140 | 'sandybrown' => '#f4a460', 141 | 'seagreen' => '#2e8b57', 142 | 'seashell' => '#fff5ee', 143 | 'slateblue' => '#6a5acd', 144 | 'slategray' => '#708090', 145 | 'slategrey' => '#708090', 146 | 'springgreen' => '#00ff7f', 147 | 'steelblue' => '#4682b4', 148 | 'turquoise' => '#40e0d0', 149 | 'white' => '#fff', 150 | 'whitesmoke' => '#f5f5f5', 151 | 'yellow' => '#ff0', 152 | 'yellowgreen' => '#9acd32' 153 | ); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /cssmin/Minifier.php: -------------------------------------------------------------------------------- 1 | raisePhpLimits = (bool) $raisePhpLimits; 75 | $this->memoryLimit = 128 * 1048576; // 128MB in bytes 76 | $this->pcreBacktrackLimit = 1000 * 1000; 77 | $this->pcreRecursionLimit = 500 * 1000; 78 | $this->hexToNamedColorsMap = Colors::getHexToNamedMap(); 79 | $this->namedToHexColorsMap = Colors::getNamedToHexMap(); 80 | $this->namedToHexColorsRegex = sprintf( 81 | '/([:,( ])(%s)( |,|\)|;|$)/Si', 82 | implode('|', array_keys($this->namedToHexColorsMap)) 83 | ); 84 | $this->numRegex = sprintf('-?\d*\.?\d+%s?', $this->unitsGroupRegex); 85 | $this->setShortenZeroValuesRegexes(); 86 | } 87 | 88 | /** 89 | * Parses & minifies the given input CSS string 90 | * @param string $css 91 | * @return string 92 | */ 93 | public function run($css = '') 94 | { 95 | if (empty($css) || !is_string($css)) { 96 | return ''; 97 | } 98 | 99 | $this->resetRunProperties(); 100 | 101 | if ($this->raisePhpLimits) { 102 | $this->doRaisePhpLimits(); 103 | } 104 | 105 | return $this->minify($css); 106 | } 107 | 108 | /** 109 | * Sets whether to keep or remove sourcemap special comment. 110 | * Sourcemap comments are removed by default. 111 | * @param bool $keepSourceMapComment 112 | */ 113 | public function keepSourceMapComment($keepSourceMapComment = true) 114 | { 115 | $this->keepSourceMapComment = (bool) $keepSourceMapComment; 116 | } 117 | 118 | /** 119 | * Sets whether to keep or remove important comments. 120 | * Important comments outside of a declaration block are kept by default. 121 | * @param bool $removeImportantComments 122 | */ 123 | public function removeImportantComments($removeImportantComments = true) 124 | { 125 | $this->keepImportantComments = !(bool) $removeImportantComments; 126 | } 127 | 128 | /** 129 | * Sets the approximate column after which long lines will be splitted in the output 130 | * with a linebreak. 131 | * @param int $position 132 | */ 133 | public function setLineBreakPosition($position) 134 | { 135 | $this->linebreakPosition = (int) $position; 136 | } 137 | 138 | /** 139 | * Sets the memory limit for this script 140 | * @param int|string $limit 141 | */ 142 | public function setMemoryLimit($limit) 143 | { 144 | $this->memoryLimit = Utils::normalizeInt($limit); 145 | } 146 | 147 | /** 148 | * Sets the maximum execution time for this script 149 | * @param int|string $seconds 150 | */ 151 | public function setMaxExecutionTime($seconds) 152 | { 153 | $this->maxExecutionTime = (int) $seconds; 154 | } 155 | 156 | /** 157 | * Sets the PCRE backtrack limit for this script 158 | * @param int $limit 159 | */ 160 | public function setPcreBacktrackLimit($limit) 161 | { 162 | $this->pcreBacktrackLimit = (int) $limit; 163 | } 164 | 165 | /** 166 | * Sets the PCRE recursion limit for this script 167 | * @param int $limit 168 | */ 169 | public function setPcreRecursionLimit($limit) 170 | { 171 | $this->pcreRecursionLimit = (int) $limit; 172 | } 173 | 174 | /** 175 | * Builds regular expressions needed for shortening zero values 176 | */ 177 | private function setShortenZeroValuesRegexes() 178 | { 179 | $zeroRegex = '0'. $this->unitsGroupRegex; 180 | $numOrPosRegex = '('. $this->numRegex .'|top|left|bottom|right|center) '; 181 | $oneZeroSafeProperties = array( 182 | '(?:line-)?height', 183 | '(?:(?:min|max)-)?width', 184 | 'top', 185 | 'left', 186 | 'background-position', 187 | 'bottom', 188 | 'right', 189 | 'border(?:-(?:top|left|bottom|right))?(?:-width)?', 190 | 'border-(?:(?:top|bottom)-(?:left|right)-)?radius', 191 | 'column-(?:gap|width)', 192 | 'margin(?:-(?:top|left|bottom|right))?', 193 | 'outline-width', 194 | 'padding(?:-(?:top|left|bottom|right))?' 195 | ); 196 | 197 | // First zero regex 198 | $regex = '/(^|;)('. implode('|', $oneZeroSafeProperties) .'):%s/Si'; 199 | $this->shortenOneZeroesRegex = sprintf($regex, $zeroRegex); 200 | 201 | // Multiple zeroes regexes 202 | $regex = '/(^|;)(margin|padding|border-(?:width|radius)|background-position):%s/Si'; 203 | $this->shortenTwoZeroesRegex = sprintf($regex, $numOrPosRegex . $zeroRegex); 204 | $this->shortenThreeZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $zeroRegex); 205 | $this->shortenFourZeroesRegex = sprintf($regex, $numOrPosRegex . $numOrPosRegex . $numOrPosRegex . $zeroRegex); 206 | } 207 | 208 | /** 209 | * Resets properties whose value may change between runs 210 | */ 211 | private function resetRunProperties() 212 | { 213 | $this->comments = array(); 214 | $this->ruleBodies = array(); 215 | $this->preservedTokens = array(); 216 | } 217 | 218 | /** 219 | * Tries to configure PHP to use at least the suggested minimum settings 220 | * @return void 221 | */ 222 | private function doRaisePhpLimits() 223 | { 224 | $phpLimits = array( 225 | 'memory_limit' => $this->memoryLimit, 226 | 'max_execution_time' => $this->maxExecutionTime, 227 | 'pcre.backtrack_limit' => $this->pcreBacktrackLimit, 228 | 'pcre.recursion_limit' => $this->pcreRecursionLimit 229 | ); 230 | 231 | // If current settings are higher respect them. 232 | foreach ($phpLimits as $name => $suggested) { 233 | $current = Utils::normalizeInt(ini_get($name)); 234 | 235 | if ($current >= $suggested) { 236 | continue; 237 | } 238 | 239 | // memoryLimit exception: allow -1 for "no memory limit". 240 | if ($name === 'memory_limit' && $current === -1) { 241 | continue; 242 | } 243 | 244 | // maxExecutionTime exception: allow 0 for "no memory limit". 245 | if ($name === 'max_execution_time' && $current === 0) { 246 | continue; 247 | } 248 | 249 | ini_set($name, $suggested); 250 | } 251 | } 252 | 253 | /** 254 | * Registers a preserved token 255 | * @param string $token 256 | * @return string The token ID string 257 | */ 258 | private function registerPreservedToken($token) 259 | { 260 | $tokenId = sprintf(self::PRESERVED_TOKEN, count($this->preservedTokens)); 261 | $this->preservedTokens[$tokenId] = $token; 262 | return $tokenId; 263 | } 264 | 265 | /** 266 | * Registers a candidate comment token 267 | * @param string $comment 268 | * @return string The comment token ID string 269 | */ 270 | private function registerCommentToken($comment) 271 | { 272 | $tokenId = sprintf(self::COMMENT_TOKEN, count($this->comments)); 273 | $this->comments[$tokenId] = $comment; 274 | return $tokenId; 275 | } 276 | 277 | /** 278 | * Registers a rule body token 279 | * @param string $body the minified rule body 280 | * @return string The rule body token ID string 281 | */ 282 | private function registerRuleBodyToken($body) 283 | { 284 | if (empty($body)) { 285 | return ''; 286 | } 287 | 288 | $tokenId = sprintf(self::RULE_BODY_TOKEN, count($this->ruleBodies)); 289 | $this->ruleBodies[$tokenId] = $body; 290 | return $tokenId; 291 | } 292 | 293 | private function registerUnquotedFontToken($body) 294 | { 295 | if (empty($body)) { 296 | return ''; 297 | } 298 | 299 | $tokenId = sprintf(self::UNQUOTED_FONT_TOKEN, count($this->unquotedFontTokens)); 300 | $this->unquotedFontTokens[$tokenId] = $body; 301 | return $tokenId; 302 | } 303 | 304 | /** 305 | * Parses & minifies the given input CSS string 306 | * @param string $css 307 | * @return string 308 | */ 309 | private function minify($css) 310 | { 311 | // Process data urls 312 | $css = $this->processDataUrls($css); 313 | 314 | // Process comments 315 | $css = preg_replace_callback( 316 | '/(?processComments($css); 355 | 356 | // Process rule bodies 357 | $css = $this->processRuleBodies($css); 358 | 359 | // Process at-rules and selectors 360 | $css = $this->processAtRulesAndSelectors($css); 361 | 362 | // Restore preserved rule bodies before splitting 363 | $css = strtr($css, $this->ruleBodies); 364 | 365 | // Split long lines in output if required 366 | $css = $this->processLongLineSplitting($css); 367 | 368 | // Restore preserved comments and strings 369 | $css = strtr($css, $this->preservedTokens); 370 | 371 | return trim($css); 372 | } 373 | 374 | /** 375 | * Searches & replaces all data urls with tokens before we start compressing, 376 | * to avoid performance issues running some of the subsequent regexes against large string chunks. 377 | * @param string $css 378 | * @return string 379 | */ 380 | private function processDataUrls($css) 381 | { 382 | $ret = ''; 383 | $searchOffset = $substrOffset = 0; 384 | 385 | // Since we need to account for non-base64 data urls, we need to handle 386 | // ' and ) being part of the data string. 387 | while (preg_match('/url\(\s*(["\']?)data:/Si', $css, $m, PREG_OFFSET_CAPTURE, $searchOffset)) { 388 | $matchStartIndex = $m[0][1]; 389 | $dataStartIndex = $matchStartIndex + 4; // url( length 390 | $searchOffset = $matchStartIndex + strlen($m[0][0]); 391 | $terminator = $m[1][0]; // ', " or empty (not quoted) 392 | $terminatorRegex = '/(?registerPreservedToken(trim($token)) .')'; 408 | // No end terminator found, re-add the whole match. Should we throw/warn here? 409 | } else { 410 | $ret .= substr($css, $matchStartIndex, $searchOffset - $matchStartIndex); 411 | } 412 | 413 | $substrOffset = $searchOffset; 414 | } 415 | 416 | $ret .= substr($css, $substrOffset); 417 | 418 | return $ret; 419 | } 420 | 421 | /** 422 | * Registers all comments found as candidates to be preserved. 423 | * @param array $matches 424 | * @return string 425 | */ 426 | private function processCommentsCallback($matches) 427 | { 428 | return '/*'. $this->registerCommentToken($matches[1]) .'*/'; 429 | } 430 | 431 | /** 432 | * Preserves old IE Matrix string definition 433 | * @param array $matches 434 | * @return string 435 | */ 436 | private function processOldIeSpecificMatrixDefinitionCallback($matches) 437 | { 438 | return 'filter:progid:DXImageTransform.Microsoft.Matrix('. $this->registerPreservedToken($matches[1]) .')'; 439 | } 440 | 441 | /** 442 | * Preserves strings found 443 | * @param array $matches 444 | * @return string 445 | */ 446 | private function processStringsCallback($matches) 447 | { 448 | $match = $matches[0]; 449 | $quote = substr($match, 0, 1); 450 | $match = substr($match, 1, -1); 451 | 452 | // maybe the string contains a comment-like substring? 453 | // one, maybe more? put'em back then 454 | if (strpos($match, self::COMMENT_TOKEN_START) !== false) { 455 | $match = strtr($match, $this->comments); 456 | } 457 | 458 | // minify alpha opacity in filter strings 459 | $match = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $match); 460 | 461 | return $quote . $this->registerPreservedToken($match) . $quote; 462 | } 463 | 464 | /** 465 | * Searches & replaces all import at-rule unquoted urls with tokens so URI reserved characters such as a semicolon 466 | * may be used safely in a URL. 467 | * @param array $matches 468 | * @return string 469 | */ 470 | private function processImportUnquotedUrlAtRulesCallback($matches) 471 | { 472 | return '@import url('. $this->registerPreservedToken($matches[1]) .')'. $matches[2]; 473 | } 474 | 475 | /** 476 | * Preserves or removes comments found. 477 | * @param string $css 478 | * @return string 479 | */ 480 | private function processComments($css) 481 | { 482 | foreach ($this->comments as $commentId => $comment) { 483 | $commentIdString = '/*'. $commentId .'*/'; 484 | 485 | // ! in the first position of the comment means preserve 486 | // so push to the preserved tokens keeping the ! 487 | if ($this->keepImportantComments && strpos($comment, '!') === 0) { 488 | $preservedTokenId = $this->registerPreservedToken($comment); 489 | // Put new lines before and after /*! important comments 490 | $css = str_replace($commentIdString, "\n/*$preservedTokenId*/\n", $css); 491 | continue; 492 | } 493 | 494 | // # sourceMappingURL= in the first position of the comment means sourcemap 495 | // so push to the preserved tokens if {$this->keepSourceMapComment} is truthy. 496 | if ($this->keepSourceMapComment && strpos($comment, '# sourceMappingURL=') === 0) { 497 | $preservedTokenId = $this->registerPreservedToken($comment); 498 | // Add new line before the sourcemap comment 499 | $css = str_replace($commentIdString, "\n/*$preservedTokenId*/", $css); 500 | continue; 501 | } 502 | 503 | // Keep empty comments after child selectors (IE7 hack) 504 | // e.g. html >/**/ body 505 | if (strlen($comment) === 0 && strpos($css, '>/*'.$commentId) !== false) { 506 | $css = str_replace($commentId, $this->registerPreservedToken(''), $css); 507 | continue; 508 | } 509 | 510 | // in all other cases kill the comment 511 | $css = str_replace($commentIdString, '', $css); 512 | } 513 | 514 | // Normalize whitespace again 515 | $css = preg_replace('/ +/S', ' ', $css); 516 | 517 | return $css; 518 | } 519 | 520 | /** 521 | * Finds, minifies & preserves all rule bodies. 522 | * @param string $css the whole stylesheet. 523 | * @return string 524 | */ 525 | private function processRuleBodies($css) 526 | { 527 | $ret = ''; 528 | $searchOffset = $substrOffset = 0; 529 | 530 | while (($blockStartPos = strpos($css, '{', $searchOffset)) !== false) { 531 | $blockEndPos = strpos($css, '}', $blockStartPos); 532 | // When ending curly brace is missing, let's 533 | // behave like there was one at the end of the block... 534 | if ( false === $blockEndPos ) { 535 | $blockEndPos = strlen($css) - 1; 536 | } 537 | $nextBlockStartPos = strpos($css, '{', $blockStartPos + 1); 538 | $ret .= substr($css, $substrOffset, $blockStartPos - $substrOffset); 539 | 540 | if ($nextBlockStartPos !== false && $nextBlockStartPos < $blockEndPos) { 541 | $ret .= substr($css, $blockStartPos, $nextBlockStartPos - $blockStartPos); 542 | $searchOffset = $nextBlockStartPos; 543 | } else { 544 | $ruleBody = substr($css, $blockStartPos + 1, $blockEndPos - $blockStartPos - 1); 545 | $ruleBodyToken = $this->registerRuleBodyToken($this->processRuleBody($ruleBody)); 546 | $ret .= '{'. $ruleBodyToken .'}'; 547 | $searchOffset = $blockEndPos + 1; 548 | } 549 | 550 | $substrOffset = $searchOffset; 551 | } 552 | 553 | $ret .= substr($css, $substrOffset); 554 | 555 | return $ret; 556 | } 557 | 558 | /** 559 | * Compresses non-group rule bodies. 560 | * @param string $body The rule body without curly braces 561 | * @return string 562 | */ 563 | private function processRuleBody($body) 564 | { 565 | $body = trim($body); 566 | 567 | // Remove spaces before the things that should not have spaces before them. 568 | $body = preg_replace('/ ([:=,)*\/;\n])/S', '$1', $body); 569 | 570 | // Remove the spaces after the things that should not have spaces after them. 571 | $body = preg_replace('/([:=,(*\/!;\n]) /S', '$1', $body); 572 | 573 | // Replace multiple semi-colons in a row by a single one 574 | $body = preg_replace('/;;+/S', ';', $body); 575 | 576 | // Remove semicolon before closing brace except when: 577 | // - The last property is prefixed with a `*` (lte IE7 hack) to avoid issues on Symbian S60 3.x browsers. 578 | if (!preg_match('/\*[a-z0-9-]+:[^;]+;$/Si', $body)) { 579 | $body = rtrim($body, ';'); 580 | } 581 | 582 | // Remove important comments inside a rule body (because they make no sense here). 583 | if (strpos($body, '/*') !== false) { 584 | $body = preg_replace('/\n?\/\*[A-Z0-9_]+\*\/\n?/S', '', $body); 585 | } 586 | 587 | // Empty rule body? Exit :) 588 | if (empty($body)) { 589 | return ''; 590 | } 591 | 592 | // Shorten font-weight values 593 | $body = preg_replace( 594 | array('/(font-weight:)bold\b/Si', '/(font-weight:)normal\b/Si'), 595 | array('${1}700', '${1}400'), 596 | $body 597 | ); 598 | 599 | // Shorten background property 600 | $body = preg_replace('/(background:)(?:none|transparent)( !|;|$)/Si', '${1}0 0$2', $body); 601 | 602 | // Shorten opacity IE filter 603 | $body = str_ireplace('progid:DXImageTransform.Microsoft.Alpha(Opacity=', 'alpha(opacity=', $body); 604 | 605 | // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) 606 | // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) 607 | // This makes it more likely that it'll get further compressed in the next step. 608 | $body = preg_replace_callback( 609 | '/(rgb|hsl)\(([0-9,.% -]+)\)(.|$)/Si', 610 | array($this, 'shortenHslAndRgbToHexCallback'), 611 | $body 612 | ); 613 | 614 | // Shorten colors from #AABBCC to #ABC or shorter color name: 615 | // - Look for hex colors which don't have a "=" in front of them (to avoid MSIE filters) 616 | $body = preg_replace_callback( 617 | '/(?unquotedFontsRegex, 626 | array($this, 'preserveUnquotedFontTokens'), 627 | $body 628 | ); 629 | 630 | // Shorten long named colors with a shorter HEX counterpart: white -> #fff. 631 | // Run at least 2 times to cover most cases 632 | $body = preg_replace_callback( 633 | array($this->namedToHexColorsRegex, $this->namedToHexColorsRegex), 634 | array($this, 'shortenNamedColorsCallback'), 635 | $body 636 | ); 637 | 638 | // Restore unquoted font tokens now after colors have been changed. 639 | $body = $this->restoreUnquotedFontTokens($body); 640 | 641 | // Replace positive sign from numbers before the leading space is removed. 642 | // +1.2em to 1.2em, +.8px to .8px, +2% to 2% 643 | $body = preg_replace('/([ :,(])\+(\.?\d+)/S', '$1$2', $body); 644 | 645 | // shorten ms to s 646 | $body = preg_replace_callback('/([ :,(])(-?)(\d{3,})ms/Si', function ($matches) { 647 | return $matches[1] . $matches[2] . ((int) $matches[3] / 1000) .'s'; 648 | }, $body); 649 | 650 | // Remove leading zeros from integer and float numbers. 651 | // 000.6 to .6, -0.8 to -.8, 0050 to 50, -01.05 to -1.05 652 | $body = preg_replace('/([ :,(])(-?)0+([1-9]?\.?\d+)/S', '$1$2$3', $body); 653 | 654 | // Remove trailing zeros from float numbers. 655 | // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px 656 | $body = preg_replace('/([ :,(])(-?\d?\.\d+?)0+([^\d])/S', '$1$2$3', $body); 657 | 658 | // Remove trailing .0 -> -9.0 to -9 659 | $body = preg_replace('/([ :,(])(-?\d+)\.0([^\d])/S', '$1$2$3', $body); 660 | 661 | // Replace 0 length numbers with 0 662 | $body = preg_replace('/([ :,(])-?\.?0+([^\d])/S', '${1}0$2', $body); 663 | 664 | // Shorten zero values for safe properties only 665 | $body = preg_replace( 666 | array( 667 | $this->shortenOneZeroesRegex, 668 | $this->shortenTwoZeroesRegex, 669 | $this->shortenThreeZeroesRegex, 670 | $this->shortenFourZeroesRegex 671 | ), 672 | array( 673 | '$1$2:0', 674 | '$1$2:$3 0', 675 | '$1$2:$3 $4 0', 676 | '$1$2:$3 $4 $5 0' 677 | ), 678 | $body 679 | ); 680 | 681 | // Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property. 682 | $body = preg_replace('/(background-position):0(?: 0){2,3}( !|;|$)/Si', '$1:0 0$2', $body); 683 | 684 | // Shorten suitable shorthand properties with repeated values 685 | $body = preg_replace( 686 | array( 687 | '/(margin|padding|border-(?:width|radius)):('.$this->numRegex.')(?: \2)+( !|;|$)/Si', 688 | '/(border-(?:style|color)):([#a-z0-9]+)(?: \2)+( !|;|$)/Si' 689 | ), 690 | '$1:$2$3', 691 | $body 692 | ); 693 | $body = preg_replace( 694 | array( 695 | '/(margin|padding|border-(?:width|radius)):'. 696 | '('.$this->numRegex.') ('.$this->numRegex.') \2 \3( !|;|$)/Si', 697 | '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) \2 \3( !|;|$)/Si' 698 | ), 699 | '$1:$2 $3$4', 700 | $body 701 | ); 702 | $body = preg_replace( 703 | array( 704 | '/(margin|padding|border-(?:width|radius)):'. 705 | '('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') \3( !|;|$)/Si', 706 | '/(border-(?:style|color)):([#a-z0-9]+) ([#a-z0-9]+) ([#a-z0-9]+) \3( !|;|$)/Si' 707 | ), 708 | '$1:$2 $3 $4$5', 709 | $body 710 | ); 711 | 712 | // Lowercase some common functions that can be values 713 | $body = preg_replace_callback( 714 | '/(?:attr|blur|brightness|circle|contrast|cubic-bezier|drop-shadow|ellipse|from|grayscale|'. 715 | 'hsla?|hue-rotate|inset|invert|local|minmax|opacity|perspective|polygon|rgba?|rect|repeat|saturate|sepia|'. 716 | 'steps|to|url|var|-webkit-gradient|'. 717 | '(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|(?:repeating-)?(?:linear|radial)-gradient))\(/Si', 718 | array($this, 'strtolowerCallback'), 719 | $body 720 | ); 721 | 722 | // Lowercase all uppercase properties 723 | $body = preg_replace_callback('/(?:^|;)[A-Z-]+:/S', array($this, 'strtolowerCallback'), $body); 724 | 725 | return $body; 726 | } 727 | 728 | private function preserveUnquotedFontTokens($matches) 729 | { 730 | return $this->registerUnquotedFontToken($matches[0]); 731 | } 732 | 733 | private function restoreUnquotedFontTokens($body) 734 | { 735 | return strtr($body, $this->unquotedFontTokens); 736 | } 737 | 738 | /** 739 | * Compresses At-rules and selectors. 740 | * @param string $css the whole stylesheet with rule bodies tokenized. 741 | * @return string 742 | */ 743 | private function processAtRulesAndSelectors($css) 744 | { 745 | $charset = ''; 746 | $imports = ''; 747 | $namespaces = ''; 748 | 749 | // Remove spaces before the things that should not have spaces before them. 750 | $css = preg_replace('/ ([@{};>+)\]~=,\/\n])/S', '$1', $css); 751 | 752 | // Remove the spaces after the things that should not have spaces after them. 753 | $css = preg_replace('/([{}:;>+(\[~=,\/\n]) /S', '$1', $css); 754 | 755 | // Shorten shortable double colon (CSS3) pseudo-elements to single colon (CSS2) 756 | $css = preg_replace('/::(before|after|first-(?:line|letter))(\{|,)/Si', ':$1$2', $css); 757 | 758 | // Retain space for special IE6 cases 759 | $css = preg_replace_callback('/:first-(line|letter)(\{|,)/Si', function ($matches) { 760 | return ':first-'. strtolower($matches[1]) .' '. $matches[2]; 761 | }, $css); 762 | 763 | // Find a fraction that may used in some @media queries such as: (min-aspect-ratio: 1/1) 764 | // Add token to add the "/" back in later 765 | $css = preg_replace('/\(([a-z-]+):([0-9]+)\/([0-9]+)\)/Si', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); 766 | 767 | // Remove empty rule blocks up to 2 levels deep. 768 | $css = preg_replace(array_fill(0, 2, '/(\{)[^{};\/\n]+\{\}/S'), '$1', $css); 769 | $css = preg_replace('/[^{};\/\n]+\{\}/S', '', $css); 770 | 771 | // Two important comments next to each other? Remove extra newline. 772 | if ($this->keepImportantComments) { 773 | $css = str_replace("\n\n", "\n", $css); 774 | } 775 | 776 | // Restore fraction 777 | $css = str_replace(self::QUERY_FRACTION, '/', $css); 778 | 779 | // Lowercase some popular @directives 780 | $css = preg_replace_callback( 781 | '/(?charsetRegex, $css, $matches)) { 806 | // Keep the first @charset at-rule found 807 | $charset = $matches[0]; 808 | // Delete all @charset at-rules 809 | $css = preg_replace($this->charsetRegex, '', $css); 810 | } 811 | 812 | // @import handling 813 | $css = preg_replace_callback($this->importRegex, function ($matches) use (&$imports) { 814 | // Keep all @import at-rules found for later 815 | $imports .= $matches[0]; 816 | // Delete all @import at-rules 817 | return ''; 818 | }, $css); 819 | 820 | // @namespace handling 821 | $css = preg_replace_callback($this->namespaceRegex, function ($matches) use (&$namespaces) { 822 | // Keep all @namespace at-rules found for later 823 | $namespaces .= $matches[0]; 824 | // Delete all @namespace at-rules 825 | return ''; 826 | }, $css); 827 | 828 | // Order critical at-rules: 829 | // 1. @charset first 830 | // 2. @imports below @charset 831 | // 3. @namespaces below @imports 832 | $css = $charset . $imports . $namespaces . $css; 833 | 834 | return $css; 835 | } 836 | 837 | /** 838 | * Splits long lines after a specific column. 839 | * 840 | * Some source control tools don't like it when files containing lines longer 841 | * than, say 8000 characters, are checked in. The linebreak option is used in 842 | * that case to split long lines after a specific column. 843 | * 844 | * @param string $css the whole stylesheet. 845 | * @return string 846 | */ 847 | private function processLongLineSplitting($css) 848 | { 849 | if ($this->linebreakPosition > 0) { 850 | $l = strlen($css); 851 | $offset = $this->linebreakPosition; 852 | while (preg_match('/(?linebreakPosition; 856 | $l += 1; 857 | if ($offset > $l) { 858 | break; 859 | } 860 | } 861 | } 862 | 863 | return $css; 864 | } 865 | 866 | /** 867 | * Converts hsl() & rgb() colors to HEX format. 868 | * @param $matches 869 | * @return string 870 | */ 871 | private function shortenHslAndRgbToHexCallback($matches) 872 | { 873 | $type = $matches[1]; 874 | $values = explode(',', $matches[2]); 875 | $terminator = $matches[3]; 876 | 877 | if ($type === 'hsl') { 878 | $values = Utils::hslToRgb($values); 879 | } 880 | 881 | $hexColors = Utils::rgbToHex($values); 882 | 883 | // Restore space after rgb() or hsl() function in some cases such as: 884 | // background-image: linear-gradient(to bottom, rgb(210,180,140) 10%, rgb(255,0,0) 90%); 885 | if (!empty($terminator) && !preg_match('/[ ,);]/S', $terminator)) { 886 | $terminator = ' '. $terminator; 887 | } 888 | 889 | return '#'. implode('', $hexColors) . $terminator; 890 | } 891 | 892 | /** 893 | * Compresses HEX color values of the form #AABBCC to #ABC or short color name. 894 | * @param $matches 895 | * @return string 896 | */ 897 | private function shortenHexColorsCallback($matches) 898 | { 899 | $hex = $matches[1]; 900 | 901 | // Shorten suitable 6 chars HEX colors 902 | if (strlen($hex) === 6 && preg_match('/^([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3$/Si', $hex, $m)) { 903 | $hex = $m[1] . $m[2] . $m[3]; 904 | } 905 | 906 | // Lowercase 907 | $hex = '#'. strtolower($hex); 908 | 909 | // Replace Hex colors with shorter color names 910 | $color = array_key_exists($hex, $this->hexToNamedColorsMap) ? $this->hexToNamedColorsMap[$hex] : $hex; 911 | 912 | return $color . $matches[2]; 913 | } 914 | 915 | /** 916 | * Shortens all named colors with a shorter HEX counterpart for a set of safe properties 917 | * e.g. white -> #fff 918 | * @param array $matches 919 | * @return string 920 | */ 921 | private function shortenNamedColorsCallback($matches) 922 | { 923 | return $matches[1] . $this->namedToHexColorsMap[strtolower($matches[2])] . $matches[3]; 924 | } 925 | 926 | /** 927 | * Makes a string lowercase 928 | * @param array $matches 929 | * @return string 930 | */ 931 | private function strtolowerCallback($matches) 932 | { 933 | return strtolower($matches[0]); 934 | } 935 | } 936 | -------------------------------------------------------------------------------- /cssmin/Utils.php: -------------------------------------------------------------------------------- 1 | 1 ? $vh - 1 : $vh); 68 | 69 | if ($vh * 6 < 1) { 70 | return $v1 + ($v2 - $v1) * 6 * $vh; 71 | } 72 | 73 | if ($vh * 2 < 1) { 74 | return $v2; 75 | } 76 | 77 | if ($vh * 3 < 2) { 78 | return $v1 + ($v2 - $v1) * ((2 / 3) - $vh) * 6; 79 | } 80 | 81 | return $v1; 82 | } 83 | 84 | /** 85 | * Convert strings like "64M" or "30" to int values 86 | * @param mixed $size 87 | * @return int 88 | */ 89 | public static function normalizeInt($size) 90 | { 91 | if (is_string($size)) { 92 | $letter = substr($size, -1); 93 | $size = intval($size); 94 | switch ($letter) { 95 | case 'M': 96 | case 'm': 97 | return (int) $size * 1048576; 98 | case 'K': 99 | case 'k': 100 | return (int) $size * 1024; 101 | case 'G': 102 | case 'g': 103 | return (int) $size * 1073741824; 104 | } 105 | } 106 | return (int) $size; 107 | } 108 | 109 | /** 110 | * Converts a string containing and RGB percentage value into a RGB integer value i.e. '90%' -> 229.5 111 | * @param $rgbPercentage 112 | * @return int 113 | */ 114 | public static function rgbPercentageToRgbInteger($rgbPercentage) 115 | { 116 | if (strpos($rgbPercentage, '%') !== false) { 117 | $rgbPercentage = self::roundNumber(floatval(str_replace('%', '', $rgbPercentage)) * 2.55); 118 | } 119 | 120 | return intval($rgbPercentage, 10); 121 | } 122 | 123 | /** 124 | * Converts a RGB color into a HEX color 125 | * @param array $rgbColors 126 | * @return array 127 | */ 128 | public static function rgbToHex($rgbColors) 129 | { 130 | $hexColors = array(); 131 | 132 | // Values outside the sRGB color space should be clipped (0-255) 133 | for ($i = 0, $l = count($rgbColors); $i < $l; $i++) { 134 | $hexColors[$i] = sprintf("%02x", self::clampNumberSrgb(self::rgbPercentageToRgbInteger($rgbColors[$i]))); 135 | } 136 | 137 | return $hexColors; 138 | } 139 | 140 | /** 141 | * Rounds a number to its closest integer 142 | * @param $n 143 | * @return int 144 | */ 145 | public static function roundNumber($n) 146 | { 147 | return intval(round(floatval($n)), 10); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /cssmin/cssmin.php: -------------------------------------------------------------------------------- 1 | site_url = $site_url; 34 | $this->site_uri_path = parse_url( $site_url, PHP_URL_PATH ); 35 | $this->site_dir = trailingslashit( $site_dir ); 36 | 37 | // Only resolve content URLs if they are under the site URL 38 | if ( $this->is_internal_uri( $content_url ) ) { 39 | $this->content_uri_path = parse_url( trailingslashit( $content_url ), PHP_URL_PATH ); 40 | $this->content_dir = trailingslashit( $content_dir ); 41 | } 42 | 43 | // Only resolve plugin URLs if they are under the site URL 44 | if ( $this->is_internal_uri( $plugin_url ) ) { 45 | $this->plugin_uri_path = parse_url( trailingslashit( $plugin_url ), PHP_URL_PATH ); 46 | $this->plugin_dir = trailingslashit( $plugin_dir ); 47 | } 48 | } 49 | 50 | /** 51 | * Given the full URL of a script/style dependency, return its local filesystem path. 52 | */ 53 | function dependency_src_to_fs_path( $src ) { 54 | if ( ! $this->is_internal_uri( $src ) ) { 55 | // If a URI is not internal, we can have no confidence 56 | // we are resolving to the correct file. 57 | return false; 58 | } 59 | 60 | $src_parts = parse_url( $src ); 61 | if ( false === $src_parts ) { 62 | return false; 63 | } 64 | 65 | if ( empty( $src_parts['path'] ) ) { 66 | // We can't find anything to resolve 67 | return false; 68 | } 69 | $path = $src_parts['path']; 70 | 71 | if ( empty( $src_parts['host'] ) ) { 72 | // With no host, this is a path relative to the WordPress root 73 | $fs_path = "{$this->site_dir}{$path}"; 74 | 75 | return file_exists( $fs_path ) ? $fs_path : false; 76 | } 77 | 78 | return $this->uri_path_to_fs_path( $path ); 79 | } 80 | 81 | /** 82 | * Given a URI path of a script/style resource, return its local filesystem path. 83 | */ 84 | function uri_path_to_fs_path( $uri_path ) { 85 | if ( 1 === preg_match( '#(?:^|/)\.\.?(?:/|$)#', $uri_path ) ) { 86 | // Reject relative paths 87 | return false; 88 | } 89 | 90 | // The plugin URI path may be contained within the content URI path, so we check it before the content URI. 91 | // And both the plugin and content URI paths must be contained within the site URI path, 92 | // so we check them before checking the site URI. 93 | if ( isset( $this->plugin_uri_path ) && static::is_descendant_uri( $this->plugin_uri_path, $uri_path ) ) { 94 | $file_path = $this->plugin_dir . substr( $uri_path, strlen( $this->plugin_uri_path ) ); 95 | } else if ( isset( $this->content_uri_path ) && static::is_descendant_uri( $this->content_uri_path, $uri_path ) ) { 96 | $file_path = $this->content_dir . substr( $uri_path, strlen( $this->content_uri_path ) ); 97 | } else if ( static::is_descendant_uri( $this->site_uri_path, $uri_path ) ) { 98 | $file_path = $this->site_dir . substr( $uri_path, strlen( $this->site_uri_path ) ); 99 | } 100 | 101 | if ( isset( $file_path ) && file_exists( $file_path ) ) { 102 | return $file_path; 103 | } else { 104 | return false; 105 | } 106 | } 107 | 108 | /** 109 | * Determine whether a URI is internal, contained by this site. 110 | * 111 | * This method helps ensure we only resolve to local FS paths. 112 | */ 113 | function is_internal_uri( $uri ) { 114 | if ( page_optimize_starts_with( '/', $uri ) && ! page_optimize_starts_with( '//', $uri ) ) { 115 | // Absolute paths are internal because they are based on the site dir (typically ABSPATH), 116 | // and this looks like an absolute path. 117 | return true; 118 | } 119 | 120 | // To be internal, a URL must have the same scheme, host, and port as the site URL 121 | // and start with the same path as the site URL. 122 | return static::is_descendant_uri( $this->site_url, $uri ); 123 | } 124 | 125 | /** 126 | * Check whether a path is descended from the given directory path. 127 | * 128 | * Does not handle relative paths. 129 | */ 130 | static function is_descendant_uri( $dir_path, $candidate ) { 131 | // Ensure a trailing slash to avoid false matches like 132 | // "/wp-content/resource" being judged a descendant of "/wp". 133 | $dir_path = trailingslashit( $dir_path ); 134 | 135 | return page_optimize_starts_with( $dir_path, $candidate ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /page-optimize.php: -------------------------------------------------------------------------------- 1 | filemtime( $cache_file ) ) { 59 | unlink( $cache_file ); 60 | } 61 | } 62 | } 63 | add_action( PAGE_OPTIMIZE_CRON_CACHE_CLEANUP_JOB, 'page_optimize_cache_cleanup' ); 64 | 65 | // Unschedule cache cleanup, and purge cache directory 66 | function page_optimize_deactivate() { 67 | $cache_folder = false; 68 | if ( defined( 'PAGE_OPTIMIZE_CACHE_DIR' ) && ! empty( PAGE_OPTIMIZE_CACHE_DIR ) ) { 69 | $cache_folder = PAGE_OPTIMIZE_CACHE_DIR; 70 | } 71 | 72 | page_optimize_cache_cleanup( $cache_folder, 0 /* max file age in seconds */ ); 73 | 74 | wp_clear_scheduled_hook( PAGE_OPTIMIZE_CRON_CACHE_CLEANUP_JOB, [ $cache_folder ] ); 75 | } 76 | register_deactivation_hook( __FILE__, 'page_optimize_deactivate' ); 77 | 78 | function page_optimize_uninstall() { 79 | // Run cleanup on uninstall. You can uninstall an active plugin w/o deactivation. 80 | page_optimize_deactivate(); 81 | 82 | // JS 83 | delete_option( 'page_optimize-js' ); 84 | delete_option( 'page_optimize-load-mode' ); 85 | delete_option( 'page_optimize-js-exclude' ); 86 | // CSS 87 | delete_option( 'page_optimize-css' ); 88 | delete_option( 'page_optimize-css-exclude' ); 89 | 90 | } 91 | register_uninstall_hook( __FILE__, 'page_optimize_uninstall' ); 92 | 93 | function page_optimize_get_text_domain() { 94 | return 'page-optimize'; 95 | } 96 | 97 | function page_optimize_should_concat_js() { 98 | // Support query param for easy testing 99 | if ( isset( $_GET['concat-js'] ) ) { 100 | return $_GET['concat-js'] !== '0'; 101 | } 102 | 103 | return !! get_option( 'page_optimize-js', page_optimize_js_default() ); 104 | } 105 | 106 | // TODO: Support JS load mode regardless of whether concat is enabled 107 | function page_optimize_load_mode_js() { 108 | // Support query param for easy testing 109 | if ( ! empty( $_GET['load-mode-js'] ) ) { 110 | $load_mode = page_optimize_sanitize_js_load_mode( $_GET['load-mode-js'] ); 111 | } else { 112 | $load_mode = page_optimize_sanitize_js_load_mode( get_option( 'page_optimize-load-mode', page_optimize_js_load_mode_default() ) ); 113 | } 114 | 115 | return $load_mode; 116 | } 117 | 118 | function page_optimize_should_concat_css() { 119 | // Support query param for easy testing 120 | if ( isset( $_GET['concat-css'] ) ) { 121 | return $_GET['concat-css'] !== '0'; 122 | } 123 | 124 | return !! get_option( 'page_optimize-css', page_optimize_css_default() ); 125 | } 126 | 127 | function page_optimize_js_default() { 128 | return true; 129 | } 130 | 131 | function page_optimize_css_default() { 132 | return true; 133 | } 134 | 135 | function page_optimize_js_load_mode_default() { 136 | return ''; 137 | } 138 | 139 | function page_optimize_js_exclude_list() { 140 | $exclude_list = get_option( 'page_optimize-js-exclude' ); 141 | if ( false === $exclude_list ) { 142 | // Use the default since the option is not set 143 | return page_optimize_js_exclude_list_default(); 144 | } 145 | if ( '' === $exclude_list ) { 146 | return []; 147 | } 148 | 149 | return explode( ',', $exclude_list ); 150 | } 151 | 152 | function page_optimize_js_exclude_list_default() { 153 | // WordPress core stuff, a lot of other plugins depend on it. 154 | return [ 'jquery', 'jquery-core', 'underscore', 'backbone' ]; 155 | } 156 | 157 | function page_optimize_css_exclude_list() { 158 | $exclude_list = get_option( 'page_optimize-css-exclude' ); 159 | if ( false === $exclude_list ) { 160 | // Use the default since the option is not set 161 | return page_optimize_css_exclude_list_default(); 162 | } 163 | if ( '' === $exclude_list ) { 164 | return []; 165 | } 166 | 167 | return explode( ',', $exclude_list ); 168 | } 169 | 170 | function page_optimize_css_exclude_list_default() { 171 | // WordPress Core and known conflicting plugins 172 | return [ 'admin-bar', 'dashicons', 'elementor-app' ]; 173 | } 174 | 175 | function page_optimize_sanitize_js_load_mode( $value ) { 176 | switch ( $value ) { 177 | case 'async': 178 | case 'defer': 179 | break; 180 | default: 181 | $value = ''; 182 | break; 183 | } 184 | 185 | return $value; 186 | } 187 | 188 | function page_optimize_sanitize_exclude_field( $value ) { 189 | if ( empty( $value ) ) { 190 | return ''; 191 | } 192 | 193 | $excluded_strings = explode( ',', sanitize_text_field( $value ) ); 194 | $sanitized_values = []; 195 | foreach ( $excluded_strings as $excluded_string ) { 196 | if ( ! empty( $excluded_string ) ) { 197 | $sanitized_values[] = trim( $excluded_string ); 198 | } 199 | } 200 | 201 | return implode( ',', $sanitized_values ); 202 | } 203 | 204 | /** 205 | * Determines whether a string starts with another string. 206 | */ 207 | function page_optimize_starts_with( $prefix, $str ) { 208 | $prefix_length = strlen( $prefix ); 209 | if ( strlen( $str ) < $prefix_length ) { 210 | return false; 211 | } 212 | 213 | return substr( $str, 0, $prefix_length ) === $prefix; 214 | } 215 | 216 | /** 217 | * Answers whether the plugin should provide concat resource URIs 218 | * that are relative to a common ancestor directory. Assuming a common ancestor 219 | * allows us to skip resolving resource URIs to filesystem paths later on. 220 | */ 221 | function page_optimize_use_concat_base_dir() { 222 | return defined( 'PAGE_OPTIMIZE_CONCAT_BASE_DIR' ) && file_exists( PAGE_OPTIMIZE_CONCAT_BASE_DIR ); 223 | } 224 | 225 | /** 226 | * Get a filesystem path relative to a configured base path for resources 227 | * that will be concatenated. Assuming a common ancestor allows us to skip 228 | * resolving resource URIs to filesystem paths later on. 229 | */ 230 | function page_optimize_remove_concat_base_prefix( $original_fs_path ) { 231 | // Always check longer path first 232 | if ( strlen( PAGE_OPTIMIZE_ABSPATH ) > strlen( PAGE_OPTIMIZE_CONCAT_BASE_DIR ) ) { 233 | $longer_path = PAGE_OPTIMIZE_ABSPATH; 234 | $shorter_path = PAGE_OPTIMIZE_CONCAT_BASE_DIR; 235 | } else { 236 | $longer_path = PAGE_OPTIMIZE_CONCAT_BASE_DIR; 237 | $shorter_path = PAGE_OPTIMIZE_ABSPATH; 238 | } 239 | 240 | $prefix_abspath = trailingslashit( $longer_path ); 241 | if ( page_optimize_starts_with( $prefix_abspath, $original_fs_path ) ) { 242 | return substr( $original_fs_path, strlen( $prefix_abspath ) ); 243 | } 244 | 245 | $prefix_basedir = trailingslashit( $shorter_path ); 246 | if ( page_optimize_starts_with( $prefix_basedir, $original_fs_path ) ) { 247 | return substr( $original_fs_path, strlen( $prefix_basedir ) ); 248 | } 249 | 250 | // If we end up here, this is a resource we shouldn't have tried to concat in the first place 251 | return '/page-optimize-resource-outside-base-path/' . basename( $original_fs_path ); 252 | } 253 | 254 | function page_optimize_schedule_cache_cleanup() { 255 | $cache_folder = false; 256 | if ( defined( 'PAGE_OPTIMIZE_CACHE_DIR' ) && ! empty( PAGE_OPTIMIZE_CACHE_DIR ) ) { 257 | $cache_folder = PAGE_OPTIMIZE_CACHE_DIR; 258 | } 259 | $args = [ $cache_folder ]; 260 | 261 | // If caching is on, and job isn't queued for current cache folder 262 | if( false !== $cache_folder && false === wp_next_scheduled( PAGE_OPTIMIZE_CRON_CACHE_CLEANUP_JOB, $args ) ) { 263 | wp_schedule_event( time(), 'daily', PAGE_OPTIMIZE_CRON_CACHE_CLEANUP_JOB, $args ); 264 | } 265 | } 266 | 267 | // Cases when we don't want to concat 268 | function page_optimize_bail() { 269 | // Bail if we're in customizer 270 | global $wp_customize; 271 | if ( isset( $wp_customize ) ) { 272 | return true; 273 | } 274 | 275 | // Bail if we're in any of the excluded pages. 276 | global $pagenow; 277 | $excluded_pages = array( 278 | 'post.php', 279 | 'post-new.php', 280 | 'site-editor.php', 281 | ); 282 | if ( isset( $pagenow ) && in_array( $pagenow, $excluded_pages ) ) { 283 | return true; 284 | } 285 | 286 | // Bail if Divi theme is active, and we're in the Divi Front End Builder 287 | if ( ! empty( $_GET['et_fb'] ) && 'Divi' === wp_get_theme()->get_template() ) { 288 | return true; 289 | } 290 | 291 | // Bail if we're editing pages in Brizy Editor 292 | if ( class_exists( 'Brizy_Editor' ) && method_exists( 'Brizy_Editor', 'prefix' ) && ( isset( $_GET[ Brizy_Editor::prefix( '-edit-iframe' ) ] ) || isset( $_GET[ Brizy_Editor::prefix( '-edit' ) ] ) ) ) { 293 | return true; 294 | } 295 | 296 | return false; 297 | } 298 | 299 | function page_optimize_init() { 300 | if ( page_optimize_bail() ) { 301 | return; 302 | } 303 | 304 | page_optimize_schedule_cache_cleanup(); 305 | 306 | require_once __DIR__ . '/settings.php'; 307 | require_once __DIR__ . '/concat-css.php'; 308 | require_once __DIR__ . '/concat-js.php'; 309 | 310 | // Disable Jetpack photon-cdn for static JS/CSS 311 | add_filter( 'jetpack_force_disable_site_accelerator', '__return_true' ); 312 | } 313 | add_action( 'plugins_loaded', 'page_optimize_init' ); 314 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Page Optimize === 2 | Contributors: aidvu, bjorsch, bpayton, rcrdortiz 3 | Tags: performance 4 | Requires at least: 5.3 5 | Tested up to: 6.8 6 | Requires PHP: 7.4 7 | Stable tag: 0.5.7 8 | License: GPLv2 or later 9 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | Optimize pages for faster load and render in the browser. 12 | 13 | == Description == 14 | 15 | This plugin supports a few features that may improve the performance of page loading and rendering in the browser: 16 | 17 | * Concatenate CSS 18 | * Concatenate JavaScript 19 | * Execution timing of non-critical scripts 20 | * Note: Changing script execution timing can be risky and will not work well for all sites. 21 | 22 | == Installation == 23 | 24 | This plugin uses sensible defaults so it can operate without configuration, but there are a number of constants you may use for a custom configuration. 25 | 26 | = PAGE_OPTIMIZE_CACHE_DIR = 27 | 28 | Page Optimize caches concatenated scripts and styles by default, and this constant controls where the cache files are stored. The default directory is `cache/page_optimize` under your site's `wp-content` folder. 29 | 30 | To change the cache location, set this constant to the absolute filesystem path of that location. 31 | 32 | To disable caching, set this constant to `false`. Please note that disabling Page Optimize caching may negatively impact performance unless you are caching elsewhere. 33 | 34 | = PAGE_OPTIMIZE_CSS_MINIFY = 35 | 36 | Page Optimize has CSS Minification capabilities which are off by default. 37 | 38 | If you're using caching, and not minifying CSS elsewhere, it is recommended to enable it by setting it to `true`. 39 | 40 | == Testing == 41 | 42 | To test features without enabling them for the entire site, you may append query params to a WordPress post or page URL. For example, to test enabling JavaScript concatenation for `https://example.com/blog/`, you can use the URL `https://example.com/blog/?concat-js=1`. 43 | 44 | Supported query params: 45 | 46 | * `concat-css` controls CSS concatenation. Values: `1` for ON and `0` for OFF. 47 | * `concat-js` controls JavaScript concatenation. Values: `1` for ON and `0` for OFF. 48 | * `load-mode-js` controls how non-critical JavaScript are loaded. Values: 'defer' for [deferred](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-defer), 'async' for [async loading](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script#attr-async), any other value indicates the feature should be disabled. 49 | 50 | == Changelog == 51 | 52 | = 0.5.7 = 53 | * Update Tested Up To Version to 6.8. 54 | 55 | = 0.5.6 = 56 | * Update Tested Up To version to 6.7. 57 | 58 | = 0.5.5 = 59 | * Fix: Stop skipping inline scripts when src is empty. 60 | 61 | = 0.5.4 = 62 | * Bail when editing pages or posts in the Editor. Increased the max concatenated file limit. 63 | 64 | = 0.5.1 = 65 | * Bail when editing pages in Brizy Editor (it errors when JavaScript load mode is `async`). 66 | 67 | = 0.5.0 = 68 | * Apply the `script_loader_tag` filter for scripts that are concatenate-able but have no neighbors to concatenate with. This fixes a case where the TwentyTwenty theme wanted to apply a `defer` attribute to its script but was never given the opportunity. 69 | 70 | = 0.4.5, 0.4.6 = 71 | * Force absolute paths for CSS replacements. 72 | * Lower required PHP version to 7.0. 73 | 74 | = 0.4.4 = 75 | * Don't queue the cache cleaning WP Cron job if we aren't caching. 76 | * Cleanup cache if we turned caching off or directory changed. 77 | 78 | = 0.4.3 = 79 | * gzip in PHP slows stuff down a bit. Simply don't do this. Any web server can handle this better. 80 | * also remove the output buffering, no need for that anymore 81 | * CSS Minification can sometimes slow things down significantly. Add constant to enable/disable. 82 | 83 | = 0.4.2 = 84 | * Initial release. No changes yet. :) 85 | -------------------------------------------------------------------------------- /service.php: -------------------------------------------------------------------------------- 1 | 'text/css', 5 | 'js' => 'application/javascript' 6 | ); 7 | 8 | function page_optimize_service_request() { 9 | $use_cache = defined( 'PAGE_OPTIMIZE_CACHE_DIR' ) && ! empty( PAGE_OPTIMIZE_CACHE_DIR ); 10 | if ( $use_cache && ! is_dir( PAGE_OPTIMIZE_CACHE_DIR ) && ! mkdir( PAGE_OPTIMIZE_CACHE_DIR, 0775, true ) ) { 11 | $use_cache = false; 12 | error_log( sprintf( 13 | /* translators: a filesystem path to a directory */ 14 | __( "Disabling page-optimize cache. Unable to create cache directory '%s'.", page_optimize_get_text_domain() ), 15 | PAGE_OPTIMIZE_CACHE_DIR 16 | ) ); 17 | } 18 | if ( $use_cache && ( ! is_dir( PAGE_OPTIMIZE_CACHE_DIR ) || ! is_writable( PAGE_OPTIMIZE_CACHE_DIR ) || ! is_executable( PAGE_OPTIMIZE_CACHE_DIR ) ) ) { 19 | $use_cache = false; 20 | error_log( sprintf( 21 | /* translators: a filesystem path to a directory */ 22 | __( "Disabling page-optimize cache. Unable to write to cache directory '%s'.", page_optimize_get_text_domain() ), 23 | PAGE_OPTIMIZE_CACHE_DIR 24 | ) ); 25 | } 26 | 27 | if ( $use_cache ) { 28 | $request_uri_hash = md5( $_SERVER['REQUEST_URI'] ); 29 | $cache_file = PAGE_OPTIMIZE_CACHE_DIR . "/page-optimize-cache-$request_uri_hash"; 30 | $cache_file_meta = PAGE_OPTIMIZE_CACHE_DIR . "/page-optimize-cache-meta-$request_uri_hash"; 31 | 32 | if ( file_exists( $cache_file ) ) { 33 | if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) { 34 | if ( strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) < filemtime( $cache_file ) ) { 35 | header( 'HTTP/1.1 304 Not Modified' ); 36 | exit; 37 | } 38 | } 39 | 40 | if ( file_exists( $cache_file_meta ) ) { 41 | $meta = json_decode( file_get_contents( $cache_file_meta ) ); 42 | if ( null !== $meta && isset( $meta->headers ) ) { 43 | foreach ( $meta->headers as $header ) { 44 | header( $header ); 45 | } 46 | } 47 | } 48 | 49 | $etag = '"' . md5( file_get_contents( $cache_file ) ) . '"'; 50 | 51 | header( 'X-Page-Optimize: cached' ); 52 | header( 'Cache-Control: max-age=' . 31536000 ); 53 | header( 'ETag: ' . $etag ); 54 | 55 | echo file_get_contents( $cache_file ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately. 56 | die(); 57 | } 58 | } 59 | 60 | $output = page_optimize_build_output(); 61 | $content = $output['content']; 62 | $headers = $output['headers']; 63 | 64 | foreach( $headers as $header ) { 65 | header( $header ); 66 | } 67 | header( 'X-Page-Optimize: uncached' ); 68 | header( 'Cache-Control: max-age=' . 31536000 ); 69 | header( 'ETag: "' . md5( $content ) . '"' ); 70 | 71 | echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately. 72 | 73 | if ( $use_cache ) { 74 | file_put_contents( $cache_file, $content ); 75 | file_put_contents( $cache_file_meta, json_encode( array( 'headers' => $headers ) ) ); 76 | } 77 | 78 | die(); 79 | } 80 | 81 | function page_optimize_build_output() { 82 | global $page_optimize_types; 83 | 84 | require_once __DIR__ . '/cssmin/cssmin.php'; 85 | 86 | /* Config */ 87 | $concat_max_files = 300; 88 | $concat_unique = true; 89 | 90 | /* Main() */ 91 | if ( ! in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD' ) ) ) { 92 | page_optimize_status_exit( 400 ); 93 | } 94 | 95 | // /_static/??/foo/bar.css,/foo1/bar/baz.css?m=293847g 96 | // or 97 | // /_static/??-eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM= 98 | $args = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY ); 99 | if ( ! $args || false === strpos( $args, '?' ) ) { 100 | page_optimize_status_exit( 400 ); 101 | } 102 | 103 | $args = substr( $args, strpos( $args, '?' ) + 1 ); 104 | 105 | // /foo/bar.css,/foo1/bar/baz.css?m=293847g 106 | // or 107 | // -eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM= 108 | if ( '-' == $args[0] ) { 109 | $args = @gzuncompress( base64_decode( substr( $args, 1 ) ) ); 110 | 111 | // Invalid data, abort! 112 | if ( false === $args ) { 113 | page_optimize_status_exit( 400 ); 114 | } 115 | } 116 | 117 | // /foo/bar.css,/foo1/bar/baz.css?m=293847g 118 | $version_string_pos = strpos( $args, '?' ); 119 | if ( false !== $version_string_pos ) { 120 | $args = substr( $args, 0, $version_string_pos ); 121 | } 122 | 123 | // /foo/bar.css,/foo1/bar/baz.css 124 | $args = explode( ',', $args ); 125 | if ( ! $args ) { 126 | page_optimize_status_exit( 400 ); 127 | } 128 | 129 | // array( '/foo/bar.css', '/foo1/bar/baz.css' ) 130 | if ( 0 == count( $args ) || count( $args ) > $concat_max_files ) { 131 | page_optimize_status_exit( 400 ); 132 | } 133 | 134 | // If we're in a subdirectory context, use that as the root. 135 | // We can't assume that the root serves the same content as the subdir. 136 | $subdir_path_prefix = ''; 137 | $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ); 138 | $_static_index = strpos( $request_path, '/_static/' ); 139 | if ( $_static_index > 0 ) { 140 | $subdir_path_prefix = substr( $request_path, 0, $_static_index ); 141 | } 142 | unset( $request_path, $_static_index ); 143 | 144 | $last_modified = 0; 145 | $pre_output = ''; 146 | $output = ''; 147 | 148 | $should_minify_css = defined( 'PAGE_OPTIMIZE_CSS_MINIFY' ) && ! empty( PAGE_OPTIMIZE_CSS_MINIFY ); 149 | 150 | if ( $should_minify_css ) { 151 | $css_minify = new tubalmartin\CssMin\Minifier; 152 | } 153 | 154 | foreach ( $args as $uri ) { 155 | $fullpath = page_optimize_get_path( $uri ); 156 | 157 | if ( ! file_exists( $fullpath ) ) { 158 | page_optimize_status_exit( 404 ); 159 | } 160 | 161 | $mime_type = page_optimize_get_mime_type( $fullpath ); 162 | if ( ! in_array( $mime_type, $page_optimize_types ) ) { 163 | page_optimize_status_exit( 400 ); 164 | } 165 | 166 | if ( $concat_unique ) { 167 | if ( ! isset( $last_mime_type ) ) { 168 | $last_mime_type = $mime_type; 169 | } 170 | 171 | if ( $last_mime_type != $mime_type ) { 172 | page_optimize_status_exit( 400 ); 173 | } 174 | } 175 | 176 | $stat = stat( $fullpath ); 177 | if ( false === $stat ) { 178 | page_optimize_status_exit( 500 ); 179 | } 180 | 181 | if ( $stat['mtime'] > $last_modified ) { 182 | $last_modified = $stat['mtime']; 183 | } 184 | 185 | $buf = file_get_contents( $fullpath ); 186 | if ( false === $buf ) { 187 | page_optimize_status_exit( 500 ); 188 | } 189 | 190 | if ( 'text/css' == $mime_type ) { 191 | $dirpath = '/' . ltrim( $subdir_path_prefix . dirname( $uri ), '/' ); 192 | 193 | // url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file) 194 | $buf = page_optimize_relative_path_replace( $buf, $dirpath ); 195 | 196 | // AlphaImageLoader(...src='relative/path/to/file'...) -> AlphaImageLoader(...src='/absolute/path/to/file'...) 197 | $buf = preg_replace( 198 | '/(Microsoft.AlphaImageLoader\s*\([^\)]*src=(?:\'|")?)([^\/\'"\s\)](?:(?@charset\s+[\'"][^\'"]+[\'"];)/i', 207 | function ( $match ) { 208 | global $pre_output; 209 | 210 | if ( 0 === strpos( (string) $pre_output, '@charset' ) ) { 211 | return ''; 212 | } 213 | 214 | $pre_output = $match[0] . "\n" . $pre_output; 215 | 216 | return ''; 217 | }, 218 | $buf 219 | ); 220 | } 221 | 222 | // Move the @import rules on top of the concatenated output. 223 | // Only @charset rule are allowed before them. 224 | if ( false !== strpos( $buf, '@import' ) ) { 225 | $buf = preg_replace_callback( 226 | '/(?P@import\s+(?:url\s*\()?[\'"\s]*)(?P[^\'"\s](?:https?:\/\/.+\/?)?.+?)(?P[\'"\s\)]*;)/i', 227 | function ( $match ) use ( $dirpath ) { 228 | global $pre_output; 229 | 230 | if ( 0 !== strpos( $match['path'], 'http' ) && '/' != $match['path'][0] ) { 231 | $pre_output .= $match['pre_path'] . ( $dirpath == '/' ? '/' : $dirpath . '/' ) . 232 | $match['path'] . $match['post_path'] . "\n"; 233 | } else { 234 | $pre_output .= $match[0] . "\n"; 235 | } 236 | 237 | return ''; 238 | }, 239 | $buf 240 | ); 241 | } 242 | 243 | if ( $should_minify_css ) { 244 | $buf = $css_minify->run( $buf ); 245 | } 246 | } 247 | 248 | if ( $page_optimize_types['js'] === $mime_type ) { 249 | $output .= "$buf;\n"; 250 | } else { 251 | $output .= "$buf"; 252 | } 253 | } 254 | 255 | $headers = array( 256 | 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT', 257 | 'Content-Length: ' . ( strlen( $pre_output ) + strlen( $output ) ), 258 | "Content-Type: $mime_type", 259 | ); 260 | 261 | return array( 262 | 'headers' => $headers, 263 | 'content' => $pre_output . $output, 264 | ); 265 | } 266 | 267 | function page_optimize_status_exit( $status ) { 268 | http_response_code( $status ); 269 | exit; 270 | } 271 | 272 | function page_optimize_get_mime_type( $file ) { 273 | global $page_optimize_types; 274 | 275 | $lastdot_pos = strrpos( $file, '.' ); 276 | if ( false === $lastdot_pos ) { 277 | return false; 278 | } 279 | 280 | $ext = substr( $file, $lastdot_pos + 1 ); 281 | 282 | return isset( $page_optimize_types[ $ext ] ) ? $page_optimize_types[ $ext ] : false; 283 | } 284 | 285 | function page_optimize_relative_path_replace( $buf, $dirpath ) { 286 | // url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file) 287 | $buf = preg_replace( 288 | '/(:?\s*url\s*\()\s*(?:\'|")?\s*([^\/\'"\s\)](?:(?uri_path_to_fs_path( $uri ); 319 | } 320 | 321 | if ( false === $path ) { 322 | page_optimize_status_exit( 404 ); 323 | } 324 | 325 | return $path; 326 | } 327 | 328 | page_optimize_service_request(); 329 | -------------------------------------------------------------------------------- /settings.php: -------------------------------------------------------------------------------- 1 | 5 |
6 |

7 |
8 | 13 |
14 |
15 | '; 24 | _e( 25 | 'This plugin disables Jetpack "Speed up static file load times".', 26 | page_optimize_get_text_domain() 27 | ); 28 | } 29 | 30 | function page_optimize_settings_field_js( $args ) { 31 | ?> 32 |
33 | 37 |
38 | 41 |
42 | 47 |
48 | 53 |
54 | 58 | 62 | 66 | 67 |

68 | This option might break your site, so use carefully.', page_optimize_get_text_domain() ); ?> 69 |

70 |
71 | 76 |
77 | 81 |
82 | 85 |
86 | 91 |
92 | __( 'JavaScript concatenation', page_optimize_get_text_domain() ), 113 | 'type' => 'boolean', 114 | 'default' => page_optimize_js_default(), 115 | ) 116 | ); 117 | register_setting( 118 | 'performance', 119 | 'page_optimize-load-mode', 120 | array( 121 | 'description' => __( 'Non-critical script execution mode', page_optimize_get_text_domain() ), 122 | 'type' => 'string', 123 | 'default' => page_optimize_js_load_mode_default(), 124 | 'sanitize_callback' => 'page_optimize_sanitize_js_load_mode', 125 | ) 126 | ); 127 | register_setting( 128 | 'performance', 129 | 'page_optimize-js-exclude', 130 | array( 131 | 'description' => __( 'Comma separated list of strings to exclude from JS concatenating', page_optimize_get_text_domain() ), 132 | 'type' => 'string', 133 | 'default' => implode( ',', page_optimize_js_exclude_list_default() ), 134 | 'sanitize_callback' => 'page_optimize_sanitize_exclude_field', 135 | ) 136 | ); 137 | register_setting( 138 | 'performance', 139 | 'page_optimize-css', 140 | array( 141 | 'description' => __( 'CSS concatenation', page_optimize_get_text_domain() ), 142 | 'type' => 'boolean', 143 | 'default' => page_optimize_css_default(), 144 | ) 145 | ); 146 | register_setting( 147 | 'performance', 148 | 'page_optimize-css-exclude', 149 | array( 150 | 'description' => __( 'Comma separated list of strings to exclude from CSS concating', page_optimize_get_text_domain() ), 151 | 'type' => 'string', 152 | 'default' => implode( ',', page_optimize_css_exclude_list_default() ), 153 | 'sanitize_callback' => 'page_optimize_sanitize_exclude_field', 154 | ) 155 | ); 156 | 157 | add_settings_section( 158 | 'page_optimize_settings_section', 159 | __( 'Page Optimization', page_optimize_get_text_domain() ), 160 | 'page_optimize_settings_section', 161 | 'page-optimize' 162 | ); 163 | add_settings_field( 164 | 'page_optimize_js', 165 | __( 'JavaScript', page_optimize_get_text_domain() ), 166 | 'page_optimize_settings_field_js', 167 | 'page-optimize', 168 | 'page_optimize_settings_section' 169 | ); 170 | add_settings_field( 171 | 'page_optimize_js_load_mode', 172 | __( 'Non-critical script execution mode (experimental)', page_optimize_get_text_domain() ), 173 | 'page_optimize_settings_field_js_load_mode', 174 | 'page-optimize', 175 | 'page_optimize_settings_section' 176 | ); 177 | add_settings_field( 178 | 'page_optimize_css', 179 | __( 'CSS', page_optimize_get_text_domain() ), 180 | 'page_optimize_settings_field_css', 181 | 'page-optimize', 182 | 'page_optimize_settings_section' 183 | ); 184 | } 185 | 186 | add_action( 'admin_init', 'page_optimize_settings_init' ); 187 | 188 | function page_optimize_add_plugin_settings_link( $plugin_action_links, $plugin_file = null ) { 189 | if ( ! ( 'page-optimize/page-optimize.php' === $plugin_file && current_user_can( 'manage_options' ) ) ) { 190 | return $plugin_action_links; 191 | } 192 | 193 | $settings_link = sprintf( 194 | '%s', 195 | __( 'Settings', page_optimize_get_text_domain() ) 196 | ); 197 | array_unshift( $plugin_action_links, $settings_link ); 198 | 199 | return $plugin_action_links; 200 | } 201 | 202 | add_filter( 'plugin_action_links', 'page_optimize_add_plugin_settings_link', 10, 2 ); 203 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | assertEquals( "$site_dir/exists", realpath( $dpm->dependency_src_to_fs_path( '/exists' ) ) ); 36 | $this->assertFalse( $dpm->dependency_src_to_fs_path( '/nonexistent' ) ); 37 | } 38 | } 39 | 40 | class Test_URI_Path_To_File_Mapping extends PHPUnit\Framework\TestCase { 41 | 42 | // TODO: Test URI and FS paths with and without trailing slashes 43 | 44 | /** 45 | * @dataProvider provide_test_data 46 | * @test 47 | */ 48 | function run_test( 49 | $label, 50 | $site_host, 51 | $site_uri_path, 52 | $site_dir, 53 | $content_host, 54 | $content_uri_path, 55 | $content_dir, 56 | $plugin_host, 57 | $plugin_uri_path, 58 | $plugin_dir 59 | ) { 60 | $site_url = "{$site_host}{$site_uri_path}"; 61 | $content_url = "{$content_host}{$content_uri_path}"; 62 | $plugin_url = "{$plugin_host}{$plugin_uri_path}"; 63 | 64 | $root = __DIR__ . '/data/url-to-file-mapping'; 65 | $site_dir = "{$root}{$site_dir}"; 66 | $content_dir = "{$root}{$content_dir}"; 67 | $plugin_dir = "{$root}{$plugin_dir}"; 68 | 69 | $dpm = new Page_Optimize_Dependency_Path_Mapping( 70 | $site_url, 71 | $site_dir, 72 | $content_url, 73 | $content_dir, 74 | $plugin_url, 75 | $plugin_dir 76 | ); 77 | 78 | $this->assertEquals( "$site_dir/exists", $dpm->uri_path_to_fs_path( "$site_uri_path/exists" ), "$label: Cannot find file based on site URI path" ); 79 | $this->assertFalse( $dpm->uri_path_to_fs_path( "$site_uri_path/nonexistent" ), "$label: Should have failed for nonexistent file based on site URI path" ); 80 | 81 | $actual_content_path = $dpm->uri_path_to_fs_path( "$content_uri_path/exists" ); 82 | if ( 0 === strpos( $content_url, $site_url ) ) { 83 | // Content is under site URL. We expect this path to resolve. 84 | $this->assertEquals( "$content_dir/exists", $actual_content_path, "$label: Cannot find file based on content URI path" ); 85 | } else { 86 | // Content is not under site URL. We expect a resolution failure. 87 | $this->assertFalse( $actual_content_path, "$label: Should have failed for content URI path outside of site URL" ); 88 | } 89 | $this->assertFalse( $dpm->uri_path_to_fs_path( "$content_uri_path/nonexistent" ), "$label: Should have failed for nonexistent file based on content URI path" ); 90 | 91 | $actual_plugin_path = $dpm->uri_path_to_fs_path( "$plugin_uri_path/exists" ); 92 | if ( 0 === strpos( $plugin_url, $site_url ) ) { 93 | // Plugins are under site URL. We expect this path to resolve. 94 | $this->assertEquals( "$plugin_dir/exists", $actual_plugin_path, "$label: Cannot find file based on plugin URI path" ); 95 | } else { 96 | // Plugins are not under site URL. We expect a resolution failure. 97 | $this->assertFalse( $actual_plugin_path, "$label: Should have failed for plugin URI path outside of site URL" ); 98 | } 99 | $this->assertFalse( $dpm->uri_path_to_fs_path( "$plugin_uri_path/nonexistent" ), "$label: Should have failed for nonexistent file based on plugin URI path" ); 100 | } 101 | 102 | function provide_test_data() { 103 | return array( 104 | array( 105 | 'Nested site->content->plugin dirs', 106 | 'https://example.com', 107 | '/subdir', 108 | '/site', 109 | 'https://example.com', 110 | '/subdir/wp-content', 111 | '/site/content', 112 | 'https://example.com', 113 | '/subdir/wp-content/plugins', 114 | '/site/content/plugins', 115 | ), 116 | array( 117 | 'Nested content->plugin dirs, separate from ABSPATH', 118 | 'https://example.com', 119 | '/subdir', 120 | '/site', 121 | 'https://example.com', 122 | '/subdir/wp-content', 123 | '/content', 124 | 'https://example.com', 125 | '/subdir/wp-content/plugins', 126 | '/content/plugins' 127 | ), 128 | array( 129 | 'Content and plugin dirs separate from ABSPATH and each other', 130 | 'https://example.com', 131 | '/subdir', 132 | '/site', 133 | 'https://example.com', 134 | '/subdir/wp-content', 135 | '/content', 136 | 'https://example.com', 137 | '/subdir/wp-content/plugins', 138 | '/plugins' 139 | ), 140 | array( 141 | 'Content and plugin URLs have same host but are not under the site URL', 142 | 'https://example.com', 143 | '/subdir', 144 | '/site', 145 | 'https://example.com', 146 | '/wp-content', // Not descended from site URL path 147 | '/site/content', 148 | 'https://example.com', 149 | '/wp-content/plugins', // Not descended from site URL path 150 | '/site/content/plugins' 151 | ), 152 | array( 153 | 'Content and plugin URLs have different host from site URL', 154 | 'https://example.com', 155 | '/subdir', 156 | '/site', 157 | 'https://example.com:1234', 158 | '/subdir/wp-content', 159 | '/site/content', 160 | 'https://other1.com', 161 | '/subdir/wp-content/plugins', 162 | '/site/content/plugins', 163 | ), 164 | ); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /utils.php: -------------------------------------------------------------------------------- 1 | dependency_src_to_fs_path( $url ); 23 | 24 | $mtime = false; 25 | if ( file_exists( $file ) ) { 26 | $mtime = filemtime( $file ); 27 | } 28 | 29 | if ( ! $mtime ) { 30 | return $url; 31 | } 32 | 33 | if ( false === strpos( $url, '?' ) ) { 34 | $q = ''; 35 | } else { 36 | list( $url, $q ) = explode( '?', $url, 2 ); 37 | if ( strlen( $q ) ) { 38 | $q = '&' . $q; 39 | } 40 | } 41 | 42 | return "$url?m={$mtime}{$q}"; 43 | } 44 | } 45 | --------------------------------------------------------------------------------