jenkins-bot has submitted this change and it was merged. Change subject: Initial checkin ......................................................................
Initial checkin Change-Id: I9c083d47bb5c1a9e06223634f816a4ffb89036b9 --- A .editorconfig A .gitattributes A .gitignore A .gitreview A .travis.yml A COPYING A Doxyfile A README.md A composer.json A formats.md A phpcs.xml A phpunit.xml.dist A src/Wikimedia/PhpSessionSerializer.php A tests/Wikimedia/PhpSessionSerializerTest.php 14 files changed, 1,622 insertions(+), 0 deletions(-) Approvals: BryanDavis: Looks good to me, approved Legoktm: Looks good to me, approved Gergő Tisza: Looks good to me, but someone else must approve jenkins-bot: Verified diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..42aefb6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +# http://editorconfig.org +root = true + +[*] +indent_style = tab + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b6fe658 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.gitreview export-ignore +.travis.yml export-ignore +Doxyfile export-ignore +composer.json export-ignore +phpcs.xml export-ignore +phpunit.xml.dist export-ignore +tests/ export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..374daeb --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/coverage +/doc +/vendor +/composer.lock + +# Editors +*.kate-swp +*~ +\#*# +.#* +.*.swp +*.orig diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..507dea8 --- /dev/null +++ b/.gitreview @@ -0,0 +1,6 @@ +[gerrit] +host=gerrit.wikimedia.org +port=29418 +project=php-session-serializer.git +defaultbranch=master +defaultrebase=0 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c25a516 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +sudo: false +language: php +php: + - "5.3.3" + - "5.3" + - "5.4" + - "5.5" + - "5.6" + - "hhvm" +install: + - composer install +script: + - composer test diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 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 + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..df428b0 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,26 @@ +# Doxyfile for php-session-serializer +# +# See <http://www.stack.nl/~dimitri/doxygen/manual/config.html> +# for help on how to use this file to configure Doxygen. + +PROJECT_NAME = "php-session-serializer" +PROJECT_BRIEF = "Provides methods like PHP's session_encode and session_decode that don't mess with $_SESSION" +OUTPUT_DIRECTORY = doc +JAVADOC_AUTOBRIEF = YES +QT_AUTOBRIEF = YES +WARN_NO_PARAMDOC = YES +INPUT = README.md src/ +FILE_PATTERNS = *.php +RECURSIVE = YES +USE_MDFILE_AS_MAINPAGE = README.md +HTML_DYNAMIC_SECTIONS = YES +GENERATE_TREEVIEW = YES +TREEVIEW_WIDTH = 250 +GENERATE_LATEX = NO +HAVE_DOT = YES +DOT_FONTNAME = Helvetica +DOT_FONTSIZE = 10 +TEMPLATE_RELATIONS = YES +CALL_GRAPH = NO +CALLER_GRAPH = NO +DOT_MULTI_TARGETS = YES diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a7f69e --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +[![Latest Stable Version]](https://packagist.org/packages/wikimedia/php-session-serializer) [![License]](https://packagist.org/packages/wikimedia/php-session-serializer) + +php-session-serializer +====================== + +php-session-serializer is a PHP library that provides methods that work like +PHP's [`session_encode`][phpencode] and [`session_decode`][phpdecode] +functions, but don't mess with the `$_SESSION` superglobal. + +It supports the `php`, `php_binary`, and `php_serialize` serialize handlers. +`wddx` is not supported, since it is inferior to `php` and `php_binary`. + + +Usage +----- + + // (optional) Send logs to a PSR-3 logger + \Wikimedia\PhpSesssionSerializer::setLogger( $logger ) + + // (optional) Ensure that session.serialize_handler is set to a usable value + \Wikimedia\PhpSesssionSerializer::setSerializeHandler(); + + // Encode session data + $string = \Wikimedia\PhpSesssionSerializer::encode( $array ); + + // Decode session data + $array = \Wikimedia\PhpSesssionSerializer::decode( $string ); + + +Running tests +------------- + + composer install --prefer-dist + composer test + + +History +------- + +This library was created to support custom session handler [read][] and +[write][] methods that are more useful than blindly storing the serialized data +that PHP gives to custom handlers. + + +--- +[phpencode]: https://php.net/manual/en/function.session-encode.php +[phpdecode]: https://php.net/manual/en/function.session-decode.php +[read]: https://php.net/manual/en/sessionhandlerinterface.read.php +[write]: https://php.net/manual/en/sessionhandlerinterface.write.php +[Latest Stable Version]: https://poser.pugx.org/wikimedia/php-session-serializer/v/stable.svg +[License]: https://poser.pugx.org/wikimedia/php-session-serializer/license.svg diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c7299cf --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "mediawiki/php-session-serializer", + "description": "Provides methods like PHP's session_encode and session_decode that don't mess with $_SESSION", + "license": "GPL-2.0+", + "homepage": "https://www.mediawiki.org/wiki/Php-session-serializer", + "authors": [ + { + "name": "Brad Jorsch", + "email": "[email protected]" + } + ], + "autoload": { + "classmap": ["src/"] + }, + "require": { + "php": ">=5.3.3", + "mediawiki/at-ease": "^1.0", + "psr/log": "1.0.0" + }, + "require-dev": { + "jakub-onderka/php-parallel-lint": "^0.9.0.0", + "mediawiki/mediawiki-codesniffer": "^0.4.0.0", + "phpunit/phpunit": "~4.5" + }, + "scripts": { + "test": [ + "parallel-lint . --exclude vendor", + "phpunit $PHPUNIT_ARGS", + "phpcs -p" + ] + } +} diff --git a/formats.md b/formats.md new file mode 100644 index 0000000..365338c --- /dev/null +++ b/formats.md @@ -0,0 +1,82 @@ +PHP session serialization formats +================================= + +These are the currently available formats in basic PHP. + +In the older formats, you can see the legacy of PHP's old "register random +global variables for the session" mechanism in the fact that they have the +ability to indicate unset-but-present as a distinct value. + + +php +--- + +This is PHP's default format. Set values are encoded as +`{$key}|{$serializedValue}`, while unset values are `{$key}!` and placed at the +end of the string. An attempt to insert unset-but-present in the middle of the +string will append it to the next key (since 5.2.6 or earlier, anyway). + +For example, `foo|i:1;baz|s:6:"string";bar!` encodes session data with +'foo' = 1, 'baz' = "string", and 'bar' is present but unset. + +Due to these delimiters, keys may not contain pipe (`|`) or bang (`!`) characters. +Attempting to serialize a `$_SESSION` array containing these characters will +fail. + +Keys also may not be numeric. Any numeric entries in `$_SESSION` will be ignored. + +There is no value length or delimiter. + + +php_binary +---------- + +Keys are encoded as a byte length (up to 127) followed by the indicated number +of bytes. If the variable is unset, the high bit of the length is set. +Otherwise, the serialized value follows immediately after the end of the key. + +Depending on the version of PHP, an unset-but-present might ignore the key, +might set the key to null, or might stop processing the rest of the string +entirely. This library takes the first option. + +For example, `\x03fooi:1;\x03bazs:6:"string";\x83bar` encodes session data with +'foo' = 1, 'baz' = "string", and 'bar' is present but unset. + +Due to the size of the length field, keys may not be more than 127 bytes long. +Entries in `$_SESSION` with longer keys are ignored. + +Keys also may not be numeric. Any numeric entries in `$_SESSION` will be ignored. + +There is no value length or delimiter. + + +php_serialize +------------- + +This is the most reliable format, added in PHP 5.5.4. The format is just +`$_SESSION` passed to the standard [`serialize()`][serialize] function. It does +not have the ability to indicate unset-but-present. + +For example, `a:2:{s:3:"foo";i:1;s:3:"baz";s:6:"string";}` encodes session data +with 'foo' = 1 and 'baz' = "string". + +Unlike other formats, numeric keys are allowed and are stored correctly. + +When decoding the session, PHP does not check that the encoded string encodes +an array, and will happily set `$_SESSION` to other types. It will refuse to +re-serialize such a `$_SESSION`, however. + + +wddx +---- + +When WDDX support is compiled into PHP, the WDDX format may be used to store +session data. This format, however, cannot represent the full range of PHP data +types (e.g. `INF`, `NAN`, data structures containing references) and so is not +likely to be particularly useful unless you're trying to share saved session +data with code in some other language that has WDDX support. + + + +--- +[serialize]: https://php.net/manual/en/function.serialize.php diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..06c43bc --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<ruleset> + <rule ref="vendor/mediawiki/mediawiki-codesniffer/MediaWiki"/> + <file>.</file> + <arg name="encoding" value="UTF-8"/> + <arg name="extensions" value="php"/> + <exclude-pattern>coverage</exclude-pattern> + <exclude-pattern>vendor</exclude-pattern> + <exclude-pattern>doc/html</exclude-pattern> +</ruleset> diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f0f5428 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,14 @@ +<phpunit colors="true" + beStrictAboutTestsThatDoNotTestAnything="true" + beStrictAboutOutputDuringTests="true"> + <testsuites> + <testsuite name="php-session-serializer Tests"> + <directory>./tests</directory> + </testsuite> + </testsuites> + <filter> + <whitelist addUncoveredFilesFromWhitelist="true"> + <directory suffix=".php">./src</directory> + </whitelist> + </filter> +</phpunit> diff --git a/src/Wikimedia/PhpSessionSerializer.php b/src/Wikimedia/PhpSessionSerializer.php new file mode 100644 index 0000000..54d1b34 --- /dev/null +++ b/src/Wikimedia/PhpSessionSerializer.php @@ -0,0 +1,394 @@ +<?php +/** + * Wikimedia\PhpSessionSerializer + * + * Copyright (C) 2015 Brad Jorsch <[email protected]> + * + * 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 Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Brad Jorsch <[email protected]> + */ + +namespace Wikimedia; + +use Psr\Log\LoggerInterface; + +/** + * Provides for encoding and decoding session arrays to PHP's serialization + * formats. + * + * Supported formats are: + * - php + * - php_binary + * - php_serialize + * + * WDDX is not supported, since it breaks on all sorts of things. + */ +class PhpSessionSerializer { + /** @var LoggerInterface */ + protected static $logger; + + /** + * Set the logger to which to log + */ + public static function setLogger( LoggerInterface $logger ) { + self::$logger = $logger; + } + + /** + * Try to set session.serialize_handler to a supported format + * + * This may change the format even if the current format is also supported. + * + * @return string Format set + * @throws \\DomainException + */ + public static function setSerializeHandler() { + $formats = array( + 'php_serialize', + 'php', + 'php_binary', + ); + + // First, try php_serialize since that's the only one that doesn't suck in some way. + \MediaWiki\suppressWarnings(); + ini_set( 'session.serialize_handler', 'php_serialize' ); + \MediaWiki\restoreWarnings(); + if ( ini_get( 'session.serialize_handler' ) === 'php_serialize' ) { + return 'php_serialize'; + } + + // Next, just use the current format if it's supported. + $format = ini_get( 'session.serialize_handler' ); + if ( in_array( $format, $formats, true ) ) { + return $format; + } + + // Last chance, see if any of our supported formats are accepted. + foreach ( $formats as $format ) { + \MediaWiki\suppressWarnings(); + ini_set( 'session.serialize_handler', $format ); + \MediaWiki\restoreWarnings(); + if ( ini_get( 'session.serialize_handler' ) === $format ) { + return $format; + } + } + + throw new \DomainException( + 'Failed to set serialize handler to a supported format.' . + ' Supported formats are: ' . join( ', ', $formats ) . '.' + ); + } + + /** + * Encode a session array to a string, using the format in session.serialize_handler + * @param array $data Session data + * @return string|null Encoded string, or null on failure + * @throws \\DomainException + */ + public static function encode( array $data ) { + $format = ini_get( 'session.serialize_handler' ); + if ( !is_string( $format ) ) { + throw new \UnexpectedValueException( + 'Could not fetch the value of session.serialize_handler' + ); + } + switch ( $format ) { + case 'php': + return self::encodePhp( $data ); + + case 'php_binary': + return self::encodePhpBinary( $data ); + + case 'php_serialize': + return self::encodePhpSerialize( $data ); + + default: + throw new \DomainException( "Unsupported format \"$format\"" ); + } + } + + /** + * Decode a session string to an array, using the format in session.serialize_handler + * @param string $data Session data + * @return array|null Data, or null on failure + * @throws \\DomainException + * @throws \\InvalidArgumentException + */ + public static function decode( $data ) { + if ( !is_string( $data ) ) { + throw new \InvalidArgumentException( '$data must be a string' ); + } + + $format = ini_get( 'session.serialize_handler' ); + if ( !is_string( $format ) ) { + throw new \UnexpectedValueException( + 'Could not fetch the value of session.serialize_handler' + ); + } + switch ( $format ) { + case 'php': + return self::decodePhp( $data ); + + case 'php_binary': + return self::decodePhpBinary( $data ); + + case 'php_serialize': + return self::decodePhpSerialize( $data ); + + default: + throw new \DomainException( "Unsupported format \"$format\"" ); + } + } + + /** + * Serialize a value with error logging + * @param mixed $value + * @return string|null + */ + private function serializeValue( $value ) { + try { + return serialize( $value ); + } catch ( \Exception $ex ) { + self::$logger->error( 'Value serialization failed: ' . $ex->getMessage() ); + return null; + } + } + + /** + * Unserialize a value with error logging + * @param string &$string On success, the portion used is removed + * @return array ( bool $success, mixed $value ) + */ + private function unserializeValue( &$string ) { + $error = null; + set_error_handler( function ( $errno, $errstr ) use ( &$error ) { + $error = $errstr; + return true; + } ); + $ret = unserialize( $string ); + restore_error_handler(); + + if ( $error !== null ) { + self::$logger->error( 'Value unserialization failed: ' . $error ); + return array( false, null ); + } + + $serialized = serialize( $ret ); + $l = strlen( $serialized ); + if ( substr( $string, 0, $l ) !== $serialized ) { + self::$logger->error( + 'Value unserialization failed: read value does not match original string' + ); + return array( false, null ); + } + + $string = substr( $string, $l ); + return array( true, $ret ); + } + + /** + * Encode a session array to a string in 'php' format + * @note Generally you'll use self::encode() instead of this method. + * @param array $data Session data + * @return string|null Encoded string, or null on failure + */ + public static function encodePhp( array $data ) { + $ret = ''; + foreach ( $data as $key => $value ) { + if ( strcmp( $key, intval( $key ) ) === 0 ) { + self::$logger->warning( "Ignoring unsupported integer key \"$key\"" ); + continue; + } + if ( strcspn( $key, '|!' ) !== strlen( $key ) ) { + self::$logger->error( "Serialization failed: Key with unsupported characters \"$key\"" ); + return null; + } + $v = self::serializeValue( $value ); + if ( $v === null ) { + return null; + } + $ret .= "$key|$v"; + } + return $ret; + } + + /** + * Decode a session string in 'php' format to an array + * @note Generally you'll use self::decode() instead of this method. + * @param string $data Session data + * @return array|null Data, or null on failure + * @throws \\InvalidArgumentException + */ + public static function decodePhp( $data ) { + if ( !is_string( $data ) ) { + throw new \InvalidArgumentException( '$data must be a string' ); + } + + $ret = array(); + while ( $data !== '' && $data !== false ) { + $i = strpos( $data, '|' ); + if ( $i === false ) { + if ( substr( $data, -1 ) !== '!' ) { + self::$logger->warning( 'Ignoring garbage at end of string' ); + } + break; + } + + $key = substr( $data, 0, $i ); + $data = substr( $data, $i + 1 ); + + if ( strpos( $key, '!' ) !== false ) { + self::$logger->warning( "Decoding found a key with unsupported characters: \"$key\"" ); + } + + if ( $data === '' || $data === false ) { + self::$logger->error( 'Unserialize failed: unexpected end of string' ); + return null; + } + + list( $ok, $value ) = self::unserializeValue( $data ); + if ( !$ok ) { + return null; + } + $ret[$key] = $value; + } + return $ret; + } + + /** + * Encode a session array to a string in 'php_binary' format + * @note Generally you'll use self::encode() instead of this method. + * @param array $data Session data + * @return string|null Encoded string, or null on failure + */ + public static function encodePhpBinary( array $data ) { + $ret = ''; + foreach ( $data as $key => $value ) { + if ( strcmp( $key, intval( $key ) ) === 0 ) { + self::$logger->warning( "Ignoring unsupported integer key \"$key\"" ); + continue; + } + $l = strlen( $key ); + if ( $l > 127 ) { + self::$logger->warning( "Ignoring overlong key \"$key\"" ); + continue; + } + $v = self::serializeValue( $value ); + if ( $v === null ) { + return null; + } + $ret .= chr( $l ) . $key . $v; + } + return $ret; + } + + /** + * Decode a session string in 'php_binary' format to an array + * @note Generally you'll use self::decode() instead of this method. + * @param string $data Session data + * @return array|null Data, or null on failure + * @throws \\InvalidArgumentException + */ + public static function decodePhpBinary( $data ) { + if ( !is_string( $data ) ) { + throw new \InvalidArgumentException( '$data must be a string' ); + } + + $ret = array(); + while ( $data !== '' && $data !== false ) { + $l = ord( $data[0] ); + if ( strlen( $data ) < ( $l & 127 ) + 1 ) { + self::$logger->error( 'Unserialize failed: unexpected end of string' ); + return null; + } + + // "undefined" marker + if ( $l > 127 ) { + $data = substr( $data, ( $l & 127 ) + 1 ); + continue; + } + + $key = substr( $data, 1, $l ); + $data = substr( $data, $l + 1 ); + if ( $data === '' || $data === false ) { + self::$logger->error( 'Unserialize failed: unexpected end of string' ); + return null; + } + + list( $ok, $value ) = self::unserializeValue( $data ); + if ( !$ok ) { + return null; + } + $ret[$key] = $value; + } + return $ret; + } + + /** + * Encode a session array to a string in 'php_serialize' format + * @note Generally you'll use self::encode() instead of this method. + * @param array $data Session data + * @return string|null Encoded string, or null on failure + */ + public static function encodePhpSerialize( array $data ) { + try { + return serialize( $data ); + } catch ( \Exception $ex ) { + self::$logger->error( 'PHP serialization failed: ' . $ex->getMessage() ); + return null; + } + } + + /** + * Decode a session string in 'php_serialize' format to an array + * @note Generally you'll use self::decode() instead of this method. + * @param string $data Session data + * @return array|null Data, or null on failure + * @throws \\InvalidArgumentException + */ + public static function decodePhpSerialize( $data ) { + if ( !is_string( $data ) ) { + throw new \InvalidArgumentException( '$data must be a string' ); + } + + $error = null; + set_error_handler( function ( $errno, $errstr ) use ( &$error ) { + $error = $errstr; + return true; + } ); + $ret = unserialize( $data ); + restore_error_handler(); + + if ( $error !== null ) { + self::$logger->error( 'PHP unserialization failed: ' . $error ); + return null; + } + + // PHP strangely allows non-arrays to session_decode(), even though + // that breaks $_SESSION. Let's not do that. + if ( !is_array( $ret ) ) { + self::$logger->error( 'PHP unserialization failed (value was not an array)' ); + return null; + } + + return $ret; + } + +} + +PhpSessionSerializer::setLogger( new \Psr\Log\NullLogger() ); diff --git a/tests/Wikimedia/PhpSessionSerializerTest.php b/tests/Wikimedia/PhpSessionSerializerTest.php new file mode 100644 index 0000000..799b105 --- /dev/null +++ b/tests/Wikimedia/PhpSessionSerializerTest.php @@ -0,0 +1,627 @@ +<?php +/** + * php-session-serializer + * + * Copyright (C) 2015 Brad Jorsch <[email protected]> + * + * 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 Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @author Brad Jorsch <[email protected]> + */ + +namespace Wikimedia; + +use Wikimedia\PhpSessionSerializer; +use Psr\Log\LogLevel; + +// Wikimedia\PhpSessionSerializer relies on the auto-importing of ini_set and +// ini_get from the global namespace. In this unit test, we override these with +// a namespace-local version to allow for testing failure modes. +$wgMockIniInstance = null; + +function ini_set( $var, $value ) { + global $wgMockIniInstance; + return $wgMockIniInstance + ? $wgMockIniInstance->mockIniSet( $var, $value ) + : \ini_set( $var, $value ); +} + +function ini_get( $var ) { + global $wgMockIniInstance; + return $wgMockIniInstance + ? $wgMockIniInstance->mockIniGet( $var ) + : \ini_get( $var ); +} + +/** + * @covers Wikimedia\PhpSessionSerializer + */ +class PhpSessionSerializerTest extends \PHPUnit_Framework_TestCase { + + protected $oldFormat; + + protected $mockIniAllowed; + protected $mockIniValue; + + protected static $standardArray, $longKey, $closure; + + public function mockIniGet( $var ) { + if ( $var !== 'session.serialize_handler' || !is_array( $this->mockIniAllowed ) ) { + return \ini_get( $var ); + } + + return $this->mockIniValue; + } + + public function mockIniSet( $var, $value ) { + if ( $var !== 'session.serialize_handler' || !is_array( $this->mockIniAllowed ) ) { + return \ini_set( $var, $value ); + } + + if ( in_array( $value, $this->mockIniAllowed, true ) ) { + $old = $this->mockIniValue; + $this->mockIniValue = $value; + return $old; + } else { + // trigger_error to test that warnings are properly suppressed + trigger_error( "mockIniSet disallows setting to '$value'", E_USER_WARNING ); + return false; + } + } + + protected static function initTestData() { + self::$longKey = str_pad( 'long key ', 128, '-' ); + self::$standardArray = array( + 'true' => true, + 'false' => false, + 'int' => 42, + 'zero' => 0, + 'double' => 12.34, + 'inf' => INF, + '-inf' => -INF, + 'string' => 'string', + 'empty string' => '', + 'array' => array( 0, 1, 100 => 100, 3 => 3, 2 => 2, 'foo' => 'bar' ), + 'empty array' => array(), + 'object' => (object)array( 'foo' => 'foo' ), + 'empty object' => new \stdClass, + 'null' => null, + '' => 'empty key', + 42 => 42, + self::$longKey => 'long key', + ); + + self::$closure = function () { + }; + } + + protected function setUp() { + global $wgMockIniInstance; + parent::setUp(); + $this->oldFormat = \ini_get( 'session.serialize_handler' ); + PhpSessionSerializer::setLogger( new TestLogger() ); + $wgMockIniInstance = $this; + $this->mockIniValue = $this->oldFormat; + } + + protected function tearDown() { + global $wgMockIniInstance; + $wgMockIniInstance = null; + \ini_set( 'session.serialize_handler', $this->oldFormat ); + parent::tearDown(); + } + + public function testSetSerializeHandler() { + // Test normal operation + $ret = PhpSessionSerializer::setSerializeHandler(); + $this->assertSame( $ret, \ini_get( 'session.serialize_handler' ) ); + + // Test setting php_serialize + $this->mockIniAllowed = array( 'php_serialize', 'php', 'php_binary' ); + $this->mockIniValue = 'php_binary'; + $ret = PhpSessionSerializer::setSerializeHandler(); + $this->assertSame( 'php_serialize', $this->mockIniValue ); + $this->assertSame( $ret, $this->mockIniValue ); + + // Test defaulting to current if it's supported + $this->mockIniAllowed = array( 'php', 'php_binary' ); + $this->mockIniValue = 'php_binary'; + $ret = PhpSessionSerializer::setSerializeHandler(); + $this->assertSame( 'php_binary', $this->mockIniValue ); + $this->assertSame( $ret, $this->mockIniValue ); + + // Test choosing a supported format + $this->mockIniAllowed = array( 'php', 'php_binary' ); + $this->mockIniValue = 'bogus'; + $ret = PhpSessionSerializer::setSerializeHandler(); + $this->assertSame( 'php', $this->mockIniValue ); + $this->assertSame( $ret, $this->mockIniValue ); + + // Test failure + $this->mockIniAllowed = array( 'nothing', 'useful' ); + $this->mockIniValue = 'bogus'; + try { + PhpSessionSerializer::setSerializeHandler(); + $this->fail( 'Expected exception not thrown' ); + } catch ( \DomainException $ex ) { + $this->assertSame( + 'Failed to set serialize handler to a supported format.' . + ' Supported formats are: php_serialize, php, php_binary.', + $ex->getMessage() + ); + } + } + + public static function provideHandlers() { + return array( + array( 'php', 'test|b:1;' ), + array( 'php_binary', "\x04testb:1;" ), + array( 'php_serialize', 'a:1:{s:4:"test";b:1;}' ), + array( 'bogus', new \DomainException( 'Unsupported format "bogus"' ) ), + array( + false, + new \UnexpectedValueException( 'Could not fetch the value of session.serialize_handler' ) + ), + ); + } + + /** + * @dataProvider provideHandlers + */ + public function testEncode( $format, $encoded ) { + $this->mockIniAllowed = array( 'php_serialize', 'php', 'php_binary' ); + $this->mockIniValue = $format; + + $data = array( 'test' => true ); + if ( $encoded instanceof \Exception ) { + try { + PhpSessionSerializer::encode( $data ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \Exception $ex ) { + $this->assertInstanceOf( get_class( $encoded ), $ex ); + $this->assertSame( $encoded->getMessage(), $ex->getMessage() ); + } + } else { + $this->assertSame( $encoded, PhpSessionSerializer::encode( $data ) ); + } + } + + /** + * @dataProvider provideHandlers + */ + public function testDecode( $format, $encoded ) { + $this->mockIniAllowed = array( 'php_serialize', 'php', 'php_binary' ); + $this->mockIniValue = $format; + + $data = array( 'test' => true ); + if ( $encoded instanceof \Exception ) { + try { + PhpSessionSerializer::decode( '' ); + $this->fail( 'Expected exception not thrown' ); + } catch ( \Exception $ex ) { + $this->assertInstanceOf( get_class( $encoded ), $ex ); + $this->assertSame( $encoded->getMessage(), $ex->getMessage() ); + } + } else { + $this->assertSame( $data, PhpSessionSerializer::decode( $encoded ) ); + } + } + + public static function provideEncodePhp() { + self::initTestData(); + return array( + 'std' => array( + self::$standardArray, + 'true|b:1;false|b:0;int|i:42;zero|i:0;double|d:12.34;inf|d:INF;-inf|d:-INF;string|s:' . + '6:"string";empty string|s:0:"";array|a:6:{i:0;i:0;i:1;i:1;i:100;i:100;i:3;i:3;i:2;i' . + ':2;s:3:"foo";s:3:"bar";}empty array|a:0:{}object|O:8:"stdClass":1:{s:3:"foo";s:3:"f' . + 'oo";}empty object|O:8:"stdClass":0:{}null|N;|s:9:"empty key";long key -------------' . + '-----------------------------------------------------------------------------------' . + '-----------------------|s:8:"long key";', + array( + array( LogLevel::WARNING, 'Ignoring unsupported integer key "42"' ), + ), + ), + array( + array( 'pipe|key' => 'piped' ), + null, + array( + array( LogLevel::ERROR, 'Serialization failed: Key with unsupported characters "pipe|key"' ), + ), + ), + array( + array( 'bang!key' => 'banged' ), + null, + array( + array( LogLevel::ERROR, 'Serialization failed: Key with unsupported characters "bang!key"' ), + ), + ), + array( + array( 'nan' => NAN ), + 'nan|d:NAN;', + array(), + ), + array( + array( 'function' => self::$closure ), + null, + array( + array( LogLevel::ERROR, 'Value serialization failed: [serialize Closure error]' ), + ), + ), + ); + } + + public static function provideDecodePhp() { + $ret = array_filter( self::provideEncodePhp(), function ( $x ) { + return $x[1] !== null; + } ); + unset( $ret['std'][0][42] ); + $ret['std'][2] = array(); + + $ret[] = array( + null, + 'test|i:042;', + array( + array( + LogLevel::ERROR, 'Value unserialization failed: read value does not match original string' + ) + ), + ); + $ret[] = array( + null, + 'test|i:42', + array( + array( + LogLevel::ERROR, 'Value unserialization failed: [unserialize error]' + ) + ), + ); + $ret[] = array( + null, + 'test|i:42;X|', + array( + array( LogLevel::ERROR, 'Unserialize failed: unexpected end of string' ) + ), + ); + $ret[] = array( + array( 'test' => 42, 'test2' => 43 ), + 'test|i:42;test2|i:43;X!', + array(), + ); + $ret[] = array( + array( 'test' => 42, 'X!test2' => 43 ), + 'test|i:42;X!test2|i:43;', + array( + array( LogLevel::WARNING, 'Decoding found a key with unsupported characters: "X!test2"' ) + ), + ); + $ret[] = array( + array(), + 'test!', + array(), + ); + $ret[] = array( + array( 'test' => 42 ), + 'test|i:42;test2', + array( + array( LogLevel::WARNING, 'Ignoring garbage at end of string' ) + ), + ); + + return $ret; + } + + /** + * @dataProvider provideEncodePhp + */ + public function testEncodePhp( $data, $encoded, $log ) { + $logger = new TestLogger( true ); + PhpSessionSerializer::setLogger( $logger ); + $this->assertSame( $encoded, PhpSessionSerializer::encodePhp( $data ) ); + $this->assertSame( $log, $logger->getBuffer() ); + } + + /** + * @dataProvider provideDecodePhp + */ + public function testDecodePhp( $data, $encoded, $log ) { + $logger = new TestLogger( true ); + PhpSessionSerializer::setLogger( $logger ); + if ( isset( $data['nan'] ) ) { + $ret = PhpSessionSerializer::decodePhp( $encoded ); + $this->assertTrue( is_nan( $ret['nan'] ) ); + } else { + $this->assertEquals( $data, PhpSessionSerializer::decodePhp( $encoded ) ); + } + $this->assertSame( $log, $logger->getBuffer() ); + } + + public static function provideEncodePhpBinary() { + self::initTestData(); + return array( + 'std' => array( + self::$standardArray, + "\x04trueb:1;\x05falseb:0;\x03inti:42;\x04zeroi:0;\x06doubled:12.34;\x03infd:INF;\x04" . + "-infd:-INF;\x06strings:6:\"string\";\x0cempty strings:0:\"\";\x05arraya:6:{i:0;i:0;i:1;" . + "i:1;i:100;i:100;i:3;i:3;i:2;i:2;s:3:\"foo\";s:3:\"bar\";}\x0bempty arraya:0:{}\x06object" . + "O:8:\"stdClass\":1:{s:3:\"foo\";s:3:\"foo\";}\x0cempty objectO:8:\"stdClass\":0:{}\x04" . + "nullN;\0s:9:\"empty key\";", + array( + array( LogLevel::WARNING, 'Ignoring unsupported integer key "42"' ), + array( LogLevel::WARNING, 'Ignoring overlong key "' . self::$longKey . '"' ), + ), + ), + array( + array( 'pipe|key' => 'piped' ), + "\x08pipe|keys:5:\"piped\";", + array(), + ), + array( + array( 'bang!key' => 'banged' ), + "\x08bang!keys:6:\"banged\";", + array(), + ), + array( + array( 'nan' => NAN ), + "\x03nand:NAN;", + array(), + ), + array( + array( 'function' => self::$closure ), + null, + array( + array( LogLevel::ERROR, 'Value serialization failed: [serialize Closure error]' ), + ), + ), + ); + } + + public static function provideDecodePhpBinary() { + $ret = array_filter( self::provideEncodePhpBinary(), function ( $x ) { + return $x[1] !== null; + } ); + unset( $ret['std'][0][42] ); + unset( $ret['std'][0][self::$longKey] ); + $ret['std'][2] = array(); + + $ret[] = array( + null, + "\x04testi:042;", + array( + array( + LogLevel::ERROR, 'Value unserialization failed: read value does not match original string' + ) + ), + ); + $ret[] = array( + null, + "\x04testi:42", + array( + array( + LogLevel::ERROR, 'Value unserialization failed: [unserialize error]' + ) + ), + ); + $ret[] = array( + null, + "\x50test", + array( + array( + LogLevel::ERROR, 'Unserialize failed: unexpected end of string' + ) + ), + ); + $ret[] = array( + null, + "\x04test", + array( + array( + LogLevel::ERROR, 'Unserialize failed: unexpected end of string' + ) + ), + ); + $ret[] = array( + array( 'test' => 42, 'test2' => 43 ), + "\x04testi:42;\x05test2i:43;\x81X", + array(), + ); + $ret[] = array( + array(), + "\x84test", + array(), + ); + + return $ret; + } + + /** + * @dataProvider provideEncodePhpBinary + */ + public function testEncodePhpBinary( $data, $encoded, $log ) { + $logger = new TestLogger( true ); + PhpSessionSerializer::setLogger( $logger ); + $this->assertSame( $encoded, PhpSessionSerializer::encodePhpBinary( $data ) ); + $this->assertSame( $log, $logger->getBuffer() ); + } + + /** + * @dataProvider provideDecodePhpBinary + */ + public function testDecodePhpBinary( $data, $encoded, $log ) { + $logger = new TestLogger( true ); + PhpSessionSerializer::setLogger( $logger ); + if ( isset( $data['nan'] ) ) { + $ret = PhpSessionSerializer::decodePhpBinary( $encoded ); + $this->assertTrue( is_nan( $ret['nan'] ) ); + } else { + $this->assertEquals( $data, PhpSessionSerializer::decodePhpBinary( $encoded ) ); + } + $this->assertSame( $log, $logger->getBuffer() ); + } + + public static function provideEncodePhpSerialize() { + self::initTestData(); + return array( + array( + self::$standardArray, + 'a:17:{s:4:"true";b:1;s:5:"false";b:0;s:3:"int";i:42;s:4:"zero";i:0;s:6:"double";d:1' . + '2.34;s:3:"inf";d:INF;s:4:"-inf";d:-INF;s:6:"string";s:6:"string";s:12:"empty string"' . + ';s:0:"";s:5:"array";a:6:{i:0;i:0;i:1;i:1;i:100;i:100;i:3;i:3;i:2;i:2;s:3:"foo";s:3:"' . + 'bar";}s:11:"empty array";a:0:{}s:6:"object";O:8:"stdClass":1:{s:3:"foo";s:3:"foo";}s' . + ':12:"empty object";O:8:"stdClass":0:{}s:4:"null";N;s:0:"";s:9:"empty key";i:42;i:42;' . + 's:128:"long key --------------------------------------------------------------------' . + '---------------------------------------------------";s:8:"long key";}', + array(), + ), + array( + array( 'pipe|key' => 'piped' ), + 'a:1:{s:8:"pipe|key";s:5:"piped";}', + array(), + ), + array( + array( 'bang!key' => 'banged' ), + 'a:1:{s:8:"bang!key";s:6:"banged";}', + array(), + ), + array( + array( 'nan' => NAN ), + 'a:1:{s:3:"nan";d:NAN;}', + array(), + ), + array( + array( 'function' => self::$closure ), + null, + array( + array( LogLevel::ERROR, 'PHP serialization failed: [serialize Closure error]' ), + ), + ), + ); + } + + public static function provideDecodePhpSerialize() { + $ret = array_filter( self::provideEncodePhpSerialize(), function ( $x ) { + return $x[1] !== null; + } ); + + $ret[] = array( + null, + 'Bogus', + array( + array( + LogLevel::ERROR, 'PHP unserialization failed: [unserialize error]' + ) + ), + ); + $ret[] = array( + null, + 'O:8:"stdClass":1:{s:3:"foo";s:3:"bar";}', + array( + array( LogLevel::ERROR, 'PHP unserialization failed (value was not an array)' ) + ), + ); + + return $ret; + } + + /** + * @dataProvider provideEncodePhpSerialize + */ + public function testEncodePhpSerialize( $data, $encoded, $log ) { + $logger = new TestLogger( true ); + PhpSessionSerializer::setLogger( $logger ); + $this->assertSame( $encoded, PhpSessionSerializer::encodePhpSerialize( $data ) ); + $this->assertSame( $log, $logger->getBuffer() ); + } + + /** + * @dataProvider provideDecodePhpSerialize + */ + public function testDecodePhpSerialize( $data, $encoded, $log ) { + $logger = new TestLogger( true ); + PhpSessionSerializer::setLogger( $logger ); + if ( isset( $data['nan'] ) ) { + $ret = PhpSessionSerializer::decodePhpSerialize( $encoded ); + $this->assertTrue( is_nan( $ret['nan'] ) ); + } else { + $this->assertEquals( $data, PhpSessionSerializer::decodePhpSerialize( $encoded ) ); + } + $this->assertSame( $log, $logger->getBuffer() ); + } + + public static function provideDecoders() { + return array( + array( 'decode' ), + array( 'decodePhp' ), + array( 'decodePhpBinary' ), + array( 'decodePhpSerialize' ), + ); + } + + /** + * @dataProvider provideDecoders + * @expectedException InvalidArgumentException + * @expectedExceptionMessage $data must be a string + */ + public function testDecoderTypeCheck( $method ) { + call_user_func( array( '\\Wikimedia\\PhpSessionSerializer', $method ), 1 ); + } + +} + +class TestLogger extends \Psr\Log\AbstractLogger { + private $collect = false; + private $redact = null; + private $buffer = array(); + + public function __construct( $collect = false, $closure = null ) { + $this->collect = $collect; + $this->redact = null; + } + + public function getBuffer() { + return $this->buffer; + } + + public function log( $level, $message, array $context = array() ) { + $message = trim( $message ); + + if ( $this->collect ) { + // HHVM and Zend produce different error messages, correct for that. + $message = preg_replace( + '/(?:' . join( '|', array( + 'unserialize\(\): Error at offset 0 of [45] bytes', + 'Unable to unserialize: \[.*\]. Unexpected end of buffer during unserialization.', + 'Unable to unserialize: \[Bogus\]. Expected \':\' but got \'o\'.', + ) ) . ')$/', + '[unserialize error]', + $message + ); + $message = preg_replace( + '/(?:' . join( '|', array( + 'Serialization of \'Closure\' is not allowed', + 'Attempted to serialize unserializable builtin class Closure\$\S+' + ) ) . ')$/', + '[serialize Closure error]', + $message + ); + + $this->buffer[] = array( $level, $message ); + } else { + echo "LOG[$level]: $message\n"; + } + } +} -- To view, visit https://gerrit.wikimedia.org/r/246255 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I9c083d47bb5c1a9e06223634f816a4ffb89036b9 Gerrit-PatchSet: 10 Gerrit-Project: php-session-serializer Gerrit-Branch: master Gerrit-Owner: Anomie <[email protected]> Gerrit-Reviewer: Anomie <[email protected]> Gerrit-Reviewer: BryanDavis <[email protected]> Gerrit-Reviewer: Gergő Tisza <[email protected]> Gerrit-Reviewer: Legoktm <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
