diff options
Diffstat (limited to 'includes/libs')
43 files changed, 4474 insertions, 4349 deletions
diff --git a/includes/libs/APACHE-LICENSE-2.0.txt b/includes/libs/APACHE-LICENSE-2.0.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/includes/libs/APACHE-LICENSE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/includes/libs/ArrayUtils.php b/includes/libs/ArrayUtils.php new file mode 100644 index 00000000..f9340210 --- /dev/null +++ b/includes/libs/ArrayUtils.php @@ -0,0 +1,187 @@ +<?php +/** + * Methods to play with arrays. + * + * 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 + */ + +/** + * A collection of static methods to play with arrays. + * + * @since 1.21 + */ +class ArrayUtils { + /** + * Sort the given array in a pseudo-random order which depends only on the + * given key and each element value. This is typically used for load + * balancing between servers each with a local cache. + * + * Keys are preserved. The input array is modified in place. + * + * Note: Benchmarking on PHP 5.3 and 5.4 indicates that for small + * strings, md5() is only 10% slower than hash('joaat',...) etc., + * since the function call overhead dominates. So there's not much + * justification for breaking compatibility with installations + * compiled with ./configure --disable-hash. + * + * @param array $array Array to sort + * @param string $key + * @param string $separator A separator used to delimit the array elements and the + * key. This can be chosen to provide backwards compatibility with + * various consistent hash implementations that existed before this + * function was introduced. + */ + public static function consistentHashSort( &$array, $key, $separator = "\000" ) { + $hashes = array(); + foreach ( $array as $elt ) { + $hashes[$elt] = md5( $elt . $separator . $key ); + } + uasort( $array, function ( $a, $b ) use ( $hashes ) { + return strcmp( $hashes[$a], $hashes[$b] ); + } ); + } + + /** + * Given an array of non-normalised probabilities, this function will select + * an element and return the appropriate key + * + * @param array $weights + * @return bool|int|string + */ + public static function pickRandom( $weights ) { + if ( !is_array( $weights ) || count( $weights ) == 0 ) { + return false; + } + + $sum = array_sum( $weights ); + if ( $sum == 0 ) { + # No loads on any of them + # In previous versions, this triggered an unweighted random selection, + # but this feature has been removed as of April 2006 to allow for strict + # separation of query groups. + return false; + } + $max = mt_getrandmax(); + $rand = mt_rand( 0, $max ) / $max * $sum; + + $sum = 0; + foreach ( $weights as $i => $w ) { + $sum += $w; + # Do not return keys if they have 0 weight. + # Note that the "all 0 weight" case is handed above + if ( $w > 0 && $sum >= $rand ) { + break; + } + } + + return $i; + } + + /** + * Do a binary search, and return the index of the largest item that sorts + * less than or equal to the target value. + * + * @since 1.23 + * + * @param callable $valueCallback A function to call to get the value with + * a given array index. + * @param int $valueCount The number of items accessible via $valueCallback, + * indexed from 0 to $valueCount - 1 + * @param callable $comparisonCallback A callback to compare two values, returning + * -1, 0 or 1 in the style of strcmp(). + * @param string $target The target value to find. + * + * @return int|bool The item index of the lower bound, or false if the target value + * sorts before all items. + */ + public static function findLowerBound( $valueCallback, $valueCount, + $comparisonCallback, $target + ) { + if ( $valueCount === 0 ) { + return false; + } + + $min = 0; + $max = $valueCount; + do { + $mid = $min + ( ( $max - $min ) >> 1 ); + $item = call_user_func( $valueCallback, $mid ); + $comparison = call_user_func( $comparisonCallback, $target, $item ); + if ( $comparison > 0 ) { + $min = $mid; + } elseif ( $comparison == 0 ) { + $min = $mid; + break; + } else { + $max = $mid; + } + } while ( $min < $max - 1 ); + + if ( $min == 0 ) { + $item = call_user_func( $valueCallback, $min ); + $comparison = call_user_func( $comparisonCallback, $target, $item ); + if ( $comparison < 0 ) { + // Before the first item + return false; + } + } + return $min; + } + + /** + * Do array_diff_assoc() on multi-dimensional arrays. + * + * Note: empty arrays are removed. + * + * @since 1.23 + * + * @param array $array1 The array to compare from + * @param array $array2,... More arrays to compare against + * @return array An array containing all the values from array1 + * that are not present in any of the other arrays. + */ + public static function arrayDiffAssocRecursive( $array1 ) { + $arrays = func_get_args(); + array_shift( $arrays ); + $ret = array(); + + foreach ( $array1 as $key => $value ) { + if ( is_array( $value ) ) { + $args = array( $value ); + foreach ( $arrays as $array ) { + if ( isset( $array[$key] ) ) { + $args[] = $array[$key]; + } + } + $valueret = call_user_func_array( __METHOD__, $args ); + if ( count( $valueret ) ) { + $ret[$key] = $valueret; + } + } else { + foreach ( $arrays as $array ) { + if ( isset( $array[$key] ) && $array[$key] === $value ) { + continue 2; + } + } + $ret[$key] = $value; + } + } + + return $ret; + } +} diff --git a/includes/libs/BufferingStatsdDataFactory.php b/includes/libs/BufferingStatsdDataFactory.php new file mode 100644 index 00000000..ea5b09dc --- /dev/null +++ b/includes/libs/BufferingStatsdDataFactory.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright 2015 + * + * 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 + */ + +use Liuggio\StatsdClient\Factory\StatsdDataFactory; + +/** + * A factory for application metric data. + * + * This class prepends a context-specific prefix to each metric key and keeps + * a reference to each constructed metric in an internal array buffer. + * + * @since 1.25 + */ +class BufferingStatsdDataFactory extends StatsdDataFactory { + protected $buffer = array(); + + public function __construct( $prefix ) { + parent::__construct(); + $this->prefix = $prefix; + } + + public function produceStatsdData( $key, $value = 1, $metric = self::STATSD_METRIC_COUNT ) { + $this->buffer[] = $entity = $this->produceStatsdDataEntity(); + if ( $key !== null ) { + $prefixedKey = ltrim( $this->prefix . '.' . $key, '.' ); + $entity->setKey( $prefixedKey ); + } + if ( $value !== null ) { + $entity->setValue( $value ); + } + if ( $metric !== null ) { + $entity->setMetric( $metric ); + } + return $entity; + } + + public function getBuffer() { + return $this->buffer; + } +} diff --git a/includes/libs/CSSJanus.php b/includes/libs/CSSJanus.php deleted file mode 100644 index 07a83a54..00000000 --- a/includes/libs/CSSJanus.php +++ /dev/null @@ -1,458 +0,0 @@ -<?php -/** - * PHP port of CSSJanus. - * - * 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 - */ - -/** - * This is a PHP port of CSSJanus, a utility that transforms CSS style sheets - * written for LTR to RTL. - * - * The original Python version of CSSJanus is Copyright 2008 by Google Inc. and - * is distributed under the Apache license. This PHP port is Copyright 2010 by - * Roan Kattouw and is dual-licensed under the GPL (as in the comment above) and - * the Apache (as in the original code) licenses. - * - * Original code: http://code.google.com/p/cssjanus/source/browse/trunk/cssjanus.py - * License of original code: http://code.google.com/p/cssjanus/source/browse/trunk/LICENSE - * @author Roan Kattouw - * - */ -class CSSJanus { - // Patterns defined as null are built dynamically by buildPatterns() - private static $patterns = array( - 'tmpToken' => '`TMP`', - 'nonAscii' => '[\200-\377]', - 'unicode' => '(?:(?:\\[0-9a-f]{1,6})(?:\r\n|\s)?)', - 'num' => '(?:[0-9]*\.[0-9]+|[0-9]+)', - 'unit' => '(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)', - 'body_selector' => 'body\s*{\s*', - 'direction' => 'direction\s*:\s*', - 'escape' => null, - 'nmstart' => null, - 'nmchar' => null, - 'ident' => null, - 'quantity' => null, - 'possibly_negative_quantity' => null, - 'color' => null, - 'url_special_chars' => '[!#$%&*-~]', - 'valid_after_uri_chars' => '[\'\"]?\s*', - 'url_chars' => null, - 'lookahead_not_open_brace' => null, - 'lookahead_not_closing_paren' => null, - 'lookahead_for_closing_paren' => null, - 'lookahead_not_letter' => '(?![a-zA-Z])', - 'lookbehind_not_letter' => '(?<![a-zA-Z])', - 'chars_within_selector' => '[^\}]*?', - 'noflip_annotation' => '\/\*\!?\s*@noflip\s*\*\/', - 'noflip_single' => null, - 'noflip_class' => null, - 'comment' => '/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//', - 'direction_ltr' => null, - 'direction_rtl' => null, - 'left' => null, - 'right' => null, - 'left_in_url' => null, - 'right_in_url' => null, - 'ltr_in_url' => null, - 'rtl_in_url' => null, - 'cursor_east' => null, - 'cursor_west' => null, - 'four_notation_quantity' => null, - 'four_notation_color' => null, - 'border_radius' => null, - 'box_shadow' => null, - 'text_shadow1' => null, - 'text_shadow2' => null, - 'bg_horizontal_percentage' => null, - 'bg_horizontal_percentage_x' => null, - ); - - /** - * Build patterns we can't define above because they depend on other patterns. - */ - private static function buildPatterns() { - if (!is_null(self::$patterns['escape'])) { - // Patterns have already been built - return; - } - - // @codingStandardsIgnoreStart Generic.Files.LineLength.TooLong - $patterns =& self::$patterns; - $patterns['escape'] = "(?:{$patterns['unicode']}|\\[^\r\n\f0-9a-f])"; - $patterns['nmstart'] = "(?:[_a-z]|{$patterns['nonAscii']}|{$patterns['escape']})"; - $patterns['nmchar'] = "(?:[_a-z0-9-]|{$patterns['nonAscii']}|{$patterns['escape']})"; - $patterns['ident'] = "-?{$patterns['nmstart']}{$patterns['nmchar']}*"; - $patterns['quantity'] = "{$patterns['num']}(?:\s*{$patterns['unit']}|{$patterns['ident']})?"; - $patterns['possibly_negative_quantity'] = "((?:-?{$patterns['quantity']})|(?:inherit|auto))"; - $patterns['color'] = "(#?{$patterns['nmchar']}+|(?:rgba?|hsla?)\([ \d.,%-]+\))"; - $patterns['url_chars'] = "(?:{$patterns['url_special_chars']}|{$patterns['nonAscii']}|{$patterns['escape']})*"; - $patterns['lookahead_not_open_brace'] = "(?!({$patterns['nmchar']}|\r?\n|\s|#|\:|\.|\,|\+|>|\(|\)|\[|\]|=|\*=|~=|\^=|'[^']*'])*?{)"; - $patterns['lookahead_not_closing_paren'] = "(?!{$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; - $patterns['lookahead_for_closing_paren'] = "(?={$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; - $patterns['noflip_single'] = "/({$patterns['noflip_annotation']}{$patterns['lookahead_not_open_brace']}[^;}]+;?)/i"; - $patterns['noflip_class'] = "/({$patterns['noflip_annotation']}{$patterns['chars_within_selector']}})/i"; - $patterns['direction_ltr'] = "/({$patterns['direction']})ltr/i"; - $patterns['direction_rtl'] = "/({$patterns['direction']})rtl/i"; - $patterns['left'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; - $patterns['right'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_not_letter']}{$patterns['lookahead_not_closing_paren']}{$patterns['lookahead_not_open_brace']}/i"; - $patterns['left_in_url'] = "/{$patterns['lookbehind_not_letter']}(left){$patterns['lookahead_for_closing_paren']}/i"; - $patterns['right_in_url'] = "/{$patterns['lookbehind_not_letter']}(right){$patterns['lookahead_for_closing_paren']}/i"; - $patterns['ltr_in_url'] = "/{$patterns['lookbehind_not_letter']}(ltr){$patterns['lookahead_for_closing_paren']}/i"; - $patterns['rtl_in_url'] = "/{$patterns['lookbehind_not_letter']}(rtl){$patterns['lookahead_for_closing_paren']}/i"; - $patterns['cursor_east'] = "/{$patterns['lookbehind_not_letter']}([ns]?)e-resize/"; - $patterns['cursor_west'] = "/{$patterns['lookbehind_not_letter']}([ns]?)w-resize/"; - $patterns['four_notation_quantity_props'] = "((?:margin|padding|border-width)\s*:\s*)"; - $patterns['four_notation_quantity'] = "/{$patterns['four_notation_quantity_props']}{$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s+){$patterns['possibly_negative_quantity']}(\s*[;}])/i"; - $patterns['four_notation_color'] = "/((?:-color|border-style)\s*:\s*){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s+){$patterns['color']}(\s*[;}])/i"; - $patterns['border_radius'] = "/(border-radius\s*:\s*)([^;}]*)/"; - $patterns['box_shadow'] = "/(box-shadow\s*:\s*(?:inset\s*)?){$patterns['possibly_negative_quantity']}/i"; - $patterns['text_shadow1'] = "/(text-shadow\s*:\s*){$patterns['color']}(\s*){$patterns['possibly_negative_quantity']}/i"; - $patterns['text_shadow2'] = "/(text-shadow\s*:\s*){$patterns['possibly_negative_quantity']}/i"; - $patterns['bg_horizontal_percentage'] = "/(background(?:-position)?\s*:\s*(?:[^:;}\s]+\s+)*?)({$patterns['quantity']})/i"; - $patterns['bg_horizontal_percentage_x'] = "/(background-position-x\s*:\s*)(-?{$patterns['num']}%)/i"; - // @codingStandardsIgnoreEnd - - } - - /** - * Transform an LTR stylesheet to RTL - * @param string $css stylesheet to transform - * @param $swapLtrRtlInURL Boolean: If true, swap 'ltr' and 'rtl' in URLs - * @param $swapLeftRightInURL Boolean: If true, swap 'left' and 'right' in URLs - * @return string Transformed stylesheet - */ - public static function transform($css, $swapLtrRtlInURL = false, $swapLeftRightInURL = false) { - // We wrap tokens in ` , not ~ like the original implementation does. - // This was done because ` is not a legal character in CSS and can only - // occur in URLs, where we escape it to %60 before inserting our tokens. - $css = str_replace('`', '%60', $css); - - self::buildPatterns(); - - // Tokenize single line rules with /* @noflip */ - $noFlipSingle = new CSSJanusTokenizer(self::$patterns['noflip_single'], '`NOFLIP_SINGLE`'); - $css = $noFlipSingle->tokenize($css); - - // Tokenize class rules with /* @noflip */ - $noFlipClass = new CSSJanusTokenizer(self::$patterns['noflip_class'], '`NOFLIP_CLASS`'); - $css = $noFlipClass->tokenize($css); - - // Tokenize comments - $comments = new CSSJanusTokenizer(self::$patterns['comment'], '`C`'); - $css = $comments->tokenize($css); - - // LTR->RTL fixes start here - $css = self::fixDirection($css); - if ($swapLtrRtlInURL) { - $css = self::fixLtrRtlInURL($css); - } - - if ($swapLeftRightInURL) { - $css = self::fixLeftRightInURL($css); - } - $css = self::fixLeftAndRight($css); - $css = self::fixCursorProperties($css); - $css = self::fixFourPartNotation($css); - $css = self::fixBorderRadius($css); - $css = self::fixBackgroundPosition($css); - $css = self::fixShadows($css); - - // Detokenize stuff we tokenized before - $css = $comments->detokenize($css); - $css = $noFlipClass->detokenize($css); - $css = $noFlipSingle->detokenize($css); - - return $css; - } - - /** - * Replace direction: ltr; with direction: rtl; and vice versa. - * - * The original implementation only does this inside body selectors - * and misses "body\n{\ndirection:ltr;\n}". This function does not have - * these problems. - * - * See https://code.google.com/p/cssjanus/issues/detail?id=15 - * - * @param $css string - * @return string - */ - private static function fixDirection($css) { - $css = preg_replace( - self::$patterns['direction_ltr'], - '$1' . self::$patterns['tmpToken'], - $css - ); - $css = preg_replace(self::$patterns['direction_rtl'], '$1ltr', $css); - $css = str_replace(self::$patterns['tmpToken'], 'rtl', $css); - - return $css; - } - - /** - * Replace 'ltr' with 'rtl' and vice versa in background URLs - * @param $css string - * @return string - */ - private static function fixLtrRtlInURL($css) { - $css = preg_replace(self::$patterns['ltr_in_url'], self::$patterns['tmpToken'], $css); - $css = preg_replace(self::$patterns['rtl_in_url'], 'ltr', $css); - $css = str_replace(self::$patterns['tmpToken'], 'rtl', $css); - - return $css; - } - - /** - * Replace 'left' with 'right' and vice versa in background URLs - * @param $css string - * @return string - */ - private static function fixLeftRightInURL($css) { - $css = preg_replace(self::$patterns['left_in_url'], self::$patterns['tmpToken'], $css); - $css = preg_replace(self::$patterns['right_in_url'], 'left', $css); - $css = str_replace(self::$patterns['tmpToken'], 'right', $css); - - return $css; - } - - /** - * Flip rules like left: , padding-right: , etc. - * @param $css string - * @return string - */ - private static function fixLeftAndRight($css) { - $css = preg_replace(self::$patterns['left'], self::$patterns['tmpToken'], $css); - $css = preg_replace(self::$patterns['right'], 'left', $css); - $css = str_replace(self::$patterns['tmpToken'], 'right', $css); - - return $css; - } - - /** - * Flip East and West in rules like cursor: nw-resize; - * @param $css string - * @return string - */ - private static function fixCursorProperties($css) { - $css = preg_replace( - self::$patterns['cursor_east'], - '$1' . self::$patterns['tmpToken'], - $css - ); - $css = preg_replace(self::$patterns['cursor_west'], '$1e-resize', $css); - $css = str_replace(self::$patterns['tmpToken'], 'w-resize', $css); - - return $css; - } - - /** - * Swap the second and fourth parts in four-part notation rules like - * padding: 1px 2px 3px 4px; - * - * Unlike the original implementation, this function doesn't suffer from - * the bug where whitespace is not preserved when flipping four-part rules - * and four-part color rules with multiple whitespace characters between - * colors are not recognized. - * See https://code.google.com/p/cssjanus/issues/detail?id=16 - * @param $css string - * @return string - */ - private static function fixFourPartNotation($css) { - $css = preg_replace(self::$patterns['four_notation_quantity'], '$1$2$3$8$5$6$7$4$9', $css); - $css = preg_replace(self::$patterns['four_notation_color'], '$1$2$3$8$5$6$7$4$9', $css); - return $css; - } - - /** - * Swaps appropriate corners in border-radius values. - * - * @param $css string - * @return string - */ - private static function fixBorderRadius($css) { - $css = preg_replace_callback(self::$patterns['border_radius'], function ($matches) { - $pre = $matches[1]; - $values = $matches[2]; - $numValues = count(preg_split('/\s+/', trim($values))); - switch ($numValues) { - case 4: - $values = preg_replace('/^(\S+)(\s*)(\S+)(\s*)(\S+)(\s*)(\S+)/', '$3$2$1$4$7$6$5', $values); - break; - case 3: - case 2: - $values = preg_replace('/^(\S+)(\s*)(\S+)/', '$3$2$1', $values); - break; - } - return $pre . $values; - }, $css); - - return $css; - } - - /** - * Negates horizontal offset in box-shadow and text-shadow rules. - * - * @param $css string - * @return string - */ - private static function fixShadows($css) { - // Flips the sign of a CSS value, possibly with a unit. - // (We can't just negate the value with unary minus due to the units.) - $flipSign = function ($cssValue) { - // Don't mangle zeroes - if (floatval($cssValue) === 0.0) { - return $cssValue; - } elseif ($cssValue[0] === '-') { - return substr($cssValue, 1); - } else { - return "-" . $cssValue; - } - }; - - $css = preg_replace_callback(self::$patterns['box_shadow'], function ($matches) use ($flipSign) { - return $matches[1] . $flipSign($matches[2]); - }, $css); - - $css = preg_replace_callback(self::$patterns['text_shadow1'], function ($matches) use ($flipSign) { - return $matches[1] . $matches[2] . $matches[3] . $flipSign($matches[4]); - }, $css); - - $css = preg_replace_callback(self::$patterns['text_shadow2'], function ($matches) use ($flipSign) { - return $matches[1] . $flipSign($matches[2]); - }, $css); - - return $css; - } - - /** - * Flip horizontal background percentages. - * @param $css string - * @return string - */ - private static function fixBackgroundPosition($css) { - $replaced = preg_replace_callback( - self::$patterns['bg_horizontal_percentage'], - array('self', 'calculateNewBackgroundPosition'), - $css - ); - if ($replaced !== null) { - // preg_replace_callback() sometimes returns null - $css = $replaced; - } - $replaced = preg_replace_callback( - self::$patterns['bg_horizontal_percentage_x'], - array('self', 'calculateNewBackgroundPosition'), - $css - ); - if ($replaced !== null) { - $css = $replaced; - } - - return $css; - } - - /** - * Callback for fixBackgroundPosition() - * @param $matches array - * @return string - */ - private static function calculateNewBackgroundPosition($matches) { - $value = $matches[2]; - if (substr($value, -1) === '%') { - $idx = strpos($value, '.'); - if ($idx !== false) { - $len = strlen($value) - $idx - 2; - $value = number_format(100 - $value, $len) . '%'; - } else { - $value = (100 - $value) . '%'; - } - } - return $matches[1] . $value; - } -} - -/** - * Utility class used by CSSJanus that tokenizes and untokenizes things we want - * to protect from being janused. - * @author Roan Kattouw - */ -class CSSJanusTokenizer { - private $regex; - private $token; - private $originals; - - /** - * Constructor - * @param string $regex Regular expression whose matches to replace by a token. - * @param string $token Token - */ - public function __construct($regex, $token) { - $this->regex = $regex; - $this->token = $token; - $this->originals = array(); - } - - /** - * Replace all occurrences of $regex in $str with a token and remember - * the original strings. - * @param string $str to tokenize - * @return string Tokenized string - */ - public function tokenize($str) { - return preg_replace_callback($this->regex, array($this, 'tokenizeCallback'), $str); - } - - /** - * @param $matches array - * @return string - */ - private function tokenizeCallback($matches) { - $this->originals[] = $matches[0]; - return $this->token; - } - - /** - * Replace tokens with their originals. If multiple strings were tokenized, it's important they be - * detokenized in exactly the SAME ORDER. - * @param string $str previously run through tokenize() - * @return string Original string - */ - public function detokenize($str) { - // PHP has no function to replace only the first occurrence or to - // replace occurrences of the same string with different values, - // so we use preg_replace_callback() even though we don't really need a regex - return preg_replace_callback( - '/' . preg_quote($this->token, '/') . '/', - array($this, 'detokenizeCallback'), - $str - ); - } - - /** - * @param $matches - * @return mixed - */ - private function detokenizeCallback($matches) { - $retval = current($this->originals); - next($this->originals); - - return $retval; - } -} diff --git a/includes/libs/CSSMin.php b/includes/libs/CSSMin.php index c69e79f5..ffe26a96 100644 --- a/includes/libs/CSSMin.php +++ b/includes/libs/CSSMin.php @@ -32,12 +32,9 @@ class CSSMin { /* Constants */ /** - * Maximum file size to still qualify for in-line embedding as a data-URI - * - * 24,576 is used because Internet Explorer has a 32,768 byte limit for data URIs, - * which when base64 encoded will result in a 1/3 increase in size. + * Internet Explorer data URI length limit. See encodeImageAsDataURI(). */ - const EMBED_SIZE_LIMIT = 24576; + const DATA_URI_SIZE_LIMIT = 32768; const URL_REGEX = 'url\(\s*[\'"]?(?P<file>[^\?\)\'"]*?)(?P<query>\?[^\)\'"]*?|)[\'"]?\s*\)'; const EMBED_REGEX = '\/\*\s*\@embed\s*\*\/'; const COMMENT_REGEX = '\/\*.*?\*\/'; @@ -100,33 +97,68 @@ class CSSMin { } /** - * Encode an image file as a base64 data URI. - * If the image file has a suitable MIME type and size, encode it as a - * base64 data URI. Return false if the image type is unfamiliar or exceeds - * the size limit. + * Encode an image file as a data URI. + * + * If the image file has a suitable MIME type and size, encode it as a data URI, base64-encoded + * for binary files or just percent-encoded otherwise. Return false if the image type is + * unfamiliar or file exceeds the size limit. * * @param string $file Image file to encode. * @param string|null $type File's MIME type or null. If null, CSSMin will * try to autodetect the type. - * @param int|bool $sizeLimit If the size of the target file is greater than - * this value, decline to encode the image file and return false - * instead. If $sizeLimit is false, no limit is enforced. - * @return string|bool: Image contents encoded as a data URI or false. + * @param bool $ie8Compat By default, a data URI will only be produced if it can be made short + * enough to fit in Internet Explorer 8 (and earlier) URI length limit (32,768 bytes). Pass + * `false` to remove this limitation. + * @return string|bool Image contents encoded as a data URI or false. */ - public static function encodeImageAsDataURI( $file, $type = null, - $sizeLimit = self::EMBED_SIZE_LIMIT - ) { - if ( $sizeLimit !== false && filesize( $file ) >= $sizeLimit ) { + public static function encodeImageAsDataURI( $file, $type = null, $ie8Compat = true ) { + // Fast-fail for files that definitely exceed the maximum data URI length + if ( $ie8Compat && filesize( $file ) >= self::DATA_URI_SIZE_LIMIT ) { return false; } + if ( $type === null ) { $type = self::getMimeType( $file ); } if ( !$type ) { return false; } - $data = base64_encode( file_get_contents( $file ) ); - return 'data:' . $type . ';base64,' . $data; + + return self::encodeStringAsDataURI( file_get_contents( $file ), $type, $ie8Compat ); + } + + /** + * Encode file contents as a data URI with chosen MIME type. + * + * The URI will be base64-encoded for binary files or just percent-encoded otherwise. + * + * @since 1.25 + * + * @param string $contents File contents to encode. + * @param string $type File's MIME type. + * @param bool $ie8Compat See encodeImageAsDataURI(). + * @return string|bool Image contents encoded as a data URI or false. + */ + public static function encodeStringAsDataURI( $contents, $type, $ie8Compat = true ) { + // Try #1: Non-encoded data URI + // The regular expression matches ASCII whitespace and printable characters. + if ( preg_match( '/^[\r\n\t\x20-\x7e]+$/', $contents ) ) { + // Do not base64-encode non-binary files (sane SVGs). + // (This often produces longer URLs, but they compress better, yielding a net smaller size.) + $uri = 'data:' . $type . ',' . rawurlencode( $contents ); + if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) { + return $uri; + } + } + + // Try #2: Encoded data URI + $uri = 'data:' . $type . ';base64,' . base64_encode( $contents ); + if ( !$ie8Compat || strlen( $uri ) < self::DATA_URI_SIZE_LIMIT ) { + return $uri; + } + + // A data URI couldn't be produced + return false; } /** @@ -248,9 +280,12 @@ class CSSMin { ); if ( $embedData ) { + // Remember the occurring MIME types to avoid fallbacks when embedding some files. + $mimeTypes = array(); + $ruleWithEmbedded = preg_replace_callback( $pattern, - function ( $match ) use ( $embedAll, $local, $remote ) { + function ( $match ) use ( $embedAll, $local, $remote, &$mimeTypes ) { $embed = $embedAll || $match['embed']; $embedded = CSSMin::remapOne( $match['file'], @@ -260,21 +295,35 @@ class CSSMin { $embed ); + $url = $match['file'] . $match['query']; + $file = $local . $match['file']; + if ( + !CSSMin::isRemoteUrl( $url ) && !CSSMin::isLocalUrl( $url ) + && file_exists( $file ) + ) { + $mimeTypes[ CSSMin::getMimeType( $file ) ] = true; + } + return CSSMin::buildUrlValue( $embedded ); }, $rule ); + + // Are all referenced images SVGs? + $needsEmbedFallback = $mimeTypes !== array( 'image/svg+xml' => true ); } - if ( $embedData && $ruleWithEmbedded !== $ruleWithRemapped ) { - // Build 2 CSS properties; one which uses a base64 encoded data URI in place - // of the @embed comment to try and retain line-number integrity, and the - // other with a remapped an versioned URL and an Internet Explorer hack + if ( !$embedData || $ruleWithEmbedded === $ruleWithRemapped ) { + // We're not embedding anything, or we tried to but the file is not embeddable + return $ruleWithRemapped; + } elseif ( $embedData && $needsEmbedFallback ) { + // Build 2 CSS properties; one which uses a data URI in place of the @embed comment, and + // the other with a remapped and versioned URL with an Internet Explorer 6 and 7 hack // making it ignored in all browsers that support data URIs return "$ruleWithEmbedded;$ruleWithRemapped!ie"; } else { - // No reason to repeat twice - return $ruleWithRemapped; + // Look ma, no fallbacks! This is for files which IE 6 and 7 don't support anyway: SVG. + return $ruleWithEmbedded; } }, $source ); @@ -289,6 +338,34 @@ class CSSMin { } /** + * Is this CSS rule referencing a remote URL? + * + * @private Until we require PHP 5.5 and we can access self:: from closures. + * @param string $maybeUrl + * @return bool + */ + public static function isRemoteUrl( $maybeUrl ) { + if ( substr( $maybeUrl, 0, 2 ) === '//' || parse_url( $maybeUrl, PHP_URL_SCHEME ) ) { + return true; + } + return false; + } + + /** + * Is this CSS rule referencing a local URL? + * + * @private Until we require PHP 5.5 and we can access self:: from closures. + * @param string $maybeUrl + * @return bool + */ + public static function isLocalUrl( $maybeUrl ) { + if ( $maybeUrl !== '' && $maybeUrl[0] === '/' && !self::isRemoteUrl( $maybeUrl ) ) { + return true; + } + return false; + } + + /** * Remap or embed a CSS URL path. * * @param string $file URL to remap/embed @@ -302,22 +379,16 @@ class CSSMin { // The full URL possibly with query, as passed to the 'url()' value in CSS $url = $file . $query; - // Skip fully-qualified and protocol-relative URLs and data URIs - if ( substr( $url, 0, 2 ) === '//' || parse_url( $url, PHP_URL_SCHEME ) ) { - return $url; + // Expand local URLs with absolute paths like /w/index.php to possibly protocol-relative URL, if + // wfExpandUrl() is available. (This will not be the case if we're running outside of MW.) + if ( self::isLocalUrl( $url ) && function_exists( 'wfExpandUrl' ) ) { + return wfExpandUrl( $url, PROTO_RELATIVE ); } - // URLs with absolute paths like /w/index.php need to be expanded - // to absolute URLs but otherwise left alone - if ( $url !== '' && $url[0] === '/' ) { - // Replace the file path with an expanded (possibly protocol-relative) URL - // ...but only if wfExpandUrl() is even available. - // This will not be the case if we're running outside of MW - if ( function_exists( 'wfExpandUrl' ) ) { - return wfExpandUrl( $url, PROTO_RELATIVE ); - } else { - return $url; - } + // Pass thru fully-qualified and protocol-relative URLs and data URIs, as well as local URLs if + // we can't expand them. + if ( self::isRemoteUrl( $url ) || self::isLocalUrl( $url ) ) { + return $url; } if ( $local === false ) { diff --git a/includes/libs/Cookie.php b/includes/libs/Cookie.php new file mode 100644 index 00000000..0fe94444 --- /dev/null +++ b/includes/libs/Cookie.php @@ -0,0 +1,291 @@ +<?php +/** + * Cookie for HTTP requests. + * + * 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 + * @ingroup HTTP + */ + +class Cookie { + protected $name; + protected $value; + protected $expires; + protected $path; + protected $domain; + protected $isSessionKey = true; + // TO IMPLEMENT protected $secure + // TO IMPLEMENT? protected $maxAge (add onto expires) + // TO IMPLEMENT? protected $version + // TO IMPLEMENT? protected $comment + + function __construct( $name, $value, $attr ) { + $this->name = $name; + $this->set( $value, $attr ); + } + + /** + * Sets a cookie. Used before a request to set up any individual + * cookies. Used internally after a request to parse the + * Set-Cookie headers. + * + * @param string $value The value of the cookie + * @param array $attr Possible key/values: + * expires A date string + * path The path this cookie is used on + * domain Domain this cookie is used on + * @throws InvalidArgumentException + */ + public function set( $value, $attr ) { + $this->value = $value; + + if ( isset( $attr['expires'] ) ) { + $this->isSessionKey = false; + $this->expires = strtotime( $attr['expires'] ); + } + + if ( isset( $attr['path'] ) ) { + $this->path = $attr['path']; + } else { + $this->path = '/'; + } + + if ( isset( $attr['domain'] ) ) { + if ( self::validateCookieDomain( $attr['domain'] ) ) { + $this->domain = $attr['domain']; + } + } else { + throw new InvalidArgumentException( '$attr must contain a domain' ); + } + } + + /** + * Return the true if the cookie is valid is valid. Otherwise, + * false. The uses a method similar to IE cookie security + * described here: + * http://kuza55.blogspot.com/2008/02/understanding-cookie-security.html + * A better method might be to use a blacklist like + * http://publicsuffix.org/ + * + * @todo fixme fails to detect 3-letter top-level domains + * @todo fixme fails to detect 2-letter top-level domains for single-domain use (probably + * not a big problem in practice, but there are test cases) + * + * @param string $domain The domain to validate + * @param string $originDomain (optional) the domain the cookie originates from + * @return bool + */ + public static function validateCookieDomain( $domain, $originDomain = null ) { + $dc = explode( ".", $domain ); + + // Don't allow a trailing dot or addresses without a or just a leading dot + if ( substr( $domain, -1 ) == '.' || + count( $dc ) <= 1 || + count( $dc ) == 2 && $dc[0] === '' + ) { + return false; + } + + // Only allow full, valid IP addresses + if ( preg_match( '/^[0-9.]+$/', $domain ) ) { + if ( count( $dc ) != 4 ) { + return false; + } + + if ( ip2long( $domain ) === false ) { + return false; + } + + if ( $originDomain == null || $originDomain == $domain ) { + return true; + } + + } + + // Don't allow cookies for "co.uk" or "gov.uk", etc, but allow "supermarket.uk" + if ( strrpos( $domain, "." ) - strlen( $domain ) == -3 ) { + if ( ( count( $dc ) == 2 && strlen( $dc[0] ) <= 2 ) + || ( count( $dc ) == 3 && strlen( $dc[0] ) == "" && strlen( $dc[1] ) <= 2 ) ) { + return false; + } + if ( ( count( $dc ) == 2 || ( count( $dc ) == 3 && $dc[0] == '' ) ) + && preg_match( '/(com|net|org|gov|edu)\...$/', $domain ) ) { + return false; + } + } + + if ( $originDomain != null ) { + if ( substr( $domain, 0, 1 ) != '.' && $domain != $originDomain ) { + return false; + } + + if ( substr( $domain, 0, 1 ) == '.' + && substr_compare( + $originDomain, + $domain, + -strlen( $domain ), + strlen( $domain ), + true + ) != 0 + ) { + return false; + } + } + + return true; + } + + /** + * Serialize the cookie jar into a format useful for HTTP Request headers. + * + * @param string $path The path that will be used. Required. + * @param string $domain The domain that will be used. Required. + * @return string + */ + public function serializeToHttpRequest( $path, $domain ) { + $ret = ''; + + if ( $this->canServeDomain( $domain ) + && $this->canServePath( $path ) + && $this->isUnExpired() ) { + $ret = $this->name . '=' . $this->value; + } + + return $ret; + } + + /** + * @param string $domain + * @return bool + */ + protected function canServeDomain( $domain ) { + if ( $domain == $this->domain + || ( strlen( $domain ) > strlen( $this->domain ) + && substr( $this->domain, 0, 1 ) == '.' + && substr_compare( + $domain, + $this->domain, + -strlen( $this->domain ), + strlen( $this->domain ), + true + ) == 0 + ) + ) { + return true; + } + + return false; + } + + /** + * @param string $path + * @return bool + */ + protected function canServePath( $path ) { + return ( $this->path && substr_compare( $this->path, $path, 0, strlen( $this->path ) ) == 0 ); + } + + /** + * @return bool + */ + protected function isUnExpired() { + return $this->isSessionKey || $this->expires > time(); + } +} + +class CookieJar { + private $cookie = array(); + + /** + * Set a cookie in the cookie jar. Make sure only one cookie per-name exists. + * @see Cookie::set() + * @param string $name + * @param string $value + * @param array $attr + */ + public function setCookie( $name, $value, $attr ) { + /* cookies: case insensitive, so this should work. + * We'll still send the cookies back in the same case we got them, though. + */ + $index = strtoupper( $name ); + + if ( isset( $this->cookie[$index] ) ) { + $this->cookie[$index]->set( $value, $attr ); + } else { + $this->cookie[$index] = new Cookie( $name, $value, $attr ); + } + } + + /** + * @see Cookie::serializeToHttpRequest + * @param string $path + * @param string $domain + * @return string + */ + public function serializeToHttpRequest( $path, $domain ) { + $cookies = array(); + + foreach ( $this->cookie as $c ) { + $serialized = $c->serializeToHttpRequest( $path, $domain ); + + if ( $serialized ) { + $cookies[] = $serialized; + } + } + + return implode( '; ', $cookies ); + } + + /** + * Parse the content of an Set-Cookie HTTP Response header. + * + * @param string $cookie + * @param string $domain Cookie's domain + * @return null + */ + public function parseCookieResponseHeader( $cookie, $domain ) { + $len = strlen( 'Set-Cookie:' ); + + if ( substr_compare( 'Set-Cookie:', $cookie, 0, $len, true ) === 0 ) { + $cookie = substr( $cookie, $len ); + } + + $bit = array_map( 'trim', explode( ';', $cookie ) ); + + if ( count( $bit ) >= 1 ) { + list( $name, $value ) = explode( '=', array_shift( $bit ), 2 ); + $attr = array(); + + foreach ( $bit as $piece ) { + $parts = explode( '=', $piece ); + if ( count( $parts ) > 1 ) { + $attr[strtolower( $parts[0] )] = $parts[1]; + } else { + $attr[strtolower( $parts[0] )] = true; + } + } + + if ( !isset( $attr['domain'] ) ) { + $attr['domain'] = $domain; + } elseif ( !Cookie::validateCookieDomain( $attr['domain'], $domain ) ) { + return null; + } + + $this->setCookie( $name, $value, $attr ); + } + } +} diff --git a/includes/libs/DeferredStringifier.php b/includes/libs/DeferredStringifier.php new file mode 100644 index 00000000..a6fd11a4 --- /dev/null +++ b/includes/libs/DeferredStringifier.php @@ -0,0 +1,57 @@ +<?php +/** + * Class that defers a slow string generation until the string is actually needed. + * + * 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 + */ + +/** + * @since 1.25 + */ +class DeferredStringifier { + /** @var callable Callback used for result string generation */ + private $callback; + + /** @var array */ + private $params; + + /** @var string */ + private $result; + + /** + * @param callable $callback Callback that gets called by __toString + * @param mixed $param,... Parameters to the callback + */ + public function __construct( $callback /*...*/ ) { + $this->params = func_get_args(); + array_shift( $this->params ); + $this->callback = $callback; + } + + /** + * Get the string generated from the callback + * + * @return string + */ + public function __toString() { + if ( $this->result === null ) { + $this->result = call_user_func_array( $this->callback, $this->params ); + } + return $this->result; + } +} diff --git a/includes/libs/ExplodeIterator.php b/includes/libs/ExplodeIterator.php new file mode 100644 index 00000000..3b34d9bc --- /dev/null +++ b/includes/libs/ExplodeIterator.php @@ -0,0 +1,116 @@ +<?php +/** + * 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 + */ + +/** + * An iterator which works exactly like: + * + * foreach ( explode( $delim, $s ) as $element ) { + * ... + * } + * + * Except it doesn't use 193 byte per element + */ +class ExplodeIterator implements Iterator { + // The subject string + private $subject, $subjectLength; + + // The delimiter + private $delim, $delimLength; + + // The position of the start of the line + private $curPos; + + // The position after the end of the next delimiter + private $endPos; + + // The current token + private $current; + + /** + * Construct a DelimIterator + * @param string $delim + * @param string $subject + */ + public function __construct( $delim, $subject ) { + $this->subject = $subject; + $this->delim = $delim; + + // Micro-optimisation (theoretical) + $this->subjectLength = strlen( $subject ); + $this->delimLength = strlen( $delim ); + + $this->rewind(); + } + + public function rewind() { + $this->curPos = 0; + $this->endPos = strpos( $this->subject, $this->delim ); + $this->refreshCurrent(); + } + + public function refreshCurrent() { + if ( $this->curPos === false ) { + $this->current = false; + } elseif ( $this->curPos >= $this->subjectLength ) { + $this->current = ''; + } elseif ( $this->endPos === false ) { + $this->current = substr( $this->subject, $this->curPos ); + } else { + $this->current = substr( $this->subject, $this->curPos, $this->endPos - $this->curPos ); + } + } + + public function current() { + return $this->current; + } + + /** + * @return int|bool Current position or boolean false if invalid + */ + public function key() { + return $this->curPos; + } + + /** + * @return string + */ + public function next() { + if ( $this->endPos === false ) { + $this->curPos = false; + } else { + $this->curPos = $this->endPos + $this->delimLength; + if ( $this->curPos >= $this->subjectLength ) { + $this->endPos = false; + } else { + $this->endPos = strpos( $this->subject, $this->delim, $this->curPos ); + } + } + $this->refreshCurrent(); + + return $this->current; + } + + /** + * @return bool + */ + public function valid() { + return $this->curPos !== false; + } +} diff --git a/includes/libs/GenericArrayObject.php b/includes/libs/GenericArrayObject.php index db8a7ecf..93ae83b2 100644 --- a/includes/libs/GenericArrayObject.php +++ b/includes/libs/GenericArrayObject.php @@ -117,7 +117,7 @@ abstract class GenericArrayObject extends ArrayObject { * * @param mixed $value * - * @return boolean + * @return bool */ protected function hasValidType( $value ) { $class = $this->getObjectType(); @@ -171,7 +171,7 @@ abstract class GenericArrayObject extends ArrayObject { * @param integer|string $index * @param mixed $value * - * @return boolean + * @return bool */ protected function preSetElement( $index, $value ) { return true; @@ -232,7 +232,7 @@ abstract class GenericArrayObject extends ArrayObject { * * @since 1.20 * - * @return boolean + * @return bool */ public function isEmpty() { return $this->count() === 0; diff --git a/includes/libs/IPSet.php b/includes/libs/IPSet.php index ae593785..c1c841e6 100644 --- a/includes/libs/IPSet.php +++ b/includes/libs/IPSet.php @@ -1,6 +1,5 @@ <?php /** - * @section LICENSE * 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 @@ -163,7 +162,7 @@ class IPSet { * Match an IP address against the set * * @param string $ip string IPv[46] address - * @return boolean true is match success, false is match failure + * @return bool true is match success, false is match failure * * If $ip is unparseable, inet_pton may issue an E_WARNING to that effect */ diff --git a/includes/libs/MapCacheLRU.php b/includes/libs/MapCacheLRU.php new file mode 100644 index 00000000..0b6db32e --- /dev/null +++ b/includes/libs/MapCacheLRU.php @@ -0,0 +1,131 @@ +<?php +/** + * Per-process memory cache for storing items. + * + * 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 + * @ingroup Cache + */ + +/** + * Handles a simple LRU key/value map with a maximum number of entries + * + * Use ProcessCacheLRU if hierarchical purging is needed or objects can become stale + * + * @see ProcessCacheLRU + * @ingroup Cache + * @since 1.23 + */ +class MapCacheLRU { + /** @var array */ + protected $cache = array(); // (key => value) + + protected $maxCacheKeys; // integer; max entries + + /** + * @param int $maxKeys Maximum number of entries allowed (min 1). + * @throws Exception When $maxCacheKeys is not an int or =< 0. + */ + public function __construct( $maxKeys ) { + if ( !is_int( $maxKeys ) || $maxKeys < 1 ) { + throw new Exception( __METHOD__ . " must be given an integer and >= 1" ); + } + $this->maxCacheKeys = $maxKeys; + } + + /** + * Set a key/value pair. + * This will prune the cache if it gets too large based on LRU. + * If the item is already set, it will be pushed to the top of the cache. + * + * @param string $key + * @param mixed $value + * @return void + */ + public function set( $key, $value ) { + if ( array_key_exists( $key, $this->cache ) ) { + $this->ping( $key ); // push to top + } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) { + reset( $this->cache ); + $evictKey = key( $this->cache ); + unset( $this->cache[$evictKey] ); + } + $this->cache[$key] = $value; + } + + /** + * Check if a key exists + * + * @param string $key + * @return bool + */ + public function has( $key ) { + return array_key_exists( $key, $this->cache ); + } + + /** + * Get the value for a key. + * This returns null if the key is not set. + * If the item is already set, it will be pushed to the top of the cache. + * + * @param string $key + * @return mixed + */ + public function get( $key ) { + if ( array_key_exists( $key, $this->cache ) ) { + $this->ping( $key ); // push to top + return $this->cache[$key]; + } else { + return null; + } + } + + /** + * @return array + * @since 1.25 + */ + public function getAllKeys() { + return array_keys( $this->cache ); + } + + /** + * Clear one or several cache entries, or all cache entries + * + * @param string|array $keys + * @return void + */ + public function clear( $keys = null ) { + if ( $keys === null ) { + $this->cache = array(); + } else { + foreach ( (array)$keys as $key ) { + unset( $this->cache[$key] ); + } + } + } + + /** + * Push an entry to the top of the cache + * + * @param string $key + */ + protected function ping( $key ) { + $item = $this->cache[$key]; + unset( $this->cache[$key] ); + $this->cache[$key] = $item; + } +} diff --git a/includes/libs/MessageSpecifier.php b/includes/libs/MessageSpecifier.php new file mode 100644 index 00000000..b417f299 --- /dev/null +++ b/includes/libs/MessageSpecifier.php @@ -0,0 +1,39 @@ +<?php +/** + * 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 + */ + +interface MessageSpecifier { + /** + * Returns the message key + * + * If a list of multiple possible keys was supplied to the constructor, this method may + * return any of these keys. After the message has been fetched, this method will return + * the key that was actually used to fetch the message. + * + * @return string + */ + public function getKey(); + + /** + * Returns the message parameters + * + * @return array + */ + public function getParams(); +} diff --git a/includes/libs/MultiHttpClient.php b/includes/libs/MultiHttpClient.php index 8c982c43..fb2daa69 100644 --- a/includes/libs/MultiHttpClient.php +++ b/includes/libs/MultiHttpClient.php @@ -34,6 +34,7 @@ * array bodies are encoded as multipart/form-data and strings * use application/x-www-form-urlencoded (headers sent automatically) * - stream : resource to stream the HTTP response body to + * - proxy : HTTP proxy to use * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'. * * @author Aaron Schulz @@ -52,13 +53,17 @@ class MultiHttpClient { protected $usePipelining = false; /** @var integer */ protected $maxConnsPerHost = 50; + /** @var string|null proxy */ + protected $proxy; /** * @param array $options * - connTimeout : default connection timeout * - reqTimeout : default request timeout + * - proxy : HTTP proxy to use * - usePipelining : whether to use HTTP pipelining if possible (for all hosts) * - maxConnsPerHost : maximum number of concurrent connections (per host) + * @throws Exception */ public function __construct( array $options ) { if ( isset( $options['caBundlePath'] ) ) { @@ -67,7 +72,7 @@ class MultiHttpClient { throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath ); } } - static $opts = array( 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost' ); + static $opts = array( 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost', 'proxy' ); foreach ( $opts as $key ) { if ( isset( $options[$key] ) ) { $this->$key = $options[$key]; @@ -83,7 +88,7 @@ class MultiHttpClient { * - reason : HTTP response reason (empty if there was a serious cURL error) * - headers : <header name/value associative array> * - body : HTTP response body or resource (if "stream" was set) - * - err : Any cURL error string + * - error : Any cURL error string * The map also stores integer-indexed copies of these values. This lets callers do: * <code> * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $http->run( $req ); @@ -103,14 +108,14 @@ class MultiHttpClient { * Execute a set of HTTP(S) requests concurrently * * The maps are returned by this method with the 'response' field set to a map of: - * - code : HTTP response code or 0 if there was a serious cURL error - * - reason : HTTP response reason (empty if there was a serious cURL error) - * - headers : <header name/value associative array> - * - body : HTTP response body or resource (if "stream" was set) - * - err : Any cURL error string - * The map also stores integer-indexed copies of these values. This lets callers do: - * <code> - * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req['response']; + * - code : HTTP response code or 0 if there was a serious cURL error + * - reason : HTTP response reason (empty if there was a serious cURL error) + * - headers : <header name/value associative array> + * - body : HTTP response body or resource (if "stream" was set) + * - error : Any cURL error string + * The map also stores integer-indexed copies of these values. This lets callers do: + * <code> + * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req['response']; * </code> * All headers in the 'headers' field are normalized to use lower case names. * This is true for the request headers and the response headers. Integer-indexed @@ -123,6 +128,7 @@ class MultiHttpClient { * - usePipelining : whether to use HTTP pipelining if possible * - maxConnsPerHost : maximum number of concurrent connections (per host) * @return array $reqs With response array populated for each + * @throws Exception */ public function runMulti( array $reqs, array $opts = array() ) { $chm = $this->getCurlMulti(); @@ -244,12 +250,14 @@ class MultiHttpClient { * - connTimeout : default connection timeout * - reqTimeout : default request timeout * @return resource + * @throws Exception */ protected function getCurlHandle( array &$req, array $opts = array() ) { $ch = curl_init(); curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout ); + curl_setopt( $ch, CURLOPT_PROXY, isset( $req['proxy'] ) ? $req['proxy'] : $this->proxy ); curl_setopt( $ch, CURLOPT_TIMEOUT, isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout ); curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); diff --git a/includes/libs/ObjectFactory.php b/includes/libs/ObjectFactory.php new file mode 100644 index 00000000..ec8c36a1 --- /dev/null +++ b/includes/libs/ObjectFactory.php @@ -0,0 +1,93 @@ +<?php +/** + * 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 + */ + +/** + * Construct objects from configuration instructions. + * + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + */ +class ObjectFactory { + + /** + * Instantiate an object based on a specification array. + * + * The specification array must contain a 'class' key with string value + * that specifies the class name to instantiate or a 'factory' key with + * a callable (is_callable() === true). It can optionally contain + * an 'args' key that provides arguments to pass to the + * constructor/callable. + * + * Object construction using a specification having both 'class' and + * 'args' members will call the constructor of the class using + * ReflectionClass::newInstanceArgs. The use of ReflectionClass carries + * a performance penalty and should not be used to create large numbers of + * objects. If this is needed, consider introducing a factory method that + * can be called via call_user_func_array() instead. + * + * Values in the arguments collection which are Closure instances will be + * expanded by invoking them with no arguments before passing the + * resulting value on to the constructor/callable. This can be used to + * pass DatabaseBase instances or other live objects to the + * constructor/callable. This behavior can be suppressed by adding + * closure_expansion => false to the specification. + * + * @param array $spec Object specification + * @return object + * @throws InvalidArgumentException when object specification does not + * contain 'class' or 'factory' keys + * @throws ReflectionException when 'args' are supplied and 'class' + * constructor is non-public or non-existent + */ + public static function getObjectFromSpec( $spec ) { + $args = isset( $spec['args'] ) ? $spec['args'] : array(); + + if ( !isset( $spec['closure_expansion'] ) || + $spec['closure_expansion'] === true + ) { + $args = array_map( function ( $value ) { + if ( is_object( $value ) && $value instanceof Closure ) { + // If an argument is a Closure, call it. + return $value(); + } else { + return $value; + } + }, $args ); + } + + if ( isset( $spec['class'] ) ) { + $clazz = $spec['class']; + if ( !$args ) { + $obj = new $clazz(); + } else { + $ref = new ReflectionClass( $clazz ); + $obj = $ref->newInstanceArgs( $args ); + } + } elseif ( isset( $spec['factory'] ) ) { + $obj = call_user_func_array( $spec['factory'], $args ); + } else { + throw new InvalidArgumentException( + 'Provided specification lacks both factory and class parameters.' + ); + } + + return $obj; + } +} diff --git a/includes/libs/ProcessCacheLRU.php b/includes/libs/ProcessCacheLRU.php index f988207a..8d80eb38 100644 --- a/includes/libs/ProcessCacheLRU.php +++ b/includes/libs/ProcessCacheLRU.php @@ -28,13 +28,14 @@ class ProcessCacheLRU { /** @var Array */ protected $cache = array(); // (key => prop => value) + /** @var Array */ protected $cacheTimes = array(); // (key => prop => UNIX timestamp) protected $maxCacheKeys; // integer; max entries /** - * @param $maxKeys integer Maximum number of entries allowed (min 1). + * @param int $maxKeys Maximum number of entries allowed (min 1). * @throws UnexpectedValueException When $maxCacheKeys is not an int or =< 0. */ public function __construct( $maxKeys ) { @@ -46,9 +47,9 @@ class ProcessCacheLRU { * This will prune the cache if it gets too large based on LRU. * If the item is already set, it will be pushed to the top of the cache. * - * @param $key string - * @param $prop string - * @param $value mixed + * @param string $key + * @param string $prop + * @param mixed $value * @return void */ public function set( $key, $prop, $value ) { @@ -61,20 +62,22 @@ class ProcessCacheLRU { unset( $this->cacheTimes[$evictKey] ); } $this->cache[$key][$prop] = $value; - $this->cacheTimes[$key][$prop] = time(); + $this->cacheTimes[$key][$prop] = microtime( true ); } /** * Check if a property field exists for a cache entry. * - * @param $key string - * @param $prop string - * @param $maxAge integer Ignore items older than this many seconds (since 1.21) + * @param string $key + * @param string $prop + * @param float $maxAge Ignore items older than this many seconds (since 1.21) * @return bool */ - public function has( $key, $prop, $maxAge = 0 ) { + public function has( $key, $prop, $maxAge = 0.0 ) { if ( isset( $this->cache[$key][$prop] ) ) { - return ( $maxAge <= 0 || ( time() - $this->cacheTimes[$key][$prop] ) <= $maxAge ); + return ( $maxAge <= 0 || + ( microtime( true ) - $this->cacheTimes[$key][$prop] ) <= $maxAge + ); } return false; @@ -85,13 +88,14 @@ class ProcessCacheLRU { * This returns null if the property is not set. * If the item is already set, it will be pushed to the top of the cache. * - * @param $key string - * @param $prop string + * @param string $key + * @param string $prop * @return mixed */ public function get( $key, $prop ) { if ( isset( $this->cache[$key][$prop] ) ) { - $this->ping( $key ); // push to top + // push to top + $this->ping( $key ); return $this->cache[$key][$prop]; } else { return null; @@ -99,9 +103,9 @@ class ProcessCacheLRU { } /** - * Clear one or several cache entries, or all cache entries + * Clear one or several cache entries, or all cache entries. * - * @param $keys string|Array + * @param string|array $keys * @return void */ public function clear( $keys = null ) { @@ -119,8 +123,9 @@ class ProcessCacheLRU { /** * Resize the maximum number of cache entries, removing older entries as needed * - * @param $maxKeys integer + * @param int $maxKeys * @return void + * @throws UnexpectedValueException */ public function resize( $maxKeys ) { if ( !is_int( $maxKeys ) || $maxKeys < 1 ) { @@ -138,7 +143,7 @@ class ProcessCacheLRU { /** * Push an entry to the top of the cache * - * @param $key string + * @param string $key */ protected function ping( $key ) { $item = $this->cache[$key]; diff --git a/includes/libs/ReplacementArray.php b/includes/libs/ReplacementArray.php new file mode 100644 index 00000000..7fdb3093 --- /dev/null +++ b/includes/libs/ReplacementArray.php @@ -0,0 +1,125 @@ +<?php +/** + * 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 + */ + +/** + * Replacement array for FSS with fallback to strtr() + * Supports lazy initialisation of FSS resource + */ +class ReplacementArray { + private $data = false; + private $fss = false; + + /** + * Create an object with the specified replacement array + * The array should have the same form as the replacement array for strtr() + * @param array $data + */ + public function __construct( $data = array() ) { + $this->data = $data; + } + + /** + * @return array + */ + public function __sleep() { + return array( 'data' ); + } + + public function __wakeup() { + $this->fss = false; + } + + /** + * Set the whole replacement array at once + * @param array $data + */ + public function setArray( $data ) { + $this->data = $data; + $this->fss = false; + } + + /** + * @return array|bool + */ + public function getArray() { + return $this->data; + } + + /** + * Set an element of the replacement array + * @param string $from + * @param string $to + */ + public function setPair( $from, $to ) { + $this->data[$from] = $to; + $this->fss = false; + } + + /** + * @param array $data + */ + public function mergeArray( $data ) { + $this->data = array_merge( $this->data, $data ); + $this->fss = false; + } + + /** + * @param ReplacementArray $other + */ + public function merge( ReplacementArray $other ) { + $this->data = array_merge( $this->data, $other->data ); + $this->fss = false; + } + + /** + * @param string $from + */ + public function removePair( $from ) { + unset( $this->data[$from] ); + $this->fss = false; + } + + /** + * @param array $data + */ + public function removeArray( $data ) { + foreach ( $data as $from => $to ) { + $this->removePair( $from ); + } + $this->fss = false; + } + + /** + * @param string $subject + * @return string + */ + public function replace( $subject ) { + if ( function_exists( 'fss_prep_replace' ) ) { + if ( $this->fss === false ) { + $this->fss = fss_prep_replace( $this->data ); + } + $result = fss_exec_replace( $this->fss, $subject ); + } else { + $result = strtr( $subject, $this->data ); + } + + return $result; + } +} diff --git a/includes/libs/RunningStat.php b/includes/libs/RunningStat.php index dda5254e..8bd4656c 100644 --- a/includes/libs/RunningStat.php +++ b/includes/libs/RunningStat.php @@ -60,10 +60,10 @@ class RunningStat implements Countable { /** @var float The second central moment (or variance). **/ public $m2 = 0.0; - /** @var float The least value in the the set. **/ + /** @var float The least value in the set. **/ public $min = INF; - /** @var float The most value in the set. **/ + /** @var float The greatest value in the set. **/ public $max = NEGATIVE_INF; /** @@ -126,10 +126,10 @@ class RunningStat implements Countable { } /** - * Get the estimated stanard deviation. + * Get the estimated standard deviation. * * The standard deviation of a statistical population is the square root of - * its variance. It shows shows how much variation from the mean exists. In + * its variance. It shows how much variation from the mean exists. In * addition to expressing the variability of a population, the standard * deviation is commonly used to measure confidence in statistical conclusions. * diff --git a/includes/libs/ScopedCallback.php b/includes/libs/ScopedCallback.php index 631b6519..1ec9eaa6 100644 --- a/includes/libs/ScopedCallback.php +++ b/includes/libs/ScopedCallback.php @@ -28,16 +28,20 @@ class ScopedCallback { /** @var callable */ protected $callback; + /** @var array */ + protected $params; /** - * @param callable $callback + * @param callable|null $callback + * @param array $params Callback arguments (since 1.25) * @throws Exception */ - public function __construct( $callback ) { - if ( !is_callable( $callback ) ) { + public function __construct( $callback, array $params = array() ) { + if ( $callback !== null && !is_callable( $callback ) ) { throw new InvalidArgumentException( "Provided callback is not valid." ); } $this->callback = $callback; + $this->params = $params; } /** @@ -67,7 +71,7 @@ class ScopedCallback { */ function __destruct() { if ( $this->callback !== null ) { - call_user_func( $this->callback ); + call_user_func_array( $this->callback, $this->params ); } } } diff --git a/includes/libs/StatusValue.php b/includes/libs/StatusValue.php new file mode 100644 index 00000000..3c2dd409 --- /dev/null +++ b/includes/libs/StatusValue.php @@ -0,0 +1,316 @@ +<?php +/** + * 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 + */ + +/** + * Generic operation result class + * Has warning/error list, boolean status and arbitrary value + * + * "Good" means the operation was completed with no warnings or errors. + * + * "OK" means the operation was partially or wholly completed. + * + * An operation which is not OK should have errors so that the user can be + * informed as to what went wrong. Calling the fatal() function sets an error + * message and simultaneously switches off the OK flag. + * + * The recommended pattern for Status objects is to return a StatusValue + * unconditionally, i.e. both on success and on failure -- so that the + * developer of the calling code is reminded that the function can fail, and + * so that a lack of error-handling will be explicit. + * + * The use of Message objects should be avoided when serializability is needed. + * + * @since 1.25 + */ +class StatusValue { + /** @var bool */ + protected $ok = true; + /** @var array */ + protected $errors = array(); + + /** @var mixed */ + public $value; + /** @var array Map of (key => bool) to indicate success of each part of batch operations */ + public $success = array(); + /** @var int Counter for batch operations */ + public $successCount = 0; + /** @var int Counter for batch operations */ + public $failCount = 0; + + /** + * Factory function for fatal errors + * + * @param string|MessageSpecifier $message Message key or object + * @return Status + */ + public static function newFatal( $message /*, parameters...*/ ) { + $params = func_get_args(); + $result = new static(); + call_user_func_array( array( &$result, 'fatal' ), $params ); + return $result; + } + + /** + * Factory function for good results + * + * @param mixed $value + * @return Status + */ + public static function newGood( $value = null ) { + $result = new static(); + $result->value = $value; + return $result; + } + + /** + * Returns whether the operation completed and didn't have any error or + * warnings + * + * @return bool + */ + public function isGood() { + return $this->ok && !$this->errors; + } + + /** + * Returns whether the operation completed + * + * @return bool + */ + public function isOK() { + return $this->ok; + } + + /** + * @return mixed + */ + public function getValue() { + return $this->value; + } + + /** + * Get the list of errors + * + * Each error is a (message:string or MessageSpecifier,params:array) map + * + * @return array + */ + public function getErrors() { + return $this->errors; + } + + /** + * Change operation status + * + * @param bool $ok + */ + public function setOK( $ok ) { + $this->ok = $ok; + } + + /** + * Change operation resuklt + * + * @param bool $ok Whether the operation completed + * @param mixed $value + */ + public function setResult( $ok, $value = null ) { + $this->ok = $ok; + $this->value = $value; + } + + /** + * Add a new warning + * + * @param string|MessageSpecifier $message Message key or object + */ + public function warning( $message /*, parameters... */ ) { + $this->errors[] = array( + 'type' => 'warning', + 'message' => $message, + 'params' => array_slice( func_get_args(), 1 ) + ); + } + + /** + * Add an error, do not set fatal flag + * This can be used for non-fatal errors + * + * @param string|MessageSpecifier $message Message key or object + */ + public function error( $message /*, parameters... */ ) { + $this->errors[] = array( + 'type' => 'error', + 'message' => $message, + 'params' => array_slice( func_get_args(), 1 ) + ); + } + + /** + * Add an error and set OK to false, indicating that the operation + * as a whole was fatal + * + * @param string|MessageSpecifier $message Message key or object + */ + public function fatal( $message /*, parameters... */ ) { + $this->errors[] = array( + 'type' => 'error', + 'message' => $message, + 'params' => array_slice( func_get_args(), 1 ) + ); + $this->ok = false; + } + + /** + * Merge another status object into this one + * + * @param Status $other Other Status object + * @param bool $overwriteValue Whether to override the "value" member + */ + public function merge( $other, $overwriteValue = false ) { + $this->errors = array_merge( $this->errors, $other->errors ); + $this->ok = $this->ok && $other->ok; + if ( $overwriteValue ) { + $this->value = $other->value; + } + $this->successCount += $other->successCount; + $this->failCount += $other->failCount; + } + + /** + * Returns a list of status messages of the given type + * + * Each entry is a map of (message:string or MessageSpecifier,params:array)) + * + * @param string $type + * @return array + */ + public function getErrorsByType( $type ) { + $result = array(); + foreach ( $this->errors as $error ) { + if ( $error['type'] === $type ) { + $result[] = $error; + } + } + + return $result; + } + + /** + * Returns true if the specified message is present as a warning or error + * + * @param string|MessageSpecifier $message Message key or object to search for + * + * @return bool + */ + public function hasMessage( $message ) { + if ( $message instanceof MessageSpecifier ) { + $message = $message->getKey(); + } + foreach ( $this->errors as $error ) { + if ( $error['message'] instanceof MessageSpecifier + && $error['message']->getKey() === $message + ) { + return true; + } elseif ( $error['message'] === $message ) { + return true; + } + } + + return false; + } + + /** + * If the specified source message exists, replace it with the specified + * destination message, but keep the same parameters as in the original error. + * + * Note, due to the lack of tools for comparing IStatusMessage objects, this + * function will not work when using such an object as the search parameter. + * + * @param IStatusMessage|string $source Message key or object to search for + * @param IStatusMessage|string $dest Replacement message key or object + * @return bool Return true if the replacement was done, false otherwise. + */ + public function replaceMessage( $source, $dest ) { + $replaced = false; + + foreach ( $this->errors as $index => $error ) { + if ( $error['message'] === $source ) { + $this->errors[$index]['message'] = $dest; + $replaced = true; + } + } + + return $replaced; + } + + /** + * @return string + */ + public function __toString() { + $status = $this->isOK() ? "OK" : "Error"; + if ( count( $this->errors ) ) { + $errorcount = "collected " . ( count( $this->errors ) ) . " error(s) on the way"; + } else { + $errorcount = "no errors detected"; + } + if ( isset( $this->value ) ) { + $valstr = gettype( $this->value ) . " value set"; + if ( is_object( $this->value ) ) { + $valstr .= "\"" . get_class( $this->value ) . "\" instance"; + } + } else { + $valstr = "no value set"; + } + $out = sprintf( "<%s, %s, %s>", + $status, + $errorcount, + $valstr + ); + if ( count( $this->errors ) > 0 ) { + $hdr = sprintf( "+-%'-4s-+-%'-25s-+-%'-40s-+\n", "", "", "" ); + $i = 1; + $out .= "\n"; + $out .= $hdr; + foreach ( $this->errors as $error ) { + if ( $error['message'] instanceof MessageSpecifier ) { + $key = $error['message']->getKey(); + $params = $error['message']->getParams(); + } elseif ( $error['params'] ) { + $key = $error['message']; + $params = $error['params']; + } else { + $key = $error['message']; + $params = array(); + } + + $out .= sprintf( "| %4d | %-25.25s | %-40.40s |\n", + $i, + $key, + implode( " ", $params ) + ); + $i += 1; + } + $out .= $hdr; + } + + return $out; + } +} diff --git a/includes/libs/StringUtils.php b/includes/libs/StringUtils.php new file mode 100644 index 00000000..11ae0b26 --- /dev/null +++ b/includes/libs/StringUtils.php @@ -0,0 +1,317 @@ +<?php +/** + * Methods to play with strings. + * + * 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 + */ + +/** + * A collection of static methods to play with strings. + */ +class StringUtils { + /** + * Test whether a string is valid UTF-8. + * + * The function check for invalid byte sequences, overlong encoding but + * not for different normalisations. + * + * This relies internally on the mbstring function mb_check_encoding() + * hardcoded to check against UTF-8. Whenever the function is not available + * we fallback to a pure PHP implementation. Setting $disableMbstring to + * true will skip the use of mb_check_encoding, this is mostly intended for + * unit testing our internal implementation. + * + * @since 1.21 + * @note In MediaWiki 1.21, this function did not provide proper UTF-8 validation. + * In particular, the pure PHP code path did not in fact check for overlong forms. + * Beware of this when backporting code to that version of MediaWiki. + * + * @param string $value String to check + * @param bool $disableMbstring Whether to use the pure PHP + * implementation instead of trying mb_check_encoding. Intended for unit + * testing. Default: false + * + * @return bool Whether the given $value is a valid UTF-8 encoded string + */ + static function isUtf8( $value, $disableMbstring = false ) { + $value = (string)$value; + + // If the mbstring extension is loaded, use it. However, before PHP 5.4, values above + // U+10FFFF are incorrectly allowed, so we have to check for them separately. + if ( !$disableMbstring && function_exists( 'mb_check_encoding' ) ) { + static $newPHP; + if ( $newPHP === null ) { + $newPHP = !mb_check_encoding( "\xf4\x90\x80\x80", 'UTF-8' ); + } + + return mb_check_encoding( $value, 'UTF-8' ) && + ( $newPHP || preg_match( "/\xf4[\x90-\xbf]|[\xf5-\xff]/S", $value ) === 0 ); + } + + if ( preg_match( "/[\x80-\xff]/S", $value ) === 0 ) { + // String contains only ASCII characters, has to be valid + return true; + } + + // PCRE implements repetition using recursion; to avoid a stack overflow (and segfault) + // for large input, we check for invalid sequences (<= 5 bytes) rather than valid + // sequences, which can be as long as the input string is. Multiple short regexes are + // used rather than a single long regex for performance. + static $regexes; + if ( $regexes === null ) { + $cont = "[\x80-\xbf]"; + $after = "(?!$cont)"; // "(?:[^\x80-\xbf]|$)" would work here + $regexes = array( + // Continuation byte at the start + "/^$cont/", + + // ASCII byte followed by a continuation byte + "/[\\x00-\x7f]$cont/S", + + // Illegal byte + "/[\xc0\xc1\xf5-\xff]/S", + + // Invalid 2-byte sequence, or valid one then an extra continuation byte + "/[\xc2-\xdf](?!$cont$after)/S", + + // Invalid 3-byte sequence, or valid one then an extra continuation byte + "/\xe0(?![\xa0-\xbf]$cont$after)/", + "/[\xe1-\xec\xee\xef](?!$cont{2}$after)/S", + "/\xed(?![\x80-\x9f]$cont$after)/", + + // Invalid 4-byte sequence, or valid one then an extra continuation byte + "/\xf0(?![\x90-\xbf]$cont{2}$after)/", + "/[\xf1-\xf3](?!$cont{3}$after)/S", + "/\xf4(?![\x80-\x8f]$cont{2}$after)/", + ); + } + + foreach ( $regexes as $regex ) { + if ( preg_match( $regex, $value ) !== 0 ) { + return false; + } + } + + return true; + } + + /** + * Perform an operation equivalent to + * + * preg_replace( "!$startDelim(.*?)$endDelim!", $replace, $subject ); + * + * except that it's worst-case O(N) instead of O(N^2) + * + * Compared to delimiterReplace(), this implementation is fast but memory- + * hungry and inflexible. The memory requirements are such that I don't + * recommend using it on anything but guaranteed small chunks of text. + * + * @param string $startDelim + * @param string $endDelim + * @param string $replace + * @param string $subject + * + * @return string + */ + static function hungryDelimiterReplace( $startDelim, $endDelim, $replace, $subject ) { + $segments = explode( $startDelim, $subject ); + $output = array_shift( $segments ); + foreach ( $segments as $s ) { + $endDelimPos = strpos( $s, $endDelim ); + if ( $endDelimPos === false ) { + $output .= $startDelim . $s; + } else { + $output .= $replace . substr( $s, $endDelimPos + strlen( $endDelim ) ); + } + } + + return $output; + } + + /** + * Perform an operation equivalent to + * + * preg_replace_callback( "!$startDelim(.*)$endDelim!s$flags", $callback, $subject ) + * + * This implementation is slower than hungryDelimiterReplace but uses far less + * memory. The delimiters are literal strings, not regular expressions. + * + * If the start delimiter ends with an initial substring of the end delimiter, + * e.g. in the case of C-style comments, the behavior differs from the model + * regex. In this implementation, the end must share no characters with the + * start, so e.g. /*\/ is not considered to be both the start and end of a + * comment. /*\/xy/*\/ is considered to be a single comment with contents /xy/. + * + * @param string $startDelim Start delimiter + * @param string $endDelim End delimiter + * @param callable $callback Function to call on each match + * @param string $subject + * @param string $flags Regular expression flags + * @throws InvalidArgumentException + * @return string + */ + static function delimiterReplaceCallback( $startDelim, $endDelim, $callback, + $subject, $flags = '' + ) { + $inputPos = 0; + $outputPos = 0; + $output = ''; + $foundStart = false; + $encStart = preg_quote( $startDelim, '!' ); + $encEnd = preg_quote( $endDelim, '!' ); + $strcmp = strpos( $flags, 'i' ) === false ? 'strcmp' : 'strcasecmp'; + $endLength = strlen( $endDelim ); + $m = array(); + + while ( $inputPos < strlen( $subject ) && + preg_match( "!($encStart)|($encEnd)!S$flags", $subject, $m, PREG_OFFSET_CAPTURE, $inputPos ) + ) { + $tokenOffset = $m[0][1]; + if ( $m[1][0] != '' ) { + if ( $foundStart && + $strcmp( $endDelim, substr( $subject, $tokenOffset, $endLength ) ) == 0 + ) { + # An end match is present at the same location + $tokenType = 'end'; + $tokenLength = $endLength; + } else { + $tokenType = 'start'; + $tokenLength = strlen( $m[0][0] ); + } + } elseif ( $m[2][0] != '' ) { + $tokenType = 'end'; + $tokenLength = strlen( $m[0][0] ); + } else { + throw new InvalidArgumentException( 'Invalid delimiter given to ' . __METHOD__ ); + } + + if ( $tokenType == 'start' ) { + # Only move the start position if we haven't already found a start + # This means that START START END matches outer pair + if ( !$foundStart ) { + # Found start + $inputPos = $tokenOffset + $tokenLength; + # Write out the non-matching section + $output .= substr( $subject, $outputPos, $tokenOffset - $outputPos ); + $outputPos = $tokenOffset; + $contentPos = $inputPos; + $foundStart = true; + } else { + # Move the input position past the *first character* of START, + # to protect against missing END when it overlaps with START + $inputPos = $tokenOffset + 1; + } + } elseif ( $tokenType == 'end' ) { + if ( $foundStart ) { + # Found match + $output .= call_user_func( $callback, array( + substr( $subject, $outputPos, $tokenOffset + $tokenLength - $outputPos ), + substr( $subject, $contentPos, $tokenOffset - $contentPos ) + ) ); + $foundStart = false; + } else { + # Non-matching end, write it out + $output .= substr( $subject, $inputPos, $tokenOffset + $tokenLength - $outputPos ); + } + $inputPos = $outputPos = $tokenOffset + $tokenLength; + } else { + throw new InvalidArgumentException( 'Invalid delimiter given to ' . __METHOD__ ); + } + } + if ( $outputPos < strlen( $subject ) ) { + $output .= substr( $subject, $outputPos ); + } + + return $output; + } + + /** + * Perform an operation equivalent to + * + * preg_replace( "!$startDelim(.*)$endDelim!$flags", $replace, $subject ) + * + * @param string $startDelim Start delimiter regular expression + * @param string $endDelim End delimiter regular expression + * @param string $replace Replacement string. May contain $1, which will be + * replaced by the text between the delimiters + * @param string $subject String to search + * @param string $flags Regular expression flags + * @return string The string with the matches replaced + */ + static function delimiterReplace( $startDelim, $endDelim, $replace, $subject, $flags = '' ) { + $replacer = new RegexlikeReplacer( $replace ); + + return self::delimiterReplaceCallback( $startDelim, $endDelim, + $replacer->cb(), $subject, $flags ); + } + + /** + * More or less "markup-safe" explode() + * Ignores any instances of the separator inside <...> + * @param string $separator + * @param string $text + * @return array + */ + static function explodeMarkup( $separator, $text ) { + $placeholder = "\x00"; + + // Remove placeholder instances + $text = str_replace( $placeholder, '', $text ); + + // Replace instances of the separator inside HTML-like tags with the placeholder + $replacer = new DoubleReplacer( $separator, $placeholder ); + $cleaned = StringUtils::delimiterReplaceCallback( '<', '>', $replacer->cb(), $text ); + + // Explode, then put the replaced separators back in + $items = explode( $separator, $cleaned ); + foreach ( $items as $i => $str ) { + $items[$i] = str_replace( $placeholder, $separator, $str ); + } + + return $items; + } + + /** + * Escape a string to make it suitable for inclusion in a preg_replace() + * replacement parameter. + * + * @param string $string + * @return string + */ + static function escapeRegexReplacement( $string ) { + $string = str_replace( '\\', '\\\\', $string ); + $string = str_replace( '$', '\\$', $string ); + + return $string; + } + + /** + * Workalike for explode() with limited memory usage. + * Returns an Iterator + * @param string $separator + * @param string $subject + * @return ArrayIterator|ExplodeIterator + */ + static function explode( $separator, $subject ) { + if ( substr_count( $subject, $separator ) > 1000 ) { + return new ExplodeIterator( $separator, $subject ); + } else { + return new ArrayIterator( explode( $separator, $subject ) ); + } + } +} diff --git a/includes/libs/UDPTransport.php b/includes/libs/UDPTransport.php new file mode 100644 index 00000000..7fad882a --- /dev/null +++ b/includes/libs/UDPTransport.php @@ -0,0 +1,102 @@ +<?php +/** + * 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 + */ + +/** + * A generic class to send a message over UDP + * + * If a message prefix is provided to the constructor or via + * UDPTransport::newFromString(), the payload of the UDP datagrams emitted + * will be formatted with the prefix and a single space at the start of each + * line. This is the payload format expected by the udp2log service. + * + * @since 1.25 + */ +class UDPTransport { + private $host, $port, $prefix, $domain; + + /** + * @param string $host IP address to send to + * @param int $port port number + * @param int $domain AF_INET or AF_INET6 constant + * @param string|bool $prefix Prefix to use, false for no prefix + */ + public function __construct( $host, $port, $domain, $prefix = false ) { + $this->host = $host; + $this->port = $port; + $this->domain = $domain; + $this->prefix = $prefix; + } + + /** + * @param string $info In the format of "udp://host:port/prefix" + * @return UDPTransport + * @throws InvalidArgumentException + */ + public static function newFromString( $info ) { + if ( preg_match( '!^udp:(?://)?\[([0-9a-fA-F:]+)\]:(\d+)(?:/(.*))?$!', $info, $m ) ) { + // IPv6 bracketed host + $host = $m[1]; + $port = intval( $m[2] ); + $prefix = isset( $m[3] ) ? $m[3] : false; + $domain = AF_INET6; + } elseif ( preg_match( '!^udp:(?://)?([a-zA-Z0-9.-]+):(\d+)(?:/(.*))?$!', $info, $m ) ) { + $host = $m[1]; + if ( !IP::isIPv4( $host ) ) { + $host = gethostbyname( $host ); + } + $port = intval( $m[2] ); + $prefix = isset( $m[3] ) ? $m[3] : false; + $domain = AF_INET; + } else { + throw new InvalidArgumentException( __METHOD__ . ': Invalid UDP specification' ); + } + + return new self( $host, $port, $domain, $prefix ); + } + + /** + * @param string $text + */ + public function emit( $text ) { + // Clean it up for the multiplexer + if ( $this->prefix !== false ) { + $text = preg_replace( '/^/m', $this->prefix . ' ', $text ); + + // Limit to 64KB + if ( strlen( $text ) > 65506 ) { + $text = substr( $text, 0, 65506 ); + } + + if ( substr( $text, -1 ) != "\n" ) { + $text .= "\n"; + } + } elseif ( strlen( $text ) > 65507 ) { + $text = substr( $text, 0, 65507 ); + } + + $sock = socket_create( $this->domain, SOCK_DGRAM, SOL_UDP ); + if ( !$sock ) { // @todo should this throw an exception? + return; + } + + socket_sendto( $sock, $text, strlen( $text ), 0, $this->host, $this->port ); + socket_close( $sock ); + } +} diff --git a/includes/libs/Xhprof.php b/includes/libs/Xhprof.php new file mode 100644 index 00000000..5ed67c73 --- /dev/null +++ b/includes/libs/Xhprof.php @@ -0,0 +1,445 @@ +<?php +/** + * 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 + */ + +/** + * Convenience class for working with XHProf + * <https://github.com/phacility/xhprof>. XHProf can be installed as a PECL + * package for use with PHP5 (Zend PHP) and is built-in to HHVM 3.3.0. + * + * @author Bryan Davis <bd808@wikimedia.org> + * @copyright © 2014 Bryan Davis and Wikimedia Foundation. + * @since 1.25 + */ +class Xhprof { + + /** + * @var array $config + */ + protected $config; + + /** + * Hierarchical profiling data returned by xhprof. + * @var array $hieraData + */ + protected $hieraData; + + /** + * Per-function inclusive data. + * @var array $inclusive + */ + protected $inclusive; + + /** + * Per-function inclusive and exclusive data. + * @var array $complete + */ + protected $complete; + + /** + * Configuration data can contain: + * - flags: Optional flags to add additional information to the + * profiling data collected. + * (XHPROF_FLAGS_NO_BUILTINS, XHPROF_FLAGS_CPU, + * XHPROF_FLAGS_MEMORY) + * - exclude: Array of function names to exclude from profiling. + * - include: Array of function names to include in profiling. + * - sort: Key to sort per-function reports on. + * + * Note: When running under HHVM, xhprof will always behave as though the + * XHPROF_FLAGS_NO_BUILTINS flag has been used unless the + * Eval.JitEnableRenameFunction option is enabled for the HHVM process. + * + * @param array $config + */ + public function __construct( array $config = array() ) { + $this->config = array_merge( + array( + 'flags' => 0, + 'exclude' => array(), + 'include' => null, + 'sort' => 'wt', + ), + $config + ); + + xhprof_enable( $this->config['flags'], array( + 'ignored_functions' => $this->config['exclude'] + ) ); + } + + /** + * Stop collecting profiling data. + * + * Only the first invocation of this method will effect the internal + * object state. Subsequent calls will return the data collected by the + * initial call. + * + * @return array Collected profiling data (possibly cached) + */ + public function stop() { + if ( $this->hieraData === null ) { + $this->hieraData = $this->pruneData( xhprof_disable() ); + } + return $this->hieraData; + } + + /** + * Load raw data from a prior run for analysis. + * Stops any existing data collection and clears internal caches. + * + * Any 'include' filters configured for this Xhprof instance will be + * enforced on the data as it is loaded. 'exclude' filters will however + * not be enforced as they are an XHProf intrinsic behavior. + * + * @param array $data + * @see getRawData() + */ + public function loadRawData( array $data ) { + $this->stop(); + $this->inclusive = null; + $this->complete = null; + $this->hieraData = $this->pruneData( $data ); + } + + /** + * Get raw data collected by xhprof. + * + * If data collection has not been stopped yet this method will halt + * collection to gather the profiling data. + * + * Each key in the returned array is an edge label for the call graph in + * the form "caller==>callee". There is once special case edge labled + * simply "main()" which represents the global scope entry point of the + * application. + * + * XHProf will collect different data depending on the flags that are used: + * - ct: Number of matching events seen. + * - wt: Inclusive elapsed wall time for this event in microseconds. + * - cpu: Inclusive elapsed cpu time for this event in microseconds. + * (XHPROF_FLAGS_CPU) + * - mu: Delta of memory usage from start to end of callee in bytes. + * (XHPROF_FLAGS_MEMORY) + * - pmu: Delta of peak memory usage from start to end of callee in + * bytes. (XHPROF_FLAGS_MEMORY) + * - alloc: Delta of amount memory requested from malloc() by the callee, + * in bytes. (XHPROF_FLAGS_MALLOC) + * - free: Delta of amount of memory passed to free() by the callee, in + * bytes. (XHPROF_FLAGS_MALLOC) + * + * @return array + * @see stop() + * @see getInclusiveMetrics() + * @see getCompleteMetrics() + */ + public function getRawData() { + return $this->stop(); + } + + /** + * Convert an xhprof data key into an array of ['parent', 'child'] + * function names. + * + * The resulting array is left padded with nulls, so a key + * with no parent (eg 'main()') will return [null, 'function']. + * + * @return array + */ + public static function splitKey( $key ) { + return array_pad( explode( '==>', $key, 2 ), -2, null ); + } + + /** + * Remove data for functions that are not included in the 'include' + * configuration array. + * + * @param array $data Raw xhprof data + * @return array + */ + protected function pruneData( $data ) { + if ( !$this->config['include'] ) { + return $data; + } + + $want = array_fill_keys( $this->config['include'], true ); + $want['main()'] = true; + + $keep = array(); + foreach ( $data as $key => $stats ) { + list( $parent, $child ) = self::splitKey( $key ); + if ( isset( $want[$parent] ) || isset( $want[$child] ) ) { + $keep[$key] = $stats; + } + } + return $keep; + } + + /** + * Get the inclusive metrics for each function call. Inclusive metrics + * for given function include the metrics for all functions that were + * called from that function during the measurement period. + * + * If data collection has not been stopped yet this method will halt + * collection to gather the profiling data. + * + * See getRawData() for a description of the metric that are returned for + * each funcition call. The values for the wt, cpu, mu and pmu metrics are + * arrays with these values: + * - total: Cumulative value + * - min: Minimum value + * - mean: Mean (average) value + * - max: Maximum value + * - variance: Variance (spread) of the values + * + * @return array + * @see getRawData() + * @see getCompleteMetrics() + */ + public function getInclusiveMetrics() { + if ( $this->inclusive === null ) { + // Make sure we have data to work with + $this->stop(); + + $main = $this->hieraData['main()']; + $hasCpu = isset( $main['cpu'] ); + $hasMu = isset( $main['mu'] ); + $hasAlloc = isset( $main['alloc'] ); + + $this->inclusive = array(); + foreach ( $this->hieraData as $key => $stats ) { + list( $parent, $child ) = self::splitKey( $key ); + if ( !isset( $this->inclusive[$child] ) ) { + $this->inclusive[$child] = array( + 'ct' => 0, + 'wt' => new RunningStat(), + ); + if ( $hasCpu ) { + $this->inclusive[$child]['cpu'] = new RunningStat(); + } + if ( $hasMu ) { + $this->inclusive[$child]['mu'] = new RunningStat(); + $this->inclusive[$child]['pmu'] = new RunningStat(); + } + if ( $hasAlloc ) { + $this->inclusive[$child]['alloc'] = new RunningStat(); + $this->inclusive[$child]['free'] = new RunningStat(); + } + } + + $this->inclusive[$child]['ct'] += $stats['ct']; + foreach ( $stats as $stat => $value ) { + if ( $stat === 'ct' ) { + continue; + } + + if ( !isset( $this->inclusive[$child][$stat] ) ) { + // Ignore unknown stats + continue; + } + + for ( $i = 0; $i < $stats['ct']; $i++ ) { + $this->inclusive[$child][$stat]->push( + $value / $stats['ct'] + ); + } + } + } + + // Convert RunningStat instances to static arrays and add + // percentage stats. + foreach ( $this->inclusive as $func => $stats ) { + foreach ( $stats as $name => $value ) { + if ( $value instanceof RunningStat ) { + $total = $value->m1 * $value->n; + $percent = ( isset( $main[$name] ) && $main[$name] ) + ? 100 * $total / $main[$name] + : 0; + $this->inclusive[$func][$name] = array( + 'total' => $total, + 'min' => $value->min, + 'mean' => $value->m1, + 'max' => $value->max, + 'variance' => $value->m2, + 'percent' => $percent, + ); + } + } + } + + uasort( $this->inclusive, self::makeSortFunction( + $this->config['sort'], 'total' + ) ); + } + return $this->inclusive; + } + + /** + * Get the inclusive and exclusive metrics for each function call. + * + * If data collection has not been stopped yet this method will halt + * collection to gather the profiling data. + * + * In addition to the normal data contained in the inclusive metrics, the + * metrics have an additional 'exclusive' measurement which is the total + * minus the totals of all child function calls. + * + * @return array + * @see getRawData() + * @see getInclusiveMetrics() + */ + public function getCompleteMetrics() { + if ( $this->complete === null ) { + // Start with inclusive data + $this->complete = $this->getInclusiveMetrics(); + + foreach ( $this->complete as $func => $stats ) { + foreach ( $stats as $stat => $value ) { + if ( $stat === 'ct' ) { + continue; + } + // Initialize exclusive data with inclusive totals + $this->complete[$func][$stat]['exclusive'] = $value['total']; + } + // Add sapce for call tree information to be filled in later + $this->complete[$func]['calls'] = array(); + $this->complete[$func]['subcalls'] = array(); + } + + foreach ( $this->hieraData as $key => $stats ) { + list( $parent, $child ) = self::splitKey( $key ); + if ( $parent !== null ) { + // Track call tree information + $this->complete[$child]['calls'][$parent] = $stats; + $this->complete[$parent]['subcalls'][$child] = $stats; + } + + if ( isset( $this->complete[$parent] ) ) { + // Deduct child inclusive data from exclusive data + foreach ( $stats as $stat => $value ) { + if ( $stat === 'ct' ) { + continue; + } + + if ( !isset( $this->complete[$parent][$stat] ) ) { + // Ignore unknown stats + continue; + } + + $this->complete[$parent][$stat]['exclusive'] -= $value; + } + } + } + + uasort( $this->complete, self::makeSortFunction( + $this->config['sort'], 'exclusive' + ) ); + } + return $this->complete; + } + + /** + * Get a list of all callers of a given function. + * + * @param string $function Function name + * @return array + * @see getEdges() + */ + public function getCallers( $function ) { + $edges = $this->getCompleteMetrics(); + if ( isset( $edges[$function]['calls'] ) ) { + return array_keys( $edges[$function]['calls'] ); + } else { + return array(); + } + } + + /** + * Get a list of all callees from a given function. + * + * @param string $function Function name + * @return array + * @see getEdges() + */ + public function getCallees( $function ) { + $edges = $this->getCompleteMetrics(); + if ( isset( $edges[$function]['subcalls'] ) ) { + return array_keys( $edges[$function]['subcalls'] ); + } else { + return array(); + } + } + + /** + * Find the critical path for the given metric. + * + * @param string $metric Metric to find critical path for + * @return array + */ + public function getCriticalPath( $metric = 'wt' ) { + $this->stop(); + $func = 'main()'; + $path = array( + $func => $this->hieraData[$func], + ); + while ( $func ) { + $callees = $this->getCallees( $func ); + $maxCallee = null; + $maxCall = null; + foreach ( $callees as $callee ) { + $call = "{$func}==>{$callee}"; + if ( $maxCall === null || + $this->hieraData[$call][$metric] > + $this->hieraData[$maxCall][$metric] + ) { + $maxCallee = $callee; + $maxCall = $call; + } + } + if ( $maxCall !== null ) { + $path[$maxCall] = $this->hieraData[$maxCall]; + } + $func = $maxCallee; + } + return $path; + } + + /** + * Make a closure to use as a sort function. The resulting function will + * sort by descending numeric values (largest value first). + * + * @param string $key Data key to sort on + * @param string $sub Sub key to sort array values on + * @return Closure + */ + public static function makeSortFunction( $key, $sub ) { + return function ( $a, $b ) use ( $key, $sub ) { + if ( isset( $a[$key] ) && isset( $b[$key] ) ) { + // Descending sort: larger values will be first in result. + // Assumes all values are numeric. + // Values for 'main()' will not have sub keys + $valA = is_array( $a[$key] ) ? $a[$key][$sub] : $a[$key]; + $valB = is_array( $b[$key] ) ? $b[$key][$sub] : $b[$key]; + return $valB - $valA; + } else { + // Sort datum with the key before those without + return isset( $a[$key] ) ? -1 : 1; + } + }; + } +} diff --git a/includes/libs/XmlTypeCheck.php b/includes/libs/XmlTypeCheck.php index 31a4e28a..6d01986d 100644 --- a/includes/libs/XmlTypeCheck.php +++ b/includes/libs/XmlTypeCheck.php @@ -75,7 +75,7 @@ class XmlTypeCheck { * SAX element handler event. This gives you access to the element * namespace, name, attributes, and text contents. * Filter should return 'true' to toggle on $this->filterMatch - * @param boolean $isFile (optional) indicates if the first parameter is a + * @param bool $isFile (optional) indicates if the first parameter is a * filename (default, true) or if it is a string (false) * @param array $options list of additional parsing options: * processing_instruction_handler: Callback for xml_set_processing_instruction_handler diff --git a/includes/libs/composer/ComposerJson.php b/includes/libs/composer/ComposerJson.php new file mode 100644 index 00000000..796acb56 --- /dev/null +++ b/includes/libs/composer/ComposerJson.php @@ -0,0 +1,54 @@ +<?php + +/** + * Reads a composer.json file and provides accessors to get + * its hash and the required dependencies + * + * @since 1.25 + */ +class ComposerJson { + + /** + * @param string $location + */ + public function __construct( $location ) { + $this->hash = md5_file( $location ); + $this->contents = json_decode( file_get_contents( $location ), true ); + } + + public function getHash() { + return $this->hash; + } + + /** + * Dependencies as specified by composer.json + * + * @return array + */ + public function getRequiredDependencies() { + $deps = array(); + foreach ( $this->contents['require'] as $package => $version ) { + if ( $package !== "php" && strpos( $package, 'ext-' ) !== 0 ) { + $deps[$package] = self::normalizeVersion( $version ); + } + } + + return $deps; + } + + /** + * Strip a leading "v" from the version name + * + * @param string $version + * @return string + */ + public static function normalizeVersion( $version ) { + if ( strpos( $version, 'v' ) === 0 ) { + // Composer auto-strips the "v" in front of the tag name + $version = ltrim( $version, 'v' ); + } + + return $version; + } + +} diff --git a/includes/libs/composer/ComposerLock.php b/includes/libs/composer/ComposerLock.php new file mode 100644 index 00000000..9c7bf2f9 --- /dev/null +++ b/includes/libs/composer/ComposerLock.php @@ -0,0 +1,38 @@ +<?php + +/** + * Reads a composer.lock file and provides accessors to get + * its hash and what is installed + * + * @since 1.25 + */ +class ComposerLock { + + /** + * @param string $location + */ + public function __construct( $location ) { + $this->contents = json_decode( file_get_contents( $location ), true ); + } + + public function getHash() { + return $this->contents['hash']; + } + + /** + * Dependencies currently installed according to composer.lock + * + * @return array + */ + public function getInstalledDependencies() { + $deps = array(); + foreach ( $this->contents['packages'] as $installed ) { + $deps[$installed['name']] = array( + 'version' => ComposerJson::normalizeVersion( $installed['version'] ), + 'type' => $installed['type'], + ); + } + + return $deps; + } +} diff --git a/includes/libs/jsminplus.php b/includes/libs/jsminplus.php index ed0382cf..99cf399b 100644 --- a/includes/libs/jsminplus.php +++ b/includes/libs/jsminplus.php @@ -1017,7 +1017,7 @@ class JSParser case KEYWORD_CATCH: case KEYWORD_FINALLY: - throw $this->t->newSyntaxError($tt + ' without preceding try'); + throw $this->t->newSyntaxError($tt . ' without preceding try'); case KEYWORD_THROW: $n = new JSNode($this->t); diff --git a/includes/libs/lessc.inc.php b/includes/libs/lessc.inc.php deleted file mode 100644 index 61ed771a..00000000 --- a/includes/libs/lessc.inc.php +++ /dev/null @@ -1,3796 +0,0 @@ -<?php -// @codingStandardsIgnoreFile File external to MediaWiki. Ignore coding conventions checks. -/** - * lessphp v0.4.0@2cc77e3c7b - * http://leafo.net/lessphp - * - * LESS CSS compiler, adapted from http://lesscss.org - * - * For ease of distribution, lessphp 0.4.0 is under a dual license. - * You are free to pick which one suits your needs. - * - * MIT LICENSE - * - * Copyright 2013, Leaf Corcoran <leafot@gmail.com> - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be - * included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE - * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION - * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - * - * GPL VERSION 3 - * - * Please refer to http://www.gnu.org/licenses/gpl-3.0.html for the full - * text of the GPL version 3 - */ - - -/** - * The LESS compiler and parser. - * - * Converting LESS to CSS is a three stage process. The incoming file is parsed - * by `lessc_parser` into a syntax tree, then it is compiled into another tree - * representing the CSS structure by `lessc`. The CSS tree is fed into a - * formatter, like `lessc_formatter` which then outputs CSS as a string. - * - * During the first compile, all values are *reduced*, which means that their - * types are brought to the lowest form before being dump as strings. This - * handles math equations, variable dereferences, and the like. - * - * The `parse` function of `lessc` is the entry point. - * - * In summary: - * - * The `lessc` class creates an instance of the parser, feeds it LESS code, - * then transforms the resulting tree to a CSS tree. This class also holds the - * evaluation context, such as all available mixins and variables at any given - * time. - * - * The `lessc_parser` class is only concerned with parsing its input. - * - * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, - * handling things like indentation. - */ -class lessc { - static public $VERSION = "v0.4.0"; - - static public $TRUE = array("keyword", "true"); - static public $FALSE = array("keyword", "false"); - - protected $libFunctions = array(); - protected $registeredVars = array(); - protected $preserveComments = false; - - public $vPrefix = '@'; // prefix of abstract properties - public $mPrefix = '$'; // prefix of abstract blocks - public $parentSelector = '&'; - - public $importDisabled = false; - public $importDir = ''; - - protected $numberPrecision = null; - - protected $allParsedFiles = array(); - - // set to the parser that generated the current line when compiling - // so we know how to create error messages - protected $sourceParser = null; - protected $sourceLoc = null; - - static protected $nextImportId = 0; // uniquely identify imports - - // attempts to find the path of an import url, returns null for css files - protected function findImport($url) { - foreach ((array)$this->importDir as $dir) { - $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url; - if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) { - return $file; - } - } - - return null; - } - - protected function fileExists($name) { - return is_file($name); - } - - static public function compressList($items, $delim) { - if (!isset($items[1]) && isset($items[0])) return $items[0]; - else return array('list', $delim, $items); - } - - static public function preg_quote($what) { - return preg_quote($what, '/'); - } - - protected function tryImport($importPath, $parentBlock, $out) { - if ($importPath[0] == "function" && $importPath[1] == "url") { - $importPath = $this->flattenList($importPath[2]); - } - - $str = $this->coerceString($importPath); - if ($str === null) return false; - - $url = $this->compileValue($this->lib_e($str)); - - // don't import if it ends in css - if (substr_compare($url, '.css', -4, 4) === 0) return false; - - $realPath = $this->findImport($url); - - if ($realPath === null) return false; - - if ($this->importDisabled) { - return array(false, "/* import disabled */"); - } - - if (isset($this->allParsedFiles[realpath($realPath)])) { - return array(false, null); - } - - $this->addParsedFile($realPath); - $parser = $this->makeParser($realPath); - $root = $parser->parse(file_get_contents($realPath)); - - // set the parents of all the block props - foreach ($root->props as $prop) { - if ($prop[0] == "block") { - $prop[1]->parent = $parentBlock; - } - } - - // copy mixins into scope, set their parents - // bring blocks from import into current block - // TODO: need to mark the source parser these came from this file - foreach ($root->children as $childName => $child) { - if (isset($parentBlock->children[$childName])) { - $parentBlock->children[$childName] = array_merge( - $parentBlock->children[$childName], - $child); - } else { - $parentBlock->children[$childName] = $child; - } - } - - $pi = pathinfo($realPath); - $dir = $pi["dirname"]; - - list($top, $bottom) = $this->sortProps($root->props, true); - $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); - - return array(true, $bottom, $parser, $dir); - } - - protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) { - $oldSourceParser = $this->sourceParser; - - $oldImport = $this->importDir; - - // TODO: this is because the importDir api is stupid - $this->importDir = (array)$this->importDir; - array_unshift($this->importDir, $importDir); - - foreach ($props as $prop) { - $this->compileProp($prop, $block, $out); - } - - $this->importDir = $oldImport; - $this->sourceParser = $oldSourceParser; - } - - /** - * Recursively compiles a block. - * - * A block is analogous to a CSS block in most cases. A single LESS document - * is encapsulated in a block when parsed, but it does not have parent tags - * so all of it's children appear on the root level when compiled. - * - * Blocks are made up of props and children. - * - * Props are property instructions, array tuples which describe an action - * to be taken, eg. write a property, set a variable, mixin a block. - * - * The children of a block are just all the blocks that are defined within. - * This is used to look up mixins when performing a mixin. - * - * Compiling the block involves pushing a fresh environment on the stack, - * and iterating through the props, compiling each one. - * - * See lessc::compileProp() - * - */ - protected function compileBlock($block) { - switch ($block->type) { - case "root": - $this->compileRoot($block); - break; - case null: - $this->compileCSSBlock($block); - break; - case "media": - $this->compileMedia($block); - break; - case "directive": - $name = "@" . $block->name; - if (!empty($block->value)) { - $name .= " " . $this->compileValue($this->reduce($block->value)); - } - - $this->compileNestedBlock($block, array($name)); - break; - default: - $this->throwError("unknown block type: $block->type\n"); - } - } - - protected function compileCSSBlock($block) { - $env = $this->pushEnv(); - - $selectors = $this->compileSelectors($block->tags); - $env->selectors = $this->multiplySelectors($selectors); - $out = $this->makeOutputBlock(null, $env->selectors); - - $this->scope->children[] = $out; - $this->compileProps($block, $out); - - $block->scope = $env; // mixins carry scope with them! - $this->popEnv(); - } - - protected function compileMedia($media) { - $env = $this->pushEnv($media); - $parentScope = $this->mediaParent($this->scope); - - $query = $this->compileMediaQuery($this->multiplyMedia($env)); - - $this->scope = $this->makeOutputBlock($media->type, array($query)); - $parentScope->children[] = $this->scope; - - $this->compileProps($media, $this->scope); - - if (count($this->scope->lines) > 0) { - $orphanSelelectors = $this->findClosestSelectors(); - if (!is_null($orphanSelelectors)) { - $orphan = $this->makeOutputBlock(null, $orphanSelelectors); - $orphan->lines = $this->scope->lines; - array_unshift($this->scope->children, $orphan); - $this->scope->lines = array(); - } - } - - $this->scope = $this->scope->parent; - $this->popEnv(); - } - - protected function mediaParent($scope) { - while (!empty($scope->parent)) { - if (!empty($scope->type) && $scope->type != "media") { - break; - } - $scope = $scope->parent; - } - - return $scope; - } - - protected function compileNestedBlock($block, $selectors) { - $this->pushEnv($block); - $this->scope = $this->makeOutputBlock($block->type, $selectors); - $this->scope->parent->children[] = $this->scope; - - $this->compileProps($block, $this->scope); - - $this->scope = $this->scope->parent; - $this->popEnv(); - } - - protected function compileRoot($root) { - $this->pushEnv(); - $this->scope = $this->makeOutputBlock($root->type); - $this->compileProps($root, $this->scope); - $this->popEnv(); - } - - protected function compileProps($block, $out) { - foreach ($this->sortProps($block->props) as $prop) { - $this->compileProp($prop, $block, $out); - } - $out->lines = $this->deduplicate($out->lines); - } - - /** - * Deduplicate lines in a block. Comments are not deduplicated. If a - * duplicate rule is detected, the comments immediately preceding each - * occurence are consolidated. - */ - protected function deduplicate($lines) { - $unique = array(); - $comments = array(); - - foreach($lines as $line) { - if (strpos($line, '/*') === 0) { - $comments[] = $line; - continue; - } - if (!in_array($line, $unique)) { - $unique[] = $line; - } - array_splice($unique, array_search($line, $unique), 0, $comments); - $comments = array(); - } - return array_merge($unique, $comments); - } - - protected function sortProps($props, $split = false) { - $vars = array(); - $imports = array(); - $other = array(); - $stack = array(); - - foreach ($props as $prop) { - switch ($prop[0]) { - case "comment": - $stack[] = $prop; - break; - case "assign": - $stack[] = $prop; - if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { - $vars = array_merge($vars, $stack); - } else { - $other = array_merge($other, $stack); - } - $stack = array(); - break; - case "import": - $id = self::$nextImportId++; - $prop[] = $id; - $stack[] = $prop; - $imports = array_merge($imports, $stack); - $other[] = array("import_mixin", $id); - $stack = array(); - break; - default: - $stack[] = $prop; - $other = array_merge($other, $stack); - $stack = array(); - break; - } - } - $other = array_merge($other, $stack); - - if ($split) { - return array(array_merge($vars, $imports), $other); - } else { - return array_merge($vars, $imports, $other); - } - } - - protected function compileMediaQuery($queries) { - $compiledQueries = array(); - foreach ($queries as $query) { - $parts = array(); - foreach ($query as $q) { - switch ($q[0]) { - case "mediaType": - $parts[] = implode(" ", array_slice($q, 1)); - break; - case "mediaExp": - if (isset($q[2])) { - $parts[] = "($q[1]: " . - $this->compileValue($this->reduce($q[2])) . ")"; - } else { - $parts[] = "($q[1])"; - } - break; - case "variable": - $parts[] = $this->compileValue($this->reduce($q)); - break; - } - } - - if (count($parts) > 0) { - $compiledQueries[] = implode(" and ", $parts); - } - } - - $out = "@media"; - if (!empty($parts)) { - $out .= " " . - implode($this->formatter->selectorSeparator, $compiledQueries); - } - return $out; - } - - protected function multiplyMedia($env, $childQueries = null) { - if (is_null($env) || - !empty($env->block->type) && $env->block->type != "media") - { - return $childQueries; - } - - // plain old block, skip - if (empty($env->block->type)) { - return $this->multiplyMedia($env->parent, $childQueries); - } - - $out = array(); - $queries = $env->block->queries; - if (is_null($childQueries)) { - $out = $queries; - } else { - foreach ($queries as $parent) { - foreach ($childQueries as $child) { - $out[] = array_merge($parent, $child); - } - } - } - - return $this->multiplyMedia($env->parent, $out); - } - - protected function expandParentSelectors(&$tag, $replace) { - $parts = explode("$&$", $tag); - $count = 0; - foreach ($parts as &$part) { - $part = str_replace($this->parentSelector, $replace, $part, $c); - $count += $c; - } - $tag = implode($this->parentSelector, $parts); - return $count; - } - - protected function findClosestSelectors() { - $env = $this->env; - $selectors = null; - while ($env !== null) { - if (isset($env->selectors)) { - $selectors = $env->selectors; - break; - } - $env = $env->parent; - } - - return $selectors; - } - - - // multiply $selectors against the nearest selectors in env - protected function multiplySelectors($selectors) { - // find parent selectors - - $parentSelectors = $this->findClosestSelectors(); - if (is_null($parentSelectors)) { - // kill parent reference in top level selector - foreach ($selectors as &$s) { - $this->expandParentSelectors($s, ""); - } - - return $selectors; - } - - $out = array(); - foreach ($parentSelectors as $parent) { - foreach ($selectors as $child) { - $count = $this->expandParentSelectors($child, $parent); - - // don't prepend the parent tag if & was used - if ($count > 0) { - $out[] = trim($child); - } else { - $out[] = trim($parent . ' ' . $child); - } - } - } - - return $out; - } - - // reduces selector expressions - protected function compileSelectors($selectors) { - $out = array(); - - foreach ($selectors as $s) { - if (is_array($s)) { - list(, $value) = $s; - $out[] = trim($this->compileValue($this->reduce($value))); - } else { - $out[] = $s; - } - } - - return $out; - } - - protected function eq($left, $right) { - return $left == $right; - } - - protected function patternMatch($block, $orderedArgs, $keywordArgs) { - // match the guards if it has them - // any one of the groups must have all its guards pass for a match - if (!empty($block->guards)) { - $groupPassed = false; - foreach ($block->guards as $guardGroup) { - foreach ($guardGroup as $guard) { - $this->pushEnv(); - $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); - - $negate = false; - if ($guard[0] == "negate") { - $guard = $guard[1]; - $negate = true; - } - - $passed = $this->reduce($guard) == self::$TRUE; - if ($negate) $passed = !$passed; - - $this->popEnv(); - - if ($passed) { - $groupPassed = true; - } else { - $groupPassed = false; - break; - } - } - - if ($groupPassed) break; - } - - if (!$groupPassed) { - return false; - } - } - - if (empty($block->args)) { - return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); - } - - $remainingArgs = $block->args; - if ($keywordArgs) { - $remainingArgs = array(); - foreach ($block->args as $arg) { - if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { - continue; - } - - $remainingArgs[] = $arg; - } - } - - $i = -1; // no args - // try to match by arity or by argument literal - foreach ($remainingArgs as $i => $arg) { - switch ($arg[0]) { - case "lit": - if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { - return false; - } - break; - case "arg": - // no arg and no default value - if (!isset($orderedArgs[$i]) && !isset($arg[2])) { - return false; - } - break; - case "rest": - $i--; // rest can be empty - break 2; - } - } - - if ($block->isVararg) { - return true; // not having enough is handled above - } else { - $numMatched = $i + 1; - // greater than becuase default values always match - return $numMatched >= count($orderedArgs); - } - } - - protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { - $matches = null; - foreach ($blocks as $block) { - // skip seen blocks that don't have arguments - if (isset($skip[$block->id]) && !isset($block->args)) { - continue; - } - - if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { - $matches[] = $block; - } - } - - return $matches; - } - - // attempt to find blocks matched by path and args - protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) { - if ($searchIn == null) return null; - if (isset($seen[$searchIn->id])) return null; - $seen[$searchIn->id] = true; - - $name = $path[0]; - - if (isset($searchIn->children[$name])) { - $blocks = $searchIn->children[$name]; - if (count($path) == 1) { - $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); - if (!empty($matches)) { - // This will return all blocks that match in the closest - // scope that has any matching block, like lessjs - return $matches; - } - } else { - $matches = array(); - foreach ($blocks as $subBlock) { - $subMatches = $this->findBlocks($subBlock, - array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); - - if (!is_null($subMatches)) { - foreach ($subMatches as $sm) { - $matches[] = $sm; - } - } - } - - return count($matches) > 0 ? $matches : null; - } - } - if ($searchIn->parent === $searchIn) return null; - return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); - } - - // sets all argument names in $args to either the default value - // or the one passed in through $values - protected function zipSetArgs($args, $orderedValues, $keywordValues) { - $assignedValues = array(); - - $i = 0; - foreach ($args as $a) { - if ($a[0] == "arg") { - if (isset($keywordValues[$a[1]])) { - // has keyword arg - $value = $keywordValues[$a[1]]; - } elseif (isset($orderedValues[$i])) { - // has ordered arg - $value = $orderedValues[$i]; - $i++; - } elseif (isset($a[2])) { - // has default value - $value = $a[2]; - } else { - $this->throwError("Failed to assign arg " . $a[1]); - $value = null; // :( - } - - $value = $this->reduce($value); - $this->set($a[1], $value); - $assignedValues[] = $value; - } else { - // a lit - $i++; - } - } - - // check for a rest - $last = end($args); - if ($last[0] == "rest") { - $rest = array_slice($orderedValues, count($args) - 1); - $this->set($last[1], $this->reduce(array("list", " ", $rest))); - } - - // wow is this the only true use of PHP's + operator for arrays? - $this->env->arguments = $assignedValues + $orderedValues; - } - - // compile a prop and update $lines or $blocks appropriately - protected function compileProp($prop, $block, $out) { - // set error position context - $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; - - switch ($prop[0]) { - case 'assign': - list(, $name, $value) = $prop; - if ($name[0] == $this->vPrefix) { - $this->set($name, $value); - } else { - $out->lines[] = $this->formatter->property($name, - $this->compileValue($this->reduce($value))); - } - break; - case 'block': - list(, $child) = $prop; - $this->compileBlock($child); - break; - case 'mixin': - list(, $path, $args, $suffix) = $prop; - - $orderedArgs = array(); - $keywordArgs = array(); - foreach ((array)$args as $arg) { - $argval = null; - switch ($arg[0]) { - case "arg": - if (!isset($arg[2])) { - $orderedArgs[] = $this->reduce(array("variable", $arg[1])); - } else { - $keywordArgs[$arg[1]] = $this->reduce($arg[2]); - } - break; - - case "lit": - $orderedArgs[] = $this->reduce($arg[1]); - break; - default: - $this->throwError("Unknown arg type: " . $arg[0]); - } - } - - $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); - - if ($mixins === null) { - $this->throwError("{$prop[1][0]} is undefined"); - } - - foreach ($mixins as $mixin) { - if ($mixin === $block && !$orderedArgs) { - continue; - } - - $haveScope = false; - if (isset($mixin->parent->scope)) { - $haveScope = true; - $mixinParentEnv = $this->pushEnv(); - $mixinParentEnv->storeParent = $mixin->parent->scope; - } - - $haveArgs = false; - if (isset($mixin->args)) { - $haveArgs = true; - $this->pushEnv(); - $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); - } - - $oldParent = $mixin->parent; - if ($mixin != $block) $mixin->parent = $block; - - foreach ($this->sortProps($mixin->props) as $subProp) { - if ($suffix !== null && - $subProp[0] == "assign" && - is_string($subProp[1]) && - $subProp[1]{0} != $this->vPrefix) - { - $subProp[2] = array( - 'list', ' ', - array($subProp[2], array('keyword', $suffix)) - ); - } - - $this->compileProp($subProp, $mixin, $out); - } - - $mixin->parent = $oldParent; - - if ($haveArgs) $this->popEnv(); - if ($haveScope) $this->popEnv(); - } - - break; - case 'raw': - $out->lines[] = $prop[1]; - break; - case "directive": - list(, $name, $value) = $prop; - $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; - break; - case "comment": - $out->lines[] = $prop[1]; - break; - case "import"; - list(, $importPath, $importId) = $prop; - $importPath = $this->reduce($importPath); - - if (!isset($this->env->imports)) { - $this->env->imports = array(); - } - - $result = $this->tryImport($importPath, $block, $out); - - $this->env->imports[$importId] = $result === false ? - array(false, "@import " . $this->compileValue($importPath).";") : - $result; - - break; - case "import_mixin": - list(,$importId) = $prop; - $import = $this->env->imports[$importId]; - if ($import[0] === false) { - if (isset($import[1])) { - $out->lines[] = $import[1]; - } - } else { - list(, $bottom, $parser, $importDir) = $import; - $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); - } - - break; - default: - $this->throwError("unknown op: {$prop[0]}\n"); - } - } - - - /** - * Compiles a primitive value into a CSS property value. - * - * Values in lessphp are typed by being wrapped in arrays, their format is - * typically: - * - * array(type, contents [, additional_contents]*) - * - * The input is expected to be reduced. This function will not work on - * things like expressions and variables. - */ - public function compileValue($value) { - switch ($value[0]) { - case 'list': - // [1] - delimiter - // [2] - array of values - return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); - case 'raw_color': - if (!empty($this->formatter->compressColors)) { - return $this->compileValue($this->coerceColor($value)); - } - return $value[1]; - case 'keyword': - // [1] - the keyword - return $value[1]; - case 'number': - list(, $num, $unit) = $value; - // [1] - the number - // [2] - the unit - if ($this->numberPrecision !== null) { - $num = round($num, $this->numberPrecision); - } - return $num . $unit; - case 'string': - // [1] - contents of string (includes quotes) - list(, $delim, $content) = $value; - foreach ($content as &$part) { - if (is_array($part)) { - $part = $this->compileValue($part); - } - } - return $delim . implode($content) . $delim; - case 'color': - // [1] - red component (either number or a %) - // [2] - green component - // [3] - blue component - // [4] - optional alpha component - list(, $r, $g, $b) = $value; - $r = round($r); - $g = round($g); - $b = round($b); - - if (count($value) == 5 && $value[4] != 1) { // rgba - return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; - } - - $h = sprintf("#%02x%02x%02x", $r, $g, $b); - - if (!empty($this->formatter->compressColors)) { - // Converting hex color to short notation (e.g. #003399 to #039) - if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { - $h = '#' . $h[1] . $h[3] . $h[5]; - } - } - - return $h; - - case 'function': - list(, $name, $args) = $value; - return $name.'('.$this->compileValue($args).')'; - default: // assumed to be unit - $this->throwError("unknown value type: $value[0]"); - } - } - - protected function lib_pow($args) { - list($base, $exp) = $this->assertArgs($args, 2, "pow"); - return pow($this->assertNumber($base), $this->assertNumber($exp)); - } - - protected function lib_pi() { - return pi(); - } - - protected function lib_mod($args) { - list($a, $b) = $this->assertArgs($args, 2, "mod"); - return $this->assertNumber($a) % $this->assertNumber($b); - } - - protected function lib_tan($num) { - return tan($this->assertNumber($num)); - } - - protected function lib_sin($num) { - return sin($this->assertNumber($num)); - } - - protected function lib_cos($num) { - return cos($this->assertNumber($num)); - } - - protected function lib_atan($num) { - $num = atan($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_asin($num) { - $num = asin($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_acos($num) { - $num = acos($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_sqrt($num) { - return sqrt($this->assertNumber($num)); - } - - protected function lib_extract($value) { - list($list, $idx) = $this->assertArgs($value, 2, "extract"); - $idx = $this->assertNumber($idx); - // 1 indexed - if ($list[0] == "list" && isset($list[2][$idx - 1])) { - return $list[2][$idx - 1]; - } - } - - protected function lib_isnumber($value) { - return $this->toBool($value[0] == "number"); - } - - protected function lib_isstring($value) { - return $this->toBool($value[0] == "string"); - } - - protected function lib_iscolor($value) { - return $this->toBool($this->coerceColor($value)); - } - - protected function lib_iskeyword($value) { - return $this->toBool($value[0] == "keyword"); - } - - protected function lib_ispixel($value) { - return $this->toBool($value[0] == "number" && $value[2] == "px"); - } - - protected function lib_ispercentage($value) { - return $this->toBool($value[0] == "number" && $value[2] == "%"); - } - - protected function lib_isem($value) { - return $this->toBool($value[0] == "number" && $value[2] == "em"); - } - - protected function lib_isrem($value) { - return $this->toBool($value[0] == "number" && $value[2] == "rem"); - } - - protected function lib_rgbahex($color) { - $color = $this->coerceColor($color); - if (is_null($color)) - $this->throwError("color expected for rgbahex"); - - return sprintf("#%02x%02x%02x%02x", - isset($color[4]) ? $color[4]*255 : 255, - $color[1],$color[2], $color[3]); - } - - protected function lib_argb($color){ - return $this->lib_rgbahex($color); - } - - /** - * Given an url, decide whether to output a regular link or the base64-encoded contents of the file - * - * @param array $value either an argument list (two strings) or a single string - * @return string formatted url(), either as a link or base64-encoded - */ - protected function lib_data_uri($value) { - $mime = ($value[0] === 'list') ? $value[2][0][2] : null; - $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; - - $fullpath = $this->findImport($url); - - if($fullpath && ($fsize = filesize($fullpath)) !== false) { - // IE8 can't handle data uris larger than 32KB - if($fsize/1024 < 32) { - if(is_null($mime)) { - if(class_exists('finfo')) { // php 5.3+ - $finfo = new finfo(FILEINFO_MIME); - $mime = explode('; ', $finfo->file($fullpath)); - $mime = $mime[0]; - } elseif(function_exists('mime_content_type')) { // PHP 5.2 - $mime = mime_content_type($fullpath); - } - } - - if(!is_null($mime)) // fallback if the MIME type is still unknown - $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); - } - } - - return 'url("'.$url.'")'; - } - - // utility func to unquote a string - protected function lib_e($arg) { - switch ($arg[0]) { - case "list": - $items = $arg[2]; - if (isset($items[0])) { - return $this->lib_e($items[0]); - } - $this->throwError("unrecognised input"); - case "string": - $arg[1] = ""; - return $arg; - case "keyword": - return $arg; - default: - return array("keyword", $this->compileValue($arg)); - } - } - - protected function lib__sprintf($args) { - if ($args[0] != "list") return $args; - $values = $args[2]; - $string = array_shift($values); - $template = $this->compileValue($this->lib_e($string)); - - $i = 0; - if (preg_match_all('/%[dsa]/', $template, $m)) { - foreach ($m[0] as $match) { - $val = isset($values[$i]) ? - $this->reduce($values[$i]) : array('keyword', ''); - - // lessjs compat, renders fully expanded color, not raw color - if ($color = $this->coerceColor($val)) { - $val = $color; - } - - $i++; - $rep = $this->compileValue($this->lib_e($val)); - $template = preg_replace('/'.self::preg_quote($match).'/', - $rep, $template, 1); - } - } - - $d = $string[0] == "string" ? $string[1] : '"'; - return array("string", $d, array($template)); - } - - protected function lib_floor($arg) { - $value = $this->assertNumber($arg); - return array("number", floor($value), $arg[2]); - } - - protected function lib_ceil($arg) { - $value = $this->assertNumber($arg); - return array("number", ceil($value), $arg[2]); - } - - protected function lib_round($arg) { - if($arg[0] != "list") { - $value = $this->assertNumber($arg); - return array("number", round($value), $arg[2]); - } else { - $value = $this->assertNumber($arg[2][0]); - $precision = $this->assertNumber($arg[2][1]); - return array("number", round($value, $precision), $arg[2][0][2]); - } - } - - protected function lib_unit($arg) { - if ($arg[0] == "list") { - list($number, $newUnit) = $arg[2]; - return array("number", $this->assertNumber($number), - $this->compileValue($this->lib_e($newUnit))); - } else { - return array("number", $this->assertNumber($arg), ""); - } - } - - /** - * Helper function to get arguments for color manipulation functions. - * takes a list that contains a color like thing and a percentage - */ - public function colorArgs($args) { - if ($args[0] != 'list' || count($args[2]) < 2) { - return array(array('color', 0, 0, 0), 0); - } - list($color, $delta) = $args[2]; - $color = $this->assertColor($color); - $delta = floatval($delta[1]); - - return array($color, $delta); - } - - protected function lib_darken($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[3] = $this->clamp($hsl[3] - $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_lighten($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[3] = $this->clamp($hsl[3] + $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_saturate($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[2] = $this->clamp($hsl[2] + $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_desaturate($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[2] = $this->clamp($hsl[2] - $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_spin($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - - $hsl[1] = $hsl[1] + $delta % 360; - if ($hsl[1] < 0) $hsl[1] += 360; - - return $this->toRGB($hsl); - } - - protected function lib_fadeout($args) { - list($color, $delta) = $this->colorArgs($args); - $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); - return $color; - } - - protected function lib_fadein($args) { - list($color, $delta) = $this->colorArgs($args); - $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); - return $color; - } - - protected function lib_hue($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[1]); - } - - protected function lib_saturation($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[2]); - } - - protected function lib_lightness($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[3]); - } - - // get the alpha of a color - // defaults to 1 for non-colors or colors without an alpha - protected function lib_alpha($value) { - if (!is_null($color = $this->coerceColor($value))) { - return isset($color[4]) ? $color[4] : 1; - } - } - - // set the alpha of the color - protected function lib_fade($args) { - list($color, $alpha) = $this->colorArgs($args); - $color[4] = $this->clamp($alpha / 100.0); - return $color; - } - - protected function lib_percentage($arg) { - $num = $this->assertNumber($arg); - return array("number", $num*100, "%"); - } - - // mixes two colors by weight - // mix(@color1, @color2, [@weight: 50%]); - // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method - protected function lib_mix($args) { - if ($args[0] != "list" || count($args[2]) < 2) - $this->throwError("mix expects (color1, color2, weight)"); - - list($first, $second) = $args[2]; - $first = $this->assertColor($first); - $second = $this->assertColor($second); - - $first_a = $this->lib_alpha($first); - $second_a = $this->lib_alpha($second); - - if (isset($args[2][2])) { - $weight = $args[2][2][1] / 100.0; - } else { - $weight = 0.5; - } - - $w = $weight * 2 - 1; - $a = $first_a - $second_a; - - $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; - $w2 = 1.0 - $w1; - - $new = array('color', - $w1 * $first[1] + $w2 * $second[1], - $w1 * $first[2] + $w2 * $second[2], - $w1 * $first[3] + $w2 * $second[3], - ); - - if ($first_a != 1.0 || $second_a != 1.0) { - $new[] = $first_a * $weight + $second_a * ($weight - 1); - } - - return $this->fixColor($new); - } - - protected function lib_contrast($args) { - $darkColor = array('color', 0, 0, 0); - $lightColor = array('color', 255, 255, 255); - $threshold = 0.43; - - if ( $args[0] == 'list' ) { - $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor; - $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor; - $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor; - $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold; - } - else { - $inputColor = $this->assertColor($args); - } - - $inputColor = $this->coerceColor($inputColor); - $darkColor = $this->coerceColor($darkColor); - $lightColor = $this->coerceColor($lightColor); - - //Figure out which is actually light and dark! - if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) { - $t = $lightColor; - $lightColor = $darkColor; - $darkColor = $t; - } - - $inputColor_alpha = $this->lib_alpha($inputColor); - if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) { - return $lightColor; - } - return $darkColor; - } - - protected function lib_luma($color) { - $color = $this->coerceColor($color); - return (0.2126 * $color[0] / 255) + (0.7152 * $color[1] / 255) + (0.0722 * $color[2] / 255); - } - - - public function assertColor($value, $error = "expected color value") { - $color = $this->coerceColor($value); - if (is_null($color)) $this->throwError($error); - return $color; - } - - public function assertNumber($value, $error = "expecting number") { - if ($value[0] == "number") return $value[1]; - $this->throwError($error); - } - - public function assertArgs($value, $expectedArgs, $name="") { - if ($expectedArgs == 1) { - return $value; - } else { - if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); - $values = $value[2]; - $numValues = count($values); - if ($expectedArgs != $numValues) { - if ($name) { - $name = $name . ": "; - } - - $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); - } - - return $values; - } - } - - protected function toHSL($color) { - if ($color[0] == 'hsl') return $color; - - $r = $color[1] / 255; - $g = $color[2] / 255; - $b = $color[3] / 255; - - $min = min($r, $g, $b); - $max = max($r, $g, $b); - - $L = ($min + $max) / 2; - if ($min == $max) { - $S = $H = 0; - } else { - if ($L < 0.5) - $S = ($max - $min)/($max + $min); - else - $S = ($max - $min)/(2.0 - $max - $min); - - if ($r == $max) $H = ($g - $b)/($max - $min); - elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); - elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); - - } - - $out = array('hsl', - ($H < 0 ? $H + 6 : $H)*60, - $S*100, - $L*100, - ); - - if (count($color) > 4) $out[] = $color[4]; // copy alpha - return $out; - } - - protected function toRGB_helper($comp, $temp1, $temp2) { - if ($comp < 0) $comp += 1.0; - elseif ($comp > 1) $comp -= 1.0; - - if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; - if (2 * $comp < 1) return $temp2; - if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; - - return $temp1; - } - - /** - * Converts a hsl array into a color value in rgb. - * Expects H to be in range of 0 to 360, S and L in 0 to 100 - */ - protected function toRGB($color) { - if ($color[0] == 'color') return $color; - - $H = $color[1] / 360; - $S = $color[2] / 100; - $L = $color[3] / 100; - - if ($S == 0) { - $r = $g = $b = $L; - } else { - $temp2 = $L < 0.5 ? - $L*(1.0 + $S) : - $L + $S - $L * $S; - - $temp1 = 2.0 * $L - $temp2; - - $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); - $g = $this->toRGB_helper($H, $temp1, $temp2); - $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); - } - - // $out = array('color', round($r*255), round($g*255), round($b*255)); - $out = array('color', $r*255, $g*255, $b*255); - if (count($color) > 4) $out[] = $color[4]; // copy alpha - return $out; - } - - protected function clamp($v, $max = 1, $min = 0) { - return min($max, max($min, $v)); - } - - /** - * Convert the rgb, rgba, hsl color literals of function type - * as returned by the parser into values of color type. - */ - protected function funcToColor($func) { - $fname = $func[1]; - if ($func[2][0] != 'list') return false; // need a list of arguments - $rawComponents = $func[2][2]; - - if ($fname == 'hsl' || $fname == 'hsla') { - $hsl = array('hsl'); - $i = 0; - foreach ($rawComponents as $c) { - $val = $this->reduce($c); - $val = isset($val[1]) ? floatval($val[1]) : 0; - - if ($i == 0) $clamp = 360; - elseif ($i < 3) $clamp = 100; - else $clamp = 1; - - $hsl[] = $this->clamp($val, $clamp); - $i++; - } - - while (count($hsl) < 4) $hsl[] = 0; - return $this->toRGB($hsl); - - } elseif ($fname == 'rgb' || $fname == 'rgba') { - $components = array(); - $i = 1; - foreach ($rawComponents as $c) { - $c = $this->reduce($c); - if ($i < 4) { - if ($c[0] == "number" && $c[2] == "%") { - $components[] = 255 * ($c[1] / 100); - } else { - $components[] = floatval($c[1]); - } - } elseif ($i == 4) { - if ($c[0] == "number" && $c[2] == "%") { - $components[] = 1.0 * ($c[1] / 100); - } else { - $components[] = floatval($c[1]); - } - } else break; - - $i++; - } - while (count($components) < 3) $components[] = 0; - array_unshift($components, 'color'); - return $this->fixColor($components); - } - - return false; - } - - protected function reduce($value, $forExpression = false) { - switch ($value[0]) { - case "interpolate": - $reduced = $this->reduce($value[1]); - $var = $this->compileValue($reduced); - $res = $this->reduce(array("variable", $this->vPrefix . $var)); - - if ($res[0] == "raw_color") { - $res = $this->coerceColor($res); - } - - if (empty($value[2])) $res = $this->lib_e($res); - - return $res; - case "variable": - $key = $value[1]; - if (is_array($key)) { - $key = $this->reduce($key); - $key = $this->vPrefix . $this->compileValue($this->lib_e($key)); - } - - $seen =& $this->env->seenNames; - - if (!empty($seen[$key])) { - $this->throwError("infinite loop detected: $key"); - } - - $seen[$key] = true; - $out = $this->reduce($this->get($key)); - $seen[$key] = false; - return $out; - case "list": - foreach ($value[2] as &$item) { - $item = $this->reduce($item, $forExpression); - } - return $value; - case "expression": - return $this->evaluate($value); - case "string": - foreach ($value[2] as &$part) { - if (is_array($part)) { - $strip = $part[0] == "variable"; - $part = $this->reduce($part); - if ($strip) $part = $this->lib_e($part); - } - } - return $value; - case "escape": - list(,$inner) = $value; - return $this->lib_e($this->reduce($inner)); - case "function": - $color = $this->funcToColor($value); - if ($color) return $color; - - list(, $name, $args) = $value; - if ($name == "%") $name = "_sprintf"; - - $f = isset($this->libFunctions[$name]) ? - $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name)); - - if (is_callable($f)) { - if ($args[0] == 'list') - $args = self::compressList($args[2], $args[1]); - - $ret = call_user_func($f, $this->reduce($args, true), $this); - - if (is_null($ret)) { - return array("string", "", array( - $name, "(", $args, ")" - )); - } - - // convert to a typed value if the result is a php primitive - if (is_numeric($ret)) $ret = array('number', $ret, ""); - elseif (!is_array($ret)) $ret = array('keyword', $ret); - - return $ret; - } - - // plain function, reduce args - $value[2] = $this->reduce($value[2]); - return $value; - case "unary": - list(, $op, $exp) = $value; - $exp = $this->reduce($exp); - - if ($exp[0] == "number") { - switch ($op) { - case "+": - return $exp; - case "-": - $exp[1] *= -1; - return $exp; - } - } - return array("string", "", array($op, $exp)); - } - - if ($forExpression) { - switch ($value[0]) { - case "keyword": - if ($color = $this->coerceColor($value)) { - return $color; - } - break; - case "raw_color": - return $this->coerceColor($value); - } - } - - return $value; - } - - - // coerce a value for use in color operation - protected function coerceColor($value) { - switch($value[0]) { - case 'color': return $value; - case 'raw_color': - $c = array("color", 0, 0, 0); - $colorStr = substr($value[1], 1); - $num = hexdec($colorStr); - $width = strlen($colorStr) == 3 ? 16 : 256; - - for ($i = 3; $i > 0; $i--) { // 3 2 1 - $t = $num % $width; - $num /= $width; - - $c[$i] = $t * (256/$width) + $t * floor(16/$width); - } - - return $c; - case 'keyword': - $name = $value[1]; - if (isset(self::$cssColors[$name])) { - $rgba = explode(',', self::$cssColors[$name]); - - if(isset($rgba[3])) - return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); - - return array('color', $rgba[0], $rgba[1], $rgba[2]); - } - return null; - } - } - - // make something string like into a string - protected function coerceString($value) { - switch ($value[0]) { - case "string": - return $value; - case "keyword": - return array("string", "", array($value[1])); - } - return null; - } - - // turn list of length 1 into value type - protected function flattenList($value) { - if ($value[0] == "list" && count($value[2]) == 1) { - return $this->flattenList($value[2][0]); - } - return $value; - } - - public function toBool($a) { - if ($a) return self::$TRUE; - else return self::$FALSE; - } - - // evaluate an expression - protected function evaluate($exp) { - list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; - - $left = $this->reduce($left, true); - $right = $this->reduce($right, true); - - if ($leftColor = $this->coerceColor($left)) { - $left = $leftColor; - } - - if ($rightColor = $this->coerceColor($right)) { - $right = $rightColor; - } - - $ltype = $left[0]; - $rtype = $right[0]; - - // operators that work on all types - if ($op == "and") { - return $this->toBool($left == self::$TRUE && $right == self::$TRUE); - } - - if ($op == "=") { - return $this->toBool($this->eq($left, $right) ); - } - - if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { - return $str; - } - - // type based operators - $fname = "op_${ltype}_${rtype}"; - if (is_callable(array($this, $fname))) { - $out = $this->$fname($op, $left, $right); - if (!is_null($out)) return $out; - } - - // make the expression look it did before being parsed - $paddedOp = $op; - if ($whiteBefore) $paddedOp = " " . $paddedOp; - if ($whiteAfter) $paddedOp .= " "; - - return array("string", "", array($left, $paddedOp, $right)); - } - - protected function stringConcatenate($left, $right) { - if ($strLeft = $this->coerceString($left)) { - if ($right[0] == "string") { - $right[1] = ""; - } - $strLeft[2][] = $right; - return $strLeft; - } - - if ($strRight = $this->coerceString($right)) { - array_unshift($strRight[2], $left); - return $strRight; - } - } - - - // make sure a color's components don't go out of bounds - protected function fixColor($c) { - foreach (range(1, 3) as $i) { - if ($c[$i] < 0) $c[$i] = 0; - if ($c[$i] > 255) $c[$i] = 255; - } - - return $c; - } - - protected function op_number_color($op, $lft, $rgt) { - if ($op == '+' || $op == '*') { - return $this->op_color_number($op, $rgt, $lft); - } - } - - protected function op_color_number($op, $lft, $rgt) { - if ($rgt[0] == '%') $rgt[1] /= 100; - - return $this->op_color_color($op, $lft, - array_fill(1, count($lft) - 1, $rgt[1])); - } - - protected function op_color_color($op, $left, $right) { - $out = array('color'); - $max = count($left) > count($right) ? count($left) : count($right); - foreach (range(1, $max - 1) as $i) { - $lval = isset($left[$i]) ? $left[$i] : 0; - $rval = isset($right[$i]) ? $right[$i] : 0; - switch ($op) { - case '+': - $out[] = $lval + $rval; - break; - case '-': - $out[] = $lval - $rval; - break; - case '*': - $out[] = $lval * $rval; - break; - case '%': - $out[] = $lval % $rval; - break; - case '/': - if ($rval == 0) $this->throwError("evaluate error: can't divide by zero"); - $out[] = $lval / $rval; - break; - default: - $this->throwError('evaluate error: color op number failed on op '.$op); - } - } - return $this->fixColor($out); - } - - function lib_red($color){ - $color = $this->coerceColor($color); - if (is_null($color)) { - $this->throwError('color expected for red()'); - } - - return $color[1]; - } - - function lib_green($color){ - $color = $this->coerceColor($color); - if (is_null($color)) { - $this->throwError('color expected for green()'); - } - - return $color[2]; - } - - function lib_blue($color){ - $color = $this->coerceColor($color); - if (is_null($color)) { - $this->throwError('color expected for blue()'); - } - - return $color[3]; - } - - - // operator on two numbers - protected function op_number_number($op, $left, $right) { - $unit = empty($left[2]) ? $right[2] : $left[2]; - - $value = 0; - switch ($op) { - case '+': - $value = $left[1] + $right[1]; - break; - case '*': - $value = $left[1] * $right[1]; - break; - case '-': - $value = $left[1] - $right[1]; - break; - case '%': - $value = $left[1] % $right[1]; - break; - case '/': - if ($right[1] == 0) $this->throwError('parse error: divide by zero'); - $value = $left[1] / $right[1]; - break; - case '<': - return $this->toBool($left[1] < $right[1]); - case '>': - return $this->toBool($left[1] > $right[1]); - case '>=': - return $this->toBool($left[1] >= $right[1]); - case '=<': - return $this->toBool($left[1] <= $right[1]); - default: - $this->throwError('parse error: unknown number operator: '.$op); - } - - return array("number", $value, $unit); - } - - - /* environment functions */ - - protected function makeOutputBlock($type, $selectors = null) { - $b = new stdclass; - $b->lines = array(); - $b->children = array(); - $b->selectors = $selectors; - $b->type = $type; - $b->parent = $this->scope; - return $b; - } - - // the state of execution - protected function pushEnv($block = null) { - $e = new stdclass; - $e->parent = $this->env; - $e->store = array(); - $e->block = $block; - - $this->env = $e; - return $e; - } - - // pop something off the stack - protected function popEnv() { - $old = $this->env; - $this->env = $this->env->parent; - return $old; - } - - // set something in the current env - protected function set($name, $value) { - $this->env->store[$name] = $value; - } - - - // get the highest occurrence entry for a name - protected function get($name) { - $current = $this->env; - - $isArguments = $name == $this->vPrefix . 'arguments'; - while ($current) { - if ($isArguments && isset($current->arguments)) { - return array('list', ' ', $current->arguments); - } - - if (isset($current->store[$name])) - return $current->store[$name]; - else { - $current = isset($current->storeParent) ? - $current->storeParent : $current->parent; - } - } - - $this->throwError("variable $name is undefined"); - } - - // inject array of unparsed strings into environment as variables - protected function injectVariables($args) { - $this->pushEnv(); - $parser = new lessc_parser($this, __METHOD__); - foreach ($args as $name => $strValue) { - if ($name{0} != '@') $name = '@'.$name; - $parser->count = 0; - $parser->buffer = (string)$strValue; - if (!$parser->propertyValue($value)) { - throw new Exception("failed to parse passed in variable $name: $strValue"); - } - - $this->set($name, $value); - } - } - - /** - * Initialize any static state, can initialize parser for a file - * $opts isn't used yet - */ - public function __construct($fname = null) { - if ($fname !== null) { - // used for deprecated parse method - $this->_parseFile = $fname; - } - } - - public function compile($string, $name = null) { - $locale = setlocale(LC_NUMERIC, 0); - setlocale(LC_NUMERIC, "C"); - - $this->parser = $this->makeParser($name); - $root = $this->parser->parse($string); - - $this->env = null; - $this->scope = null; - - $this->formatter = $this->newFormatter(); - - if (!empty($this->registeredVars)) { - $this->injectVariables($this->registeredVars); - } - - $this->sourceParser = $this->parser; // used for error messages - $this->compileBlock($root); - - ob_start(); - $this->formatter->block($this->scope); - $out = ob_get_clean(); - setlocale(LC_NUMERIC, $locale); - return $out; - } - - public function compileFile($fname, $outFname = null) { - if (!is_readable($fname)) { - throw new Exception('load error: failed to find '.$fname); - } - - $pi = pathinfo($fname); - - $oldImport = $this->importDir; - - $this->importDir = (array)$this->importDir; - $this->importDir[] = $pi['dirname'].'/'; - - $this->addParsedFile($fname); - - $out = $this->compile(file_get_contents($fname), $fname); - - $this->importDir = $oldImport; - - if ($outFname !== null) { - return file_put_contents($outFname, $out); - } - - return $out; - } - - // compile only if changed input has changed or output doesn't exist - public function checkedCompile($in, $out) { - if (!is_file($out) || filemtime($in) > filemtime($out)) { - $this->compileFile($in, $out); - return true; - } - return false; - } - - /** - * Execute lessphp on a .less file or a lessphp cache structure - * - * The lessphp cache structure contains information about a specific - * less file having been parsed. It can be used as a hint for future - * calls to determine whether or not a rebuild is required. - * - * The cache structure contains two important keys that may be used - * externally: - * - * compiled: The final compiled CSS - * updated: The time (in seconds) the CSS was last compiled - * - * The cache structure is a plain-ol' PHP associative array and can - * be serialized and unserialized without a hitch. - * - * @param mixed $in Input - * @param bool $force Force rebuild? - * @return array lessphp cache structure - */ - public function cachedCompile($in, $force = false) { - // assume no root - $root = null; - - if (is_string($in)) { - $root = $in; - } elseif (is_array($in) and isset($in['root'])) { - if ($force or ! isset($in['files'])) { - // If we are forcing a recompile or if for some reason the - // structure does not contain any file information we should - // specify the root to trigger a rebuild. - $root = $in['root']; - } elseif (isset($in['files']) and is_array($in['files'])) { - foreach ($in['files'] as $fname => $ftime ) { - if (!file_exists($fname) or filemtime($fname) > $ftime) { - // One of the files we knew about previously has changed - // so we should look at our incoming root again. - $root = $in['root']; - break; - } - } - } - } else { - // TODO: Throw an exception? We got neither a string nor something - // that looks like a compatible lessphp cache structure. - return null; - } - - if ($root !== null) { - // If we have a root value which means we should rebuild. - $out = array(); - $out['root'] = $root; - $out['compiled'] = $this->compileFile($root); - $out['files'] = $this->allParsedFiles(); - $out['updated'] = time(); - return $out; - } else { - // No changes, pass back the structure - // we were given initially. - return $in; - } - - } - - // parse and compile buffer - // This is deprecated - public function parse($str = null, $initialVariables = null) { - if (is_array($str)) { - $initialVariables = $str; - $str = null; - } - - $oldVars = $this->registeredVars; - if ($initialVariables !== null) { - $this->setVariables($initialVariables); - } - - if ($str == null) { - if (empty($this->_parseFile)) { - throw new exception("nothing to parse"); - } - - $out = $this->compileFile($this->_parseFile); - } else { - $out = $this->compile($str); - } - - $this->registeredVars = $oldVars; - return $out; - } - - protected function makeParser($name) { - $parser = new lessc_parser($this, $name); - $parser->writeComments = $this->preserveComments; - - return $parser; - } - - public function setFormatter($name) { - $this->formatterName = $name; - } - - protected function newFormatter() { - $className = "lessc_formatter_lessjs"; - if (!empty($this->formatterName)) { - if (!is_string($this->formatterName)) - return $this->formatterName; - $className = "lessc_formatter_$this->formatterName"; - } - - return new $className; - } - - public function setPreserveComments($preserve) { - $this->preserveComments = $preserve; - } - - public function registerFunction($name, $func) { - $this->libFunctions[$name] = $func; - } - - public function unregisterFunction($name) { - unset($this->libFunctions[$name]); - } - - public function setVariables($variables) { - $this->registeredVars = array_merge($this->registeredVars, $variables); - } - - public function unsetVariable($name) { - unset($this->registeredVars[$name]); - } - - public function setImportDir($dirs) { - $this->importDir = (array)$dirs; - } - - public function addImportDir($dir) { - $this->importDir = (array)$this->importDir; - $this->importDir[] = $dir; - } - - public function allParsedFiles() { - return $this->allParsedFiles; - } - - public function addParsedFile($file) { - $this->allParsedFiles[realpath($file)] = filemtime($file); - } - - /** - * Uses the current value of $this->count to show line and line number - */ - public function throwError($msg = null) { - if ($this->sourceLoc >= 0) { - $this->sourceParser->throwError($msg, $this->sourceLoc); - } - throw new exception($msg); - } - - // compile file $in to file $out if $in is newer than $out - // returns true when it compiles, false otherwise - public static function ccompile($in, $out, $less = null) { - if ($less === null) { - $less = new self; - } - return $less->checkedCompile($in, $out); - } - - public static function cexecute($in, $force = false, $less = null) { - if ($less === null) { - $less = new self; - } - return $less->cachedCompile($in, $force); - } - - static protected $cssColors = array( - 'aliceblue' => '240,248,255', - 'antiquewhite' => '250,235,215', - 'aqua' => '0,255,255', - 'aquamarine' => '127,255,212', - 'azure' => '240,255,255', - 'beige' => '245,245,220', - 'bisque' => '255,228,196', - 'black' => '0,0,0', - 'blanchedalmond' => '255,235,205', - 'blue' => '0,0,255', - 'blueviolet' => '138,43,226', - 'brown' => '165,42,42', - 'burlywood' => '222,184,135', - 'cadetblue' => '95,158,160', - 'chartreuse' => '127,255,0', - 'chocolate' => '210,105,30', - 'coral' => '255,127,80', - 'cornflowerblue' => '100,149,237', - 'cornsilk' => '255,248,220', - 'crimson' => '220,20,60', - 'cyan' => '0,255,255', - 'darkblue' => '0,0,139', - 'darkcyan' => '0,139,139', - 'darkgoldenrod' => '184,134,11', - 'darkgray' => '169,169,169', - 'darkgreen' => '0,100,0', - 'darkgrey' => '169,169,169', - 'darkkhaki' => '189,183,107', - 'darkmagenta' => '139,0,139', - 'darkolivegreen' => '85,107,47', - 'darkorange' => '255,140,0', - 'darkorchid' => '153,50,204', - 'darkred' => '139,0,0', - 'darksalmon' => '233,150,122', - 'darkseagreen' => '143,188,143', - 'darkslateblue' => '72,61,139', - 'darkslategray' => '47,79,79', - 'darkslategrey' => '47,79,79', - 'darkturquoise' => '0,206,209', - 'darkviolet' => '148,0,211', - 'deeppink' => '255,20,147', - 'deepskyblue' => '0,191,255', - 'dimgray' => '105,105,105', - 'dimgrey' => '105,105,105', - 'dodgerblue' => '30,144,255', - 'firebrick' => '178,34,34', - 'floralwhite' => '255,250,240', - 'forestgreen' => '34,139,34', - 'fuchsia' => '255,0,255', - 'gainsboro' => '220,220,220', - 'ghostwhite' => '248,248,255', - 'gold' => '255,215,0', - 'goldenrod' => '218,165,32', - 'gray' => '128,128,128', - 'green' => '0,128,0', - 'greenyellow' => '173,255,47', - 'grey' => '128,128,128', - 'honeydew' => '240,255,240', - 'hotpink' => '255,105,180', - 'indianred' => '205,92,92', - 'indigo' => '75,0,130', - 'ivory' => '255,255,240', - 'khaki' => '240,230,140', - 'lavender' => '230,230,250', - 'lavenderblush' => '255,240,245', - 'lawngreen' => '124,252,0', - 'lemonchiffon' => '255,250,205', - 'lightblue' => '173,216,230', - 'lightcoral' => '240,128,128', - 'lightcyan' => '224,255,255', - 'lightgoldenrodyellow' => '250,250,210', - 'lightgray' => '211,211,211', - 'lightgreen' => '144,238,144', - 'lightgrey' => '211,211,211', - 'lightpink' => '255,182,193', - 'lightsalmon' => '255,160,122', - 'lightseagreen' => '32,178,170', - 'lightskyblue' => '135,206,250', - 'lightslategray' => '119,136,153', - 'lightslategrey' => '119,136,153', - 'lightsteelblue' => '176,196,222', - 'lightyellow' => '255,255,224', - 'lime' => '0,255,0', - 'limegreen' => '50,205,50', - 'linen' => '250,240,230', - 'magenta' => '255,0,255', - 'maroon' => '128,0,0', - 'mediumaquamarine' => '102,205,170', - 'mediumblue' => '0,0,205', - 'mediumorchid' => '186,85,211', - 'mediumpurple' => '147,112,219', - 'mediumseagreen' => '60,179,113', - 'mediumslateblue' => '123,104,238', - 'mediumspringgreen' => '0,250,154', - 'mediumturquoise' => '72,209,204', - 'mediumvioletred' => '199,21,133', - 'midnightblue' => '25,25,112', - 'mintcream' => '245,255,250', - 'mistyrose' => '255,228,225', - 'moccasin' => '255,228,181', - 'navajowhite' => '255,222,173', - 'navy' => '0,0,128', - 'oldlace' => '253,245,230', - 'olive' => '128,128,0', - 'olivedrab' => '107,142,35', - 'orange' => '255,165,0', - 'orangered' => '255,69,0', - 'orchid' => '218,112,214', - 'palegoldenrod' => '238,232,170', - 'palegreen' => '152,251,152', - 'paleturquoise' => '175,238,238', - 'palevioletred' => '219,112,147', - 'papayawhip' => '255,239,213', - 'peachpuff' => '255,218,185', - 'peru' => '205,133,63', - 'pink' => '255,192,203', - 'plum' => '221,160,221', - 'powderblue' => '176,224,230', - 'purple' => '128,0,128', - 'red' => '255,0,0', - 'rosybrown' => '188,143,143', - 'royalblue' => '65,105,225', - 'saddlebrown' => '139,69,19', - 'salmon' => '250,128,114', - 'sandybrown' => '244,164,96', - 'seagreen' => '46,139,87', - 'seashell' => '255,245,238', - 'sienna' => '160,82,45', - 'silver' => '192,192,192', - 'skyblue' => '135,206,235', - 'slateblue' => '106,90,205', - 'slategray' => '112,128,144', - 'slategrey' => '112,128,144', - 'snow' => '255,250,250', - 'springgreen' => '0,255,127', - 'steelblue' => '70,130,180', - 'tan' => '210,180,140', - 'teal' => '0,128,128', - 'thistle' => '216,191,216', - 'tomato' => '255,99,71', - 'transparent' => '0,0,0,0', - 'turquoise' => '64,224,208', - 'violet' => '238,130,238', - 'wheat' => '245,222,179', - 'white' => '255,255,255', - 'whitesmoke' => '245,245,245', - 'yellow' => '255,255,0', - 'yellowgreen' => '154,205,50' - ); -} - -// responsible for taking a string of LESS code and converting it into a -// syntax tree -class lessc_parser { - static protected $nextBlockId = 0; // used to uniquely identify blocks - - static protected $precedence = array( - '=<' => 0, - '>=' => 0, - '=' => 0, - '<' => 0, - '>' => 0, - - '+' => 1, - '-' => 1, - '*' => 2, - '/' => 2, - '%' => 2, - ); - - static protected $whitePattern; - static protected $commentMulti; - - static protected $commentSingle = "//"; - static protected $commentMultiLeft = "/*"; - static protected $commentMultiRight = "*/"; - - // regex string to match any of the operators - static protected $operatorString; - - // these properties will supress division unless it's inside parenthases - static protected $supressDivisionProps = - array('/border-radius$/i', '/^font$/i'); - - protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); - protected $lineDirectives = array("charset"); - - /** - * if we are in parens we can be more liberal with whitespace around - * operators because it must evaluate to a single value and thus is less - * ambiguous. - * - * Consider: - * property1: 10 -5; // is two numbers, 10 and -5 - * property2: (10 -5); // should evaluate to 5 - */ - protected $inParens = false; - - // caches preg escaped literals - static protected $literalCache = array(); - - public function __construct($lessc, $sourceName = null) { - $this->eatWhiteDefault = true; - // reference to less needed for vPrefix, mPrefix, and parentSelector - $this->lessc = $lessc; - - $this->sourceName = $sourceName; // name used for error messages - - $this->writeComments = false; - - if (!self::$operatorString) { - self::$operatorString = - '('.implode('|', array_map(array('lessc', 'preg_quote'), - array_keys(self::$precedence))).')'; - - $commentSingle = lessc::preg_quote(self::$commentSingle); - $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); - $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); - - self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; - self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; - } - } - - public function parse($buffer) { - $this->count = 0; - $this->line = 1; - - $this->env = null; // block stack - $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); - $this->pushSpecialBlock("root"); - $this->eatWhiteDefault = true; - $this->seenComments = array(); - - // trim whitespace on head - // if (preg_match('/^\s+/', $this->buffer, $m)) { - // $this->line += substr_count($m[0], "\n"); - // $this->buffer = ltrim($this->buffer); - // } - $this->whitespace(); - - // parse the entire file - while (false !== $this->parseChunk()); - - if ($this->count != strlen($this->buffer)) - $this->throwError(); - - // TODO report where the block was opened - if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) ) - throw new exception('parse error: unclosed block'); - - return $this->env; - } - - /** - * Parse a single chunk off the head of the buffer and append it to the - * current parse environment. - * Returns false when the buffer is empty, or when there is an error. - * - * This function is called repeatedly until the entire document is - * parsed. - * - * This parser is most similar to a recursive descent parser. Single - * functions represent discrete grammatical rules for the language, and - * they are able to capture the text that represents those rules. - * - * Consider the function lessc::keyword(). (all parse functions are - * structured the same) - * - * The function takes a single reference argument. When calling the - * function it will attempt to match a keyword on the head of the buffer. - * If it is successful, it will place the keyword in the referenced - * argument, advance the position in the buffer, and return true. If it - * fails then it won't advance the buffer and it will return false. - * - * All of these parse functions are powered by lessc::match(), which behaves - * the same way, but takes a literal regular expression. Sometimes it is - * more convenient to use match instead of creating a new function. - * - * Because of the format of the functions, to parse an entire string of - * grammatical rules, you can chain them together using &&. - * - * But, if some of the rules in the chain succeed before one fails, then - * the buffer position will be left at an invalid state. In order to - * avoid this, lessc::seek() is used to remember and set buffer positions. - * - * Before parsing a chain, use $s = $this->seek() to remember the current - * position into $s. Then if a chain fails, use $this->seek($s) to - * go back where we started. - */ - protected function parseChunk() { - if (empty($this->buffer)) return false; - $s = $this->seek(); - - if ($this->whitespace()) { - return true; - } - - // setting a property - if ($this->keyword($key) && $this->assign() && - $this->propertyValue($value, $key) && $this->end()) - { - $this->append(array('assign', $key, $value), $s); - return true; - } else { - $this->seek($s); - } - - - // look for special css blocks - if ($this->literal('@', false)) { - $this->count--; - - // media - if ($this->literal('@media')) { - if (($this->mediaQueryList($mediaQueries) || true) - && $this->literal('{')) - { - $media = $this->pushSpecialBlock("media"); - $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; - return true; - } else { - $this->seek($s); - return false; - } - } - - if ($this->literal("@", false) && $this->keyword($dirName)) { - if ($this->isDirective($dirName, $this->blockDirectives)) { - if (($this->openString("{", $dirValue, null, array(";")) || true) && - $this->literal("{")) - { - $dir = $this->pushSpecialBlock("directive"); - $dir->name = $dirName; - if (isset($dirValue)) $dir->value = $dirValue; - return true; - } - } elseif ($this->isDirective($dirName, $this->lineDirectives)) { - if ($this->propertyValue($dirValue) && $this->end()) { - $this->append(array("directive", $dirName, $dirValue)); - return true; - } - } - } - - $this->seek($s); - } - - // setting a variable - if ($this->variable($var) && $this->assign() && - $this->propertyValue($value) && $this->end()) - { - $this->append(array('assign', $var, $value), $s); - return true; - } else { - $this->seek($s); - } - - if ($this->import($importValue)) { - $this->append($importValue, $s); - return true; - } - - // opening parametric mixin - if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && - ($this->guards($guards) || true) && - $this->literal('{')) - { - $block = $this->pushBlock($this->fixTags(array($tag))); - $block->args = $args; - $block->isVararg = $isVararg; - if (!empty($guards)) $block->guards = $guards; - return true; - } else { - $this->seek($s); - } - - // opening a simple block - if ($this->tags($tags) && $this->literal('{', false)) { - $tags = $this->fixTags($tags); - $this->pushBlock($tags); - return true; - } else { - $this->seek($s); - } - - // closing a block - if ($this->literal('}', false)) { - try { - $block = $this->pop(); - } catch (exception $e) { - $this->seek($s); - $this->throwError($e->getMessage()); - } - - $hidden = false; - if (is_null($block->type)) { - $hidden = true; - if (!isset($block->args)) { - foreach ($block->tags as $tag) { - if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { - $hidden = false; - break; - } - } - } - - foreach ($block->tags as $tag) { - if (is_string($tag)) { - $this->env->children[$tag][] = $block; - } - } - } - - if (!$hidden) { - $this->append(array('block', $block), $s); - } - - // this is done here so comments aren't bundled into he block that - // was just closed - $this->whitespace(); - return true; - } - - // mixin - if ($this->mixinTags($tags) && - ($this->argumentDef($argv, $isVararg) || true) && - ($this->keyword($suffix) || true) && $this->end()) - { - $tags = $this->fixTags($tags); - $this->append(array('mixin', $tags, $argv, $suffix), $s); - return true; - } else { - $this->seek($s); - } - - // spare ; - if ($this->literal(';')) return true; - - return false; // got nothing, throw error - } - - protected function isDirective($dirname, $directives) { - // TODO: cache pattern in parser - $pattern = implode("|", - array_map(array("lessc", "preg_quote"), $directives)); - $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; - - return preg_match($pattern, $dirname); - } - - protected function fixTags($tags) { - // move @ tags out of variable namespace - foreach ($tags as &$tag) { - if ($tag{0} == $this->lessc->vPrefix) - $tag[0] = $this->lessc->mPrefix; - } - return $tags; - } - - // a list of expressions - protected function expressionList(&$exps) { - $values = array(); - - while ($this->expression($exp)) { - $values[] = $exp; - } - - if (count($values) == 0) return false; - - $exps = lessc::compressList($values, ' '); - return true; - } - - /** - * Attempt to consume an expression. - * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code - */ - protected function expression(&$out) { - if ($this->value($lhs)) { - $out = $this->expHelper($lhs, 0); - - // look for / shorthand - if (!empty($this->env->supressedDivision)) { - unset($this->env->supressedDivision); - $s = $this->seek(); - if ($this->literal("/") && $this->value($rhs)) { - $out = array("list", "", - array($out, array("keyword", "/"), $rhs)); - } else { - $this->seek($s); - } - } - - return true; - } - return false; - } - - /** - * recursively parse infix equation with $lhs at precedence $minP - */ - protected function expHelper($lhs, $minP) { - $this->inExp = true; - $ss = $this->seek(); - - while (true) { - $whiteBefore = isset($this->buffer[$this->count - 1]) && - ctype_space($this->buffer[$this->count - 1]); - - // If there is whitespace before the operator, then we require - // whitespace after the operator for it to be an expression - $needWhite = $whiteBefore && !$this->inParens; - - if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { - if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { - foreach (self::$supressDivisionProps as $pattern) { - if (preg_match($pattern, $this->env->currentProperty)) { - $this->env->supressedDivision = true; - break 2; - } - } - } - - - $whiteAfter = isset($this->buffer[$this->count - 1]) && - ctype_space($this->buffer[$this->count - 1]); - - if (!$this->value($rhs)) break; - - // peek for next operator to see what to do with rhs - if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { - $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); - } - - $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); - $ss = $this->seek(); - - continue; - } - - break; - } - - $this->seek($ss); - - return $lhs; - } - - // consume a list of values for a property - public function propertyValue(&$value, $keyName = null) { - $values = array(); - - if ($keyName !== null) $this->env->currentProperty = $keyName; - - $s = null; - while ($this->expressionList($v)) { - $values[] = $v; - $s = $this->seek(); - if (!$this->literal(',')) break; - } - - if ($s) $this->seek($s); - - if ($keyName !== null) unset($this->env->currentProperty); - - if (count($values) == 0) return false; - - $value = lessc::compressList($values, ', '); - return true; - } - - protected function parenValue(&$out) { - $s = $this->seek(); - - // speed shortcut - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { - return false; - } - - $inParens = $this->inParens; - if ($this->literal("(") && - ($this->inParens = true) && $this->expression($exp) && - $this->literal(")")) - { - $out = $exp; - $this->inParens = $inParens; - return true; - } else { - $this->inParens = $inParens; - $this->seek($s); - } - - return false; - } - - // a single value - protected function value(&$value) { - $s = $this->seek(); - - // speed shortcut - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { - // negation - if ($this->literal("-", false) && - (($this->variable($inner) && $inner = array("variable", $inner)) || - $this->unit($inner) || - $this->parenValue($inner))) - { - $value = array("unary", "-", $inner); - return true; - } else { - $this->seek($s); - } - } - - if ($this->parenValue($value)) return true; - if ($this->unit($value)) return true; - if ($this->color($value)) return true; - if ($this->func($value)) return true; - if ($this->string($value)) return true; - - if ($this->keyword($word)) { - $value = array('keyword', $word); - return true; - } - - // try a variable - if ($this->variable($var)) { - $value = array('variable', $var); - return true; - } - - // unquote string (should this work on any type? - if ($this->literal("~") && $this->string($str)) { - $value = array("escape", $str); - return true; - } else { - $this->seek($s); - } - - // css hack: \0 - if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { - $value = array('keyword', '\\'.$m[1]); - return true; - } else { - $this->seek($s); - } - - return false; - } - - // an import statement - protected function import(&$out) { - if (!$this->literal('@import')) return false; - - // @import "something.css" media; - // @import url("something.css") media; - // @import url(something.css) media; - - if ($this->propertyValue($value)) { - $out = array("import", $value); - return true; - } - } - - protected function mediaQueryList(&$out) { - if ($this->genericList($list, "mediaQuery", ",", false)) { - $out = $list[2]; - return true; - } - return false; - } - - protected function mediaQuery(&$out) { - $s = $this->seek(); - - $expressions = null; - $parts = array(); - - if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { - $prop = array("mediaType"); - if (isset($only)) $prop[] = "only"; - if (isset($not)) $prop[] = "not"; - $prop[] = $mediaType; - $parts[] = $prop; - } else { - $this->seek($s); - } - - - if (!empty($mediaType) && !$this->literal("and")) { - // ~ - } else { - $this->genericList($expressions, "mediaExpression", "and", false); - if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); - } - - if (count($parts) == 0) { - $this->seek($s); - return false; - } - - $out = $parts; - return true; - } - - protected function mediaExpression(&$out) { - $s = $this->seek(); - $value = null; - if ($this->literal("(") && - $this->keyword($feature) && - ($this->literal(":") && $this->expression($value) || true) && - $this->literal(")")) - { - $out = array("mediaExp", $feature); - if ($value) $out[] = $value; - return true; - } elseif ($this->variable($variable)) { - $out = array('variable', $variable); - return true; - } - - $this->seek($s); - return false; - } - - // an unbounded string stopped by $end - protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = false; - - $stop = array("'", '"', "@{", $end); - $stop = array_map(array("lessc", "preg_quote"), $stop); - // $stop[] = self::$commentMulti; - - if (!is_null($rejectStrs)) { - $stop = array_merge($stop, $rejectStrs); - } - - $patt = '(.*?)('.implode("|", $stop).')'; - - $nestingLevel = 0; - - $content = array(); - while ($this->match($patt, $m, false)) { - if (!empty($m[1])) { - $content[] = $m[1]; - if ($nestingOpen) { - $nestingLevel += substr_count($m[1], $nestingOpen); - } - } - - $tok = $m[2]; - - $this->count-= strlen($tok); - if ($tok == $end) { - if ($nestingLevel == 0) { - break; - } else { - $nestingLevel--; - } - } - - if (($tok == "'" || $tok == '"') && $this->string($str)) { - $content[] = $str; - continue; - } - - if ($tok == "@{" && $this->interpolation($inter)) { - $content[] = $inter; - continue; - } - - if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { - break; - } - - $content[] = $tok; - $this->count+= strlen($tok); - } - - $this->eatWhiteDefault = $oldWhite; - - if (count($content) == 0) return false; - - // trim the end - if (is_string(end($content))) { - $content[count($content) - 1] = rtrim(end($content)); - } - - $out = array("string", "", $content); - return true; - } - - protected function string(&$out) { - $s = $this->seek(); - if ($this->literal('"', false)) { - $delim = '"'; - } elseif ($this->literal("'", false)) { - $delim = "'"; - } else { - return false; - } - - $content = array(); - - // look for either ending delim , escape, or string interpolation - $patt = '([^\n]*?)(@\{|\\\\|' . - lessc::preg_quote($delim).')'; - - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = false; - - while ($this->match($patt, $m, false)) { - $content[] = $m[1]; - if ($m[2] == "@{") { - $this->count -= strlen($m[2]); - if ($this->interpolation($inter, false)) { - $content[] = $inter; - } else { - $this->count += strlen($m[2]); - $content[] = "@{"; // ignore it - } - } elseif ($m[2] == '\\') { - $content[] = $m[2]; - if ($this->literal($delim, false)) { - $content[] = $delim; - } - } else { - $this->count -= strlen($delim); - break; // delim - } - } - - $this->eatWhiteDefault = $oldWhite; - - if ($this->literal($delim)) { - $out = array("string", $delim, $content); - return true; - } - - $this->seek($s); - return false; - } - - protected function interpolation(&$out) { - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = true; - - $s = $this->seek(); - if ($this->literal("@{") && - $this->openString("}", $interp, null, array("'", '"', ";")) && - $this->literal("}", false)) - { - $out = array("interpolate", $interp); - $this->eatWhiteDefault = $oldWhite; - if ($this->eatWhiteDefault) $this->whitespace(); - return true; - } - - $this->eatWhiteDefault = $oldWhite; - $this->seek($s); - return false; - } - - protected function unit(&$unit) { - // speed shortcut - if (isset($this->buffer[$this->count])) { - $char = $this->buffer[$this->count]; - if (!ctype_digit($char) && $char != ".") return false; - } - - if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { - $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); - return true; - } - return false; - } - - // a # color - protected function color(&$out) { - if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { - if (strlen($m[1]) > 7) { - $out = array("string", "", array($m[1])); - } else { - $out = array("raw_color", $m[1]); - } - return true; - } - - return false; - } - - // consume an argument definition list surrounded by () - // each argument is a variable name with optional value - // or at the end a ... or a variable named followed by ... - // arguments are separated by , unless a ; is in the list, then ; is the - // delimiter. - protected function argumentDef(&$args, &$isVararg) { - $s = $this->seek(); - if (!$this->literal('(')) return false; - - $values = array(); - $delim = ","; - $method = "expressionList"; - - $isVararg = false; - while (true) { - if ($this->literal("...")) { - $isVararg = true; - break; - } - - if ($this->$method($value)) { - if ($value[0] == "variable") { - $arg = array("arg", $value[1]); - $ss = $this->seek(); - - if ($this->assign() && $this->$method($rhs)) { - $arg[] = $rhs; - } else { - $this->seek($ss); - if ($this->literal("...")) { - $arg[0] = "rest"; - $isVararg = true; - } - } - - $values[] = $arg; - if ($isVararg) break; - continue; - } else { - $values[] = array("lit", $value); - } - } - - - if (!$this->literal($delim)) { - if ($delim == "," && $this->literal(";")) { - // found new delim, convert existing args - $delim = ";"; - $method = "propertyValue"; - - // transform arg list - if (isset($values[1])) { // 2 items - $newList = array(); - foreach ($values as $i => $arg) { - switch($arg[0]) { - case "arg": - if ($i) { - $this->throwError("Cannot mix ; and , as delimiter types"); - } - $newList[] = $arg[2]; - break; - case "lit": - $newList[] = $arg[1]; - break; - case "rest": - $this->throwError("Unexpected rest before semicolon"); - } - } - - $newList = array("list", ", ", $newList); - - switch ($values[0][0]) { - case "arg": - $newArg = array("arg", $values[0][1], $newList); - break; - case "lit": - $newArg = array("lit", $newList); - break; - } - - } elseif ($values) { // 1 item - $newArg = $values[0]; - } - - if ($newArg) { - $values = array($newArg); - } - } else { - break; - } - } - } - - if (!$this->literal(')')) { - $this->seek($s); - return false; - } - - $args = $values; - - return true; - } - - // consume a list of tags - // this accepts a hanging delimiter - protected function tags(&$tags, $simple = false, $delim = ',') { - $tags = array(); - while ($this->tag($tt, $simple)) { - $tags[] = $tt; - if (!$this->literal($delim)) break; - } - if (count($tags) == 0) return false; - - return true; - } - - // list of tags of specifying mixin path - // optionally separated by > (lazy, accepts extra >) - protected function mixinTags(&$tags) { - $tags = array(); - while ($this->tag($tt, true)) { - $tags[] = $tt; - $this->literal(">"); - } - - if (count($tags) == 0) return false; - - return true; - } - - // a bracketed value (contained within in a tag definition) - protected function tagBracket(&$parts, &$hasExpression) { - // speed shortcut - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { - return false; - } - - $s = $this->seek(); - - $hasInterpolation = false; - - if ($this->literal("[", false)) { - $attrParts = array("["); - // keyword, string, operator - while (true) { - if ($this->literal("]", false)) { - $this->count--; - break; // get out early - } - - if ($this->match('\s+', $m)) { - $attrParts[] = " "; - continue; - } - if ($this->string($str)) { - // escape parent selector, (yuck) - foreach ($str[2] as &$chunk) { - $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); - } - - $attrParts[] = $str; - $hasInterpolation = true; - continue; - } - - if ($this->keyword($word)) { - $attrParts[] = $word; - continue; - } - - if ($this->interpolation($inter, false)) { - $attrParts[] = $inter; - $hasInterpolation = true; - continue; - } - - // operator, handles attr namespace too - if ($this->match('[|-~\$\*\^=]+', $m)) { - $attrParts[] = $m[0]; - continue; - } - - break; - } - - if ($this->literal("]", false)) { - $attrParts[] = "]"; - foreach ($attrParts as $part) { - $parts[] = $part; - } - $hasExpression = $hasExpression || $hasInterpolation; - return true; - } - $this->seek($s); - } - - $this->seek($s); - return false; - } - - // a space separated list of selectors - protected function tag(&$tag, $simple = false) { - if ($simple) - $chars = '^@,:;{}\][>\(\) "\''; - else - $chars = '^@,;{}["\''; - - $s = $this->seek(); - - $hasExpression = false; - $parts = array(); - while ($this->tagBracket($parts, $hasExpression)); - - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = false; - - while (true) { - if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { - $parts[] = $m[1]; - if ($simple) break; - - while ($this->tagBracket($parts, $hasExpression)); - continue; - } - - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { - if ($this->interpolation($interp)) { - $hasExpression = true; - $interp[2] = true; // don't unescape - $parts[] = $interp; - continue; - } - - if ($this->literal("@")) { - $parts[] = "@"; - continue; - } - } - - if ($this->unit($unit)) { // for keyframes - $parts[] = $unit[1]; - $parts[] = $unit[2]; - continue; - } - - break; - } - - $this->eatWhiteDefault = $oldWhite; - if (!$parts) { - $this->seek($s); - return false; - } - - if ($hasExpression) { - $tag = array("exp", array("string", "", $parts)); - } else { - $tag = trim(implode($parts)); - } - - $this->whitespace(); - return true; - } - - // a css function - protected function func(&$func) { - $s = $this->seek(); - - if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { - $fname = $m[1]; - - $sPreArgs = $this->seek(); - - $args = array(); - while (true) { - $ss = $this->seek(); - // this ugly nonsense is for ie filter properties - if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { - $args[] = array("string", "", array($name, "=", $value)); - } else { - $this->seek($ss); - if ($this->expressionList($value)) { - $args[] = $value; - } - } - - if (!$this->literal(',')) break; - } - $args = array('list', ',', $args); - - if ($this->literal(')')) { - $func = array('function', $fname, $args); - return true; - } elseif ($fname == 'url') { - // couldn't parse and in url? treat as string - $this->seek($sPreArgs); - if ($this->openString(")", $string) && $this->literal(")")) { - $func = array('function', $fname, $string); - return true; - } - } - } - - $this->seek($s); - return false; - } - - // consume a less variable - protected function variable(&$name) { - $s = $this->seek(); - if ($this->literal($this->lessc->vPrefix, false) && - ($this->variable($sub) || $this->keyword($name))) - { - if (!empty($sub)) { - $name = array('variable', $sub); - } else { - $name = $this->lessc->vPrefix.$name; - } - return true; - } - - $name = null; - $this->seek($s); - return false; - } - - /** - * Consume an assignment operator - * Can optionally take a name that will be set to the current property name - */ - protected function assign($name = null) { - if ($name) $this->currentProperty = $name; - return $this->literal(':') || $this->literal('='); - } - - // consume a keyword - protected function keyword(&$word) { - if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { - $word = $m[1]; - return true; - } - return false; - } - - // consume an end of statement delimiter - protected function end() { - if ($this->literal(';', false)) { - return true; - } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { - // if there is end of file or a closing block next then we don't need a ; - return true; - } - return false; - } - - protected function guards(&$guards) { - $s = $this->seek(); - - if (!$this->literal("when")) { - $this->seek($s); - return false; - } - - $guards = array(); - - while ($this->guardGroup($g)) { - $guards[] = $g; - if (!$this->literal(",")) break; - } - - if (count($guards) == 0) { - $guards = null; - $this->seek($s); - return false; - } - - return true; - } - - // a bunch of guards that are and'd together - // TODO rename to guardGroup - protected function guardGroup(&$guardGroup) { - $s = $this->seek(); - $guardGroup = array(); - while ($this->guard($guard)) { - $guardGroup[] = $guard; - if (!$this->literal("and")) break; - } - - if (count($guardGroup) == 0) { - $guardGroup = null; - $this->seek($s); - return false; - } - - return true; - } - - protected function guard(&$guard) { - $s = $this->seek(); - $negate = $this->literal("not"); - - if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { - $guard = $exp; - if ($negate) $guard = array("negate", $guard); - return true; - } - - $this->seek($s); - return false; - } - - /* raw parsing functions */ - - protected function literal($what, $eatWhitespace = null) { - if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; - - // shortcut on single letter - if (!isset($what[1]) && isset($this->buffer[$this->count])) { - if ($this->buffer[$this->count] == $what) { - if (!$eatWhitespace) { - $this->count++; - return true; - } - // goes below... - } else { - return false; - } - } - - if (!isset(self::$literalCache[$what])) { - self::$literalCache[$what] = lessc::preg_quote($what); - } - - return $this->match(self::$literalCache[$what], $m, $eatWhitespace); - } - - protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { - $s = $this->seek(); - $items = array(); - while ($this->$parseItem($value)) { - $items[] = $value; - if ($delim) { - if (!$this->literal($delim)) break; - } - } - - if (count($items) == 0) { - $this->seek($s); - return false; - } - - if ($flatten && count($items) == 1) { - $out = $items[0]; - } else { - $out = array("list", $delim, $items); - } - - return true; - } - - - // advance counter to next occurrence of $what - // $until - don't include $what in advance - // $allowNewline, if string, will be used as valid char set - protected function to($what, &$out, $until = false, $allowNewline = false) { - if (is_string($allowNewline)) { - $validChars = $allowNewline; - } else { - $validChars = $allowNewline ? "." : "[^\n]"; - } - if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; - if ($until) $this->count -= strlen($what); // give back $what - $out = $m[1]; - return true; - } - - // try to match something on head of buffer - protected function match($regex, &$out, $eatWhitespace = null) { - if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; - - $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; - if (preg_match($r, $this->buffer, $out, null, $this->count)) { - $this->count += strlen($out[0]); - if ($eatWhitespace && $this->writeComments) $this->whitespace(); - return true; - } - return false; - } - - // match some whitespace - protected function whitespace() { - if ($this->writeComments) { - $gotWhite = false; - while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { - if (isset($m[1]) && empty($this->seenComments[$this->count])) { - $this->append(array("comment", $m[1])); - $this->seenComments[$this->count] = true; - } - $this->count += strlen($m[0]); - $gotWhite = true; - } - return $gotWhite; - } else { - $this->match("", $m); - return strlen($m[0]) > 0; - } - } - - // match something without consuming it - protected function peek($regex, &$out = null, $from=null) { - if (is_null($from)) $from = $this->count; - $r = '/'.$regex.'/Ais'; - $result = preg_match($r, $this->buffer, $out, null, $from); - - return $result; - } - - // seek to a spot in the buffer or return where we are on no argument - protected function seek($where = null) { - if ($where === null) return $this->count; - else $this->count = $where; - return true; - } - - /* misc functions */ - - public function throwError($msg = "parse error", $count = null) { - $count = is_null($count) ? $this->count : $count; - - $line = $this->line + - substr_count(substr($this->buffer, 0, $count), "\n"); - - if (!empty($this->sourceName)) { - $loc = "$this->sourceName on line $line"; - } else { - $loc = "line: $line"; - } - - // TODO this depends on $this->count - if ($this->peek("(.*?)(\n|$)", $m, $count)) { - throw new exception("$msg: failed at `$m[1]` $loc"); - } else { - throw new exception("$msg: $loc"); - } - } - - protected function pushBlock($selectors=null, $type=null) { - $b = new stdclass; - $b->parent = $this->env; - - $b->type = $type; - $b->id = self::$nextBlockId++; - - $b->isVararg = false; // TODO: kill me from here - $b->tags = $selectors; - - $b->props = array(); - $b->children = array(); - - $this->env = $b; - return $b; - } - - // push a block that doesn't multiply tags - protected function pushSpecialBlock($type) { - return $this->pushBlock(null, $type); - } - - // append a property to the current block - protected function append($prop, $pos = null) { - if ($pos !== null) $prop[-1] = $pos; - $this->env->props[] = $prop; - } - - // pop something off the stack - protected function pop() { - $old = $this->env; - $this->env = $this->env->parent; - return $old; - } - - // remove comments from $text - // todo: make it work for all functions, not just url - protected function removeComments($text) { - $look = array( - 'url(', '//', '/*', '"', "'" - ); - - $out = ''; - $min = null; - while (true) { - // find the next item - foreach ($look as $token) { - $pos = strpos($text, $token); - if ($pos !== false) { - if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); - } - } - - if (is_null($min)) break; - - $count = $min[1]; - $skip = 0; - $newlines = 0; - switch ($min[0]) { - case 'url(': - if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) - $count += strlen($m[0]) - strlen($min[0]); - break; - case '"': - case "'": - if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count)) - $count += strlen($m[0]) - 1; - break; - case '//': - $skip = strpos($text, "\n", $count); - if ($skip === false) $skip = strlen($text) - $count; - else $skip -= $count; - break; - case '/*': - if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { - $skip = strlen($m[0]); - $newlines = substr_count($m[0], "\n"); - } - break; - } - - if ($skip == 0) $count += strlen($min[0]); - - $out .= substr($text, 0, $count).str_repeat("\n", $newlines); - $text = substr($text, $count + $skip); - - $min = null; - } - - return $out.$text; - } - -} - -class lessc_formatter_classic { - public $indentChar = " "; - - public $break = "\n"; - public $open = " {"; - public $close = "}"; - public $selectorSeparator = ", "; - public $assignSeparator = ":"; - - public $openSingle = " { "; - public $closeSingle = " }"; - - public $disableSingle = false; - public $breakSelectors = false; - - public $compressColors = false; - - public function __construct() { - $this->indentLevel = 0; - } - - public function indentStr($n = 0) { - return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); - } - - public function property($name, $value) { - return $name . $this->assignSeparator . $value . ";"; - } - - protected function isEmpty($block) { - if (empty($block->lines)) { - foreach ($block->children as $child) { - if (!$this->isEmpty($child)) return false; - } - - return true; - } - return false; - } - - public function block($block) { - if ($this->isEmpty($block)) return; - - $inner = $pre = $this->indentStr(); - - $isSingle = !$this->disableSingle && - is_null($block->type) && count($block->lines) == 1; - - if (!empty($block->selectors)) { - $this->indentLevel++; - - if ($this->breakSelectors) { - $selectorSeparator = $this->selectorSeparator . $this->break . $pre; - } else { - $selectorSeparator = $this->selectorSeparator; - } - - echo $pre . - implode($selectorSeparator, $block->selectors); - if ($isSingle) { - echo $this->openSingle; - $inner = ""; - } else { - echo $this->open . $this->break; - $inner = $this->indentStr(); - } - - } - - if (!empty($block->lines)) { - $glue = $this->break.$inner; - echo $inner . implode($glue, $block->lines); - if (!$isSingle && !empty($block->children)) { - echo $this->break; - } - } - - foreach ($block->children as $child) { - $this->block($child); - } - - if (!empty($block->selectors)) { - if (!$isSingle && empty($block->children)) echo $this->break; - - if ($isSingle) { - echo $this->closeSingle . $this->break; - } else { - echo $pre . $this->close . $this->break; - } - - $this->indentLevel--; - } - } -} - -class lessc_formatter_compressed extends lessc_formatter_classic { - public $disableSingle = true; - public $open = "{"; - public $selectorSeparator = ","; - public $assignSeparator = ":"; - public $break = ""; - public $compressColors = true; - - public function indentStr($n = 0) { - return ""; - } -} - -class lessc_formatter_lessjs extends lessc_formatter_classic { - public $disableSingle = true; - public $breakSelectors = true; - public $assignSeparator = ": "; - public $selectorSeparator = ","; -} - - diff --git a/includes/libs/normal/UtfNormal.php b/includes/libs/normal/UtfNormal.php new file mode 100644 index 00000000..c9c05a07 --- /dev/null +++ b/includes/libs/normal/UtfNormal.php @@ -0,0 +1,129 @@ +<?php +/** + * Unicode normalization routines + * + * Copyright © 2004 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * 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 + * @ingroup UtfNormal + */ + +/** + * @defgroup UtfNormal UtfNormal + */ + +use UtfNormal\Validator; + +/** + * Unicode normalization routines for working with UTF-8 strings. + * Currently assumes that input strings are valid UTF-8! + * + * Not as fast as I'd like, but should be usable for most purposes. + * UtfNormal::toNFC() will bail early if given ASCII text or text + * it can quickly determine is already normalized. + * + * All functions can be called static. + * + * See description of forms at http://www.unicode.org/reports/tr15/ + * + * @deprecated since 1.25, use UtfNormal\Validator directly + * @ingroup UtfNormal + */ +class UtfNormal { + /** + * The ultimate convenience function! Clean up invalid UTF-8 sequences, + * and convert to normal form C, canonical composition. + * + * Fast return for pure ASCII strings; some lesser optimizations for + * strings containing only known-good characters. Not as fast as toNFC(). + * + * @param string $string a UTF-8 string + * @return string a clean, shiny, normalized UTF-8 string + */ + static function cleanUp( $string ) { + return Validator::cleanUp( $string ); + } + + /** + * Convert a UTF-8 string to normal form C, canonical composition. + * Fast return for pure ASCII strings; some lesser optimizations for + * strings containing only known-good characters. + * + * @param string $string a valid UTF-8 string. Input is not validated. + * @return string a UTF-8 string in normal form C + */ + static function toNFC( $string ) { + return Validator::toNFC( $string ); + } + + /** + * Convert a UTF-8 string to normal form D, canonical decomposition. + * Fast return for pure ASCII strings. + * + * @param string $string a valid UTF-8 string. Input is not validated. + * @return string a UTF-8 string in normal form D + */ + static function toNFD( $string ) { + return Validator::toNFD( $string ); + } + + /** + * Convert a UTF-8 string to normal form KC, compatibility composition. + * This may cause irreversible information loss, use judiciously. + * Fast return for pure ASCII strings. + * + * @param string $string a valid UTF-8 string. Input is not validated. + * @return string a UTF-8 string in normal form KC + */ + static function toNFKC( $string ) { + return Validator::toNFKC( $string ); + } + + /** + * Convert a UTF-8 string to normal form KD, compatibility decomposition. + * This may cause irreversible information loss, use judiciously. + * Fast return for pure ASCII strings. + * + * @param string $string a valid UTF-8 string. Input is not validated. + * @return string a UTF-8 string in normal form KD + */ + static function toNFKD( $string ) { + return Validator::toNFKD( $string ); + } + + /** + * Returns true if the string is _definitely_ in NFC. + * Returns false if not or uncertain. + * @param string $string a valid UTF-8 string. Input is not validated. + * @return bool + */ + static function quickIsNFC( $string ) { + return Validator::quickIsNFC( $string ); + } + + /** + * Returns true if the string is _definitely_ in NFC. + * Returns false if not or uncertain. + * @param string $string a UTF-8 string, altered on output to be valid UTF-8 safe for XML. + * @return bool + */ + static function quickIsNFCVerify( &$string ) { + return Validator::quickIsNFCVerify( $string ); + } +} diff --git a/includes/libs/normal/UtfNormalDefines.php b/includes/libs/normal/UtfNormalDefines.php new file mode 100644 index 00000000..b8e44c77 --- /dev/null +++ b/includes/libs/normal/UtfNormalDefines.php @@ -0,0 +1,186 @@ +<?php +/** + * Backwards-compatability constants which are now provided by the + * UtfNormal library. They are hardcoded here since they are needed + * before the composer autoloader is initialized. + * + * 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 + * @ingroup UtfNormal + */ + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_FIRST', 0xac00 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_LAST', 0xd7a3 ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_LBASE', 0x1100 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_VBASE', 0x1161 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_TBASE', 0x11a7 ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_LCOUNT', 19 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_VCOUNT', 21 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_TCOUNT', 28 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_NCOUNT', UNICODE_HANGUL_VCOUNT * UNICODE_HANGUL_TCOUNT ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_LEND', UNICODE_HANGUL_LBASE + UNICODE_HANGUL_LCOUNT - 1 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_VEND', UNICODE_HANGUL_VBASE + UNICODE_HANGUL_VCOUNT - 1 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_HANGUL_TEND', UNICODE_HANGUL_TBASE + UNICODE_HANGUL_TCOUNT - 1 ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_SURROGATE_FIRST', 0xd800 ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_SURROGATE_LAST', 0xdfff ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_MAX', 0x10ffff ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UNICODE_REPLACEMENT', 0xfffd ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_FIRST', "\xea\xb0\x80" /*codepointToUtf8( UNICODE_HANGUL_FIRST )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_LAST', "\xed\x9e\xa3" /*codepointToUtf8( UNICODE_HANGUL_LAST )*/ ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_LBASE', "\xe1\x84\x80" /*codepointToUtf8( UNICODE_HANGUL_LBASE )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_VBASE', "\xe1\x85\xa1" /*codepointToUtf8( UNICODE_HANGUL_VBASE )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_TBASE', "\xe1\x86\xa7" /*codepointToUtf8( UNICODE_HANGUL_TBASE )*/ ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_LEND', "\xe1\x84\x92" /*codepointToUtf8( UNICODE_HANGUL_LEND )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_VEND', "\xe1\x85\xb5" /*codepointToUtf8( UNICODE_HANGUL_VEND )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HANGUL_TEND', "\xe1\x87\x82" /*codepointToUtf8( UNICODE_HANGUL_TEND )*/ ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_SURROGATE_FIRST', "\xed\xa0\x80" /*codepointToUtf8( UNICODE_SURROGATE_FIRST )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_SURROGATE_LAST', "\xed\xbf\xbf" /*codepointToUtf8( UNICODE_SURROGATE_LAST )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_MAX', "\xf4\x8f\xbf\xbf" /*codepointToUtf8( UNICODE_MAX )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_REPLACEMENT', "\xef\xbf\xbd" /*codepointToUtf8( UNICODE_REPLACEMENT )*/ ); +#define( 'UTF8_REPLACEMENT', '!' ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_OVERLONG_A', "\xc1\xbf" ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_OVERLONG_B', "\xe0\x9f\xbf" ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_OVERLONG_C', "\xf0\x8f\xbf\xbf" ); + +# These two ranges are illegal +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_FDD0', "\xef\xb7\x90" /*codepointToUtf8( 0xfdd0 )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_FDEF', "\xef\xb7\xaf" /*codepointToUtf8( 0xfdef )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_FFFE', "\xef\xbf\xbe" /*codepointToUtf8( 0xfffe )*/ ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_FFFF', "\xef\xbf\xbf" /*codepointToUtf8( 0xffff )*/ ); + +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_HEAD', false ); +/** + * @deprecated since 1.25, use UtfNormal\Constants instead + */ +define( 'UTF8_TAIL', true ); diff --git a/includes/libs/normal/UtfNormalUtil.php b/includes/libs/normal/UtfNormalUtil.php new file mode 100644 index 00000000..ad9a2b9a --- /dev/null +++ b/includes/libs/normal/UtfNormalUtil.php @@ -0,0 +1,99 @@ +<?php +/** + * Some of these functions are adapted from places in MediaWiki. + * Should probably merge them for consistency. + * + * Copyright © 2004 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * 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 + * @ingroup UtfNormal + */ + + +use UtfNormal\Utils; +/** + * Return UTF-8 sequence for a given Unicode code point. + * + * @param $codepoint Integer: + * @return String + * @throws InvalidArgumentException if fed out of range data. + * @public + * @deprecated since 1.25, use UtfNormal\Utils directly + */ +function codepointToUtf8( $codepoint ) { + return Utils::codepointToUtf8( $codepoint ); +} + +/** + * Take a series of space-separated hexadecimal numbers representing + * Unicode code points and return a UTF-8 string composed of those + * characters. Used by UTF-8 data generation and testing routines. + * + * @param $sequence String + * @return String + * @throws InvalidArgumentException if fed out of range data. + * @private + * @deprecated since 1.25, use UtfNormal\Utils directly + */ +function hexSequenceToUtf8( $sequence ) { + return Utils::hexSequenceToUtf8( $sequence ); +} + +/** + * Take a UTF-8 string and return a space-separated series of hex + * numbers representing Unicode code points. For debugging. + * + * @fixme this is private but extensions + maint scripts are using it + * @param string $str UTF-8 string. + * @return string + * @private + */ +function utf8ToHexSequence( $str ) { + $buf = ''; + foreach ( preg_split( '//u', $str, -1, PREG_SPLIT_NO_EMPTY ) as $cp ) { + $buf .= sprintf( '%04x ', UtfNormal\Utils::utf8ToCodepoint( $cp ) ); + } + + return rtrim( $buf ); +} + +/** + * Determine the Unicode codepoint of a single-character UTF-8 sequence. + * Does not check for invalid input data. + * + * @param $char String + * @return Integer + * @public + * @deprecated since 1.25, use UtfNormal\Utils directly + */ +function utf8ToCodepoint( $char ) { + return Utils::utf8ToCodepoint( $char ); +} + +/** + * Escape a string for inclusion in a PHP single-quoted string literal. + * + * @param string $string string to be escaped. + * @return String: escaped string. + * @public + * @deprecated since 1.25, use UtfNormal\Utils directly + */ +function escapeSingleString( $string ) { + return Utils::escapeSingleString( $string ); +} diff --git a/includes/libs/objectcache/APCBagOStuff.php b/includes/libs/objectcache/APCBagOStuff.php new file mode 100644 index 00000000..eaf11557 --- /dev/null +++ b/includes/libs/objectcache/APCBagOStuff.php @@ -0,0 +1,69 @@ +<?php +/** + * Object caching using PHP's APC accelerator. + * + * 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 + * @ingroup Cache + */ + +/** + * This is a wrapper for APC's shared memory functions + * + * @ingroup Cache + */ +class APCBagOStuff extends BagOStuff { + public function get( $key, &$casToken = null ) { + $val = apc_fetch( $key ); + + $casToken = $val; + + if ( is_string( $val ) ) { + if ( $this->isInteger( $val ) ) { + $val = intval( $val ); + } else { + $val = unserialize( $val ); + } + } + + return $val; + } + + public function set( $key, $value, $exptime = 0 ) { + if ( !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + + apc_store( $key, $value, $exptime ); + + return true; + } + + public function delete( $key ) { + apc_delete( $key ); + + return true; + } + + public function incr( $key, $value = 1 ) { + return apc_inc( $key, $value ); + } + + public function decr( $key, $value = 1 ) { + return apc_dec( $key, $value ); + } +} diff --git a/includes/libs/objectcache/BagOStuff.php b/includes/libs/objectcache/BagOStuff.php new file mode 100644 index 00000000..0b791e5a --- /dev/null +++ b/includes/libs/objectcache/BagOStuff.php @@ -0,0 +1,438 @@ +<?php +/** + * Classes to cache objects in PHP accelerators, SQL database or DBA files + * + * Copyright © 2003-2004 Brion Vibber <brion@pobox.com> + * https://www.mediawiki.org/ + * + * 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 + * @ingroup Cache + */ + +/** + * @defgroup Cache Cache + */ + +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +/** + * interface is intended to be more or less compatible with + * the PHP memcached client. + * + * backends for local hash array and SQL table included: + * <code> + * $bag = new HashBagOStuff(); + * $bag = new SqlBagOStuff(); # connect to db first + * </code> + * + * @ingroup Cache + */ +abstract class BagOStuff implements LoggerAwareInterface { + private $debugMode = false; + + protected $lastError = self::ERR_NONE; + + /** + * @var LoggerInterface + */ + protected $logger; + + /** Possible values for getLastError() */ + const ERR_NONE = 0; // no error + const ERR_NO_RESPONSE = 1; // no response + const ERR_UNREACHABLE = 2; // can't connect + const ERR_UNEXPECTED = 3; // response gave some error + + public function __construct( array $params = array() ) { + if ( isset( $params['logger'] ) ) { + $this->setLogger( $params['logger'] ); + } else { + $this->setLogger( new NullLogger() ); + } + } + + /** + * @param LoggerInterface $logger + * @return null + */ + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * @param bool $bool + */ + public function setDebug( $bool ) { + $this->debugMode = $bool; + } + + /** + * Get an item with the given key. Returns false if it does not exist. + * @param string $key + * @param mixed $casToken [optional] + * @return mixed Returns false on failure + */ + abstract public function get( $key, &$casToken = null ); + + /** + * Set an item. + * @param string $key + * @param mixed $value + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @return bool Success + */ + abstract public function set( $key, $value, $exptime = 0 ); + + /** + * Delete an item. + * @param string $key + * @return bool True if the item was deleted or not found, false on failure + */ + abstract public function delete( $key ); + + /** + * Merge changes into the existing cache value (possibly creating a new one). + * The callback function returns the new value given the current value (possibly false), + * and takes the arguments: (this BagOStuff object, cache key, current value). + * + * @param string $key + * @param callable $callback Callback method to be executed + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @param int $attempts The amount of times to attempt a merge in case of failure + * @return bool Success + */ + public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) { + if ( !is_callable( $callback ) ) { + throw new Exception( "Got invalid callback." ); + } + + return $this->mergeViaLock( $key, $callback, $exptime, $attempts ); + } + + /** + * @see BagOStuff::merge() + * + * @param string $key + * @param callable $callback Callback method to be executed + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @param int $attempts The amount of times to attempt a merge in case of failure + * @return bool Success + */ + protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10 ) { + do { + $casToken = null; // passed by reference + $currentValue = $this->get( $key, $casToken ); + // Derive the new value from the old value + $value = call_user_func( $callback, $this, $key, $currentValue ); + + if ( $value === false ) { + $success = true; // do nothing + } elseif ( $currentValue === false ) { + // Try to create the key, failing if it gets created in the meantime + $success = $this->add( $key, $value, $exptime ); + } else { + // Try to update the key, failing if it gets changed in the meantime + $success = $this->cas( $casToken, $key, $value, $exptime ); + } + } while ( !$success && --$attempts ); + + return $success; + } + + /** + * Check and set an item + * + * @param mixed $casToken + * @param string $key + * @param mixed $value + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @return bool Success + */ + protected function cas( $casToken, $key, $value, $exptime = 0 ) { + throw new Exception( "CAS is not implemented in " . __CLASS__ ); + } + + /** + * @see BagOStuff::merge() + * + * @param string $key + * @param callable $callback Callback method to be executed + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @param int $attempts The amount of times to attempt a merge in case of failure + * @return bool Success + */ + protected function mergeViaLock( $key, $callback, $exptime = 0, $attempts = 10 ) { + if ( !$this->lock( $key, 6 ) ) { + return false; + } + + $currentValue = $this->get( $key ); + // Derive the new value from the old value + $value = call_user_func( $callback, $this, $key, $currentValue ); + + if ( $value === false ) { + $success = true; // do nothing + } else { + $success = $this->set( $key, $value, $exptime ); // set the new value + } + + if ( !$this->unlock( $key ) ) { + // this should never happen + trigger_error( "Could not release lock for key '$key'." ); + } + + return $success; + } + + /** + * @param string $key + * @param int $timeout Lock wait timeout [optional] + * @param int $expiry Lock expiry [optional] + * @return bool Success + */ + public function lock( $key, $timeout = 6, $expiry = 6 ) { + $this->clearLastError(); + $timestamp = microtime( true ); // starting UNIX timestamp + if ( $this->add( "{$key}:lock", 1, $expiry ) ) { + return true; + } elseif ( $this->getLastError() ) { + return false; + } + + $uRTT = ceil( 1e6 * ( microtime( true ) - $timestamp ) ); // estimate RTT (us) + $sleep = 2 * $uRTT; // rough time to do get()+set() + + $locked = false; // lock acquired + $attempts = 0; // failed attempts + do { + if ( ++$attempts >= 3 && $sleep <= 5e5 ) { + // Exponentially back off after failed attempts to avoid network spam. + // About 2*$uRTT*(2^n-1) us of "sleep" happen for the next n attempts. + $sleep *= 2; + } + usleep( $sleep ); // back off + $this->clearLastError(); + $locked = $this->add( "{$key}:lock", 1, $expiry ); + if ( $this->getLastError() ) { + return false; + } + } while ( !$locked && ( microtime( true ) - $timestamp ) < $timeout ); + + return $locked; + } + + /** + * @param string $key + * @return bool Success + */ + public function unlock( $key ) { + return $this->delete( "{$key}:lock" ); + } + + /** + * Delete all objects expiring before a certain date. + * @param string $date The reference date in MW format + * @param callable|bool $progressCallback Optional, a function which will be called + * regularly during long-running operations with the percentage progress + * as the first parameter. + * + * @return bool Success, false if unimplemented + */ + public function deleteObjectsExpiringBefore( $date, $progressCallback = false ) { + // stub + return false; + } + + /* *** Emulated functions *** */ + + /** + * Get an associative array containing the item for each of the keys that have items. + * @param array $keys List of strings + * @return array + */ + public function getMulti( array $keys ) { + $res = array(); + foreach ( $keys as $key ) { + $val = $this->get( $key ); + if ( $val !== false ) { + $res[$key] = $val; + } + } + return $res; + } + + /** + * Batch insertion + * @param array $data $key => $value assoc array + * @param int $exptime Either an interval in seconds or a unix timestamp for expiry + * @return bool Success + * @since 1.24 + */ + public function setMulti( array $data, $exptime = 0 ) { + $res = true; + foreach ( $data as $key => $value ) { + if ( !$this->set( $key, $value, $exptime ) ) { + $res = false; + } + } + return $res; + } + + /** + * @param string $key + * @param mixed $value + * @param int $exptime + * @return bool Success + */ + public function add( $key, $value, $exptime = 0 ) { + if ( $this->get( $key ) === false ) { + return $this->set( $key, $value, $exptime ); + } + return false; // key already set + } + + /** + * Increase stored value of $key by $value while preserving its TTL + * @param string $key Key to increase + * @param int $value Value to add to $key (Default 1) + * @return int|bool New value or false on failure + */ + public function incr( $key, $value = 1 ) { + if ( !$this->lock( $key ) ) { + return false; + } + $n = $this->get( $key ); + if ( $this->isInteger( $n ) ) { // key exists? + $n += intval( $value ); + $this->set( $key, max( 0, $n ) ); // exptime? + } else { + $n = false; + } + $this->unlock( $key ); + + return $n; + } + + /** + * Decrease stored value of $key by $value while preserving its TTL + * @param string $key + * @param int $value + * @return int + */ + public function decr( $key, $value = 1 ) { + return $this->incr( $key, - $value ); + } + + /** + * Increase stored value of $key by $value while preserving its TTL + * + * This will create the key with value $init and TTL $ttl if not present + * + * @param string $key + * @param int $ttl + * @param int $value + * @param int $init + * @return bool + * @since 1.24 + */ + public function incrWithInit( $key, $ttl, $value = 1, $init = 1 ) { + return $this->incr( $key, $value ) || + $this->add( $key, (int)$init, $ttl ) || $this->incr( $key, $value ); + } + + /** + * Get the "last error" registered; clearLastError() should be called manually + * @return int ERR_* constant for the "last error" registry + * @since 1.23 + */ + public function getLastError() { + return $this->lastError; + } + + /** + * Clear the "last error" registry + * @since 1.23 + */ + public function clearLastError() { + $this->lastError = self::ERR_NONE; + } + + /** + * Set the "last error" registry + * @param int $err ERR_* constant + * @since 1.23 + */ + protected function setLastError( $err ) { + $this->lastError = $err; + } + + /** + * @param string $text + */ + protected function debug( $text ) { + if ( $this->debugMode ) { + $this->logger->debug( "{class} debug: $text", array( + 'class' => get_class( $this ), + ) ); + } + } + + /** + * Convert an optionally relative time to an absolute time + * @param int $exptime + * @return int + */ + protected function convertExpiry( $exptime ) { + if ( ( $exptime != 0 ) && ( $exptime < 86400 * 3650 /* 10 years */ ) ) { + return time() + $exptime; + } else { + return $exptime; + } + } + + /** + * Convert an optionally absolute expiry time to a relative time. If an + * absolute time is specified which is in the past, use a short expiry time. + * + * @param int $exptime + * @return int + */ + protected function convertToRelative( $exptime ) { + if ( $exptime >= 86400 * 3650 /* 10 years */ ) { + $exptime -= time(); + if ( $exptime <= 0 ) { + $exptime = 1; + } + return $exptime; + } else { + return $exptime; + } + } + + /** + * Check if a value is an integer + * + * @param mixed $value + * @return bool + */ + protected function isInteger( $value ) { + return ( is_int( $value ) || ctype_digit( $value ) ); + } +} diff --git a/includes/libs/objectcache/EmptyBagOStuff.php b/includes/libs/objectcache/EmptyBagOStuff.php new file mode 100644 index 00000000..4ccf2707 --- /dev/null +++ b/includes/libs/objectcache/EmptyBagOStuff.php @@ -0,0 +1,45 @@ +<?php +/** + * Dummy object caching. + * + * 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 + * @ingroup Cache + */ + +/** + * A BagOStuff object with no objects in it. Used to provide a no-op object to calling code. + * + * @ingroup Cache + */ +class EmptyBagOStuff extends BagOStuff { + public function get( $key, &$casToken = null ) { + return false; + } + + public function set( $key, $value, $exp = 0 ) { + return true; + } + + public function delete( $key ) { + return true; + } + + public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) { + return true; // faster + } +} diff --git a/includes/libs/objectcache/HashBagOStuff.php b/includes/libs/objectcache/HashBagOStuff.php new file mode 100644 index 00000000..2c8b05a5 --- /dev/null +++ b/includes/libs/objectcache/HashBagOStuff.php @@ -0,0 +1,87 @@ +<?php +/** + * Object caching using PHP arrays. + * + * 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 + * @ingroup Cache + */ + +/** + * This is a test of the interface, mainly. It stores things in an associative + * array, which is not going to persist between program runs. + * + * @ingroup Cache + */ +class HashBagOStuff extends BagOStuff { + /** @var array */ + protected $bag; + + function __construct( $params = array() ) { + parent::__construct( $params ); + $this->bag = array(); + } + + protected function expire( $key ) { + $et = $this->bag[$key][1]; + + if ( ( $et == 0 ) || ( $et > time() ) ) { + return false; + } + + $this->delete( $key ); + + return true; + } + + public function get( $key, &$casToken = null ) { + if ( !isset( $this->bag[$key] ) ) { + return false; + } + + if ( $this->expire( $key ) ) { + return false; + } + + $casToken = $this->bag[$key][0]; + + return $this->bag[$key][0]; + } + + public function set( $key, $value, $exptime = 0 ) { + $this->bag[$key] = array( $value, $this->convertExpiry( $exptime ) ); + return true; + } + + function delete( $key ) { + if ( !isset( $this->bag[$key] ) ) { + return false; + } + + unset( $this->bag[$key] ); + + return true; + } + + public function lock( $key, $timeout = 6, $expiry = 6 ) { + return true; + } + + function unlock( $key ) { + return true; + } +} diff --git a/includes/libs/objectcache/WinCacheBagOStuff.php b/includes/libs/objectcache/WinCacheBagOStuff.php new file mode 100644 index 00000000..53625746 --- /dev/null +++ b/includes/libs/objectcache/WinCacheBagOStuff.php @@ -0,0 +1,99 @@ +<?php +/** + * Object caching using WinCache. + * + * 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 + * @ingroup Cache + */ + +/** + * Wrapper for WinCache object caching functions; identical interface + * to the APC wrapper + * + * @ingroup Cache + */ +class WinCacheBagOStuff extends BagOStuff { + + /** + * Get a value from the WinCache object cache + * + * @param string $key Cache key + * @param int $casToken [optional] Cas token + * @return mixed + */ + public function get( $key, &$casToken = null ) { + $val = wincache_ucache_get( $key ); + + $casToken = $val; + + if ( is_string( $val ) ) { + $val = unserialize( $val ); + } + + return $val; + } + + /** + * Store a value in the WinCache object cache + * + * @param string $key Cache key + * @param mixed $value Value to store + * @param int $expire Expiration time + * @return bool + */ + public function set( $key, $value, $expire = 0 ) { + $result = wincache_ucache_set( $key, serialize( $value ), $expire ); + + /* wincache_ucache_set returns an empty array on success if $value + was an array, bool otherwise */ + return ( is_array( $result ) && $result === array() ) || $result; + } + + /** + * Store a value in the WinCache object cache, race condition-safe + * + * @param int $casToken Cas token + * @param string $key Cache key + * @param int $value Object to store + * @param int $exptime Expiration time + * @return bool + */ + protected function cas( $casToken, $key, $value, $exptime = 0 ) { + return wincache_ucache_cas( $key, $casToken, serialize( $value ) ); + } + + /** + * Remove a value from the WinCache object cache + * + * @param string $key Cache key + * @return bool + */ + public function delete( $key ) { + wincache_ucache_delete( $key ); + + return true; + } + + public function merge( $key, $callback, $exptime = 0, $attempts = 10 ) { + if ( !is_callable( $callback ) ) { + throw new Exception( "Got invalid callback." ); + } + + return $this->mergeViaCas( $key, $callback, $exptime, $attempts ); + } +} diff --git a/includes/libs/objectcache/XCacheBagOStuff.php b/includes/libs/objectcache/XCacheBagOStuff.php new file mode 100644 index 00000000..cfee9236 --- /dev/null +++ b/includes/libs/objectcache/XCacheBagOStuff.php @@ -0,0 +1,89 @@ +<?php +/** + * Object caching using XCache. + * + * 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 + * @ingroup Cache + */ + +/** + * Wrapper for XCache object caching functions; identical interface + * to the APC wrapper + * + * @ingroup Cache + */ +class XCacheBagOStuff extends BagOStuff { + /** + * Get a value from the XCache object cache + * + * @param string $key Cache key + * @param mixed $casToken Cas token + * @return mixed + */ + public function get( $key, &$casToken = null ) { + $val = xcache_get( $key ); + + if ( is_string( $val ) ) { + if ( $this->isInteger( $val ) ) { + $val = intval( $val ); + } else { + $val = unserialize( $val ); + } + } elseif ( is_null( $val ) ) { + return false; + } + + return $val; + } + + /** + * Store a value in the XCache object cache + * + * @param string $key Cache key + * @param mixed $value Object to store + * @param int $expire Expiration time + * @return bool + */ + public function set( $key, $value, $expire = 0 ) { + if ( !$this->isInteger( $value ) ) { + $value = serialize( $value ); + } + + xcache_set( $key, $value, $expire ); + return true; + } + + /** + * Remove a value from the XCache object cache + * + * @param string $key Cache key + * @return bool + */ + public function delete( $key ) { + xcache_unset( $key ); + return true; + } + + public function incr( $key, $value = 1 ) { + return xcache_inc( $key, $value ); + } + + public function decr( $key, $value = 1 ) { + return xcache_dec( $key, $value ); + } +} diff --git a/includes/libs/replacers/DoubleReplacer.php b/includes/libs/replacers/DoubleReplacer.php new file mode 100644 index 00000000..fed023b1 --- /dev/null +++ b/includes/libs/replacers/DoubleReplacer.php @@ -0,0 +1,43 @@ +<?php +/** + * 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 + */ + +/** + * Class to perform secondary replacement within each replacement string + */ +class DoubleReplacer extends Replacer { + /** + * @param mixed $from + * @param mixed $to + * @param int $index + */ + public function __construct( $from, $to, $index = 0 ) { + $this->from = $from; + $this->to = $to; + $this->index = $index; + } + + /** + * @param array $matches + * @return mixed + */ + public function replace( array $matches ) { + return str_replace( $this->from, $this->to, $matches[$this->index] ); + } +} diff --git a/includes/libs/replacers/HashtableReplacer.php b/includes/libs/replacers/HashtableReplacer.php new file mode 100644 index 00000000..b3c219d4 --- /dev/null +++ b/includes/libs/replacers/HashtableReplacer.php @@ -0,0 +1,44 @@ +<?php +/** + * 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 + */ + +/** + * Class to perform replacement based on a simple hashtable lookup + */ +class HashtableReplacer extends Replacer { + private $table, $index; + + /** + * @param array $table + * @param int $index + */ + public function __construct( $table, $index = 0 ) { + $this->table = $table; + $this->index = $index; + } + + /** + * @param array $matches + * @return mixed + */ + public function replace( array $matches ) { + return $this->table[$matches[$this->index]]; + } +} + diff --git a/includes/libs/replacers/RegexlikeReplacer.php b/includes/libs/replacers/RegexlikeReplacer.php new file mode 100644 index 00000000..2b1fa740 --- /dev/null +++ b/includes/libs/replacers/RegexlikeReplacer.php @@ -0,0 +1,46 @@ +<?php +/** + * 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 + */ + +/** + * Class to replace regex matches with a string similar to that used in preg_replace() + */ +class RegexlikeReplacer extends Replacer { + private $r; + + /** + * @param string $r + */ + public function __construct( $r ) { + $this->r = $r; + } + + /** + * @param array $matches + * @return string + */ + public function replace( array $matches ) { + $pairs = array(); + foreach ( $matches as $i => $match ) { + $pairs["\$$i"] = $match; + } + + return strtr( $this->r, $pairs ); + } +} diff --git a/includes/libs/replacers/Replacer.php b/includes/libs/replacers/Replacer.php new file mode 100644 index 00000000..f4850bf6 --- /dev/null +++ b/includes/libs/replacers/Replacer.php @@ -0,0 +1,38 @@ +<?php +/** + * 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 + */ + +/** + * Base class for "replacers", objects used in preg_replace_callback() and + * StringUtils::delimiterReplaceCallback() + */ +abstract class Replacer { + /** + * @return array + */ + public function cb() { + return array( &$this, 'replace' ); + } + + /** + * @param array $matches + * @return string + */ + abstract public function replace( array $matches ); +} diff --git a/includes/libs/virtualrest/ParsoidVirtualRESTService.php b/includes/libs/virtualrest/ParsoidVirtualRESTService.php new file mode 100644 index 00000000..32a27f79 --- /dev/null +++ b/includes/libs/virtualrest/ParsoidVirtualRESTService.php @@ -0,0 +1,126 @@ +<?php +/** + * Virtual HTTP service client for Parsoid + * + * 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 + */ + +/** + * Virtual REST service for Parsoid + * @since 1.25 + */ +class ParsoidVirtualRESTService extends VirtualRESTService { + /** + * Example requests: + * GET /local/v1/page/$title/html/$oldid + * * $oldid is optional + * POST /local/v1/transform/html/to/wikitext/$title/$oldid + * * body: array( 'html' => ... ) + * * $title and $oldid are optional + * POST /local/v1/transform/wikitext/to/html/$title + * * body: array( 'wikitext' => ... ) or array( 'wikitext' => ..., 'body' => true/false ) + * * $title is optional + * @param array $params Key/value map + * - url : Parsoid server URL + * - prefix : Parsoid prefix for this wiki + * - timeout : Parsoid timeout (optional) + * - forwardCookies : Cookies to forward to Parsoid, or false. (optional) + * - HTTPProxy : Parsoid HTTP proxy (optional) + */ + public function __construct( array $params ) { + // for backwards compatibility: + if ( isset( $params['URL'] ) ) { + $params['url'] = $params['URL']; + unset( $params['URL'] ); + } + parent::__construct( $params ); + } + + public function onRequests( array $reqs, Closure $idGeneratorFunc ) { + $result = array(); + foreach ( $reqs as $key => $req ) { + $parts = explode( '/', $req['url'] ); + + list( + $targetWiki, // 'local' + $version, // 'v1' + $reqType // 'page' or 'transform' + ) = $parts; + + if ( $targetWiki !== 'local' ) { + throw new Exception( "Only 'local' target wiki is currently supported" ); + } elseif ( $version !== 'v1' ) { + throw new Exception( "Only version 1 exists" ); + } elseif ( $reqType !== 'page' && $reqType !== 'transform' ) { + throw new Exception( "Request type must be either 'page' or 'transform'" ); + } + + $req['url'] = $this->params['url'] . '/' . urlencode( $this->params['prefix'] ) . '/'; + + if ( $reqType === 'page' ) { + $title = $parts[3]; + if ( $parts[4] !== 'html' ) { + throw new Exception( "Only 'html' output format is currently supported" ); + } + if ( isset( $parts[5] ) ) { + $req['url'] .= $title . '?oldid=' . $parts[5]; + } else { + $req['url'] .= $title; + } + } elseif ( $reqType === 'transform' ) { + if ( $parts[4] !== 'to' ) { + throw new Exception( "Part index 4 is not 'to'" ); + } + + if ( isset( $parts[6] ) ) { + $req['url'] .= $parts[6]; + } + + if ( $parts[3] === 'html' & $parts[5] === 'wikitext' ) { + if ( !isset( $req['body']['html'] ) ) { + throw new Exception( "You must set an 'html' body key for this request" ); + } + if ( isset( $parts[7] ) ) { + $req['body']['oldid'] = $parts[7]; + } + } elseif ( $parts[3] == 'wikitext' && $parts[5] == 'html' ) { + if ( !isset( $req['body']['wikitext'] ) ) { + throw new Exception( "You must set a 'wikitext' body key for this request" ); + } + $req['body']['wt'] = $req['body']['wikitext']; + unset( $req['body']['wikitext'] ); + } else { + throw new Exception( "Transformation unsupported" ); + } + } + + if ( isset( $this->params['HTTPProxy'] ) && $this->params['HTTPProxy'] ) { + $req['proxy'] = $this->params['HTTPProxy']; + } + if ( isset( $this->params['timeout'] ) ) { + $req['reqTimeout'] = $this->params['timeout']; + } + + // Forward cookies + if ( isset( $this->params['forwardCookies'] ) ) { + $req['headers']['Cookie'] = $this->params['forwardCookies']; + } + + $result[$key] = $req; + } + return $result; + } +} diff --git a/includes/libs/virtualrest/RestbaseVirtualRESTService.php b/includes/libs/virtualrest/RestbaseVirtualRESTService.php new file mode 100644 index 00000000..8fe5b921 --- /dev/null +++ b/includes/libs/virtualrest/RestbaseVirtualRESTService.php @@ -0,0 +1,177 @@ +<?php +/** + * Virtual HTTP service client for Restbase + * + * 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 + */ + +/** + * Virtual REST service for Restbase + * @since 1.25 + */ +class RestbaseVirtualRESTService extends VirtualRESTService { + /** + * Example requests: + * GET /local/v1/page/{title}/html{/revision} + * POST /local/v1/transform/html/to/wikitext{/title}{/revision} + * * body: array( 'html' => ... ) + * POST /local/v1/transform/wikitext/to/html{/title}{/revision} + * * body: array( 'wikitext' => ... ) or array( 'wikitext' => ..., 'bodyOnly' => true/false ) + * + * @param array $params Key/value map + * - url : Restbase server URL + * - domain : Wiki domain to use + * - timeout : request timeout in seconds (optional) + * - forwardCookies : cookies to forward to Restbase/Parsoid (as a Cookie + * header string) or false (optional) + * Note: forwardCookies will in the future be a boolean + * only, signifing request cookies should be forwarded + * to the service; the current state is due to the way + * VE handles this particular parameter + * - HTTPProxy : HTTP proxy to use (optional) + * - parsoidCompat : whether to parse URL as if they were meant for Parsoid + * boolean (optional) + */ + public function __construct( array $params ) { + // set up defaults and merge them with the given params + $mparams = array_merge( array( + 'url' => 'http://localhost:7231', + 'domain' => 'localhost', + 'timeout' => 100, + 'forwardCookies' => false, + 'HTTPProxy' => null, + 'parsoidCompat' => false + ), $params ); + // ensure the correct domain format + $mparams['domain'] = preg_replace( + '/^(https?:\/\/)?([^\/:]+?)(\/|:\d+\/?)?$/', + '$2', + $mparams['domain'] + ); + parent::__construct( $mparams ); + } + + public function onRequests( array $reqs, Closure $idGenFunc ) { + + if ( $this->params['parsoidCompat'] ) { + return $this->onParsoidRequests( $reqs, $idGenFunc ); + } + + $result = array(); + foreach ( $reqs as $key => $req ) { + // replace /local/ with the current domain + $req['url'] = preg_replace( '/^\/local\//', '/' . $this->params['domain'] . '/', $req['url'] ); + // and prefix it with the service URL + $req['url'] = $this->params['url'] . $req['url']; + // set the appropriate proxy, timeout and headers + if ( $this->params['HTTPProxy'] ) { + $req['proxy'] = $this->params['HTTPProxy']; + } + if ( $this->params['timeout'] != null ) { + $req['reqTimeout'] = $this->params['timeout']; + } + if ( $this->params['forwardCookies'] ) { + $req['headers']['Cookie'] = $this->params['forwardCookies']; + } + $result[$key] = $req; + } + + return $result; + + } + + /** + * Remaps Parsoid requests to Restbase paths + */ + public function onParsoidRequests( array $reqs, Closure $idGeneratorFunc ) { + + $result = array(); + foreach ( $reqs as $key => $req ) { + $parts = explode( '/', $req['url'] ); + list( + $targetWiki, // 'local' + $version, // 'v1' + $reqType // 'page' or 'transform' + ) = $parts; + if ( $targetWiki !== 'local' ) { + throw new Exception( "Only 'local' target wiki is currently supported" ); + } elseif ( $reqType !== 'page' && $reqType !== 'transform' ) { + throw new Exception( "Request type must be either 'page' or 'transform'" ); + } + $req['url'] = $this->params['url'] . '/' . $this->params['domain'] . '/v1/' . $reqType . '/'; + if ( $reqType === 'page' ) { + $title = $parts[3]; + if ( $parts[4] !== 'html' ) { + throw new Exception( "Only 'html' output format is currently supported" ); + } + $req['url'] .= 'html/' . $title; + if ( isset( $parts[5] ) ) { + $req['url'] .= '/' . $parts[5]; + } elseif ( isset( $req['query']['oldid'] ) && $req['query']['oldid'] ) { + $req['url'] .= '/' . $req['query']['oldid']; + unset( $req['query']['oldid'] ); + } + } elseif ( $reqType === 'transform' ) { + // from / to transform + $req['url'] .= $parts[3] . '/to/' . $parts[5]; + // the title + if ( isset( $parts[6] ) ) { + $req['url'] .= '/' . $parts[6]; + } + // revision id + if ( isset( $parts[7] ) ) { + $req['url'] .= '/' . $parts[7]; + } elseif ( isset( $req['body']['oldid'] ) && $req['body']['oldid'] ) { + $req['url'] .= '/' . $req['body']['oldid']; + unset( $req['body']['oldid'] ); + } + if ( $parts[4] !== 'to' ) { + throw new Exception( "Part index 4 is not 'to'" ); + } + if ( $parts[3] === 'html' & $parts[5] === 'wikitext' ) { + if ( !isset( $req['body']['html'] ) ) { + throw new Exception( "You must set an 'html' body key for this request" ); + } + } elseif ( $parts[3] == 'wikitext' && $parts[5] == 'html' ) { + if ( !isset( $req['body']['wikitext'] ) ) { + throw new Exception( "You must set a 'wikitext' body key for this request" ); + } + if ( isset( $req['body']['body'] ) ) { + $req['body']['bodyOnly'] = $req['body']['body']; + unset( $req['body']['body'] ); + } + } else { + throw new Exception( "Transformation unsupported" ); + } + } + // set the appropriate proxy, timeout and headers + if ( $this->params['HTTPProxy'] ) { + $req['proxy'] = $this->params['HTTPProxy']; + } + if ( $this->params['timeout'] != null ) { + $req['reqTimeout'] = $this->params['timeout']; + } + if ( $this->params['forwardCookies'] ) { + $req['headers']['Cookie'] = $this->params['forwardCookies']; + } + $result[$key] = $req; + } + + return $result; + + } + +} diff --git a/includes/libs/virtualrest/VirtualRESTServiceClient.php b/includes/libs/virtualrest/VirtualRESTServiceClient.php index 2d21d3cf..e8bb38d8 100644 --- a/includes/libs/virtualrest/VirtualRESTServiceClient.php +++ b/includes/libs/virtualrest/VirtualRESTServiceClient.php @@ -125,17 +125,17 @@ class VirtualRESTServiceClient { * - reason : HTTP response reason (empty if there was a serious cURL error) * - headers : <header name/value associative array> * - body : HTTP response body or resource (if "stream" was set) - * - err : Any cURL error string + * - error : Any cURL error string * The map also stores integer-indexed copies of these values. This lets callers do: * <code> * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $client->run( $req ); * </code> - * @param array $req Virtual HTTP request array + * @param array $req Virtual HTTP request maps * @return array Response array for request */ public function run( array $req ) { - $req = $this->runMulti( array( $req ) ); - return $req[0]['response']; + $responses = $this->runMulti( array( $req ) ); + return $responses[0]; } /** @@ -146,14 +146,15 @@ class VirtualRESTServiceClient { * - reason : HTTP response reason (empty if there was a serious cURL error) * - headers : <header name/value associative array> * - body : HTTP response body or resource (if "stream" was set) - * - err : Any cURL error string + * - error : Any cURL error string * The map also stores integer-indexed copies of these values. This lets callers do: - * <code> - * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $responses[0]; + * <code> + * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $responses[0]; * </code> * - * @param array $req Map of Virtual HTTP request arrays + * @param array $reqs Map of Virtual HTTP request maps * @return array $reqs Map of corresponding response values with the same keys/order + * @throws Exception */ public function runMulti( array $reqs ) { foreach ( $reqs as $index => &$req ) { @@ -207,6 +208,9 @@ class VirtualRESTServiceClient { if ( ++$rounds > 5 ) { // sanity throw new Exception( "Too many replacement rounds detected. Aborting." ); } + // Track requests executed this round that have a prefix/service. + // Note that this also includes requests where 'response' was forced. + $checkReqIndexesByPrefix = array(); // Resolve the virtual URLs valid and qualified HTTP(S) URLs // and add any required authentication headers for the backend. // Services can also replace requests with new ones, either to @@ -219,7 +223,7 @@ class VirtualRESTServiceClient { if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) { // A current or original request which was not modified } else { - // Replacement requests with pre-set responses should not execute + // Replacement request that will convert to original requests $newReplaceReqsByService[$prefix][$index] = $req; } if ( isset( $req['response'] ) ) { @@ -231,6 +235,7 @@ class VirtualRESTServiceClient { // Original or mangled request included $executeReqs[$index] = $req; } + $checkReqIndexesByPrefix[$prefix][$index] = 1; } } // Update index of requests to inspect for replacement @@ -245,12 +250,12 @@ class VirtualRESTServiceClient { // defer the original or to set a proxy response to the original. // Any replacement requests executed above will need to be replaced // with new requests (eventually the original). The responses can be - // forced instead of having the request sent over the wire. + // forced by setting 'response' rather than actually be sent over the wire. $newReplaceReqsByService = array(); - foreach ( $replaceReqsByService as $prefix => $servReqs ) { + foreach ( $checkReqIndexesByPrefix as $prefix => $servReqIndexes ) { $service = $this->instances[$prefix]; - // Only the request copies stored in $doneReqs actually have the response - $servReqs = array_intersect_key( $doneReqs, $servReqs ); + // $doneReqs actually has the requests (with 'response' set) + $servReqs = array_intersect_key( $doneReqs, $servReqIndexes ); foreach ( $service->onResponses( $servReqs, $idFunc ) as $index => $req ) { // Services use unique IDs for replacement requests if ( isset( $servReqs[$index] ) || isset( $origPending[$index] ) ) { |