summaryrefslogtreecommitdiff
path: root/includes/libs
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2015-06-04 07:31:04 +0200
committerPierre Schmitz <pierre@archlinux.de>2015-06-04 07:58:39 +0200
commitf6d65e533c62f6deb21342d4901ece24497b433e (patch)
treef28adf0362d14bcd448f7b65a7aaf38650f923aa /includes/libs
parentc27b2e832fe25651ef2410fae85b41072aae7519 (diff)
Update to MediaWiki 1.25.1
Diffstat (limited to 'includes/libs')
-rw-r--r--includes/libs/APACHE-LICENSE-2.0.txt202
-rw-r--r--includes/libs/ArrayUtils.php187
-rw-r--r--includes/libs/BufferingStatsdDataFactory.php59
-rw-r--r--includes/libs/CSSJanus.php458
-rw-r--r--includes/libs/CSSMin.php151
-rw-r--r--includes/libs/Cookie.php291
-rw-r--r--includes/libs/DeferredStringifier.php57
-rw-r--r--includes/libs/ExplodeIterator.php116
-rw-r--r--includes/libs/GenericArrayObject.php6
-rw-r--r--includes/libs/IPSet.php3
-rw-r--r--includes/libs/MapCacheLRU.php131
-rw-r--r--includes/libs/MessageSpecifier.php39
-rw-r--r--includes/libs/MultiHttpClient.php28
-rw-r--r--includes/libs/ObjectFactory.php93
-rw-r--r--includes/libs/ProcessCacheLRU.php39
-rw-r--r--includes/libs/ReplacementArray.php125
-rw-r--r--includes/libs/RunningStat.php8
-rw-r--r--includes/libs/ScopedCallback.php12
-rw-r--r--includes/libs/StatusValue.php316
-rw-r--r--includes/libs/StringUtils.php317
-rw-r--r--includes/libs/UDPTransport.php102
-rw-r--r--includes/libs/Xhprof.php445
-rw-r--r--includes/libs/XmlTypeCheck.php2
-rw-r--r--includes/libs/composer/ComposerJson.php54
-rw-r--r--includes/libs/composer/ComposerLock.php38
-rw-r--r--includes/libs/jsminplus.php2
-rw-r--r--includes/libs/lessc.inc.php3796
-rw-r--r--includes/libs/normal/UtfNormal.php129
-rw-r--r--includes/libs/normal/UtfNormalDefines.php186
-rw-r--r--includes/libs/normal/UtfNormalUtil.php99
-rw-r--r--includes/libs/objectcache/APCBagOStuff.php69
-rw-r--r--includes/libs/objectcache/BagOStuff.php438
-rw-r--r--includes/libs/objectcache/EmptyBagOStuff.php45
-rw-r--r--includes/libs/objectcache/HashBagOStuff.php87
-rw-r--r--includes/libs/objectcache/WinCacheBagOStuff.php99
-rw-r--r--includes/libs/objectcache/XCacheBagOStuff.php89
-rw-r--r--includes/libs/replacers/DoubleReplacer.php43
-rw-r--r--includes/libs/replacers/HashtableReplacer.php44
-rw-r--r--includes/libs/replacers/RegexlikeReplacer.php46
-rw-r--r--includes/libs/replacers/Replacer.php38
-rw-r--r--includes/libs/virtualrest/ParsoidVirtualRESTService.php126
-rw-r--r--includes/libs/virtualrest/RestbaseVirtualRESTService.php177
-rw-r--r--includes/libs/virtualrest/VirtualRESTServiceClient.php31
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] ) ) {