mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/TemplateStyles
synced 2024-11-15 03:35:47 +00:00
Merge "Use wikimedia/css-sanitizer, and rewrite the hooking"
This commit is contained in:
commit
7b28582fcd
23
.gitignore
vendored
23
.gitignore
vendored
|
@ -1,2 +1,23 @@
|
|||
/composer.lock
|
||||
# Editors
|
||||
*.kate-swp
|
||||
*~
|
||||
\#*#
|
||||
.#*
|
||||
.*.swp
|
||||
.project
|
||||
cscope.files
|
||||
cscope.out
|
||||
*.orig
|
||||
## NetBeans
|
||||
nbproject*
|
||||
project.index
|
||||
## Sublime
|
||||
sublime-*
|
||||
sftp-config.json
|
||||
|
||||
# Composer
|
||||
/vendor
|
||||
/composer.lock
|
||||
|
||||
# NPM
|
||||
/node_modules
|
||||
|
|
339
COPYING
Normal file
339
COPYING
Normal file
|
@ -0,0 +1,339 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
369
CSSParser.php
369
CSSParser.php
|
@ -1,369 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* @ingroup Extensions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a style sheet as a structured tree, organized
|
||||
* in rule blocks nested in at-rule blocks.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
class CSSParser {
|
||||
|
||||
/** @var array $tokens */
|
||||
private $tokens;
|
||||
/** @var int $index */
|
||||
private $index;
|
||||
|
||||
/**
|
||||
* Parse and (minimally) validate the passed string as a CSS, and
|
||||
* constructs an array of tokens for parsing, as well as an index
|
||||
* into that array.
|
||||
*
|
||||
* Internally, the class behaves as a lexer.
|
||||
*
|
||||
* @param string $css
|
||||
*/
|
||||
function __construct( $css ) {
|
||||
$this->index = 0;
|
||||
preg_match_all( '/(
|
||||
[ \n\t]+
|
||||
(?# Sequences of whitespace )
|
||||
| \/\* (?: [^*]+ | \*[^\/] )* \*\/ [ \n\t]*
|
||||
(?# Comments and any trailing whitespace )
|
||||
| " (?: [^"\\\\\n]+ | \\\\. )* ["\n]
|
||||
(?# Double-quoted string literals (to newline when unclosed )
|
||||
| \' (?: [^\'\\\\\n]+ | \\\\. )* [\'\n]
|
||||
(?# Single-quoted string literals (to newline when unclosed )
|
||||
| [+-]? (?: [0-9]* \. )? [0-9]+ (?: [_a-z][_a-z0-9-]* | % )?
|
||||
(?# Numerical literals - including optional trailing units or percent sign )
|
||||
| @? -? (?: [_a-z] | \\\\[0-9a-f]{1,6} [ \n\t]? )
|
||||
(?: [_a-z0-9-]+ | \\\\[0-9a-f]{1,6} [ \n\t]? | [^\0-\177] )* (?: [ \n\t]* \( )?
|
||||
(?# Identifiers - including leading `@` for at-rule blocks )
|
||||
(?# Trailing open captures are captured to match functional values )
|
||||
| \# (?: [_a-z0-9-]+ | \\\\[0-9a-f]{1,6} [ \n\t]? | [^\0-\177] )*
|
||||
(?# So-called hatch literals )
|
||||
| u\+ [0-9a-f]{1,6} (?: - [0-9a-f]{1,6} )?
|
||||
(?# Unicode range literals )
|
||||
| u\+ [0-9a-f?]{1,6}
|
||||
(?# Unicode mask literals )
|
||||
| .
|
||||
(?# Any unmatched token is reduced to single characters )
|
||||
)/xis',
|
||||
$css,
|
||||
$match
|
||||
);
|
||||
|
||||
$inWhitespaceRun = false;
|
||||
foreach ( $match[0] as $token ) {
|
||||
if ( preg_match( '/^(?: [ \n\t] | \/\* )/x', $token ) ) {
|
||||
// Fold any sequence of whitespace to a single space token
|
||||
if ( !$inWhitespaceRun ) {
|
||||
$inWhitespaceRun = true;
|
||||
$this->tokens[] = ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
} else {
|
||||
// Decode any hexadecimal escape character into its
|
||||
// corresponding UTF-8 sequence - output is UTF-8 so the
|
||||
// escaping is unnecessary and this prevents trying to
|
||||
// obfuscate ASCII in identifiers to prevent matches.
|
||||
$token = preg_replace_callback(
|
||||
'/\\\\([0-9a-f]{1,6})[ \n\t]?/',
|
||||
function( $match ) {
|
||||
return html_entity_decode(
|
||||
'&#x' . $match[1] . ';', ENT_NOQUOTES, 'UTF-8' );
|
||||
},
|
||||
$token
|
||||
);
|
||||
|
||||
// Close unclosed string literals
|
||||
if ( preg_match( '/^ ([\'"]) (.*) \n $/x', $token, $match ) ) {
|
||||
$token = $match[1] . $match[2] . $match[1];
|
||||
}
|
||||
$inWhitespaceRun = false;
|
||||
$this->tokens[] = $token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a token from the input stream without advancing the current
|
||||
* position.
|
||||
*
|
||||
* @param int $offset Offset from current stream location
|
||||
* @return string|null Token or null if offset is past the end of the
|
||||
* input stream
|
||||
*/
|
||||
private function peek( $offset = 0 ) {
|
||||
if ( ( $this->index + $offset ) >= count( $this->tokens ) ) {
|
||||
return null;
|
||||
}
|
||||
return $this->tokens[$this->index + $offset];
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a list of tokens from the input stream.
|
||||
*
|
||||
* @param int $num Number of tokens to take
|
||||
* @return array List of tokens
|
||||
*/
|
||||
private function consume( $num = 1 ) {
|
||||
if ( $num > 0 ) {
|
||||
if ( $this->index+$num >= count( $this->tokens ) ) {
|
||||
$num = count( $this->tokens ) - $this->index;
|
||||
}
|
||||
$text = array_slice( $this->tokens, $this->index, $num );
|
||||
$this->index += $num;
|
||||
return $text;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Take tokens from the input stream up to but not including a delimiter
|
||||
* from the provided list.
|
||||
*
|
||||
* The next token in the stream should always be validated after using
|
||||
* this function as it may return early if the end of the token stream is
|
||||
* reached.
|
||||
*
|
||||
* @param array $delim List of delimiters
|
||||
* @return array List of tokens
|
||||
*/
|
||||
private function consumeTo( $delim ) {
|
||||
if ( !in_array( null, $delim ) ) {
|
||||
// Make sure we don't hit an infinte loop on malformed input
|
||||
$delim[] = null;
|
||||
}
|
||||
$consume = 0;
|
||||
while ( !in_array( $this->peek( $consume ), $delim ) ) {
|
||||
$consume++;
|
||||
}
|
||||
return $this->consume( $consume );
|
||||
}
|
||||
|
||||
/**
|
||||
* Take consecutive whitespace tokens from the input stream.
|
||||
*
|
||||
* @return array List of whitespace tokens
|
||||
*/
|
||||
private function consumeWS() {
|
||||
$consume = 0;
|
||||
while ( $this->peek( $consume ) === ' ' ) {
|
||||
$consume++;
|
||||
}
|
||||
return $this->consume( $consume );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSS declaration.
|
||||
*
|
||||
* Grammar:
|
||||
*
|
||||
* decl : WS* IDENT WS* ':' TOKEN* ';'
|
||||
* | WS* IDENT <error> ';' -> skip
|
||||
* ;
|
||||
*
|
||||
* @return array [ name => value ]
|
||||
*/
|
||||
private function parseDecl() {
|
||||
$this->consumeWS();
|
||||
$name = $this->consume()[0];
|
||||
$this->consumeWS();
|
||||
if ( $this->peek() != ':' ) {
|
||||
$this->consumeTo( [ ';', '}' ] );
|
||||
if ( $this->peek() ) {
|
||||
$this->consume();
|
||||
$this->consumeWS();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
$this->consume();
|
||||
$this->consumeWS();
|
||||
$value = $this->consumeTo( [ ';', '}' ] );
|
||||
if ( $this->peek() === ';' ) {
|
||||
$this->consume();
|
||||
$this->consumeWS();
|
||||
}
|
||||
return [ $name => $value ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a list of CSS declarations.
|
||||
*
|
||||
* Grammar:
|
||||
*
|
||||
* decls : '}'
|
||||
* | decl decls
|
||||
* ;
|
||||
*
|
||||
* @return array List of decls
|
||||
* @see parseDecl
|
||||
*/
|
||||
private function parseDecls() {
|
||||
$decls = [];
|
||||
while ( $this->peek() !== null && $this->peek() != '}' ) {
|
||||
$decl = $this->parseDecl();
|
||||
if ( $decl ) {
|
||||
foreach ( $decl as $k => $d ) {
|
||||
$decls[$k] = $d;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $decls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CSS rule.
|
||||
*
|
||||
* Grammar:
|
||||
*
|
||||
* rule : WS* selectors ';'
|
||||
* | WS* selectors '{' decls
|
||||
* ;
|
||||
* selectors : TOKEN*
|
||||
* | selectors ',' TOKEN*
|
||||
* ;
|
||||
*
|
||||
* @param string $baseSelectors Selector to prepend to all rules to
|
||||
* enforce scoping.
|
||||
* @return array|null [ selectors => [ selector* ], decls => [ decl* ] ]
|
||||
*/
|
||||
public function parseRule( $baseSelectors ) {
|
||||
$selectors = [];
|
||||
$text = '';
|
||||
$this->consumeWS();
|
||||
while ( !in_array( $this->peek(), [ '{', ';', null ] ) ) {
|
||||
if ( $this->peek() === ',' ) {
|
||||
if ( $text !== '' ) {
|
||||
$selectors[] = "{$baseSelectors}{$text}";
|
||||
}
|
||||
$this->consume();
|
||||
$this->consumeWS();
|
||||
$text = '';
|
||||
} else {
|
||||
$text .= $this->consume()[0];
|
||||
}
|
||||
}
|
||||
if ( $text !== '' ) {
|
||||
$selectors[] = "{$baseSelectors}{$text}";
|
||||
}
|
||||
if ( $this->peek() === '{' ) {
|
||||
$this->consume();
|
||||
return [
|
||||
'selectors' => $selectors,
|
||||
'decls' => $this->parseDecls()
|
||||
];
|
||||
}
|
||||
if ( $this->peek( 0 ) ) {
|
||||
$this->consume();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the token array, and returns a tree representing the CSS
|
||||
* suitable for feeding CSSRenderer objects.
|
||||
*
|
||||
* Grammar:
|
||||
*
|
||||
* anyrule : ATIDENT='@media' WS* TOKEN* '{' rules '}'
|
||||
* | ATIDENT WS* TOKEN* ';'
|
||||
* | ATIDENT WS* TOKEN* '{' decls '}'
|
||||
* | rule
|
||||
* ;
|
||||
* rules : anyrule
|
||||
* | rules anyrule
|
||||
* ;
|
||||
*
|
||||
* Output:
|
||||
*
|
||||
* [ [ name=>ATIDENT? , text=>body? , rules=>rules? ]* ]
|
||||
*
|
||||
* @param string $baseSelectors Selector to prepend to all rules to
|
||||
* enforce scoping.
|
||||
* @param array $end An array of string representing tokens that can end
|
||||
* the parse. Defaults to ending only at the end of the string.
|
||||
* @return array A tree describing the CSS rule blocks.
|
||||
*/
|
||||
public function rules( $baseSelectors = '', $end = [ null ] ) {
|
||||
$atrules = [];
|
||||
$rules = [];
|
||||
$this->consumeWS();
|
||||
while ( !in_array( $this->peek(), $end ) ) {
|
||||
if ( strtolower( $this->peek() ) === '@media' ) {
|
||||
$at = $this->consume()[0];
|
||||
$this->consumeWS();
|
||||
|
||||
$text = '';
|
||||
while ( !in_array( $this->peek(), [ '{', ';', null ] ) ) {
|
||||
$text .= $this->consume()[0];
|
||||
}
|
||||
|
||||
if ( $this->peek() === '{' ) {
|
||||
$this->consume();
|
||||
$r = $this->rules( $baseSelectors, [ '}', null ] );
|
||||
if ( $r ) {
|
||||
$atrules[] = [
|
||||
'name' => $at,
|
||||
'text' => $text,
|
||||
'rules' => $r,
|
||||
];
|
||||
}
|
||||
|
||||
} else {
|
||||
$atrules[] = [
|
||||
'name' => $at,
|
||||
'text' => $text,
|
||||
];
|
||||
}
|
||||
} elseif ( $this->peek()[0] === '@' ) {
|
||||
$at = $this->consume()[0];
|
||||
$text = '';
|
||||
while ( !in_array( $this->peek(), [ '{', ';', null ] ) ) {
|
||||
$text .= $this->consume()[0];
|
||||
}
|
||||
if ( $this->peek() === '{' ) {
|
||||
$this->consume();
|
||||
$decl = $this->parseDecls();
|
||||
if ( $decl ) {
|
||||
$atrules[] = [
|
||||
'name' => $at,
|
||||
'text' => $text,
|
||||
'rules' => [
|
||||
'selectors' => '',
|
||||
'decls' => $decl,
|
||||
],
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$atrules[] = [
|
||||
'name' => $at,
|
||||
'text' => $text,
|
||||
];
|
||||
}
|
||||
} elseif ( $this->peek() === '}' ) {
|
||||
$this->consume();
|
||||
} else {
|
||||
$rules[] = $this->parseRule( $baseSelectors );
|
||||
}
|
||||
$this->consumeWS();
|
||||
}
|
||||
if ( $rules ) {
|
||||
$atrules[] = [
|
||||
'name' => '',
|
||||
'rules' => $rules,
|
||||
];
|
||||
}
|
||||
$this->consumeWS();
|
||||
if ( $this->peek() !== null ) {
|
||||
$this->consume();
|
||||
}
|
||||
return $atrules;
|
||||
}
|
||||
}
|
||||
|
143
CSSRenderer.php
143
CSSRenderer.php
|
@ -1,143 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* @ingroup Extensions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Collects parsed CSS trees, and merges them for rendering into text.
|
||||
*
|
||||
* @class
|
||||
*/
|
||||
class CSSRenderer {
|
||||
|
||||
/** @var array $byMedia */
|
||||
private $byMedia;
|
||||
|
||||
function __construct() {
|
||||
$this->byMedia = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds (and merge) a parsed CSS tree to the render list.
|
||||
*
|
||||
* @param array $rules The parsed tree as created by CSSParser::rules()
|
||||
* @param string $media Forcibly specified @media block selector.
|
||||
*/
|
||||
function add( $rules, $media = '' ) {
|
||||
if ( !array_key_exists( $media, $this->byMedia ) ) {
|
||||
$this->byMedia[$media] = [];
|
||||
}
|
||||
|
||||
foreach ( $rules as $rule ) {
|
||||
switch ( strtolower( $rule['name'] ) ) {
|
||||
case '@media':
|
||||
if ( $media == '' ) {
|
||||
$this->add(
|
||||
$rule['rules'],
|
||||
"@media {$rule['text']}"
|
||||
);
|
||||
}
|
||||
break;
|
||||
case '':
|
||||
$this->byMedia[$media] = array_merge(
|
||||
$this->byMedia[$media],
|
||||
$rule['rules']
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the collected CSS trees into a string suitable for inclusion
|
||||
* in a <style> tag.
|
||||
*
|
||||
* @param array $functionWhitelist List of functions that are allowed
|
||||
* @param array $propertyBlacklist List of properties that not allowed
|
||||
* @return string Rendered CSS
|
||||
*/
|
||||
function render(
|
||||
array $functionWhitelist = [],
|
||||
array $propertyBlacklist = []
|
||||
) {
|
||||
// Normalize whitelist and blacklist values to lowercase
|
||||
$functionWhitelist = array_map( 'strtolower', $functionWhitelist );
|
||||
$propertyBlacklist = array_map( 'strtolower', $propertyBlacklist );
|
||||
|
||||
$css = '';
|
||||
foreach ( $this->byMedia as $media => $rules ) {
|
||||
if ( $media !== '' ) {
|
||||
$css .= "{$media} {\n";
|
||||
}
|
||||
foreach ( $rules as $rule ) {
|
||||
if ( $rule !== null ) {
|
||||
$css .= $this->renderRule(
|
||||
$rule, $functionWhitelist, $propertyBlacklist );
|
||||
}
|
||||
}
|
||||
if ( $media !== '' ) {
|
||||
$css .= '} ';
|
||||
}
|
||||
}
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single rule.
|
||||
*
|
||||
* @param array $rule Parsed rule
|
||||
* @param array $functionWhitelist List of functions that are allowed
|
||||
* @param array $propertyBlacklist List of properties that not allowed
|
||||
* @return string Rendered CSS
|
||||
*/
|
||||
private function renderRule(
|
||||
array $rule,
|
||||
array $functionWhitelist,
|
||||
array $propertyBlacklist
|
||||
) {
|
||||
$css = '';
|
||||
if ( $rule &&
|
||||
array_key_exists( 'selectors', $rule ) &&
|
||||
array_key_exists( 'decls', $rule )
|
||||
) {
|
||||
$css .= implode( ',', $rule['selectors'] ) . '{';
|
||||
foreach ( $rule['decls'] as $prop => $values ) {
|
||||
$css .= $this->renderDecl(
|
||||
$prop, $values, $functionWhitelist, $propertyBlacklist );
|
||||
}
|
||||
$css .= '} ';
|
||||
}
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a property declaration.
|
||||
*
|
||||
* @param string $prop Property name
|
||||
* @param array $values Parsed property values
|
||||
* @param array $functionWhitelist List of functions that are allowed
|
||||
* @param array $propertyBlacklist List of properties that not allowed
|
||||
* @return string Rendered CSS
|
||||
*/
|
||||
private function renderDecl(
|
||||
$prop,
|
||||
array $values,
|
||||
array $functionWhitelist,
|
||||
array $propertyBlacklist
|
||||
) {
|
||||
if ( in_array( strtolower( $prop ), $propertyBlacklist ) ) {
|
||||
// Property is blacklisted
|
||||
return '';
|
||||
}
|
||||
foreach ( $values as $value ) {
|
||||
if ( preg_match( '/^ (\S+) \s* \( $/x', $value, $match ) ) {
|
||||
if ( !in_array( strtolower( $match[1] ), $functionWhitelist ) ) {
|
||||
// Function is blacklisted
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $prop . ':' . implode( '', $values ) . ';';
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* TemplateStyles extension hooks
|
||||
*
|
||||
* @file
|
||||
* @ingroup Extensions
|
||||
* @license LGPL-2.0+
|
||||
*/
|
||||
class TemplateStylesHooks {
|
||||
/**
|
||||
* Register parser hooks
|
||||
* @param Parser $parser
|
||||
* @return bool
|
||||
*/
|
||||
public static function onParserFirstCallInit( &$parser ) {
|
||||
$parser->setHook( 'templatestyles', 'TemplateStylesHooks::render' );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $blob
|
||||
* @return string
|
||||
*/
|
||||
private static function decodeFromBlob( $blob ) {
|
||||
$tree = gzdecode( $blob );
|
||||
if ( $tree ) {
|
||||
$tree = unserialize( $tree );
|
||||
}
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tree
|
||||
* @return string
|
||||
*/
|
||||
private static function encodeToBlob( $tree ) {
|
||||
return gzencode( serialize( $tree ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputPage $out
|
||||
* @param ParserOutput $parserOutput
|
||||
*/
|
||||
public static function onOutputPageParserOutput( &$out, $parserOutput ) {
|
||||
$config = \MediaWiki\MediaWikiServices::getInstance()
|
||||
->getConfigFactory()
|
||||
->makeConfig( 'templatestyles' );
|
||||
$renderer = new CSSRenderer();
|
||||
$pages = [];
|
||||
|
||||
foreach ( self::getConfigArray( $config, 'Namespaces' ) as $ns ) {
|
||||
if ( array_key_exists( $ns, $parserOutput->getTemplates() ) ) {
|
||||
foreach ( $parserOutput->getTemplates()[$ns] as $title => $pageid ) {
|
||||
$pages[$pageid] = $title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $pages ) ) {
|
||||
$db = wfGetDB( DB_SLAVE );
|
||||
$res = $db->select(
|
||||
'page_props',
|
||||
[ 'pp_page', 'pp_value' ],
|
||||
[
|
||||
'pp_page' => array_keys( $pages ),
|
||||
'pp_propname' => 'templatestyles'
|
||||
],
|
||||
__METHOD__,
|
||||
[ 'ORDER BY', 'pp_page' ]
|
||||
);
|
||||
foreach ( $res as $row ) {
|
||||
$css = self::decodeFromBlob( $row->pp_value );
|
||||
if ( $css ) {
|
||||
$renderer->add( $css );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$selfcss = $parserOutput->getProperty( 'templatestyles' );
|
||||
if ( $selfcss ) {
|
||||
$selfcss = self::decodeFromBlob( $selfcss );
|
||||
if ( $selfcss ) {
|
||||
$renderer->add( $selfcss );
|
||||
}
|
||||
}
|
||||
|
||||
$css = $renderer->render(
|
||||
self::getConfigArray( $config, 'FunctionWhitelist' ),
|
||||
self::getConfigArray( $config, 'PropertyBlacklist' )
|
||||
);
|
||||
if ( $css ) {
|
||||
$out->addInlineStyle( $css );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a object-style configuration value to a plain array by
|
||||
* returning the array keys from the found configuration where the
|
||||
* associated value is truthy.
|
||||
*
|
||||
* @param Config $config Configuration instance
|
||||
* @param string $name Name of configuration option
|
||||
* @return array Configuration data
|
||||
*/
|
||||
private static function getConfigArray( Config $config, $name ) {
|
||||
return array_keys( array_filter(
|
||||
$config->get( "TemplateStyles{$name}" ),
|
||||
function ( $val ) {
|
||||
return (bool)$val;
|
||||
}
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser hook for <templatestyles>.
|
||||
* If there is a CSS provided, render its source on the page and attach the
|
||||
* parsed stylesheet to the page as a Property.
|
||||
*
|
||||
* @param string $input: The content of the tag.
|
||||
* @param array $args: The attributes of the tag.
|
||||
* @param Parser $parser: Parser instance available to render
|
||||
* wikitext into html, or parser methods.
|
||||
* @param PPFrame $frame: Can be used to see what template parameters ("{{{1}}}", etc.)
|
||||
* this hook was used with.
|
||||
*
|
||||
* @return string: HTML to insert in the page.
|
||||
*/
|
||||
public static function render( $input, $args, $parser, $frame ) {
|
||||
$css = new CSSParser( $input );
|
||||
|
||||
$parser->getOutput()->setProperty(
|
||||
'templatestyles',
|
||||
self::encodeToBlob( $css->rules( '#mw-content-text ' ) )
|
||||
);
|
||||
|
||||
// TODO: The UX would benefit from the CSS being run through the
|
||||
// hook for syntax highlighting rather that simply being presented
|
||||
// as a preformatted block.
|
||||
return
|
||||
Html::openElement( 'div', [ 'class' => 'mw-templatestyles-doc' ] )
|
||||
. Html::rawElement(
|
||||
'p',
|
||||
[ 'class' => 'mw-templatestyles-caption' ],
|
||||
wfMessage( 'templatestyles-doc-header' )
|
||||
) . Html::element(
|
||||
'pre',
|
||||
[ 'class' => 'mw-templatestyles-stylesheet' ],
|
||||
$input
|
||||
) . Html::closeElement( 'div' );
|
||||
}
|
||||
|
||||
}
|
129
TemplateStylesContent.php
Normal file
129
TemplateStylesContent.php
Normal file
|
@ -0,0 +1,129 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @license https://opensource.org/licenses/GPL-2.0 GPL-2.0+
|
||||
*/
|
||||
|
||||
use Wikimedia\CSS\Parser\Parser as CSSParser;
|
||||
use Wikimedia\CSS\Util as CSSUtil;
|
||||
|
||||
/**
|
||||
* Content object for sanitized CSS.
|
||||
*/
|
||||
class TemplateStylesContent extends TextContent {
|
||||
|
||||
public function __construct( $text, $modelId = 'sanitized-css' ) {
|
||||
parent::__construct( $text, $modelId );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle errors from the CSS parser and/or sanitizer
|
||||
* @param StatusValue $status Object to add errors to
|
||||
* @param array $errors Error array
|
||||
* @param string $severity Whether to consider errors as 'warning' or 'fatal'
|
||||
*/
|
||||
protected static function processErrors( StatusValue $status, array $errors, $severity ) {
|
||||
if ( $severity !== 'warning' && $severity !== 'fatal' ) {
|
||||
throw new \InvalidArgumentException( 'Invalid $severity' );
|
||||
}
|
||||
foreach ( $errors as $error ) {
|
||||
$error[0] = 'templatestyles-error-' . $error[0];
|
||||
call_user_func_array( [ $status, $severity ], $error );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize the content
|
||||
* @param array $options Options are:
|
||||
* - class: (string) Class to prefix selectors with
|
||||
* - flip: (bool) Have CSSJanus flip the stylesheet.
|
||||
* - minify: (bool) Whether to minify. Default true.
|
||||
* - novalue: (bool) Don't bother returning the actual stylesheet, just
|
||||
* fill the Status with warnings.
|
||||
* - severity: (string) Whether to consider errors as 'warning' or 'fatal'
|
||||
* @return Status
|
||||
*/
|
||||
public function sanitize( array $options = [] ) {
|
||||
$options += [
|
||||
'class' => false,
|
||||
'flip' => false,
|
||||
'minify' => true,
|
||||
'novalue' => false,
|
||||
'severity' => 'warning',
|
||||
];
|
||||
|
||||
$status = Status::newGood();
|
||||
|
||||
$style = $this->getNativeData();
|
||||
$maxSize = TemplateStylesHooks::getConfig()->get( 'TemplateStylesMaxStylesheetSize' );
|
||||
if ( $maxSize !== null && strlen( $style ) > $maxSize ) {
|
||||
$status->fatal(
|
||||
// Status::getWikiText() chokes on the Message::sizeParam if we
|
||||
// don't wrap it in a Message ourself.
|
||||
wfMessage( 'templatestyles-size-exceeded', $maxSize, Message::sizeParam( $maxSize ) )
|
||||
);
|
||||
return $status;
|
||||
}
|
||||
|
||||
if ( $options['flip'] ) {
|
||||
$style = CSSJanus::transform( $style, true, false );
|
||||
}
|
||||
|
||||
// Parse it, and collect any errors
|
||||
$cssParser = CSSParser::newFromString( $style );
|
||||
$stylesheet = $cssParser->parseStylesheet();
|
||||
self::processErrors( $status, $cssParser->getParseErrors(), $options['severity'] );
|
||||
|
||||
// Sanitize it, and collect any errors
|
||||
$sanitizer = TemplateStylesHooks::getSanitizer( $options['class'] ?: 'mw-parser-output' );
|
||||
$sanitizer->clearSanitizationErrors(); // Just in case
|
||||
$stylesheet = $sanitizer->sanitize( $stylesheet );
|
||||
self::processErrors( $status, $sanitizer->getSanitizationErrors(), $options['severity'] );
|
||||
$sanitizer->clearSanitizationErrors();
|
||||
|
||||
// Stringify it while minifying
|
||||
if ( !$options['novalue'] ) {
|
||||
$status->value = CSSUtil::stringify( $stylesheet, [ 'minify' => $options['minify'] ] );
|
||||
|
||||
// Sanity check, don't allow raw U+007F if one somehow sneaks through the sanitizer
|
||||
$status->value = strtr( $status->value, [ "\x7f" => '<27>' ] );
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function prepareSave( WikiPage $page, $flags, $parentRevId, User $user ) {
|
||||
return $this->sanitize( [ 'novalue' => true, 'severity' => 'fatal' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string CSS wrapped in a <pre> tag.
|
||||
*/
|
||||
protected function getHtml() {
|
||||
$html = "";
|
||||
$html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
|
||||
$html .= htmlspecialchars( $this->getNativeData() );
|
||||
$html .= "\n</pre>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
public function getParserOutput( Title $title, $revId = null,
|
||||
ParserOptions $options = null, $generateHtml = true
|
||||
) {
|
||||
if ( $options === null ) {
|
||||
$options = $this->getContentHandler()->makeParserOptions( 'canonical' );
|
||||
}
|
||||
|
||||
// Inject our warnings into the resulting ParserOutput
|
||||
$po = parent::getParserOutput( $title, $revId, $options, $generateHtml );
|
||||
$status = $this->sanitize( [ 'novalue' => true, 'class' => $options->getWrapOutputClass() ] );
|
||||
foreach ( $status->getErrors() as $error ) {
|
||||
$po->addWarning(
|
||||
Message::newFromSpecifier( array_merge( [ $error['message'] ], $error['params'] ) )->parse()
|
||||
);
|
||||
}
|
||||
return $po;
|
||||
}
|
||||
}
|
23
TemplateStylesContentHandler.php
Normal file
23
TemplateStylesContentHandler.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @license https://opensource.org/licenses/GPL-2.0 GPL-2.0+
|
||||
*/
|
||||
|
||||
/**
|
||||
* Content handler for sanitized CSS
|
||||
*/
|
||||
class TemplateStylesContentHandler extends CodeContentHandler {
|
||||
|
||||
/**
|
||||
* @param string $modelId
|
||||
*/
|
||||
public function __construct( $modelId = 'sanitized-css' ) {
|
||||
parent::__construct( $modelId, [ CONTENT_FORMAT_CSS ] );
|
||||
}
|
||||
|
||||
protected function getContentClass() {
|
||||
return TemplateStylesContent::class;
|
||||
}
|
||||
}
|
41
TemplateStylesFontFaceAtRuleSanitizer.php
Normal file
41
TemplateStylesFontFaceAtRuleSanitizer.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* @license https://opensource.org/licenses/GPL-2.0 GPL-2.0+
|
||||
*/
|
||||
|
||||
use Wikimedia\CSS\Grammar\Alternative;
|
||||
use Wikimedia\CSS\Grammar\Juxtaposition;
|
||||
use Wikimedia\CSS\Grammar\MatcherFactory;
|
||||
use Wikimedia\CSS\Grammar\Quantifier;
|
||||
use Wikimedia\CSS\Grammar\TokenMatcher;
|
||||
use Wikimedia\CSS\Objects\Token;
|
||||
use Wikimedia\CSS\Sanitizer\FontFaceAtRuleSanitizer;
|
||||
|
||||
/**
|
||||
* Extend the standard `@font-face` matcher to require a prefix on families.
|
||||
*/
|
||||
class TemplateStylesFontFaceAtRuleSanitizer extends FontFaceAtRuleSanitizer {
|
||||
|
||||
/**
|
||||
* @param MatcherFactory $matcherFactory
|
||||
*/
|
||||
public function __construct( MatcherFactory $matcherFactory ) {
|
||||
parent::__construct( $matcherFactory );
|
||||
|
||||
// Only allow the font-family if it begins with "TemplateStyles"
|
||||
$this->propertySanitizer->setKnownProperties( [
|
||||
'font-family' => new Alternative( [
|
||||
new TokenMatcher( Token::T_STRING, function ( Token $t ) {
|
||||
return substr( $t->value(), 0, 14 ) === 'TemplateStyles';
|
||||
} ),
|
||||
new Juxtaposition( [
|
||||
new TokenMatcher( Token::T_IDENT, function ( Token $t ) {
|
||||
return substr( $t->value(), 0, 14 ) === 'TemplateStyles';
|
||||
} ),
|
||||
Quantifier::star( $matcherFactory->ident() ),
|
||||
] ),
|
||||
] ),
|
||||
] + $this->propertySanitizer->getKnownProperties() );
|
||||
}
|
||||
}
|
276
TemplateStylesHooks.php
Normal file
276
TemplateStylesHooks.php
Normal file
|
@ -0,0 +1,276 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* @license https://opensource.org/licenses/GPL-2.0 GPL-2.0+
|
||||
*/
|
||||
|
||||
use Wikimedia\CSS\Objects\Token;
|
||||
use Wikimedia\CSS\Sanitizer\FontFeatureValuesAtRuleSanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\KeyframesAtRuleSanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\MediaAtRuleSanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\NamespaceAtRuleSanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\PageAtRuleSanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\Sanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\StylePropertySanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\StyleRuleSanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\StylesheetSanitizer;
|
||||
use Wikimedia\CSS\Sanitizer\SupportsAtRuleSanitizer;
|
||||
|
||||
/**
|
||||
* TemplateStyles extension hooks
|
||||
*/
|
||||
class TemplateStylesHooks {
|
||||
|
||||
/** @var Config|null */
|
||||
private static $config = null;
|
||||
|
||||
/** @var Sanitizer[] */
|
||||
private static $sanitizers = [];
|
||||
|
||||
/**
|
||||
* Get our Config
|
||||
* @return Config
|
||||
*/
|
||||
public static function getConfig() {
|
||||
if ( !self::$config ) {
|
||||
self::$config = \MediaWiki\MediaWikiServices::getInstance()
|
||||
->getConfigFactory()
|
||||
->makeConfig( 'templatestyles' );
|
||||
}
|
||||
return self::$config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get our Sanitizer
|
||||
* @param string $class Class to limit selectors to
|
||||
* @return Sanitizer
|
||||
*/
|
||||
public static function getSanitizer( $class ) {
|
||||
if ( !isset( self::$sanitizers[$class] ) ) {
|
||||
$config = TemplateStylesHooks::getConfig();
|
||||
$matcherFactory = new TemplateStylesMatcherFactory(
|
||||
$config->get( 'TemplateStylesAllowedUrls' )
|
||||
);
|
||||
|
||||
$propertySanitizer = new StylePropertySanitizer( $matcherFactory );
|
||||
$propertySanitizer->setKnownProperties( array_diff_key(
|
||||
$propertySanitizer->getKnownProperties(),
|
||||
array_flip( $config->get( 'TemplateStylesPropertyBlacklist' ) )
|
||||
) );
|
||||
Hooks::run( 'TemplateStylesPropertySanitizer', [ &$propertySanitizer, $matcherFactory ] );
|
||||
|
||||
$atRuleBlacklist = array_flip( $config->get( 'TemplateStylesAtRuleBlacklist' ) );
|
||||
$ruleSanitizers = [
|
||||
'styles' => new StyleRuleSanitizer(
|
||||
$matcherFactory->cssSelectorList(),
|
||||
$propertySanitizer,
|
||||
[
|
||||
'prependSelectors' => [
|
||||
new Token( Token::T_DELIM, '.' ),
|
||||
new Token( Token::T_IDENT, $class ),
|
||||
new Token( Token::T_WHITESPACE ),
|
||||
],
|
||||
]
|
||||
),
|
||||
'@font-face' => new TemplateStylesFontFaceAtRuleSanitizer( $matcherFactory ),
|
||||
'@font-feature-values' => new FontFeatureValuesAtRuleSanitizer( $matcherFactory ),
|
||||
'@keyframes' => new KeyframesAtRuleSanitizer( $matcherFactory, $propertySanitizer ),
|
||||
'@page' => new PageAtRuleSanitizer( $matcherFactory, $propertySanitizer ),
|
||||
'@media' => new MediaAtRuleSanitizer( $matcherFactory->cssMediaQueryList() ),
|
||||
'@supports' => new SupportsAtRuleSanitizer( $matcherFactory, [
|
||||
'declarationSanitizer' => $propertySanitizer,
|
||||
] ),
|
||||
];
|
||||
$ruleSanitizers = array_diff_key( $ruleSanitizers, $atRuleBlacklist );
|
||||
if ( isset( $ruleSanitizers['@media'] ) ) { // In case @media was blacklisted
|
||||
$ruleSanitizers['@media']->setRuleSanitizers( $ruleSanitizers );
|
||||
}
|
||||
if ( isset( $ruleSanitizers['@supports'] ) ) { // In case @supports was blacklisted
|
||||
$ruleSanitizers['@supports']->setRuleSanitizers( $ruleSanitizers );
|
||||
}
|
||||
|
||||
$allRuleSanitizers = $ruleSanitizers + [
|
||||
// Omit @import, it's not secure. Maybe someday we'll make an "@-mw-import" or something.
|
||||
'@namespace' => new NamespaceAtRuleSanitizer( $matcherFactory ),
|
||||
];
|
||||
$allRuleSanitizers = array_diff_key( $allRuleSanitizers, $atRuleBlacklist );
|
||||
$sanitizer = new StylesheetSanitizer( $allRuleSanitizers );
|
||||
Hooks::run( 'TemplateStylesStylesheetSanitizer',
|
||||
[ &$sanitizer, $propertySanitizer, $matcherFactory ]
|
||||
);
|
||||
self::$sanitizers[$class] = $sanitizer;
|
||||
}
|
||||
return self::$sanitizers[$class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update $wgTextModelsToParse
|
||||
*/
|
||||
public static function onRegistration() {
|
||||
// This gets called before ConfigFactory is set up, so I guess we need
|
||||
// to use globals.
|
||||
global $wgTextModelsToParse, $wgTemplateStylesAutoParseContent;
|
||||
|
||||
if ( in_array( CONTENT_MODEL_CSS, $wgTextModelsToParse, true ) &&
|
||||
$wgTemplateStylesAutoParseContent
|
||||
) {
|
||||
$wgTextModelsToParse[] = 'sanitized-css';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add `<templatestyles>` to the parser.
|
||||
* @param Parser &$parser Parser object being cleared
|
||||
* @return bool
|
||||
*/
|
||||
public static function onParserFirstCallInit( &$parser ) {
|
||||
$parser->setHook( 'templatestyles', 'TemplateStylesHooks::handleTag' );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix Tidy screw-ups
|
||||
*
|
||||
* It seems some versions of Tidy try to wrap the contents of a `<style>`
|
||||
* tag in bare `<![CDATA[` ... `]]>`, which makes it invalid CSS. It should
|
||||
* be wrapping those additions with CSS comments.
|
||||
*
|
||||
* @todo When we kill Tidy in favor of RemexHTML or the like, kill this too.
|
||||
* @param Parser &$parser Parser object being used
|
||||
* @param string &$text text that will be returned
|
||||
*/
|
||||
public static function onParserAfterTidy( &$parser, &$text ) {
|
||||
$text = preg_replace( '/(<(?i:style)[^>]*>\s*)(<!\[CDATA\[)/', '$1/*$2*/', $text );
|
||||
$text = preg_replace( '/(\]\]>)(\s*<\/style>)/i', '/*$1*/$2', $text );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default content model to 'sanitized-css' when appropriate.
|
||||
* @param Title $title the Title in question
|
||||
* @param string &$model The model name
|
||||
* @return bool
|
||||
*/
|
||||
public static function onContentHandlerDefaultModelFor( $title, &$model ) {
|
||||
$enabledNamespaces = self::getConfig()->get( 'TemplateStylesNamespaces' );
|
||||
if ( !empty( $enabledNamespaces[$title->getNamespace()] ) &&
|
||||
$title->isSubpage() && substr( $title->getText(), -4 ) === '.css'
|
||||
) {
|
||||
$model = 'sanitized-css';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit our CSS content model like core's CSS
|
||||
* @param Title $title Title being edited
|
||||
* @param string &$lang CodeEditor language to use
|
||||
* @param string $model Content model
|
||||
* @param string $format Content format
|
||||
* @return bool
|
||||
*/
|
||||
public static function onCodeEditorGetPageLanguage( $title, &$lang, $model, $format ) {
|
||||
if ( $model === 'sanitized-css' && self::getConfig()->get( 'TemplateStylesUseCodeEditor' ) ) {
|
||||
$lang = 'css';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser hook for `<templatestyles>`
|
||||
* @param string $text Contents of the tag (ignored).
|
||||
* @param array $params Tag attributes
|
||||
* @param Parser $parser
|
||||
* @param PPFrame $frame
|
||||
* @return string HTML
|
||||
*/
|
||||
public static function handleTag( $text, $params, $parser, $frame ) {
|
||||
global $wgContLang;
|
||||
|
||||
if ( !isset( $params['src'] ) || trim( $params['src'] ) === '' ) {
|
||||
return '<strong class="error">' .
|
||||
wfMessage( 'templatestyles-missing-src' )->inContentLanguage()->parse() .
|
||||
'</strong>';
|
||||
}
|
||||
|
||||
// Default to the Template namespace because that's the most likely
|
||||
// situation. We can't allow for subpage syntax like src="/styles.css"
|
||||
// or the like, though, because stuff like substing and Parsoid would
|
||||
// wind up wanting to make that relative to the wrong page.
|
||||
$title = Title::newFromText( $params['src'], NS_TEMPLATE );
|
||||
if ( !$title ) {
|
||||
return '<strong class="error">' .
|
||||
wfMessage( 'templatestyles-invalid-src' )->inContentLanguage()->parse() .
|
||||
'</strong>';
|
||||
}
|
||||
|
||||
$rev = $parser->fetchCurrentRevisionOfTitle( $title );
|
||||
|
||||
// It's not really a "template", but it has the same implications
|
||||
// for needing reparse when the stylesheet is edited.
|
||||
$parser->getOutput()->addTemplate( $title, $title->getArticleId(), $rev ? $rev->getId() : null );
|
||||
|
||||
$content = $rev ? $rev->getContent() : null;
|
||||
if ( !$content ) {
|
||||
$title = $title->getPrefixedText();
|
||||
return '<strong class="error">' .
|
||||
wfMessage(
|
||||
'templatestyles-bad-src-missing',
|
||||
$title,
|
||||
wfEscapeWikiText( $title )
|
||||
)->inContentLanguage()->parse() .
|
||||
'</strong>';
|
||||
}
|
||||
if ( !$content instanceof TemplateStylesContent ) {
|
||||
$title = $title->getPrefixedText();
|
||||
return '<strong class="error">' .
|
||||
wfMessage(
|
||||
'templatestyles-bad-src',
|
||||
$title,
|
||||
wfEscapeWikiText( $title ),
|
||||
ContentHandler::getLocalizedName( $content->getModel() )
|
||||
)->inContentLanguage()->parse() .
|
||||
'</strong>';
|
||||
}
|
||||
|
||||
// For the moment just output the styles inline.
|
||||
// @todo: If T160563 happens, it would be good to convert this to use that.
|
||||
|
||||
$status = $content->sanitize( [
|
||||
'flip' => $parser->getTargetLanguage()->getDir() !== $wgContLang->getDir(),
|
||||
'minify' => !ResourceLoader::inDebugMode(),
|
||||
'class' => $parser->getOptions()->getWrapOutputClass(),
|
||||
] );
|
||||
$style = $status->isOk() ? $status->getValue() : '/* Fatal error, no CSS will be output */';
|
||||
|
||||
// Prepend errors. This should normally never happen, but might if an
|
||||
// update or configuration change causes something that was formerly
|
||||
// valid to become invalid or something like that.
|
||||
if ( !$status->isGood() ) {
|
||||
$comment = wfMessage(
|
||||
'templatestyles-errorcomment',
|
||||
$title->getPrefixedText(),
|
||||
$rev->getId(),
|
||||
$status->getWikiText( null, 'rawmessage' )
|
||||
)->text();
|
||||
$comment = trim( strtr( $comment, [
|
||||
// Use some lookalike unicode characters to avoid things that might
|
||||
// otherwise confuse browsers.
|
||||
'*' => '•', '-' => '‐', '<' => '⧼', '>' => '⧽',
|
||||
] ) );
|
||||
$style = "/*\n$comment\n*/\n$style";
|
||||
}
|
||||
|
||||
// Hide the CSS from Parser::doBlockLevels
|
||||
$marker = Parser::MARKER_PREFIX . '-templatestyles-' .
|
||||
sprintf( '%08X', $parser->mMarkerIndex++ ) . Parser::MARKER_SUFFIX;
|
||||
$parser->mStripState->addNoWiki( $marker, $style );
|
||||
|
||||
// Return the inline <style>, which the Parser will wrap in a 'general'
|
||||
// strip marker.
|
||||
return Html::inlineStyle( $marker );
|
||||
}
|
||||
|
||||
}
|
80
TemplateStylesMatcherFactory.php
Normal file
80
TemplateStylesMatcherFactory.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* @license https://opensource.org/licenses/GPL-2.0 GPL-2.0+
|
||||
*/
|
||||
|
||||
use Wikimedia\CSS\Objects\ComponentValue;
|
||||
use Wikimedia\CSS\Objects\Token;
|
||||
use Wikimedia\CSS\Grammar\TokenMatcher;
|
||||
use Wikimedia\CSS\Grammar\UrlMatcher;
|
||||
|
||||
/**
|
||||
* Extend the standard factory for TemplateStyles-specific matchers
|
||||
*/
|
||||
class TemplateStylesMatcherFactory extends \Wikimedia\CSS\Grammar\MatcherFactory {
|
||||
|
||||
/** @var array URL validation regexes */
|
||||
protected $allowedDomains;
|
||||
|
||||
/**
|
||||
* @param array $allowedDomains See $wgTemplateStylesAllowedUrls
|
||||
*/
|
||||
public function __construct( array $allowedDomains ) {
|
||||
$this->allowedDomains = $allowedDomains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a URL for safety
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkUrl( $type, $url ) {
|
||||
// Undo unnecessary percent encoding
|
||||
$url = preg_replace_callback( '/%[2-7][0-9A-Fa-f]/', function ( $m ) {
|
||||
$char = urldecode( $m[0] );
|
||||
if ( strpos( '"#%<>[\]^`{|}/?&=+;', $char ) === false ) {
|
||||
# Unescape it
|
||||
return $char;
|
||||
}
|
||||
return $m[0];
|
||||
}, $url );
|
||||
|
||||
// Don't allow unescaped \ or /../ in the non-query part of the URL
|
||||
$tmp = preg_replace( '<[#?].*$>', '', $url );
|
||||
if ( strpos( $tmp, '\\' ) !== false || preg_match( '<(?:^|/|%2[fF])\.+(?:/|%2[fF]|$)>', $tmp ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run it through the whitelist
|
||||
$regexes = isset( $this->allowedDomains[$type] ) ? $this->allowedDomains[$type] : [];
|
||||
foreach ( $regexes as $regex ) {
|
||||
if ( preg_match( $regex, $url ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function urlstring( $type ) {
|
||||
$key = __METHOD__ . ':' . $type;
|
||||
if ( !isset( $this->cache[$key] ) ) {
|
||||
$this->cache[$key] = new TokenMatcher( Token::T_STRING, function ( Token $t ) use ( $type ) {
|
||||
return $this->checkUrl( $type, $t->value() );
|
||||
} );
|
||||
}
|
||||
return $this->cache[$key];
|
||||
}
|
||||
|
||||
public function url( $type ) {
|
||||
$key = __METHOD__ . ':' . $type;
|
||||
if ( !isset( $this->cache[$key] ) ) {
|
||||
$this->cache[$key] = new UrlMatcher( function ( $url, $modifiers ) use ( $type ) {
|
||||
return !$modifiers && $this->checkUrl( $type, $url );
|
||||
} );
|
||||
}
|
||||
return $this->cache[$key];
|
||||
}
|
||||
}
|
|
@ -1,8 +1,12 @@
|
|||
{
|
||||
"license": "LGPL-2.1+",
|
||||
"license": "GPL-2.0+",
|
||||
"require": {
|
||||
"cssjanus/cssjanus": "1.2.0",
|
||||
"wikimedia/css-sanitizer": "~1.0.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"jakub-onderka/php-parallel-lint": "0.9",
|
||||
"mediawiki/mediawiki-codesniffer": "0.5.0",
|
||||
"jakub-onderka/php-parallel-lint": "0.9.2",
|
||||
"mediawiki/mediawiki-codesniffer": "0.7.2",
|
||||
"jakub-onderka/php-console-highlighter": "0.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -1,46 +1,82 @@
|
|||
{
|
||||
"name": "TemplateStyles",
|
||||
"version": "0.9",
|
||||
"version": "1.0",
|
||||
"author": [
|
||||
"Brad Jorsch",
|
||||
"Marc A. Pelletier"
|
||||
],
|
||||
"url": "https://www.mediawiki.org/wiki/Extension:TemplateStyles",
|
||||
"namemsg": "templatestyles",
|
||||
"descriptionmsg": "templatestyles-desc",
|
||||
"license-name": "LGPL-2.0+",
|
||||
"type": "other",
|
||||
"license-name": "GPL-2.0+",
|
||||
"type": "parserhook",
|
||||
"manifest_version": 1,
|
||||
"load_composer_autoloader": true,
|
||||
"MessagesDirs": {
|
||||
"TemplateStyles": [
|
||||
"i18n"
|
||||
]
|
||||
},
|
||||
"AutoloadClasses": {
|
||||
"TemplateStylesHooks": "TemplateStyles.hooks.php",
|
||||
"CSSParser": "CSSParser.php",
|
||||
"CSSRenderer": "CSSRenderer.php"
|
||||
"TemplateStylesContentHandler": "TemplateStylesContentHandler.php",
|
||||
"TemplateStylesContent": "TemplateStylesContent.php",
|
||||
"TemplateStylesFontFaceAtRuleSanitizer": "TemplateStylesFontFaceAtRuleSanitizer.php",
|
||||
"TemplateStylesHooks": "TemplateStylesHooks.php",
|
||||
"TemplateStylesMatcherFactory": "TemplateStylesMatcherFactory.php"
|
||||
},
|
||||
"ContentHandlers": {
|
||||
"sanitized-css": "TemplateStylesContentHandler"
|
||||
},
|
||||
"SyntaxHighlightModels": {
|
||||
"sanitized-css": "css"
|
||||
},
|
||||
"callback": "TemplateStylesHooks::onRegistration",
|
||||
"Hooks": {
|
||||
"ParserFirstCallInit": [
|
||||
"TemplateStylesHooks::onParserFirstCallInit"
|
||||
],
|
||||
"OutputPageParserOutput": [
|
||||
"TemplateStylesHooks::onOutputPageParserOutput"
|
||||
"ParserAfterTidy": [
|
||||
"TemplateStylesHooks::onParserAfterTidy"
|
||||
],
|
||||
"ContentHandlerDefaultModelFor": [
|
||||
"TemplateStylesHooks::onContentHandlerDefaultModelFor"
|
||||
],
|
||||
"CodeEditorGetPageLanguage": [
|
||||
"TemplateStylesHooks::onCodeEditorGetPageLanguage"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"@TemplateStylesAllowedUrls": "PCRE regexes to match allowed URLs for various types of external references. Known types are:\n; audio: Sound files\n; image: Images\n; svg: SVGs for the Filter and Masking modules\n; font: External fonts\n; namespace: @namespace declarations\n; css: @import declarations\nIf you want to allow an entire domain, be sure to include a trailing '/', e.g. \"<^https://allowed\\.example\\.org/>\" rather than \"<^https://allowed\\.example\\.org>\", so people can't bypass your filter by creating a subdomain like \"https://allowed.example.org.evil.com\".",
|
||||
"TemplateStylesAllowedUrls": {
|
||||
"audio": [
|
||||
"<^https://upload\\.wikimedia\\.org/wikipedia/commons/>"
|
||||
],
|
||||
"image": [
|
||||
"<^https://upload\\.wikimedia\\.org/wikipedia/commons/>"
|
||||
],
|
||||
"svg": [
|
||||
"<^https://upload\\.wikimedia\\.org/wikipedia/commons/[^?#]*\\.svg(?:[?#]|$)>"
|
||||
],
|
||||
"font": [],
|
||||
"namespace": [
|
||||
"<.>"
|
||||
],
|
||||
"css": []
|
||||
},
|
||||
"@TemplateStylesNamespaces": "Namespaces to default the content model to CSS on .css subpages.",
|
||||
"TemplateStylesNamespaces": {
|
||||
"10": true
|
||||
},
|
||||
"TemplateStylesFunctionWhitelist": {
|
||||
"rgb": true
|
||||
},
|
||||
"TemplateStylesPropertyBlacklist": {
|
||||
"url": true,
|
||||
"behavior": true,
|
||||
"-moz-binding": true,
|
||||
"-o-link": true
|
||||
}
|
||||
"@TemplateStylesPropertyBlacklist": "Blacklist style properties that would otherwise be allowed. See also the TemplateStylesPropertySanitizer hook, which allows for finer-grained control.",
|
||||
"TemplateStylesPropertyBlacklist": [],
|
||||
"@TemplateStylesAtRuleBlacklist": "Blacklist at-rules that would otherwise be allowed. Include the '@' in the name. See also the TemplateStylesStylesheetSanitizer hook, which allows for finer-grained control.",
|
||||
"TemplateStylesAtRuleBlacklist": [],
|
||||
"@TemplateStylesUseCodeEditor": "Use CodeEditor when editing TemplateStyles CSS pages.",
|
||||
"TemplateStylesUseCodeEditor": true,
|
||||
"@TemplateStylesAutoParseContent": "Set this false if you want to manage an entry for 'sanitized-css' in $wgTextModelsToParse manually. If true, an entry will be added to $wgTextModelsToParse automatically if CONTENT_MODEL_CSS is in the array.",
|
||||
"TemplateStylesAutoParseContent": true,
|
||||
"@TemplateStylesMaxStylesheetSize": "The maximum size of a stylesheet, in bytes. Set null if you don't want to impose a limit.",
|
||||
"TemplateStylesMaxStylesheetSize": 102400
|
||||
},
|
||||
"ConfigRegistry": {
|
||||
"templatestyles": "GlobalVarConfig::newInstance"
|
||||
|
|
24
hooks.txt
Normal file
24
hooks.txt
Normal file
|
@ -0,0 +1,24 @@
|
|||
This document describes MediaWiki hooks added by the TemplateStyles extension.
|
||||
See MediaWiki core's docs/hooks.txt for details on how hooks work.
|
||||
|
||||
==Events and parameters==
|
||||
|
||||
'TemplateStylesPropertySanitizer': Allows for adjusting or replacing the
|
||||
StylePropertySanitizer used when sanitizing style rules. For example, you might
|
||||
add, remove, or redefine known properties.
|
||||
&$propertySanitizer: Wikimedia\CSS\Sanitizer\StylePropertySanitizer to be used
|
||||
for sanitization.
|
||||
$matcherFactory: Wikimedia\CSS\Grammar\MatcherFactory being used, for use in
|
||||
adding or redefining known properties or replacing the entire sanitizer.
|
||||
|
||||
'TemplateStylesStylesheetSanitizer': Allows for adjusting or replacing the
|
||||
StylesheetSanitizer. For example, you might add, remove, or redefine at-rule
|
||||
sanitizers.
|
||||
&$sanitizer: Wikimedia\CSS\Sanitizer\StylesheetSanitizer to be used for
|
||||
sanitization. The array returned by `$sanitizer->getRuleSanitizers()` will use
|
||||
the at-rule names (including the '@') as keys. The style rule sanitizer has
|
||||
key 'styles'.
|
||||
$propertySanitizer: Wikimedia\CSS\Sanitizer\StylePropertySanitizer being used
|
||||
for sanitization, for use in adding or redefining rule sanitizers.
|
||||
$matcherFactory: Wikimedia\CSS\Grammar\MatcherFactory being used, for use in
|
||||
adding or redefining rule sanitizers.
|
57
i18n/en.json
57
i18n/en.json
|
@ -1,10 +1,65 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Brad Jorsch",
|
||||
"Marc A. Pelletier"
|
||||
]
|
||||
},
|
||||
"templatestyles": "TemplateStyles",
|
||||
"templatestyles-desc": "Implement per-template style sheets",
|
||||
"templatestyles-doc-header": "Template-specific style sheet:"
|
||||
"templatestyles-missing-src": "TemplateStyles' <code>src</code> attribute must not be empty.",
|
||||
"templatestyles-invalid-src": "Invalid title for TemplateStyles' <code>src</code> attribute.",
|
||||
"templatestyles-bad-src-missing": "Page [[:$1|$2]] has no content.",
|
||||
"templatestyles-bad-src": "Page [[:$1|$2]] must have content model \"{{int:content-model-sanitized-css}}\" for TemplateStyles (current model is \"$3\").",
|
||||
"templatestyles-errorcomment": "Errors processing stylesheet [[:$1]] (rev $2):\n$3",
|
||||
"templatestyles-size-exceeded": "The stylesheet is larger than the maximum size of $2.",
|
||||
"content-model-sanitized-css": "Sanitized CSS",
|
||||
|
||||
"templatestyles-error-at-rule-block-not-allowed": "Block not allowed for <code>@$3</code> at line $1 character $2.",
|
||||
"templatestyles-error-at-rule-block-required": "Block required for <code>@$3</code> at line $1 character $2.",
|
||||
"templatestyles-error-bad-character-in-url": "Invalid character in URL at line $1 character $2.",
|
||||
"templatestyles-error-bad-escape": "Invalid character in escape at line $1 character $2.",
|
||||
"templatestyles-error-bad-value-for-property": "Invalid or unsupported value for property <code>$3</code> at line $1 character $2.",
|
||||
"templatestyles-error-expected-at-rule": "Expected <code>@$3</code> at line $1 character $2.",
|
||||
"templatestyles-error-expected-colon": "Expected a colon at line $1 character $2.",
|
||||
"templatestyles-error-expected-declaration": "Expected a declaration at line $1 character $2.",
|
||||
"templatestyles-error-expected-declaration-list": "Expected a declaration list at line $1 character $2.",
|
||||
"templatestyles-error-expected-eof": "Expected the end of the stylesheet at line $1 character $2.",
|
||||
"templatestyles-error-expected-ident": "Expected an identifier a line $1 character $2.",
|
||||
"templatestyles-error-expected-page-margin-at-rule": "Expected a <code>@page</code> margin at-rule at line $1 character $2.",
|
||||
"templatestyles-error-expected-qualified-rule": "Expected a style rule at line $1 character $2.",
|
||||
"templatestyles-error-expected-stylesheet": "Expected a stylesheet at line $1 character $2.",
|
||||
"templatestyles-error-invalid-font-face-at-rule": "<code>@font-face</code> does not allow anything before the block at line $1 character $2.",
|
||||
"templatestyles-error-invalid-font-feature-value": "<code>@$3</code> does not allow anything before the block at line $1 character $2.",
|
||||
"templatestyles-error-invalid-font-feature-value-declaration": "Invalid value for font feature value property at line $1 character $2.",
|
||||
"templatestyles-error-invalid-font-feature-values-font-list": "Invalid font list for <code>@font-feature-values</code> at line $1 character $2.",
|
||||
"templatestyles-error-invalid-import-value": "Invalid value for <code>@import</code> at line $1 character $2.",
|
||||
"templatestyles-error-invalid-keyframe-name": "Invalid keyframe name at line $1 character $2.",
|
||||
"templatestyles-error-invalid-media-query": "Invalid media query at line $1 character $2.",
|
||||
"templatestyles-error-invalid-namespace-value": "Invalid value for <code>@namespace</code> at line $1 character $2.",
|
||||
"templatestyles-error-invalid-page-margin-at-rule": "<code>@$3</code> does not allow anything before the block at line $1 character $2.",
|
||||
"templatestyles-error-invalid-page-rule-content": "Invalid content for <code>@page</code> at line $1 character $2.",
|
||||
"templatestyles-error-invalid-page-selector": "Invalid page selector at line $1 character $2.",
|
||||
"templatestyles-error-invalid-selector-list": "Invalid selector list at line $1 character $2.",
|
||||
"templatestyles-error-invalid-supports-condition": "Invalid condition for <code>@supports</code> at line $1 character $2.",
|
||||
"templatestyles-error-misordered-rule": "Misordered rule at line $1 character $2.",
|
||||
"templatestyles-error-missing-font-feature-values-font-list": "Missing font list for <code>@font-feature-values</code> at line $1 character $2.",
|
||||
"templatestyles-error-missing-import-source": "Missing source for <code>@import</code> at line $1 character $2.",
|
||||
"templatestyles-error-missing-keyframe-name": "Missing name for <code>@keyframes</code> at line $1 character $2.",
|
||||
"templatestyles-error-missing-namespace-value": "Missing value for <code>@namespace</code> at line $1 character $2.",
|
||||
"templatestyles-error-missing-selector-list": "Missing selector list at line $1 character $2.",
|
||||
"templatestyles-error-missing-supports-condition": "Missing condition for <code>@supports</code> at line $1 character $2.",
|
||||
"templatestyles-error-missing-value-for-property": "Missing value for property <code>$3</code> at line $1 character $2.",
|
||||
"templatestyles-error-newline-in-string": "Invalid newline in string at line $1 character $2.",
|
||||
"templatestyles-error-recursion-depth-exceeded": "Too many nested blocks and/or functions at line $1 character $2.",
|
||||
"templatestyles-error-unclosed-comment": "Unclosed comment starting at line $1 character $2.",
|
||||
"templatestyles-error-unclosed-string": "Unclosed string starting at line $1 character $2.",
|
||||
"templatestyles-error-unclosed-url": "Unclosed URL at line $1 character $2.",
|
||||
"templatestyles-error-unexpected-eof": "Unexpected end of stylesheet at line $1 character $2.",
|
||||
"templatestyles-error-unexpected-eof-in-block": "Unexpected end of stylesheet in block at line $1 character $2.",
|
||||
"templatestyles-error-unexpected-eof-in-function": "Unexpected end of stylesheet in function at line $1 character $2.",
|
||||
"templatestyles-error-unexpected-eof-in-rule": "Unexpected end of stylesheet in rule at line $1 character $2.",
|
||||
"templatestyles-error-unexpected-token-in-declaration-list": "Unexpected token in declaration list at line $1 character $2.",
|
||||
"templatestyles-error-unrecognized-property": "Unrecognized or unsupported property at line $1 character $2.",
|
||||
"templatestyles-error-unrecognized-rule": "Unrecognized or unsupported rule at line $1 character $2."
|
||||
}
|
||||
|
|
|
@ -1,10 +1,65 @@
|
|||
{
|
||||
"@metadata": {
|
||||
"authors": [
|
||||
"Brad Jorsch",
|
||||
"Marc A. Pelletier"
|
||||
]
|
||||
},
|
||||
"templatestyles": "The name of the extension",
|
||||
"templatestyles-desc": "{{desc|name=TemplateStyles|url=https://www.mediawiki.org/wiki/Extension:TemplateStyles}}",
|
||||
"templatestyles-doc-header": "Used as caption for the display of the style sheet of the current template."
|
||||
"templatestyles-missing-src": "Error message displayed when the <code>src</code> attribute is not present on <code><nowiki><templatestyles></nowiki></code>.",
|
||||
"templatestyles-invalid-src": "Error message displayed when the <code>src</code> attribute is not a valid title.",
|
||||
"templatestyles-bad-src-missing": "Error message displayed when the title specified has no content. Parameters:\n* $1 - The title specified.\n* $2 - The title with wikitext escaped.",
|
||||
"templatestyles-bad-src": "Error message displayed when the title specified is not a usable stylesheet. Parameters:\n* $1 - The title specified.\n* $2 - The title with wikitext escaped.\n* $3 - Current content model of the page in question.",
|
||||
"templatestyles-errorcomment": "Formatting for the comment used to display TemplateStyles errors encountered during the parse. Parameters:\n* $1 - Source stylesheet.\n* $2 - Revision of the stylesheet.* $3 - Errors.",
|
||||
"templatestyles-size-exceeded": "Error returned when the stylesheet is more than $wgTemplateStylesMaxStylesheetSize bytes. Parameters:\n* $1 - Maximum size in bytes\n* $2 - Maximum size in \"human units\" (i.e. KB, MB, GB, etc).",
|
||||
"content-model-sanitized-css": "Name for TemplateStyles sanitized-css content model.",
|
||||
|
||||
"templatestyles-error-at-rule-block-not-allowed": "Error in CSS validation. Note \"block\" in this message refers to the part of CSS syntax inside the <code>{ ... }</code>. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.\n* $3 - Name of the at-rule in question.",
|
||||
"templatestyles-error-at-rule-block-required": "Error in CSS validation. Note \"block\" in this message refers to the part of CSS syntax inside the <code>{ ... }</code>. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.\n* $3 - Name of the at-rule in question.",
|
||||
"templatestyles-error-bad-character-in-url": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-bad-escape": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-bad-value-for-property": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.\n* $3 - Name of the property in question.",
|
||||
"templatestyles-error-expected-at-rule": "Error in CSS validation. If this message is used, it probably indicates an error in the code. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.\n* $3 - Name of the at-rule that was expected.",
|
||||
"templatestyles-error-expected-colon": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-expected-declaration": "Error in CSS validation. If this message is used, it probably indicates an error in the code. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-expected-declaration-list": "Error in CSS validation. If this message is used, it probably indicates an error in the code. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-expected-eof": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-expected-ident": "Error in CSS parsing. Note \"identifier\" in this message refers to a keyword such as a class name, property name, property value, or the like. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-expected-page-margin-at-rule": "Error in CSS validation. If this message is used, it probably indicates an error in the code. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-expected-qualified-rule": "Error in CSS validation. If this message is used, it probably indicates an error in the code. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-expected-stylesheet": "Error in CSS validation. If this message is used, it probably indicates an error in the code. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-font-face-at-rule": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-font-feature-value": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.\n* $3 - Name of the specific feature value at-rule in question.",
|
||||
"templatestyles-error-invalid-font-feature-value-declaration": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-font-feature-values-font-list": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-import-value": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-keyframe-name": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-media-query": "Error in CSS validation. Note \"media query\" in this message refers to the syntax described at https://www.w3.org/TR/css3-mediaqueries/. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-namespace-value": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-page-margin-at-rule": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.\n* $3 - Name of the specific page margin at-rule in question.",
|
||||
"templatestyles-error-invalid-page-rule-content": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-page-selector": "Error in CSS validation. Note \"page selector\" refers to the syntax used to specify a page in the <code>@page</code> rule, e.g. the <code>:left</code> in <code>@page :left { ... }</code>. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-selector-list": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-invalid-supports-condition": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-misordered-rule": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-missing-font-feature-values-font-list": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-missing-import-source": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-missing-keyframe-name": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-missing-namespace-value": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-missing-selector-list": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-missing-supports-condition": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-missing-value-for-property": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.\n* $3 - Name of the property in question.",
|
||||
"templatestyles-error-newline-in-string": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-recursion-depth-exceeded": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unclosed-comment": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unclosed-string": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unclosed-url": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unexpected-eof": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unexpected-eof-in-block": "Error in CSS parsing. Note \"block\" in this message refers to the part of CSS syntax inside <code>{ ... }</code>, <code>[ ... ]</code>, or <code>( ... )</code>. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unexpected-eof-in-function": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unexpected-eof-in-rule": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unexpected-token-in-declaration-list": "Error in CSS parsing. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unrecognized-property": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line.",
|
||||
"templatestyles-error-unrecognized-rule": "Error in CSS validation. Parameters:\n* $1 - Line number of the error.\n* $2 - Location of the error within the line."
|
||||
}
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
{
|
||||
"private": true,
|
||||
"name": "TemplateStyles",
|
||||
"version": "0.9.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gerrit.wikimedia.org/r/mediawiki/extensions/TemplateStyles"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "grunt test"
|
||||
},
|
||||
|
|
|
@ -1,258 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group TemplateStyles
|
||||
*/
|
||||
class CSSParseRenderTest extends MediaWikiTestCase {
|
||||
|
||||
/**
|
||||
* Parse a CSS string and then validate the rendered output.
|
||||
*
|
||||
* @param string $expect Expected CSS output from renderer
|
||||
* @param string $source Input for CSS parser
|
||||
* @param string $baseSelector Prefix for generated rules
|
||||
* @param array $functionWhitelist Allowed functions
|
||||
* @param array $propertyBlacklist Excluded properties
|
||||
* @dataProvider provideRendererAfterParse
|
||||
*/
|
||||
public function testRendererAfterParse(
|
||||
$expect,
|
||||
$source,
|
||||
$baseSelector = '.X ',
|
||||
array $functionWhitelist = [ 'whitelisted' ],
|
||||
array $propertyBlacklist = [ '-evil' ]
|
||||
) {
|
||||
$tree = new CSSParser( $source );
|
||||
$rules = $tree->rules( $baseSelector );
|
||||
if ( !$rules ) {
|
||||
$this->fail( "Failed to parse $source" );
|
||||
}
|
||||
|
||||
$r = new CSSRenderer();
|
||||
$r->add( $rules );
|
||||
$css = $r->render( $functionWhitelist, $propertyBlacklist );
|
||||
|
||||
$this->assertEquals(
|
||||
$expect,
|
||||
// Normalize whitespace inherited from the heredocs
|
||||
preg_replace( '/[ \t\n]+/', ' ', $css )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see testRendererAfterParse
|
||||
*/
|
||||
public function provideRendererAfterParse() {
|
||||
return [
|
||||
'Bare declaration' => [
|
||||
'expect' => '',
|
||||
'css' => <<<CSS
|
||||
prop: val;
|
||||
CSS
|
||||
],
|
||||
'Blacklisted property' => [
|
||||
'expect' => '.X .sel {good:123;} ',
|
||||
'css' => <<<CSS
|
||||
.sel {
|
||||
good: 123;
|
||||
-evil: "boo";
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Case insensivity' => [
|
||||
'expect' => '@media screen { .X .sel1 {prop:WhiteListed(foo);} } ',
|
||||
'css' => <<<CSS
|
||||
@MEDIA screen {
|
||||
.sel1 {
|
||||
prop: WhiteListed(foo);
|
||||
-EVIL: evil;
|
||||
}
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Comment trickery' => [
|
||||
'expect' => '.X .sel1 {} .X .sel2 .sel3 {prop3:val3;} ',
|
||||
'css' => <<<CSS
|
||||
.sel1 {
|
||||
-ev/* x */il: evil;
|
||||
}
|
||||
.sel2 /* { prop2: val2; } */
|
||||
.sel3 {
|
||||
prop3: val3;
|
||||
} /* unfinishe
|
||||
CSS
|
||||
],
|
||||
'Complex selectors' => [
|
||||
'expect' => '.X .sel1[foo=\'ba{r\'] #id a.foo::hover {prop1:val1;} ',
|
||||
'css' => <<<CSS
|
||||
.sel1[foo='ba{r'] #id a.foo::hover {
|
||||
prop1: val1;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Edge cases' => [
|
||||
'expect' => '.X :sel {} ',
|
||||
'css' => <<<CSS
|
||||
:sel {
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Function in function' => [
|
||||
'expect' => '.X .sel1 {} ',
|
||||
'css' => <<<CSS
|
||||
.sel1 {
|
||||
prop1: whitelisted(1, evil(2));
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Incomplete rule' => [
|
||||
'expect' => '.X .sel {prop:val;} ',
|
||||
'css' => <<<CSS
|
||||
.sel {
|
||||
prop: val;
|
||||
CSS
|
||||
],
|
||||
'Media block' => [
|
||||
'expect' => '.X .sel2 {prop2:val2;} @media print { .X .sel1 {prop1:val1;} } ',
|
||||
'css' => <<<CSS
|
||||
@media print {
|
||||
.sel1 {
|
||||
prop1: val1;
|
||||
}
|
||||
}
|
||||
|
||||
.sel2 {
|
||||
prop2: val2;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Multiple rules' => [
|
||||
'expect' => '.X .sel1 A {prop1:val1;} .X T.sel2 {prop2:val2;} ',
|
||||
'css' => <<<CSS
|
||||
.sel1 A {
|
||||
prop1: val1;
|
||||
}
|
||||
|
||||
T.sel2 {
|
||||
prop2: val2;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Multiple selectors' => [
|
||||
'expect' => '.X .sel1,.X TD .sel2["a,comma"],.X #id {prop:val;} ',
|
||||
'css' => <<<CSS
|
||||
.sel1, TD .sel2["a,comma"], #id {
|
||||
prop: val;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'No selector' => [
|
||||
'expect' => '{prop1:val1;} ',
|
||||
'css' => <<<CSS
|
||||
{
|
||||
prop1: val1;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Not a declaration' => [
|
||||
'expect' => '.X .sel {prop:val;} ',
|
||||
'css' => <<<CSS
|
||||
.sel {
|
||||
not a declaration;
|
||||
prop: val;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Obfuscated properties' => [
|
||||
'expect' => '.X .sel {good:val2;} ',
|
||||
'css' => <<<CSS
|
||||
.sel {
|
||||
-\\065 vil: val1;
|
||||
go\\00006fd: val2;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Rule within rule' => [
|
||||
'expect' => '.X .sel1 {prop1:val1;} .X .sel3 {prop4:val4;} ',
|
||||
'css' => <<<CSS
|
||||
.sel1 {
|
||||
prop1: val1;
|
||||
.sel2 {
|
||||
prop2: val2;
|
||||
}
|
||||
prop3: val3;
|
||||
}
|
||||
|
||||
.sel3 {
|
||||
prop4: val4;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'String literals' => [
|
||||
'expect' => '.X .sel {prop1:\'val1\';prop3:"v/**/al\"3";bad:"broken";} ',
|
||||
'css' => <<<CSS
|
||||
.sel {
|
||||
prop1: 'val1';
|
||||
prop3: "v/**/al\"3";
|
||||
bad: "broken
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Unsupported block' => [
|
||||
'expect' => '.X .sel {prop2:val2;} ',
|
||||
'css' => <<<CSS
|
||||
@font-face {
|
||||
prop1: val1;
|
||||
}
|
||||
|
||||
.sel {
|
||||
prop2: val2;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Unwhitelisted function' => [
|
||||
'expect' => '.X .sel {prop1:whitelisted(val1);} ',
|
||||
'css' => <<<CSS
|
||||
.sel {
|
||||
prop1: whitelisted(val1);
|
||||
prop2: evil(val2);
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Values' => [
|
||||
'expect' => '.X .sel {prop:1em .5px 12% #FFF;} ',
|
||||
'css' => <<<CSS
|
||||
.sel {
|
||||
prop: 1em .5px 12% #FFF;
|
||||
}
|
||||
CSS
|
||||
],
|
||||
'Whitespace' => [
|
||||
'expect' => '.X .sel1 #id{prop2:whitelisted ( val2 ) ;prop3:not whitelisted( val3 );} ',
|
||||
'css' => <<<CSS
|
||||
.sel1
|
||||
#id{
|
||||
-evil
|
||||
:val1;
|
||||
prop2/*
|
||||
comment */: whitelisted ( val2 )
|
||||
;prop3 :not/**/whitelisted( val3 );}
|
||||
CSS
|
||||
],
|
||||
'Whitelist normalized' => [
|
||||
'expect' => '.foo {bar:whitelisted(1);} ',
|
||||
'css' => '.foo { bar: whitelisted(1); baz: url(1); }',
|
||||
'prefix' => '',
|
||||
'whitelist' => [ 'WHITELISTED' ],
|
||||
],
|
||||
'Blacklist normalized' => [
|
||||
'expect' => '.foo {baz:1;} ',
|
||||
'css' => '.foo { blacklisted: 1; baz: 1; }',
|
||||
'prefix' => '',
|
||||
'whitelist' => [],
|
||||
'blacklist' => [ 'BLACKLISTED' ],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
21
tests/phpunit/TemplateStylesContentHandlerTest.php
Normal file
21
tests/phpunit/TemplateStylesContentHandlerTest.php
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group TemplateStyles
|
||||
*/
|
||||
class TemplateStylesContentHandlerTest extends MediaWikiLangTestCase {
|
||||
|
||||
public function testBasics() {
|
||||
$handler = new TemplateStylesContentHandler();
|
||||
|
||||
$this->assertSame( 'sanitized-css', $handler->getModelID() );
|
||||
$this->assertSame( [ 'text/css' ], $handler->getSupportedFormats() );
|
||||
$this->assertInstanceOf( TemplateStylesContent::class, $handler->makeEmptyContent() );
|
||||
|
||||
$this->assertFalse( $handler->supportsRedirects() );
|
||||
|
||||
$title = Title::newFromText( 'Template:Example/styles.css' );
|
||||
$this->assertNull( $handler->makeRedirectContent( $title ) );
|
||||
}
|
||||
|
||||
}
|
253
tests/phpunit/TemplateStylesContentTest.php
Normal file
253
tests/phpunit/TemplateStylesContentTest.php
Normal file
|
@ -0,0 +1,253 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group TemplateStyles
|
||||
*/
|
||||
class TemplateStylesContentTest extends TextContentTest {
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->setMwGlobals( [
|
||||
'wgTextModelsToParse' => [
|
||||
'sanitized-css',
|
||||
],
|
||||
'wgTemplateStylesMaxStylesheetSize' => 1024000,
|
||||
] );
|
||||
}
|
||||
|
||||
public function newContent( $text ) {
|
||||
return new TemplateStylesContent( $text );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideSanitize
|
||||
* @param string $text Input text
|
||||
* @param array $options
|
||||
* @param Status $expect
|
||||
*/
|
||||
public function testSanitize( $text, $options, $expect ) {
|
||||
$this->assertEquals( $expect, $this->newContent( $text )->sanitize( $options ) );
|
||||
}
|
||||
|
||||
public static function provideSanitize() {
|
||||
$status1 = Status::newGood( '.mw-parser-output .foo{}' );
|
||||
$status1->warning( 'templatestyles-error-bad-value-for-property', 1, 15, 'color' );
|
||||
|
||||
return [
|
||||
'flip' => [
|
||||
'.foo { margin-left: 10px; /*@noflip*/ padding-left: 1em; }',
|
||||
[ 'flip' => true ],
|
||||
Status::newGood( '.mw-parser-output .foo{margin-right:10px;padding-left:1em}' )
|
||||
],
|
||||
'no minify' => [
|
||||
'.foo { margin-left: 10px }',
|
||||
[ 'minify' => false ],
|
||||
Status::newGood( '.mw-parser-output .foo { margin-left: 10px ; }' )
|
||||
],
|
||||
'With warnings' => [
|
||||
'.foo { color: bogus; }',
|
||||
[],
|
||||
$status1
|
||||
],
|
||||
'With warnings, fatal and no value' => [
|
||||
'.foo { bogus: bogus; }',
|
||||
[ 'severity' => 'fatal', 'novalue' => true ],
|
||||
Status::newFatal( 'templatestyles-error-unrecognized-property', 1, 8 ),
|
||||
],
|
||||
'With overridden class prefix' => [
|
||||
'.foo { margin-left: 10px }',
|
||||
[ 'class' => 'foo bar', 'minify' => false ],
|
||||
Status::newGood( '.foo\ bar .foo { margin-left: 10px ; }' )
|
||||
],
|
||||
'With boolean false as a class prefix' => [
|
||||
'.foo { margin-left: 10px }',
|
||||
[ 'class' => false, 'minify' => false ],
|
||||
Status::newGood( '.mw-parser-output .foo { margin-left: 10px ; }' )
|
||||
],
|
||||
'Escaping U+007F' => [
|
||||
".foo\\\x7f { content: '\x7f'; }",
|
||||
[],
|
||||
Status::newGood(
|
||||
'.mw-parser-output .foo\\7f {content:"\\7f "}'
|
||||
)
|
||||
],
|
||||
'@font-face prefixing' => [
|
||||
'@font-face { font-family: nope; }',
|
||||
[ 'severity' => 'fatal', 'novalue' => true ],
|
||||
Status::newFatal( 'templatestyles-error-bad-value-for-property', 1, 27, 'font-family' ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testSizeLimit() {
|
||||
$this->setMwGlobals( [
|
||||
'wgTemplateStylesMaxStylesheetSize' => 10,
|
||||
] );
|
||||
|
||||
$this->assertEquals(
|
||||
Status::newGood( '.mw-parser-output .foobar{}' ),
|
||||
$this->newContent( '.foobar {}' )->sanitize()
|
||||
);
|
||||
$this->assertEquals(
|
||||
Status::newFatal( wfMessage( 'templatestyles-size-exceeded', 10, Message::sizeParam( 10 ) ) ),
|
||||
$this->newContent( '.foobar2 {}' )->sanitize()
|
||||
);
|
||||
|
||||
$this->setMwGlobals( [
|
||||
'wgTemplateStylesMaxStylesheetSize' => null,
|
||||
] );
|
||||
$long = str_repeat( 'X', 102400 );
|
||||
$this->assertEquals(
|
||||
Status::newGood( ".mw-parser-output .{$long}{}" ),
|
||||
$this->newContent( ".{$long} {}" )->sanitize()
|
||||
);
|
||||
}
|
||||
|
||||
public function testPrepareSave() {
|
||||
$this->assertEquals(
|
||||
$this->newContent( '.foo { bogus: bogus; }' )->prepareSave(
|
||||
WikiPage::factory( Title::newFromText( 'Template:Test/styles.css' ) ),
|
||||
0,
|
||||
123,
|
||||
new User
|
||||
),
|
||||
Status::newFatal( 'templatestyles-error-unrecognized-property', 1, 8 )
|
||||
);
|
||||
}
|
||||
|
||||
public static function dataGetParserOutput() {
|
||||
return [
|
||||
[
|
||||
'Template:Test/styles.css',
|
||||
'sanitized-css',
|
||||
".hello { content: 'world'; color: bogus; }\n\n<ok>\n",
|
||||
// @codingStandardsIgnoreStart Generic.Files.LineLength
|
||||
"<pre class=\"mw-code mw-css\" dir=\"ltr\">\n.hello { content: 'world'; color: bogus; }\n\n<ok>\n\n</pre>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
[
|
||||
'Warnings' => [
|
||||
'Unexpected end of stylesheet in rule at line 4 character 1.',
|
||||
'Invalid or unsupported value for property <code>color</code> at line 1 character 35.',
|
||||
]
|
||||
]
|
||||
],
|
||||
[
|
||||
'Template:Test/styles.css',
|
||||
'sanitized-css',
|
||||
"/* hello [[world]] */\n",
|
||||
"<pre class=\"mw-code mw-css\" dir=\"ltr\">\n/* hello [[world]] */\n\n</pre>",
|
||||
[
|
||||
'Links' => [
|
||||
[ 'World' => 0 ]
|
||||
]
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function dataPreSaveTransform() {
|
||||
return [
|
||||
[
|
||||
'hello this is ~~~',
|
||||
'hello this is ~~~',
|
||||
],
|
||||
[
|
||||
'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
|
||||
'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
|
||||
],
|
||||
[
|
||||
" Foo \n ",
|
||||
" Foo",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public static function dataPreloadTransform() {
|
||||
return [
|
||||
[
|
||||
'hello this is ~~~',
|
||||
'hello this is ~~~',
|
||||
],
|
||||
[
|
||||
'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
|
||||
'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testGetModel() {
|
||||
$content = $this->newContent( 'hello world.' );
|
||||
|
||||
$this->assertEquals( 'sanitized-css', $content->getModel() );
|
||||
}
|
||||
|
||||
public function testGetContentHandler() {
|
||||
$content = $this->newContent( 'hello world.' );
|
||||
|
||||
$this->assertEquals( 'sanitized-css', $content->getContentHandler()->getModelID() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects aren't supported
|
||||
*/
|
||||
public static function provideUpdateRedirect() {
|
||||
// @codingStandardsIgnoreStart Generic.Files.LineLength
|
||||
return [
|
||||
[
|
||||
'#REDIRECT [[Someplace]]',
|
||||
'#REDIRECT [[Someplace]]',
|
||||
],
|
||||
|
||||
// The style supported by CssContent
|
||||
[
|
||||
'/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);',
|
||||
'/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);',
|
||||
],
|
||||
];
|
||||
// @codingStandardsIgnoreEnd
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideGetRedirectTarget
|
||||
*/
|
||||
public function testGetRedirectTarget( $title, $text ) {
|
||||
$this->setMwGlobals( [
|
||||
'wgServer' => '//example.org',
|
||||
'wgScriptPath' => '/w',
|
||||
'wgScript' => '/w/index.php',
|
||||
] );
|
||||
$content = $this->newContent( $text );
|
||||
$target = $content->getRedirectTarget();
|
||||
$this->assertEquals( $title, $target ? $target->getPrefixedText() : null );
|
||||
}
|
||||
|
||||
public static function provideGetRedirectTarget() {
|
||||
// @codingStandardsIgnoreStart Generic.Files.LineLength
|
||||
return [
|
||||
[ null, "/* #REDIRECT */@import url(//example.org/w/index.php?title=MediaWiki:MonoBook.css&action=raw&ctype=text/css);" ],
|
||||
[ null, "/* #REDIRECT */@import url(//example.org/w/index.php?title=User:FooBar/common.css&action=raw&ctype=text/css);" ],
|
||||
[ null, "/* #REDIRECT */@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ],
|
||||
[ null, "@import url(//example.org/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ],
|
||||
[ null, "/* #REDIRECT */@import url(//example.com/w/index.php?title=Gadget:FooBaz.css&action=raw&ctype=text/css);" ],
|
||||
];
|
||||
// @codingStandardsIgnoreEnd
|
||||
}
|
||||
|
||||
public static function dataEquals() {
|
||||
return [
|
||||
[ new TemplateStylesContent( 'hallo' ), null, false ],
|
||||
[ new TemplateStylesContent( 'hallo' ), new TemplateStylesContent( 'hallo' ), true ],
|
||||
[ new TemplateStylesContent( 'hallo' ), new CssContent( 'hallo' ), false ],
|
||||
[ new TemplateStylesContent( 'hallo' ), new WikitextContent( 'hallo' ), false ],
|
||||
[ new TemplateStylesContent( 'hallo' ), new TemplateStylesContent( 'HALLO' ), false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataEquals
|
||||
*/
|
||||
public function testEquals( Content $a, Content $b = null, $equal = false ) {
|
||||
$this->assertEquals( $equal, $a->equals( $b ) );
|
||||
}
|
||||
}
|
90
tests/phpunit/TemplateStylesFontFaceAtRuleSanitizer.php
Normal file
90
tests/phpunit/TemplateStylesFontFaceAtRuleSanitizer.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
use Wikimedia\CSS\Grammar\MatcherFactory;
|
||||
use Wikimedia\CSS\Parser\Parser;
|
||||
use Wikimedia\CSS\Util;
|
||||
|
||||
/**
|
||||
* @group TemplateStyles
|
||||
*/
|
||||
class TemplateSantitizerFontFaceAtRuleSanitizerTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* @dataProvider provideRules
|
||||
* @param string $input
|
||||
* @param bool $handled
|
||||
* @param string|null $output
|
||||
* @param string|null $minified
|
||||
* @param array $errors
|
||||
* @param array $options
|
||||
*/
|
||||
public function testRules( $input, $handled, $output, $minified, $errors = [], $options = [] ) {
|
||||
$san = new TemplateStylesFontFaceAtRuleSanitizer( new MatcherFactory() );
|
||||
$rule = Parser::newFromString( $input )->parseRule();
|
||||
$oldRule = clone( $rule );
|
||||
|
||||
$this->assertSame( $handled, $san->handlesRule( $rule ) );
|
||||
$ret = $san->sanitize( $rule );
|
||||
$this->assertSame( $errors, $san->getSanitizationErrors() );
|
||||
if ( $output === null ) {
|
||||
$this->assertNull( $ret );
|
||||
} else {
|
||||
$this->assertNotNull( $ret );
|
||||
$this->assertSame( $output, (string)$ret );
|
||||
$this->assertSame( $minified, Util::stringify( $ret, [ 'minify' => true ] ) );
|
||||
}
|
||||
|
||||
$this->assertEquals( (string)$oldRule, (string)$rule, 'Rule wasn\'t overwritten' );
|
||||
}
|
||||
|
||||
public static function provideRules() {
|
||||
return [
|
||||
'non-prefixed font family as string' => [
|
||||
'@font-face {
|
||||
font-family: "foo bar";
|
||||
}',
|
||||
true,
|
||||
'@font-face {}',
|
||||
'@font-face{}',
|
||||
[
|
||||
[ 'bad-value-for-property', 2, 19, 'font-family' ],
|
||||
]
|
||||
],
|
||||
'non-prefixed font family as idents' => [
|
||||
'@font-face {
|
||||
font-family: foo bar;
|
||||
}',
|
||||
true,
|
||||
'@font-face {}',
|
||||
'@font-face{}',
|
||||
[
|
||||
[ 'bad-value-for-property', 2, 19, 'font-family' ],
|
||||
]
|
||||
],
|
||||
'prefixed font family as string' => [
|
||||
'@font-face {
|
||||
font-family: "TemplateStyles foo bar";
|
||||
}',
|
||||
true,
|
||||
'@font-face { font-family: "TemplateStyles foo bar"; }',
|
||||
'@font-face{font-family:"TemplateStyles foo bar"}',
|
||||
],
|
||||
'non-prefixed font family as idents (1)' => [
|
||||
'@font-face {
|
||||
font-family: TemplateStyles foo bar;
|
||||
}',
|
||||
true,
|
||||
'@font-face { font-family: TemplateStyles foo bar; }',
|
||||
'@font-face{font-family:TemplateStyles foo bar}',
|
||||
],
|
||||
'non-prefixed font family as idents (2)' => [
|
||||
'@font-face {
|
||||
font-family: TemplateStylesFoo bar;
|
||||
}',
|
||||
true,
|
||||
'@font-face { font-family: TemplateStylesFoo bar; }',
|
||||
'@font-face{font-family:TemplateStylesFoo bar}',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
265
tests/phpunit/TemplateStylesHooksTest.php
Normal file
265
tests/phpunit/TemplateStylesHooksTest.php
Normal file
|
@ -0,0 +1,265 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @group TemplateStyles
|
||||
* @group Database
|
||||
*/
|
||||
class TemplateStylesHooksTest extends MediaWikiLangTestCase {
|
||||
|
||||
protected function addPage( $page, $text, $model ) {
|
||||
$title = Title::newFromText( 'Template:TemplateStyles test/' . $page );
|
||||
$content = ContentHandler::makeContent( $text, $title, $model );
|
||||
|
||||
$page = WikiPage::factory( $title );
|
||||
$user = static::getTestSysop()->getUser();
|
||||
$status = $page->doEditContent( $content, 'Test for TemplateStyles', 0, false, $user );
|
||||
if ( !$status->isOk() ) {
|
||||
$this->fail( "Failed to create $title: " . $status->getWikiText( false, false, 'en' ) );
|
||||
}
|
||||
}
|
||||
|
||||
public function addDBDataOnce() {
|
||||
$this->addPage( 'wikitext', '.foo { color: red; }', CONTENT_MODEL_WIKITEXT );
|
||||
$this->addPage( 'nonsanitized.css', '.foo { color: red; }', CONTENT_MODEL_CSS );
|
||||
$this->addPage( 'styles1.css', '.foo { color: blue; }', 'sanitized-css' );
|
||||
$this->addPage( 'styles2.css', '.bar { color: green; }', 'sanitized-css' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideOnRegistration
|
||||
* @param array $textModelsToParse
|
||||
* @param bool $autoParseContent
|
||||
* @param array $expect
|
||||
*/
|
||||
public function testOnRegistration( $textModelsToParse, $autoParseContent, $expect ) {
|
||||
$this->setMwGlobals( [
|
||||
'wgTextModelsToParse' => $textModelsToParse,
|
||||
'wgTemplateStylesAutoParseContent' => $autoParseContent,
|
||||
] );
|
||||
|
||||
global $wgTextModelsToParse;
|
||||
TemplateStylesHooks::onRegistration();
|
||||
$this->assertSame( $expect, $wgTextModelsToParse );
|
||||
}
|
||||
|
||||
public static function provideOnRegistration() {
|
||||
return [
|
||||
[
|
||||
[ CONTENT_MODEL_WIKITEXT ],
|
||||
true,
|
||||
[ CONTENT_MODEL_WIKITEXT ]
|
||||
],
|
||||
[
|
||||
[ CONTENT_MODEL_WIKITEXT, CONTENT_MODEL_CSS ],
|
||||
true,
|
||||
[ CONTENT_MODEL_WIKITEXT, CONTENT_MODEL_CSS, 'sanitized-css' ],
|
||||
],
|
||||
[
|
||||
[ CONTENT_MODEL_WIKITEXT, CONTENT_MODEL_CSS ],
|
||||
false,
|
||||
[ CONTENT_MODEL_WIKITEXT, CONTENT_MODEL_CSS ],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideOnParserAfterTidy
|
||||
*/
|
||||
public function testOnParserAfterTidy( $text, $expect ) {
|
||||
$p = new Parser();
|
||||
TemplateStylesHooks::onParserAfterTidy( $p, $text );
|
||||
$this->assertSame( $expect, $text );
|
||||
}
|
||||
|
||||
public static function provideOnParserAfterTidy() {
|
||||
return [
|
||||
[
|
||||
"<style>\n.foo { color: red; }\n</style>",
|
||||
"<style>\n.foo { color: red; }\n</style>",
|
||||
],
|
||||
[
|
||||
"<style>\n<![CDATA[\n.foo { color: red; }\n]]>\n</style>",
|
||||
"<style>\n/*<![CDATA[*/\n.foo { color: red; }\n/*]]>*/\n</style>",
|
||||
],
|
||||
[
|
||||
"<StYlE type='text/css'>\n<![CDATA[\n.foo { color: red; }\n]]>\n</sTyLe>",
|
||||
"<StYlE type='text/css'>\n/*<![CDATA[*/\n.foo { color: red; }\n/*]]>*/\n</sTyLe>",
|
||||
],
|
||||
[
|
||||
"<style>\n/*<![CDATA[*/\n.foo { color: red; }\n/*]]>*/\n</style>",
|
||||
"<style>\n/*<![CDATA[*/\n.foo { color: red; }\n/*]]>*/\n</style>",
|
||||
],
|
||||
[
|
||||
"<style>x\n<![CDATA[\n.foo { color: red; }\n]]>\n</style>",
|
||||
"<style>x\n<![CDATA[\n.foo { color: red; }\n/*]]>*/\n</style>",
|
||||
],
|
||||
[
|
||||
"<script>\n<![CDATA[\n.foo { color: red; }\n]]>\n</script>",
|
||||
"<script>\n<![CDATA[\n.foo { color: red; }\n]]>\n</script>",
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideOnContentHandlerDefaultModelFor
|
||||
*/
|
||||
public function testOnContentHandlerDefaultModelFor( $ns, $title, $expect ) {
|
||||
$this->setMwGlobals( [
|
||||
'wgTemplateStylesNamespaces' => [ 10 => true, 2 => false, 3000 => true, 3002 => true ],
|
||||
'wgNamespacesWithSubpages' => [ 10 => true, 2 => true, 3000 => true, 3002 => false ],
|
||||
] );
|
||||
|
||||
$model = 'unchanged';
|
||||
$ret = TemplateStylesHooks::onContentHandlerDefaultModelFor(
|
||||
Title::makeTitle( $ns, $title ), $model
|
||||
);
|
||||
$this->assertSame( !$expect, $ret );
|
||||
$this->assertSame( $expect ? 'sanitized-css' : 'unchanged', $model );
|
||||
}
|
||||
|
||||
public static function provideOnContentHandlerDefaultModelFor() {
|
||||
return [
|
||||
[ 10, 'Test/test.css', true ],
|
||||
[ 10, 'Test.css', false ],
|
||||
[ 10, 'Test/test.xss', false ],
|
||||
[ 10, 'Test/test.CSS', false ],
|
||||
[ 3000, 'Test/test.css', true ],
|
||||
[ 3002, 'Test/test.css', false ],
|
||||
[ 2, 'Test/test.css', false ],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unfortunately we can't just use a parserTests.txt file because our
|
||||
* tag's output depends on the revision IDs of the input pages.
|
||||
* @dataProvider provideTag
|
||||
*/
|
||||
public function testTag( $popt, $wikitext, $expect ) {
|
||||
global $wgParserConf;
|
||||
|
||||
$this->setMwGlobals( [
|
||||
'wgScriptPath' => '',
|
||||
'wgScript' => '/index.php',
|
||||
'wgArticlePath' => '/wiki/$1',
|
||||
] );
|
||||
|
||||
$oldCurrentRevisionCallback = $popt->setCurrentRevisionCallback(
|
||||
function ( $title, $parser = false ) use ( &$oldCurrentRevisionCallback ) {
|
||||
if ( $title->getPrefixedText() === 'Template:Test replacement' ) {
|
||||
$user = RequestContext::getMain()->getUser();
|
||||
return new Revision( [
|
||||
'page' => $title->getArticleID(),
|
||||
'user_text' => $user->getName(),
|
||||
'user' => $user->getId(),
|
||||
'parent_id' => $title->getLatestRevId(),
|
||||
'title' => $title,
|
||||
'content' => new TemplateStylesContent( '.baz { color:orange; bogus:bogus; }' )
|
||||
] );
|
||||
}
|
||||
return call_user_func( $oldCurrentRevisionCallback, $title, $parser );
|
||||
}
|
||||
);
|
||||
|
||||
$class = $wgParserConf['class'];
|
||||
$parser = new $class( $wgParserConf );
|
||||
$parser->firstCallInit();
|
||||
if ( !isset( $parser->mTagHooks['templatestyles'] ) ) {
|
||||
$this->markTestSkipped( 'templatestyles tag hook is not in the parser' );
|
||||
}
|
||||
$out = $parser->parse( $wikitext, Title::newFromText( 'Test' ), $popt );
|
||||
$parser->mPreprocessor = null; # Break the Parser <-> Preprocessor cycle
|
||||
|
||||
$this->assertEquals( $expect, $out->getText() );
|
||||
}
|
||||
|
||||
public static function provideTag() {
|
||||
$popt = ParserOptions::newFromContext( RequestContext::getMain() );
|
||||
$popt->setWrapOutputClass( 'templatestyles-test' );
|
||||
|
||||
$popt2 = ParserOptions::newFromContext( RequestContext::getMain() );
|
||||
$popt2->setWrapOutputClass( false );
|
||||
|
||||
return [
|
||||
'Tag without src' => [
|
||||
$popt,
|
||||
'<templatestyles />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><strong class=\"error\">TemplateStyles' <code>src</code> attribute must not be empty.</strong>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Tag with invalid src' => [
|
||||
$popt,
|
||||
'<templatestyles src="Test<>" />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><strong class=\"error\">Invalid title for TemplateStyles' <code>src</code> attribute.</strong>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Tag with valid but nonexistent title' => [
|
||||
$popt,
|
||||
'<templatestyles src="ThisDoes\'\'\'Not\'\'\'Exist" />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><strong class=\"error\">Page <a href=\"/index.php?title=Template:ThisDoes%27%27%27Not%27%27%27Exist&action=edit&redlink=1\" class=\"new\" title=\"Template:ThisDoes'''Not'''Exist (page does not exist)\">Template:ThisDoes'''Not'''Exist</a> has no content.</strong>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Tag with valid but nonexistent title, main namespace' => [
|
||||
$popt,
|
||||
'<templatestyles src=":ThisDoes\'\'\'Not\'\'\'Exist" />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><strong class=\"error\">Page <a href=\"/index.php?title=ThisDoes%27%27%27Not%27%27%27Exist&action=edit&redlink=1\" class=\"new\" title=\"ThisDoes'''Not'''Exist (page does not exist)\">ThisDoes'''Not'''Exist</a> has no content.</strong>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Tag with wikitext page' => [
|
||||
$popt,
|
||||
'<templatestyles src="TemplateStyles test/wikitext" />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><strong class=\"error\">Page <a href=\"/wiki/Template:TemplateStyles_test/wikitext\" title=\"Template:TemplateStyles test/wikitext\">Template:TemplateStyles test/wikitext</a> must have content model \"Sanitized CSS\" for TemplateStyles (current model is \"wikitext\").</strong>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Tag with CSS (not sanitized-css) page' => [
|
||||
$popt,
|
||||
'<templatestyles src="TemplateStyles test/nonsanitized.css" />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><strong class=\"error\">Page <a href=\"/wiki/Template:TemplateStyles_test/nonsanitized.css\" title=\"Template:TemplateStyles test/nonsanitized.css\">Template:TemplateStyles test/nonsanitized.css</a> must have content model \"Sanitized CSS\" for TemplateStyles (current model is \"CSS\").</strong>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Working tag' => [
|
||||
$popt,
|
||||
'<templatestyles src="TemplateStyles test/styles1.css" />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><style>.templatestyles-test .foo{color:blue}</style>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Replaced content (which includes sanitization errors)' => [
|
||||
$popt,
|
||||
'<templatestyles src="Test replacement" />',
|
||||
// @codingStandardsIgnoreStart Ignore Generic.Files.LineLength.TooLong
|
||||
"<div class=\"templatestyles-test\"><p><style>/*\nErrors processing stylesheet [[:Template:Test replacement]] (rev ):\n• Unrecognized or unsupported property at line 1 character 22.\n*/\n.templatestyles-test .baz{color:orange}</style>\n</p></div>",
|
||||
// @codingStandardsIgnoreEnd
|
||||
],
|
||||
'Still prefixed despite no wrapper' => [
|
||||
$popt2,
|
||||
'<templatestyles src="TemplateStyles test/styles1.css" />',
|
||||
"<p><style>.mw-parser-output .foo{color:blue}</style>\n</p>",
|
||||
],
|
||||
'Not yet deduplicated tags' => [
|
||||
$popt,
|
||||
trim( '
|
||||
<templatestyles src="TemplateStyles test/styles1.css" />
|
||||
<templatestyles src="TemplateStyles test/styles1.css" />
|
||||
<templatestyles src="TemplateStyles test/styles2.css" />
|
||||
<templatestyles src="TemplateStyles test/styles1.css" />
|
||||
<templatestyles src="TemplateStyles test/styles2.css" />
|
||||
' ),
|
||||
trim( '
|
||||
<div class="templatestyles-test"><p><style>.templatestyles-test .foo{color:blue}</style>
|
||||
<style>.templatestyles-test .foo{color:blue}</style>
|
||||
<style>.templatestyles-test .bar{color:green}</style>
|
||||
<style>.templatestyles-test .foo{color:blue}</style>
|
||||
<style>.templatestyles-test .bar{color:green}</style>
|
||||
</p></div>
|
||||
' ),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
67
tests/phpunit/TemplateStylesMatcherFactoryTest.php
Normal file
67
tests/phpunit/TemplateStylesMatcherFactoryTest.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
use Wikimedia\CSS\Objects\ComponentValueList;
|
||||
use Wikimedia\CSS\Objects\Token;
|
||||
|
||||
/**
|
||||
* @group TemplateStyles
|
||||
*/
|
||||
class TemplateStylesMatcherFactoryTest extends PHPUnit_Framework_TestCase {
|
||||
|
||||
/**
|
||||
* @dataProvider provideUrls
|
||||
* @param string $type
|
||||
* @param string $url
|
||||
* @param bool $expect
|
||||
*/
|
||||
public function testUrls( $type, $url, $expect ) {
|
||||
$factory = new TemplateStylesMatcherFactory( [
|
||||
'test1' => [
|
||||
'<^http://example\.com/test1/>',
|
||||
],
|
||||
'test2' => [
|
||||
'<^http://example\.com/test2/A/>',
|
||||
'<^http://example\.com/test2/B/>',
|
||||
],
|
||||
'anything' => [
|
||||
'<.>',
|
||||
],
|
||||
] );
|
||||
|
||||
$list = new ComponentValueList( [
|
||||
new Token( Token::T_STRING, $url )
|
||||
] );
|
||||
$this->assertSame( $expect, (bool)$factory->urlstring( $type )->match( $list ) );
|
||||
|
||||
$list = new ComponentValueList( [
|
||||
new Token( Token::T_URL, $url )
|
||||
] );
|
||||
$this->assertSame( $expect, (bool)$factory->url( $type )->match( $list ) );
|
||||
}
|
||||
|
||||
public static function provideUrls() {
|
||||
return [
|
||||
[ 'test1', 'http://example.com/test1/foobar', true ],
|
||||
[ 'test2', 'http://example.com/test1/foobar', false ],
|
||||
[ 'test2', 'http://example.com/test2/A/foobar', true ],
|
||||
[ 'test2', 'http://example.com/test2/B/foobar', true ],
|
||||
[ 'test2', 'http://example.com/test2/C/foobar', false ],
|
||||
[ 'test3', 'http://example.com/test3/foobar', false ],
|
||||
[ 'test1', 'http://example.com/test1/../../etc/password', false ],
|
||||
[ 'test1', 'http://example.com/test1/..%2F..%2Fetc%2Fpassword', false ],
|
||||
[ 'test1', 'http://example.com/test1/etc\\password', false ],
|
||||
[ 'test1', 'http://example.com/test%31/foobar', true ],
|
||||
[ 'test1', 'http://example.com/test1/x=/%2E/foobar', false ],
|
||||
[ 'test1', 'http://example.com/test1/?x=/%2E/foobar', true ],
|
||||
[ 'test1', 'http://example.com/test1/%3Fx=/%2E/foobar', false ],
|
||||
[ 'test1', 'http://example.com/test1/#x=/%2E/foobar', true ],
|
||||
[ 'test1', 'http://example.com/test1/%23x=/%2E/foobar', false ],
|
||||
[ 'anything', 'totally bogus', true ],
|
||||
[ 'anything', '/dotdot/../still/fails/though', false ],
|
||||
[ 'anything', '../still/fails/though', false ],
|
||||
[ 'anything', 'still/fails/..', false ],
|
||||
[ 'anything', '..', false ],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue