├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── assets ├── banner-1544x500.png ├── banner-772x250.png ├── icon-128x128.png └── icon-256x256.png ├── bin └── install-wp-tests.sh ├── karma.conf ├── package.json ├── phpunit.xml.dist ├── sw-tests.js ├── tests ├── bootstrap.php ├── service-worker │ ├── localforage.mock.js │ ├── testDeleteOutdatedCaches.js │ ├── testFetch.js │ ├── testGet.js │ └── testUpdateCache.js └── test-sample.php └── wp-offline-content ├── class-wp-offline-content-admin.php ├── class-wp-offline-content-options.php ├── class-wp-offline-content-plugin.php ├── composer.json ├── composer.lock ├── lang └── offline-content.pot ├── lib ├── js │ └── sw.js └── pages │ └── admin.php ├── readme.txt ├── uninstall.php └── wp-offline-content.php /.gitignore: -------------------------------------------------------------------------------- 1 | tools/ 2 | node_modules/ 3 | .tern-port 4 | svn/ 5 | wp-offline-content/vendor/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | dist: trusty 3 | language: php 4 | php: 5 | # Broken on Travis 6 | # - 5.3 7 | - 5.4 8 | - 5.5 9 | - 5.6 10 | - 7.0 11 | addons: 12 | firefox: 'latest' 13 | apt: 14 | sources: 15 | - google-chrome 16 | packages: 17 | - google-chrome-stable 18 | - mysql-server-5.6 19 | - mysql-client-core-5.6 20 | - mysql-client-5.6 21 | env: 22 | global: 23 | - FIREFOX_NIGHTLY_BIN=firefox 24 | - CHROME_BIN=google-chrome-stable 25 | matrix: 26 | - WP_VERSION=3.8 WP_MULTISITE=0 27 | - WP_VERSION=3.9 WP_MULTISITE=0 28 | - WP_VERSION=4.0 WP_MULTISITE=0 29 | - WP_VERSION=4.1 WP_MULTISITE=0 30 | - WP_VERSION=4.2 WP_MULTISITE=0 31 | - WP_VERSION=4.3 WP_MULTISITE=0 32 | - WP_VERSION=4.4 WP_MULTISITE=0 33 | - WP_VERSION=latest WP_MULTISITE=0 34 | - WP_VERSION=latest WP_MULTISITE=1 35 | matrix: 36 | allow_failures: 37 | - php: 7.0 38 | install: 39 | - ./bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION 40 | - composer install --working-dir=wp-offline-content --optimize-autoloader 41 | before_script: 42 | - export DISPLAY=:99.0 43 | - sh -e /etc/init.d/xvfb start 44 | - sleep 3 45 | script: 46 | - make test 47 | - make test-sw 48 | 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | wp-offline-content - Web Push plugin for WordPress 2 | Copyright (C) 2016 by its contributors 3 | 4 | This program is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU General Public License 6 | as published by the Free Software Foundation; either version 2 7 | of the License, or (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program; if not, write to the Free Software 16 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= 19 | 20 | GNU GENERAL PUBLIC LICENSE 21 | Version 2, June 1991 22 | 23 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 24 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 25 | Everyone is permitted to copy and distribute verbatim copies 26 | of this license document, but changing it is not allowed. 27 | 28 | Preamble 29 | 30 | The licenses for most software are designed to take away your 31 | freedom to share and change it. By contrast, the GNU General Public 32 | License is intended to guarantee your freedom to share and change free 33 | software--to make sure the software is free for all its users. This 34 | General Public License applies to most of the Free Software 35 | Foundation's software and to any other program whose authors commit to 36 | using it. (Some other Free Software Foundation software is covered by 37 | the GNU Lesser General Public License instead.) You can apply it to 38 | your programs, too. 39 | 40 | When we speak of free software, we are referring to freedom, not 41 | price. Our General Public Licenses are designed to make sure that you 42 | have the freedom to distribute copies of free software (and charge for 43 | this service if you wish), that you receive source code or can get it 44 | if you want it, that you can change the software or use pieces of it 45 | in new free programs; and that you know you can do these things. 46 | 47 | To protect your rights, we need to make restrictions that forbid 48 | anyone to deny you these rights or to ask you to surrender the rights. 49 | These restrictions translate to certain responsibilities for you if you 50 | distribute copies of the software, or if you modify it. 51 | 52 | For example, if you distribute copies of such a program, whether 53 | gratis or for a fee, you must give the recipients all the rights that 54 | you have. You must make sure that they, too, receive or can get the 55 | source code. And you must show them these terms so they know their 56 | rights. 57 | 58 | We protect your rights with two steps: (1) copyright the software, and 59 | (2) offer you this license which gives you legal permission to copy, 60 | distribute and/or modify the software. 61 | 62 | Also, for each author's protection and ours, we want to make certain 63 | that everyone understands that there is no warranty for this free 64 | software. If the software is modified by someone else and passed on, we 65 | want its recipients to know that what they have is not the original, so 66 | that any problems introduced by others will not reflect on the original 67 | authors' reputations. 68 | 69 | Finally, any free program is threatened constantly by software 70 | patents. We wish to avoid the danger that redistributors of a free 71 | program will individually obtain patent licenses, in effect making the 72 | program proprietary. To prevent this, we have made it clear that any 73 | patent must be licensed for everyone's free use or not licensed at all. 74 | 75 | The precise terms and conditions for copying, distribution and 76 | modification follow. 77 | 78 | GNU GENERAL PUBLIC LICENSE 79 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 80 | 81 | 0. This License applies to any program or other work which contains 82 | a notice placed by the copyright holder saying it may be distributed 83 | under the terms of this General Public License. The "Program", below, 84 | refers to any such program or work, and a "work based on the Program" 85 | means either the Program or any derivative work under copyright law: 86 | that is to say, a work containing the Program or a portion of it, 87 | either verbatim or with modifications and/or translated into another 88 | language. (Hereinafter, translation is included without limitation in 89 | the term "modification".) Each licensee is addressed as "you". 90 | 91 | Activities other than copying, distribution and modification are not 92 | covered by this License; they are outside its scope. The act of 93 | running the Program is not restricted, and the output from the Program 94 | is covered only if its contents constitute a work based on the 95 | Program (independent of having been made by running the Program). 96 | Whether that is true depends on what the Program does. 97 | 98 | 1. You may copy and distribute verbatim copies of the Program's 99 | source code as you receive it, in any medium, provided that you 100 | conspicuously and appropriately publish on each copy an appropriate 101 | copyright notice and disclaimer of warranty; keep intact all the 102 | notices that refer to this License and to the absence of any warranty; 103 | and give any other recipients of the Program a copy of this License 104 | along with the Program. 105 | 106 | You may charge a fee for the physical act of transferring a copy, and 107 | you may at your option offer warranty protection in exchange for a fee. 108 | 109 | 2. You may modify your copy or copies of the Program or any portion 110 | of it, thus forming a work based on the Program, and copy and 111 | distribute such modifications or work under the terms of Section 1 112 | above, provided that you also meet all of these conditions: 113 | 114 | a) You must cause the modified files to carry prominent notices 115 | stating that you changed the files and the date of any change. 116 | 117 | b) You must cause any work that you distribute or publish, that in 118 | whole or in part contains or is derived from the Program or any 119 | part thereof, to be licensed as a whole at no charge to all third 120 | parties under the terms of this License. 121 | 122 | c) If the modified program normally reads commands interactively 123 | when run, you must cause it, when started running for such 124 | interactive use in the most ordinary way, to print or display an 125 | announcement including an appropriate copyright notice and a 126 | notice that there is no warranty (or else, saying that you provide 127 | a warranty) and that users may redistribute the program under 128 | these conditions, and telling the user how to view a copy of this 129 | License. (Exception: if the Program itself is interactive but 130 | does not normally print such an announcement, your work based on 131 | the Program is not required to print an announcement.) 132 | 133 | These requirements apply to the modified work as a whole. If 134 | identifiable sections of that work are not derived from the Program, 135 | and can be reasonably considered independent and separate works in 136 | themselves, then this License, and its terms, do not apply to those 137 | sections when you distribute them as separate works. But when you 138 | distribute the same sections as part of a whole which is a work based 139 | on the Program, the distribution of the whole must be on the terms of 140 | this License, whose permissions for other licensees extend to the 141 | entire whole, and thus to each and every part regardless of who wrote it. 142 | 143 | Thus, it is not the intent of this section to claim rights or contest 144 | your rights to work written entirely by you; rather, the intent is to 145 | exercise the right to control the distribution of derivative or 146 | collective works based on the Program. 147 | 148 | In addition, mere aggregation of another work not based on the Program 149 | with the Program (or with a work based on the Program) on a volume of 150 | a storage or distribution medium does not bring the other work under 151 | the scope of this License. 152 | 153 | 3. You may copy and distribute the Program (or a work based on it, 154 | under Section 2) in object code or executable form under the terms of 155 | Sections 1 and 2 above provided that you also do one of the following: 156 | 157 | a) Accompany it with the complete corresponding machine-readable 158 | source code, which must be distributed under the terms of Sections 159 | 1 and 2 above on a medium customarily used for software interchange; or, 160 | 161 | b) Accompany it with a written offer, valid for at least three 162 | years, to give any third party, for a charge no more than your 163 | cost of physically performing source distribution, a complete 164 | machine-readable copy of the corresponding source code, to be 165 | distributed under the terms of Sections 1 and 2 above on a medium 166 | customarily used for software interchange; or, 167 | 168 | c) Accompany it with the information you received as to the offer 169 | to distribute corresponding source code. (This alternative is 170 | allowed only for noncommercial distribution and only if you 171 | received the program in object code or executable form with such 172 | an offer, in accord with Subsection b above.) 173 | 174 | The source code for a work means the preferred form of the work for 175 | making modifications to it. For an executable work, complete source 176 | code means all the source code for all modules it contains, plus any 177 | associated interface definition files, plus the scripts used to 178 | control compilation and installation of the executable. However, as a 179 | special exception, the source code distributed need not include 180 | anything that is normally distributed (in either source or binary 181 | form) with the major components (compiler, kernel, and so on) of the 182 | operating system on which the executable runs, unless that component 183 | itself accompanies the executable. 184 | 185 | If distribution of executable or object code is made by offering 186 | access to copy from a designated place, then offering equivalent 187 | access to copy the source code from the same place counts as 188 | distribution of the source code, even though third parties are not 189 | compelled to copy the source along with the object code. 190 | 191 | 4. You may not copy, modify, sublicense, or distribute the Program 192 | except as expressly provided under this License. Any attempt 193 | otherwise to copy, modify, sublicense or distribute the Program is 194 | void, and will automatically terminate your rights under this License. 195 | However, parties who have received copies, or rights, from you under 196 | this License will not have their licenses terminated so long as such 197 | parties remain in full compliance. 198 | 199 | 5. You are not required to accept this License, since you have not 200 | signed it. However, nothing else grants you permission to modify or 201 | distribute the Program or its derivative works. These actions are 202 | prohibited by law if you do not accept this License. Therefore, by 203 | modifying or distributing the Program (or any work based on the 204 | Program), you indicate your acceptance of this License to do so, and 205 | all its terms and conditions for copying, distributing or modifying 206 | the Program or works based on it. 207 | 208 | 6. Each time you redistribute the Program (or any work based on the 209 | Program), the recipient automatically receives a license from the 210 | original licensor to copy, distribute or modify the Program subject to 211 | these terms and conditions. You may not impose any further 212 | restrictions on the recipients' exercise of the rights granted herein. 213 | You are not responsible for enforcing compliance by third parties to 214 | this License. 215 | 216 | 7. If, as a consequence of a court judgment or allegation of patent 217 | infringement or for any other reason (not limited to patent issues), 218 | conditions are imposed on you (whether by court order, agreement or 219 | otherwise) that contradict the conditions of this License, they do not 220 | excuse you from the conditions of this License. If you cannot 221 | distribute so as to satisfy simultaneously your obligations under this 222 | License and any other pertinent obligations, then as a consequence you 223 | may not distribute the Program at all. For example, if a patent 224 | license would not permit royalty-free redistribution of the Program by 225 | all those who receive copies directly or indirectly through you, then 226 | the only way you could satisfy both it and this License would be to 227 | refrain entirely from distribution of the Program. 228 | 229 | If any portion of this section is held invalid or unenforceable under 230 | any particular circumstance, the balance of the section is intended to 231 | apply and the section as a whole is intended to apply in other 232 | circumstances. 233 | 234 | It is not the purpose of this section to induce you to infringe any 235 | patents or other property right claims or to contest validity of any 236 | such claims; this section has the sole purpose of protecting the 237 | integrity of the free software distribution system, which is 238 | implemented by public license practices. Many people have made 239 | generous contributions to the wide range of software distributed 240 | through that system in reliance on consistent application of that 241 | system; it is up to the author/donor to decide if he or she is willing 242 | to distribute software through any other system and a licensee cannot 243 | impose that choice. 244 | 245 | This section is intended to make thoroughly clear what is believed to 246 | be a consequence of the rest of this License. 247 | 248 | 8. If the distribution and/or use of the Program is restricted in 249 | certain countries either by patents or by copyrighted interfaces, the 250 | original copyright holder who places the Program under this License 251 | may add an explicit geographical distribution limitation excluding 252 | those countries, so that distribution is permitted only in or among 253 | countries not thus excluded. In such case, this License incorporates 254 | the limitation as if written in the body of this License. 255 | 256 | 9. The Free Software Foundation may publish revised and/or new versions 257 | of the General Public License from time to time. Such new versions will 258 | be similar in spirit to the present version, but may differ in detail to 259 | address new problems or concerns. 260 | 261 | Each version is given a distinguishing version number. If the Program 262 | specifies a version number of this License which applies to it and "any 263 | later version", you have the option of following the terms and conditions 264 | either of that version or of any later version published by the Free 265 | Software Foundation. If the Program does not specify a version number of 266 | this License, you may choose any version ever published by the Free Software 267 | Foundation. 268 | 269 | 10. If you wish to incorporate parts of the Program into other free 270 | programs whose distribution conditions are different, write to the author 271 | to ask for permission. For software which is copyrighted by the Free 272 | Software Foundation, write to the Free Software Foundation; we sometimes 273 | make exceptions for this. Our decision will be guided by the two goals 274 | of preserving the free status of all derivatives of our free software and 275 | of promoting the sharing and reuse of software generally. 276 | 277 | NO WARRANTY 278 | 279 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 280 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 281 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 282 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 283 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 284 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 285 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 286 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 287 | REPAIR OR CORRECTION. 288 | 289 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 290 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 291 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 292 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 293 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 294 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 295 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 296 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 297 | POSSIBILITY OF SUCH DAMAGES. 298 | 299 | END OF TERMS AND CONDITIONS 300 | 301 | How to Apply These Terms to Your New Programs 302 | 303 | If you develop a new program, and you want it to be of the greatest 304 | possible use to the public, the best way to achieve this is to make it 305 | free software which everyone can redistribute and change under these terms. 306 | 307 | To do so, attach the following notices to the program. It is safest 308 | to attach them to the start of each source file to most effectively 309 | convey the exclusion of warranty; and each file should have at least 310 | the "copyright" line and a pointer to where the full notice is found. 311 | 312 | 313 | Copyright (C) 314 | 315 | This program is free software; you can redistribute it and/or modify 316 | it under the terms of the GNU General Public License as published by 317 | the Free Software Foundation; either version 2 of the License, or 318 | (at your option) any later version. 319 | 320 | This program is distributed in the hope that it will be useful, 321 | but WITHOUT ANY WARRANTY; without even the implied warranty of 322 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 323 | GNU General Public License for more details. 324 | 325 | You should have received a copy of the GNU General Public License along 326 | with this program; if not, write to the Free Software Foundation, Inc., 327 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 328 | 329 | Also add information on how to contact you by electronic and paper mail. 330 | 331 | If the program is interactive, make it output a short notice like this 332 | when it starts in an interactive mode: 333 | 334 | Gnomovision version 69, Copyright (C) year name of author 335 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 336 | This is free software, and you are welcome to redistribute it 337 | under certain conditions; type `show c' for details. 338 | 339 | The hypothetical commands `show w' and `show c' should show the appropriate 340 | parts of the General Public License. Of course, the commands you use may 341 | be called something other than `show w' and `show c'; they could even be 342 | mouse-clicks or menu items--whatever suits your program. 343 | 344 | You should also get your employer (if you work as a programmer) or your 345 | school, if any, to sign a "copyright disclaimer" for the program, if 346 | necessary. Here is a sample; alter the names: 347 | 348 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 349 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 350 | 351 | , 1 April 1989 352 | Ty Coon, President of Vice 353 | 354 | This General Public License does not permit incorporating your program into 355 | proprietary programs. If your program is a subroutine library, you may 356 | consider it more useful to permit linking proprietary applications with the 357 | library. If this is what you want to do, use the GNU Lesser General 358 | Public License instead of this License. 359 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: reinstall test test-sw svn 2 | 3 | PLUGIN_DIR = wp-offline-content 4 | 5 | WP_CLI = tools/wp-cli.phar 6 | PHPUNIT = tools/phpunit.phar 7 | PLUGIN_ZIP = $(PLUGIN_DIR).zip 8 | 9 | reinstall: $(WP_CLI) 10 | $(WP_CLI) plugin uninstall --deactivate $(PLUGIN_DIR) --path=$(WORDPRESS_PATH) 11 | rm -f $(PLUGIN_ZIP) 12 | zip $(PLUGIN_ZIP) -r $(PLUGIN_DIR) 13 | $(WP_CLI) plugin install --activate $(PLUGIN_ZIP) --path=$(WORDPRESS_PATH) 14 | 15 | svn: 16 | @echo "Copying $(PLUGIN_DIR) contents to svn/trunk" 17 | @rsync -a --delete $(PLUGIN_DIR)/* svn/trunk 18 | @echo "Removing .git repositories from bundle" 19 | @find svn/trunk \( -name ".git" \) -prune -exec rm -rf {} \; 20 | 21 | test: $(PHPUNIT) 22 | $(PHPUNIT) 23 | 24 | test-sw: node_modules 25 | $(NODE) node_modules/karma/bin/karma start karma.conf 26 | 27 | node_modules: 28 | npm install 29 | 30 | tools/wp-cli.phar: 31 | mkdir -p tools 32 | wget -P tools -N https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar 33 | chmod +x $(WP_CLI) 34 | 35 | tools/phpunit.phar: 36 | mkdir -p tools 37 | wget -P tools -N https://phar.phpunit.de/phpunit-old.phar 38 | mv tools/phpunit-old.phar tools/phpunit.phar 39 | chmod +x $(PHPUNIT) 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wp-offline-content 2 | > A WordPress plugin for offlining content. 3 | 4 | [![Build Status](https://travis-ci.org/mozilla/wp-offline-content.svg?branch=master)](https://travis-ci.org/mozilla/wp-offline-content) [![WordPress plugin](https://img.shields.io/wordpress/plugin/v/offline-content.svg)](https://wordpress.org/plugins/offline-content/) [![WordPress](https://img.shields.io/wordpress/plugin/dt/offline-content.svg)](https://wordpress.org/plugins/offline-content/) 5 | 6 | > **IMPORTANT**: I'm very sorry to announce this plugin is **unmaintained** thought it is still compatible with WordPress up to version 4.5.9. 7 | 8 | ## Install the plugin 9 | 10 | You can find this plugin in the [WordPress Plugin repository](https://wordpress.org/plugins/offline-content/) so you can install it from the _Plugins_ menu of your WordPress installation. 11 | 12 | In case you want to do it manually, here are the instructions: 13 | 14 | First, clone the repository. 15 | 16 | Now, at the root of the repository, run (you need [composer](https://getcomposer.org) for this): 17 | 18 | ``` 19 | $ composer install --working-dir=wp-offline-content --optimize-autoloader 20 | ``` 21 | 22 | And copy (or symlink) the folder `wp-offline-content` inside your WordPress `plugins` directory. 23 | 24 | Once installed, activate the plugin from the _Plugins_ menu in the _Dashboard_. Options are available to customize under the _Offline content_ submenu in _Settings_. 25 | 26 | ## Running tests 27 | 28 | Install dependencies: 29 | ```bash 30 | ./bin/install-wp-tests.sh MYSQL_DATABASE_NAME MYSQL_USER MYSQL_PASSWORD localhost latest 31 | ``` 32 | 33 | Run tests: 34 | ```bash 35 | make test 36 | ``` 37 | 38 | Run service worker tests: 39 | ```bash 40 | make test-sw 41 | ``` 42 | -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/wp-offline-content/fe07667c388f1979fb2ab24654e7c1490a416eae/assets/banner-1544x500.png -------------------------------------------------------------------------------- /assets/banner-772x250.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/wp-offline-content/fe07667c388f1979fb2ab24654e7c1490a416eae/assets/banner-772x250.png -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/wp-offline-content/fe07667c388f1979fb2ab24654e7c1490a416eae/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/wp-offline-content/fe07667c388f1979fb2ab24654e7c1490a416eae/assets/icon-256x256.png -------------------------------------------------------------------------------- /bin/install-wp-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ $# -lt 3 ]; then 4 | echo "usage: $0 [db-host] [wp-version]" 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 | 14 | WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} 15 | WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} 16 | 17 | download() { 18 | if [ `which curl` ]; then 19 | curl -s "$1" > "$2"; 20 | elif [ `which wget` ]; then 21 | wget -nv -O "$2" "$1" 22 | fi 23 | } 24 | 25 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then 26 | WP_TESTS_TAG="tags/$WP_VERSION" 27 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 28 | WP_TESTS_TAG="trunk" 29 | else 30 | # http serves a single offer, whereas https serves multiple. we only want one 31 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json 32 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json 33 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') 34 | if [[ -z "$LATEST_VERSION" ]]; then 35 | echo "Latest WordPress version could not be found" 36 | exit 1 37 | fi 38 | WP_TESTS_TAG="tags/$LATEST_VERSION" 39 | fi 40 | 41 | set -ex 42 | 43 | install_wp() { 44 | 45 | if [ -d $WP_CORE_DIR ]; then 46 | return; 47 | fi 48 | 49 | mkdir -p $WP_CORE_DIR 50 | 51 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then 52 | mkdir -p /tmp/wordpress-nightly 53 | download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip 54 | unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ 55 | mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR 56 | else 57 | if [ $WP_VERSION == 'latest' ]; then 58 | local ARCHIVE_NAME='latest' 59 | else 60 | local ARCHIVE_NAME="wordpress-$WP_VERSION" 61 | fi 62 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz 63 | tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR 64 | fi 65 | 66 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php 67 | } 68 | 69 | install_test_suite() { 70 | # portable in-place argument for both GNU sed and Mac OSX sed 71 | if [[ $(uname -s) == 'Darwin' ]]; then 72 | local ioption='-i .bak' 73 | else 74 | local ioption='-i' 75 | fi 76 | 77 | # set up testing suite if it doesn't yet exist 78 | if [ ! -d $WP_TESTS_DIR ]; then 79 | # set up testing suite 80 | mkdir -p $WP_TESTS_DIR 81 | svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes 82 | fi 83 | 84 | cd $WP_TESTS_DIR 85 | 86 | if [ ! -f wp-tests-config.php ]; then 87 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php 88 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR':" "$WP_TESTS_DIR"/wp-tests-config.php 89 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php 90 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php 91 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php 92 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php 93 | fi 94 | 95 | } 96 | 97 | install_db() { 98 | # parse DB_HOST for port or socket references 99 | local PARTS=(${DB_HOST//\:/ }) 100 | local DB_HOSTNAME=${PARTS[0]}; 101 | local DB_SOCK_OR_PORT=${PARTS[1]}; 102 | local EXTRA="" 103 | 104 | if ! [ -z $DB_HOSTNAME ] ; then 105 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then 106 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" 107 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then 108 | EXTRA=" --socket=$DB_SOCK_OR_PORT" 109 | elif ! [ -z $DB_HOSTNAME ] ; then 110 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" 111 | fi 112 | fi 113 | 114 | # create database 115 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA 116 | } 117 | 118 | install_wp 119 | install_test_suite 120 | install_db 121 | -------------------------------------------------------------------------------- /karma.conf: -------------------------------------------------------------------------------- 1 | // Karma configuration for working with sw-mocha in a setup with Mocha, Chai and Sinon. 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: './', 8 | 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['sw-mocha', 'sinon', 'chai'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | { pattern: 'tests/service-worker/*.js', included: false }, 18 | { pattern: 'wp-offline-content/lib/js/*.js', included: false } 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | ], 25 | 26 | 27 | // preprocess matching files before serving them to the browser 28 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 29 | preprocessors: { 30 | }, 31 | 32 | 33 | // test results reporter to use 34 | // possible values: 'dots', 'progress' 35 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 36 | reporters: ['progress'], 37 | 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | 43 | // enable / disable colors in the output (reporters and logs) 44 | colors: true, 45 | 46 | 47 | // level of logging 48 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 49 | logLevel: config.LOG_INFO, 50 | 51 | 52 | // enable / disable watching file and executing tests whenever any file changes 53 | autoWatch: true, 54 | 55 | 56 | // start these browsers 57 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 58 | browsers: ['Chrome', 'NightlySW'], 59 | 60 | customLaunchers: { 61 | 'NightlySW': { 62 | base: 'FirefoxNightly', 63 | prefs: { 64 | 'devtools.serviceWorkers.testing.enabled': true, 65 | 'dom.serviceWorkers.enabled': true 66 | } 67 | } 68 | }, 69 | 70 | 71 | // Continuous Integration mode 72 | // if true, Karma captures browsers, runs the tests and exits 73 | singleRun: true 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-offline-content", 3 | "version": "0.6.1", 4 | "description": "A WordPress plugin for offlining content", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "dependencies": {}, 10 | "devDependencies": { 11 | "chai": "^3.5.0", 12 | "karma": "^0.13.19", 13 | "karma-chai": "^0.1.0", 14 | "karma-chrome-launcher": "^0.2.2", 15 | "karma-firefox-launcher": "^0.1.7", 16 | "karma-sinon": "^1.0.4", 17 | "karma-sw-mocha": "^0.1.2", 18 | "mocha": "^2.4.5", 19 | "sinon": "^1.17.3" 20 | }, 21 | "scripts": { 22 | "test": "make test-sw" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/delapuente/wp-offline-content.git" 27 | }, 28 | "keywords": [ 29 | "wordpress", 30 | "offline", 31 | "plugin", 32 | "content" 33 | ], 34 | "author": "Salvador de la Puente González", 35 | "license": "GPL-2.0", 36 | "bugs": { 37 | "url": "https://github.com/delapuente/wp-offline-content/issues" 38 | }, 39 | "homepage": "https://github.com/delapuente/wp-offline-content#readme" 40 | } 41 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sw-tests.js: -------------------------------------------------------------------------------- 1 | 2 | // This is the set of the SW tests. They will be run inside a SW environment. 3 | // Karma publishes the static content from /base/ path. 4 | var SW_TESTS = [ 5 | '/base/tests/service-worker/testGet.js', 6 | '/base/tests/service-worker/testFetch.js', 7 | '/base/tests/service-worker/testDeleteOutdatedCaches.js', 8 | '/base/tests/service-worker/testUpdateCache.js' 9 | ]; 10 | 11 | // Import chai and sinon into the ServiceWorkerGlobalScope 12 | importScripts('/base/node_modules/chai/chai.js'); 13 | importScripts('/base/node_modules/sinon/pkg/sinon.js'); 14 | 15 | // Import mock for localForage 16 | importScripts('/base/tests/service-worker/localforage.mock.js'); 17 | 18 | // Setup mocha to be bdd and make chai.expect globally available 19 | self.assert = chai.assert; 20 | mocha.setup({ ui: 'bdd' }); 21 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | { 21 | it('deletes any prefixed cache distinct than the current cache)', 22 | function() { 23 | wpOfflineContent.cacheName = 'currentcache'; 24 | var currentCache = [wpOfflineContent.cacheName]; 25 | var otherCaches = ['othercache1', 'othercache2']; 26 | var cacheSet = outdatedSet.concat(otherCaches).concat(currentCache); 27 | sinon.stub(self.caches, 'keys').returns(Promise.resolve(cacheSet)); 28 | 29 | return wpOfflineContent.deleteOutdatedCaches(prefix).then(() => { 30 | assert.equal(self.caches.delete.callCount, outdatedSet.length); 31 | self.caches.delete.args.forEach((callArgs, index) => { 32 | assert.equal(callArgs[0], outdatedSet[index]); 33 | }); 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/service-worker/testFetch.js: -------------------------------------------------------------------------------- 1 | describe('onFetch()', function() { 2 | 'use strict'; 3 | 4 | var fakeEvent; 5 | var fakeResponse = {}; 6 | 7 | beforeEach(function() { 8 | fakeEvent = { 9 | request: {}, 10 | respondWith: sinon.stub() 11 | }; 12 | importScripts('/base/wp-offline-content/lib/js/sw.js'); 13 | wpOfflineContent.get = sinon.stub().returns(fakeResponse); 14 | }); 15 | 16 | 17 | it('do not respond if excluded', function() { 18 | sinon.stub(wpOfflineContent, 'shouldBeHandled').returns(false); 19 | wpOfflineContent.onFetch(fakeEvent); 20 | assert.isFalse(fakeEvent.respondWith.called); 21 | }); 22 | 23 | it('respond if not excluded', function() { 24 | sinon.stub(wpOfflineContent, 'shouldBeHandled').returns(true); 25 | wpOfflineContent.onFetch(fakeEvent); 26 | assert.isTrue(fakeEvent.respondWith.calledOnce); 27 | assert.isTrue(fakeEvent.respondWith.calledWith(fakeResponse)); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/service-worker/testGet.js: -------------------------------------------------------------------------------- 1 | var NETWORK_TIMEOUT = 100; 2 | var MORE_THAN_NETWORK_TIMEOUT = NETWORK_TIMEOUT + 10; 3 | var LESS_THAN_NETWORK_TIMEOUT = NETWORK_TIMEOUT - 10; 4 | 5 | var $version = ''; 6 | var $debug = false; 7 | var $resources = []; 8 | var $excludedPaths = []; 9 | var $cacheName = 'testCache'; 10 | var $networkTimeout = NETWORK_TIMEOUT; 11 | 12 | describe('get()', function() { 13 | 'use strict'; 14 | 15 | var clock; 16 | var fakeCache; 17 | 18 | function testWithTimeout(request) { 19 | return testAdvancingTime(request, MORE_THAN_NETWORK_TIMEOUT); 20 | } 21 | 22 | function testWithoutTimeout(request) { 23 | return testAdvancingTime(request, LESS_THAN_NETWORK_TIMEOUT); 24 | } 25 | 26 | function testAdvancingTime(request, time) { 27 | var result = wpOfflineContent.get(request); 28 | clock.tick(time); 29 | return result; 30 | } 31 | 32 | beforeEach(function() { 33 | fakeCache = { 34 | put: sinon.stub().returns(Promise.resolve()) 35 | }; 36 | clock = sinon.useFakeTimers(); 37 | importScripts('/base/wp-offline-content/lib/js/sw.js'); 38 | sinon.stub(wpOfflineContent, 'openCache').returns(Promise.resolve(fakeCache)); 39 | sinon.stub(Response.prototype, 'clone').returnsThis(); 40 | }); 41 | 42 | afterEach(function() { 43 | clock.restore(); 44 | Response.prototype.clone.restore(); 45 | }); 46 | 47 | // TODO: It remains to check clone(). This can bit us. 48 | describe('get() when network is available and it does not time out', function() { 49 | var networkResponse = new Response('success!'); 50 | 51 | before(function() { 52 | sinon.stub(self, 'fetch').returns(Promise.resolve(networkResponse)); 53 | }); 54 | 55 | after(function() { 56 | self.fetch.restore(); 57 | }); 58 | 59 | it('fetches from network', function() { 60 | return testWithoutTimeout(new Request('test/url')) 61 | .then(response => { 62 | assert.strictEqual(response, networkResponse); 63 | }); 64 | }); 65 | 66 | it('stores a fresh copy in the cache', function() { 67 | var request = new Request('some/url'); 68 | return testWithoutTimeout(request) 69 | .then(() => { 70 | assert.isOk(fakeCache.put.calledOnce); 71 | assert.isOk(fakeCache.put.calledWith(request, networkResponse)); 72 | }); 73 | }); 74 | 75 | }); 76 | 77 | describe('get() when network is available but times out', function() { 78 | var networkResponse = new Response('network success!'); 79 | var cacheResponse = new Response('cache success!'); 80 | 81 | before(function() { 82 | sinon.stub(self, 'fetch').returns(new Promise(fulfil => { 83 | setTimeout(() => fulfil(networkResponse), (NETWORK_TIMEOUT + MORE_THAN_NETWORK_TIMEOUT)/2); 84 | })); 85 | }); 86 | 87 | after(function() { 88 | self.fetch.restore(); 89 | }); 90 | 91 | afterEach(function() { 92 | if (self.caches.match.restore) { self.caches.match.restore(); } 93 | }); 94 | 95 | it('fetches from cache if there is a match', function() { 96 | sinon.stub(self.caches, 'match').returns(Promise.resolve(cacheResponse)); 97 | return testWithTimeout(new Request('test/url')) 98 | .then(response => { 99 | assert.strictEqual(response, cacheResponse); 100 | }); 101 | }); 102 | 103 | it('stores a fresh copy in the cache', function() { 104 | var request = new Request('some/url'); 105 | return testWithTimeout(request) 106 | .then(() => { 107 | assert.isOk(fakeCache.put.calledOnce); 108 | assert.isOk(fakeCache.put.calledWith(request, networkResponse)); 109 | }); 110 | }); 111 | }); 112 | 113 | describe('get() when network is not available', function() { 114 | var networkError = {}; 115 | var cacheResponse = new Response('cache success!'); 116 | 117 | before(function() { 118 | sinon.stub(self, 'fetch').returns(Promise.reject(networkError)); 119 | }); 120 | 121 | after(function() { 122 | self.fetch.restore(); 123 | }); 124 | 125 | afterEach(function() { 126 | if (self.caches.match.restore) { self.caches.match.restore(); } 127 | }); 128 | 129 | it('fetches from cache if there is a match', function() { 130 | sinon.stub(self.caches, 'match').returns(Promise.resolve(cacheResponse)); 131 | return wpOfflineContent.get(new Request('/test/url')) 132 | .then(response => { 133 | assert.strictEqual(response, cacheResponse); 134 | }); 135 | }); 136 | 137 | it('error if there is no match', function() { 138 | sinon.stub(self.caches, 'match').returns(Promise.resolve(undefined)); 139 | return wpOfflineContent.get(new Request('test/url')) 140 | .then(response => { 141 | assert.isOk(false); 142 | }) 143 | .catch(error => { 144 | assert.strictEqual(error, networkError); 145 | }); 146 | }); 147 | 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /tests/service-worker/testUpdateCache.js: -------------------------------------------------------------------------------- 1 | describe('update()', function() { 2 | 'use strict'; 3 | 4 | function update(item) { 5 | item[1] += 1; 6 | return item; 7 | } 8 | 9 | function toMap(list) { 10 | return list.reduce((obj, item) => { 11 | obj[item[0]] = item[1]; 12 | return obj; 13 | }, {}); 14 | } 15 | 16 | var items = { 17 | get a() { return ['/path/to/a', '1']; }, 18 | get b() { return ['/path/to/b', '2']; }, 19 | get c() { return ['/path/to/c', '3']; }, 20 | get d() { return ['/path/to/d', '4']; } 21 | }; 22 | 23 | beforeEach(function() { 24 | importScripts('/base/wp-offline-content/lib/js/sw.js'); 25 | }); 26 | 27 | afterEach(function() { 28 | }); 29 | 30 | describe('needsUpdate()', function() { 31 | var swVersion = 'a-version'; 32 | var sameVersion = swVersion; 33 | var otherVersion = 'other-version'; 34 | 35 | it('resolves to true if version stored and in the sw do not match', function () { 36 | wpOfflineContent.version = swVersion; 37 | wpOfflineContent.storage.getItem = 38 | sinon.stub().withArgs('version').returns(Promise.resolve(otherVersion)); 39 | 40 | return wpOfflineContent.needsUpdate() 41 | .then(function(isUpdateNeeded) { 42 | assert.isTrue(isUpdateNeeded); 43 | }); 44 | }); 45 | 46 | it('resolves to false if version stored and in the sw match', function () { 47 | wpOfflineContent.version = swVersion; 48 | wpOfflineContent.storage.getItem = 49 | sinon.stub().withArgs('version').returns(Promise.resolve(sameVersion)); 50 | 51 | return wpOfflineContent.needsUpdate() 52 | .then(function(isUpdateNeeded) { 53 | assert.isFalse(isUpdateNeeded); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('computeUpdateOrder()', function () { 59 | 60 | it('computes the sets for the different actions: to remove, to update and add new', 61 | function () { 62 | var updatedC = update(items.c); 63 | 64 | var expected = { 65 | remove: [items.a], 66 | update: [updatedC], 67 | addnew: [items.d] 68 | }; 69 | 70 | var oldSet = toMap([items.a, items.b, items.c]); 71 | var newSet = toMap([items.b, updatedC, items.d]); 72 | 73 | var result = wpOfflineContent.computeUpdateOrder(oldSet, newSet); 74 | assert.deepEqual(result.update, expected.update); 75 | assert.deepEqual(result, expected); 76 | }); 77 | 78 | it('computes that all need to be added new if the old set is empty', 79 | function () { 80 | var expected = { 81 | remove: [], 82 | update: [], 83 | addnew: [items.a, items.b, items.c, items.d] 84 | }; 85 | 86 | var oldSet = toMap([]); 87 | var newSet = toMap([items.a, items.b, items.c, items.d]); 88 | 89 | var result = wpOfflineContent.computeUpdateOrder(oldSet, newSet); 90 | assert.deepEqual(result, expected); 91 | }); 92 | 93 | }); 94 | 95 | describe('doOrder()', function () { 96 | 97 | var fakeCache; 98 | var networkResponse = { ok: true }; 99 | 100 | beforeEach(function() { 101 | fakeCache = { 102 | put: sinon.stub().returns(Promise.resolve()), 103 | delete: sinon.stub().returns(Promise.resolve()) 104 | }; 105 | sinon.stub(self, 'fetch').returns(Promise.resolve(networkResponse)); 106 | sinon.stub(wpOfflineContent, 'openCache').returns(Promise.resolve(fakeCache)); 107 | }); 108 | 109 | afterEach(function() { 110 | self.fetch.restore(); 111 | wpOfflineContent.openCache.restore(); 112 | }); 113 | 114 | it('update the cache, fetching from the network when needed', 115 | function() { 116 | var a = items.a; 117 | var b = items.b; 118 | var c = items.c; 119 | 120 | var order = { 121 | remove: [a], 122 | update: [b], 123 | addnew: [c] 124 | }; 125 | 126 | return wpOfflineContent.doOrder(order) 127 | .then(() => { 128 | var expectedDeletions = order.remove.concat(order.update); 129 | assert.equal(fakeCache.delete.callCount, expectedDeletions.length); 130 | expectedDeletions.forEach( 131 | item => assert.isTrue(fakeCache.delete.calledWith(item[0])) 132 | ); 133 | 134 | var expectedFetches = order.addnew.concat(order.update); 135 | assert.equal(self.fetch.callCount, expectedFetches.length); 136 | expectedFetches.forEach( 137 | item => assert.isTrue(self.fetch.calledWith(item[0])) 138 | ); 139 | 140 | var expectedPuts = expectedFetches; 141 | assert.equal(fakeCache.put.callCount, expectedPuts.length); 142 | expectedPuts.forEach( 143 | item => assert.isTrue(fakeCache.put.calledWith(item[0], networkResponse)) 144 | ); 145 | }); 146 | }); 147 | }); 148 | 149 | }); 150 | -------------------------------------------------------------------------------- /tests/test-sample.php: -------------------------------------------------------------------------------- 1 | assertTrue( true ); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /wp-offline-content/class-wp-offline-content-admin.php: -------------------------------------------------------------------------------- 1 | options = WP_Offline_Content_Options::get_options(); 24 | add_action('admin_menu', array($this, 'admin_menu')); 25 | add_action('admin_init', array($this, 'admin_init')); 26 | } 27 | 28 | public function admin_init() { 29 | $group = self::$options_group; 30 | register_setting($group, 'offline_network_timeout', array($this, 'sanitize_network_timeout')); 31 | register_setting($group, 'offline_debug_sw', array($this, 'sanitize_debug_sw')); 32 | register_setting($group, 'offline_precache', array($this, 'sanitize_precache')); 33 | 34 | add_settings_section( 35 | 'default', 36 | '', 37 | function () {}, 38 | self::$options_page_id 39 | ); 40 | 41 | add_settings_field( 42 | 'debug-sw', 43 | __('Debug service worker', 'offline-content'), 44 | array($this, 'debug_sw_input'), 45 | self::$options_page_id, 46 | 'default' 47 | ); 48 | 49 | add_settings_section( 50 | 'precache', 51 | __('Precache', 'offline-content'), 52 | array($this, 'print_precache_info'), 53 | self::$options_page_id 54 | ); 55 | 56 | add_settings_field( 57 | 'precache', 58 | __('Content', 'offline-content'), 59 | array($this, 'precache_input'), 60 | self::$options_page_id, 61 | 'precache' 62 | ); 63 | 64 | add_settings_section( 65 | 'serving-policy', 66 | __('Serving policy', 'offline-content'), 67 | array($this, 'print_serving_policy_info'), 68 | self::$options_page_id 69 | ); 70 | 71 | add_settings_field( 72 | 'network-timeout', 73 | __('Network timeout', 'offline-content'), 74 | array($this, 'network_timeout_input'), 75 | self::$options_page_id, 76 | 'serving-policy' 77 | ); 78 | } 79 | 80 | public function admin_menu() { 81 | add_options_page( 82 | __('Offline Content Options', 'offline-content'), __('Offline Content', 'offline-content'), 83 | 'manage_options', self::$options_page_id, array($this, 'create_admin_page') 84 | ); 85 | } 86 | 87 | public function create_admin_page() { 88 | include_once(plugin_dir_path(__FILE__) . 'lib/pages/admin.php'); 89 | } 90 | 91 | public function network_timeout_input() { 92 | $network_timeout = $this->options->get('offline_network_timeout') / 1000; 93 | ?> 94 | 97 | options->get('offline_debug_sw'); 102 | ?> 103 | 108 | options->get('offline_precache'); 113 | ?> 114 | 119 | options->get('offline_network_timeout'); 131 | } 132 | return $value; 133 | } 134 | 135 | public function sanitize_debug_sw($value) { 136 | return isset($value); 137 | } 138 | 139 | public function sanitize_precache($value) { 140 | $sanitized = array(); 141 | $sanitized['pages'] = isset($value['pages']); 142 | return $sanitized; 143 | } 144 | 145 | public function print_serving_policy_info() { 146 | ?> 147 |

