9 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | ./tests/
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | php:
3 | - 5.3
4 | - 5.6
5 | addons:
6 | firefox: 'latest'
7 | apt:
8 | sources:
9 | - google-chrome
10 | packages:
11 | - google-chrome-stable
12 | env:
13 | global:
14 | - FIREFOX_NIGHTLY_BIN=firefox
15 | - CHROME_BIN=google-chrome-stable
16 | matrix:
17 | - WP_VERSION=latest WP_MULTISITE=0
18 | - WP_VERSION=4.4.1 WP_MULTISITE=0
19 | matrix:
20 | include:
21 | - php: 5.3
22 | env: WP_VERSION=latest WP_MULTISITE=1
23 | before_script:
24 | - ./bin/install-wp-tests.sh wordpress_test root '' localhost $WP_VERSION
25 | - "composer install --working-dir=wp-offline-content"
26 | - "export DISPLAY=:99.0"
27 | - "sh -e /etc/init.d/xvfb start"
28 | - sleep 3
29 | script:
30 | - make test
31 | - make test-sw
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: reinstall test
2 |
3 | WP_CLI = tools/wp-cli.phar
4 | PHPUNIT = tools/phpunit.phar
5 |
6 | reinstall: $(WP_CLI)
7 | $(WP_CLI) plugin uninstall --deactivate wp-offline-content --path=$(WORDPRESS_PATH)
8 | rm -f wp-offline-content.zip
9 | zip wp-offline-content.zip -r wp-offline-content/
10 | $(WP_CLI) plugin install --activate wp-offline-content.zip --path=$(WORDPRESS_PATH)
11 |
12 | test: $(PHPUNIT)
13 | $(PHPUNIT)
14 |
15 | test-sw: node_modules
16 | $(NODE) node_modules/karma/bin/karma start karma.conf
17 |
18 | node_modules:
19 | npm install
20 |
21 | tools/wp-cli.phar:
22 | mkdir -p tools
23 | wget -P tools -N https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
24 | chmod +x $(WP_CLI)
25 |
26 | tools/phpunit.phar:
27 | mkdir -p tools
28 | wget -P tools -N https://phar.phpunit.de/phpunit-old.phar
29 | mv tools/phpunit-old.phar tools/phpunit.phar
30 | chmod +x $(PHPUNIT)
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wp-offline-content",
3 | "version": "0.5.0",
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 |
--------------------------------------------------------------------------------
/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 | ?>
--------------------------------------------------------------------------------
/tests/service-worker/testDeleteOutdatedCaches.js:
--------------------------------------------------------------------------------
1 | describe('deleteOutdatedCaches()', function() {
2 | 'use strict';
3 |
4 | beforeEach(function() {
5 | importScripts('/base/wp-offline-content/lib/js/sw.js');
6 | sinon.stub(self.caches, 'delete').returns(Promise.resolve());
7 | });
8 |
9 | afterEach(function() {
10 | self.caches.keys.restore();
11 | self.caches.delete.restore();
12 | });
13 |
14 | var prefix = 'testprefix::';
15 |
16 | [
17 | [],
18 | [prefix + 'old1'],
19 | [prefix + 'old1', prefix + 'old2']
20 | ].forEach(outdatedSet => {
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wp-offline-content
2 | > A WordPress plugin for offlining content.
3 |
4 | [](https://travis-ci.org/mozilla/wp-offline-content) [](https://wordpress.org/plugins/offline-content/) [](https://wordpress.org/plugins/offline-content/)
5 |
6 | ## Install the plugin
7 |
8 | 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.
9 |
10 | In case you want to do it manually, here are the instructions:
11 |
12 | First, clone the repository.
13 |
14 | Now, at the root of the repository, run (you need [composer](https://getcomposer.org) for this):
15 |
16 | ```
17 | $ composer update --working-dir=wp-offline-content
18 | ```
19 |
20 | And copy (or symlink) the folder `wp-offline-content` inside your WordPress `plugins` directory.
21 |
22 | Once installed, activate the plugin from the _Plugins_ menu in the _Dashboard_. Options are available to customize under the _Offline content_ submenu in _Settings_.
23 |
24 | ## Running tests
25 |
26 | Install dependencies:
27 | ```bash
28 | ./bin/install-wp-tests.sh MYSQL_DATABASE_NAME MYSQL_USER MYSQL_PASSWORD localhost latest
29 | ```
30 |
31 | Run tests:
32 | ```bash
33 | make test
34 | ```
35 |
36 | Run service worker tests:
37 | ```bash
38 | make test-sw
39 | ```
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/class-wp-offline-content-plugin.php:
--------------------------------------------------------------------------------
1 | options = WP_Offline_Content_Options::get_options();
21 | $this->set_urls();
22 | $this->setup_sw();
23 | register_activation_hook($plugin_main_file, array($this, 'activate'));
24 | register_deactivation_hook($plugin_main_file, array($this, 'deactivate'));
25 | }
26 |
27 | private function set_urls() {
28 | $this->sw_scope = home_url('/');
29 | }
30 |
31 | private function setup_sw() {
32 | WP_SW_Manager::get_manager()->sw()->add_content(array($this, 'render_sw'));
33 | }
34 |
35 | public function activate() {
36 | $this->options->set_defaults();
37 | }
38 |
39 | public static function deactivate() {
40 | }
41 |
42 | public function render_sw() {
43 | $sw_scope = $this->sw_scope;
44 | $this->render(plugin_dir_path(__FILE__) . 'lib/js/sw.js', array(
45 | '$debug' => boolval($this->options->get('offline_debug_sw')),
46 | '$networkTimeout' => intval($this->options->get('offline_network_timeout')),
47 | '$resources' => $this->get_precache_list(),
48 | '$excludedPaths' => $this->get_excluded_paths()
49 | ));
50 | }
51 |
52 | private function render($path, $replacements) {
53 | $contents = file_get_contents($path);
54 | $incremental_hash = hash_init('md5');
55 | hash_update($incremental_hash, $contents);
56 | foreach ($replacements as $key => $replacement) {
57 | $value = json_encode($replacement);
58 | hash_update($incremental_hash, $value);
59 | $contents = str_replace($key, $value, $contents);
60 | }
61 | $version = json_encode(hash_final($incremental_hash));
62 | $contents = str_replace('$version', $version, $contents);
63 | echo $contents;
64 | }
65 |
66 | private function get_precache_list() {
67 | $precache_options = $this->options->get('offline_precache');
68 | $precache_list = array();
69 | if ($precache_options['pages']) {
70 | foreach (get_pages() as $page) {
71 | $precache_list[get_page_link($page)] = $page->post_modified;
72 | }
73 | }
74 | return $precache_list;
75 | }
76 |
77 | private function get_excluded_paths() {
78 | return array(admin_url(), content_url(), includes_url());
79 | }
80 | }
81 |
82 | ?>
--------------------------------------------------------------------------------
/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.7
5 | Tested up to: 4.4.1
6 | Stable tag: 0.5.0
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.5.0 =
47 | Prevent undesired updates when used with other service worker supported plugins for WordPress.
48 |
49 | = 0.4.0 =
50 | New smart update algorithm minifies the number of background downloads when adding new content.
51 |
52 | = 0.3.0 =
53 | Cleaning old caches when changing the name of the cache where offline content is stored.
54 |
55 | = 0.2.0 =
56 | 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/).
57 |
58 | = 0.1.0 =
59 | Initial release.
60 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------