├── .babelrc ├── .editorconfig ├── .gitignore ├── .phpcs.xml.dist ├── .svnignore ├── .travis.yml ├── .wordpress-org ├── banner-1544x500.jpg ├── banner-772x250.jpg ├── icon-128x128.png ├── icon-256x256..png ├── screenshot-1.png ├── screenshot-2.png ├── screenshot-3.png ├── screenshot-4.png └── screenshot-5.png ├── LICENSE ├── README.md ├── browsersync-config.json ├── composer.json ├── composer.lock ├── css ├── images │ ├── ui-bg_gloss-wave_55_5c9ccc_500x100.png │ └── ui-bg_inset-hard_100_fcfdfd_1x100.png └── progressbar.css ├── includes ├── class-regeneratethumbnails-regenerator.php └── class-regeneratethumbnails-rest-controller.php ├── js └── api-request.min.js ├── package.json ├── phpunit.xml.dist ├── readme.txt ├── regenerate-thumbnails.php ├── src ├── components │ ├── ProgressBar.vue │ ├── ThumbnailSize.vue │ └── ThumbnailStatus.vue ├── helpers │ └── formatUnicorn.js ├── main.js ├── routes.js └── routes │ ├── Home.vue │ ├── RegenerateSingle.vue │ └── subpages │ ├── HomeIntro.vue │ └── HomeRegenerateMultiple.vue ├── tests ├── bin │ ├── install-imagick.sh │ └── install-wp-tests.sh ├── bootstrap.php ├── helper.php ├── test-regenerator.php └── test-rest-api.php ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"] 3 | } -------------------------------------------------------------------------------- /.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 | [{.jshintrc,*.json,*.yml}] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [{*.txt,wp-config-sample.php}] 21 | end_of_line = crlf 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.svn/ 3 | /dist/ 4 | /node_modules/ 5 | /npm-debug.log 6 | /tests/rest-api.http 7 | /vendor/ 8 | /yarn-error.log 9 | .DS_Store -------------------------------------------------------------------------------- /.phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | . 31 | 32 | 33 | 34 | 35 | 36 | 37 | /node_modules/* 38 | /vendor/* 39 | /tests/* 40 | 41 | -------------------------------------------------------------------------------- /.svnignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .editorconfig 3 | .git 4 | .gitignore 5 | .idea 6 | .phpcs.xml.dist 7 | .svnignore 8 | .travis.yml 9 | .wordpress-org 10 | bin 11 | browsersync-config.json 12 | composer.json 13 | composer.lock 14 | LICENSE 15 | node_modules 16 | package.json 17 | phpunit.xml.dist 18 | README.md 19 | src 20 | tests 21 | vendor 22 | webpack.config.js 23 | yarn.lock 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ## Travis CI Configuration File 2 | 3 | sudo: false 4 | language: php 5 | dist: trusty 6 | 7 | cache: 8 | apt: true 9 | directories: 10 | - node_modules 11 | - vendor 12 | - $HOME/.composer/cache 13 | - $HOME/opt/$TRAVIS_PHP_VERSION 14 | 15 | addons: 16 | apt: 17 | packages: 18 | - bc 19 | 20 | # Test against these versions of PHP. 21 | # Some additional PHP versions are also included in the matrix below. 22 | php: 23 | - 5.4 24 | - 5.5 25 | - 5.6 26 | - '7.0' 27 | - 7.1 28 | - 7.2 29 | - 7.3 30 | - nightly 31 | 32 | # Test against these versions of WordPress. 33 | env: 34 | - WP_VERSION=4.7 # Plugin minimum version 35 | - WP_VERSION=4.8 36 | - WP_VERSION=4.9 37 | - WP_VERSION=latest 38 | - WP_VERSION=nightly 39 | 40 | # Some additional environments to test: 41 | # * PHP 5.2 and 5.3 require precise not trusty 42 | # * PHP_CodeSniffer 43 | matrix: 44 | include: 45 | - php: 5.2 46 | dist: precise 47 | env: WP_VERSION=4.7 48 | - php: 5.2 49 | dist: precise 50 | env: WP_VERSION=4.8 51 | - php: 5.2 52 | dist: precise 53 | env: WP_VERSION=latest 54 | - php: 5.2 55 | dist: precise 56 | env: WP_VERSION=nightly 57 | - php: 5.3 58 | dist: precise 59 | env: WP_VERSION=4.7 60 | - php: 5.3 61 | dist: precise 62 | env: WP_VERSION=4.8 63 | - php: 5.3 64 | dist: precise 65 | env: WP_VERSION=latest 66 | - php: 5.3 67 | dist: precise 68 | env: WP_VERSION=nightly 69 | exclude: # PHP 7.x doesn't play nice with older versions of WordPress and/or PHPUnit 70 | - php: '7.0' 71 | env: WP_VERSION=4.7 72 | - php: 7.1 73 | env: WP_VERSION=4.7 74 | - php: 7.2 75 | env: WP_VERSION=4.7 76 | - php: 7.3 77 | env: WP_VERSION=4.7 78 | - php: 7.3 79 | env: WP_VERSION=4.8 80 | - php: 7.3 81 | env: WP_VERSION=4.9 82 | - php: nightly 83 | env: WP_VERSION=4.7 84 | - php: nightly 85 | env: WP_VERSION=4.8 86 | - php: nightly 87 | env: WP_VERSION=4.9 88 | 89 | before_script: 90 | - | 91 | # Export Composer's global bin dir to PATH, but not on PHP 5.2 92 | if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.2" ]]; then 93 | composer config --list --global 94 | export PATH=`composer config --list --global | grep '\[home\]' | { read a; echo "${a#* }/vendor/bin:$PATH"; }` 95 | fi 96 | - | 97 | # Remove Xdebug for a huge performance increase 98 | if [ -f ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini ]; then 99 | phpenv config-rm xdebug.ini 100 | else 101 | echo "xdebug.ini does not exist" 102 | fi 103 | - | 104 | # Install WordPress and PHPUnit 105 | if [[ ! -z "$WP_VERSION" ]] ; then 106 | bash tests/bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 107 | case "$TRAVIS_PHP_VERSION" in 108 | 7.3|7.2|7.1|7.0|nightly) 109 | echo "Using PHPUnit 6.x" 110 | composer global require "phpunit/phpunit:^6" 111 | ;; 112 | 5.6|5.5|5.4|5.3) 113 | echo "Using PHPUnit 4.x" 114 | composer global require "phpunit/phpunit:^4" 115 | ;; 116 | 5.2) 117 | # Do nothing, use default PHPUnit 3.6.x 118 | echo "Using default PHPUnit, hopefully 3.6" 119 | ;; 120 | *) 121 | echo "No PHPUnit version handling for PHP version $TRAVIS_PHP_VERSION" 122 | exit 1 123 | ;; 124 | esac 125 | fi 126 | - | 127 | # Install PHP_CodeSniffer if it's requested for this test 128 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then 129 | composer global require wp-coding-standards/wpcs 130 | phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs 131 | fi 132 | 133 | script: 134 | - | 135 | # Run PHPUnit in both single site and multisite mode 136 | if [[ ! -z "$WP_VERSION" && "$WP_TRAVISCI" != "phpcs" ]] ; then 137 | phpunit --verbose 138 | WP_MULTISITE=1 phpunit --verbose 139 | fi 140 | - | 141 | # Run PHP_CodeSniffer coding standards if it's requested for this test 142 | if [[ "$WP_TRAVISCI" == "phpcs" ]] ; then 143 | phpcs 144 | fi 145 | -------------------------------------------------------------------------------- /.wordpress-org/banner-1544x500.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/banner-1544x500.jpg -------------------------------------------------------------------------------- /.wordpress-org/banner-772x250.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/banner-772x250.jpg -------------------------------------------------------------------------------- /.wordpress-org/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/icon-128x128.png -------------------------------------------------------------------------------- /.wordpress-org/icon-256x256..png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/icon-256x256..png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/screenshot-1.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/screenshot-2.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/screenshot-3.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/screenshot-4.png -------------------------------------------------------------------------------- /.wordpress-org/screenshot-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/.wordpress-org/screenshot-5.png -------------------------------------------------------------------------------- /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. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Regenerate Thumbnails 2 | 3 | [![Travis CI Build Status](https://img.shields.io/travis/Viper007Bond/regenerate-thumbnails/master.svg?style=flat-square)](https://travis-ci.org/Viper007Bond/regenerate-thumbnails) 4 | [![WordPress Plugin Version](https://img.shields.io/wordpress/plugin/v/regenerate-thumbnails.svg?style=flat-square)](https://wordpress.org/plugins/regenerate-thumbnails/) 5 | [![WordPress Tested](https://img.shields.io/wordpress/v/regenerate-thumbnails.svg?style=flat-square)](https://wordpress.org/plugins/regenerate-thumbnails/) 6 | [![WordPress Plugin Downloads](https://img.shields.io/wordpress/plugin/dt/regenerate-thumbnails.svg?style=flat-square)](https://wordpress.org/plugins/regenerate-thumbnails/advanced/) 7 | [![WordPress Plugin Rating](https://img.shields.io/wordpress/plugin/r/regenerate-thumbnails.svg?style=flat-square)](https://wordpress.org/support/plugin/regenerate-thumbnails/reviews/) 8 | 9 | Regenerate Thumbnails is a WordPress plugin that will regenerate all thumbnail sizes for one or more images that have been uploaded to your WordPress Media Library. 10 | 11 | This is useful for situations such as: 12 | 13 | * A new thumbnail size has been added and you want past uploads to have a thumbnail in that size. 14 | * You've changed the dimensions of an existing thumbnail size, for example via Settings → Media. 15 | * You've switched to a new WordPress theme that uses featured images of a different size. 16 | 17 | It also offers the ability to delete old, unused thumbnails as well as update the content of posts to use the new thumbnail sizes. 18 | 19 | ## Alternatives 20 | 21 | ### WP-CLI 22 | 23 | If you have command line access to your server, I highly recommend using [WP-CLI](https://wp-cli.org/) instead of this plugin as it's faster (no HTTP requests overhead) and can be run inside of a `screen` for those with many thumbnails. For details, see the documentation of its [`media regenerate` command](https://developer.wordpress.org/cli/commands/media/regenerate/). 24 | 25 | ### Jetpack's Site Accelerator Module 26 | 27 | [Jetpack](https://jetpack.com/) is a plugin by Automattic, makers of WordPress.com. It gives your self-hosted WordPress site some of the functionality that is available to WordPress.com-hosted sites. 28 | 29 | [The Site Accelerator Module](https://jetpack.com/support/site-accelerator/) makes the images on your site be served from WordPress.com's global content delivery network (CDN) which should speed up the loading of images. Importantly though it can create thumbnails on the fly which means you'll never need to use this plugin. 30 | 31 | ## Building The Plugin 32 | 33 | The latest release can be [downloaded from WordPress.org](https://wordpress.org/plugins/regenerate-thumbnails/), but if you wish to build your own copy, here's how: 34 | 35 | 1. Make sure you have [Node.js](https://nodejs.org/) installed. 36 | 37 | 2. Clone this repository inside your `plugins` directory: 38 | ``` 39 | $ git clone https://github.com/automattic/regenerate-thumbnails.git 40 | $ cd regenerate-thumbnails 41 | ``` 42 | 43 | 3. Install [yarn](https://yarnpkg.com/) package manager. It's like npm but better. 44 | 45 | 4. Install the other dependencies: 46 | ``` 47 | yarn 48 | ``` 49 | 50 | 5. Build the plugin's JavaScript file in production mode: 51 | ``` 52 | yarn build-production 53 | ``` 54 | 55 | 6. Activate the plugin and visit Tools → Regenerate Thumbnails. 56 | 57 | ### Development Mode 58 | 59 | If you're looking to make modifications to this plugin's Vue.js code, run the following command: 60 | 61 | ``` 62 | yarn watch 63 | ``` 64 | 65 | This will do the following things: 66 | 67 | * Automatically rebuild the `build.js` file whenever any of the source files change. 68 | * Put Vue.js in development mode which will allow you to use [a browser extension](https://github.com/vuejs/vue-devtools#vue-devtools) to help with debugging. 69 | * Spawn a [Browsersync](https://www.browsersync.io/) server at [http://localhost:3030/](http://localhost:3030/) that will load a proxied version of your development WordPress install that automatically refresh the page in your browser when changes are made to files. Also if you open the site in multiple browsers, it will sync your navigation and scrolling between them. By default, this assumes that your WordPress install lives at `localhost`. If this is not the case (for example you're using [Varying Vagrant Vagrants](https://varyingvagrantvagrants.org/)), then edit `browsersync-config.json`. 70 | 71 | Alternatively if you just want to manually build a development copy of the Javascript, then run this command: 72 | 73 | ``` 74 | yarn build 75 | ``` 76 | 77 | ## Unit Tests 78 | 79 | To run the [PHPUnit](https://phpunit.de/) unit tests, first run the `install-wp-tests.sh` script from the `bin` directory. Then simply run `phpunit` from the plugin's root directory. 80 | -------------------------------------------------------------------------------- /browsersync-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "WordPressInstallURL": "http://localhost", 3 | 4 | "proxyHost": "localhost", 5 | "proxyPort": 3000 6 | } 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "automattic/regenerate-thumbnails", 3 | "description": "Regenerate the thumbnails for one or more of your image uploads. Useful when changing their sizes or your theme.", 4 | "homepage": "https://alex.blog/wordpress-plugins/regenerate-thumbnails/", 5 | "type": "wordpress-plugin", 6 | "keywords": ["wordpress", "plugin", "thumbnails"], 7 | "license": "GPL-2.0-or-later", 8 | "authors": [ 9 | { 10 | "name": "Alex Mills (Viper007Bond)", 11 | "homepage": "https://alex.blog/" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/automattic/regenerate-thumbnails/issues" 16 | }, 17 | "require": { 18 | "php": ">=5.2.4", 19 | "composer/installers": "~1.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "*", 23 | "dealerdirect/phpcodesniffer-composer-installer": "*", 24 | "wp-coding-standards/wpcs": "*", 25 | "sirbrillig/phpcs-variable-analysis": "*", 26 | "poststatus/wptest": "dev-master", 27 | "phpcompatibility/php-compatibility": "*" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /css/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/css/images/ui-bg_gloss-wave_55_5c9ccc_500x100.png -------------------------------------------------------------------------------- /css/images/ui-bg_inset-hard_100_fcfdfd_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/regenerate-thumbnails/582a6839af829794264fc318e0f6697d19269b03/css/images/ui-bg_inset-hard_100_fcfdfd_1x100.png -------------------------------------------------------------------------------- /css/progressbar.css: -------------------------------------------------------------------------------- 1 | /* This is based on jQuery UI */ 2 | #regenerate-thumbnails-app .ui-progressbar { 3 | height: 2em; 4 | text-align: center; 5 | overflow: hidden; 6 | } 7 | #regenerate-thumbnails-app .ui-progressbar .ui-progressbar-value { 8 | margin: -1px; 9 | height: 100%; 10 | transition-duration: 0.5s; 11 | } 12 | #regenerate-thumbnails-app .ui-widget.ui-widget-content { 13 | border: 1px solid #c5dbec; 14 | } 15 | #regenerate-thumbnails-app .ui-widget-content { 16 | border: 1px solid #a6c9e2; 17 | background: #fcfdfd url("images/ui-bg_inset-hard_100_fcfdfd_1x100.png") 50% bottom repeat-x; 18 | color: #222222; 19 | } 20 | #regenerate-thumbnails-app .ui-widget-header { 21 | border: 1px solid #4297d7; 22 | background: #5c9ccc url("images/ui-bg_gloss-wave_55_5c9ccc_500x100.png") 50% 50% repeat-x; 23 | color: #ffffff; 24 | font-weight: bold; 25 | } 26 | #regenerate-thumbnails-app .ui-corner-all { 27 | border-radius: 5px; 28 | } 29 | #regenerate-thumbnails-app .ui-corner-left { 30 | border-top-left-radius: 5px; 31 | border-bottom-left-radius: 5px; 32 | } 33 | -------------------------------------------------------------------------------- /includes/class-regeneratethumbnails-regenerator.php: -------------------------------------------------------------------------------- 1 | 404, 74 | ) 75 | ); 76 | } 77 | 78 | // We can only regenerate thumbnails for attachments. 79 | if ( 'attachment' !== get_post_type( $attachment ) ) { 80 | return new WP_Error( 81 | 'regenerate_thumbnails_regenerator_not_attachment', 82 | __( 'This item is not an attachment.', 'regenerate-thumbnails' ), 83 | array( 84 | 'status' => 400, 85 | ) 86 | ); 87 | } 88 | 89 | // Don't touch any attachments that are being used as a site icon. Their thumbnails are usually custom cropped. 90 | if ( self::is_site_icon( $attachment ) ) { 91 | return new WP_Error( 92 | 'regenerate_thumbnails_regenerator_is_site_icon', 93 | __( "This attachment is a site icon and therefore the thumbnails shouldn't be touched.", 'regenerate-thumbnails' ), 94 | array( 95 | 'status' => 415, 96 | 'attachment' => $attachment, 97 | ) 98 | ); 99 | } 100 | 101 | return new RegenerateThumbnails_Regenerator( $attachment ); 102 | } 103 | 104 | /** 105 | * The constructor for this class. Don't call this directly, see get_instance() instead. 106 | * This is done so that WP_Error objects can be returned during class initiation. 107 | * 108 | * @since 3.0.0 109 | * 110 | * @param WP_Post $attachment The WP_Post object for the attachment that is being operated on. 111 | */ 112 | private function __construct( WP_Post $attachment ) { 113 | $this->attachment = $attachment; 114 | } 115 | 116 | /** 117 | * Returns whether the attachment is or was a site icon. 118 | * 119 | * @since 3.0.0 120 | * 121 | * @param WP_Post $attachment The WP_Post object for the attachment that is being operated on. 122 | * 123 | * @return bool Whether the attachment is or was a site icon. 124 | */ 125 | public static function is_site_icon( WP_Post $attachment ) { 126 | return ( 'site-icon' === get_post_meta( $attachment->ID, '_wp_attachment_context', true ) ); 127 | } 128 | 129 | /** 130 | * Get the path to the fullsize attachment. 131 | * 132 | * @return string|WP_Error The path to the fullsize attachment, or a WP_Error object on error. 133 | */ 134 | public function get_fullsizepath() { 135 | if ( $this->fullsizepath ) { 136 | return $this->fullsizepath; 137 | } 138 | 139 | if ( function_exists( 'wp_get_original_image_path' ) ) { 140 | $this->fullsizepath = wp_get_original_image_path( $this->attachment->ID ); 141 | } else { 142 | $this->fullsizepath = get_attached_file( $this->attachment->ID ); 143 | } 144 | 145 | if ( false === $this->fullsizepath || ! file_exists( $this->fullsizepath ) ) { 146 | $error = new WP_Error( 147 | 'regenerate_thumbnails_regenerator_file_not_found', 148 | sprintf( 149 | /* translators: The relative upload path to the attachment. */ 150 | __( "The fullsize image file cannot be found in your uploads directory at %s. Without it, new thumbnail images can't be generated.", 'regenerate-thumbnails' ), 151 | _wp_relative_upload_path( $this->fullsizepath ) 152 | ), 153 | array( 154 | 'status' => 404, 155 | 'fullsizepath' => _wp_relative_upload_path( $this->fullsizepath ), 156 | 'attachment' => $this->attachment, 157 | ) 158 | ); 159 | 160 | $this->fullsizepath = $error; 161 | } 162 | 163 | return $this->fullsizepath; 164 | } 165 | 166 | /** 167 | * Regenerate the thumbnails for this instance's attachment. 168 | * 169 | * @since 3.0.0 170 | * 171 | * @param array|string $args { 172 | * Optional. Array or string of arguments for thumbnail regeneration. 173 | * 174 | * @type bool $only_regenerate_missing_thumbnails Skip regenerating existing thumbnail files. Default true. 175 | * @type bool $delete_unregistered_thumbnail_files Delete any thumbnail sizes that are no longer registered. Default false. 176 | * } 177 | * 178 | * @return mixed|WP_Error Metadata for attachment (see wp_generate_attachment_metadata()), or WP_Error on error. 179 | */ 180 | public function regenerate( $args = array() ) { 181 | global $wpdb; 182 | 183 | do_action( 'regenerate_thumbnails_regenerator_pre_regenerate', $this->attachment->ID ); 184 | 185 | $args = wp_parse_args( $args, array( 186 | 'only_regenerate_missing_thumbnails' => true, 187 | 'delete_unregistered_thumbnail_files' => false, 188 | ) ); 189 | 190 | $fullsizepath = $this->get_fullsizepath(); 191 | if ( is_wp_error( $fullsizepath ) ) { 192 | $fullsizepath->add_data( array( 'attachment' => $this->attachment ) ); 193 | 194 | return $fullsizepath; 195 | } 196 | 197 | $this->old_metadata = wp_get_attachment_metadata( $this->attachment->ID ); 198 | 199 | if ( $args['only_regenerate_missing_thumbnails'] ) { 200 | add_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10, 2 ); 201 | } 202 | 203 | require_once( ABSPATH . 'wp-admin/includes/admin.php' ); 204 | $new_metadata = wp_generate_attachment_metadata( $this->attachment->ID, $fullsizepath ); 205 | 206 | if ( $args['only_regenerate_missing_thumbnails'] ) { 207 | // Thumbnail sizes that existed were removed and need to be added back to the metadata. 208 | foreach ( $this->skipped_thumbnails as $skipped_thumbnail ) { 209 | if ( ! empty( $this->old_metadata['sizes'][ $skipped_thumbnail ] ) ) { 210 | $new_metadata['sizes'][ $skipped_thumbnail ] = $this->old_metadata['sizes'][ $skipped_thumbnail ]; 211 | } 212 | } 213 | $this->skipped_thumbnails = array(); 214 | 215 | remove_filter( 'intermediate_image_sizes_advanced', array( $this, 'filter_image_sizes_to_only_missing_thumbnails' ), 10 ); 216 | } 217 | 218 | $wp_upload_dir = dirname( $fullsizepath ) . DIRECTORY_SEPARATOR; 219 | 220 | if ( $args['delete_unregistered_thumbnail_files'] ) { 221 | // Delete old sizes that are still in the metadata. 222 | $intermediate_image_sizes = get_intermediate_image_sizes(); 223 | foreach ( $this->old_metadata['sizes'] as $old_size => $old_size_data ) { 224 | if ( in_array( $old_size, $intermediate_image_sizes ) ) { 225 | continue; 226 | } 227 | 228 | wp_delete_file( $wp_upload_dir . $old_size_data['file'] ); 229 | 230 | unset( $new_metadata['sizes'][ $old_size ] ); 231 | } 232 | 233 | $relative_path = dirname( $new_metadata['file'] ) . DIRECTORY_SEPARATOR; 234 | 235 | // It's possible to upload an image with a filename like image-123x456.jpg and it shouldn't be deleted. 236 | $whitelist = $wpdb->get_col( $wpdb->prepare( " 237 | SELECT 238 | meta_value 239 | FROM 240 | {$wpdb->postmeta} 241 | WHERE 242 | meta_key = '_wp_attached_file' 243 | AND meta_value REGEXP %s 244 | /* Regenerate Thumbnails */ 245 | ", 246 | '^' . preg_quote( $relative_path ) . '[^' . preg_quote( DIRECTORY_SEPARATOR ) . ']+-[0-9]+x[0-9]+\.' 247 | ) ); 248 | $whitelist = array_map( 'basename', $whitelist ); 249 | 250 | $filelist = array(); 251 | foreach ( scandir( $wp_upload_dir ) as $file ) { 252 | if ( '.' == $file || '..' == $file || ! is_file( $wp_upload_dir . $file ) ) { 253 | continue; 254 | } 255 | 256 | $filelist[] = $file; 257 | } 258 | 259 | $registered_thumbnails = array(); 260 | foreach ( $new_metadata['sizes'] as $size ) { 261 | $registered_thumbnails[] = $size['file']; 262 | } 263 | 264 | $fullsize_parts = pathinfo( $fullsizepath ); 265 | 266 | foreach ( $filelist as $file ) { 267 | if ( in_array( $file, $whitelist ) || in_array( $file, $registered_thumbnails ) ) { 268 | continue; 269 | } 270 | 271 | if ( ! preg_match( '#^' . preg_quote( $fullsize_parts['filename'], '#' ) . '-[0-9]+x[0-9]+\.' . preg_quote( $fullsize_parts['extension'], '#' ) . '$#', $file ) ) { 272 | continue; 273 | } 274 | 275 | wp_delete_file( $wp_upload_dir . $file ); 276 | } 277 | } elseif ( ! empty( $this->old_metadata ) && ! empty( $this->old_metadata['sizes'] ) && is_array( $this->old_metadata['sizes'] ) ) { 278 | // If not deleting, rename any size conflicts to avoid them being lost if the file still exists. 279 | foreach ( $this->old_metadata['sizes'] as $old_size => $old_size_data ) { 280 | if ( empty( $new_metadata['sizes'][ $old_size ] ) ) { 281 | $new_metadata['sizes'][ $old_size ] = $this->old_metadata['sizes'][ $old_size ]; 282 | continue; 283 | } 284 | 285 | $new_size_data = $new_metadata['sizes'][ $old_size ]; 286 | 287 | if ( 288 | $new_size_data['width'] !== $old_size_data['width'] 289 | && $new_size_data['height'] !== $old_size_data['height'] 290 | && file_exists( $wp_upload_dir . $old_size_data['file'] ) 291 | ) { 292 | $new_metadata['sizes'][ $old_size . '_old_' . $old_size_data['width'] . 'x' . $old_size_data['height'] ] = $old_size_data; 293 | } 294 | } 295 | } 296 | 297 | wp_update_attachment_metadata( $this->attachment->ID, $new_metadata ); 298 | 299 | return $new_metadata; 300 | } 301 | 302 | /** 303 | * Filters the list of thumbnail sizes to only include those which have missing files. 304 | * 305 | * @since 3.0.0 306 | * 307 | * @param array $sizes An associative array of registered thumbnail image sizes. 308 | * @param array $fullsize_metadata An associative array of fullsize image metadata: width, height, file. 309 | * 310 | * @return array An associative array of image sizes. 311 | */ 312 | public function filter_image_sizes_to_only_missing_thumbnails( $sizes, $fullsize_metadata ) { 313 | if ( ! $sizes ) { 314 | return $sizes; 315 | } 316 | 317 | $fullsizepath = $this->get_fullsizepath(); 318 | if ( is_wp_error( $fullsizepath ) ) { 319 | return $sizes; 320 | } 321 | 322 | $editor = wp_get_image_editor( $fullsizepath ); 323 | if ( is_wp_error( $editor ) ) { 324 | return $sizes; 325 | } 326 | 327 | $metadata = $this->old_metadata; 328 | 329 | // This is based on WP_Image_Editor_GD::multi_resize() and others. 330 | foreach ( $sizes as $size => $size_data ) { 331 | if ( empty( $metadata['sizes'][ $size ] ) ) { 332 | continue; 333 | } 334 | 335 | if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) { 336 | continue; 337 | } 338 | 339 | if ( ! isset( $size_data['width'] ) ) { 340 | $size_data['width'] = null; 341 | } 342 | if ( ! isset( $size_data['height'] ) ) { 343 | $size_data['height'] = null; 344 | } 345 | 346 | if ( ! isset( $size_data['crop'] ) ) { 347 | $size_data['crop'] = false; 348 | } 349 | 350 | $thumbnail = $this->get_thumbnail( 351 | $editor, 352 | $fullsize_metadata['width'], 353 | $fullsize_metadata['height'], 354 | $size_data['width'], 355 | $size_data['height'], 356 | $size_data['crop'] 357 | ); 358 | 359 | 360 | // The false check filters out thumbnails that would be larger than the fullsize image. 361 | // The size comparison makes sure that the size is also correct. 362 | if ( 363 | false === $thumbnail 364 | || ( 365 | $thumbnail['width'] === $metadata['sizes'][ $size ]['width'] 366 | && $thumbnail['height'] === $metadata['sizes'][ $size ]['height'] 367 | && file_exists( $thumbnail['filename'] ) 368 | ) 369 | ) { 370 | $this->skipped_thumbnails[] = $size; 371 | unset( $sizes[ $size ] ); 372 | } 373 | } 374 | 375 | /** 376 | * Filters the list of missing thumbnail sizes if you want to add/remove any. 377 | * 378 | * @since 3.1.0 379 | * 380 | * @param array $sizes An associative array of image sizes that are missing. 381 | * @param array $fullsize_metadata An associative array of fullsize image metadata: width, height, file. 382 | * @param object $this The current instance of this class. 383 | * 384 | * @return array An associative array of image sizes. 385 | */ 386 | return apply_filters( 'regenerate_thumbnails_missing_thumbnails', $sizes, $fullsize_metadata, $this ); 387 | } 388 | 389 | /** 390 | * Generate the thumbnail filename and dimensions for a given set of constraint dimensions. 391 | * 392 | * @since 3.0.0 393 | * 394 | * @param WP_Image_Editor|WP_Error $editor An instance of WP_Image_Editor, as returned by wp_get_image_editor(). 395 | * @param int $fullsize_width The width of the fullsize image. 396 | * @param int $fullsize_height The height of the fullsize image. 397 | * @param int $thumbnail_width The width of the thumbnail. 398 | * @param int $thumbnail_height The height of the thumbnail. 399 | * @param bool $crop Whether to crop or not. 400 | * 401 | * @return array|false An array of the filename, thumbnail width, and thumbnail height, 402 | * or false on failure to resize such as the thumbnail being larger than the fullsize image. 403 | */ 404 | public function get_thumbnail( $editor, $fullsize_width, $fullsize_height, $thumbnail_width, $thumbnail_height, $crop ) { 405 | $dims = image_resize_dimensions( $fullsize_width, $fullsize_height, $thumbnail_width, $thumbnail_height, $crop ); 406 | 407 | if ( ! $dims ) { 408 | return false; 409 | } 410 | 411 | list( , , , , $dst_w, $dst_h ) = $dims; 412 | 413 | $suffix = "{$dst_w}x{$dst_h}"; 414 | $file_ext = strtolower( pathinfo( $this->get_fullsizepath(), PATHINFO_EXTENSION ) ); 415 | 416 | return array( 417 | 'filename' => $editor->generate_filename( $suffix, null, $file_ext ), 418 | 'width' => $dst_w, 419 | 'height' => $dst_h, 420 | ); 421 | } 422 | 423 | /** 424 | * Update the post content of any public post types (posts and pages by default) 425 | * that make use of this attachment. 426 | * 427 | * @since 3.0.0 428 | * 429 | * @param array|string $args { 430 | * Optional. Array or string of arguments for controlling the updating. 431 | * 432 | * @type array $post_type The post types to update. Defaults to public post types (posts and pages by default). 433 | * @type array $post_ids Specific post IDs to update as opposed to any that uses the attachment. 434 | * @type int $posts_per_loop How many posts to query at a time to keep memory usage down. You shouldn't need to modify this. 435 | * } 436 | * 437 | * @return array|WP_Error List of post IDs that were modified. The key is the post ID and the value is either the post ID again or a WP_Error object if wp_update_post() failed. 438 | */ 439 | public function update_usages_in_posts( $args = array() ) { 440 | // Temporarily disabled until it can be even better tested for edge cases 441 | return array(); 442 | 443 | $args = wp_parse_args( $args, array( 444 | 'post_type' => array(), 445 | 'post_ids' => array(), 446 | 'posts_per_loop' => 10, 447 | ) ); 448 | 449 | if ( empty( $args['post_type'] ) ) { 450 | $args['post_type'] = array_values( get_post_types( array( 'public' => true ) ) ); 451 | unset( $args['post_type']['attachment'] ); 452 | } 453 | 454 | $offset = 0; 455 | $posts_updated = array(); 456 | 457 | while ( true ) { 458 | $posts = get_posts( array( 459 | 'numberposts' => $args['posts_per_loop'], 460 | 'offset' => $offset, 461 | 'orderby' => 'ID', 462 | 'order' => 'ASC', 463 | 'include' => $args['post_ids'], 464 | 'post_type' => $args['post_type'], 465 | 's' => 'wp-image-' . $this->attachment->ID, 466 | 467 | // For faster queries. 468 | 'update_post_meta_cache' => false, 469 | 'update_post_term_cache' => false, 470 | ) ); 471 | 472 | if ( ! $posts ) { 473 | break; 474 | } 475 | 476 | $offset += $args['posts_per_loop']; 477 | 478 | foreach ( $posts as $post ) { 479 | $content = $post->post_content; 480 | $search = array(); 481 | $replace = array(); 482 | 483 | // Find all tags for this attachment and update them. 484 | preg_match_all( 485 | '#]+wp-image-' . $this->attachment->ID . '[^>]+/>#i', 486 | $content, 487 | $matches, 488 | PREG_SET_ORDER 489 | ); 490 | if ( $matches ) { 491 | foreach ( $matches as $img_tag ) { 492 | preg_match( '# class="([^"]+)?size-([^" ]+)#i', $img_tag[0], $thumbnail_size ); 493 | 494 | if ( $thumbnail_size ) { 495 | $thumbnail = image_downsize( $this->attachment->ID, $thumbnail_size[2] ); 496 | 497 | if ( ! $thumbnail ) { 498 | continue; 499 | } 500 | 501 | $search[] = $img_tag[0]; 502 | 503 | $img_tag[0] = preg_replace( '# src="[^"]+"#i', ' src="' . esc_url( $thumbnail[0] ) . '"', $img_tag[0] ); 504 | $img_tag[0] = preg_replace( 505 | '# width="[^"]+" height="[^"]+"#i', 506 | ' width="' . esc_attr( $thumbnail[1] ) . '" height="' . esc_attr( $thumbnail[2] ) . '"', 507 | $img_tag[0] 508 | ); 509 | 510 | $replace[] = $img_tag[0]; 511 | } 512 | } 513 | } 514 | $content = str_replace( $search, $replace, $content ); 515 | $search = array(); 516 | $replace = array(); 517 | 518 | // Update the width in any [caption] shortcodes. 519 | preg_match_all( 520 | '#\[caption id="attachment_' . $this->attachment->ID . '"([^\]]+)? width="[^"]+"\]([^\[]+)size-([^" ]+)([^\[]+)\[\/caption\]#i', 521 | $content, 522 | $matches, 523 | PREG_SET_ORDER 524 | ); 525 | if ( $matches ) { 526 | foreach ( $matches as $match ) { 527 | $thumbnail = image_downsize( $this->attachment->ID, $match[3] ); 528 | 529 | if ( ! $thumbnail ) { 530 | continue; 531 | } 532 | 533 | $search[] = $match[0]; 534 | $replace[] = '[caption id="attachment_' . $this->attachment->ID . '"' . $match[1] . ' width="' . esc_attr( $thumbnail[1] ) . '"]' . $match[2] . 'size-' . $match[3] . $match[4] . '[/caption]'; 535 | } 536 | } 537 | $content = str_replace( $search, $replace, $content ); 538 | 539 | $updated_post_object = (object) array( 540 | 'ID' => $post->ID, 541 | 'post_content' => $content, 542 | ); 543 | 544 | $posts_updated[ $post->ID ] = wp_update_post( $updated_post_object, true ); 545 | } 546 | } 547 | 548 | return $posts_updated; 549 | } 550 | 551 | /** 552 | * Returns information about the current attachment for use in the REST API. 553 | * 554 | * @since 3.0.0 555 | * 556 | * @return array|WP_Error The attachment name, fullsize URL, registered thumbnail size status, and any unregistered sizes, or WP_Error on error. 557 | */ 558 | public function get_attachment_info() { 559 | $fullsizepath = $this->get_fullsizepath(); 560 | if ( is_wp_error( $fullsizepath ) ) { 561 | $fullsizepath->add_data( array( 'attachment' => $this->attachment ) ); 562 | 563 | return $fullsizepath; 564 | } 565 | 566 | $editor = wp_get_image_editor( $fullsizepath ); 567 | if ( is_wp_error( $editor ) ) { 568 | // Display a more helpful error message. 569 | if ( 'image_no_editor' === $editor->get_error_code() ) { 570 | $editor = new WP_Error( 'image_no_editor', __( 'The current image editor cannot process this file type.', 'regenerate-thumbnails' ) ); 571 | } 572 | 573 | $editor->add_data( array( 574 | 'attachment' => $this->attachment, 575 | 'status' => 415, 576 | ) ); 577 | 578 | return $editor; 579 | } 580 | 581 | $metadata = wp_get_attachment_metadata( $this->attachment->ID ); 582 | 583 | if ( false === $metadata || ! is_array( $metadata ) ) { 584 | return new WP_Error( 585 | 'regenerate_thumbnails_regenerator_no_metadata', 586 | __( 'Unable to load the metadata for this attachment.', 'regenerate-thumbnails' ), 587 | array( 588 | 'status' => 404, 589 | 'attachment' => $this->attachment, 590 | ) 591 | ); 592 | } 593 | 594 | if ( ! isset( $metadata['sizes'] ) ) { 595 | $metadata['sizes'] = array(); 596 | } 597 | 598 | // PDFs don't have width/height set. 599 | $width = ( isset( $metadata['width'] ) ) ? $metadata['width'] : null; 600 | $height = ( isset( $metadata['height'] ) ) ? $metadata['height'] : null; 601 | 602 | require_once( ABSPATH . '/wp-admin/includes/image.php' ); 603 | 604 | $preview = false; 605 | if ( file_is_displayable_image( $fullsizepath ) ) { 606 | $preview = wp_get_attachment_url( $this->attachment->ID ); 607 | } elseif ( 608 | is_array( $metadata['sizes'] ) && 609 | is_array( $metadata['sizes']['full'] ) && 610 | ! empty( $metadata['sizes']['full']['file'] ) 611 | ) { 612 | $preview = str_replace( 613 | wp_basename( $fullsizepath ), 614 | $metadata['sizes']['full']['file'], 615 | wp_get_attachment_url( $this->attachment->ID ) 616 | ); 617 | 618 | if ( ! file_exists( $preview ) ) { 619 | $preview = false; 620 | } 621 | } 622 | 623 | $response = array( 624 | 'name' => ( $this->attachment->post_title ) ? $this->attachment->post_title : sprintf( __( 'Attachment %d', 'regenerate-thumbnails' ), $this->attachment->ID ), 625 | 'preview' => $preview, 626 | 'relative_path' => _wp_get_attachment_relative_path( $fullsizepath ) . DIRECTORY_SEPARATOR . wp_basename( $fullsizepath ), 627 | 'edit_url' => get_edit_post_link( $this->attachment->ID, 'raw' ), 628 | 'width' => $width, 629 | 'height' => $height, 630 | 'registered_sizes' => array(), 631 | 'unregistered_sizes' => array(), 632 | ); 633 | 634 | $wp_upload_dir = dirname( $fullsizepath ) . DIRECTORY_SEPARATOR; 635 | 636 | $registered_sizes = RegenerateThumbnails()->get_thumbnail_sizes(); 637 | 638 | if ( 'application/pdf' === get_post_mime_type( $this->attachment ) ) { 639 | $registered_sizes = array_intersect_key( 640 | $registered_sizes, 641 | array( 642 | 'thumbnail' => true, 643 | 'medium' => true, 644 | 'large' => true, 645 | ) 646 | ); 647 | } 648 | 649 | // Check the status of all currently registered sizes. 650 | foreach ( $registered_sizes as $size ) { 651 | // Width and height are needed to generate the thumbnail filename. 652 | if ( $width && $height ) { 653 | $thumbnail = $this->get_thumbnail( $editor, $width, $height, $size['width'], $size['height'], $size['crop'] ); 654 | 655 | if ( $thumbnail ) { 656 | $size['filename'] = wp_basename( $thumbnail['filename'] ); 657 | $size['fileexists'] = file_exists( $thumbnail['filename'] ); 658 | } else { 659 | $size['filename'] = false; 660 | $size['fileexists'] = false; 661 | } 662 | } elseif ( ! empty( $metadata['sizes'][ $size['label'] ]['file'] ) ) { 663 | $size['filename'] = wp_basename( $metadata['sizes'][ $size['label'] ]['file'] ); 664 | $size['fileexists'] = file_exists( $wp_upload_dir . $metadata['sizes'][ $size['label'] ]['file'] ); 665 | } else { 666 | $size['filename'] = false; 667 | $size['fileexists'] = false; 668 | } 669 | 670 | $response['registered_sizes'][] = $size; 671 | } 672 | 673 | if ( ! $width && ! $height && is_array( $metadata['sizes']['full'] ) ) { 674 | $response['registered_sizes'][] = array( 675 | 'label' => 'full', 676 | 'width' => $metadata['sizes']['full']['width'], 677 | 'height' => $metadata['sizes']['full']['height'], 678 | 'filename' => $metadata['sizes']['full']['file'], 679 | 'fileexists' => file_exists( $wp_upload_dir . $metadata['sizes']['full']['file'] ), 680 | ); 681 | } 682 | 683 | // Look at the attachment metadata and see if we have any extra files from sizes that are no longer registered. 684 | foreach ( $metadata['sizes'] as $label => $size ) { 685 | if ( ! file_exists( $wp_upload_dir . $size['file'] ) ) { 686 | continue; 687 | } 688 | 689 | // An unregistered size could match a registered size's dimensions. Ignore these. 690 | foreach ( $response['registered_sizes'] as $registered_size ) { 691 | if ( $size['file'] === $registered_size['filename'] ) { 692 | continue 2; 693 | } 694 | } 695 | 696 | if ( ! empty( $registered_sizes[ $label ] ) ) { 697 | /* translators: Used for listing old sizes of currently registered thumbnails */ 698 | $label = sprintf( __( '%s (old)', 'regenerate-thumbnails' ), $label ); 699 | } 700 | 701 | $response['unregistered_sizes'][] = array( 702 | 'label' => $label, 703 | 'width' => $size['width'], 704 | 'height' => $size['height'], 705 | 'filename' => $size['file'], 706 | 'fileexists' => true, 707 | ); 708 | } 709 | 710 | return $response; 711 | } 712 | } 713 | -------------------------------------------------------------------------------- /includes/class-regeneratethumbnails-rest-controller.php: -------------------------------------------------------------------------------- 1 | namespace, '/regenerate/(?P[\d]+)', array( 31 | array( 32 | 'methods' => WP_REST_Server::ALLMETHODS, 33 | 'callback' => array( $this, 'regenerate_item' ), 34 | 'permission_callback' => array( $this, 'permissions_check' ), 35 | 'args' => array( 36 | 'only_regenerate_missing_thumbnails' => array( 37 | 'description' => __( "Whether to only regenerate missing thumbnails. It's faster with this enabled.", 'regenerate-thumbnails' ), 38 | 'type' => 'boolean', 39 | 'default' => true, 40 | ), 41 | 'delete_unregistered_thumbnail_files' => array( 42 | 'description' => __( 'Whether to delete any old, now unregistered thumbnail files.', 'regenerate-thumbnails' ), 43 | 'type' => 'boolean', 44 | 'default' => false, 45 | ), 46 | 'update_usages_in_posts' => array( 47 | 'description' => __( 'Whether to update the image tags in any posts that make use of this attachment.', 'regenerate-thumbnails' ), 48 | 'type' => 'boolean', 49 | 'default' => true, 50 | ), 51 | 'update_usages_in_posts_post_type' => array( 52 | 'description' => __( 'The types of posts to update. Defaults to all public post types.', 'regenerate-thumbnails' ), 53 | 'type' => 'array', 54 | 'default' => array(), 55 | 'validate_callback' => array( $this, 'is_array' ), 56 | ), 57 | 'update_usages_in_posts_post_ids' => array( 58 | 'description' => __( 'Specific post IDs to update rather than any posts that use this attachment.', 'regenerate-thumbnails' ), 59 | 'type' => 'array', 60 | 'default' => array(), 61 | 'validate_callback' => array( $this, 'is_array' ), 62 | ), 63 | 'update_usages_in_posts_posts_per_loop' => array( 64 | 'description' => __( "Posts to process per loop. This is to control memory usage and you likely don't need to adjust this.", 'regenerate-thumbnails' ), 65 | 'type' => 'integer', 66 | 'default' => 10, 67 | 'sanitize_callback' => 'absint', 68 | ), 69 | ), 70 | ), 71 | ) ); 72 | 73 | register_rest_route( $this->namespace, '/attachmentinfo/(?P[\d]+)', array( 74 | array( 75 | 'methods' => WP_REST_Server::READABLE, 76 | 'callback' => array( $this, 'attachment_info' ), 77 | 'permission_callback' => array( $this, 'permissions_check' ), 78 | ), 79 | ) ); 80 | 81 | register_rest_route( $this->namespace, '/featuredimages', array( 82 | array( 83 | 'methods' => WP_REST_Server::READABLE, 84 | 'callback' => array( $this, 'featured_images' ), 85 | 'permission_callback' => array( $this, 'permissions_check' ), 86 | 'args' => $this->get_paging_collection_params(), 87 | ), 88 | ) ); 89 | } 90 | 91 | /** 92 | * Register a filter to allow excluding site icons via a query parameter. 93 | * 94 | * @since 3.0.0 95 | */ 96 | public function register_filters() { 97 | add_filter( 'rest_attachment_query', array( $this, 'maybe_filter_out_site_icons' ), 10, 2 ); 98 | add_filter( 'rest_attachment_query', array( $this, 'maybe_filter_mimes_types' ), 10, 2 ); 99 | } 100 | 101 | /** 102 | * If the exclude_site_icons parameter is set on a media (attachment) request, 103 | * filter out any attachments that are or were being used as a site icon. 104 | * 105 | * @param array $args Key value array of query var to query value. 106 | * @param WP_REST_Request $request The request used. 107 | * 108 | * @return array Key value array of query var to query value. 109 | */ 110 | public function maybe_filter_out_site_icons( $args, $request ) { 111 | if ( empty( $request['exclude_site_icons'] ) ) { 112 | return $args; 113 | } 114 | 115 | if ( ! isset( $args['meta_query'] ) ) { 116 | $args['meta_query'] = array(); 117 | } 118 | 119 | $args['meta_query'][] = array( 120 | 'key' => '_wp_attachment_context', 121 | 'value' => 'site-icon', 122 | 'compare' => 'NOT EXISTS', 123 | ); 124 | 125 | return $args; 126 | } 127 | 128 | /** 129 | * If the is_regeneratable parameter is set on a media (attachment) request, 130 | * filter results to only include images and PDFs. 131 | * 132 | * @param array $args Key value array of query var to query value. 133 | * @param WP_REST_Request $request The request used. 134 | * 135 | * @return array Key value array of query var to query value. 136 | */ 137 | public function maybe_filter_mimes_types( $args, $request ) { 138 | if ( empty( $request['is_regeneratable'] ) ) { 139 | return $args; 140 | } 141 | 142 | $args['post_mime_type'] = array(); 143 | foreach ( get_allowed_mime_types() as $mime_type ) { 144 | if ( 'image/svg+xml' === $mime_type ) { 145 | continue; 146 | } 147 | 148 | if ( 'application/pdf' == $mime_type || 'image/' == substr( $mime_type, 0, 6 ) ) { 149 | $args['post_mime_type'][] = $mime_type; 150 | } 151 | } 152 | 153 | return $args; 154 | } 155 | 156 | /** 157 | * Retrieves the paging query params for the collections. 158 | * 159 | * @since 3.0.0 160 | * 161 | * @return array Query parameters for the collection. 162 | */ 163 | public function get_paging_collection_params() { 164 | return array_intersect_key( 165 | parent::get_collection_params(), 166 | array_flip( array( 'page', 'per_page' ) ) 167 | ); 168 | } 169 | 170 | /** 171 | * Regenerate the thumbnails for a specific media item. 172 | * 173 | * @since 3.0.0 174 | * 175 | * @param WP_REST_Request $request Full data about the request. 176 | * 177 | * @return true|WP_Error True on success, otherwise a WP_Error object. 178 | */ 179 | public function regenerate_item( $request ) { 180 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $request->get_param( 'id' ) ); 181 | 182 | if ( is_wp_error( $regenerator ) ) { 183 | return $regenerator; 184 | } 185 | 186 | $result = $regenerator->regenerate( array( 187 | 'only_regenerate_missing_thumbnails' => $request->get_param( 'only_regenerate_missing_thumbnails' ), 188 | 'delete_unregistered_thumbnail_files' => $request->get_param( 'delete_unregistered_thumbnail_files' ), 189 | ) ); 190 | 191 | if ( is_wp_error( $result ) ) { 192 | return $result; 193 | } 194 | 195 | if ( $request->get_param( 'update_usages_in_posts' ) ) { 196 | $posts_updated = $regenerator->update_usages_in_posts( array( 197 | 'post_type' => $request->get_param( 'update_usages_in_posts_post_type' ), 198 | 'post_ids' => $request->get_param( 'update_usages_in_posts_post_ids' ), 199 | 'posts_per_loop' => $request->get_param( 'update_usages_in_posts_posts_per_loop' ), 200 | ) ); 201 | 202 | // If wp_update_post() failed for any posts, return that error. 203 | foreach ( $posts_updated as $post_updated_result ) { 204 | if ( is_wp_error( $post_updated_result ) ) { 205 | return $post_updated_result; 206 | } 207 | } 208 | } 209 | 210 | return $this->attachment_info( $request ); 211 | } 212 | 213 | /** 214 | * Return a bunch of information about the current attachment for use in the UI 215 | * including details about the thumbnails. 216 | * 217 | * @since 3.0.0 218 | * 219 | * @param WP_REST_Request $request Full data about the request. 220 | * 221 | * @return array|WP_Error The data array or a WP_Error object on error. 222 | */ 223 | public function attachment_info( $request ) { 224 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $request->get_param( 'id' ) ); 225 | 226 | if ( is_wp_error( $regenerator ) ) { 227 | return $regenerator; 228 | } 229 | 230 | return $regenerator->get_attachment_info(); 231 | } 232 | 233 | /** 234 | * Return attachment IDs that are being used as featured images. 235 | * 236 | * @since 3.0.0 237 | * 238 | * @param WP_REST_Request $request Full data about the request. 239 | * 240 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 241 | */ 242 | public function featured_images( $request ) { 243 | global $wpdb; 244 | 245 | $page = $request->get_param( 'page' ); 246 | $per_page = $request->get_param( 'per_page' ); 247 | 248 | if ( 0 == $per_page ) { 249 | $per_page = 10; 250 | } 251 | 252 | $featured_image_ids = $wpdb->get_results( $wpdb->prepare( 253 | "SELECT SQL_CALC_FOUND_ROWS meta_value AS id FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id' GROUP BY meta_value ORDER BY MIN(meta_id) LIMIT %d OFFSET %d", 254 | $per_page, 255 | ( $per_page * $page ) - $per_page 256 | ) ); 257 | 258 | $total = $wpdb->get_var( "SELECT FOUND_ROWS()" ); 259 | $max_pages = ceil( $total / $per_page ); 260 | 261 | if ( $page > $max_pages && $total > 0 ) { 262 | return new WP_Error( 'rest_post_invalid_page_number', __( 'The page number requested is larger than the number of pages available.' ), array( 'status' => 400 ) ); 263 | } 264 | 265 | $response = rest_ensure_response( $featured_image_ids ); 266 | 267 | $response->header( 'X-WP-Total', (int) $total ); 268 | $response->header( 'X-WP-TotalPages', (int) $max_pages ); 269 | 270 | $request_params = $request->get_query_params(); 271 | $base = add_query_arg( $request_params, rest_url( $this->namespace . '/featuredimages' ) ); 272 | 273 | if ( $page > 1 ) { 274 | $prev_page = $page - 1; 275 | 276 | if ( $prev_page > $max_pages ) { 277 | $prev_page = $max_pages; 278 | } 279 | 280 | $prev_link = add_query_arg( 'page', $prev_page, $base ); 281 | $response->link_header( 'prev', $prev_link ); 282 | } 283 | 284 | if ( $max_pages > $page ) { 285 | $next_page = $page + 1; 286 | $next_link = add_query_arg( 'page', $next_page, $base ); 287 | 288 | $response->link_header( 'next', $next_link ); 289 | } 290 | 291 | return $response; 292 | } 293 | 294 | /** 295 | * Check to see if the current user is allowed to use this endpoint. 296 | * 297 | * @since 3.0.0 298 | * 299 | * @param WP_REST_Request $request Full data about the request. 300 | * 301 | * @return bool Whether the current user has permission to regenerate thumbnails. 302 | */ 303 | public function permissions_check( $request ) { 304 | return current_user_can( RegenerateThumbnails()->capability ); 305 | } 306 | 307 | /** 308 | * Returns whether a variable is an array or not. This is needed because 3 arguments are 309 | * passed to validation callbacks but is_array() only accepts one argument. 310 | * 311 | * @since 3.0.0 312 | * 313 | * @see https://core.trac.wordpress.org/ticket/34659 314 | * 315 | * @param mixed $param The parameter value to validate. 316 | * @param WP_REST_Request $request The REST request. 317 | * @param string $key The parameter name. 318 | * 319 | * @return bool Whether the parameter is an array or not. 320 | */ 321 | public function is_array( $param, $request, $key ) { 322 | return is_array( $param ); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /js/api-request.min.js: -------------------------------------------------------------------------------- 1 | !function(a){function b(a){return a=b.buildAjaxOptions(a),b.transport(a)}var c=window.wpApiSettings;b.buildAjaxOptions=function(b){var d,e,f,g,h,i=b.url,j=b.path;if("string"==typeof b.namespace&&"string"==typeof b.endpoint&&(d=b.namespace.replace(/^\/|\/$/g,""),e=b.endpoint.replace(/^\//,""),j=e?d+"/"+e:d),"string"==typeof j&&(i=c.root+j.replace(/^\//,"")),g=!(b.data&&b.data._wpnonce),f=b.headers||{},g)for(h in f)if(f.hasOwnProperty(h)&&"x-wp-nonce"===h.toLowerCase()){g=!1;break}return g&&(f=a.extend({"X-WP-Nonce":c.nonce},f)),b=a.extend({},b,{headers:f,url:i}),delete b.path,delete b.namespace,delete b.endpoint,b},b.transport=a.ajax,window.wp=window.wp||{},window.wp.apiRequest=b}(jQuery); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regenerate-thumbnails", 3 | "version": "3.1.6", 4 | "description": "A WordPress plugin for regenerating the thumbnails for one or more of your image uploads. Useful when changing their sizes or your theme.", 5 | "homepage": "https://alex.blog/wordpress-plugins/regenerate-thumbnails/", 6 | "author": "Alex Mills (Viper007Bond)", 7 | "license": "GPL-2.0+", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/automattic/regenerate-thumbnails.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/automattic/regenerate-thumbnails/issues" 14 | }, 15 | "scripts": { 16 | "build": "yarn install-if-deps-outdated && cross-env NODE_ENV=development webpack --progress --hide-modules", 17 | "watch": "yarn install-if-deps-outdated && cross-env NODE_ENV=development webpack --watch --progress --hide-modules", 18 | "build-production": "yarn install-if-deps-outdated && cross-env NODE_ENV=production webpack --mode=production --progress --hide-modules", 19 | "install-if-deps-outdated": "( yarn check 2> /dev/null || yarn install --check-files ) && ( check-node-version --package --print || exit 0 )", 20 | "distclean": "rm -rf node_modules && rm -rf dist" 21 | }, 22 | "engines": { 23 | "node": ">=8", 24 | "yarn": "^1.3.2" 25 | }, 26 | "dependencies": { 27 | "vue": "^2.5.13", 28 | "vue-router": "^3.0.1" 29 | }, 30 | "devDependencies": { 31 | "babel-core": "^6.26.0", 32 | "babel-loader": "^7.1.2", 33 | "babel-preset-env": "^1.6.1", 34 | "browser-sync": "^2.26.7", 35 | "browser-sync-webpack-plugin": "^1.2.0", 36 | "check-node-version": "^3.2.0", 37 | "clean-webpack-plugin": "^0.1.18", 38 | "cross-env": "^5.1.3", 39 | "css-loader": "^3.4.1", 40 | "file-loader": "^1.1.6", 41 | "node-sass": "^4.13.1", 42 | "sass-loader": "^6.0.6", 43 | "terser-webpack-plugin": "^2.3.1", 44 | "vue-loader": "^14.2.4", 45 | "vue-template-compiler": "^2.5.13", 46 | "webpack": "^4.42.1", 47 | "webpack-cli": "^3.3.11" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Regenerate Thumbnails === 2 | Contributors: Viper007Bond 3 | Tags: thumbnail, thumbnails, post thumbnail, post thumbnails 4 | Requires at least: 4.7 5 | Tested up to: 6.3 6 | Requires PHP: 5.2.4 7 | Stable tag: 3.1.6 8 | License: GPLv2 or later 9 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | Regenerate the thumbnails for one or more of your image uploads. Useful when changing their sizes or your theme. 12 | 13 | == Description == 14 | 15 | Regenerate Thumbnails allows you to regenerate all thumbnail sizes for one or more images that have been uploaded to your Media Library. 16 | 17 | This is useful for situations such as: 18 | 19 | * A new thumbnail size has been added and you want past uploads to have a thumbnail in that size. 20 | * You've changed the dimensions of an existing thumbnail size, for example via Settings → Media. 21 | * You've switched to a new WordPress theme that uses featured images of a different size. 22 | 23 | It also offers the ability to delete old, unused thumbnails in order to free up server space. 24 | 25 | = In Memory of Alex Mills = 26 | 27 | In February 2019 Alex Mills, the author of this plugin, [passed away](https://alex.blog/2019/02/27/from-alexs-family/). He leaves behind a number of plugins which will be maintained by Automattic and members of the WordPress community. If this plugin is useful to you please consider donating to the Oregon Health and Science University. You can find more information [here](https://alex.blog/2019/03/13/in-memory-of-alex-donation-link-update/). 28 | 29 | = Alternatives = 30 | 31 | **WP-CLI** 32 | 33 | If you have command line access to your server, I highly recommend using [WP-CLI](https://wp-cli.org/) instead of this plugin as it's faster (no HTTP requests overhead) and can be run inside of a `screen` for those with many thumbnails. For details, see the documentation of its [`media regenerate` command](https://developer.wordpress.org/cli/commands/media/regenerate/). 34 | 35 | **Jetpack's Photon Module** 36 | 37 | [Jetpack](https://jetpack.com/) is a plugin by Automattic, makers of WordPress.com. It gives your self-hosted WordPress site some of the functionality that is available to WordPress.com-hosted sites. 38 | 39 | [The Photon module](https://jetpack.com/support/photon/) makes the images on your site be served from WordPress.com's global content delivery network (CDN) which should speed up the loading of images. Importantly though it can create thumbnails on the fly which means you'll never need to use this plugin. 40 | 41 | I personally use Photon on my own website. 42 | 43 | *Disclaimer: I work for Automattic but I would recommend Photon even if I didn't.* 44 | 45 | = Need Help? Found A Bug? Want To Contribute Code? = 46 | 47 | Support for this plugin is provided via the [WordPress.org forums](https://wordpress.org/support/plugin/regenerate-thumbnails). 48 | 49 | The source code for this plugin is available on [GitHub](https://github.com/automattic/regenerate-thumbnails). 50 | 51 | == Installation == 52 | 53 | 1. Go to your admin area and select Plugins → Add New from the menu. 54 | 2. Search for "Regenerate Thumbnails". 55 | 3. Click install. 56 | 4. Click activate. 57 | 5. Navigate to Tools → Regenerate Thumbnails. 58 | 59 | == Frequently Asked Questions == 60 | 61 | = Is this plugin [GDPR](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation) compliant? = 62 | 63 | This plugin does not log nor transmit any user data. Infact it doesn't even do anything on the user-facing part of your website, only in the admin area. This means it should be compliant but I'm not a lawyer. 64 | 65 | == Screenshots == 66 | 67 | 1. The main plugin interface. 68 | 2. Regenerating in progress. 69 | 3. Interface for regenerating a single attachment. 70 | 4. Individual images can be regenerated from the media library in list view. 71 | 5. They can also be regenerated from the edit attachment screen. 72 | 73 | == ChangeLog == 74 | 75 | = Version 3.1.6 = 76 | 77 | * Fix: Respect "Skip regenerating existing correctly sized thumbnails" setting. 78 | * Fix: Don't delete all thumbnails when deleting old unregistered thumbnails size. 79 | 80 | = Version 3.1.5 = 81 | 82 | * Fix: Don't overwrite 'All X Attachment' button label with featured images count. 83 | * Tested successfully with PHP 8.1. 84 | * Tested successfully with PHP 8.2. 85 | 86 | = Version 3.1.4 = 87 | 88 | * Fix: Don't attempt to regenerate SVG's. 89 | * Bump tested version. 90 | * Update dependencies. 91 | 92 | = Version 3.1.3 = 93 | 94 | * Update plugin dependencies to the latest version. 95 | 96 | = Version 3.1.2 = 97 | * Use wp_get_original_image_path() in WordPress 5.3 98 | 99 | = Version 3.1.1 = 100 | 101 | * Minor fix to avoid a divide by zero error when displaying thumbnail filenames. 102 | 103 | = Version 3.1.0 = 104 | 105 | * Bring back the ability to delete old, unregistered thumbnail sizes. Support for updating post contents is still disabled (too buggy). 106 | * Various code improvements including string localization disambiguation. 107 | 108 | = Version 3.0.2 = 109 | 110 | * Fix slowdown in certain cases in the media library. 111 | * Fix not being able to regenerate existing thumbnails for single images. Props @idofri. 112 | * Fix JavaScript error that could occur if the REST API response was unexpected (empty or PHP error). 113 | * Fix bug related to multibyte filenames. 114 | * If an image is used as the featured image on multiple posts, only regenerate it once instead of once per post. 115 | 116 | = Version 3.0.1 = 117 | 118 | * Temporarily disable the update post functionality. I tested it a lot but it seems there's still some bugs. 119 | * Temporarily disable the delete old thumbnails functionality. It seems to work fine but without the update post functionality, it's not as useful. 120 | * Try to more gracefully handle cases where there's missing metadata for attachments. 121 | * Wait until `init` to initialize the plugin so themes can filter the plugin's capability. `plugins_loaded` is too early. 122 | * Fix a JavaScript error that would cause the whole regeneration process to stop if an individual image returned non-JSON, such as a 500 error code. 123 | * Accept GET requests for the regenerate REST API endpoint instead of just POSTs. For some reasons some people's sites are using GET despite the code saying use POST. 124 | * Make the attachment ID clickable in error messages. 125 | * Fetch 25 attachments at a time instead of 5. I was using 5 for testing. 126 | * PHP notice fixes. 127 | 128 | = Version 3.0.0 = 129 | 130 | * Complete rewrite from scratch using Vue.js and the WordPress REST API. 131 | 132 | = Version 2.2.4 = 133 | 134 | * Better AJAX response error handling in the JavaScript. This should fix a long-standing bug in this plugin. Props Hew Sutton. 135 | 136 | = Version 2.2.3 = 137 | 138 | * Make the capability required to use this plugin filterable so themes and other plugins can change it. Props [Jackson Whelan](http://jacksonwhelan.com/). 139 | 140 | = Version 2.2.2 = 141 | 142 | * Don't check the nonce until we're sure that the action called was for this plugin. Fixes lots of "Are you sure you want to do this?" error messages. 143 | 144 | = Version 2.2.1 = 145 | 146 | * Fix the bottom bulk action dropdown. Thanks Stefan for pointing out the issue! 147 | 148 | = Version 2.2.0 = 149 | 150 | * Changes to the Bulk Action functionality were made shortly before the release of WordPress 3.1 which broke the way I implemented the specific multiple image regeneration feature. This version adds to the Bulk Action menu using Javascript as that's the only way to do it currently. 151 | 152 | = Version 2.1.3 = 153 | 154 | * Move the `error_reporting()` call in the AJAX handler to the beginning so that we're more sure that no PHP errors are outputted. Some hosts disable usage of `set_time_limit()` and calling it was causing a PHP warning to be outputted. 155 | 156 | = Version 2.1.2 = 157 | 158 | * When regenerating all images, newest images are done first rather than the oldest. 159 | * Fixed a bug with regeneration error reporting in some browsers. Thanks to pete-sch for reporting the error. 160 | * Supress PHP errors in the AJAX handler to avoid sending an invalid JSON response. Thanks to pete-sch for reporting the error. 161 | * Better and more detailed error reporting for when `wp_generate_attachment_metadata()` fails. 162 | 163 | = Version 2.1.1 = 164 | 165 | * Clean up the wording a bit to better match the new features and just be easier to understand. 166 | * Updated screenshots. 167 | 168 | = Version 2.1.0 = 169 | 170 | Lots of new features! 171 | 172 | * Thanks to a lot of jQuery help from [Boris Schapira](http://borisschapira.com/), a failed image regeneration will no longer stop the whole process. 173 | * The results of each image regeneration is now outputted. You can easily see which images were successfully regenerated and which failed. Was inspired by a concept by Boris. 174 | * There is now a button on the regeneration page that will allow you to abort resizing images for any reason. Based on code by Boris. 175 | * You can now regenerate single images from the Media page. The link to do so will show up in the actions list when you hover over the row. 176 | * You can now bulk regenerate multiple from the Media page. Check the boxes and then select "Regenerate Thumbnails" form the "Bulk Actions" dropdown. WordPress 3.1+ only. 177 | * The total time that the regeneration process took is now displayed in the final status message. 178 | * jQuery UI Progressbar version upgraded. 179 | 180 | = Version 2.0.3 = 181 | 182 | * Switch out deprecated function call. 183 | 184 | = Version 2.0.2 = 185 | 186 | * Directly query the database to only fetch what the plugin needs (the attachment ID). This will reduce the memory required as it's not storing the whole row for each attachment. 187 | 188 | = Version 2.0.1 = 189 | 190 | * I accidentally left a `check_admin_referer()` (nonce check) commented out. 191 | 192 | = Version 2.0.0 = 193 | 194 | * Recoded from scratch. Now uses an AJAX request per attachment to do the resizing. No more PHP maximum execution time errors or anything like that. Also features a pretty progress bar to let the user know how it's going. 195 | 196 | = Version 1.1.0 = 197 | 198 | * WordPress 2.7 updates -- code + UI. Thanks to jdub and Patrick F. 199 | 200 | = Version 1.0.0 = 201 | 202 | * Initial release. 203 | 204 | = Upgrade Notice = 205 | Support for WordPress 5.3 206 | -------------------------------------------------------------------------------- /regenerate-thumbnails.php: -------------------------------------------------------------------------------- 1 | setup(); 106 | } 107 | 108 | return self::$instance; 109 | } 110 | 111 | /** 112 | * Register all of the needed hooks and actions. 113 | */ 114 | public function setup() { 115 | // Prevent fatals on old versions of WordPress 116 | if ( ! class_exists( 'WP_REST_Controller' ) ) { 117 | return; 118 | } 119 | 120 | require dirname( __FILE__ ) . '/includes/class-regeneratethumbnails-regenerator.php'; 121 | require dirname( __FILE__ ) . '/includes/class-regeneratethumbnails-rest-controller.php'; 122 | 123 | // Allow people to change what capability is required to use this plugin. 124 | $this->capability = apply_filters( 'regenerate_thumbs_cap', $this->capability ); 125 | 126 | // Initialize the REST API routes. 127 | add_action( 'rest_api_init', array( $this, 'rest_api_init' ) ); 128 | 129 | // Add a new item to the Tools menu in the admin menu. 130 | add_action( 'admin_menu', array( $this, 'add_admin_menu' ) ); 131 | 132 | // Load the required JavaScript and CSS. 133 | add_action( 'admin_enqueue_scripts', array( $this, 'admin_enqueues' ) ); 134 | 135 | // For the bulk action dropdowns. 136 | add_action( 'admin_head-upload.php', array( $this, 'add_bulk_actions_via_javascript' ) ); 137 | add_action( 'admin_action_bulk_regenerate_thumbnails', array( $this, 'bulk_action_handler' ) ); // Top drowndown. 138 | add_action( 'admin_action_-1', array( $this, 'bulk_action_handler' ) ); // Bottom dropdown. 139 | 140 | // Add a regenerate button to the non-modal edit media page. 141 | add_action( 'attachment_submitbox_misc_actions', array( $this, 'add_button_to_media_edit_page' ), 99 ); 142 | 143 | // Add a regenerate button to the list of fields in the edit media modal. 144 | // Ideally this would with the action links but I'm not good enough with JavaScript to do it. 145 | add_filter( 'attachment_fields_to_edit', array( $this, 'add_button_to_edit_media_modal_fields_area' ), 99, 2 ); 146 | 147 | // Add a regenerate link to actions list in the media list view. 148 | add_filter( 'media_row_actions', array( $this, 'add_regenerate_link_to_media_list_view' ), 10, 2 ); 149 | } 150 | 151 | /** 152 | * Initialize the REST API routes. 153 | */ 154 | public function rest_api_init() { 155 | $this->rest_api = new RegenerateThumbnails_REST_Controller(); 156 | $this->rest_api->register_routes(); 157 | $this->rest_api->register_filters(); 158 | } 159 | 160 | /** 161 | * Adds a the new item to the admin menu. 162 | */ 163 | public function add_admin_menu() { 164 | $this->menu_id = add_management_page( 165 | _x( 'Regenerate Thumbnails', 'admin page title', 'regenerate-thumbnails' ), 166 | _x( 'Regenerate Thumbnails', 'admin menu entry title', 'regenerate-thumbnails' ), 167 | $this->capability, 168 | 'regenerate-thumbnails', 169 | array( $this, 'regenerate_interface' ) 170 | ); 171 | 172 | add_action( 'admin_head-' . $this->menu_id, array( $this, 'add_admin_notice_if_resizing_not_supported' ) ); 173 | } 174 | 175 | /** 176 | * Enqueues the requires JavaScript file and stylesheet on the plugin's admin page. 177 | * 178 | * @param string $hook_suffix The current page's hook suffix as provided by admin-header.php. 179 | */ 180 | public function admin_enqueues( $hook_suffix ) { 181 | if ( $hook_suffix != $this->menu_id ) { 182 | return; 183 | } 184 | 185 | // Pre-4.9 compatibility. 186 | if ( ! wp_script_is( 'wp-api-request', 'registered' ) ) { 187 | wp_register_script( 188 | 'wp-api-request', 189 | plugins_url( 'js/api-request.min.js', __FILE__ ), 190 | array( 'jquery' ), 191 | '4.9', 192 | true 193 | ); 194 | 195 | wp_localize_script( 'wp-api-request', 'wpApiSettings', array( 196 | 'root' => esc_url_raw( get_rest_url() ), 197 | 'nonce' => ( wp_installing() && ! is_multisite() ) ? '' : wp_create_nonce( 'wp_rest' ), 198 | 'versionString' => 'wp/v2/', 199 | ) ); 200 | } 201 | 202 | wp_enqueue_script( 203 | 'regenerate-thumbnails', 204 | plugins_url( 'dist/build.js', __FILE__ ), 205 | array( 'wp-api-request' ), 206 | ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? filemtime( dirname( __FILE__ ) . '/dist/build.js' ) : $this->version, 207 | true 208 | ); 209 | 210 | // phpcs:disable WordPress.Arrays.MultipleStatementAlignment 211 | $script_data = array( 212 | 'data' => array( 213 | 'thumbnailSizes' => $this->get_thumbnail_sizes(), 214 | 'genericEditURL' => admin_url( 'post.php?action=edit&post=' ), 215 | ), 216 | 'options' => array( 217 | 'onlyMissingThumbnails' => apply_filters( 'regenerate_thumbnails_options_onlymissingthumbnails', true ), 218 | 'updatePostContents' => apply_filters( 'regenerate_thumbnails_options_updatepostcontents', false ), 219 | 'deleteOldThumbnails' => apply_filters( 'regenerate_thumbnails_options_deleteoldthumbnails', false ), 220 | ), 221 | 'l10n' => array( 222 | 'common' => array( 223 | 'loading' => __( 'Loading…', 'regenerate-thumbnails' ), 224 | 'onlyRegenerateMissingThumbnails' => __( 'Skip regenerating existing correctly sized thumbnails (faster).', 'regenerate-thumbnails' ), 225 | 'deleteOldThumbnails' => __( "Delete thumbnail files for old unregistered sizes in order to free up server space. This may result in broken images in your posts and pages.", 'regenerate-thumbnails' ), 226 | 'thumbnailSizeItemWithCropMethodNoFilename' => __( '{label}: {width}×{height} pixels ({cropMethod})', 'regenerate-thumbnails' ), 227 | 'thumbnailSizeItemWithCropMethod' => __( '{label}: {width}×{height} pixels ({cropMethod}) {filename}', 'regenerate-thumbnails' ), 228 | 'thumbnailSizeItemWithoutCropMethod' => __( '{label}: {width}×{height} pixels {filename}', 'regenerate-thumbnails' ), 229 | 'thumbnailSizeBiggerThanOriginal' => __( '{label}: {width}×{height} pixels (thumbnail would be larger than original)', 'regenerate-thumbnails' ), 230 | 'thumbnailSizeItemIsCropped' => __( 'cropped to fit', 'regenerate-thumbnails' ), 231 | 'thumbnailSizeItemIsProportional' => __( 'proportionally resized to fit inside dimensions', 'regenerate-thumbnails' ), 232 | ), 233 | 'Home' => array( 234 | 'intro1' => sprintf( 235 | /* translators: %s: Media options URL */ 236 | __( 'When you change WordPress themes or change the sizes of your thumbnails at Settings → Media, images that you have previously uploaded to you media library will be missing thumbnail files for those new image sizes. This tool will allow you to create those missing thumbnail files for all images.', 'regenerate-thumbnails' ), 237 | esc_url( admin_url( 'options-media.php' ) ) 238 | ), 239 | 'intro2' => sprintf( 240 | /* translators: %s: Media library URL */ 241 | __( 'To process a specific image, visit your media library and click the "Regenerate Thumbnails" link or button. To process multiple specific images, make sure you\'re in the list view and then use the Bulk Actions dropdown after selecting one or more images.', 'regenerate-thumbnails' ), 242 | esc_url( admin_url( 'upload.php?mode=list' ) ) 243 | ), 244 | 'updatePostContents' => __( 'Update the content of posts to use the new sizes.', 'regenerate-thumbnails' ), 245 | 'RegenerateThumbnailsForAllAttachments' => __( 'Regenerate Thumbnails For All Attachments', 'regenerate-thumbnails' ), 246 | 'RegenerateThumbnailsForAllXAttachments' => __( 'Regenerate Thumbnails For All {attachmentCount} Attachments', 'regenerate-thumbnails' ), 247 | 'RegenerateThumbnailsForFeaturedImagesOnly' => __( 'Regenerate Thumbnails For Featured Images Only', 'regenerate-thumbnails' ), 248 | 'RegenerateThumbnailsForXFeaturedImagesOnly' => __( 'Regenerate Thumbnails For The {attachmentCount} Featured Images Only', 'regenerate-thumbnails' ), 249 | 'thumbnailSizes' => __( 'Thumbnail Sizes', 'regenerate-thumbnails' ), 250 | 'thumbnailSizesDescription' => __( 'These are all of the thumbnail sizes that are currently registered:', 'regenerate-thumbnails' ), 251 | 'alternatives' => __( 'Alternatives', 'regenerate-thumbnails' ), 252 | 'alternativesText1' => __( 'If you have command-line access to your site\'s server, consider using WP-CLI instead of this tool. It has a built-in regenerate command that works similarly to this tool but should be significantly faster since it has the advantage of being a command-line tool.', 'regenerate-thumbnails' ), 253 | 'alternativesText2' => __( 'Another alternative is to use the Photon functionality that comes with the Jetpack plugin. It generates thumbnails on-demand using WordPress.com\'s infrastructure. Disclaimer: The author of this plugin, Regenerate Thumbnails, is an employee of the company behind WordPress.com and Jetpack but I would recommend it even if I wasn\'t.', 'regenerate-thumbnails' ), 254 | ), 255 | 'RegenerateSingle' => array( 256 | 'regenerateThumbnails' => _x( 'Regenerate Thumbnails', 'action for a single image', 'regenerate-thumbnails' ), 257 | /* translators: single image sdmin page title */ 258 | 'title' => __( 'Regenerate Thumbnails: {name} — WordPress', 'regenerate-thumbnails' ), 259 | 'errorWithMessage' => __( 'ERROR: {error}', 'regenerate-thumbnails' ), 260 | 'filenameAndDimensions' => __( '{filename} {width}×{height} pixels', 'regenerate-thumbnails' ), 261 | 'preview' => __( 'Preview', 'regenerate-thumbnails' ), 262 | 'updatePostContents' => __( 'Update the content of posts that use this attachment to use the new sizes.', 'regenerate-thumbnails' ), 263 | 'regenerating' => __( 'Regenerating…', 'regenerate-thumbnails' ), 264 | 'done' => __( 'Done! Click here to go back.', 'regenerate-thumbnails' ), 265 | 'errorRegenerating' => __( 'Error Regenerating', 'regenerate-thumbnails' ), 266 | 'errorRegeneratingMessage' => __( 'There was an error regenerating this attachment. The error was: {message}', 'regenerate-thumbnails' ), 267 | 'registeredSizes' => __( 'These are the currently registered thumbnail sizes, whether they exist for this attachment, and their filenames:', 'regenerate-thumbnails' ), 268 | 'unregisteredSizes' => __( 'The attachment says it also has these thumbnail sizes but they are no longer in use by WordPress. You can probably safely have this plugin delete them, especially if you have this plugin update any posts that make use of this attachment.', 'regenerate-thumbnails' ), 269 | ), 270 | 'RegenerateMultiple' => array( 271 | 'errorsEncountered' => __( 'Errors Encountered', 'regenerate-thumbnails' ), 272 | 'regenerationLog' => __( 'Regeneration Log', 'regenerate-thumbnails' ), 273 | 'pause' => __( 'Pause', 'regenerate-thumbnails' ), 274 | 'resume' => __( 'Resume', 'regenerate-thumbnails' ), 275 | 'logRegeneratedItem' => __( 'Regenerated {name}', 'regenerate-thumbnails' ), 276 | 'logSkippedItem' => __( 'Skipped Attachment ID {id} ({name}): {reason}', 'regenerate-thumbnails' ), 277 | 'logSkippedItemNoName' => __( 'Skipped Attachment ID {id}: {reason}', 'regenerate-thumbnails' ), 278 | 'duration' => __( 'All done in {duration}.', 'regenerate-thumbnails' ), 279 | 'hours' => __( '{count} hours', 'regenerate-thumbnails' ), 280 | 'minutes' => __( '{count} minutes', 'regenerate-thumbnails' ), 281 | 'seconds' => __( '{count} seconds', 'regenerate-thumbnails' ), 282 | 'error' => __( "Unable to fetch a list of attachment IDs to process from the WordPress REST API. You can check your browser's console for details.", 'regenerate-thumbnails' ), 283 | ), 284 | ), 285 | ); 286 | // phpcs:enable 287 | 288 | // Bulk regeneration 289 | // phpcs:disable WordPress.Security.NonceVerification 290 | if ( ! empty( $_GET['ids'] ) ) { 291 | $script_data['data']['thumbnailIDs'] = array_map( 'intval', explode( ',', $_GET['ids'] ) ); 292 | 293 | $script_data['l10n']['Home']['RegenerateThumbnailsForXAttachments'] = sprintf( 294 | __( 'Regenerate Thumbnails For The %d Selected Attachments', 'regenerate-thumbnails' ), 295 | count( $script_data['data']['thumbnailIDs'] ) 296 | ); 297 | } 298 | // phpcs:enable 299 | 300 | wp_localize_script( 'regenerate-thumbnails', 'regenerateThumbnails', $script_data ); 301 | 302 | wp_enqueue_style( 303 | 'regenerate-thumbnails-progressbar', 304 | plugins_url( 'css/progressbar.css', __FILE__ ), 305 | array(), 306 | ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? filemtime( dirname( __FILE__ ) . '/css/progressbar.css' ) : $this->version 307 | ); 308 | } 309 | 310 | /** 311 | * The main Regenerate Thumbnails interface, as displayed at Tools → Regenerate Thumbnails. 312 | */ 313 | public function regenerate_interface() { 314 | global $wp_version; 315 | 316 | echo '
'; 317 | echo '