148 | 153 |

154 | 160 | -------------------------------------------------------------------------------- /wp-offline-content/class-wp-offline-content-options.php: -------------------------------------------------------------------------------- 1 | 4000, 8 | 'offline_cache_name' => 'wpOfflineContent', 9 | 'offline_debug_sw' => false, 10 | 'offline_precache' => array('pages' => true) 11 | ); 12 | 13 | public static function get_options() { 14 | if(!self::$instance) { 15 | self::$instance = new self(); 16 | } 17 | return self::$instance; 18 | } 19 | 20 | private function __construct() { 21 | } 22 | 23 | public function set_defaults() { 24 | foreach (self::$DEFAULTS as $name => $value) { 25 | if (!get_option($name)) { 26 | add_option($name, $value); 27 | } 28 | } 29 | } 30 | 31 | public function remove_all() { 32 | foreach (self::$DEFAULTS as $name => $value) { 33 | delete_option($name); 34 | } 35 | } 36 | 37 | public function set($name, $value) { 38 | update_option($name, $value); 39 | return $this; 40 | } 41 | 42 | public function get($name) { 43 | return get_option($name); 44 | } 45 | } 46 | 47 | ?> -------------------------------------------------------------------------------- /wp-offline-content/class-wp-offline-content-plugin.php: -------------------------------------------------------------------------------- 1 | options = WP_Offline_Content_Options::get_options(); 20 | $this->set_urls(); 21 | $this->setup_sw(); 22 | register_activation_hook($plugin_main_file, array($this, 'activate')); 23 | register_deactivation_hook($plugin_main_file, array($this, 'deactivate')); 24 | } 25 | 26 | private function set_urls() { 27 | $this->sw_scope = home_url('/'); 28 | } 29 | 30 | private function setup_sw() { 31 | Mozilla\WP_SW_Manager::get_manager()->sw()->add_content(array($this, 'render_sw')); 32 | } 33 | 34 | public function activate() { 35 | $this->options->set_defaults(); 36 | } 37 | 38 | public static function deactivate() { 39 | } 40 | 41 | public function render_sw() { 42 | $sw_scope = $this->sw_scope; 43 | $this->render(plugin_dir_path(__FILE__) . 'lib/js/sw.js', array( 44 | '$debug' => boolval($this->options->get('offline_debug_sw')), 45 | '$networkTimeout' => intval($this->options->get('offline_network_timeout')), 46 | '$resources' => $this->get_precache_list(), 47 | '$excludedPaths' => $this->get_excluded_paths() 48 | )); 49 | } 50 | 51 | private function render($path, $replacements) { 52 | $contents = file_get_contents($path); 53 | $incremental_hash = hash_init('md5'); 54 | hash_update($incremental_hash, $contents); 55 | foreach ($replacements as $key => $replacement) { 56 | $value = json_encode($replacement); 57 | hash_update($incremental_hash, $value); 58 | $contents = str_replace($key, $value, $contents); 59 | } 60 | $version = json_encode(hash_final($incremental_hash)); 61 | $contents = str_replace('$version', $version, $contents); 62 | echo $contents; 63 | } 64 | 65 | private function get_precache_list() { 66 | $precache_options = $this->options->get('offline_precache'); 67 | $precache_list = array(); 68 | if ($precache_options['pages']) { 69 | foreach (get_pages() as $page) { 70 | $precache_list[get_page_link($page)] = $page->post_modified; 71 | } 72 | } 73 | return $precache_list; 74 | } 75 | 76 | private function get_excluded_paths() { 77 | return array(admin_url(), content_url(), includes_url()); 78 | } 79 | } 80 | 81 | ?> 82 | -------------------------------------------------------------------------------- /wp-offline-content/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "delapuente/wp-offline-content", 3 | "description": "A WordPress plugin for offlining content.", 4 | "authors": [ 5 | { 6 | "name": "Salvador de la Puente González", 7 | "email": "salva@mozilla.com" 8 | } 9 | ], 10 | "repositories": [ 11 | { 12 | "type": "vcs", 13 | "url": "https://github.com/mozilla/wp-sw-manager" 14 | } 15 | ], 16 | "require": { 17 | "mozilla/wp-sw-manager": "dev-master" 18 | }, 19 | "minimum-stability": "dev" 20 | } 21 | -------------------------------------------------------------------------------- /wp-offline-content/composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "02661cf27e89141b0be6735860b8b1db", 8 | "content-hash": "ea03102b0d669f94342c7fe31b99f4c4", 9 | "packages": [ 10 | { 11 | "name": "marco-c/wp_serve_file", 12 | "version": "dev-master", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/marco-c/WP_Serve_File.git", 16 | "reference": "4b1c0d812375e609d31342118cb886e12a235b9f" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/marco-c/WP_Serve_File/zipball/4b1c0d812375e609d31342118cb886e12a235b9f", 21 | "reference": "4b1c0d812375e609d31342118cb886e12a235b9f", 22 | "shasum": "" 23 | }, 24 | "type": "library", 25 | "autoload": { 26 | "psr-4": { 27 | "Mozilla\\": "" 28 | } 29 | }, 30 | "notification-url": "https://packagist.org/downloads/", 31 | "license": [ 32 | "GPLv2" 33 | ], 34 | "authors": [ 35 | { 36 | "name": "Marco Castelluccio", 37 | "email": "mcastelluccio@mozilla.com" 38 | } 39 | ], 40 | "description": "Class to serve dynamic files in WordPress with performance in mind.", 41 | "time": "2016-04-04 19:09:54" 42 | }, 43 | { 44 | "name": "mozilla/wp-sw-manager", 45 | "version": "dev-master", 46 | "source": { 47 | "type": "git", 48 | "url": "https://github.com/mozilla/wp-sw-manager.git", 49 | "reference": "e6618e47bc21cb1a4daea6aca92ddd5d9e8d9c64" 50 | }, 51 | "dist": { 52 | "type": "zip", 53 | "url": "https://api.github.com/repos/mozilla/wp-sw-manager/zipball/e6618e47bc21cb1a4daea6aca92ddd5d9e8d9c64", 54 | "reference": "e6618e47bc21cb1a4daea6aca92ddd5d9e8d9c64", 55 | "shasum": "" 56 | }, 57 | "require": { 58 | "marco-c/wp_serve_file": "dev-master" 59 | }, 60 | "type": "library", 61 | "autoload": { 62 | "psr-4": { 63 | "Mozilla\\": "" 64 | } 65 | }, 66 | "license": [ 67 | "GPLv2" 68 | ], 69 | "authors": [ 70 | { 71 | "name": "Salvador de la Puente", 72 | "email": "salva@mozilla.com" 73 | } 74 | ], 75 | "description": "Service Worker infrastructure for WordPress plugins.", 76 | "support": { 77 | "source": "https://github.com/mozilla/wp-sw-manager/tree/0.5.0", 78 | "issues": "https://github.com/mozilla/wp-sw-manager/issues" 79 | }, 80 | "time": "2016-04-15 17:39:45" 81 | } 82 | ], 83 | "packages-dev": [], 84 | "aliases": [], 85 | "minimum-stability": "dev", 86 | "stability-flags": { 87 | "mozilla/wp-sw-manager": 20 88 | }, 89 | "prefer-stable": false, 90 | "prefer-lowest": false, 91 | "platform": [], 92 | "platform-dev": [] 93 | } 94 | -------------------------------------------------------------------------------- /wp-offline-content/lang/offline-content.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2016 Offline Content 2 | # This file is distributed under the same license as the Offline Content package. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Offline Content 0.3.0\n" 6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/wp-offline-content\n" 7 | "POT-Creation-Date: 2016-02-25 16:01:49+00:00\n" 8 | "MIME-Version: 1.0\n" 9 | "Content-Type: text/plain; charset=UTF-8\n" 10 | "Content-Transfer-Encoding: 8bit\n" 11 | "PO-Revision-Date: 2016-MO-DA HO:MI+ZONE\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language-Team: LANGUAGE \n" 14 | 15 | #: class-wp-offline-content-admin.php:43 16 | msgid "Debug service worker" 17 | msgstr "" 18 | 19 | #: class-wp-offline-content-admin.php:51 20 | msgid "Precache" 21 | msgstr "" 22 | 23 | #: class-wp-offline-content-admin.php:58 24 | msgid "Content" 25 | msgstr "" 26 | 27 | #: class-wp-offline-content-admin.php:66 28 | msgid "Serving policy" 29 | msgstr "" 30 | 31 | #: class-wp-offline-content-admin.php:73 32 | msgid "Network timeout" 33 | msgstr "" 34 | 35 | #: class-wp-offline-content-admin.php:82 36 | msgid "Offline Content Options" 37 | msgstr "" 38 | 39 | #: class-wp-offline-content-admin.php:82 lib/pages/admin.php:2 40 | msgid "Offline Content" 41 | msgstr "" 42 | 43 | #: class-wp-offline-content-admin.php:96 44 | msgid "seconds before serving cached content" 45 | msgstr "" 46 | 47 | #: class-wp-offline-content-admin.php:106 48 | msgid "Enable debug traces from the service worker in the console." 49 | msgstr "" 50 | 51 | #: class-wp-offline-content-admin.php:117 52 | msgid "Precache published pages." 53 | msgstr "" 54 | 55 | #: class-wp-offline-content-admin.php:128 56 | msgid "Network timeout must be at least 1 second." 57 | msgstr "" 58 | 59 | #: class-wp-offline-content-admin.php:147 60 | msgid "Offline plugin prefers to serve fresh living content from the Internet but it will serve cached content in case network is not available or not reliable." 61 | msgstr "" 62 | 63 | #: class-wp-offline-content-admin.php:153 64 | msgid "Precache options allows you to customize which content will be available even if the user never visit it before." 65 | msgstr "" 66 | 67 | #: lib/pages/admin.php:6 68 | msgid "Save Changes" 69 | msgstr "" 70 | #. Plugin Name of the plugin/theme 71 | msgid "Offline Content" 72 | msgstr "" 73 | 74 | #. Plugin URI of the plugin/theme 75 | msgid "https://github.com/delapuente/wp-offline-content" 76 | msgstr "" 77 | 78 | #. Description of the plugin/theme 79 | msgid "Allow your users to read your content even while offline." 80 | msgstr "" 81 | 82 | #. Author of the plugin/theme 83 | msgid "Mozilla" 84 | msgstr "" 85 | 86 | #. Author URI of the plugin/theme 87 | msgid "https://www.mozilla.org/" 88 | msgstr "" 89 | -------------------------------------------------------------------------------- /wp-offline-content/lib/js/sw.js: -------------------------------------------------------------------------------- 1 | (function (self, localforage) { 2 | var PRIVATE_NAME = '__wp-offline-content'; 3 | 4 | var CACHE_PREFIX = PRIVATE_NAME + '::'; 5 | 6 | var wpOfflineContent = self.wpOfflineContent = { 7 | 8 | version: $version, 9 | 10 | storage: localforage.createInstance({ name: PRIVATE_NAME }), 11 | 12 | resources: $resources, 13 | 14 | excludedPaths: $excludedPaths, 15 | 16 | debug: $debug, 17 | 18 | cacheName: CACHE_PREFIX + 'v1', 19 | 20 | networkTimeout: $networkTimeout, 21 | 22 | log: function () { 23 | if (this.debug) { 24 | console.log.apply(console, arguments); 25 | } 26 | }, 27 | 28 | origin: self.location.origin, 29 | 30 | onInstall: function (event) { 31 | event.waitUntil(Promise.all([ 32 | self.skipWaiting(), 33 | wpOfflineContent.update() 34 | ])); 35 | }, 36 | 37 | onActivate: function (event) { 38 | event.waitUntil(Promise.all([self.clients.claim(), this.deleteOutdatedCaches(CACHE_PREFIX)])); 39 | }, 40 | 41 | onFetch: function (event) { 42 | var request = event.request; 43 | if (this.shouldBeHandled(request)) { 44 | event.respondWith(wpOfflineContent.get(request)); 45 | } 46 | }, 47 | 48 | shouldBeHandled: function (request) { 49 | return request.method === 'GET' && !this.isExcluded(request.url); 50 | }, 51 | 52 | update: function () { 53 | return this.needsUpdate().then(updateIsNeeded => { 54 | if (updateIsNeeded) { 55 | return this.storage.getItem('resources') 56 | .then(currents => this.computeUpdateOrder(currents || {}, this.resources)) 57 | .then(order => this.doOrder(order)) 58 | .then(() => this.storage.setItem('resources', this.resources)) 59 | .then(() => this.storage.setItem('version', this.version)); 60 | } 61 | return Promise.resolve(); 62 | }); 63 | }, 64 | 65 | needsUpdate: function () { 66 | return this.storage.getItem('version') 67 | .then(lastVersion => Promise.resolve(lastVersion !== this.version)); 68 | }, 69 | 70 | computeUpdateOrder: function (currentContent, newContent) { 71 | var order = { 72 | remove: [], 73 | update: [], 74 | addnew: [] 75 | }; 76 | var currentUrls = Object.keys(currentContent); 77 | currentUrls.forEach(url => { 78 | if (!(url in newContent)) { 79 | order.remove.push([url, currentContent[url]]); 80 | } 81 | else if (currentContent[url] !== newContent[url]) { 82 | order.update.push([url, newContent[url]]); 83 | } 84 | }); 85 | var newUrls = Object.keys(newContent); 86 | newUrls.forEach(newUrl => { 87 | if (!(newUrl in currentContent)) { 88 | order.addnew.push([newUrl, newContent[newUrl]]); 89 | } 90 | }); 91 | return order; 92 | }, 93 | 94 | doOrder: function(order) { 95 | return Promise.all([ 96 | this._deleteFromCache(order.remove), 97 | this._deleteFromCache(order.update).then(() => this._cacheFromNetwork(order.update)), 98 | this._cacheFromNetwork(order.addnew) 99 | ]); 100 | }, 101 | 102 | _deleteFromCache: function(deletions) { 103 | return this.openCache() 104 | .then(cache => Promise.all(deletions.map(deletion => { 105 | var url = deletion[0]; 106 | return cache.delete(url); 107 | }))); 108 | }, 109 | 110 | _cacheFromNetwork: function(resources) { 111 | return this.openCache() 112 | .then(cache => Promise.all(resources.map(resource => { 113 | var url = resource[0]; 114 | return self.fetch(url) 115 | .then(response => { 116 | if (response.ok) { 117 | return cache.put(url, response); 118 | } 119 | this.log('Error fetching', url); 120 | return Promise.resolve(); 121 | }); 122 | }))); 123 | }, 124 | 125 | deleteOutdatedCaches: function (prefix) { 126 | return self.caches.keys().then(names => { 127 | return Promise.all(names.map(cacheName => { 128 | if (cacheName.startsWith(prefix) && cacheName !== this.cacheName) { 129 | return self.caches.delete(cacheName); 130 | } 131 | return Promise.resolve(); 132 | })); 133 | }); 134 | }, 135 | 136 | get: function (request) { 137 | var url = request.url; 138 | this.log('Fetching', url); 139 | 140 | var fetchFromNetwork = fetch(request.clone()) 141 | .catch(error => { 142 | this.log('Failed to fetch', url); 143 | throw error; 144 | }); 145 | 146 | var fetchAndCache = fetchFromNetwork.then(responseFromNetwork => { 147 | if (responseFromNetwork && responseFromNetwork.ok) { 148 | this.log('Caching', responseFromNetwork.url); 149 | this.openCache() 150 | .then(cache => cache.put(request.clone(), responseFromNetwork.clone())); 151 | } 152 | }); 153 | 154 | var waitForNetwork = new Promise((fulfill, reject) => { 155 | var expired = false; 156 | 157 | var timeout = setTimeout(() => { 158 | this.log('Timeout for', url); 159 | expired = true; 160 | reject(); 161 | }, this.networkTimeout); 162 | 163 | fetchFromNetwork 164 | .then( 165 | responseFromNetwork => { 166 | if (!expired) { 167 | clearTimeout(timeout); 168 | if (!responseFromNetwork) { 169 | this.log('Undefined response for', url); 170 | reject('network-error'); 171 | } else { 172 | this.log('Success from network for', url); 173 | fulfill(responseFromNetwork.clone()); 174 | } 175 | } 176 | }, 177 | error => { 178 | if (!expired) { 179 | this.log('Network error for', url); 180 | clearTimeout(timeout); 181 | reject(error); 182 | } 183 | } 184 | ); 185 | }); 186 | 187 | var fetchFromCache = self.caches.match(request.clone()).catch(error => console.error(error)); 188 | 189 | return waitForNetwork 190 | .catch(() => fetchFromCache.then(responseFromCache => { 191 | if (!responseFromCache) { 192 | this.log('Cache miss for', url); 193 | return fetchFromNetwork; 194 | } 195 | this.log('Cache hit for', url); 196 | return responseFromCache; 197 | })); 198 | }, 199 | 200 | isExcluded: function (url) { 201 | return this.isAnotherOrigin(url) || 202 | this.excludedPaths.some(path => url.startsWith(path)); 203 | }, 204 | 205 | openCache: function () { 206 | if (!this._openCache) { 207 | this._openCache = self.caches.open(this.cacheName); 208 | } 209 | return this._openCache; 210 | }, 211 | 212 | isAnotherOrigin: function (url) { 213 | return !url.startsWith(this.origin); 214 | } 215 | }; 216 | 217 | self.addEventListener('install', wpOfflineContent.onInstall.bind(wpOfflineContent)); 218 | self.addEventListener('activate', wpOfflineContent.onActivate.bind(wpOfflineContent)); 219 | self.addEventListener('fetch', wpOfflineContent.onFetch.bind(wpOfflineContent)); 220 | 221 | })(self, localforage); 222 | -------------------------------------------------------------------------------- /wp-offline-content/lib/pages/admin.php: -------------------------------------------------------------------------------- 1 |
2 |

