diff --git a/.distignore b/.distignore new file mode 100755 index 0000000000000000000000000000000000000000..7a767aab7b021a9d02ccfebd7846d8ce16f2f778 --- /dev/null +++ b/.distignore @@ -0,0 +1,36 @@ +# A set of files you probably don't want in your WordPress.org distribution +.distignore +.editorconfig +.git +.gitignore +.gitlab-ci.yml +.travis.yml +.wordpress-org/* +.DS_Store +Thumbs.db +behat.yml +bitbucket-pipelines.yml +bin +.circleci/config.yml +composer.json +composer.lock +Gruntfile.js +package.json +package-lock.json +phpunit.xml +phpunit.xml.dist +multisite.xml +multisite.xml.dist +.phpcs.xml +phpcs.xml +.phpcs.xml.dist +phpcs.xml.dist +README.md +wp-cli.local.yml +yarn.lock +tests +vendor +node_modules +*.sql +*.tar.gz +*.zip diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000000000000000000000000000000000000..0fcdf7fd4fc5705384ea40f93be21e0fc85ac557 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 + +[{.jshintrc,*.json,*.yml}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..e6bac7e940c35c73a2d95dd587c525f4dbfcdf4f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +# A set of files you probably don't want in your WordPress.org distribution +/.distignore export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.gitlab-ci.yml export-ignore +/.travis.yml export-ignore +/.DS_Store export-ignore +/.wordpress-org export-ignore +/Thumbs.db export-ignore +/behat.yml export-ignore +/bitbucket-pipelines.yml export-ignore +/bin export-ignore +/.circleci/config.yml export-ignore +/composer.json export-ignore +/composer.lock export-ignore +/Gruntfile.js export-ignore +/package.json export-ignore +/package-lock.json export-ignore +/phpunit.xml export-ignore +/phpunit.xml.dist export-ignore +/multisite.xml export-ignore +/multisite.xml.dist export-ignore +/.phpcs.xml export-ignore +/phpcs.xml export-ignore +/.phpcs.xml.dist export-ignore +/phpcs.xml.dist export-ignore +/README.md export-ignore +/wp-cli.local.yml export-ignore +/yarn.lock export-ignore +/tests export-ignore +/vendor export-ignore +/node_modules export-ignore +/*.sql export-ignore +/*.tar.gz export-ignore +/*.zip export-ignore diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000000000000000000000000000000000000..6f68bc7f65c25ceae8e856eb9ab25fa815acf0cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +phpcs.xml +phpunit.xml +Thumbs.db +wp-cli.local.yml +node_modules/ +*.sql +*.tar.gz +*.zip diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100755 index 0000000000000000000000000000000000000000..2e03fb89280b544ba0757996e33545cb5fe1dba9 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,88 @@ +variables: + # Configure mysql service (https://hub.docker.com/_/mysql/) + MYSQL_DATABASE: wordpress_tests + MYSQL_ROOT_PASSWORD: mysql + WP_VERSION: latest + +cache: + paths: + - $HOME/.composer + - /root/.composer + +before_script: + # Set up WordPress tests + - bash bin/install-wp-tests.sh $MYSQL_DATABASE root $MYSQL_ROOT_PASSWORD mysql $WP_VERSION true + + # PHPUnit + - | + if [[ $(php -v) =~ "PHP 7." ]]; then + composer global require "phpunit/phpunit=6.1.*" + else + composer global require "phpunit/phpunit=4.8.*" + fi + +PHPunit:PHP5.3:MySQL: + stage: test + variables: + WP_VERSION: '5.1' + image: containers.ethitter.com:443/docker/images/php:5.3 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP5.6:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:5.6 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.0:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.0 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.1:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.1 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.2:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.2 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPunit:PHP7.3:MySQL: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.3 + services: + - mysql:5.6 + script: + - find . -type "f" -iname "*.php" | xargs -L "1" php -l + - phpunit + +PHPCS: + stage: test + image: containers.ethitter.com:443/docker/images/php:7.3 + before_script: + - composer global require automattic/vipwpcs + - composer global require phpcompatibility/phpcompatibility-wp + - phpcs --config-set installed_paths $HOME/.composer/vendor/wp-coding-standards/wpcs,$HOME/.composer/vendor/automattic/vipwpcs,$HOME/.composer/vendor/phpcompatibility/php-compatibility,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-paragonie,$HOME/.composer/vendor/phpcompatibility/phpcompatibility-wp + script: + - phpcs -n diff --git a/.phpcs.xml.dist b/.phpcs.xml.dist new file mode 100644 index 0000000000000000000000000000000000000000..9a198a11bc5c5d73dac420379452204dc63d2582 --- /dev/null +++ b/.phpcs.xml.dist @@ -0,0 +1,49 @@ +<?xml version="1.0"?> +<ruleset name="WP Redis User Session Storage"> + <description>Generally-applicable sniffs for WordPress plugins.</description> + + <!-- What to scan --> + <file>.</file> + <exclude-pattern>/vendor/</exclude-pattern> + <exclude-pattern>/node_modules/</exclude-pattern> + + <!-- How to scan --> + <!-- Usage instructions: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Usage --> + <!-- Annotated ruleset: https://github.com/squizlabs/PHP_CodeSniffer/wiki/Annotated-ruleset.xml --> + <arg value="sp"/> <!-- Show sniff and progress --> + <arg name="basepath" value="./"/><!-- Strip the file paths down to the relevant bit --> + <arg name="colors"/> + <arg name="extensions" value="php"/> + <arg name="parallel" value="8"/><!-- Enables parallel processing when available for faster results. --> + + <!-- Rules: Check PHP version compatibility --> + <!-- https://github.com/PHPCompatibility/PHPCompatibility#sniffing-your-code-for-compatibility-with-specific-php-versions --> + <config name="testVersion" value="5.3-"/> + <!-- https://github.com/PHPCompatibility/PHPCompatibilityWP --> + <rule ref="PHPCompatibilityWP"/> + + <!-- Rules: WordPress Coding Standards --> + <!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards --> + <!-- https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties --> + <config name="minimum_supported_wp_version" value="4.0"/> + <rule ref="WordPress" /> + <rule ref="WordPressVIPMinimum" /> + <rule ref="WordPress-VIP-Go" /> + <rule ref="WordPress.NamingConventions.PrefixAllGlobals"> + <properties> + <!-- Value: replace the function, class, and variable prefixes used. Separate multiple prefixes with a comma. --> + <property name="prefixes" type="array" value="wp_redis_user_session_storage"/> + </properties> + </rule> + <rule ref="WordPress.WP.I18n"> + <properties> + <!-- Value: replace the text domain used. --> + <property name="text_domain" type="array" value="wp_redis_user_session_storage"/> + </properties> + </rule> + <rule ref="WordPress.WhiteSpace.ControlStructureSpacing"> + <properties> + <property name="blank_line_check" value="true"/> + </properties> + </rule> +</ruleset> diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100755 index 0000000000000000000000000000000000000000..7a1e8fa771d008e2f8c6c371e6c52da97e1eb1d2 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,56 @@ +module.exports = function( grunt ) { + + 'use strict'; + + // Project configuration + grunt.initConfig( { + + pkg: grunt.file.readJSON( 'package.json' ), + + addtextdomain: { + options: { + textdomain: 'wp_redis_user_session_storage', + }, + update_all_domains: { + options: { + updateDomains: true + }, + src: [ '*.php', '**/*.php', '!\.git/**/*', '!bin/**/*', '!node_modules/**/*', '!tests/**/*' ] + } + }, + + wp_readme_to_markdown: { + your_target: { + files: { + 'README.md': 'readme.txt' + } + }, + }, + + makepot: { + target: { + options: { + domainPath: '/languages', + exclude: [ '\.git/*', 'bin/*', 'node_modules/*', 'tests/*' ], + mainFile: 'wp-redis-user-session-storage.php', + potFilename: 'wp-redis-user-session-storage.pot', + potHeaders: { + poedit: true, + 'x-poedit-keywordslist': true + }, + type: 'wp-plugin', + updateTimestamp: true + } + } + }, + } ); + + grunt.loadNpmTasks( 'grunt-wp-i18n' ); + grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' ); + grunt.registerTask( 'default', [ 'i18n','readme' ] ); + grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] ); + grunt.registerTask( 'readme', ['wp_readme_to_markdown'] ); + + grunt.util.linefeed = '\n'; + +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6c5b18cf143be14531cdd46247bfbd4cfe08bf04 --- /dev/null +++ b/LICENSE @@ -0,0 +1,280 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b87a7c683ac92932222b9603859a3e535a3236eb --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# WP Redis User Session Storage # +**Contributors:** ethitter +**Donate link:** https://ethitter.com/donate/ +**Tags:** user sessions, session tokens, session storage +**Requires at least:** 4.0 +**Tested up to:** 5.2 +**Stable tag:** 0.1 +**License:** GPLv2 or later +**License URI:** http://www.gnu.org/licenses/gpl-2.0.html + +Store WordPress session tokens in Redis rather than the usermeta table. + +## Description ## + +Store WordPress user session tokens in Redis rather than the usermeta table. + +## Installation ## + +1. Install and configure Redis. There is a good tutorial [here](http://www.saltwebsites.com/2012/install-redis-245-service-centos-6). +2. Install the [Redis PECL module](http://pecl.php.net/package/redis). +3. Activate the plugin network-wide or by placing it in `mu-plugins`. +4. By default, the script will connect to Redis at 127.0.0.1:6379. See the *Connecting to Redis* section for further options. + +## Frequently Asked Questions ## + +### Connecting to Redis ### +By default, the plugin uses `127.0.0.1` and `6379` as the default host and port when creating a new client instance; the default database of `0` is also used. Three constants are provided to override these default values. + +Specify `WP_REDIS_USER_SESSION_HOST`, `WP_REDIS_USER_SESSION_PORT`, and `WP_REDIS_USER_SESSION_DB` to set the necessary, non-default connection values for your Redis instance. + +## Changelog ## + +### 0.1 ### +* Initial public release diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..5ceac4b84b4d05ad6b3525cc9d8b2fd055c32554 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +TMPDIR=${TMPDIR-/tmp} +TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//") +WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then + WP_BRANCH=${WP_VERSION%\-*} + WP_TESTS_TAG="branches/$WP_BRANCH" + +elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then + WP_TESTS_TAG="branches/$WP_VERSION" +elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + WP_TESTS_TAG="tags/${WP_VERSION%??}" + else + WP_TESTS_TAG="tags/$WP_VERSION" + fi +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p $TMPDIR/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip + unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/ + mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then + # https serves multiple offers, whereas http serves single. + download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json + if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then + # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x + LATEST_VERSION=${WP_VERSION%??} + else + # otherwise, scan the releases and get the most up to date minor version of the major release + local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'` + LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1) + fi + if [[ -z "$LATEST_VERSION" ]]; then + local ARCHIVE_NAME="wordpress-$WP_VERSION" + else + local ARCHIVE_NAME="wordpress-$LATEST_VERSION" + fi + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz + tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i.bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/inc/class-wp-redis-user-session-storage.php b/inc/class-wp-redis-user-session-storage.php new file mode 100644 index 0000000000000000000000000000000000000000..0be6ed061bd6ea4bc2229ad51b6b12d028def07d --- /dev/null +++ b/inc/class-wp-redis-user-session-storage.php @@ -0,0 +1,269 @@ +<?php +/** + * Offload session storage to Redis. + * + * @package WP_Redis_User_Session_Storage + */ + +/** + * Don't load in contexts that lack the WP_Session_Tokens class + */ +if ( ! class_exists( 'WP_Session_Tokens' ) ) { + return; +} + +/** + * Redis-based user sessions token manager. + * + * @since 0.1 + */ +class WP_Redis_User_Session_Storage extends WP_Session_Tokens { + /** + * Holds the Redis client. + * + * @var Redis + */ + private $redis; + + /** + * Track if Redis is available + * + * @var bool + */ + private $redis_connected = false; + + /** + * Prefix used to namespace keys + * + * @var string + */ + public $prefix = 'wpruss'; + + /** + * Create Redis connection using the Redis PECL extension + * + * @param int $user_id User ID. + */ + public function __construct( $user_id ) { + // General Redis settings. + $redis = array( + 'host' => '127.0.0.1', + 'port' => 6379, + 'socket' => null, + 'serializer' => Redis::SERIALIZER_PHP, + ); + + if ( defined( 'WP_REDIS_USER_SESSION_HOST' ) && WP_REDIS_USER_SESSION_HOST ) { + $redis['host'] = WP_REDIS_USER_SESSION_HOST; + } + if ( defined( 'WP_REDIS_USER_SESSION_PORT' ) && WP_REDIS_USER_SESSION_PORT ) { + $redis['port'] = WP_REDIS_USER_SESSION_PORT; + } + if ( defined( 'WP_REDIS_USER_SESSION_SOCKET' ) && WP_REDIS_USER_SESSION_SOCKET ) { + $redis['socket'] = WP_REDIS_USER_SESSION_SOCKET; + } + if ( defined( 'WP_REDIS_USER_SESSION_AUTH' ) && WP_REDIS_USER_SESSION_AUTH ) { + $redis['auth'] = WP_REDIS_USER_SESSION_AUTH; + } + if ( defined( 'WP_REDIS_USER_SESSION_DB' ) && WP_REDIS_USER_SESSION_DB ) { + $redis['database'] = WP_REDIS_USER_SESSION_DB; + } + if ( defined( 'WP_REDIS_USER_SESSION_SERIALIZER' ) && WP_REDIS_USER_SESSION_SERIALIZER ) { + $redis['serializer'] = WP_REDIS_USER_SESSION_SERIALIZER; + } + + // Use Redis PECL library. + try { + $this->redis = new Redis(); + + // Socket preferred, but TCP supported. + if ( $redis['socket'] ) { + $this->redis->connect( $redis['socket'] ); + } else { + $this->redis->connect( $redis['host'], $redis['port'] ); + } + + $this->redis->setOption( Redis::OPT_SERIALIZER, $redis['serializer'] ); + + if ( isset( $redis['auth'] ) ) { + $this->redis->auth( $redis['auth'] ); + } + + if ( isset( $redis['database'] ) ) { + $this->redis->select( $redis['database'] ); + } + + $this->redis_connected = true; + } catch ( RedisException $e ) { + $this->redis_connected = false; + } + + // Pass user ID to parent. + parent::__construct( $user_id ); + } + + /** + * Get all sessions of a user. + * + * @since 0.1 + * @access protected + * + * @return array Sessions of a user. + */ + protected function get_sessions() { + if ( ! $this->redis_connected ) { + return array(); + } + + $key = $this->get_key(); + + if ( ! $this->redis->exists( $key ) ) { + return array(); + } + + $sessions = $this->redis->get( $key ); + if ( ! is_array( $sessions ) ) { + return array(); + } + + $sessions = array_map( array( $this, 'prepare_session' ), $sessions ); + return array_filter( $sessions, array( $this, 'is_still_valid' ) ); + } + + /** + * Converts an expiration to an array of session information. + * + * @param mixed $session Session or expiration. + * @return array Session. + */ + protected function prepare_session( $session ) { + if ( is_int( $session ) ) { + return array( 'expiration' => $session ); + } + + return $session; + } + + /** + * Retrieve a session by its verifier (token hash). + * + * @since 0.1 + * @access protected + * + * @param string $verifier Verifier of the session to retrieve. + * @return array|null The session, or null if it does not exist + */ + protected function get_session( $verifier ) { + $sessions = $this->get_sessions(); + + if ( isset( $sessions[ $verifier ] ) ) { + return $sessions[ $verifier ]; + } + + return null; + } + + /** + * Update a session by its verifier. + * + * @since 0.1 + * @access protected + * + * @param string $verifier Verifier of the session to update. + * @param array $session Optional. Session. Omitting this argument destroys the session. + */ + protected function update_session( $verifier, $session = null ) { + $sessions = $this->get_sessions(); + + if ( $session ) { + $sessions[ $verifier ] = $session; + } else { + unset( $sessions[ $verifier ] ); + } + + $this->update_sessions( $sessions ); + } + + /** + * Update a user's sessions in Redis. + * + * @since 0.1 + * @access protected + * + * @param array $sessions Sessions. + */ + protected function update_sessions( $sessions ) { + if ( ! $this->redis_connected ) { + return; + } + + if ( ! has_filter( 'attach_session_information' ) ) { + $sessions = wp_list_pluck( $sessions, 'expiration' ); + } + + $key = $this->get_key(); + + if ( $sessions ) { + $this->redis->set( $key, $sessions ); + } elseif ( $this->redis->exists( $key ) ) { + $this->redis->del( $key ); + } + } + + /** + * Destroy all session tokens for a user, except a single session passed. + * + * @since 0.1 + * @access protected + * + * @param string $verifier Verifier of the session to keep. + */ + protected function destroy_other_sessions( $verifier ) { + $session = $this->get_session( $verifier ); + $this->update_sessions( array( $verifier => $session ) ); + } + + /** + * Destroy all session tokens for a user. + * + * @since 0.1 + * @access protected + */ + protected function destroy_all_sessions() { + $this->update_sessions( array() ); + } + + /** + * Destroy all session tokens for all users. + * + * @since 0.1 + * @access public + * @static + */ + public static function drop_sessions() { + return false; + } + + /** + * Build key for current user + * + * @since 0.1 + * @access protected + * + * @return string + */ + protected function get_key() { + return $this->prefix . ':' . $this->user_id; + } +} + +/** + * Override Core's default usermeta-based token storage + * + * @filter session_token_manager + * @return string + */ +function wp_redis_user_session_storage() { + return 'WP_Redis_User_Session_Storage'; +} +add_filter( 'session_token_manager', 'wp_redis_user_session_storage' ); diff --git a/languages/wp-redis-user-session-storage.pot b/languages/wp-redis-user-session-storage.pot new file mode 100644 index 0000000000000000000000000000000000000000..3660bda61fd28924f5686e249c6d32e8892525b8 --- /dev/null +++ b/languages/wp-redis-user-session-storage.pot @@ -0,0 +1,48 @@ +# Copyright (C) 2019 Erick Hitter +# This file is distributed under the same license as the WP Redis User Session Storage package. +msgid "" +msgstr "" +"Project-Id-Version: WP Redis User Session Storage 0.1\n" +"Report-Msgid-Bugs-To: " +"https://wordpress.org/support/plugin/wp-redis-user-session-storage\n" +"POT-Creation-Date: 2019-06-04 02:55:04+00:00\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2019-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"X-Generator: grunt-wp-i18n 0.5.4\n" +"X-Poedit-KeywordsList: " +"__;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;_nx_noop:1,2,3c;esc_" +"attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;esc_html_x:1,2c;\n" +"Language: en\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Country: United States\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-Basepath: ../\n" +"X-Poedit-SearchPath-0: .\n" +"X-Poedit-Bookmarks: \n" +"X-Textdomain-Support: yes\n" + +#. Plugin Name of the plugin/theme +msgid "WP Redis User Session Storage" +msgstr "" + +#. Plugin URI of the plugin/theme +msgid "https://ethitter.com/plugins/wp-redis-user-session-storage/" +msgstr "" + +#. Description of the plugin/theme +msgid "" +"Store WordPress session tokens in Redis rather than the usermeta table. " +"Requires the Redis PECL extension." +msgstr "" + +#. Author of the plugin/theme +msgid "Erick Hitter" +msgstr "" + +#. Author URI of the plugin/theme +msgid "https://ethitter.com/" +msgstr "" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..d1d0f68e40d4d2ccde27d1c82a5312fba5b4816a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,401 @@ +{ + "name": "wp-redis-user-session-storage", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "argparse": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-0.1.16.tgz", + "integrity": "sha1-z9AeD7uj1srtBJ+9dY1A9lGW9Xw=", + "dev": true, + "requires": { + "underscore": "~1.7.0", + "underscore.string": "~2.4.0" + }, + "dependencies": { + "underscore.string": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.4.0.tgz", + "integrity": "sha1-jN2PusTi0uoefi6Al8QvRCKA+Fs=", + "dev": true + } + } + }, + "async": { + "version": "0.1.22", + "resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz", + "integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE=", + "dev": true + }, + "coffee-script": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.3.3.tgz", + "integrity": "sha1-FQ1rTLUiiUNp7+1qIQHCC8f0pPQ=", + "dev": true + }, + "colors": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz", + "integrity": "sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=", + "dev": true + }, + "dateformat": { + "version": "1.0.2-1.2.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.2-1.2.3.tgz", + "integrity": "sha1-sCIMAt6YYXQztyhRz0fePfLNvuk=", + "dev": true + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "dev": true, + "requires": { + "iconv-lite": "~0.4.13" + }, + "dependencies": { + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + } + } + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", + "dev": true + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "findup-sync": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.1.3.tgz", + "integrity": "sha1-fz56l7gjksZTvwZYm9hRkOk8NoM=", + "dev": true, + "requires": { + "glob": "~3.2.9", + "lodash": "~2.4.1" + }, + "dependencies": { + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true, + "requires": { + "inherits": "2", + "minimatch": "0.3" + } + }, + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + } + } + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=", + "dev": true + }, + "gettext-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.1.2.tgz", + "integrity": "sha1-zw8MnJCJrtsO5RSZKRg+ncQ1hKc=", + "dev": true, + "requires": { + "encoding": "^0.1.11" + } + }, + "glob": { + "version": "3.1.21", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.1.21.tgz", + "integrity": "sha1-0p4KBV3qUTj00H7UDomC6DwgZs0=", + "dev": true, + "requires": { + "graceful-fs": "~1.2.0", + "inherits": "1", + "minimatch": "~0.2.11" + }, + "dependencies": { + "inherits": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-1.0.2.tgz", + "integrity": "sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=", + "dev": true + } + } + }, + "graceful-fs": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-1.2.3.tgz", + "integrity": "sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q=", + "dev": true + }, + "grunt": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-0.4.5.tgz", + "integrity": "sha1-VpN81RlDJK3/bSB2MYMqnWuk5/A=", + "dev": true, + "requires": { + "async": "~0.1.22", + "coffee-script": "~1.3.3", + "colors": "~0.6.2", + "dateformat": "1.0.2-1.2.3", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.1.2", + "getobject": "~0.1.0", + "glob": "~3.1.21", + "grunt-legacy-log": "~0.1.0", + "grunt-legacy-util": "~0.2.0", + "hooker": "~0.2.3", + "iconv-lite": "~0.2.11", + "js-yaml": "~2.0.5", + "lodash": "~0.9.2", + "minimatch": "~0.2.12", + "nopt": "~1.0.10", + "rimraf": "~2.2.8", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + } + }, + "grunt-legacy-log": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-0.1.3.tgz", + "integrity": "sha1-7ClCboAwIa9ZAp+H0vnNczWgVTE=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "grunt-legacy-log-utils": "~0.1.1", + "hooker": "~0.2.3", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-log-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-0.1.1.tgz", + "integrity": "sha1-wHBrndkGThFvNvI/5OawSGcsD34=", + "dev": true, + "requires": { + "colors": "~0.6.2", + "lodash": "~2.4.1", + "underscore.string": "~2.3.3" + }, + "dependencies": { + "lodash": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-2.4.2.tgz", + "integrity": "sha1-+t2DS5aDBz2hebPq5tnA0VBT9z4=", + "dev": true + }, + "underscore.string": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.3.3.tgz", + "integrity": "sha1-ccCL9rQosRM/N+ePo6Icgvcymw0=", + "dev": true + } + } + }, + "grunt-legacy-util": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-0.2.0.tgz", + "integrity": "sha1-kzJIhNv343qf98Am3/RR2UqeVUs=", + "dev": true, + "requires": { + "async": "~0.1.22", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~0.9.2", + "underscore.string": "~2.2.1", + "which": "~1.0.5" + } + }, + "grunt-wp-i18n": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/grunt-wp-i18n/-/grunt-wp-i18n-0.5.4.tgz", + "integrity": "sha1-hynlrU9LIxJpch8xcWVNLGKVVJI=", + "dev": true, + "requires": { + "async": "~0.9.0", + "gettext-parser": "~1.1.0", + "grunt": "~0.4.5", + "underscore": "~1.8.2", + "underscore.string": "~3.0.3" + }, + "dependencies": { + "async": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", + "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=", + "dev": true + }, + "underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=", + "dev": true + }, + "underscore.string": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.0.3.tgz", + "integrity": "sha1-Rhe4waJQz25QZPu7Nj0PqWzxRVI=", + "dev": true + } + } + }, + "grunt-wp-readme-to-markdown": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-wp-readme-to-markdown/-/grunt-wp-readme-to-markdown-1.0.0.tgz", + "integrity": "sha1-dJ/9gDtYTVC9ZOc6ehqRhz6djPs=", + "dev": true + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true + }, + "iconv-lite": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.2.11.tgz", + "integrity": "sha1-HOYKOleGSiktEyH/RgnKS7llrcg=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "js-yaml": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-2.0.5.tgz", + "integrity": "sha1-olrmUJmZ6X3yeMZxnaEb0Gh3Q6g=", + "dev": true, + "requires": { + "argparse": "~ 0.1.11", + "esprima": "~ 1.0.2" + } + }, + "lodash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-0.9.2.tgz", + "integrity": "sha1-jzSZxSRdNG1oLlsNO0B2fgnxqSw=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "minimatch": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz", + "integrity": "sha1-x054BXT2PG+aCQ6Q775u9TpqdWo=", + "dev": true, + "requires": { + "lru-cache": "2", + "sigmund": "~1.0.0" + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "rimraf": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz", + "integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "underscore": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true + }, + "underscore.string": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-2.2.1.tgz", + "integrity": "sha1-18D6KvXVoaZ/QlPa7pgTLnM/Dxk=", + "dev": true + }, + "which": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/which/-/which-1.0.9.tgz", + "integrity": "sha1-RgwdoPgQED0DIam2M6+eV15kSG8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000000000000000000000000000000000000..87e35547afb4412d4cff6575e311aa996e1d9d86 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "wp-redis-user-session-storage", + "version": "0.1.0", + "main": "Gruntfile.js", + "author": "Erick Hitter", + "devDependencies": { + "grunt": "~0.4.5", + "grunt-wp-i18n": "~0.5.0", + "grunt-wp-readme-to-markdown": "~1.0.0" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000000000000000000000000000000000000..16a39027e72be2cf0a2656056074b6e6ed818be1 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<phpunit + bootstrap="tests/bootstrap.php" + backupGlobals="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + > + <testsuites> + <testsuite> + <directory prefix="test-" suffix=".php">./tests/</directory> + <exclude>./tests/test-sample.php</exclude> + </testsuite> + </testsuites> +</phpunit> diff --git a/readme.md b/readme.txt similarity index 62% rename from readme.md rename to readme.txt index a26cdf6298819d100e65d007d1a5154420c1bca0..f01d7b2e1996850e3daf9bdc184aa36775dd3dc6 100644 --- a/readme.md +++ b/readme.txt @@ -1,21 +1,34 @@ -## Overview +=== WP Redis User Session Storage === +Contributors: ethitter +Donate link: https://ethitter.com/donate/ +Tags: user sessions, session tokens, session storage +Requires at least: 4.0 +Tested up to: 5.2 +Stable tag: 0.1 +License: GPLv2 or later +License URI: http://www.gnu.org/licenses/gpl-2.0.html -Store WordPress user session tokens in Redis rather than the usermeta table. Requires PECL Redis library. +Store WordPress session tokens in Redis rather than the usermeta table. -Requires WordPress 4.0, which should be released soon but isn’t currently suitable for production sites. In the meantime, **everything** is subject to change. +== Description == -## Authors +Store WordPress user session tokens in Redis rather than the usermeta table. -* Erick Hitter +== Installation == -## Installation 1. Install and configure Redis. There is a good tutorial [here](http://www.saltwebsites.com/2012/install-redis-245-service-centos-6). 2. Install the [Redis PECL module](http://pecl.php.net/package/redis). 3. Activate the plugin network-wide or by placing it in `mu-plugins`. 4. By default, the script will connect to Redis at 127.0.0.1:6379. See the *Connecting to Redis* section for further options. -### Connecting to Redis ### +== Frequently Asked Questions == += Connecting to Redis = By default, the plugin uses `127.0.0.1` and `6379` as the default host and port when creating a new client instance; the default database of `0` is also used. Three constants are provided to override these default values. Specify `WP_REDIS_USER_SESSION_HOST`, `WP_REDIS_USER_SESSION_PORT`, and `WP_REDIS_USER_SESSION_DB` to set the necessary, non-default connection values for your Redis instance. + +== Changelog == + += 0.1 = +* Initial public release diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100755 index 0000000000000000000000000000000000000000..e32bcc744ed5f44da6dc87b495c196a48f87a56b --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,43 @@ +<?php +/** + * PHPUnit bootstrap file + * + * @package WP_Revisions_Control + */ + +$wp_redis_user_session_storage = getenv( 'WP_TESTS_DIR' ); + +if ( ! $wp_redis_user_session_storage ) { + $wp_redis_user_session_storage = rtrim( sys_get_temp_dir(), '/\\' ) . '/wordpress-tests-lib'; +} + +if ( ! file_exists( $wp_redis_user_session_storage . '/includes/functions.php' ) ) { + echo "Could not find $wp_redis_user_session_storage/includes/functions.php, have you run bin/install-wp-tests.sh ?" . PHP_EOL; // WPCS: XSS ok. + exit( 1 ); +} + +// Give access to tests_add_filter() function. +require_once $wp_redis_user_session_storage . '/includes/functions.php'; + +/** + * Stub admin-only function not needed for testing. + */ +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound +if ( ! function_exists( 'post_revisions_meta_box' ) ) { + /** + * Stub for Core's revisions meta box. + */ + function post_revisions_meta_box() {} +} +// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound + +/** + * Manually load the plugin being tested. + */ +function wp_redis_user_session_storage_tests_manually_load_plugin() { + require dirname( dirname( __FILE__ ) ) . '/wp-redis-user-session-storage.php'; +} +tests_add_filter( 'muplugins_loaded', 'wp_redis_user_session_storage_tests_manually_load_plugin' ); + +// Start up the WP testing environment. +require $wp_redis_user_session_storage . '/includes/bootstrap.php'; diff --git a/tests/test-sample.php b/tests/test-sample.php new file mode 100755 index 0000000000000000000000000000000000000000..ccdf95bc181e52eca521bf967e65133f03095a4b --- /dev/null +++ b/tests/test-sample.php @@ -0,0 +1,20 @@ +<?php +/** + * Class SampleTest + * + * @package WP_Redis_User_Session_Storage + */ + +/** + * Sample test case. + */ +class SampleTest extends WP_UnitTestCase { + + /** + * A single example test. + */ + public function test_sample() { + // Replace this with some actual testing code. + $this->assertTrue( true ); + } +} diff --git a/wp-redis-user-session-storage.php b/wp-redis-user-session-storage.php index ee14ea2d8ca12269fa48cffd1bcd34fbb1a0ce9a..789c3300dcab5b8a806fa42467e74b624d7a6a98 100644 --- a/wp-redis-user-session-storage.php +++ b/wp-redis-user-session-storage.php @@ -1,284 +1,31 @@ <?php -/* -Plugin Name: WP Redis User Session Storage -Plugin URI: https://ethitter.com/plugins/wp-redis-user-session-storage/ -Description: Store WordPress session tokens in Redis rather than the usermeta table. Requires the Redis PECL extension. -Version: 0.1 -Author: Erick Hitter -Author URI: https://ethitter.com/ - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - /** - * Don't load in contexts that lack the WP_Session_Tokens class - */ -if ( ! class_exists( 'WP_Session_Tokens' ) ) { - return; -} - -/** - * Redis-based user sessions token manager. + * Load plugin. * - * @since 0.1 + * @package WP_Redis_User_Session_Storage */ -class WP_Redis_User_Session_Storage extends WP_Session_Tokens { - /** - * Holds the Redis client. - * - * @var - */ - private $redis; - - /** - * Track if Redis is available - * - * @var bool - */ - private $redis_connected = false; - - /** - * Prefix used to namespace keys - * - * @var string - */ - public $prefix = 'wpruss'; - - /** - * Create Redis connection using the Redis PECL extension - */ - public function __construct( $user_id ) { - // General Redis settings - $redis = array( - 'host' => '127.0.0.1', - 'port' => 6379, - 'socket' => null, - 'serializer' => Redis::SERIALIZER_PHP, - ); - - if ( defined( 'WP_REDIS_USER_SESSION_HOST' ) && WP_REDIS_USER_SESSION_HOST ) { - $redis['host'] = WP_REDIS_USER_SESSION_HOST; - } - if ( defined( 'WP_REDIS_USER_SESSION_PORT' ) && WP_REDIS_USER_SESSION_PORT ) { - $redis['port'] = WP_REDIS_USER_SESSION_PORT; - } - if ( defined( 'WP_REDIS_USER_SESSION_SOCKET' ) && WP_REDIS_USER_SESSION_SOCKET ) { - $redis['socket'] = WP_REDIS_USER_SESSION_SOCKET; - } - if ( defined( 'WP_REDIS_USER_SESSION_AUTH' ) && WP_REDIS_USER_SESSION_AUTH ) { - $redis['auth'] = WP_REDIS_USER_SESSION_AUTH; - } - if ( defined( 'WP_REDIS_USER_SESSION_DB' ) && WP_REDIS_USER_SESSION_DB ) { - $redis['database'] = WP_REDIS_USER_SESSION_DB; - } - if ( defined( 'WP_REDIS_USER_SESSION_SERIALIZER' ) && WP_REDIS_USER_SESSION_SERIALIZER ) { - $redis['serializer'] = WP_REDIS_USER_SESSION_SERIALIZER; - } - - // Use Redis PECL library. - try { - $this->redis = new Redis(); - - // Socket preferred, but TCP supported - if ( $redis['socket'] ) { - $this->redis->connect( $redis['socket'] ); - } else { - $this->redis->connect( $redis['host'], $redis['port'] ); - } - - $this->redis->setOption( Redis::OPT_SERIALIZER, $redis['serializer'] ); - - if ( isset( $redis['auth'] ) ) { - $this->redis->auth( $redis['auth'] ); - } - - if ( isset( $redis['database'] ) ) { - $this->redis->select( $redis['database'] ); - } - - $this->redis_connected = true; - } catch ( RedisException $e ) { - $this->redis_connected = false; - } - - // Ensure Core's session constructor fires - parent::__construct( $user_id ); - } - - /** - * Get all sessions of a user. - * - * @since 0.1 - * @access protected - * - * @return array Sessions of a user. - */ - protected function get_sessions() { - if ( ! $this->redis_connected ) { - return array(); - } - - $key = $this->get_key(); - - if ( ! $this->redis->exists( $key ) ) { - return array(); - } - - $sessions = $this->redis->get( $key ); - if ( ! is_array( $sessions ) ) { - return array(); - } - - $sessions = array_map( array( $this, 'prepare_session' ), $sessions ); - return array_filter( $sessions, array( $this, 'is_still_valid' ) ); - } - - /** - * Converts an expiration to an array of session information. - * - * @param mixed $session Session or expiration. - * @return array Session. - */ - protected function prepare_session( $session ) { - if ( is_int( $session ) ) { - return array( 'expiration' => $session ); - } - - return $session; - } - - /** - * Retrieve a session by its verifier (token hash). - * - * @since 0.1 - * @access protected - * - * @param string $verifier Verifier of the session to retrieve. - * @return array|null The session, or null if it does not exist - */ - protected function get_session( $verifier ) { - $sessions = $this->get_sessions(); - - if ( isset( $sessions[ $verifier ] ) ) { - return $sessions[ $verifier ]; - } - - return null; - } - - /** - * Update a session by its verifier. - * - * @since 0.1 - * @access protected - * - * @param string $verifier Verifier of the session to update. - * @param array $session Optional. Session. Omitting this argument destroys the session. - */ - protected function update_session( $verifier, $session = null ) { - $sessions = $this->get_sessions(); - - if ( $session ) { - $sessions[ $verifier ] = $session; - } else { - unset( $sessions[ $verifier ] ); - } - - $this->update_sessions( $sessions ); - } - - /** - * Update a user's sessions in Redis. - * - * @since 0.1 - * @access protected - * - * @param array $sessions Sessions. - */ - protected function update_sessions( $sessions ) { - if ( ! $this->redis_connected ) { - return; - } - - if ( ! has_filter( 'attach_session_information' ) ) { - $sessions = wp_list_pluck( $sessions, 'expiration' ); - } - - $key = $this->get_key(); - - if ( $sessions ) { - $this->redis->set( $key, $sessions ); - } elseif ( $this->redis->exists( $key ) ) { - $this->redis->del( $key ); - } - } - - /** - * Destroy all session tokens for a user, except a single session passed. - * - * @since 0.1 - * @access protected - * - * @param string $verifier Verifier of the session to keep. - */ - protected function destroy_other_sessions( $verifier ) { - $session = $this->get_session( $verifier ); - $this->update_sessions( array( $verifier => $session ) ); - } - - /** - * Destroy all session tokens for a user. - * - * @since 0.1 - * @access protected - */ - protected function destroy_all_sessions() { - $this->update_sessions( array() ); - } - - /** - * Destroy all session tokens for all users. - * - * @since 0.1 - * @access public - * @static - */ - public static function drop_sessions() { - return false; - } - - /** - * Build key for current user - * - * @since 0.1 - * @access protected - * - * @return string - */ - protected function get_key() { - return $this->prefix . ':' . $this->user_id; - } -} /** - * Override Core's default usermeta-based token storage + * Plugin Name: WP Redis User Session Storage + * Plugin URI: https://ethitter.com/plugins/wp-redis-user-session-storage/ + * Description: Store WordPress session tokens in Redis rather than the usermeta table. Requires the Redis PECL extension. + * Version: 0.1 + * Author: Erick Hitter + * Author URI: https://ethitter.com/ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. * - * @filter session_token_manager - * @return string + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ -function wp_redis_user_session_storage() { - return 'WP_Redis_User_Session_Storage'; -} -add_filter( 'session_token_manager', 'wp_redis_user_session_storage' ); + +require_once __DIR__ . '/inc/class-wp-redis-user-session-storage.php';