' . esc_html_x( 'Regenerate Thumbnails', 'admin page title', 'regenerate-thumbnails' ) . '

'; 318 | 319 | if ( version_compare( $wp_version, '4.7', '<' ) ) { 320 | echo '

' . sprintf( 321 | __( 'This plugin requires WordPress 4.7 or newer. You are on version %1$s. Please upgrade.', 'regenerate-thumbnails' ), 322 | esc_html( $wp_version ), 323 | esc_url( admin_url( 'update-core.php' ) ) 324 | ) . '

'; 325 | } else { 326 | 327 | ?> 328 | 329 |
330 |
331 |

332 |
333 | 334 |

335 |
336 | 337 | '; 342 | } 343 | 344 | /** 345 | * If the image editor doesn't support image resizing (thumbnailing), then add an admin notice 346 | * warning the user of this. 347 | */ 348 | public function add_admin_notice_if_resizing_not_supported() { 349 | if ( ! wp_image_editor_supports( array( 'methods' => array( 'resize' ) ) ) ) { 350 | add_action( 'admin_notices', array( $this, 'admin_notices_resizing_not_supported' ) ); 351 | } 352 | } 353 | 354 | /** 355 | * Outputs an admin notice stating that image resizing (thumbnailing) is not supported. 356 | */ 357 | public function admin_notices_resizing_not_supported() { 358 | ?> 359 |
360 |

361 |
362 | ID, '_wp_attachment_context', true ) ) { 388 | return false; 389 | } 390 | 391 | if ( wp_attachment_is_image( $post ) ) { 392 | return true; 393 | } 394 | 395 | if ( function_exists( 'wp_get_original_image_path' ) ) { 396 | $fullsize = wp_get_original_image_path( $post->ID ); 397 | } else { 398 | $fullsize = get_attached_file( $post->ID ); 399 | } 400 | 401 | if ( ! $fullsize || ! file_exists( $fullsize ) ) { 402 | return false; 403 | } 404 | 405 | $image_editor_args = array( 406 | 'path' => $fullsize, 407 | 'methods' => array( 'resize' ) 408 | ); 409 | 410 | $file_info = wp_check_filetype( $image_editor_args['path'] ); 411 | // If $file_info['type'] is false, then we let the editor attempt to 412 | // figure out the file type, rather than forcing a failure based on extension. 413 | if ( isset( $file_info ) && $file_info['type'] ) { 414 | $image_editor_args['mime_type'] = $file_info['type']; 415 | } 416 | 417 | return (bool) _wp_image_editor_choose( $image_editor_args ); 418 | } 419 | 420 | /** 421 | * Adds "Regenerate Thumbnails" below each image in the media library list view. 422 | * 423 | * @param array $actions An array of current actions. 424 | * @param WP_Post $post The current attachment's post object. 425 | * 426 | * @return array The new list of actions. 427 | */ 428 | public function add_regenerate_link_to_media_list_view( $actions, $post ) { 429 | if ( ! current_user_can( $this->capability ) || ! $this->is_regeneratable( $post ) ) { 430 | return $actions; 431 | } 432 | 433 | $actions['regenerate_thumbnails'] = '' . _x( 'Regenerate Thumbnails', 'action for a single image', 'regenerate-thumbnails' ) . ''; 434 | 435 | return $actions; 436 | } 437 | 438 | /** 439 | * Add a "Regenerate Thumbnails" button to the submit box on the non-modal "Edit Media" screen for an image attachment. 440 | */ 441 | public function add_button_to_media_edit_page() { 442 | global $post; 443 | 444 | if ( ! current_user_can( $this->capability ) || ! $this->is_regeneratable( $post ) ) { 445 | return; 446 | } 447 | 448 | echo ''; 451 | } 452 | 453 | /** 454 | * Adds a "Regenerate Thumbnails" button to the edit media modal view. 455 | * 456 | * Ideally it would be down with the actions but I'm not good enough at JavaScript 457 | * in order to be able to do it, so instead I'm adding it to the bottom of the list 458 | * of media fields. Pull requests to improve this are welcome! 459 | * 460 | * @param array $form_fields An array of existing form fields. 461 | * @param WP_Post $post The current media item, as a post object. 462 | * 463 | * @return array The new array of form fields. 464 | */ 465 | public function add_button_to_edit_media_modal_fields_area( $form_fields, $post ) { 466 | if ( ! current_user_can( $this->capability ) || ! $this->is_regeneratable( $post ) ) { 467 | return $form_fields; 468 | } 469 | 470 | $form_fields['regenerate_thumbnails'] = array( 471 | 'label' => '', 472 | 'input' => 'html', 473 | 'html' => '' . _x( 'Regenerate Thumbnails', 'action for a single image', 'regenerate-thumbnails' ) . '', 474 | 'show_in_modal' => true, 475 | 'show_in_edit' => false, 476 | ); 477 | 478 | return $form_fields; 479 | } 480 | 481 | /** 482 | * Add "Regenerate Thumbnails" to the bulk actions dropdown on the media list using Javascript. 483 | */ 484 | public function add_bulk_actions_via_javascript() { 485 | if ( ! current_user_can( $this->capability ) ) { 486 | return; 487 | } 488 | 489 | ?> 490 | 499 | 'regenerate-thumbnails', 522 | 'ids' => rawurlencode( implode( ',', array_map( 'intval', $_REQUEST['media'] ) ) ), 523 | ), 524 | admin_url( 'tools.php' ) 525 | ) 526 | ); 527 | 528 | exit(); 529 | } 530 | 531 | /** 532 | * Returns an array of all thumbnail sizes, including their label, size, and crop setting. 533 | * 534 | * @return array An array, with the thumbnail label as the key and an array of thumbnail properties (width, height, crop). 535 | */ 536 | public function get_thumbnail_sizes() { 537 | global $_wp_additional_image_sizes; 538 | 539 | $thumbnail_sizes = array(); 540 | 541 | foreach ( get_intermediate_image_sizes() as $size ) { 542 | $thumbnail_sizes[ $size ]['label'] = $size; 543 | if ( in_array( $size, array( 'thumbnail', 'medium', 'medium_large', 'large' ) ) ) { 544 | $thumbnail_sizes[ $size ]['width'] = (int) get_option( $size . '_size_w' ); 545 | $thumbnail_sizes[ $size ]['height'] = (int) get_option( $size . '_size_h' ); 546 | $thumbnail_sizes[ $size ]['crop'] = ( 'thumbnail' == $size ) ? (bool) get_option( 'thumbnail_crop' ) : false; 547 | } elseif ( ! empty( $_wp_additional_image_sizes ) && ! empty( $_wp_additional_image_sizes[ $size ] ) ) { 548 | $thumbnail_sizes[ $size ]['width'] = (int) $_wp_additional_image_sizes[ $size ]['width']; 549 | $thumbnail_sizes[ $size ]['height'] = (int) $_wp_additional_image_sizes[ $size ]['height']; 550 | $thumbnail_sizes[ $size ]['crop'] = (bool) $_wp_additional_image_sizes[ $size ]['crop']; 551 | } 552 | } 553 | 554 | return $thumbnail_sizes; 555 | } 556 | } 557 | 558 | /** 559 | * Returns the single instance of this plugin, creating one if needed. 560 | * 561 | * @return RegenerateThumbnails 562 | */ 563 | function RegenerateThumbnails() { 564 | return RegenerateThumbnails::instance(); 565 | } 566 | 567 | /** 568 | * Initialize this plugin once all other plugins have finished loading. 569 | */ 570 | add_action( 'init', 'RegenerateThumbnails' ); 571 | -------------------------------------------------------------------------------- /src/components/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/components/ThumbnailSize.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/components/ThumbnailStatus.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 33 | -------------------------------------------------------------------------------- /src/helpers/formatUnicorn.js: -------------------------------------------------------------------------------- 1 | // Placeholder replacer. See https://stackoverflow.com/a/18234317 2 | String.prototype.formatUnicorn = String.prototype.formatUnicorn || 3 | function () { 4 | "use strict"; 5 | var str = this.toString(); 6 | if (arguments.length) { 7 | var t = typeof arguments[0]; 8 | var key; 9 | var args = ("string" === t || "number" === t) ? 10 | Array.prototype.slice.call(arguments) 11 | : arguments[0]; 12 | 13 | for (key in args) { 14 | str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]); 15 | } 16 | } 17 | 18 | return str; 19 | }; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import routes from './routes.js' 4 | 5 | Vue.use(VueRouter); 6 | 7 | const router = new VueRouter({ 8 | routes: routes 9 | }); 10 | 11 | const app = new Vue({ 12 | router 13 | }).$mount('#regenerate-thumbnails-app'); 14 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import Home from './routes/Home.vue'; 2 | import RegenerateSingle from './routes/RegenerateSingle.vue'; 3 | 4 | export default [ 5 | { 6 | path : '/', 7 | name : 'home', 8 | component: Home, 9 | }, 10 | { 11 | path : '/regenerate/:id(\\d+)', 12 | name : 'regenerate-single', 13 | component: RegenerateSingle, 14 | props : true, 15 | }, 16 | { 17 | path : '*', 18 | redirect: '/', 19 | } 20 | ]; 21 | -------------------------------------------------------------------------------- /src/routes/Home.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 35 | -------------------------------------------------------------------------------- /src/routes/RegenerateSingle.vue: -------------------------------------------------------------------------------- 1 | 89 | 90 | 203 | 204 | 222 | -------------------------------------------------------------------------------- /src/routes/subpages/HomeIntro.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 188 | -------------------------------------------------------------------------------- /src/routes/subpages/HomeRegenerateMultiple.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 303 | 304 | 323 | -------------------------------------------------------------------------------- /tests/bin/install-imagick.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # ImageMagick version to use 4 | IMAGEMAGICK_VERSION='7.0.7-14' 5 | 6 | install_imagemagick() { 7 | curl -O "https://www.imagemagick.org/download/releases/ImageMagick-$IMAGEMAGICK_VERSION.tar.gz" -f 8 | tar xzf "ImageMagick-$IMAGEMAGICK_VERSION.tar.gz" 9 | rm "ImageMagick-$IMAGEMAGICK_VERSION.tar.gz" 10 | cd "ImageMagick-$IMAGEMAGICK_VERSION" 11 | 12 | ./configure --prefix="$HOME/opt/$TRAVIS_PHP_VERSION" 13 | make 14 | make install 15 | 16 | cd "$TRAVIS_BUILD_DIR" 17 | } 18 | 19 | # Install ImageMagick if the current version isn't up to date 20 | PATH="$HOME/opt/$TRAVIS_PHP_VERSION/bin:$PATH" convert -v | grep "$IMAGEMAGICK_VERSION" || install_imagemagick 21 | 22 | # Debugging 23 | ls "$HOME/opt/$TRAVIS_PHP_VERSION" 24 | 25 | # Set up environment variables 26 | export LD_FLAGS="-L$HOME/opt/$TRAVIS_PHP_VERSION/lib" 27 | export LD_LIBRARY_PATH="/lib:/usr/lib:/usr/local/lib:$HOME/opt/$TRAVIS_PHP_VERSION/lib" 28 | export CPATH="$CPATH:$HOME/opt/$TRAVIS_PHP_VERSION/include" 29 | 30 | # Install Imagick for PHP 31 | echo "$HOME/opt/$TRAVIS_PHP_VERSION" | pecl install imagick -------------------------------------------------------------------------------- /tests/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 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 16 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 17 | 18 | download() { 19 | if [ `which curl` ]; then 20 | curl -s "$1" > "$2"; 21 | elif [ `which wget` ]; then 22 | wget -nv -O "$2" "$1" 23 | fi 24 | } 25 | 26 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 27 | WP_TESTS_TAG="tags/$WP_VERSION" 28 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 29 | WP_TESTS_TAG="trunk" 30 | else 31 | # http serves a single offer, whereas https serves multiple. we only want one 32 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 33 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 34 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 35 | if [[ -z "$LATEST_VERSION" ]]; then 36 | echo "Latest WordPress version could not be found" 37 | exit 1 38 | fi 39 | WP_TESTS_TAG="tags/$LATEST_VERSION" 40 | fi 41 | 42 | set -ex 43 | 44 | install_wp() { 45 | 46 | if [ -d $WP_CORE_DIR ]; then 47 | rm -rf $WP_CORE_DIR 48 | fi 49 | 50 | mkdir -p $WP_CORE_DIR 51 | 52 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 53 | mkdir -p /tmp/wordpress-nightly 54 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 55 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 56 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 57 | else 58 | if [ $WP_VERSION == 'latest' ]; then 59 | local ARCHIVE_NAME='latest' 60 | else 61 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 62 | fi 63 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 64 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 65 | fi 66 | 67 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 68 | } 69 | 70 | install_test_suite() { 71 | # portable in-place argument for both GNU sed and Mac OSX sed 72 | if [[ $(uname -s) == 'Darwin' ]]; then 73 | local ioption='-i .bak' 74 | else 75 | local ioption='-i' 76 | fi 77 | 78 | if [ -d $WP_TESTS_DIR ]; then 79 | rm -rf $WP_TESTS_DIR 80 | fi 81 | 82 | # set up testing suite 83 | mkdir -p $WP_TESTS_DIR 84 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 85 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data 86 | 87 | if [ ! -f wp-tests-config.php ]; then 88 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 89 | # remove all forward slashes in the end 90 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") 91 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php 92 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 93 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 94 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 95 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 96 | fi 97 | 98 | } 99 | 100 | install_db() { 101 | 102 | if [ ${SKIP_DB_CREATE} = "true" ]; then 103 | return 0 104 | fi 105 | 106 | # parse DB_HOST for port or socket references 107 | local PARTS=(${DB_HOST//\:/ }) 108 | local DB_HOSTNAME=${PARTS[0]}; 109 | local DB_SOCK_OR_PORT=${PARTS[1]}; 110 | local EXTRA="" 111 | 112 | if ! [ -z $DB_HOSTNAME ] ; then 113 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 114 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 115 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 116 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 117 | elif ! [ -z $DB_HOSTNAME ] ; then 118 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 119 | fi 120 | fi 121 | 122 | # create database 123 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 124 | } 125 | 126 | install_wp 127 | install_test_suite 128 | install_db 129 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | rmdir( trailingslashit( $upload_dir ), true ); 12 | } 13 | } 14 | 15 | /** 16 | * Anonymous functions aren't supported in PHP 5.2.x which WordPress does still support, 17 | * so here's a bunch of helper functions for making filters return certain numbers. Ugh. 18 | */ 19 | 20 | public static function return_int_1() { 21 | return 1; 22 | } 23 | 24 | public static function return_int_100() { 25 | return 100; 26 | } 27 | 28 | public static function return_int_150() { 29 | return 150; 30 | } 31 | 32 | public static function return_int_300() { 33 | return 300; 34 | } 35 | 36 | public static function return_int_350() { 37 | return 350; 38 | } 39 | 40 | public static function return_int_500() { 41 | return 500; 42 | } 43 | 44 | public static function return_int_768() { 45 | return 768; 46 | } 47 | 48 | public static function return_int_1024() { 49 | return 1024; 50 | } 51 | 52 | public static function return_int_1500() { 53 | return 1500; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/test-regenerator.php: -------------------------------------------------------------------------------- 1 | array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_150' ), 28 | 'thumbnail_size_h' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_150' ), 29 | 'thumbnail_crop' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_1' ), 30 | 'medium_size_w' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_300' ), 31 | 'medium_size_h' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_300' ), 32 | 'medium_large_size_w' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_768' ), 33 | 'medium_large_size_h' => '__return_zero', 34 | 'large_size_w' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_1024' ), 35 | 'large_size_h' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_1024' ), 36 | ); 37 | 38 | foreach ( self::$default_size_functions as $filter => $function ) { 39 | add_filter( 'pre_option_' . $filter, $function ); 40 | }; 41 | 42 | Regenerate_Thumbnails_Tests_Helper::delete_upload_dir_contents(); 43 | } 44 | 45 | public static function wpTearDownAfterClass() { 46 | foreach ( self::$default_size_functions as $filter => $function ) { 47 | remove_filter( 'pre_option_' . $filter, $function ); 48 | } 49 | } 50 | 51 | public function setUp() { 52 | parent::setUp(); 53 | 54 | if ( ! wp_image_editor_supports( array( 'methods' => array( 'resize' ) ) ) ) { 55 | $this->markTestSkipped( "This system doesn't have an image editor engine capable of resizing images. Try installing Imagick or GD." ); 56 | } 57 | } 58 | 59 | public function tearDown() { 60 | wp_delete_attachment( $this->attachment_id, true ); 61 | 62 | Regenerate_Thumbnails_Tests_Helper::delete_upload_dir_contents(); 63 | 64 | parent::tearDown(); 65 | } 66 | 67 | public function helper_create_attachment() { 68 | return self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/33772.jpg' ); 69 | } 70 | 71 | public function helper_get_custom_thumbnail_size_callbacks() { 72 | return array( 73 | 'thumbnail_size_w' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_100' ), 74 | 'thumbnail_size_h' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_100' ), 75 | 'thumbnail_crop' => '__return_zero', 76 | 'medium_size_w' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_350' ), 77 | 'medium_size_h' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_350' ), 78 | 'medium_large_size_w' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_500' ), 79 | 'medium_large_size_h' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_500' ), 80 | 'large_size_w' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_1500' ), 81 | 'large_size_h' => array( 'Regenerate_Thumbnails_Tests_Helper', 'return_int_1500' ), 82 | ); 83 | } 84 | 85 | public function helper_get_filemtimes( $upload_dir, $thumbnails ) { 86 | $filemtimes = array(); 87 | 88 | foreach ( $thumbnails as $size => $filename ) { 89 | $file = $upload_dir . $filename; 90 | $this->assertFileExists( $file ); 91 | $filemtimes[ $size ] = filemtime( $file ); 92 | } 93 | 94 | return $filemtimes; 95 | } 96 | 97 | public function test_attachment_doesnt_exist() { 98 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( 0 ); 99 | 100 | $this->assertInstanceOf( 'WP_Error', $regenerator ); 101 | $this->assertEquals( 'regenerate_thumbnails_regenerator_attachment_doesnt_exist', $regenerator->get_error_code() ); 102 | } 103 | 104 | public function test_not_attachment() { 105 | $post_id = self::factory()->post->create( array() ); 106 | 107 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $post_id ); 108 | 109 | $this->assertInstanceOf( 'WP_Error', $regenerator ); 110 | $this->assertEquals( 'regenerate_thumbnails_regenerator_not_attachment', $regenerator->get_error_code() ); 111 | } 112 | 113 | public function test_missing_original_file() { 114 | $this->attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/test-image.jpg' ); 115 | 116 | if ( function_exists( 'wp_get_original_image_path' ) ) { 117 | unlink( wp_get_original_image_path( $this->attachment_id ) ); 118 | } else { 119 | unlink( get_attached_file( $this->attachment_id ) ); 120 | } 121 | 122 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 123 | $result = $regenerator->regenerate(); 124 | 125 | $this->assertInstanceOf( 'WP_Error', $result ); 126 | $this->assertEquals( 'regenerate_thumbnails_regenerator_file_not_found', $result->get_error_code() ); 127 | } 128 | 129 | public function test_regenerate_thumbnails_to_new_sizes() { 130 | $this->attachment_id = $this->helper_create_attachment(); 131 | $old_metadata = wp_get_attachment_metadata( $this->attachment_id ); 132 | 133 | if ( function_exists( 'wp_get_original_image_path' ) ) { 134 | $upload_dir = dirname( wp_get_original_image_path( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 135 | } else { 136 | $upload_dir = dirname( get_attached_file( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 137 | } 138 | 139 | $expected_default_thumbnail_sizes = array( 140 | 'thumbnail' => array( 150, 150 ), 141 | 'medium' => array( 300, 169 ), 142 | 'medium_large' => array( 768, 432 ), 143 | 'large' => array( 1024, 576 ), 144 | ); 145 | 146 | // Verify that the default thumbnails were made correctly during initial upload 147 | foreach ( $expected_default_thumbnail_sizes as $size => $dims ) { 148 | $this->assertFileExists( $upload_dir . "33772-{$dims[0]}x{$dims[1]}.jpg" ); 149 | $this->assertEquals( $dims[0], $old_metadata['sizes'][ $size ]['width'] ); 150 | $this->assertEquals( $dims[1], $old_metadata['sizes'][ $size ]['height'] ); 151 | } 152 | 153 | // Now change the thumbnail sizes to something other than the defaults 154 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 155 | add_filter( 'pre_option_' . $filter, $function ); 156 | }; 157 | 158 | // Regenerate the thumbnails! 159 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 160 | $regenerator->regenerate(); 161 | 162 | $new_metadata = wp_get_attachment_metadata( $this->attachment_id ); 163 | 164 | // Cleanup 165 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 166 | remove_filter( 'pre_option_' . $filter, $function ); 167 | } 168 | 169 | $expected_custom_thumbnail_sizes = array( 170 | 'thumbnail' => array( 100, 56 ), 171 | 'medium' => array( 350, 197 ), 172 | 'medium_large' => array( 500, 281 ), 173 | 'large' => array( 1500, 844 ), 174 | ); 175 | 176 | // Verify that the new custom thumbnails were made correctly by this plugin 177 | foreach ( $expected_custom_thumbnail_sizes as $size => $dims ) { 178 | $this->assertFileExists( $upload_dir . "33772-{$dims[0]}x{$dims[1]}.jpg" ); 179 | $this->assertEquals( $dims[0], $new_metadata['sizes'][ $size ]['width'] ); 180 | $this->assertEquals( $dims[1], $new_metadata['sizes'][ $size ]['height'] ); 181 | } 182 | } 183 | 184 | public function test_regenerate_thumbnails_for_pdf() { 185 | $test_pdf = DIR_TESTDATA . '/images/wordpress-gsoc-flyer.pdf'; 186 | 187 | $editor = wp_get_image_editor( $test_pdf ); 188 | if ( is_wp_error( $editor ) ) { 189 | $this->markTestSkipped( "The current image editor doesn't support making thumbnails for PDFs. Please install ImageMagick." ); 190 | } 191 | 192 | $this->attachment_id = self::factory()->attachment->create_upload_object( $test_pdf ); 193 | $old_metadata = wp_get_attachment_metadata( $this->attachment_id ); 194 | 195 | if ( function_exists( 'wp_get_original_image_path' ) ) { 196 | $upload_dir = dirname( wp_get_original_image_path( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 197 | } else { 198 | $upload_dir = dirname( get_attached_file( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 199 | } 200 | 201 | $expected_default_thumbnail_sizes = array( 202 | 'thumbnail' => array( 116, 150 ), 203 | 'medium' => array( 232, 300 ), 204 | 'large' => array( 791, 1024 ), 205 | ); 206 | 207 | // Verify that the default thumbnails were made correctly during initial upload 208 | foreach ( $expected_default_thumbnail_sizes as $size => $dims ) { 209 | $this->assertFileExists( $upload_dir . "wordpress-gsoc-flyer-pdf-{$dims[0]}x{$dims[1]}.jpg" ); 210 | $this->assertEquals( $dims[0], $old_metadata['sizes'][ $size ]['width'] ); 211 | $this->assertEquals( $dims[1], $old_metadata['sizes'][ $size ]['height'] ); 212 | } 213 | $this->assertFileExists( $upload_dir . 'wordpress-gsoc-flyer-pdf.jpg' ); 214 | $this->assertEquals( 1088, $old_metadata['sizes']['full']['width'] ); 215 | $this->assertEquals( 1408, $old_metadata['sizes']['full']['height'] ); 216 | 217 | // Remove the fullsize thumbnail. It causes "-1" to be added after "pdf" in the filenames. 218 | unlink( $upload_dir . 'wordpress-gsoc-flyer-pdf.jpg' ); 219 | 220 | // Now change the thumbnail sizes to something other than the defaults 221 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 222 | add_filter( 'pre_option_' . $filter, $function ); 223 | }; 224 | 225 | // Regenerate the thumbnails! 226 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 227 | $regenerator->regenerate(); 228 | 229 | $new_metadata = wp_get_attachment_metadata( $this->attachment_id ); 230 | 231 | // Cleanup 232 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 233 | remove_filter( 'pre_option_' . $filter, $function ); 234 | } 235 | 236 | $expected_custom_thumbnail_sizes = array( 237 | 'thumbnail' => array( 77, 100 ), 238 | 'medium' => array( 270, 350 ), 239 | 'large' => array( 791, 1024 ), 240 | ); 241 | 242 | // Verify that the new custom thumbnails were made correctly by this plugin 243 | foreach ( $expected_custom_thumbnail_sizes as $size => $dims ) { 244 | $this->assertFileExists( $upload_dir . "wordpress-gsoc-flyer-pdf-{$dims[0]}x{$dims[1]}.jpg" ); 245 | $this->assertEquals( $dims[0], $new_metadata['sizes'][ $size ]['width'] ); 246 | $this->assertEquals( $dims[1], $new_metadata['sizes'][ $size ]['height'] ); 247 | } 248 | } 249 | 250 | public function test_regenerate_thumbnails_skipping_existing_thumbnails() { 251 | $this->helper_regenerate_thumbnails_skipping_existing_thumbnails( true ); 252 | } 253 | 254 | public function test_regenerate_thumbnails_without_skipping_existing_thumbnails() { 255 | $this->helper_regenerate_thumbnails_skipping_existing_thumbnails( false ); 256 | } 257 | 258 | public function helper_regenerate_thumbnails_skipping_existing_thumbnails( $only_regenerate_missing_thumbnails ) { 259 | $this->attachment_id = $this->helper_create_attachment(); 260 | 261 | // These are the expected thumbnail filenames 262 | $thumbnails = array( 263 | 'thumbnail' => '33772-150x150.jpg', 264 | 'medium' => '33772-300x169.jpg', 265 | 'medium_large' => '33772-768x432.jpg', 266 | 'large' => '33772-1024x576.jpg', 267 | ); 268 | 269 | if ( function_exists( 'wp_get_original_image_path' ) ) { 270 | $upload_dir = dirname( wp_get_original_image_path( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 271 | } else { 272 | $upload_dir = dirname( get_attached_file( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 273 | } 274 | $filemtimes = $this->helper_get_filemtimes( $upload_dir, $thumbnails ); 275 | 276 | // Delete some of the thumbnail files 277 | $missing_thumbnails = array( 'medium', 'large' ); 278 | foreach ( $missing_thumbnails as $size ) { 279 | unlink( $upload_dir . $thumbnails[ $size ] ); 280 | $this->assertFileNotExists( $upload_dir . $thumbnails[ $size ] ); 281 | } 282 | 283 | // Sleep to make sure that filemtime() changes 284 | sleep( 1 ); 285 | 286 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 287 | $regenerator->regenerate( array( 288 | 'only_regenerate_missing_thumbnails' => $only_regenerate_missing_thumbnails, 289 | ) ); 290 | 291 | // Clear the file stat cache to make sure that filemtime() works correctly 292 | clearstatcache(); 293 | 294 | // Verify the modified times of files 295 | // When skipping existing thumbnails, the thumbnail files we didn't delete shouldn't change 296 | foreach ( $thumbnails as $size => $filename ) { 297 | $file = $upload_dir . $filename; 298 | if ( ! $only_regenerate_missing_thumbnails || in_array( $size, $missing_thumbnails ) ) { 299 | $this->assertFileExists( $file ); 300 | $this->assertNotEquals( $filemtimes[ $size ], filemtime( $file ) ); 301 | } else { 302 | $this->assertEquals( $filemtimes[ $size ], filemtime( $file ) ); 303 | } 304 | } 305 | } 306 | 307 | public function test_delete_unregistered_thumbnail_files_no() { 308 | $this->helper_delete_unregistered_thumbnail_files( false ); 309 | } 310 | 311 | public function test_delete_unregistered_thumbnail_files_yes() { 312 | $this->helper_delete_unregistered_thumbnail_files( true ); 313 | } 314 | 315 | public function helper_delete_unregistered_thumbnail_files( $delete_unregistered_thumbnail_files ) { 316 | add_image_size( 'regenerate-thumbnails-test-inmeta', 521, 567 ); 317 | add_image_size( 'regenerate-thumbnails-test-notinmeta', 621, 667 ); 318 | 319 | // Both test thumbnails will be created on upload 320 | $this->attachment_id = $this->helper_create_attachment(); 321 | $old_metadata = wp_get_attachment_metadata( $this->attachment_id ); 322 | 323 | copy( DIR_TESTDATA . '/images/33772.jpg', DIR_TESTDATA . '/images/33772-123x456.jpg' ); 324 | $attachment_to_keep_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/33772-123x456.jpg' ); 325 | 326 | $this->assertArrayHasKey( 'regenerate-thumbnails-test-inmeta', $old_metadata['sizes'] ); 327 | $this->assertArrayHasKey( 'regenerate-thumbnails-test-notinmeta', $old_metadata['sizes'] ); 328 | 329 | if ( function_exists( 'wp_get_original_image_path' ) ) { 330 | $thumbnail_file_to_keep = wp_get_original_image_path( $attachment_to_keep_id ); 331 | $fullsize_image = wp_get_original_image_path( $this->attachment_id ); 332 | } else { 333 | $thumbnail_file_to_keep = get_attached_file( $attachment_to_keep_id ); 334 | $fullsize_image = get_attached_file( $this->attachment_id ); 335 | } 336 | $thumbnail_file_inmeta = dirname( $fullsize_image ) . DIRECTORY_SEPARATOR . $old_metadata['sizes']['regenerate-thumbnails-test-inmeta']['file']; 337 | $thumbnail_file_notinmeta = dirname( $fullsize_image ) . DIRECTORY_SEPARATOR . $old_metadata['sizes']['regenerate-thumbnails-test-notinmeta']['file']; 338 | 339 | $this->assertFileExists( $thumbnail_file_inmeta ); 340 | $this->assertFileExists( $thumbnail_file_notinmeta ); 341 | $this->assertFileExists( $thumbnail_file_to_keep ); 342 | 343 | remove_image_size( 'regenerate-thumbnails-test-notinmeta' ); 344 | 345 | // After this, "inmeta" will be in the meta and "notinmeta" will exist but not be in the meta 346 | require_once( ABSPATH . 'wp-admin/includes/admin.php' ); 347 | if ( function_exists( 'wp_get_original_image_path' ) ) { 348 | $step2_metadata = wp_generate_attachment_metadata( $this->attachment_id, wp_get_original_image_path( $this->attachment_id ) ); 349 | } else { 350 | $step2_metadata = wp_generate_attachment_metadata( $this->attachment_id, get_attached_file( $this->attachment_id ) ); 351 | } 352 | wp_update_attachment_metadata( $this->attachment_id, $step2_metadata ); 353 | 354 | $step2_metadata = wp_get_attachment_metadata( $this->attachment_id ); 355 | 356 | $this->assertArrayHasKey( 'regenerate-thumbnails-test-inmeta', $step2_metadata['sizes'] ); 357 | $this->assertArrayNotHasKey( 'regenerate-thumbnails-test-notinmeta', $step2_metadata['sizes'] ); 358 | 359 | $this->assertFileExists( $thumbnail_file_inmeta ); 360 | $this->assertFileExists( $thumbnail_file_notinmeta ); 361 | $this->assertFileExists( $thumbnail_file_to_keep ); 362 | 363 | remove_image_size( 'regenerate-thumbnails-test-inmeta' ); 364 | 365 | // Now let's verify that the regenerate() method works as expected 366 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 367 | $regenerator->regenerate( array( 368 | 'delete_unregistered_thumbnail_files' => $delete_unregistered_thumbnail_files, 369 | ) ); 370 | 371 | $new_metadata = wp_get_attachment_metadata( $this->attachment_id ); 372 | 373 | if ( $delete_unregistered_thumbnail_files ) { 374 | $this->assertFileNotExists( $thumbnail_file_inmeta ); 375 | $this->assertArrayNotHasKey( 'regenerate-thumbnails-test-inmeta', $new_metadata['sizes'] ); 376 | $this->assertFileNotExists( $thumbnail_file_notinmeta ); 377 | $this->assertArrayNotHasKey( 'regenerate-thumbnails-test-notinmeta', $new_metadata['sizes'] ); 378 | } else { 379 | $this->assertFileExists( $thumbnail_file_inmeta ); 380 | $this->assertArrayHasKey( 'regenerate-thumbnails-test-inmeta', $new_metadata['sizes'] ); 381 | $this->assertFileExists( $thumbnail_file_notinmeta ); 382 | } 383 | 384 | $this->assertFileExists( $thumbnail_file_to_keep ); 385 | } 386 | 387 | public function test_verify_that_site_icons_are_not_regenerated() { 388 | $this->attachment_id = $this->helper_create_attachment(); 389 | 390 | // See wp_ajax_crop_image() 391 | 392 | require_once( ABSPATH . '/wp-admin/includes/class-wp-site-icon.php' ); 393 | $wp_site_icon = new WP_Site_Icon(); 394 | 395 | $cropped = wp_crop_image( $this->attachment_id, 1300, 300, 512, 512, 512, 512 ); 396 | $this->assertInternalType( 'string', $cropped ); 397 | 398 | $object = $wp_site_icon->create_attachment_object( $cropped, $this->attachment_id ); 399 | unset( $object['ID'] ); 400 | 401 | // Update the attachment. 402 | add_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) ); 403 | $this->attachment_id = $wp_site_icon->insert_attachment( $object, $cropped ); 404 | remove_filter( 'intermediate_image_sizes_advanced', array( $wp_site_icon, 'additional_sizes' ) ); 405 | 406 | $attachment_metadata = wp_get_attachment_metadata( $this->attachment_id ); 407 | 408 | $thumbnails = array(); 409 | foreach ( $attachment_metadata['sizes'] as $size => $size_data ) { 410 | $thumbnails[ $size ] = $size_data['file']; 411 | } 412 | 413 | if ( function_exists( 'wp_get_original_image_path' ) ) { 414 | $upload_dir = dirname( wp_get_original_image_path( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 415 | } else { 416 | $upload_dir = dirname( get_attached_file( $this->attachment_id ) ) . DIRECTORY_SEPARATOR; 417 | } 418 | 419 | $filemtimes = $this->helper_get_filemtimes( $upload_dir, $thumbnails ); 420 | 421 | // Sleep to make sure that filemtime() will change if the thumbnail files do 422 | sleep( 1 ); 423 | 424 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 425 | 426 | $this->assertInstanceOf( 'WP_Error', $regenerator ); 427 | $this->assertEquals( 'regenerate_thumbnails_regenerator_is_site_icon', $regenerator->get_error_code() ); 428 | 429 | // Clear the file stat cache to make sure that filemtime() works correctly 430 | clearstatcache(); 431 | 432 | // Verify that none of the thumbnail files have changed 433 | foreach ( $attachment_metadata['sizes'] as $size => $size_data ) { 434 | $file = $upload_dir . $size_data['file']; 435 | $this->assertFileExists( $file ); 436 | $this->assertEquals( $filemtimes[ $size ], filemtime( $file ) ); 437 | } 438 | } 439 | 440 | public function test_update_usages_in_posts() { 441 | $this->markTestSkipped( 'Temporarily disabled while the functionality is reworked.' ); 442 | 443 | $this->attachment_id = $this->helper_create_attachment(); 444 | 445 | $thumbnail_thumbnail = image_downsize( $this->attachment_id, 'thumbnail' ); 446 | $thumbnail_medium = image_downsize( $this->attachment_id, 'medium' ); 447 | $thumbnail_large = image_downsize( $this->attachment_id, 'large' ); 448 | 449 | $test_contents = array( 450 | '', 451 | 'WordPress', 452 | '[caption id="attachment_' . $this->attachment_id . '" align="aligncenter" width="300"] This is the caption[/caption]', 453 | 'alt', 454 | ); 455 | 456 | $post_ids = array(); 457 | foreach ( $test_contents as $test_content ) { 458 | $post_ids[] = self::factory()->post->create( array( 459 | 'post_content' => $test_content, 460 | ) ); 461 | } 462 | 463 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 464 | add_filter( 'pre_option_' . $filter, $function ); 465 | }; 466 | 467 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 468 | $regenerator->regenerate(); 469 | $regenerator->update_usages_in_posts(); 470 | 471 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 472 | remove_filter( 'pre_option_' . $filter, $function ); 473 | } 474 | 475 | $thumbnail_thumbnail = image_downsize( $this->attachment_id, 'thumbnail' ); 476 | $thumbnail_medium = image_downsize( $this->attachment_id, 'medium' ); 477 | $thumbnail_large = image_downsize( $this->attachment_id, 'large' ); 478 | 479 | $result_contents = array( 480 | '', 481 | 'WordPress', 482 | '[caption id="attachment_' . $this->attachment_id . '" align="aligncenter" width="350"] This is the caption[/caption]', 483 | 'alt', 484 | ); 485 | 486 | $counter = 0; 487 | foreach ( $post_ids as $post_id ) { 488 | $post = get_post( $post_id ); 489 | 490 | $this->assertSame( $result_contents[ $counter ], $post->post_content ); 491 | 492 | $counter ++; 493 | } 494 | } 495 | 496 | public function helper_get_current_thumbnail_statuses() { 497 | $attachment = get_post( $this->attachment_id ); 498 | if ( function_exists( 'wp_get_original_image_path' ) ) { 499 | $fullsizepath = wp_get_original_image_path( $this->attachment_id ); 500 | } else { 501 | $fullsizepath = get_attached_file( $this->attachment_id ); 502 | } 503 | 504 | return array( 505 | 'name' => $attachment->post_title, 506 | 'preview' => wp_get_attachment_url( $this->attachment_id ), 507 | 'relative_path' => _wp_get_attachment_relative_path( $fullsizepath ) . DIRECTORY_SEPARATOR . '33772.jpg', 508 | 'edit_url' => get_edit_post_link( $attachment->ID, 'raw' ), 509 | 'width' => 1920, 510 | 'height' => 1080, 511 | 'registered_sizes' => array( 512 | array( 513 | 'label' => 'thumbnail', 514 | 'width' => 150, 515 | 'height' => 150, 516 | 'crop' => true, 517 | 'filename' => '33772-150x150.jpg', 518 | 'fileexists' => true, 519 | ), 520 | array( 521 | 'label' => 'medium', 522 | 'width' => 300, 523 | 'height' => 300, 524 | 'crop' => false, 525 | 'filename' => '33772-300x169.jpg', 526 | 'fileexists' => true, 527 | ), 528 | array( 529 | 'label' => 'medium_large', 530 | 'width' => 768, 531 | 'height' => 0, 532 | 'crop' => false, 533 | 'filename' => '33772-768x432.jpg', 534 | 'fileexists' => true, 535 | ), 536 | array( 537 | 'label' => 'large', 538 | 'width' => 1024, 539 | 'height' => 1024, 540 | 'crop' => false, 541 | 'filename' => '33772-1024x576.jpg', 542 | 'fileexists' => true, 543 | ), 544 | ), 545 | 'unregistered_sizes' => array(), 546 | ); 547 | } 548 | 549 | /** 550 | * self::factory()->attachment->create_upload_object() uses basename() instead of wp_basename() 551 | * 552 | * @link https://core.trac.wordpress.org/ticket/43170 553 | * 554 | * @see WP_UnitTest_Factory_For_Attachment::create_upload_object() 555 | */ 556 | public function helper_create_upload_object_utf8( $file, $parent = 0 ) { 557 | $contents = file_get_contents( $file ); 558 | $upload = wp_upload_bits( wp_basename( $file ), null, $contents ); 559 | 560 | $type = ''; 561 | if ( ! empty( $upload['type'] ) ) { 562 | $type = $upload['type']; 563 | } else { 564 | $mime = wp_check_filetype( $upload['file'] ); 565 | if ( $mime ) { 566 | $type = $mime['type']; 567 | } 568 | } 569 | 570 | $attachment = array( 571 | 'post_title' => wp_basename( $upload['file'] ), 572 | 'post_content' => '', 573 | 'post_type' => 'attachment', 574 | 'post_parent' => $parent, 575 | 'post_mime_type' => $type, 576 | 'guid' => $upload['url'], 577 | ); 578 | 579 | // Save the data 580 | $id = wp_insert_attachment( $attachment, $upload['file'], $parent ); 581 | wp_update_attachment_metadata( $id, wp_generate_attachment_metadata( $id, $upload['file'] ) ); 582 | 583 | return $id; 584 | } 585 | 586 | public function test_get_current_thumbnail_statuses_normal() { 587 | // To get edit_url to work correctly 588 | $admin_id = self::factory()->user->create( array( 589 | 'role' => 'administrator', 590 | ) ); 591 | wp_set_current_user( $admin_id ); 592 | 593 | $this->attachment_id = $this->helper_create_attachment(); 594 | 595 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 596 | $statuses = $regenerator->get_attachment_info(); 597 | 598 | $this->assertSame( $statuses, $this->helper_get_current_thumbnail_statuses() ); 599 | } 600 | 601 | /** 602 | * @link https://github.com/Viper007Bond/regenerate-thumbnails/issues/62 603 | */ 604 | public function test_get_current_thumbnail_statuses_utf8_filename() { 605 | $test_file_path = get_temp_dir() . '麻美.jpg'; 606 | copy( DIR_TESTDATA . '/images/33772.jpg', $test_file_path ); 607 | $this->assertFileExists( $test_file_path ); 608 | 609 | $this->attachment_id = $this->helper_create_upload_object_utf8( $test_file_path ); 610 | 611 | if ( function_exists( 'wp_get_original_image_path' ) ) { 612 | $fullsizepath = wp_get_original_image_path( $this->attachment_id ); 613 | } else { 614 | $fullsizepath = get_attached_file( $this->attachment_id ); 615 | } 616 | 617 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 618 | $statuses = $regenerator->get_attachment_info(); 619 | 620 | $this->assertSame( $statuses['relative_path'], _wp_get_attachment_relative_path( $fullsizepath ) . DIRECTORY_SEPARATOR . '麻美.jpg' ); 621 | } 622 | 623 | public function test_get_current_thumbnail_statuses_with_unregistered_size() { 624 | add_image_size( 'regenerate-thumbnails-test', 500, 500 ); 625 | $this->attachment_id = $this->helper_create_attachment(); 626 | remove_image_size( 'regenerate-thumbnails-test' ); 627 | 628 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 629 | $statuses = $regenerator->get_attachment_info(); 630 | 631 | $expected_statuses = $this->helper_get_current_thumbnail_statuses(); 632 | 633 | $expected_statuses['unregistered_sizes'][] = array( 634 | 'label' => 'regenerate-thumbnails-test', 635 | 'width' => 500, 636 | 'height' => 281, 637 | 'filename' => '33772-500x281.jpg', 638 | 'fileexists' => true, 639 | ); 640 | 641 | $this->assertSame( $statuses, $expected_statuses ); 642 | } 643 | 644 | public function test_get_current_thumbnail_statuses_with_changed_sizes() { 645 | $this->attachment_id = $this->helper_create_attachment(); 646 | 647 | // Now change the thumbnail sizes to something other than the defaults 648 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 649 | add_filter( 'pre_option_' . $filter, $function ); 650 | }; 651 | 652 | $regenerator = RegenerateThumbnails_Regenerator::get_instance( $this->attachment_id ); 653 | $statuses = $regenerator->get_attachment_info(); 654 | 655 | foreach ( $this->helper_get_custom_thumbnail_size_callbacks() as $filter => $function ) { 656 | remove_filter( 'pre_option_' . $filter, $function ); 657 | } 658 | 659 | $expected_statuses = $this->helper_get_current_thumbnail_statuses(); 660 | 661 | $expected_statuses['registered_sizes'] = array( 662 | array( 663 | 'label' => 'thumbnail', 664 | 'width' => 100, 665 | 'height' => 100, 666 | 'crop' => false, 667 | 'filename' => '33772-100x56.jpg', 668 | 'fileexists' => false, 669 | ), 670 | array( 671 | 'label' => 'medium', 672 | 'width' => 350, 673 | 'height' => 350, 674 | 'crop' => false, 675 | 'filename' => '33772-350x197.jpg', 676 | 'fileexists' => false, 677 | ), 678 | array( 679 | 'label' => 'medium_large', 680 | 'width' => 500, 681 | 'height' => 500, 682 | 'crop' => false, 683 | 'filename' => '33772-500x281.jpg', 684 | 'fileexists' => false, 685 | ), 686 | array( 687 | 'label' => 'large', 688 | 'width' => 1500, 689 | 'height' => 1500, 690 | 'crop' => false, 691 | 'filename' => '33772-1500x844.jpg', 692 | 'fileexists' => false, 693 | ), 694 | ); 695 | 696 | $expected_statuses['unregistered_sizes'] = array( 697 | array( 698 | 'label' => sprintf( __( '%s (old)', 'regenerate-thumbnails' ), 'thumbnail' ), 699 | 'width' => 150, 700 | 'height' => 150, 701 | 'filename' => '33772-150x150.jpg', 702 | 'fileexists' => true, 703 | ), 704 | array( 705 | 'label' => sprintf( __( '%s (old)', 'regenerate-thumbnails' ), 'medium' ), 706 | 'width' => 300, 707 | 'height' => 169, 708 | 'filename' => '33772-300x169.jpg', 709 | 'fileexists' => true, 710 | ), 711 | array( 712 | 'label' => sprintf( __( '%s (old)', 'regenerate-thumbnails' ), 'medium_large' ), 713 | 'width' => 768, 714 | 'height' => 432, 715 | 'filename' => '33772-768x432.jpg', 716 | 'fileexists' => true, 717 | ), 718 | array( 719 | 'label' => sprintf( __( '%s (old)', 'regenerate-thumbnails' ), 'large' ), 720 | 'width' => 1024, 721 | 'height' => 576, 722 | 'filename' => '33772-1024x576.jpg', 723 | 'fileexists' => true, 724 | ), 725 | ); 726 | 727 | $this->assertSame( $statuses, $expected_statuses ); 728 | } 729 | } 730 | -------------------------------------------------------------------------------- /tests/test-rest-api.php: -------------------------------------------------------------------------------- 1 | user->create( array( 25 | 'role' => 'administrator', 26 | 'user_login' => 'superadmin', 27 | ) ); 28 | self::$contributor_id = $factory->user->create( array( 29 | 'role' => 'contributor', 30 | ) ); 31 | 32 | if ( is_multisite() ) { 33 | update_site_option( 'site_admins', array( 'superadmin' ) ); 34 | } 35 | } 36 | 37 | public static function wpTearDownAfterClass() { 38 | self::delete_user( self::$superadmin_id ); 39 | self::delete_user( self::$contributor_id ); 40 | } 41 | 42 | public function setUp() { 43 | parent::setUp(); 44 | 45 | add_filter( 'rest_url', array( $this, 'filter_rest_url_for_leading_slash' ), 10, 2 ); 46 | 47 | /** @var WP_REST_Server $wp_rest_server */ 48 | global $wp_rest_server; 49 | $this->server = $wp_rest_server = new Spy_REST_Server; 50 | do_action( 'rest_api_init' ); 51 | 52 | $this->attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/test-image.jpg' ); 53 | } 54 | 55 | public function tearDown() { 56 | remove_filter( 'rest_url', array( $this, 'test_rest_url_for_leading_slash' ), 10 ); 57 | 58 | /** @var WP_REST_Server $wp_rest_server */ 59 | global $wp_rest_server; 60 | $wp_rest_server = null; 61 | 62 | wp_delete_attachment( $this->attachment_id, true ); 63 | 64 | // Just to be sure 65 | Regenerate_Thumbnails_Tests_Helper::delete_upload_dir_contents(); 66 | 67 | parent::tearDown(); 68 | } 69 | 70 | public function filter_rest_url_for_leading_slash( $url, $path ) { 71 | if ( is_multisite() ) { 72 | return $url; 73 | } 74 | 75 | // Make sure path for rest_url has a leading slash for proper resolution. 76 | $this->assertTrue( 0 === strpos( $path, '/' ), 'REST API URL should have a leading slash.' ); 77 | 78 | return $url; 79 | } 80 | 81 | private function assertResponseStatus( $status, $response ) { 82 | $this->assertEquals( $status, $response->get_status() ); 83 | } 84 | 85 | // See https://core.trac.wordpress.org/changeset/42421 86 | private function get_rest_forbidden_status_code() { 87 | global $wp_version; 88 | 89 | if ( version_compare( $wp_version, '4.9.1', '>' ) ) { 90 | return rest_authorization_required_code(); 91 | } else { 92 | return 403; 93 | } 94 | } 95 | 96 | public function test_regenerate_logged_out() { 97 | wp_set_current_user( 0 ); 98 | 99 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/regenerate/' . $this->attachment_id ); 100 | $response = $this->server->dispatch( $request ); 101 | 102 | $this->assertErrorResponse( 'rest_forbidden', $response, $this->get_rest_forbidden_status_code() ); 103 | } 104 | 105 | public function test_regenerate_without_permission() { 106 | wp_set_current_user( self::$contributor_id ); 107 | 108 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/regenerate/' . $this->attachment_id ); 109 | $response = $this->server->dispatch( $request ); 110 | 111 | $this->assertErrorResponse( 'rest_forbidden', $response, $this->get_rest_forbidden_status_code() ); 112 | } 113 | 114 | public function test_regenerator_with_permission() { 115 | wp_set_current_user( self::$superadmin_id ); 116 | 117 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/regenerate/' . $this->attachment_id ); 118 | $response = $this->server->dispatch( $request ); 119 | 120 | $this->assertResponseStatus( 200, $response ); 121 | } 122 | 123 | public function test_attachmentinfo_logged_out() { 124 | wp_set_current_user( 0 ); 125 | 126 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/attachmentinfo/' . $this->attachment_id ); 127 | $response = $this->server->dispatch( $request ); 128 | 129 | $this->assertErrorResponse( 'rest_forbidden', $response, $this->get_rest_forbidden_status_code() ); 130 | } 131 | 132 | public function test_attachmentinfo_without_permission() { 133 | wp_set_current_user( self::$contributor_id ); 134 | 135 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/attachmentinfo/' . $this->attachment_id ); 136 | $response = $this->server->dispatch( $request ); 137 | 138 | $this->assertErrorResponse( 'rest_forbidden', $response, $this->get_rest_forbidden_status_code() ); 139 | } 140 | 141 | public function test_attachmentinfo_with_permission() { 142 | wp_set_current_user( self::$superadmin_id ); 143 | 144 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/attachmentinfo/' . $this->attachment_id ); 145 | $response = $this->server->dispatch( $request ); 146 | 147 | $this->assertResponseStatus( 200, $response ); 148 | } 149 | 150 | public function test_featuredimages_logged_out() { 151 | wp_set_current_user( 0 ); 152 | 153 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/featuredimages' ); 154 | $response = $this->server->dispatch( $request ); 155 | 156 | $this->assertErrorResponse( 'rest_forbidden', $response, $this->get_rest_forbidden_status_code() ); 157 | } 158 | 159 | public function test_featuredimages_without_permission() { 160 | wp_set_current_user( self::$contributor_id ); 161 | 162 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/featuredimages' ); 163 | $response = $this->server->dispatch( $request ); 164 | 165 | $this->assertErrorResponse( 'rest_forbidden', $response, $this->get_rest_forbidden_status_code() ); 166 | } 167 | 168 | public function test_featuredimages() { 169 | wp_set_current_user( self::$superadmin_id ); 170 | 171 | $attachment_ids = array(); 172 | for ( $i = 1; $i <= 5; $i ++ ) { 173 | $post_id = self::factory()->post->create( array() ); 174 | $attachment_id = self::factory()->attachment->create_upload_object( DIR_TESTDATA . '/images/test-image.jpg' ); 175 | $attachment_ids[] = (object) array( 'id' => $attachment_id ); 176 | 177 | set_post_thumbnail( $post_id, $attachment_id ); 178 | } 179 | 180 | $request = new WP_REST_Request( 'GET', '/regenerate-thumbnails/v1/featuredimages' ); 181 | $response = $this->server->dispatch( $request ); 182 | 183 | $this->assertResponseStatus( 200, $response ); 184 | $this->assertEquals( $attachment_ids, $response->data ); 185 | $this->assertSame( 5, $response->headers['X-WP-Total'] ); 186 | $this->assertSame( 1, $response->headers['X-WP-TotalPages'] ); 187 | } 188 | 189 | public function test_exclude_site_icons_from_results() { 190 | wp_set_current_user( self::$superadmin_id ); 191 | 192 | $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); 193 | $request->set_param( 'include', array( $this->attachment_id ) ); 194 | $request->set_param( 'exclude_site_icons', 1 ); 195 | $response = $this->server->dispatch( $request ); 196 | $this->assertContains( $this->attachment_id, wp_list_pluck( $response->data, 'id' ) ); 197 | 198 | update_post_meta( $this->attachment_id, '_wp_attachment_context', 'site-icon' ); 199 | 200 | $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); 201 | $request->set_param( 'include', array( $this->attachment_id ) ); 202 | $request->set_param( 'exclude_site_icons', 1 ); 203 | $response = $this->server->dispatch( $request ); 204 | $this->assertNotContains( $this->attachment_id, wp_list_pluck( $response->data, 'id' ) ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const BrowserSyncConfig = require('./browsersync-config.json'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const BrowserSyncPlugin = require('browser-sync-webpack-plugin'); 5 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 6 | const TerserPlugin = require('terser-webpack-plugin'); 7 | 8 | module.exports = { 9 | entry : './src/main.js', 10 | output : { 11 | path : path.resolve(__dirname, './dist'), 12 | publicPath: '/dist/', 13 | filename : 'build.js' 14 | }, 15 | module : { 16 | rules: [ 17 | { 18 | test : /\.vue$/, 19 | loader : 'vue-loader', 20 | options: { 21 | loaders: { 22 | // Since sass-loader (weirdly) has SCSS as its default parse mode, we map 23 | // the "scss" and "sass" values for the lang attribute to the right configs here. 24 | // other preprocessors should work out of the box, no loader config like this necessary. 25 | 'scss': 'vue-style-loader!css-loader?-url!sass-loader', 26 | 'sass': 'vue-style-loader!css-loader?-url!sass-loader?indentedSyntax' 27 | } 28 | } 29 | }, 30 | { 31 | test : /\.js$/, 32 | exclude: /node_modules/, 33 | use : { 34 | loader : 'babel-loader', 35 | options: { 36 | presets: ['env'] 37 | } 38 | } 39 | }, 40 | { 41 | test : /\.(png|jpg|gif|svg)$/, 42 | loader : 'file-loader', 43 | options: { 44 | name: '[name].[ext]?[hash]' 45 | } 46 | } 47 | ] 48 | }, 49 | resolve : { 50 | alias: { 51 | 'vue$': 'vue/dist/vue.esm.js' 52 | } 53 | }, 54 | performance: { 55 | hints: false 56 | }, 57 | devtool : '#eval-source-map', 58 | plugins : [ 59 | new BrowserSyncPlugin({ 60 | proxy : BrowserSyncConfig.WordPressInstallURL + '/wp-admin/tools.php?page=regenerate-thumbnails', 61 | host : BrowserSyncConfig.proxyHost, 62 | port : BrowserSyncConfig.proxyPort, 63 | files : [ 64 | '**/*.php', 65 | 'css/progressbar.css' 66 | ], 67 | reloadDelay: 0, 68 | notify : { 69 | styles: { 70 | top : 'auto', 71 | bottom: '0', 72 | } 73 | } 74 | } 75 | ), 76 | ], 77 | }; 78 | 79 | if (process.env.NODE_ENV === 'production') { 80 | module.exports.devtool = false; 81 | // http://vue-loader.vuejs.org/en/workflow/production.html 82 | module.exports.plugins = (module.exports.plugins || []).concat([ 83 | new webpack.DefinePlugin({ 84 | 'process.env': { 85 | NODE_ENV: '"production"' 86 | } 87 | }), 88 | new CleanWebpackPlugin([ 89 | path.resolve(__dirname, './dist'), 90 | ]), 91 | new webpack.LoaderOptionsPlugin({ 92 | minimize: true 93 | }) 94 | ]); 95 | 96 | module.exports.optimization = { 97 | minimize: true, 98 | minimizer: [ 99 | new TerserPlugin({ 100 | sourceMap: false 101 | }) 102 | ], 103 | }; 104 | } 105 | --------------------------------------------------------------------------------