3 |
4 | 5 | 6 | 7 |
8 |
9 | -------------------------------------------------------------------------------- /wp-offline-content/readme.txt: -------------------------------------------------------------------------------- 1 | === Offline Content === 2 | Contributors: delapuente, mozillawebapps 3 | Tags: offline, serivce, workers, service workers, read later, read offline, precache 4 | Requires at least: 3.8 5 | Tested up to: 4.5 6 | Stable tag: 0.6.1 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Allow your users to read your content even while offline. 11 | 12 | == Description == 13 | This plugin uses new [ServiceWorker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) and [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to allow your users to access the contents of your site while they are offline or under unreliable network situations by caching part of your site. 14 | 15 | Once you've installed this plugin, anyone visiting your site in [browsers that support the Service Workers API](http://caniuse.com/#feat=serviceworkers) will be able of access your content even if they run out of network or experiment network instability. 16 | 17 | Configure the plugin in the "Settings > Offline content" section of your WordPress dashboard. 18 | 19 | == Installation == 20 | 1. Download and install the plugin from the WordPress.org plugin directory 21 | 2. Activate the plugin through the "Plugins" menu in WordPress Dashboard. 22 | 23 | Alternatively, 24 | 25 | 1. Clone or download the project repository 26 | 2. Copy `wp-offline-content` directory inside your WordPress installation plugins directory 27 | 3. Enable the plugin from the admin panel 28 | 29 | == Frequently Asked Questions == 30 | = What browsers support the W3C Service Workers API? = 31 | [Browser support for the W3C Service Worker API](http://caniuse.com/#feat=serviceworkers) currently exists in Firefox, Chrome, and Chrome for Android, with other likely to follow. 32 | 33 | = What is the default policy for caching content? = 34 | The plugin will try to always serve fresh content from the Internet. After visiting a post or a page, the content will be cached in the background. In case of an unreliable network or lack of connectivity, the plugin will serve the cached content. 35 | 36 | = Can I use the plugin in combination with other plugins using SW = 37 | Since version 0.2.0, you can use this plugin in combination with other using the [WordPress Service Worker Manager library](https://github.com/mozilla/wp-sw-manager/blob/master/README.md). 38 | 39 | = Can I configure which content is available for offline reading? = 40 | In a very limited way, yes. You can enable/disable if pages should be precached is such a way the will be availables by the user even if they were never visited before. 41 | 42 | More options will be available with new versions of the plugin. 43 | 44 | == Change Log == 45 | 46 | = 0.6.1 = 47 | Includes latest Service Worker Manager which fixes a problem unregistering the service worker when all plugins using it are disabled. 48 | 49 | = 0.6.0 = 50 | The Service Worker unregister itself when no plugin using service workers is enabled. 51 | Use WordPress AJAX infrastructure for dynamically generating the service worker file while reducing server footprint. 52 | Use [WP_Serve_File](http://github.com/marco-c/wp_serve_file) to efficiently generate the registrar and avoid unnecessary WordPress loads. 53 | Relying on composer's autoload to manage plugin dependencies. 54 | 55 | = 0.5.0 = 56 | Prevent undesired updates when used with other service worker supported plugins for WordPress. 57 | 58 | = 0.4.0 = 59 | New smart update algorithm minifies the number of background downloads when adding new content. 60 | 61 | = 0.3.0 = 62 | Cleaning old caches when changing the name of the cache where offline content is stored. 63 | 64 | = 0.2.0 = 65 | Now can be combined with other WP plugins using the [WordPress Service Worker Manager library](https://github.com/mozilla/wp-sw-manager/blob/master/README.md) such as [Web Push](https://wordpress.org/plugins/web-push/). 66 | 67 | = 0.1.0 = 68 | Initial release. 69 | -------------------------------------------------------------------------------- /wp-offline-content/uninstall.php: -------------------------------------------------------------------------------- 1 | remove_all(); 9 | ?> -------------------------------------------------------------------------------- /wp-offline-content/wp-offline-content.php: -------------------------------------------------------------------------------- 1 | 24 | --------------------------------------------------------------------------------