Initial Commit

This commit is contained in:
Ampera 2022-10-24 11:27:49 -04:00
parent d6ec1121f3
commit 7227036840
8408 changed files with 1936370 additions and 0 deletions

View File

@ -0,0 +1,4 @@
<?php
$mtDbSuffix = '_mw1384';
$mtReadOnly = false;

View File

@ -0,0 +1,89 @@
<?php
//Debug
ini_set("display_errors", "on");
$wgDeprecationReleaseLimit = '1.x';
$wgShowDebug = false;
$wgDevelopmentWarnings = false;
//Paths
$wgScriptPath = "/w";
$wgArticlePath = "/wiki/$1";
$wgServer = "https://$mtWikiName";
$wgResourceBasePath = $wgScriptPath;
$wgUploadDirectory = "$mtDataPath/upload/$mtWikiName/";
$wgCacheDirectory = "$mtDataPath/cache/$mtWikiName/";
$wgImageMagickConvertCommand = "/usr/bin/convert";
$wgDiff3 = "/usr/bin/diff3";
//Branding
//$wgAppleTouchIcon = "https://cdn." . $mtWikiName . "/wiki.png";
$wgFavicon = "/favicon.ico";
//Cache
#$wgUseCdn = true;
#$wgCdnMaxAge = 2592000;
$wgMainCacheType = CACHE_MEMCACHED;
$wgMemCachedServers = [ 'unix:///sock_path/memcached.sock' ];
$wgUseFileCache = false;
//Email
$wgSMTP = [
'host' => '',
'IDHost' => '',
'port' => ,
'username' => '',
'password' => '',
'auth' => true
];
$wgEnableEmail = true;
$wgEnableUserEmail = true;
$wgEnotifUserTalk = true;
$wgEnotifWatchlist = true;
$wgEmailAuthentication = true;
$wgPasswordSender = "noreply@$mtWikiName";
//Database
$wgDBname = $mtUsername . $mtDbSuffix;
$wgDBuser = $mtUsername;
$wgDBtype = "mysql";
$wgDBserver = "";
$wgDBprefix = "";
$wgDBTableOptions = "ENGINE=InnoDB, DEFAULT CHARSET=binary";
//Variables
$wgUseImageMagick = true;
$wgLocaltimezone = "America/New_York";
$wgMaxShellTime = 300;
$wgActions['mcrundo'] = false;
$wgActions['mcrrestore'] = false;
$wgWhitelistRead = [];
$wgWhitelistReadRegexp = [];
//Footer Links
$wgHooks['SkinAddFooterLinks'][] = function ( Skin $skin, string $key, array &$footerlinks ) {
if ( $key === 'places' ) {
$footerlinks['cookies'] = $skin->footerLink( 'cookies', 'cookiepage' );
};
};

View File

@ -0,0 +1,155 @@
<?php
foreach($mtSkins as &$skin) {
wfLoadSkin($skin);
}
foreach($mtExtensions as &$exName){
$mtFuncName = "mt" . $exName;
switch($exName){
//All extensions loaded by wikis MUST be in this list.
//If the extension doesn't require a global config to be set, simply plase the name alike at the top
//Do not define permissions in here, those are always wiki specific
//For all blocks, at the TOP you MUST put wfLoadExtension($exName);
case "AbuseFilter":
case "AdminLinks":
case "AJAXPoll":
case "Arrays":
case "CharInsert":
case "CheckUser":
case "CodeMirror":
case "CookieWarning":
case "ConfirmAccount":
case "CSS":
case "DarkMode":
case "Description2":
case "Disambiguator":
case "Discord":
case "DPLforum":
case "DynamicPageList3":
case "Editcount":
case "EmbedVideo":
case "intersection":
case "Math":
case "MobileFrontend":
case "Moderation":
case "MyVariables":
case "NativeSvgHandler":
case "NewUserMessage":
case "OpenGraphMeta":
case "Popups":
case "RandomSelection":
case "Tabs":
case "TemplateSandbox":
case "TitleKey":
case "TorBlock":
case "UnusedRedirects":
case "UserMerge":
case "Variables":
//MW Defaults
case "CategoryTree":
case "Cite":
case "CiteThisPage":
case "CodeEditor":
case "Gadgets":
case "ImageMap":
case "InputBox":
case "LocalisationUpdate":
case "Nuke":
case "PageImages":
case "ParserFunctions":
case "PdfHandler":
case "Poem":
case "Renameuser":
case "ReplaceText":
case "SecureLinkFixer":
case "SpamBlacklist":
case "TemplateData":
case "TextExtracts":
case "TitleBlacklist":
case "VisualEditor":
case "WhosOnline":
case "WikiEditor":
wfLoadExtension($exName);
break;
//Do not put settings above this comment
case "Widgets":
wfLoadExtension($exName);
$wgWidgetsCompileDir = "$mtDataPath/widgets/$mtWikiName/";
break;
case "Cargo":
wfLoadExtension($exName);
$wgCargoDBname = $mtUsername . "_cargo" . $mtSiteAttributes["dbsuffix"];
$wgCargoDBuser = $mtUsername . "cargo";
break;
case "TimedMediaHandler":
wfLoadExtension($exName);
$wgEnableTranscode = false;
$wgFFmpegLocation = "/usr/bin/ffmpeg";
$wgEnabledTranscodeSet = [
'120p.vp9.webm' => false,
'160p.vp9.webm' => false,
'180p.vp9.webm' => false,
'240p.vp9.webm' => false,
'360p.vp9.webm' => false,
'480p.vp9.webm' => false,
'720p.vp9.webm' => false,
'1080p.vp9.webm' => false,
'1440p.vp9.webm' => false,
'2160p.vp9.webm' => false,
];
// $wgEnabledTranscodeSet = array();
// $wgEnabledAudioTranscodeSet = array();
// $wgTmhWebPlayer = "videojs";
break;
case "SyntaxHighlight_GeSHi":
wfLoadExtension($exName);
$wgPygmentizePath = "/usr/bin/pygmentize";
break;
case "ConfirmEdit":
wfLoadExtensions([ 'ConfirmEdit', 'ConfirmEdit/QuestyCaptcha' ]);
break;
case "ReCaptchaNoCaptcha":
wfLoadExtensions([ 'ConfirmEdit', 'ConfirmEdit/ReCaptchaNoCaptcha' ]);
// $wgCaptchaClass = 'ReCaptchaNoCaptcha';
break;
case "Scribunto":
wfLoadExtension($exName);
$wgScribuntoDefaultEngine = "luasandbox";
$wgScribuntoEngineConf['luastandalone']['cpuLimit'] = 15;
break;
case "OATHAuth":
wfLoadExtension($exName);
$wgGroupPermissions['sysop']['oathauth-disable-for-user'] = false;
$wgGroupPermissions['sysop']['oathauth-verify-user'] = false;
$wgGroupPermissions['sysop']['oathauth-view-log'] = false;
break;
case "Interwiki":
wfLoadExtension($exName);
$wgGroupPermissions['sysop']['interwiki'] = true;
break;
case "timeline":
wfLoadExtension($exName);
$wgTimelinePloticusCommand = "/usr/bin/ploticus";
$wgTimelinePerlCommand = "/usr/bin/perl";
break;
case "AWS":
wfLoadExtension($exName);
$wgFileBackends['s3']['endpoint'] = "";
$wgAWSBucketDomain = "cdn.$mtWikiName";
$wgAWSRegion = 'auto';
$wgAWSRepoHashLevels = '2';
$wgAWSRepoDeletedHashLevels = '3';
$wgAWSCredentials = [
"key" => "",
"secret" => "",
"token" => false
];
break;
default:
throw new Exception("MTMW Invalid Extension, " . $exName . ", please contact system administrator");
return;
break;
}
}
unset($extension);

20
wikiconf/MTSettings.php Normal file
View File

@ -0,0 +1,20 @@
<?php
$mtConfigPath = "/mtmw_install_dir/wikiconf";
$mtDataPath = "/mtmw_install_dir/wikidata";
require_once("$mtConfigPath/GlobalMaintenance.php");
require_once("$mtConfigPath/sites/$mtWikiName.d/settings.php");
require_once("$mtConfigPath/sites/$mtWikiName.d/maintenance.php");
require_once("$mtConfigPath/GlobalSettings.php");
require_once("$mtConfigPath/sites/$mtWikiName.d/secrets.php");
require_once("$mtConfigPath/sites/$mtWikiName.d/loadables.php");
require_once("$mtConfigPath/LoadableSettings.php");
require_once("$mtConfigPath/sites/$mtWikiName.d/extvars.php");
require_once("$mtConfigPath/sites/$mtWikiName.d/permissions.php");
if($mtReadOnly){
$msg = ($mtSiteAttributes["romsg"] != NULL) ? $mtSiteAttributes["romsg"] : "This wiki is undergoing maintenance, please try your action later.";
$wgIgnoreImageErrors = true;
$wgReadOnly = ( PHP_SAPI === 'cli' ) ? false : $msg;
}

View File

@ -0,0 +1,20 @@
<?php
//ConfirmEdit
$wgCaptchaQuestions = [
"What is 2+2?" => "4",
"What is the meaning of life, the universe, and everything?" => "42",
"What is the square root of an onion?" => "shallot"
];
$wgCaptchaTriggers['edit'] = true;
$wgCaptchaTriggers['create']= true;
$wgCaptchaTriggers['createtalk']= true;
$wgCaptchaTriggers['addurl']= true;
$wgCaptchaTriggers['createaccount'] = true;
$wgCaptchaTriggers['badlogin'] = true;
//MinimumNameLength
$wgMinimumUsernameLength = 3;
//AWS
$wgAWSBucketName = "cdn-openwikiproject-org";

View File

@ -0,0 +1,12 @@
<?php
$mtSkins = [
"MonoBook", "Timeless", "Vector"
];
$wgDefaultSkin = "Timeless";
$mtExtensions = [
"AWS", "ConfirmEdit", "CheckUser", "CiteThisPage", "Editcount", "ReplaceText", "UnusedRedirects", "Math", "CSS", "ParserFunctions", "RandomSelection", "Scribunto",
"TitleBlacklist", "Disambiguator", "VisualEditor", "PdfHandler", "NativeSvgHandler", "CodeEditor", "SyntaxHighlight_GeSHi", "WikiEditor"
];

View File

@ -0,0 +1,5 @@
<?php
//$mtDBSuffix = "other_";
$mtReadOnly = false;
//$mtReadOnlyMessage = "This is a separate romsg";
$wgShowExceptionDetails = true;

View File

@ -0,0 +1,5 @@
<?php
$wgGroupPermissions['*']['createaccount'] = false;
$wgGroupPermissions['*']['edit'] = false;
$wgGroupPermissions['user']['edit'] = true;

View File

@ -0,0 +1,8 @@
<?php
$wgAuthenticationTokenVersion = "1";
$wgDBpassword = "";
$wgSecretKey = "";
$wgUpgradeKey = "";

View File

@ -0,0 +1,38 @@
<?php
//Identity
$mtUsername = "openwikiproject";
$wgSitename = "The OpenWiki Project";
$wgMetaNamespace = "openwikiproject";
/* $wgLogos = [
"svg" => ""
];*/
//Locale
$wgLanguageCode = "en";
//Rights
$wgRightsPage = "";
$wgRightsUrl = 'https://creativecommons.org/licenses/by-nc-sa/3.0/';
$wgRightsText = "CC BY-NC-SA 3.0";
$wgRightsIcon = "/w/resources/assets/licenses/cc-by-nc-sa.png";
//Images/Uploads
$wgEnableUploads = true;
$wgUseImageMagick = true;
$wgUseInstantCommons = false;
$wgFileExtensions = [ 'png', 'gif', 'jpg', 'webp', 'svg', 'tif'];
//Namespaces
//Preferences
$wgRestrictDisplayTitle = false;

View File

@ -0,0 +1,382 @@
== License and copyright information ==
=== License ===
MediaWiki is licensed under the terms of the GNU General Public License,
version 2 or later. Derivative works and later versions of the code must be
free software licensed under the same or a compatible license. This includes
"extensions" that use MediaWiki functions or variables; see
https://www.gnu.org/licenses/gpl-faq.html#GPLAndPlugins for details.
For the full text of version 2 of the license, see
https://www.gnu.org/licenses/gpl-2.0.html or '''GNU General Public License'''
below.
=== Copyright owners ===
MediaWiki contributors, including those listed in the CREDITS file, hold the
copyright to this work.
=== Additional license information ===
Some components of MediaWiki imported from other projects may be under other
Free and Open Source, or Free Culture, licenses. Specific details of their
licensing information can be found in those components.
Sections of code written exclusively by Lee Crocker or Erik Moeller are also
released into the public domain, which does not impair the obligations of users
under the GPL for use of the whole code or other sections thereof.
MediaWiki uses the following Creative Commons icons to illustrate links to the
CC licenses:
* resources/assets/licenses/cc-0.png
* resources/assets/licenses/cc-by-nc-sa.png
* resources/assets/licenses/cc-by-sa.png
* resources/assets/licenses/cc-by.png
These icons are trademarked, and used subject to the CC trademark license,
available at https://creativecommons.org/policies#trademark
== 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 Library 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.
== 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 Library General
Public License instead of this License.

View File

@ -0,0 +1,15 @@
<?php
if ( !defined( 'MEDIAWIKI' ) ) {
exit;
}
if(php_sapi_name() == "cli"){
if(getenv("MT_WIKI_NAME") != false){
$mtWikiName = getenv("MT_WIKI_NAME");
}else{
$mtWikiName = readline("PLEASE ENTER SITENAME:");
}
}else{
$mtWikiName = $_SERVER["HTTP_HOST"];
}
require_once("/mtmw_install_dir/wikiconf/MTSettings.php");

View File

@ -0,0 +1,126 @@
<?php
/**
* The web entry point for all %Action API queries, handled by ApiMain
* and ApiBase subclasses.
*
* This is used by bots to fetch content and information about the wiki,
* its pages, and its users. See <https://www.mediawiki.org/wiki/API> for more
* information.
*
* It begins by constructing a new ApiMain using the parameter passed to it
* as an argument in the URL ('?action='). It then invokes "execute()" on the
* ApiMain object instance, which produces output in the format specified in
* the URL.
*
* Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup entrypoint
* @ingroup API
*/
use MediaWiki\Logger\LegacyLogger;
// So extensions (and other code) can check whether they're running in API mode
define( 'MW_API', true );
define( 'MW_ENTRY_POINT', 'api' );
require __DIR__ . '/includes/WebStart.php';
wfApiMain();
function wfApiMain() {
global $wgRequest, $wgTitle, $wgAPIRequestLog;
$starttime = microtime( true );
// PATH_INFO can be used for stupid things. We don't support it for api.php at
// all, so error out if it's present. (T128209)
if ( isset( $_SERVER['PATH_INFO'] ) && $_SERVER['PATH_INFO'] != '' ) {
$correctUrl = wfAppendQuery( wfScript( 'api' ), $wgRequest->getQueryValuesOnly() );
$correctUrl = wfExpandUrl( $correctUrl, PROTO_CANONICAL );
header( "Location: $correctUrl", true, 301 );
echo 'This endpoint does not support "path info", i.e. extra text between "api.php"'
. 'and the "?". Remove any such text and try again.';
die( 1 );
}
// Set a dummy $wgTitle, because $wgTitle == null breaks various things
// In a perfect world this wouldn't be necessary
$wgTitle = Title::makeTitle( NS_SPECIAL, 'Badtitle/dummy title for API calls set in api.php' );
// RequestContext will read from $wgTitle, but it will also whine about it.
// In a perfect world this wouldn't be necessary either.
RequestContext::getMain()->setTitle( $wgTitle );
try {
// Construct an ApiMain with the arguments passed via the URL. What we get back
// is some form of an ApiMain, possibly even one that produces an error message,
// but we don't care here, as that is handled by the constructor.
$processor = new ApiMain( RequestContext::getMain(), true );
// Last chance hook before executing the API
Hooks::runner()->onApiBeforeMain( $processor );
if ( !$processor instanceof ApiMain ) {
throw new MWException( 'ApiBeforeMain hook set $processor to a non-ApiMain class' );
}
} catch ( Throwable $e ) {
// Crap. Try to report the exception in API format to be friendly to clients.
ApiMain::handleApiBeforeMainException( $e );
$processor = false;
}
// Process data & print results
if ( $processor ) {
$processor->execute();
}
// Log what the user did, for book-keeping purposes.
$endtime = microtime( true );
// Log the request
if ( $wgAPIRequestLog ) {
$items = [
wfTimestamp( TS_MW ),
$endtime - $starttime,
$wgRequest->getIP(),
$wgRequest->getHeader( 'User-agent' )
];
$items[] = $wgRequest->wasPosted() ? 'POST' : 'GET';
if ( $processor ) {
try {
$manager = $processor->getModuleManager();
$module = $manager->getModule( $wgRequest->getRawVal( 'action' ), 'action' );
} catch ( Throwable $ex ) {
$module = null;
}
if ( !$module || $module->mustBePosted() ) {
$items[] = "action=" . $wgRequest->getRawVal( 'action' );
} else {
$items[] = wfArrayToCgi( $wgRequest->getValues() );
}
} else {
$items[] = "failed in ApiBeforeMain";
}
LegacyLogger::emit( implode( ',', $items ) . "\n", $wgAPIRequestLog );
wfDebug( "Logged API request to $wgAPIRequestLog" );
}
$mediawiki = new MediaWiki();
$mediawiki->doPostOutputShutdown();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,182 @@
{
"name": "mediawiki/core",
"description": "Free software wiki application developed by the Wikimedia Foundation and others",
"type": "mediawiki-core",
"keywords": [
"mediawiki",
"wiki"
],
"homepage": "https://www.mediawiki.org/",
"authors": [
{
"name": "MediaWiki Community",
"homepage": "https://www.mediawiki.org/wiki/Special:Version/Credits"
}
],
"license": "GPL-2.0-or-later",
"support": {
"issues": "https://bugs.mediawiki.org/",
"irc": "irc://irc.libera.chat/mediawiki",
"wiki": "https://www.mediawiki.org/"
},
"prefer-stable": true,
"require": {
"composer/semver": "3.2.6",
"cssjanus/cssjanus": "2.1.0",
"ext-calendar": "*",
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-iconv": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"guzzlehttp/guzzle": "7.4.5",
"justinrainbow/json-schema": "5.2.11",
"liuggio/statsd-php-client": "1.0.18",
"monolog/monolog": "2.2.0",
"oojs/oojs-ui": "0.43.2",
"pear/mail": "1.4.1",
"pear/mail_mime": "1.10.11",
"pear/net_smtp": "1.10.0",
"php": ">=7.3.19",
"psr/container": "1.1.1",
"psr/log": "1.1.4",
"ralouphie/getallheaders": "3.0.3",
"symfony/polyfill-php80": "1.25.0",
"symfony/yaml": "5.4.3",
"wikimedia/assert": "0.5.1",
"wikimedia/at-ease": "2.1.0",
"wikimedia/base-convert": "2.0.1",
"wikimedia/cdb": "2.0.0",
"wikimedia/cldr-plural-rule-parser": "2.0.0",
"wikimedia/common-passwords": "0.4.0",
"wikimedia/composer-merge-plugin": "2.0.1",
"wikimedia/html-formatter": "3.0.1",
"wikimedia/ip-set": "3.0.0",
"wikimedia/ip-utils": "4.0.0",
"wikimedia/less.php": "3.1.0",
"wikimedia/minify": "2.2.6",
"wikimedia/normalized-exception": "1.0.1",
"wikimedia/object-factory": "4.0.0",
"wikimedia/parsoid": "0.15.1",
"wikimedia/php-session-serializer": "2.0.0",
"wikimedia/purtle": "1.0.8",
"wikimedia/relpath": "3.0.0",
"wikimedia/remex-html": "3.0.2",
"wikimedia/request-timeout": "1.2.0",
"wikimedia/running-stat": "1.2.1",
"wikimedia/scoped-callback": "3.0.0",
"wikimedia/services": "2.0.1",
"wikimedia/shellbox": "3.0.0",
"wikimedia/utfnormal": "3.0.2",
"wikimedia/timestamp": "3.0.0",
"wikimedia/wait-condition-loop": "2.0.2",
"wikimedia/wrappedstring": "4.0.1",
"wikimedia/xmp-reader": "0.8.1",
"zordius/lightncandy": "1.2.6"
},
"require-dev": {
"ext-simplexml": "*",
"composer/spdx-licenses": "1.5.5",
"doctrine/dbal": "3.1.5",
"doctrine/sql-formatter": "1.1.1",
"giorgiosironi/eris": "^0.10.0",
"hamcrest/hamcrest-php": "^2.0",
"johnkary/phpunit-speedtrap": "^4.0",
"mediawiki/mediawiki-codesniffer": "38.0.0",
"mediawiki/mediawiki-phan-config": "0.11.0",
"nikic/php-parser": "^4.10.2",
"nmred/kafka-php": "0.1.5",
"php-parallel-lint/php-console-highlighter": "0.5",
"php-parallel-lint/php-parallel-lint": "1.3.1",
"phpunit/phpunit": "8.5.28",
"psy/psysh": "^0.11.1",
"seld/jsonlint": "1.8.3",
"wikimedia/testing-access-wrapper": "~2.0",
"wmde/hamcrest-html-matchers": "^1.0.0"
},
"replace": {
"symfony/polyfill-ctype": "1.99",
"symfony/polyfill-mbstring": "1.99"
},
"suggest": {
"ext-apcu": "Faster web responses overall.",
"ext-curl": "Faster HTTP services, e.g. when using InstantCommons, Swift, or Etcd.",
"ext-gd": "Enable thumbnails for file uploads.",
"ext-mysqli": "Enable the MySQL and MariaDB database type for MediaWiki.",
"ext-openssl": "Encrypt session data (or opt-out via $wgSessionInsecureSecrets).",
"ext-pdo": "Enable the SQLite database type for MediaWiki.",
"ext-pgsql": "Enable the PostgreSQL database type for MediaWiki.",
"ext-posix": "Enable CLI concurrent processing, e.g. for runJobs.php.",
"ext-pcntl": "Enable CLI concurrent processing, e.g. for runJobs.php and rebuildLocalisationCache.php.",
"ext-readline": "Enable CLI history and autocomplete, e.g. for eval.php and other REPLs.",
"ext-sockets": "Enable CLI concurrent processing, e.g. for rebuildLocalisationCache.php.",
"ext-wikidiff2": "Faster text difference engine.",
"ext-zlib": "Enable use of GZIP compression, e.g. for SqlBagOStuff (ParserCache), $wgCompressRevisions, or $wgUseFileCache.",
"monolog/monolog": "Enable use of MonologSpi ($wgMWLoggerDefaultSpi).",
"nmred/kafka-php": "Enable use of KafkaHandler (MonologSpi), or EventRelayerKafka ($wgEventRelayerConfig)."
},
"autoload": {
"psr-0": {
"ComposerHookHandler": "includes/composer",
"ComposerVendorHtaccessCreator": "includes/composer",
"ComposerPhpunitXmlCoverageEdit": "includes/composer"
}
},
"autoload-dev": {
"files": [
"vendor/hamcrest/hamcrest-php/hamcrest/Hamcrest.php",
"vendor/wmde/hamcrest-html-matchers/src/functions.php"
]
},
"scripts": {
"mw-install:sqlite": "php maintenance/install.php --server=http://localhost:4000 --dbtype sqlite --dbpath cache/ --scriptpath '' --pass adminpassword MediaWiki Admin",
"serve": "php -S localhost:4000",
"lint": "parallel-lint --exclude node_modules --exclude vendor",
"phan": "phan -d . --long-progress-bar",
"phpcs": "phpcs -p -s --cache",
"fix": [
"phpcbf"
],
"pre-install-cmd": "ComposerHookHandler::onPreInstall",
"pre-update-cmd": "ComposerHookHandler::onPreUpdate",
"post-install-cmd": "ComposerVendorHtaccessCreator::onEvent",
"post-update-cmd": "ComposerVendorHtaccessCreator::onEvent",
"releasenotes": "@phpunit:entrypoint --group ReleaseNotes",
"test": [
"@lint .",
"@phpcs ."
],
"test-some": [
"@lint",
"@phpcs"
],
"phpunit": "phpunit",
"phpunit:unit": "phpunit --colors=always --testsuite=core:unit,extensions:unit,skins:unit",
"phpunit:integration": "phpunit --colors=always --testsuite=core:integration,extensions:integration,skins:integration",
"phpunit:coverage": "phpunit --testsuite=core:unit --exclude-group Dump,Broken",
"phpunit:coverage-edit": "ComposerPhpunitXmlCoverageEdit::onEvent",
"phpunit:entrypoint": "php tests/phpunit/phpunit.php"
},
"config": {
"optimize-autoloader": true,
"prepend-autoloader": false,
"allow-plugins": {
"composer/package-versions-deprecated": true,
"wikimedia/composer-merge-plugin": true,
"composer/installers": true
}
},
"extra": {
"merge-plugin": {
"include": [
"composer.local.json"
],
"merge-dev": false
}
}
}

View File

@ -0,0 +1,10 @@
{
"extra": {
"merge-plugin": {
"include": [
"extensions/*/composer.json",
"skins/*/composer.json"
]
}
}
}

View File

@ -0,0 +1,227 @@
<?php
/**
* The web entry point for serving non-public images to logged-in users.
*
* To use this, see https://www.mediawiki.org/wiki/Manual:Image_Authorization
*
* - Set $wgUploadDirectory to a non-public directory (not web accessible)
* - Set $wgUploadPath to point to this file
*
* Optional Parameters
*
* - Set $wgImgAuthDetails = true if you want the reason the access was denied messages to
* be displayed instead of just the 403 error (doesn't work on IE anyway),
* otherwise it will only appear in error logs
*
* For security reasons, you usually don't want your user to know *why* access was denied,
* just that it was. If you want to change this, you can set $wgImgAuthDetails to 'true'
* in localsettings.php and it will give the user the reason why access was denied.
*
* Your server needs to support REQUEST_URI or PATH_INFO; CGI-based
* configurations sometimes don't.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup entrypoint
*/
define( 'MW_NO_OUTPUT_COMPRESSION', 1 );
define( 'MW_ENTRY_POINT', 'img_auth' );
require __DIR__ . '/includes/WebStart.php';
wfImageAuthMain();
$mediawiki = new MediaWiki();
$mediawiki->doPostOutputShutdown();
function wfImageAuthMain() {
global $wgImgAuthUrlPathMap, $wgScriptPath, $wgImgAuthPath;
$services = \MediaWiki\MediaWikiServices::getInstance();
$permissionManager = $services->getPermissionManager();
$request = RequestContext::getMain()->getRequest();
$publicWiki = in_array( 'read', $permissionManager->getGroupPermissions( [ '*' ] ), true );
// Find the path assuming the request URL is relative to the local public zone URL
$baseUrl = $services->getRepoGroup()->getLocalRepo()->getZoneUrl( 'public' );
if ( $baseUrl[0] === '/' ) {
$basePath = $baseUrl;
} else {
$basePath = parse_url( $baseUrl, PHP_URL_PATH );
}
$path = WebRequest::getRequestPathSuffix( $basePath );
if ( $path === false ) {
// Try instead assuming img_auth.php is the base path
$basePath = $wgImgAuthPath ?: "$wgScriptPath/img_auth.php";
$path = WebRequest::getRequestPathSuffix( $basePath );
}
if ( $path === false ) {
wfForbidden( 'img-auth-accessdenied', 'img-auth-notindir' );
return;
}
if ( $path === '' || $path[0] !== '/' ) {
// Make sure $path has a leading /
$path = "/" . $path;
}
$user = RequestContext::getMain()->getUser();
// Various extensions may have their own backends that need access.
// Check if there is a special backend and storage base path for this file.
foreach ( $wgImgAuthUrlPathMap as $prefix => $storageDir ) {
$prefix = rtrim( $prefix, '/' ) . '/'; // implicit trailing slash
if ( strpos( $path, $prefix ) === 0 ) {
$be = $services->getFileBackendGroup()->backendFromPath( $storageDir );
$filename = $storageDir . substr( $path, strlen( $prefix ) ); // strip prefix
// Check basic user authorization
$isAllowedUser = $permissionManager->userHasRight( $user, 'read' );
if ( !$isAllowedUser ) {
wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $path );
return;
}
if ( $be->fileExists( [ 'src' => $filename ] ) ) {
wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." );
$be->streamFile( [
'src' => $filename,
'headers' => [ 'Cache-Control: private', 'Vary: Cookie' ]
] );
} else {
wfForbidden( 'img-auth-accessdenied', 'img-auth-nofile', $path );
}
return;
}
}
// Get the local file repository
$repo = $services->getRepoGroup()->getRepo( 'local' );
$zone = strstr( ltrim( $path, '/' ), '/', true );
// Get the full file storage path and extract the source file name.
// (e.g. 120px-Foo.png => Foo.png or page2-120px-Foo.png => Foo.png).
// This only applies to thumbnails/transcoded, and each of them should
// be under a folder that has the source file name.
if ( $zone === 'thumb' || $zone === 'transcoded' ) {
$name = wfBaseName( dirname( $path ) );
$filename = $repo->getZonePath( $zone ) . substr( $path, strlen( "/" . $zone ) );
// Check to see if the file exists
if ( !$repo->fileExists( $filename ) ) {
wfForbidden( 'img-auth-accessdenied', 'img-auth-nofile', $filename );
return;
}
} else {
$name = wfBaseName( $path ); // file is a source file
$filename = $repo->getZonePath( 'public' ) . $path;
// Check to see if the file exists and is not deleted
$bits = explode( '!', $name, 2 );
if ( substr( $path, 0, 9 ) === '/archive/' && count( $bits ) == 2 ) {
$file = $repo->newFromArchiveName( $bits[1], $name );
} else {
$file = $repo->newFile( $name );
}
if ( !$file->exists() || $file->isDeleted( File::DELETED_FILE ) ) {
wfForbidden( 'img-auth-accessdenied', 'img-auth-nofile', $filename );
return;
}
}
$headers = []; // extra HTTP headers to send
$title = Title::makeTitleSafe( NS_FILE, $name );
if ( !$publicWiki ) {
// For private wikis, run extra auth checks and set cache control headers
$headers['Cache-Control'] = 'private';
$headers['Vary'] = 'Cookie';
if ( !$title instanceof Title ) { // files have valid titles
wfForbidden( 'img-auth-accessdenied', 'img-auth-badtitle', $name );
return;
}
// Run hook for extension authorization plugins
/** @var array $result */
$result = null;
if ( !Hooks::runner()->onImgAuthBeforeStream( $title, $path, $name, $result ) ) {
wfForbidden( $result[0], $result[1], array_slice( $result, 2 ) );
return;
}
// Check user authorization for this title
// Checks Whitelist too
if ( !$permissionManager->userCan( 'read', $user, $title ) ) {
wfForbidden( 'img-auth-accessdenied', 'img-auth-noread', $name );
return;
}
}
if ( isset( $_SERVER['HTTP_RANGE'] ) ) {
$headers['Range'] = $_SERVER['HTTP_RANGE'];
}
if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
$headers['If-Modified-Since'] = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
}
if ( $request->getCheck( 'download' ) ) {
$headers['Content-Disposition'] = 'attachment';
}
// Allow modification of headers before streaming a file
Hooks::runner()->onImgAuthModifyHeaders( $title->getTitleValue(), $headers );
// Stream the requested file
list( $headers, $options ) = HTTPFileStreamer::preprocessHeaders( $headers );
wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." );
$repo->streamFileWithStatus( $filename, $headers, $options );
}
/**
* Issue a standard HTTP 403 Forbidden header ($msg1-a message index, not a message) and an
* error message ($msg2, also a message index), (both required) then end the script
* subsequent arguments to $msg2 will be passed as parameters only for replacing in $msg2
* @param string $msg1
* @param string $msg2
* @param mixed ...$args To pass as params to wfMessage() with $msg2. Either variadic, or a single
* array argument.
*/
function wfForbidden( $msg1, $msg2, ...$args ) {
global $wgImgAuthDetails;
$args = ( isset( $args[0] ) && is_array( $args[0] ) ) ? $args[0] : $args;
$msgHdr = wfMessage( $msg1 )->text();
$detailMsgKey = $wgImgAuthDetails ? $msg2 : 'badaccess-group0';
$detailMsg = wfMessage( $detailMsgKey, $args )->text();
wfDebugLog( 'img_auth',
"wfForbidden Hdr: " . wfMessage( $msg1 )->inLanguage( 'en' )->text() . " Msg: " .
wfMessage( $msg2, $args )->inLanguage( 'en' )->text()
);
HttpStatus::header( 403 );
header( 'Cache-Control: no-cache' );
header( 'Content-Type: text/html; charset=utf-8' );
$templateParser = new TemplateParser();
echo $templateParser->processTemplate( 'ImageAuthForbidden', [
'msgHdr' => $msgHdr,
'detailMsg' => $detailMsg,
] );
}

View File

@ -0,0 +1 @@
Deny from all

View File

@ -0,0 +1,100 @@
<?php
use MediaWiki\MediaWikiServices;
use MediaWiki\User\ActorStoreFactory;
/**
* This is not intended to be a long-term part of MediaWiki; it will be
* deprecated and removed once actor migration is complete.
*
* @since 1.31
* @since 1.34 Use with 'ar_user', 'img_user', 'oi_user', 'fa_user',
* 'rc_user', 'log_user', and 'ipb_by' is deprecated. Callers should
* reference the corresponding actor fields directly.
*/
class ActorMigration extends ActorMigrationBase {
/**
* Constant for extensions to feature-test whether $wgActorTableSchemaMigrationStage
* (in MW <1.34) expects MIGRATION_* or SCHEMA_COMPAT_*
*/
public const MIGRATION_STAGE_SCHEMA_COMPAT = 1;
/**
* Field information
* @see ActorMigrationBase::getFieldInfo()
*/
public const FIELD_INFOS = [
'rev_user' => [
'tempTable' => [
'table' => 'revision_actor_temp',
'pk' => 'revactor_rev',
'field' => 'revactor_actor',
'joinPK' => 'rev_id',
'extra' => [
'revactor_timestamp' => 'rev_timestamp',
'revactor_page' => 'rev_page',
],
]
],
// Deprecated since 1.34
'ar_user' => [
'deprecatedVersion' => '1.37',
],
// Deprecated since 1.34
'img_user' => [
'deprecatedVersion' => '1.37',
],
// Deprecated since 1.34
'oi_user' => [
'deprecatedVersion' => '1.37',
],
// Deprecated since 1.34
'fa_user' => [
'deprecatedVersion' => '1.37',
],
// Deprecated since 1.34
'rc_user' => [
'deprecatedVersion' => '1.37',
],
// Deprecated since 1.34
'log_user' => [
'deprecatedVersion' => '1.37',
],
// Deprecated since 1.34
'ipb_by' => [
'deprecatedVersion' => '1.37',
'textField' => 'ipb_by_text',
'actorField' => 'ipb_by_actor'
]
];
/**
* Static constructor
* @return self
*/
public static function newMigration() {
return MediaWikiServices::getInstance()->getActorMigration();
}
/**
* @internal
*
* @param int $stage
* @param ActorStoreFactory $actorStoreFactory
*/
public function __construct(
$stage,
ActorStoreFactory $actorStoreFactory
) {
if ( $stage & SCHEMA_COMPAT_OLD ) {
throw new InvalidArgumentException(
'The old actor table schema is no longer supported' );
}
parent::__construct(
self::FIELD_INFOS,
$stage,
$actorStoreFactory
);
}
}

View File

@ -0,0 +1,514 @@
<?php
/**
* Methods to help with the actor table migration
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\User\ActorStoreFactory;
use MediaWiki\User\UserIdentity;
use Wikimedia\IPUtils;
use Wikimedia\Rdbms\IDatabase;
/**
* This abstract base class helps migrate core and extension code to use the
* actor table.
*
* @stable to extend
* @since 1.37
*/
class ActorMigrationBase {
/** @var array[] Cache for `self::getJoin()` */
private $joinCache = [];
/** @var int One of the SCHEMA_COMPAT_READ_* values */
private $readStage;
/** @var int A combination of the SCHEMA_COMPAT_WRITE_* flags */
private $writeStage;
/** @var ActorStoreFactory */
private $actorStoreFactory;
/** @var array */
private $fieldInfos;
/** @var bool */
private $allowUnknown;
/**
* @param array $fieldInfos An array of associative arrays, giving configuration
* information about fields which are being migrated. Subkeys are:
* - removedVersion: The version in which the field was removed
* - deprecatedVersion: The version in which the field was deprecated
* - component: The component for removedVersion and deprecatedVersion.
* Default: MediaWiki.
* - textField: Override the old text field name. Default {$key}_text.
* - actorField: Override the actor field name. Default {$key}_actor.
* - tempTable: An array of information about the temp table linking
* the old table to the actor table. Default: no temp table is used.
* If set, the following subkeys must be present:
* - table: Temporary table name
* - pk: Temporary table column referring to the main table's primary key
* - field: Temporary table column referring actor.actor_id
* - joinPK: Main table's primary key
* - extra: An array of extra field names to be copied into the
* temp table for indexing. The key is the field name in the temp
* table, and the value is the field name in the main table.
* - formerTempTableVersion: The version of the component in which this
* field used a temp table. If present, getInsertValuesWithTempTable()
* still works, but issues a deprecation warning.
* All subkeys are optional.
*
* @stable to override
* @stable to call
*
* @param int $stage The migration stage. This is a combination of
* SCHEMA_COMPAT_* flags:
* - SCHEMA_COMPAT_READ_OLD, SCHEMA_COMPAT_WRITE_OLD: Use the old schema,
* with *_user and *_user_text fields.
* - SCHEMA_COMPAT_READ_TEMP, SCHEMA_COMPAT_WRITE_TEMP: Use the new schema,
* with an actor table. Normal tables are joined via a *_actor field,
* whereas temp tables are joined to the actor table via an
* intermediate table.
* - SCHEMA_COMPAT_READ_NEW, SCHEMA_COMPAT_WRITE_NEW: Use the new
* schema. Former temp tables are no longer used, and all relevant
* tables join directly to the actor table.
*
* @param ActorStoreFactory $actorStoreFactory
* @param array $options Array of other options. May contain:
* - allowUnknown: Allow fields not present in $fieldInfos. True by default.
*/
public function __construct(
$fieldInfos,
$stage,
ActorStoreFactory $actorStoreFactory,
$options = []
) {
$this->fieldInfos = $fieldInfos;
$this->allowUnknown = $options['allowUnknown'] ?? true;
$writeStage = $stage & SCHEMA_COMPAT_WRITE_MASK;
$readStage = $stage & SCHEMA_COMPAT_READ_MASK;
if ( $writeStage === 0 ) {
throw new InvalidArgumentException( '$stage must include a write mode' );
}
if ( $readStage === 0 ) {
throw new InvalidArgumentException( '$stage must include a read mode' );
}
if ( !in_array( $readStage,
[ SCHEMA_COMPAT_READ_OLD, SCHEMA_COMPAT_READ_TEMP, SCHEMA_COMPAT_READ_NEW ] )
) {
throw new InvalidArgumentException( 'Cannot read multiple schemas' );
}
if ( $readStage === SCHEMA_COMPAT_READ_OLD && !( $writeStage & SCHEMA_COMPAT_WRITE_OLD ) ) {
throw new InvalidArgumentException( 'Cannot read the old schema without also writing it' );
}
if ( $readStage === SCHEMA_COMPAT_READ_TEMP && !( $writeStage & SCHEMA_COMPAT_WRITE_TEMP ) ) {
throw new InvalidArgumentException( 'Cannot read the temp schema without also writing it' );
}
if ( $readStage === SCHEMA_COMPAT_READ_NEW && !( $writeStage & SCHEMA_COMPAT_WRITE_NEW ) ) {
throw new InvalidArgumentException( 'Cannot read the new schema without also writing it' );
}
$this->readStage = $readStage;
$this->writeStage = $writeStage;
$this->actorStoreFactory = $actorStoreFactory;
}
/**
* Get config information about a field.
*
* @stable to override
*
* @param string $key
* @return array
*/
protected function getFieldInfo( $key ) {
if ( isset( $this->fieldInfos[$key] ) ) {
return $this->fieldInfos[$key];
} elseif ( $this->allowUnknown ) {
return [];
} else {
throw new InvalidArgumentException( $this->getInstanceName() . ": unknown key $key" );
}
}
/**
* Get a name for this instance to use in error messages
*
* @stable to override
*
* @return string
* @throws ReflectionException
*/
protected function getInstanceName() {
if ( ( new ReflectionClass( $this ) )->isAnonymous() ) {
// Mostly for PHPUnit
return self::class;
} else {
return static::class;
}
}
/**
* Issue deprecation warning/error as appropriate.
*
* @internal
*
* @param string $key
*/
protected function checkDeprecation( $key ) {
$fieldInfo = $this->getFieldInfo( $key );
if ( isset( $fieldInfo['removedVersion'] ) ) {
$removedVersion = $fieldInfo['removedVersion'];
$component = $fieldInfo['component'] ?? 'MediaWiki';
throw new InvalidArgumentException(
"Use of {$this->getInstanceName()} for '$key' was removed in $component $removedVersion"
);
}
if ( isset( $fieldInfo['deprecatedVersion'] ) ) {
$deprecatedVersion = $fieldInfo['deprecatedVersion'];
$component = $fieldInfo['component'] ?? 'MediaWiki';
wfDeprecated( "{$this->getInstanceName()} for '$key'", $deprecatedVersion, $component, 3 );
}
}
/**
* Return an SQL condition to test if a user field is anonymous
* @param string $field Field name or SQL fragment
* @return string
*/
public function isAnon( $field ) {
return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NULL" : "$field = 0";
}
/**
* Return an SQL condition to test if a user field is non-anonymous
* @param string $field Field name or SQL fragment
* @return string
*/
public function isNotAnon( $field ) {
return ( $this->readStage >= SCHEMA_COMPAT_READ_TEMP ) ? "$field IS NOT NULL" : "$field != 0";
}
/**
* @param string $key A key such as "rev_user" identifying the actor
* field being fetched.
* @return string[] [ $text, $actor ]
*/
private function getFieldNames( $key ) {
$fieldInfo = $this->getFieldInfo( $key );
$textField = $fieldInfo['textField'] ?? $key . '_text';
$actorField = $fieldInfo['actorField'] ?? substr( $key, 0, -5 ) . '_actor';
return [ $textField, $actorField ];
}
/**
* Convenience function for getting temp table config
*
* @param string $key
* @return array|null
*/
private function getTempTableInfo( $key ) {
$fieldInfo = $this->getFieldInfo( $key );
return $fieldInfo['tempTable'] ?? null;
}
/**
* Get SELECT fields and joins for the actor key
*
* @param string $key A key such as "rev_user" identifying the actor
* field being fetched.
* @return array[] With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
* All tables, fields, and joins are aliased, so `+` is safe to use.
* @phan-return array{tables:string[],fields:string[],joins:array}
*/
public function getJoin( $key ) {
$this->checkDeprecation( $key );
if ( !isset( $this->joinCache[$key] ) ) {
$tables = [];
$fields = [];
$joins = [];
list( $text, $actor ) = $this->getFieldNames( $key );
if ( $this->readStage === SCHEMA_COMPAT_READ_OLD ) {
$fields[$key] = $key;
$fields[$text] = $text;
$fields[$actor] = 'NULL';
} elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
$tempTableInfo = $this->getTempTableInfo( $key );
if ( $tempTableInfo ) {
$alias = "temp_$key";
$tables[$alias] = $tempTableInfo['table'];
$joins[$alias] = [ 'JOIN',
"{$alias}.{$tempTableInfo['pk']} = {$tempTableInfo['joinPK']}" ];
$joinField = "{$alias}.{$tempTableInfo['field']}";
} else {
$joinField = $actor;
}
$alias = "actor_$key";
$tables[$alias] = 'actor';
$joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$joinField}" ];
$fields[$key] = "{$alias}.actor_user";
$fields[$text] = "{$alias}.actor_name";
$fields[$actor] = $joinField;
} else /* SCHEMA_COMPAT_READ_NEW */ {
$alias = "actor_$key";
$tables[$alias] = 'actor';
$joins[$alias] = [ 'JOIN', "{$alias}.actor_id = {$actor}" ];
$fields[$key] = "{$alias}.actor_user";
$fields[$text] = "{$alias}.actor_name";
$fields[$actor] = $actor;
}
$this->joinCache[$key] = [
'tables' => $tables,
'fields' => $fields,
'joins' => $joins,
];
}
return $this->joinCache[$key];
}
/**
* Get UPDATE fields for the actor
*
* @param IDatabase $dbw Database to use for creating an actor ID, if necessary
* @param string $key A key such as "rev_user" identifying the actor
* field being fetched.
* @param UserIdentity $user User to set in the update
* @return array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()`
*/
public function getInsertValues( IDatabase $dbw, $key, UserIdentity $user ) {
$this->checkDeprecation( $key );
if ( $this->getTempTableInfo( $key ) ) {
throw new InvalidArgumentException( "Must use getInsertValuesWithTempTable() for $key" );
}
list( $text, $actor ) = $this->getFieldNames( $key );
$ret = [];
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
$ret[$key] = $user->getId();
$ret[$text] = $user->getName();
}
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP
|| $this->writeStage & SCHEMA_COMPAT_WRITE_NEW
) {
$ret[$actor] = $this->actorStoreFactory
->getActorNormalization( $dbw->getDomainID() )
->acquireActorId( $user, $dbw );
}
return $ret;
}
/**
* Get UPDATE fields for the actor
*
* @param IDatabase $dbw Database to use for creating an actor ID, if necessary
* @param string $key A key such as "rev_user" identifying the actor
* field being fetched.
* @param UserIdentity $user User to set in the update
* @return array with two values:
* - array to merge into `$values` to `IDatabase->update()` or `$a` to `IDatabase->insert()`
* - callback to call with the primary key for the main table insert
* and extra fields needed for the temp table.
*/
public function getInsertValuesWithTempTable( IDatabase $dbw, $key, UserIdentity $user ) {
$this->checkDeprecation( $key );
$fieldInfo = $this->getFieldInfo( $key );
$tempTableInfo = $fieldInfo['tempTable'] ?? null;
if ( isset( $fieldInfo['formerTempTableVersion'] ) ) {
wfDeprecated( __METHOD__ . " for $key",
$fieldInfo['formerTempTableVersion'],
$fieldInfo['component'] ?? 'MediaWiki' );
} elseif ( !$tempTableInfo ) {
throw new InvalidArgumentException( "Must use getInsertValues() for $key" );
}
list( $text, $actor ) = $this->getFieldNames( $key );
$ret = [];
$callback = null;
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_OLD ) {
$ret[$key] = $user->getId();
$ret[$text] = $user->getName();
}
if ( $this->writeStage & ( SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW ) ) {
$id = $this->actorStoreFactory
->getActorNormalization( $dbw->getDomainID() )
->acquireActorId( $user, $dbw );
if ( $tempTableInfo ) {
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_TEMP ) {
$func = __METHOD__;
$callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $dbw, $id, $func ) {
$set = [ $tempTableInfo['field'] => $id ];
foreach ( $tempTableInfo['extra'] as $to => $from ) {
if ( !array_key_exists( $from, $extra ) ) {
throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
}
$set[$to] = $extra[$from];
}
$dbw->upsert(
$tempTableInfo['table'],
[ $tempTableInfo['pk'] => $pk ] + $set,
[ [ $tempTableInfo['pk'] ] ],
$set,
$func
);
};
}
if ( $this->writeStage & SCHEMA_COMPAT_WRITE_NEW ) {
$ret[$actor] = $id;
}
} else {
$ret[$actor] = $id;
}
}
if ( $callback === null ) {
// Make a validation-only callback if there was temp table info
if ( $tempTableInfo ) {
$func = __METHOD__;
$callback = static function ( $pk, array $extra ) use ( $tempTableInfo, $func ) {
foreach ( $tempTableInfo['extra'] as $to => $from ) {
if ( !array_key_exists( $from, $extra ) ) {
throw new InvalidArgumentException( "$func callback: \$extra[$from] is not provided" );
}
}
};
} else {
$callback = static function ( $pk, array $extra ) {
};
}
}
return [ $ret, $callback ];
}
/**
* Get WHERE condition for the actor
*
* @param IDatabase $db Database to use for quoting and list-making
* @param string $key A key such as "rev_user" identifying the actor
* field being fetched.
* @param UserIdentity|UserIdentity[]|null|false $users Users to test for.
* Passing null, false, or the empty array will return 'conds' that never match,
* and an empty array for 'orconds'.
* @param bool $useId If false, don't try to query by the user ID.
* Intended for use with rc_user since it has an index on
* (rc_user_text,rc_timestamp) but not (rc_user,rc_timestamp).
* @return array With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
* - conds: (string) to include in the `$cond` to `IDatabase->select()`
* - orconds: (array[]) array of alternatives in case a union of multiple
* queries would be more efficient than a query with OR. May have keys
* 'actor', 'userid', 'username'.
* Since 1.32, this is guaranteed to contain just one alternative if
* $users contains a single user.
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
* All tables and joins are aliased, so `+` is safe to use.
*/
public function getWhere( IDatabase $db, $key, $users, $useId = true ) {
$this->checkDeprecation( $key );
$tables = [];
$conds = [];
$joins = [];
if ( $users instanceof UserIdentity ) {
$users = [ $users ];
} elseif ( $users === null || $users === false ) {
// DWIM
$users = [];
} elseif ( !is_array( $users ) ) {
$what = is_object( $users ) ? get_class( $users ) : gettype( $users );
throw new InvalidArgumentException(
__METHOD__ . ": Value for \$users must be a UserIdentity or array, got $what"
);
}
// Get information about all the passed users
$ids = [];
$names = [];
$actors = [];
foreach ( $users as $user ) {
if ( $useId && $user->getId() ) {
$ids[] = $user->getId();
} else {
// make sure to use normalized form of IP for anonymous users
$names[] = IPUtils::sanitizeIP( $user->getName() );
}
$actorId = $this->actorStoreFactory
->getActorNormalization( $db->getDomainID() )
->findActorId( $user, $db );
if ( $actorId ) {
$actors[] = $actorId;
}
}
list( $text, $actor ) = $this->getFieldNames( $key );
// Combine data into conditions to be ORed together
if ( $this->readStage === SCHEMA_COMPAT_READ_NEW ) {
if ( $actors ) {
$conds['newactor'] = $db->makeList( [ $actor => $actors ], IDatabase::LIST_AND );
}
} elseif ( $this->readStage === SCHEMA_COMPAT_READ_TEMP ) {
if ( $actors ) {
$tempTableInfo = $this->getTempTableInfo( $key );
if ( $tempTableInfo ) {
$alias = "temp_$key";
$tables[$alias] = $tempTableInfo['table'];
$joins[$alias] = [ 'JOIN',
"{$alias}.{$tempTableInfo['pk']} = {$tempTableInfo['joinPK']}" ];
$joinField = "{$alias}.{$tempTableInfo['field']}";
} else {
$joinField = $actor;
}
$conds['actor'] = $db->makeList( [ $joinField => $actors ], IDatabase::LIST_AND );
}
} else {
if ( $ids ) {
$conds['userid'] = $db->makeList( [ $key => $ids ], IDatabase::LIST_AND );
}
if ( $names ) {
$conds['username'] = $db->makeList( [ $text => $names ], IDatabase::LIST_AND );
}
}
return [
'tables' => $tables,
'conds' => $conds ? $db->makeList( array_values( $conds ), IDatabase::LIST_OR ) : '1=0',
'orconds' => $conds,
'joins' => $joins,
];
}
}

View File

@ -0,0 +1,186 @@
<?php
/**
* This defines autoloading handler for whole MediaWiki framework
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
// NO_AUTOLOAD -- file scope code, can't load self
/**
* Locations of core classes
* Extension classes are specified with $wgAutoloadClasses
*/
require_once __DIR__ . '/../autoload.php';
class AutoLoader {
protected static $autoloadLocalClassesLower = null;
/**
* @internal Only public for ExtensionRegistry
* @var string[] Namespace (ends with \) => Path (ends with /)
*/
public static $psr4Namespaces = [];
/**
* Find the file containing the given class.
*
* @param string $className Name of class we're looking for.
* @return string|null The path containing the class, not null if not found
*/
public static function find( $className ): ?string {
global $wgAutoloadLocalClasses, $wgAutoloadClasses, $wgAutoloadAttemptLowercase;
$filename = $wgAutoloadLocalClasses[$className] ?? $wgAutoloadClasses[$className] ?? false;
if ( !$filename && $wgAutoloadAttemptLowercase ) {
// Try a different capitalisation.
//
// PHP 4 objects are always serialized with the classname coerced to lowercase,
// and we are plagued with several legacy uses created by MediaWiki < 1.5, see
// https://wikitech.wikimedia.org/wiki/Text_storage_data
if ( self::$autoloadLocalClassesLower === null ) {
self::$autoloadLocalClassesLower = array_change_key_case( $wgAutoloadLocalClasses, CASE_LOWER );
}
$lowerClass = strtolower( $className );
if ( isset( self::$autoloadLocalClassesLower[$lowerClass] ) ) {
if ( function_exists( 'wfDebugLog' ) ) {
wfDebugLog( 'autoloader', "Class {$className} was loaded using incorrect case" );
}
$filename = self::$autoloadLocalClassesLower[$lowerClass];
}
}
if ( !$filename && strpos( $className, '\\' ) !== false ) {
// This class is namespaced, so look in the namespace map
$prefix = $className;
while ( ( $pos = strrpos( $prefix, '\\' ) ) !== false ) {
// Check to see if this namespace prefix is in the map
$prefix = substr( $className, 0, $pos + 1 );
if ( isset( self::$psr4Namespaces[$prefix] ) ) {
$relativeClass = substr( $className, $pos + 1 );
// Build the expected filename, and see if it exists
$file = self::$psr4Namespaces[$prefix] .
'/' .
strtr( $relativeClass, '\\', '/' ) .
'.php';
if ( is_file( $file ) ) {
$filename = $file;
break;
}
}
// Remove trailing separator for next iteration
$prefix = rtrim( $prefix, '\\' );
}
}
if ( !$filename ) {
// Class not found; let the next autoloader try to find it
return null;
}
// Make an absolute path, this improves performance by avoiding some stat calls
// Optimisation: use string offset access instead of substr
if ( $filename[0] !== '/' && $filename[1] !== ':' ) {
$filename = __DIR__ . '/../' . $filename;
}
return $filename;
}
/**
* autoload - take a class name and attempt to load it
*
* @param string $className Name of class we're looking for.
*/
public static function autoload( $className ) {
$filename = self::find( $className );
if ( $filename !== null ) {
require $filename;
}
}
/**
* Method to clear the protected class property $autoloadLocalClassesLower.
* Used in tests.
*/
public static function resetAutoloadLocalClassesLower() {
self::$autoloadLocalClassesLower = null;
}
/**
* Get a mapping of namespace => file path
* The namespaces should follow the PSR-4 standard for autoloading
*
* @see <https://www.php-fig.org/psr/psr-4/>
* @internal Only public for usage in AutoloadGenerator
* @codeCoverageIgnore
* @since 1.31
* @return string[]
*/
public static function getAutoloadNamespaces() {
return [
'MediaWiki\\' => __DIR__ . '/',
'MediaWiki\\Actions\\' => __DIR__ . '/actions/',
'MediaWiki\\Api\\' => __DIR__ . '/api/',
'MediaWiki\\Auth\\' => __DIR__ . '/auth/',
'MediaWiki\\Block\\' => __DIR__ . '/block/',
'MediaWiki\\Cache\\' => __DIR__ . '/cache/',
'MediaWiki\\ChangeTags\\' => __DIR__ . '/changetags/',
'MediaWiki\\Config\\' => __DIR__ . '/config/',
'MediaWiki\\Content\\' => __DIR__ . '/content/',
'MediaWiki\\DB\\' => __DIR__ . '/db/',
'MediaWiki\\Deferred\\LinksUpdate\\' => __DIR__ . '/deferred/LinksUpdate/',
'MediaWiki\\Diff\\' => __DIR__ . '/diff/',
'MediaWiki\\Edit\\' => __DIR__ . '/edit/',
'MediaWiki\\EditPage\\' => __DIR__ . '/editpage/',
'MediaWiki\\FileBackend\\LockManager\\' => __DIR__ . '/filebackend/lockmanager/',
'MediaWiki\\JobQueue\\' => __DIR__ . '/jobqueue/',
'MediaWiki\\Json\\' => __DIR__ . '/json/',
'MediaWiki\\Http\\' => __DIR__ . '/http/',
'MediaWiki\\Installer\\' => __DIR__ . '/installer/',
'MediaWiki\\Interwiki\\' => __DIR__ . '/interwiki/',
'MediaWiki\\Languages\\Data\\' => __DIR__ . '/languages/data/',
'MediaWiki\\Linker\\' => __DIR__ . '/linker/',
'MediaWiki\\Logger\\' => __DIR__ . '/debug/logger/',
'MediaWiki\\Logger\Monolog\\' => __DIR__ . '/debug/logger/monolog/',
'MediaWiki\\Mail\\' => __DIR__ . '/mail/',
'MediaWiki\\Page\\' => __DIR__ . '/page/',
'MediaWiki\\Preferences\\' => __DIR__ . '/preferences/',
'MediaWiki\\ResourceLoader\\' => __DIR__ . '/resourceloader/',
'MediaWiki\\Search\\' => __DIR__ . '/search/',
'MediaWiki\\Search\\SearchWidgets\\' => __DIR__ . '/search/searchwidgets/',
'MediaWiki\\Session\\' => __DIR__ . '/session/',
'MediaWiki\\Shell\\' => __DIR__ . '/shell/',
'MediaWiki\\Site\\' => __DIR__ . '/site/',
'MediaWiki\\Sparql\\' => __DIR__ . '/sparql/',
'MediaWiki\\SpecialPage\\' => __DIR__ . '/specialpage/',
'MediaWiki\\Tidy\\' => __DIR__ . '/tidy/',
'MediaWiki\\User\\' => __DIR__ . '/user/',
'MediaWiki\\Widget\\' => __DIR__ . '/widget/',
'Wikimedia\\' => __DIR__ . '/libs/',
'Wikimedia\\Http\\' => __DIR__ . '/libs/http/',
'Wikimedia\\UUID\\' => __DIR__ . '/libs/uuid/',
];
}
}
AutoLoader::$psr4Namespaces = AutoLoader::getAutoloadNamespaces();
spl_autoload_register( [ 'AutoLoader', 'autoload' ] );

View File

@ -0,0 +1,141 @@
<?php
namespace MediaWiki;
use BagOStuff;
use MalformedTitleException;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Linker\LinkTarget;
use RepoGroup;
use TitleParser;
class BadFileLookup {
/** @var callable Returns contents of bad file list (see comment for isBadFile()) */
private $listCallback;
/** @var BagOStuff Cache of parsed bad image list */
private $cache;
/** @var RepoGroup */
private $repoGroup;
/** @var TitleParser */
private $titleParser;
/** @var array<string,array<int,array<string,true>>>|null Parsed bad file list */
private $badFiles;
/** @var HookRunner */
private $hookRunner;
/**
* Do not call directly. Use MediaWikiServices.
*
* @param callable $listCallback Callback that returns wikitext of a bad file list
* @param BagOStuff $cache For caching parsed versions of the bad file list
* @param RepoGroup $repoGroup
* @param TitleParser $titleParser
* @param HookContainer $hookContainer
*/
public function __construct(
callable $listCallback,
BagOStuff $cache,
RepoGroup $repoGroup,
TitleParser $titleParser,
HookContainer $hookContainer
) {
$this->listCallback = $listCallback;
$this->cache = $cache;
$this->repoGroup = $repoGroup;
$this->titleParser = $titleParser;
$this->hookRunner = new HookRunner( $hookContainer );
}
/**
* Determine if a file exists on the 'bad image list'.
*
* The format of MediaWiki:Bad_image_list is as follows:
* * Only list items (lines starting with "*") are considered
* * The first link on a line must be a link to a bad file
* * Any subsequent links on the same line are considered to be exceptions,
* i.e. articles where the file may occur inline.
*
* @param string $name The file name to check
* @param LinkTarget|null $contextTitle The page on which the file occurs, if known
* @return bool
*/
public function isBadFile( $name, LinkTarget $contextTitle = null ) {
// Handle redirects; callers almost always hit RepoGroup::findFile() anyway,
// so just use that method because it has a fast process cache.
$file = $this->repoGroup->findFile( $name );
// XXX If we don't find the file we also don't replace spaces by underscores or otherwise
// validate or normalize the title, is this right?
if ( $file ) {
$name = $file->getTitle()->getDBkey();
}
// Run the extension hook
$bad = false;
if ( !$this->hookRunner->onBadImage( $name, $bad ) ) {
return (bool)$bad;
}
if ( $this->badFiles === null ) {
$list = ( $this->listCallback )();
$key = $this->cache->makeKey( 'bad-image-list', sha1( $list ) );
$this->badFiles = $this->cache->getWithSetCallback(
$key,
BagOStuff::TTL_DAY,
function () use ( $list ) {
return $this->buildBadFilesList( $list );
}
);
}
return isset( $this->badFiles[$name] ) && ( !$contextTitle ||
!isset( $this->badFiles[$name][$contextTitle->getNamespace()][$contextTitle->getDBkey()] ) );
}
/**
* @param string $list
* @return array<string,array<int,array<string,true>>>
*/
private function buildBadFilesList( string $list ): array {
$ret = [];
$lines = explode( "\n", $list );
foreach ( $lines as $line ) {
// List items only
if ( substr( $line, 0, 1 ) !== '*' ) {
continue;
}
// Find all links
$m = [];
// XXX What is the ':?' doing in the regex? Why not let the TitleParser strip it?
if ( !preg_match_all( '/\[\[:?(.*?)\]\]/', $line, $m ) ) {
continue;
}
$fileDBkey = null;
$exceptions = [];
foreach ( $m[1] as $i => $titleText ) {
try {
$title = $this->titleParser->parseTitle( $titleText );
} catch ( MalformedTitleException $e ) {
continue;
}
if ( $i == 0 ) {
$fileDBkey = $title->getDBkey();
} else {
$exceptions[$title->getNamespace()][$title->getDBkey()] = true;
}
}
if ( $fileDBkey !== null ) {
$ret[$fileDBkey] = $exceptions;
}
}
return $ret;
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Functions that need to be available during bootstrapping.
* Code in this file cannot expect MediaWiki to have been initialized.
* @file
*/
/**
* Decide and remember where to load LocalSettings from.
*
* This is used by Setup.php and will (if not already) store the result
* in the MW_CONFIG_FILE constant.
*
* The primary settings file is traditionally LocalSettings.php under the %MediaWiki
* installation path, but can also be placed differently and specified via the
* MW_CONFIG_FILE constant (from an entrypoint wrapper) or via a `MW_CONFIG_FILE`
* environment variable (from the web server).
*
* Experimental: The settings file can use the `.yaml` or `.json` extension, which
* must use the format described on
* https://www.mediawiki.org/wiki/Manual:YAML_settings_file_format.
*
* @internal Only for use by Setup.php and Installer.
* @since 1.38
* @param string $installationPath The installation's base path, typically global $IP.
* @return string The path to the settings file
*/
function wfDetectLocalSettingsFile( string $installationPath ): string {
if ( defined( 'MW_CONFIG_FILE' ) ) {
return MW_CONFIG_FILE;
}
// We could look for LocalSettings.yaml and LocalSettings.json,
// and use them if they exist. But having them in a web accessible
// place is dangerous, so better not to encourage that.
// In order to use LocalSettings.yaml and LocalSettings.json, they
// will have to be referenced explicitly by MW_CONFIG_FILE.
$configFile = getenv( 'MW_CONFIG_FILE' ) ?: "LocalSettings.php";
// Can't use str_contains because for maintenance scripts (update.php, install.php),
// this is called *before* Setup.php and vendor (polyfill-php80) are included.
if ( strpos( $configFile, '/' ) === false ) {
$configFile = "$installationPath/$configFile";
}
define( 'MW_CONFIG_FILE', $configFile );
return $configFile;
}

View File

@ -0,0 +1,132 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
*/
use Wikimedia\Purtle\RdfWriter;
/**
* Helper class to produce RDF representation of categories.
*/
class CategoriesRdf {
/**
* Prefix used for Mediawiki ontology in the dump.
*/
private const ONTOLOGY_PREFIX = 'mediawiki';
/**
* Base URL for Mediawiki ontology.
*/
private const ONTOLOGY_URL = 'https://www.mediawiki.org/ontology#';
/**
* OWL description of the ontology.
*/
public const OWL_URL = 'https://www.mediawiki.org/ontology/ontology.owl';
/**
* Current version of the dump format.
*/
public const FORMAT_VERSION = "1.1";
/**
* Special page for Dump identification.
* Used as head URI for each wiki's category dump, e.g.:
* https://en.wikipedia.org/wiki/Special:CategoryDump
*/
private const SPECIAL_DUMP = 'Special:CategoryDump';
/**
* @var RdfWriter
*/
private $rdfWriter;
public function __construct( RdfWriter $writer ) {
$this->rdfWriter = $writer;
}
/**
* Setup prefixes relevant for the dump
*/
public function setupPrefixes() {
$this->rdfWriter->prefix( self::ONTOLOGY_PREFIX, self::ONTOLOGY_URL );
$this->rdfWriter->prefix( 'rdfs', 'http://www.w3.org/2000/01/rdf-schema#' );
$this->rdfWriter->prefix( 'owl', 'http://www.w3.org/2002/07/owl#' );
$this->rdfWriter->prefix( 'schema', 'http://schema.org/' );
$this->rdfWriter->prefix( 'cc', 'http://creativecommons.org/ns#' );
}
/**
* Write RDF data for link between categories.
* @param string $fromName Child category name
* @param string $toName Parent category name
*/
public function writeCategoryLinkData( $fromName, $toName ) {
$titleFrom = Title::makeTitle( NS_CATEGORY, $fromName );
$titleTo = Title::makeTitle( NS_CATEGORY, $toName );
$this->rdfWriter->about( $this->titleToUrl( $titleFrom ) )
->say( self::ONTOLOGY_PREFIX, 'isInCategory' )
->is( $this->titleToUrl( $titleTo ) );
}
/**
* Write out the data for single category.
* @param string $categoryName
* @param bool $isHidden Hidden category?
* @param int $pages Page count (note this includes only Wiki articles, not subcats or files)
* @param int $subcategories Subcategory count
*/
public function writeCategoryData( $categoryName, $isHidden, $pages, $subcategories ) {
if ( $pages < 0 ) {
// Bugfix for T201119
$pages = 0;
}
$title = Title::makeTitle( NS_CATEGORY, $categoryName );
$this->rdfWriter->about( $this->titleToUrl( $title ) )
->say( 'a' )
->is( self::ONTOLOGY_PREFIX, 'Category' );
if ( $isHidden ) {
$this->rdfWriter->is( self::ONTOLOGY_PREFIX, 'HiddenCategory' );
}
$titletext = $title->getText();
$this->rdfWriter->say( 'rdfs', 'label' )->value( $titletext );
$this->rdfWriter->say( self::ONTOLOGY_PREFIX, 'pages' )->value( $pages );
$this->rdfWriter->say( self::ONTOLOGY_PREFIX, 'subcategories' )->value( $subcategories );
// TODO: do we want files too here? Easy to add, but don't have use case so far.
}
/**
* Make URL from title label
* @param string $titleLabel Short label (without namespace) of the category
* @return string URL for the category
*/
public function labelToUrl( $titleLabel ) {
return $this->titleToUrl( Title::makeTitle( NS_CATEGORY, $titleLabel ) );
}
/**
* Convert Title to link to target page.
* @param Title $title
* @return string URL for the category
*/
private function titleToUrl( Title $title ) {
return $title->getFullURL( '', false, PROTO_CANONICAL );
}
/**
* Get URI of the dump for this particular wiki.
* @return false|string
*/
public function getDumpURI() {
return $this->titleToUrl( Title::makeTitle( NS_MAIN, self::SPECIAL_DUMP ) );
}
}

View File

@ -0,0 +1,540 @@
<?php
/**
* Representation for a category.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @author Simetrical
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Page\PageIdentity;
use Wikimedia\Rdbms\ILoadBalancer;
/**
* Category objects are immutable, strictly speaking. If you call methods that change the database,
* like to refresh link counts, the objects will be appropriately reinitialized.
* Member variables are lazy-initialized.
*/
class Category {
/** Name of the category, normalized to DB-key form */
private $mName = null;
private $mID = null;
/**
* Category page title
* @var PageIdentity
*/
private $mPage = null;
/** Counts of membership (cat_pages, cat_subcats, cat_files) */
/** @var int */
private $mPages = 0;
/** @var int */
private $mSubcats = 0;
/** @var int */
private $mFiles = 0;
protected const LOAD_ONLY = 0;
protected const LAZY_INIT_ROW = 1;
public const ROW_COUNT_SMALL = 100;
public const COUNT_ALL_MEMBERS = 0;
public const COUNT_CONTENT_PAGES = 1;
/** @var ILoadBalancer */
private $loadBalancer;
/** @var ReadOnlyMode */
private $readOnlyMode;
private function __construct() {
$services = MediaWikiServices::getInstance();
$this->loadBalancer = $services->getDBLoadBalancer();
$this->readOnlyMode = $services->getReadOnlyMode();
}
/**
* Set up all member variables using a database query.
* @param int $mode One of (Category::LOAD_ONLY, Category::LAZY_INIT_ROW)
* @throws MWException
* @return bool True on success, false on failure.
*/
protected function initialize( $mode = self::LOAD_ONLY ) {
if ( $this->mName === null && $this->mID === null ) {
throw new MWException( __METHOD__ . ' has both names and IDs null' );
} elseif ( $this->mID === null ) {
$where = [ 'cat_title' => $this->mName ];
} elseif ( $this->mName === null ) {
$where = [ 'cat_id' => $this->mID ];
} else {
# Already initialized
return true;
}
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$row = $dbr->selectRow(
'category',
[ 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ],
$where,
__METHOD__
);
if ( !$row ) {
# Okay, there were no contents. Nothing to initialize.
if ( $this->mPage ) {
# If there is a page object but no record in the category table,
# treat this as an empty category.
$this->mID = false;
$this->mName = $this->mPage->getDBkey();
$this->mPages = 0;
$this->mSubcats = 0;
$this->mFiles = 0;
# If the page exists, call refreshCounts to add a row for it.
if ( $mode === self::LAZY_INIT_ROW && $this->mPage->exists() ) {
DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
}
return true;
} else {
return false; # Fail
}
}
$this->mID = $row->cat_id;
$this->mName = $row->cat_title;
$this->mPages = (int)$row->cat_pages;
$this->mSubcats = (int)$row->cat_subcats;
$this->mFiles = (int)$row->cat_files;
# (T15683) If the count is negative, then 1) it's obviously wrong
# and should not be kept, and 2) we *probably* don't have to scan many
# rows to obtain the correct figure, so let's risk a one-time recount.
if ( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) {
$this->mPages = max( $this->mPages, 0 );
$this->mSubcats = max( $this->mSubcats, 0 );
$this->mFiles = max( $this->mFiles, 0 );
if ( $mode === self::LAZY_INIT_ROW ) {
DeferredUpdates::addCallableUpdate( [ $this, 'refreshCounts' ] );
}
}
return true;
}
/**
* Factory function.
*
* @param string $name A category name (no "Category:" prefix). It need
* not be normalized, with spaces replaced by underscores.
* @return Category|bool Category, or false on a totally invalid name
*/
public static function newFromName( $name ) {
$cat = new self();
$title = Title::makeTitleSafe( NS_CATEGORY, $name );
if ( !is_object( $title ) ) {
return false;
}
$cat->mPage = $title;
$cat->mName = $title->getDBkey();
return $cat;
}
/**
* Factory function.
*
* @param PageIdentity $page Category page. Warning, no validation is performed!
* @return Category
*/
public static function newFromTitle( PageIdentity $page ): self {
$cat = new self();
$cat->mPage = $page;
$cat->mName = $page->getDBkey();
return $cat;
}
/**
* Factory function.
*
* @param int $id A category id. Warning, no validation is performed!
* @return Category
*/
public static function newFromID( $id ) {
$cat = new self();
$cat->mID = intval( $id );
return $cat;
}
/**
* Factory function, for constructing a Category object from a result set
*
* @param stdClass $row Result set row, must contain the cat_xxx fields. If the fields are
* null, the resulting Category object will represent an empty category if a page object was
* given. If the fields are null and no PageIdentity was given, this method fails and returns
* false.
* @param PageIdentity|null $page This must be provided if there is no cat_title field in $row.
* @return Category|false
*/
public static function newFromRow( stdClass $row, ?PageIdentity $page = null ) {
$cat = new self();
$cat->mPage = $page;
# NOTE: the row often results from a LEFT JOIN on categorylinks. This may result in
# all the cat_xxx fields being null, if the category page exists, but nothing
# was ever added to the category. This case should be treated link an empty
# category, if possible.
if ( $row->cat_title === null ) {
if ( $page === null ) {
# the name is probably somewhere in the row, for example as page_title,
# but we can't know that here...
return false;
} else {
# if we have a PageIdentity object, fetch the category name from there
$cat->mName = $page->getDBkey();
}
$cat->mID = false;
$cat->mSubcats = 0;
$cat->mPages = 0;
$cat->mFiles = 0;
} else {
$cat->mName = $row->cat_title;
$cat->mID = $row->cat_id;
$cat->mSubcats = (int)$row->cat_subcats;
$cat->mPages = (int)$row->cat_pages;
$cat->mFiles = (int)$row->cat_files;
}
return $cat;
}
/**
* @return string|false DB key name, or false on failure
*/
public function getName() {
return $this->getX( 'mName' );
}
/**
* @return string|false Category ID, or false on failure
*/
public function getID() {
return $this->getX( 'mID' );
}
/**
* @return int Total number of members count (sum of subcats, files and pages)
*/
public function getMemberCount(): int {
$this->initialize( self::LAZY_INIT_ROW );
return $this->mPages;
}
/**
* @param int $type One of self::COUNT_ALL_MEMBERS and self::COUNT_CONTENT_PAGES
* @return int Total number of member count or content page count
*/
public function getPageCount( $type = self::COUNT_ALL_MEMBERS ): int {
$allCount = $this->getMemberCount();
if ( $type === self::COUNT_CONTENT_PAGES ) {
return $allCount - ( $this->getSubcatCount() + $this->getFileCount() );
}
return $allCount;
}
/**
* @return int Number of subcategories
*/
public function getSubcatCount(): int {
return $this->getX( 'mSubcats' );
}
/**
* @return int Number of member files
*/
public function getFileCount(): int {
return $this->getX( 'mFiles' );
}
/**
* @since 1.37
* @return ?PageIdentity the page associated with this category, or null on failure. NOTE: This
* returns null on failure, unlike getTitle() which returns false.
*/
public function getPage(): ?PageIdentity {
if ( $this->mPage ) {
return $this->mPage;
}
if ( !$this->initialize( self::LAZY_INIT_ROW ) ) {
return null;
}
$this->mPage = Title::makeTitleSafe( NS_CATEGORY, $this->mName );
return $this->mPage;
}
/**
* @deprecated since 1.37, use getPage() instead.
* @return Title|bool Title for this category, or false on failure.
*/
public function getTitle() {
return Title::castFromPageIdentity( $this->getPage() ) ?? false;
}
/**
* Fetch a TitleArray of up to $limit category members, beginning after the
* category sort key $offset.
* @param int|bool $limit
* @param string $offset
* @return TitleArray TitleArray object for category members.
*/
public function getMembers( $limit = false, $offset = '' ) {
$dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
$conds = [ 'cl_to' => $this->getName(), 'cl_from = page_id' ];
$options = [ 'ORDER BY' => 'cl_sortkey' ];
if ( $limit ) {
$options['LIMIT'] = $limit;
}
if ( $offset !== '' ) {
$conds[] = 'cl_sortkey > ' . $dbr->addQuotes( $offset );
}
$result = TitleArray::newFromResult(
$dbr->select(
[ 'page', 'categorylinks' ],
[ 'page_id', 'page_namespace', 'page_title', 'page_len',
'page_is_redirect', 'page_latest' ],
$conds,
__METHOD__,
$options
)
);
return $result;
}
/**
* Generic accessor
* @param string $key
* @return mixed
*/
private function getX( $key ) {
$this->initialize( self::LAZY_INIT_ROW );
return $this->{$key} ?? false;
}
/**
* Refresh the counts for this category.
*
* @return bool True on success, false on failure
*/
public function refreshCounts() {
if ( $this->readOnlyMode->isReadOnly() ) {
return false;
}
# If we have just a category name, find out whether there is an
# existing row. Or if we have just an ID, get the name, because
# that's what categorylinks uses.
if ( !$this->initialize( self::LOAD_ONLY ) ) {
return false;
}
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
# Avoid excess contention on the same category (T162121)
$name = __METHOD__ . ':' . md5( $this->mName );
$scopedLock = $dbw->getScopedLockAndFlush( $name, __METHOD__, 0 );
if ( !$scopedLock ) {
return false;
}
$dbw->startAtomic( __METHOD__ );
// Lock the `category` row before locking `categorylinks` rows to try
// to avoid deadlocks with LinksDeletionUpdate (T195397)
$dbw->lockForUpdate( 'category', [ 'cat_title' => $this->mName ], __METHOD__ );
// Lock all the `categorylinks` records and gaps for this category;
// this is a separate query due to postgres limitations
$dbw->selectRowCount(
[ 'categorylinks', 'page' ],
'*',
[ 'cl_to' => $this->mName, 'page_id = cl_from' ],
__METHOD__,
[ 'LOCK IN SHARE MODE' ]
);
// Get the aggregate `categorylinks` row counts for this category
$catCond = $dbw->conditional( [ 'page_namespace' => NS_CATEGORY ], '1', 'NULL' );
$fileCond = $dbw->conditional( [ 'page_namespace' => NS_FILE ], '1', 'NULL' );
$result = $dbw->selectRow(
[ 'categorylinks', 'page' ],
[
'pages' => 'COUNT(*)',
'subcats' => "COUNT($catCond)",
'files' => "COUNT($fileCond)"
],
[ 'cl_to' => $this->mName, 'page_id = cl_from' ],
__METHOD__
);
$shouldExist = $result->pages > 0 || $this->getPage()->exists();
if ( $this->mID ) {
if ( $shouldExist ) {
# The category row already exists, so do a plain UPDATE instead
# of INSERT...ON DUPLICATE KEY UPDATE to avoid creating a gap
# in the cat_id sequence. The row may or may not be "affected".
$dbw->update(
'category',
[
'cat_pages' => $result->pages,
'cat_subcats' => $result->subcats,
'cat_files' => $result->files
],
[ 'cat_title' => $this->mName ],
__METHOD__
);
} else {
# The category is empty and has no description page, delete it
$dbw->delete(
'category',
[ 'cat_title' => $this->mName ],
__METHOD__
);
$this->mID = false;
}
} elseif ( $shouldExist ) {
# The category row doesn't exist but should, so create it. Use
# upsert in case of races.
$dbw->upsert(
'category',
[
'cat_title' => $this->mName,
'cat_pages' => $result->pages,
'cat_subcats' => $result->subcats,
'cat_files' => $result->files
],
'cat_title',
[
'cat_pages' => $result->pages,
'cat_subcats' => $result->subcats,
'cat_files' => $result->files
],
__METHOD__
);
// @todo: Should we update $this->mID here? Or not since Category
// objects tend to be short lived enough to not matter?
}
$dbw->endAtomic( __METHOD__ );
# Now we should update our local counts.
$this->mPages = (int)$result->pages;
$this->mSubcats = (int)$result->subcats;
$this->mFiles = (int)$result->files;
return true;
}
/**
* Call refreshCounts() if there are no entries in the categorylinks table
* or if the category table has a row that states that there are no entries
*
* Due to lock errors or other failures, the precomputed counts can get out of sync,
* making it hard to know when to delete the category row without checking the
* categorylinks table.
*
* @return bool Whether links were refreshed
* @since 1.32
*/
public function refreshCountsIfEmpty() {
return $this->refreshCountsIfSmall( 0 );
}
/**
* Call refreshCounts() if there are few entries in the categorylinks table
*
* Due to lock errors or other failures, the precomputed counts can get out of sync,
* making it hard to know when to delete the category row without checking the
* categorylinks table.
*
* This method will do a non-locking select first to reduce contention.
*
* @param int $maxSize Only refresh if there are this or less many backlinks
* @return bool Whether links were refreshed
* @since 1.34
*/
public function refreshCountsIfSmall( $maxSize = self::ROW_COUNT_SMALL ) {
$dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
$dbw->startAtomic( __METHOD__ );
$typeOccurances = $dbw->selectFieldValues(
'categorylinks',
'cl_type',
[ 'cl_to' => $this->getName() ],
__METHOD__,
[ 'LIMIT' => $maxSize + 1 ]
);
if ( !$typeOccurances ) {
$doRefresh = true; // delete any category table entry
} elseif ( count( $typeOccurances ) <= $maxSize ) {
$countByType = array_count_values( $typeOccurances );
$doRefresh = !$dbw->selectField(
'category',
'1',
[
'cat_title' => $this->getName(),
'cat_pages' => $countByType['page'] ?? 0,
'cat_subcats' => $countByType['subcat'] ?? 0,
'cat_files' => $countByType['file'] ?? 0
],
__METHOD__
);
} else {
$doRefresh = false; // category is too big
}
$dbw->endAtomic( __METHOD__ );
if ( $doRefresh ) {
$this->refreshCounts(); // update the row
return true;
}
return false;
}
}

View File

@ -0,0 +1,801 @@
<?php
/**
* List and paging of category members.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
use MediaWiki\Page\PageIdentity;
use MediaWiki\Page\PageReference;
class CategoryViewer extends ContextSource {
use ProtectedHookAccessorTrait;
use DeprecationHelper;
/** @var int */
public $limit;
/** @var array */
public $from;
/** @var array */
public $until;
/** @var string[] */
public $articles;
/** @var array */
public $articles_start_char;
/** @var array */
public $children;
/** @var array */
public $children_start_char;
/** @var bool */
public $showGallery;
/** @var array */
public $imgsNoGallery_start_char;
/** @var array */
public $imgsNoGallery;
/** @var array */
public $nextPage;
/** @var array */
protected $prevPage;
/** @var array */
public $flip;
/** @var PageIdentity */
protected $page;
/** @var Collation */
public $collation;
/** @var ImageGalleryBase */
public $gallery;
/** @var Category Category object for this page. */
private $cat;
/** @var array The original query array, to be used in generating paging links. */
private $query;
/** @var ILanguageConverter */
private $languageConverter;
/**
* @since 1.19 $context is a second, required parameter
* @param PageIdentity $page
* @param IContextSource $context
* @param array $from An array with keys page, subcat,
* and file for offset of results of each section (since 1.17)
* @param array $until An array with 3 keys for until of each section (since 1.17)
* @param array $query
*/
public function __construct( PageIdentity $page, IContextSource $context, array $from = [],
array $until = [], array $query = []
) {
$this->page = $page;
$this->deprecatePublicPropertyFallback(
'title',
'1.37',
function (): Title {
return Title::castFromPageIdentity( $this->page );
},
function ( PageIdentity $page ) {
$this->page = $page;
}
);
$this->setContext( $context );
$this->getOutput()->addModuleStyles( [
'mediawiki.action.view.categoryPage.styles'
] );
$this->from = $from;
$this->until = $until;
$this->limit = $context->getConfig()->get( 'CategoryPagingLimit' );
$this->cat = Category::newFromTitle( $page );
$this->query = $query;
$this->collation = MediaWikiServices::getInstance()->getCollationFactory()->getCategoryCollation();
$this->languageConverter = MediaWikiServices::getInstance()
->getLanguageConverterFactory()->getLanguageConverter();
unset( $this->query['title'] );
}
/**
* Format the category data list.
*
* @return string HTML output
*/
public function getHTML() {
$this->showGallery = $this->getConfig()->get( 'CategoryMagicGallery' )
&& !$this->getOutput()->mNoGallery;
$this->clearCategoryState();
$this->doCategoryQuery();
$this->finaliseCategoryState();
$r = $this->getSubcategorySection() .
$this->getPagesSection() .
$this->getImageSection();
if ( $r == '' ) {
// If there is no category content to display, only
// show the top part of the navigation links.
// @todo FIXME: Cannot be completely suppressed because it
// is unknown if 'until' or 'from' makes this
// give 0 results.
$r = $this->getCategoryTop();
} else {
$r = $this->getCategoryTop() .
$r .
$this->getCategoryBottom();
}
// Give a proper message if category is empty
if ( $r == '' ) {
$r = $this->msg( 'category-empty' )->parseAsBlock();
}
$lang = $this->getLanguage();
$attribs = [
'class' => 'mw-category-generated',
'lang' => $lang->getHtmlCode(),
'dir' => $lang->getDir()
];
# put a div around the headings which are in the user language
$r = Html::rawElement( 'div', $attribs, $r );
return $r;
}
protected function clearCategoryState() {
$this->articles = [];
$this->articles_start_char = [];
$this->children = [];
$this->children_start_char = [];
if ( $this->showGallery ) {
// Note that null for mode is taken to mean use default.
$mode = $this->getRequest()->getVal( 'gallerymode', null );
try {
$this->gallery = ImageGalleryBase::factory( $mode, $this->getContext() );
} catch ( ImageGalleryClassNotFoundException $e ) {
// User specified something invalid, fallback to default.
$this->gallery = ImageGalleryBase::factory( false, $this->getContext() );
}
$this->gallery->setHideBadImages();
} else {
$this->imgsNoGallery = [];
$this->imgsNoGallery_start_char = [];
}
}
/**
* Add a subcategory to the internal lists, using a Category object
* @param Category $cat
* @param string $sortkey
* @param int $pageLength
*/
public function addSubcategoryObject( Category $cat, $sortkey, $pageLength ) {
$page = $cat->getPage();
if ( !$page ) {
return;
}
// Subcategory; strip the 'Category' namespace from the link text.
$pageRecord = MediaWikiServices::getInstance()->getPageStore()
->getPageByReference( $page );
if ( !$pageRecord ) {
return;
}
$this->children[] = $this->generateLink(
'subcat',
$pageRecord,
$pageRecord->isRedirect(),
htmlspecialchars( str_replace( '_', ' ', $pageRecord->getDBkey() ) )
);
$this->children_start_char[] =
$this->getSubcategorySortChar( $page, $sortkey );
}
/**
* @param string $type
* @param PageReference $page
* @param bool $isRedirect
* @param string|null $html
* @return string
* Annotations needed to tell taint about HtmlArmor,
* due to the use of the hook it is not possible to avoid raw html handling here
* @param-taint $html tainted
* @return-taint escaped
*/
private function generateLink(
string $type, PageReference $page, bool $isRedirect, ?string $html = null
): string {
$link = null;
$legacyTitle = MediaWikiServices::getInstance()->getTitleFactory()
->castFromPageReference( $page );
$this->getHookRunner()->onCategoryViewer__generateLink( $type, $legacyTitle, $html, $link );
if ( $link === null ) {
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
if ( $html !== null ) {
$html = new HtmlArmor( $html );
}
$link = $linkRenderer->makeLink( $page, $html );
}
if ( $isRedirect ) {
$link = Html::rawElement(
'span',
[ 'class' => 'redirect-in-category' ],
$link
);
}
return $link;
}
/**
* Get the character to be used for sorting subcategories.
* If there's a link from Category:A to Category:B, the sortkey of the resulting
* entry in the categorylinks table is Category:A, not A, which it SHOULD be.
* Workaround: If sortkey == "Category:".$title, than use $title for sorting,
* else use sortkey...
*
* @param PageIdentity $page
* @param string $sortkey The human-readable sortkey (before transforming to icu or whatever).
* @return string
*/
public function getSubcategorySortChar( PageIdentity $page, string $sortkey ): string {
$titleText = MediaWikiServices::getInstance()->getTitleFormatter()
->getPrefixedText( $page );
if ( $titleText === $sortkey ) {
$word = $page->getDBkey();
} else {
$word = $sortkey;
}
$firstChar = $this->collation->getFirstLetter( $word );
return $this->languageConverter->convert( $firstChar );
}
/**
* Add a page in the image namespace
* @param PageReference $page
* @param string $sortkey
* @param int $pageLength
* @param bool $isRedirect
*/
public function addImage(
PageReference $page, string $sortkey, int $pageLength, bool $isRedirect = false
): void {
$title = MediaWikiServices::getInstance()->getTitleFactory()
->castFromPageReference( $page );
if ( $this->showGallery ) {
$flip = $this->flip['file'];
if ( $flip ) {
$this->gallery->insert( $title );
} else {
$this->gallery->add( $title );
}
} else {
$this->imgsNoGallery[] = $this->generateLink( 'image', $page, $isRedirect );
$this->imgsNoGallery_start_char[] =
$this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
}
}
/**
* Add a miscellaneous page
* @param PageReference $page
* @param string $sortkey
* @param int $pageLength
* @param bool $isRedirect
*/
public function addPage(
PageReference $page, string $sortkey, int $pageLength, bool $isRedirect = false
): void {
$this->articles[] = $this->generateLink( 'page', $page, $isRedirect );
$this->articles_start_char[] =
$this->languageConverter->convert( $this->collation->getFirstLetter( $sortkey ) );
}
protected function finaliseCategoryState() {
if ( $this->flip['subcat'] ) {
$this->children = array_reverse( $this->children );
$this->children_start_char = array_reverse( $this->children_start_char );
}
if ( $this->flip['page'] ) {
$this->articles = array_reverse( $this->articles );
$this->articles_start_char = array_reverse( $this->articles_start_char );
}
if ( !$this->showGallery && $this->flip['file'] ) {
$this->imgsNoGallery = array_reverse( $this->imgsNoGallery );
$this->imgsNoGallery_start_char = array_reverse( $this->imgsNoGallery_start_char );
}
}
protected function doCategoryQuery() {
$dbr = wfGetDB( DB_REPLICA, 'category' );
$this->nextPage = [
'page' => null,
'subcat' => null,
'file' => null,
];
$this->prevPage = [
'page' => null,
'subcat' => null,
'file' => null,
];
$this->flip = [ 'page' => false, 'subcat' => false, 'file' => false ];
foreach ( [ 'page', 'subcat', 'file' ] as $type ) {
# Get the sortkeys for start/end, if applicable. Note that if
# the collation in the database differs from the one
# set in $wgCategoryCollation, pagination might go totally haywire.
$extraConds = [ 'cl_type' => $type ];
if ( isset( $this->from[$type] ) ) {
$extraConds[] = 'cl_sortkey >= '
. $dbr->addQuotes( $this->collation->getSortKey( $this->from[$type] ) );
} elseif ( isset( $this->until[$type] ) ) {
$extraConds[] = 'cl_sortkey < '
. $dbr->addQuotes( $this->collation->getSortKey( $this->until[$type] ) );
$this->flip[$type] = true;
}
$res = $dbr->select(
[ 'page', 'categorylinks', 'category' ],
array_merge(
LinkCache::getSelectFields(),
[
'page_namespace',
'page_title',
'cl_sortkey',
'cat_id',
'cat_title',
'cat_subcats',
'cat_pages',
'cat_files',
'cl_sortkey_prefix',
'cl_collation'
]
),
array_merge( [ 'cl_to' => $this->page->getDBkey() ], $extraConds ),
__METHOD__,
[
'USE INDEX' => [ 'categorylinks' => 'cl_sortkey' ],
'LIMIT' => $this->limit + 1,
'ORDER BY' => $this->flip[$type] ? 'cl_sortkey DESC' : 'cl_sortkey',
],
[
'categorylinks' => [ 'JOIN', 'cl_from = page_id' ],
'category' => [ 'LEFT JOIN', [
'cat_title = page_title',
'page_namespace' => NS_CATEGORY
] ]
]
);
$this->getHookRunner()->onCategoryViewer__doCategoryQuery( $type, $res );
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$count = 0;
foreach ( $res as $row ) {
$title = Title::newFromRow( $row );
$linkCache->addGoodLinkObjFromRow( $title, $row );
if ( $row->cl_collation === '' ) {
// Hack to make sure that while updating from 1.16 schema
// and db is inconsistent, that the sky doesn't fall.
// See r83544. Could perhaps be removed in a couple decades...
$humanSortkey = $row->cl_sortkey;
} else {
$humanSortkey = $title->getCategorySortkey( $row->cl_sortkey_prefix );
}
if ( ++$count > $this->limit ) {
# We've reached the one extra which shows that there
# are additional pages to be had. Stop here...
$this->nextPage[$type] = $humanSortkey;
break;
}
if ( $count == $this->limit ) {
$this->prevPage[$type] = $humanSortkey;
}
if ( $title->getNamespace() === NS_CATEGORY ) {
$cat = Category::newFromRow( $row, $title );
$this->addSubcategoryObject( $cat, $humanSortkey, $row->page_len );
} elseif ( $title->getNamespace() === NS_FILE ) {
$this->addImage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
} else {
$this->addPage( $title, $humanSortkey, $row->page_len, $row->page_is_redirect );
}
}
}
}
/**
* @return string
*/
protected function getCategoryTop() {
$r = $this->getCategoryBottom();
return $r === ''
? $r
: "<br style=\"clear:both;\"/>\n" . $r;
}
/**
* @return string
*/
protected function getSubcategorySection() {
# Don't show subcategories section if there are none.
$r = '';
$rescnt = count( $this->children );
$dbcnt = $this->cat->getSubcatCount();
// This function should be called even if the result isn't used, it has side-effects
$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'subcat' );
if ( $rescnt > 0 ) {
# Showing subcategories
$r .= Html::openElement( 'div', [ 'id' => 'mw-subcategories' ] ) . "\n";
$r .= Html::rawElement( 'h2', [], $this->msg( 'subcategories' )->parse() ) . "\n";
$r .= $countmsg;
$r .= $this->getSectionPagingLinks( 'subcat' );
$r .= $this->formatList( $this->children, $this->children_start_char );
$r .= $this->getSectionPagingLinks( 'subcat' );
$r .= "\n" . Html::closeElement( 'div' );
}
return $r;
}
/**
* @return string
*/
protected function getPagesSection() {
$name = $this->getOutput()->getUnprefixedDisplayTitle();
# Don't show articles section if there are none.
$r = '';
# @todo FIXME: Here and in the other two sections: we don't need to bother
# with this rigmarole if the entire category contents fit on one page
# and have already been retrieved. We can just use $rescnt in that
# case and save a query and some logic.
$dbcnt = $this->cat->getPageCount( Category::COUNT_CONTENT_PAGES );
$rescnt = count( $this->articles );
// This function should be called even if the result isn't used, it has side-effects
$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'article' );
if ( $rescnt > 0 ) {
$r .= Html::openElement( 'div', [ 'id' => 'mw-pages' ] ) . "\n";
$r .= Html::rawElement(
'h2',
[],
$this->msg( 'category_header' )->rawParams( $name )->parse()
) . "\n";
$r .= $countmsg;
$r .= $this->getSectionPagingLinks( 'page' );
$r .= $this->formatList( $this->articles, $this->articles_start_char );
$r .= $this->getSectionPagingLinks( 'page' );
$r .= "\n" . Html::closeElement( 'div' );
}
return $r;
}
/**
* @return string
*/
protected function getImageSection() {
$name = $this->getOutput()->getUnprefixedDisplayTitle();
$r = '';
$rescnt = $this->showGallery ? $this->gallery->count() : count( $this->imgsNoGallery );
$dbcnt = $this->cat->getFileCount();
// This function should be called even if the result isn't used, it has side-effects
$countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' );
if ( $rescnt > 0 ) {
$r .= Html::openElement( 'div', [ 'id' => 'mw-category-media' ] ) . "\n";
$r .= Html::rawElement(
'h2',
[],
$this->msg( 'category-media-header' )->rawParams( $name )->parse()
) . "\n";
$r .= $countmsg;
$r .= $this->getSectionPagingLinks( 'file' );
if ( $this->showGallery ) {
$r .= $this->gallery->toHTML();
} else {
$r .= $this->formatList( $this->imgsNoGallery, $this->imgsNoGallery_start_char );
}
$r .= $this->getSectionPagingLinks( 'file' );
$r .= "\n" . Html::closeElement( 'div' );
}
return $r;
}
/**
* Get the paging links for a section (subcats/pages/files), to go at the top and bottom
* of the output.
*
* @param string $type 'page', 'subcat', or 'file'
* @return string HTML output, possibly empty if there are no other pages
*/
private function getSectionPagingLinks( $type ) {
if ( isset( $this->until[$type] ) ) {
// The new value for the until parameter should be pointing to the first
// result displayed on the page which is the second last result retrieved
// from the database.The next link should have a from parameter pointing
// to the until parameter of the current page.
if ( $this->nextPage[$type] !== null ) {
return $this->pagingLinks( $this->prevPage[$type], $this->until[$type], $type );
} else {
// If the nextPage variable is null, it means that we have reached the first page
// and therefore the previous link should be disabled.
return $this->pagingLinks( '', $this->until[$type], $type );
}
} elseif ( $this->nextPage[$type] !== null || isset( $this->from[$type] ) ) {
return $this->pagingLinks( $this->from[$type], $this->nextPage[$type], $type );
} else {
return '';
}
}
/**
* @return string
*/
protected function getCategoryBottom() {
return '';
}
/**
* Format a list of articles chunked by letter, either as a
* bullet list or a columnar format, depending on the length.
*
* @param array $articles
* @param array $articles_start_char
* @param int $cutoff
* @return string
* @internal
*/
private function formatList( $articles, $articles_start_char, $cutoff = 6 ) {
$list = '';
if ( count( $articles ) > $cutoff ) {
$list = self::columnList( $articles, $articles_start_char );
} elseif ( count( $articles ) > 0 ) {
// for short lists of articles in categories.
$list = self::shortList( $articles, $articles_start_char );
}
$pageLang = MediaWikiServices::getInstance()->getTitleFactory()
->castFromPageIdentity( $this->page )
->getPageLanguage();
$attribs = [ 'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
'class' => 'mw-content-' . $pageLang->getDir() ];
$list = Html::rawElement( 'div', $attribs, $list );
return $list;
}
/**
* Format a list of articles chunked by letter in a three-column list, ordered
* vertically. This is used for categories with a significant number of pages.
*
* @param string[] $articles HTML links to each article
* @param string[] $articles_start_char The header characters for each article
* @param string $cssClasses CSS classes for the wrapper element
* @return string HTML to output
* @internal
*/
public static function columnList(
$articles,
$articles_start_char,
$cssClasses = 'mw-category mw-category-columns'
) {
$columns = array_combine( $articles, $articles_start_char );
$ret = Html::openElement( 'div', [ 'class' => $cssClasses ] );
$colContents = [];
# Kind of like array_flip() here, but we keep duplicates in an
# array instead of dropping them.
foreach ( $columns as $article => $char ) {
if ( !isset( $colContents[$char] ) ) {
$colContents[$char] = [];
}
$colContents[$char][] = $article;
}
foreach ( $colContents as $char => $articles ) {
# Change space to non-breaking space to keep headers aligned
$h3char = $char === ' ' ? "\u{00A0}" : htmlspecialchars( $char );
$ret .= Html::openElement( 'div', [ 'class' => 'mw-category-group' ] );
$ret .= Html::rawElement( 'h3', [], $h3char ) . "\n";
$ret .= Html::openElement( 'ul' );
$ret .= implode(
"\n",
array_map(
static function ( $article ) {
return Html::rawElement( 'li', [], $article );
},
$articles
)
);
$ret .= Html::closeElement( 'ul' ) . Html::closeElement( 'div' );
}
$ret .= Html::closeElement( 'div' );
return $ret;
}
/**
* Format a list of articles chunked by letter in a bullet list. This is used
* for categories with a small number of pages (when columns aren't needed).
* @param string[] $articles HTML links to each article
* @param string[] $articles_start_char The header characters for each article
* @return string HTML to output
* @internal
*/
public static function shortList( $articles, $articles_start_char ) {
return self::columnList( $articles, $articles_start_char, 'mw-category' );
}
/**
* Create paging links, as a helper method to getSectionPagingLinks().
*
* @param string $first The 'until' parameter for the generated URL
* @param string $last The 'from' parameter for the generated URL
* @param string $type A prefix for parameters, 'page' or 'subcat' or
* 'file'
* @return string HTML
*/
private function pagingLinks( $first, $last, $type = '' ) {
$prevLink = $this->msg( 'prev-page' )->escaped();
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
if ( $first != '' ) {
$prevQuery = $this->query;
$prevQuery["{$type}until"] = $first;
unset( $prevQuery["{$type}from"] );
$prevLink = $linkRenderer->makeKnownLink(
$this->addFragmentToTitle( $this->page, $type ),
new HtmlArmor( $prevLink ),
[],
$prevQuery
);
}
$nextLink = $this->msg( 'next-page' )->escaped();
if ( $last != '' ) {
$lastQuery = $this->query;
$lastQuery["{$type}from"] = $last;
unset( $lastQuery["{$type}until"] );
$nextLink = $linkRenderer->makeKnownLink(
$this->addFragmentToTitle( $this->page, $type ),
new HtmlArmor( $nextLink ),
[],
$lastQuery
);
}
return $this->msg( 'categoryviewer-pagedlinks' )->rawParams( $prevLink, $nextLink )->escaped();
}
/**
* Takes a title, and adds the fragment identifier that
* corresponds to the correct segment of the category.
*
* @param PageReference $page The title (usually $this->title)
* @param string $section Which section
* @throws MWException
* @return LinkTarget
*/
private function addFragmentToTitle( PageReference $page, string $section ): LinkTarget {
switch ( $section ) {
case 'page':
$fragment = 'mw-pages';
break;
case 'subcat':
$fragment = 'mw-subcategories';
break;
case 'file':
$fragment = 'mw-category-media';
break;
default:
throw new MWException( __METHOD__ .
" Invalid section $section." );
}
return new TitleValue( $page->getNamespace(),
$page->getDBkey(), $fragment );
}
/**
* What to do if the category table conflicts with the number of results
* returned? This function says what. Each type is considered independently
* of the other types.
*
* @param int $rescnt The number of items returned by our database query.
* @param int $dbcnt The number of items according to the category table.
* @param string $type 'subcat', 'article', or 'file'
* @return string A message giving the number of items, to output to HTML.
*/
private function getCountMessage( $rescnt, $dbcnt, $type ) {
// There are three cases:
// 1) The category table figure seems good. It might be wrong, but
// we can't do anything about it if we don't recalculate it on ev-
// ery category view.
// 2) The category table figure isn't good, like it's smaller than the
// number of actual results, *but* the number of results is less
// than $this->limit and there's no offset. In this case we still
// know the right figure.
// 3) We have no idea.
// Check if there's a "from" or "until" for anything
// This is a little ugly, but we seem to use different names
// for the paging types then for the messages.
if ( $type === 'article' ) {
$pagingType = 'page';
} else {
$pagingType = $type;
}
$fromOrUntil = false;
if ( isset( $this->from[$pagingType] ) || isset( $this->until[$pagingType] ) ) {
$fromOrUntil = true;
}
if ( $dbcnt == $rescnt ||
( ( $rescnt == $this->limit || $fromOrUntil ) && $dbcnt > $rescnt )
) {
// Case 1: seems good.
$totalcnt = $dbcnt;
} elseif ( $rescnt < $this->limit && !$fromOrUntil ) {
// Case 2: not good, but salvageable. Use the number of results.
$totalcnt = $rescnt;
} else {
// Case 3: hopeless. Don't give a total count at all.
// Messages: category-subcat-count-limited, category-article-count-limited,
// category-file-count-limited
return $this->msg( "category-$type-count-limited" )->numParams( $rescnt )->parseAsBlock();
}
// Messages: category-subcat-count, category-article-count, category-file-count
return $this->msg( "category-$type-count" )->numParams( $rescnt, $totalcnt )->parseAsBlock();
}
}

View File

@ -0,0 +1,194 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Linker\LinkTarget;
use Traversable;
/**
* This class provides a fluent interface for formatting a batch of comments.
*
* @since 1.38
*/
class CommentBatch {
/** @var CommentFormatter */
private $formatter;
/** @var iterable<CommentItem>|Traversable */
private $comments;
/** @var bool|null */
private $useBlock;
/** @var bool|null */
private $useParentheses;
/** @var LinkTarget|null */
private $selfLinkTarget;
/** @var bool|null */
private $samePage;
/** @var string|false|null */
private $wikiId;
/** @var bool|null */
private $enableSectionLinks;
/**
* @internal Use CommentFormatter::createBatch()
*
* @param CommentFormatter $formatter
*/
public function __construct( CommentFormatter $formatter ) {
$this->formatter = $formatter;
}
/**
* Set the comments to be formatted. This can be an array of CommentItem
* objects, or it can be an iterator which generates CommentItem objects.
*
* Theoretically iterable should imply Traversable, but PHPStorm gives an
* error when RowCommentIterator is passed as iterable<CommentItem>.
*
* @param iterable<CommentItem>|Traversable $comments
* @return $this
*/
public function comments( $comments ) {
$this->comments = $comments;
return $this;
}
/**
* Specify the comments to be formatted as an array of strings. This is a
* simplified wrapper for comments() which does not allow you to set options
* on a per-comment basis.
*
* $strings must be an array -- use comments() if you want to use an iterator.
*
* @param string[] $strings
* @return $this
*/
public function strings( array $strings ) {
$this->comments = new StringCommentIterator( $strings );
return $this;
}
/**
* Wrap each comment in standard punctuation and formatting if it's
* non-empty. Empty comments remain empty. This causes the batch to work
* like the old Linker::commentBlock().
*
* If this function is not called, the option is false.
*
* @param bool $useBlock
* @return $this
*/
public function useBlock( $useBlock = true ) {
$this->useBlock = $useBlock;
return $this;
}
/**
* Wrap each comment with parentheses. This has no effect if the useBlock
* option is not enabled.
*
* Unlike the legacy Linker::commentBlock(), this option defaults to false
* if this method is not called, since that better preserves the fluent
* style.
*
* @param bool $useParentheses
* @return $this
*/
public function useParentheses( $useParentheses = true ) {
$this->useParentheses = $useParentheses;
return $this;
}
/**
* Set the title to be used for self links in the comments. If there is no
* title specified either here or in the item, fragment links are not
* expanded.
*
* @param LinkTarget $selfLinkTarget
* @return $this
*/
public function selfLinkTarget( LinkTarget $selfLinkTarget ) {
$this->selfLinkTarget = $selfLinkTarget;
return $this;
}
/**
* Set the option to enable/disable section links formatted as C-style
* comments, as used in revision comments to indicate the section which
* was edited.
*
* If the method is not called, the option is true. Setting this to false
* approximately emulates Linker::formatLinksInComment() except that HTML
* in the input is escaped.
*
* @param bool $enable
* @return $this
*/
public function enableSectionLinks( $enable ) {
$this->enableSectionLinks = $enable;
return $this;
}
/**
* Disable section links formatted as C-style comments, as used in revision
* comments to indicate the section which was edited. Calling this
* approximately emulates Linker::formatLinksInComment() except that HTML
* in the input is escaped.
*
* @return $this
*/
public function disableSectionLinks() {
$this->enableSectionLinks = false;
return $this;
}
/**
* Set the same-page option. If this is true, section links and fragment-
* only wikilinks are rendered with an href that is a fragment-only URL.
* If it is false (the default), such links go to the self link title.
*
* This can also be set per-item using CommentItem::samePage().
*
* This is equivalent to $local in the old Linker methods.
*
* @param bool $samePage
* @return $this
*/
public function samePage( $samePage = true ) {
$this->samePage = $samePage;
return $this;
}
/**
* ID of the wiki to link to (if not the local wiki), as used by WikiMap.
* This is used to render comments which are loaded from a foreign wiki.
* This only affects links which are syntactically internal -- it has no
* effect on interwiki links.
*
* This can also be set per-item using CommentItem::wikiId().
*
* @param string|false|null $wikiId
* @return $this
*/
public function wikiId( $wikiId ) {
$this->wikiId = $wikiId;
return $this;
}
/**
* Format the comments and produce an array of HTML fragments.
*
* @return string[]
*/
public function execute() {
return $this->formatter->formatItemsInternal(
$this->comments,
$this->selfLinkTarget,
$this->samePage,
$this->wikiId,
$this->enableSectionLinks,
$this->useBlock,
$this->useParentheses
);
}
}

View File

@ -0,0 +1,408 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Permissions\Authority;
use MediaWiki\Revision\RevisionRecord;
use Traversable;
/**
* This is the main service interface for converting single-line comments from
* various DB comment fields into HTML.
*
* @since 1.38
*/
class CommentFormatter {
/** @var CommentParserFactory */
protected $parserFactory;
/**
* @internal Use MediaWikiServices::getCommentFormatter()
*
* @param CommentParserFactory $parserFactory
*/
public function __construct( CommentParserFactory $parserFactory ) {
$this->parserFactory = $parserFactory;
}
/**
* Format comments using a fluent interface.
*
* @return CommentBatch
*/
public function createBatch() {
return new CommentBatch( $this );
}
/**
* Format a single comment. Similar to the old Linker::formatComment().
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string
*/
public function format( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
return $this->formatInternal( $comment, true, false, false,
$selfLinkTarget, $samePage, $wikiId );
}
/**
* Wrap a comment in standard punctuation and formatting if
* it's non-empty, otherwise return an empty string.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @param bool $useParentheses
* @return string
*/
public function formatBlock( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $useParentheses = true
) {
return $this->formatInternal( $comment, true, true, $useParentheses,
$selfLinkTarget, $samePage, $wikiId );
}
/**
* Format a comment, passing through HTML in the input to the output.
* This is unsafe and exists only for backwards compatibility with
* Linker::formatLinksInComment().
*
* In new code, use formatLinks() or createBatch()->disableSectionLinks().
*
* @internal
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string
*/
public function formatLinksUnsafe( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
$parser = $this->parserFactory->create();
$preprocessed = $parser->preprocessUnsafe( $comment, $selfLinkTarget,
$samePage, $wikiId, false );
return $parser->finalize( $preprocessed );
}
/**
* Format links in a comment, ignoring section links in C-style comments.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string
*/
public function formatLinks( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
return $this->formatInternal( $comment, false, false, false,
$selfLinkTarget, $samePage, $wikiId );
}
/**
* Format a single comment with many ugly boolean parameters.
*
* @param string $comment
* @param bool $enableSectionLinks
* @param bool $useBlock
* @param bool $useParentheses
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string|string[]
*/
private function formatInternal( $comment, $enableSectionLinks, $useBlock, $useParentheses,
$selfLinkTarget = null, $samePage = false, $wikiId = false
) {
$parser = $this->parserFactory->create();
$preprocessed = $parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId,
$enableSectionLinks );
$output = $parser->finalize( $preprocessed );
if ( $useBlock ) {
$output = $this->wrapCommentWithBlock( $output, $useParentheses );
}
return $output;
}
/**
* Format comments which are provided as strings and all have the same
* self-link target and other options.
*
* If you need a different title for each comment, use createBatch().
*
* @param string[] $strings
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @return string[]
*/
public function formatStrings( $strings, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false
) {
$parser = $this->parserFactory->create();
$outputs = [];
foreach ( $strings as $i => $comment ) {
$outputs[$i] = $parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId );
}
return $parser->finalize( $outputs );
}
/**
* Given an array of comments as strings which all have the same self link
* target, format the comments and wrap them in standard punctuation and
* formatting.
*
* If you need a different title for each comment, use createBatch().
*
* @param string[] $strings
* @param LinkTarget|null $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param string|false|null $wikiId ID of the wiki to link to (if not the local
* wiki), as used by WikiMap.
* @param bool $useParentheses
* @return string[]
*/
public function formatStringsAsBlock( $strings, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $useParentheses = true
) {
$parser = $this->parserFactory->create();
$outputs = [];
foreach ( $strings as $i => $comment ) {
$outputs[$i] = $this->wrapCommentWithBlock(
$parser->preprocess( $comment, $selfLinkTarget, $samePage, $wikiId ),
$useParentheses );
}
return $parser->finalize( $outputs );
}
/**
* Wrap and format the given revision's comment block, if the specified
* user is allowed to view it.
*
* This method produces HTML that requires CSS styles in mediawiki.interface.helpers.styles.
*
* NOTE: revision comments are special. This is not the same as getting a
* revision comment as a string and then formatting it with format().
*
* @param RevisionRecord $revision The revision to extract the comment and
* title from. The title should always be populated, to avoid an additional
* DB query.
* @param Authority $authority The user viewing the comment
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @param bool $isPublic Show only if all users can see it
* @param bool $useParentheses Whether the comment is wrapped in parentheses
* @return string
*/
public function formatRevision(
RevisionRecord $revision,
Authority $authority,
$samePage = false,
$isPublic = false,
$useParentheses = true
) {
$parser = $this->parserFactory->create();
return $parser->finalize( $this->preprocessRevComment(
$parser, $authority, $revision, $samePage, $isPublic, $useParentheses ) );
}
/**
* Format multiple revision comments.
*
* @see CommentFormatter::formatRevision()
*
* @param iterable<RevisionRecord> $revisions
* @param Authority $authority
* @param bool $samePage
* @param bool $isPublic
* @param bool $useParentheses
* @param bool $indexById
* @return string|string[]
*/
public function formatRevisions(
$revisions,
Authority $authority,
$samePage = false,
$isPublic = false,
$useParentheses = true,
$indexById = false
) {
$parser = $this->parserFactory->create();
$outputs = [];
foreach ( $revisions as $i => $rev ) {
if ( $indexById ) {
$key = $rev->getId();
} else {
$key = $i;
}
$outputs[$key] = $this->preprocessRevComment(
$parser, $authority, $rev, $samePage, $isPublic, $useParentheses );
}
return $parser->finalize( $outputs );
}
/**
* Format a batch of revision comments using a fluent interface.
*
* @return RevisionCommentBatch
*/
public function createRevisionBatch() {
return new RevisionCommentBatch( $this );
}
/**
* Format an iterator over CommentItem objects
*
* A shortcut for createBatch()->comments()->execute() for when you
* need to pass no other options.
*
* @param iterable<CommentItem>|Traversable $items
* @return string[]
*/
public function formatItems( $items ) {
return $this->formatItemsInternal( $items );
}
/**
* @internal For use by CommentBatch
*
* Format comments with nullable batch options.
*
* @param iterable<CommentItem> $items
* @param LinkTarget|null $selfLinkTarget
* @param bool|null $samePage
* @param string|false|null $wikiId
* @param bool|null $enableSectionLinks
* @param bool|null $useBlock
* @param bool|null $useParentheses
* @return string[]
*/
public function formatItemsInternal( $items, $selfLinkTarget = null,
$samePage = null, $wikiId = null, $enableSectionLinks = null,
$useBlock = null, $useParentheses = null
) {
$outputs = [];
$parser = $this->parserFactory->create();
foreach ( $items as $index => $item ) {
$preprocessed = $parser->preprocess(
$item->comment,
$item->selfLinkTarget ?? $selfLinkTarget,
$item->samePage ?? $samePage ?? false,
$item->wikiId ?? $wikiId ?? false,
$enableSectionLinks ?? true
);
if ( $useBlock ?? false ) {
$preprocessed = $this->wrapCommentWithBlock(
$preprocessed,
$useParentheses ?? true
);
}
$outputs[$index] = $preprocessed;
}
return $parser->finalize( $outputs );
}
/**
* Wrap a comment in standard punctuation and formatting if
* it's non-empty, otherwise return empty string.
*
* @param string $formatted
* @param bool $useParentheses Whether the comment is wrapped in parentheses
*
* @return string
*/
protected function wrapCommentWithBlock(
$formatted, $useParentheses
) {
// '*' used to be the comment inserted by the software way back
// in antiquity in case none was provided, here for backwards
// compatibility, acc. to brion -ævar
if ( $formatted == '' || $formatted == '*' ) {
return '';
}
if ( $useParentheses ) {
$formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped();
$classNames = 'comment';
} else {
$classNames = 'comment comment--without-parentheses';
}
return " <span class=\"$classNames\">$formatted</span>";
}
/**
* Preprocess and wrap a revision comment.
*
* @param CommentParser $parser
* @param Authority $authority
* @param RevisionRecord $revRecord
* @param bool $samePage Whether section links should refer to local page
* @param bool $isPublic Show only if all users can see it
* @param bool $useParentheses (optional) Wrap comments in parentheses where needed
* @return string HTML fragment with link markers
*/
private function preprocessRevComment(
CommentParser $parser,
Authority $authority,
RevisionRecord $revRecord,
$samePage = false,
$isPublic = false,
$useParentheses = true
) {
if ( $revRecord->getComment( RevisionRecord::RAW ) === null ) {
return "";
}
if ( $revRecord->audienceCan(
RevisionRecord::DELETED_COMMENT,
$isPublic ? RevisionRecord::FOR_PUBLIC : RevisionRecord::FOR_THIS_USER,
$authority )
) {
$comment = $revRecord->getComment( RevisionRecord::FOR_THIS_USER, $authority );
$block = $parser->preprocess(
$comment ? $comment->text : '',
$revRecord->getPageAsLinkTarget(),
$samePage,
null,
true
);
$block = $this->wrapCommentWithBlock( $block, $useParentheses );
} else {
$block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
}
if ( $revRecord->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
$class = \Linker::getRevisionDeletedClass( $revRecord );
return " <span class=\"$class comment\">$block</span>";
}
return $block;
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Linker\LinkTarget;
/**
* An object to represent one of the inputs to a batch formatting operation.
*
* @since 1.38
* @newable
*/
class CommentItem {
/**
* @var string
* @internal
*/
public $comment;
/**
* @var LinkTarget|null
* @internal
*/
public $selfLinkTarget;
/**
* @var bool|null
* @internal
*/
public $samePage;
/**
* @var string|false|null
* @internal
*/
public $wikiId;
/**
* @param string $comment The comment to format
*/
public function __construct( string $comment ) {
$this->comment = $comment;
}
/**
* Set the self-link target.
*
* @param LinkTarget $selfLinkTarget The title used for fragment-only
* and section links, formerly $title.
* @return $this
*/
public function selfLinkTarget( LinkTarget $selfLinkTarget ) {
$this->selfLinkTarget = $selfLinkTarget;
return $this;
}
/**
* Set the same-page flag.
*
* @param bool $samePage If true, self links are rendered with a fragment-
* only URL. Formerly $local.
* @return $this
*/
public function samePage( $samePage = true ) {
$this->samePage = $samePage;
return $this;
}
/**
* ID of the wiki to link to (if not the local wiki), as used by WikiMap.
* This is used to render comments which are loaded from a foreign wiki.
* This only affects links which are syntactically internal -- it has no
* effect on interwiki links.
*
* @param string|false|null $wikiId
* @return $this
*/
public function wikiId( $wikiId ) {
$this->wikiId = $wikiId;
return $this;
}
}

View File

@ -0,0 +1,529 @@
<?php
namespace MediaWiki\CommentFormatter;
use File;
use HtmlArmor;
use Language;
use LinkBatch;
use LinkCache;
use Linker;
use MalformedTitleException;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkTarget;
use NamespaceInfo;
use Parser;
use RepoGroup;
use Title;
use TitleParser;
use TitleValue;
/**
* The text processing backend for CommentFormatter.
*
* CommentParser objects should be discarded after the comment batch is
* complete, in order to reduce memory usage.
*
* @internal
*/
class CommentParser {
/** @var LinkRenderer */
private $linkRenderer;
/** @var LinkBatchFactory */
private $linkBatchFactory;
/** @var RepoGroup */
private $repoGroup;
/** @var Language */
private $userLang;
/** @var Language */
private $contLang;
/** @var TitleParser */
private $titleParser;
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var HookRunner */
private $hookRunner;
/** @var LinkCache */
private $linkCache;
/** @var callable[] */
private $links = [];
/** @var LinkBatch|null */
private $linkBatch;
/** @var array Input to RepoGroup::findFiles() */
private $fileBatch;
/** @var File[] Resolved File objects indexed by DB key */
private $files = [];
/** @var int The maximum number of digits in a marker ID */
private const MAX_ID_SIZE = 7;
/**
* @param LinkRenderer $linkRenderer
* @param LinkBatchFactory $linkBatchFactory
* @param LinkCache $linkCache
* @param RepoGroup $repoGroup
* @param Language $userLang
* @param Language $contLang
* @param TitleParser $titleParser
* @param NamespaceInfo $namespaceInfo
* @param HookContainer $hookContainer
*/
public function __construct(
LinkRenderer $linkRenderer,
LinkBatchFactory $linkBatchFactory,
LinkCache $linkCache,
RepoGroup $repoGroup,
Language $userLang,
Language $contLang,
TitleParser $titleParser,
NamespaceInfo $namespaceInfo,
HookContainer $hookContainer
) {
$this->linkRenderer = $linkRenderer;
$this->linkBatchFactory = $linkBatchFactory;
$this->linkCache = $linkCache;
$this->repoGroup = $repoGroup;
$this->userLang = $userLang;
$this->contLang = $contLang;
$this->titleParser = $titleParser;
$this->namespaceInfo = $namespaceInfo;
$this->hookRunner = new HookRunner( $hookContainer );
}
/**
* Convert a comment to HTML, but replace links with markers which are
* resolved later.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget
* @param bool $samePage
* @param string|false|null $wikiId
* @param bool $enableSectionLinks
* @return string
*/
public function preprocess( string $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $enableSectionLinks = true
) {
return $this->preprocessInternal( $comment, false, $selfLinkTarget,
$samePage, $wikiId, $enableSectionLinks );
}
/**
* Convert a comment in pseudo-HTML format to HTML, replacing links with markers.
*
* @param string $comment
* @param LinkTarget|null $selfLinkTarget
* @param bool $samePage
* @param string|false|null $wikiId
* @param bool $enableSectionLinks
* @return string
*/
public function preprocessUnsafe( $comment, LinkTarget $selfLinkTarget = null,
$samePage = false, $wikiId = false, $enableSectionLinks = true
) {
return $this->preprocessInternal( $comment, true, $selfLinkTarget,
$samePage, $wikiId, $enableSectionLinks );
}
/**
* Execute pending batch queries and replace markers in the specified
* string(s) with actual links.
*
* @param string|string[] $comments
* @return string|string[]
*/
public function finalize( $comments ) {
$this->flushLinkBatches();
return preg_replace_callback(
'/\x1b([0-9]{' . self::MAX_ID_SIZE . '})/',
function ( $m ) {
$callback = $this->links[(int)$m[1]] ?? null;
if ( $callback ) {
return $callback();
} else {
return '<!-- MISSING -->';
}
},
$comments
);
}
/**
* @param string $comment
* @param bool $unsafe
* @param LinkTarget|null $selfLinkTarget
* @param bool $samePage
* @param string|false|null $wikiId
* @param bool $enableSectionLinks
* @return string
*/
private function preprocessInternal( $comment, $unsafe, $selfLinkTarget, $samePage, $wikiId,
$enableSectionLinks
) {
// Sanitize text a bit
// \x1b needs to be stripped because it is used for link markers
$comment = strtr( $comment, "\n\x1b", " " );
// Allow HTML entities (for T15815)
if ( !$unsafe ) {
$comment = \Sanitizer::escapeHtmlAllowEntities( $comment );
}
if ( $enableSectionLinks ) {
$comment = $this->doSectionLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
}
return $this->doWikiLinks( $comment, $selfLinkTarget, $samePage, $wikiId );
}
/**
* Converts C-style comments in edit summaries into section links.
*
* Too many things are called "comments", so these are mostly now called
* section links rather than autocomments.
*
* We look for all comments, match any text before and after the comment,
* add a separator where needed and format the comment itself with CSS.
*
* @param string $comment Comment text
* @param LinkTarget|null $selfLinkTarget An optional LinkTarget object used to links to sections
* @param bool $samePage Whether section links should refer to local page
* @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
* @return string Preprocessed comment
*/
private function doSectionLinks(
$comment,
$selfLinkTarget = null,
$samePage = false,
$wikiId = false
) {
// @todo $append here is something of a hack to preserve the status
// quo. Someone who knows more about bidi and such should decide
// (1) what sensible rendering even *is* for an LTR edit summary on an RTL
// wiki, both when autocomments exist and when they don't, and
// (2) what markup will make that actually happen.
$append = '';
$comment = preg_replace_callback(
// To detect the presence of content before or after the
// auto-comment, we use capturing groups inside optional zero-width
// assertions. But older versions of PCRE can't directly make
// zero-width assertions optional, so wrap them in a non-capturing
// group.
'!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!',
function ( $match ) use ( &$append, $selfLinkTarget, $samePage, $wikiId ) {
// Ensure all match positions are defined
$match += [ '', '', '', '' ];
$pre = $match[1] !== '';
$auto = $match[2];
$post = $match[3] !== '';
$comment = null;
$this->hookRunner->onFormatAutocomments(
$comment, $pre, $auto, $post,
Title::castFromLinkTarget( $selfLinkTarget ),
$samePage,
$wikiId );
if ( $comment !== null ) {
return $comment;
}
if ( $selfLinkTarget ) {
$section = $auto;
# Remove links that a user may have manually put in the autosummary
# This could be improved by copying as much of Parser::stripSectionName as desired.
$section = str_replace( [
'[[:',
'[[',
']]'
], '', $section );
// We don't want any links in the auto text to be linked, but we still
// want to show any [[ ]]
$sectionText = str_replace( '[[', '&#91;[', $auto );
$section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 );
if ( $section !== '' ) {
if ( $samePage ) {
$sectionTitle = new TitleValue( NS_MAIN, '', $section );
} else {
$sectionTitle = $selfLinkTarget->createFragmentTarget( $section );
}
$auto = $this->makeSectionLink(
$sectionTitle,
$this->userLang->getArrow() . $this->userLang->getDirMark() . $sectionText,
$wikiId
);
}
}
if ( $pre ) {
# written summary $presep autocomment (summary /* section */)
$pre = wfMessage( 'autocomment-prefix' )->inContentLanguage()->escaped();
}
if ( $post ) {
# autocomment $postsep written summary (/* section */ summary)
$auto .= wfMessage( 'colon-separator' )->inContentLanguage()->escaped();
}
if ( $auto ) {
$auto = '<span dir="auto"><span class="autocomment">' . $auto . '</span>';
$append .= '</span>';
}
$comment = $pre . $auto;
return $comment;
},
$comment
);
return $comment . $append;
}
/**
* Make a section link. These don't need to go into the LinkBatch, since
* the link class does not depend on whether the link is known.
*
* @param LinkTarget $target
* @param string $text
* @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
*
* @return string HTML link
*/
private function makeSectionLink(
LinkTarget $target, $text, $wikiId
) {
if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) {
return Linker::makeExternalLink(
\WikiMap::getForeignURL(
$wikiId,
$target->getNamespace() === 0
? $target->getDBkey()
: $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
':' . $target->getDBkey(),
$target->getFragment()
),
$text,
/* escape = */ false // Already escaped
);
}
return $this->linkRenderer->makePreloadedLink( $target, new HtmlArmor( $text ), '' );
}
/**
* Formats wiki links and media links in text; all other wiki formatting
* is ignored
*
* @todo FIXME: Doesn't handle sub-links as in image thumb texts like the main parser
*
* @param string $comment Text to format links in. WARNING! Since the output of this
* function is html, $comment must be sanitized for use as html. You probably want
* to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
* this function.
* as used by WikiMap.
* @param LinkTarget|null $selfLinkTarget An optional LinkTarget object used to links to sections
* @param bool $samePage Whether section links should refer to local page
* @param string|false|null $wikiId Id of the wiki to link to (if not the local wiki),
* as used by WikiMap.
*
* @return string HTML
*/
private function doWikiLinks( $comment, $selfLinkTarget = null, $samePage = false, $wikiId = false ) {
return preg_replace_callback(
'/
\[\[
\s*+ # ignore leading whitespace, the *+ quantifier disallows backtracking
:? # ignore optional leading colon
([^[\]|]+) # 1. link target; page names cannot include [, ] or |
(?:\|
# 2. link text
# Stop matching at ]] without relying on backtracking.
((?:]?[^\]])*+)
)?
\]\]
([^[]*) # 3. link trail (the text up until the next link)
/x',
function ( $match ) use ( $selfLinkTarget, $samePage, $wikiId ) {
$medians = '(?:';
$medians .= preg_quote(
$this->namespaceInfo->getCanonicalName( NS_MEDIA ), '/' );
$medians .= '|';
$medians .= preg_quote(
$this->contLang->getNsText( NS_MEDIA ),
'/'
) . '):';
$comment = $match[0];
// Fix up urlencoded title texts (copied from Parser::replaceInternalLinks)
if ( strpos( $match[1], '%' ) !== false ) {
$match[1] = strtr(
rawurldecode( $match[1] ),
[ '<' => '&lt;', '>' => '&gt;' ]
);
}
// Handle link renaming [[foo|text]] will show link as "text"
if ( $match[2] != "" ) {
$text = $match[2];
} else {
$text = $match[1];
}
$submatch = [];
$linkMarker = null;
if ( preg_match( '/^' . $medians . '(.*)$/i', $match[1], $submatch ) ) {
// Media link; trail not supported.
$linkRegexp = '/\[\[(.*?)\]\]/';
$linkTarget = $this->titleParser->makeTitleValueSafe( NS_FILE, $submatch[1] );
if ( $linkTarget ) {
$linkMarker = $this->addFileLink( $linkTarget, $text );
}
} else {
// Other kind of link
// Make sure its target is non-empty
if ( isset( $match[1][0] ) && $match[1][0] == ':' ) {
$match[1] = substr( $match[1], 1 );
}
if ( $match[1] !== false && $match[1] !== null && $match[1] !== '' ) {
if ( preg_match(
$this->contLang->linkTrail(),
$match[3],
$submatch
) ) {
$trail = $submatch[1];
} else {
$trail = "";
}
$linkRegexp = '/\[\[(.*?)\]\]' . preg_quote( $trail, '/' ) . '/';
list( $inside, $trail ) = Linker::splitTrail( $trail );
$linkText = $text;
$linkTarget = Linker::normalizeSubpageLink( $selfLinkTarget, $match[1], $linkText );
try {
$target = $this->titleParser->parseTitle( $linkTarget );
if ( $target->getText() == '' && !$target->isExternal()
&& !$samePage && $selfLinkTarget
) {
$target = $selfLinkTarget->createFragmentTarget( $target->getFragment() );
}
$linkMarker = $this->addPageLink( $target, $linkText . $inside, $wikiId );
$linkMarker .= $trail;
} catch ( MalformedTitleException $e ) {
// Fall through
}
}
}
if ( $linkMarker ) {
// If the link is still valid, go ahead and replace it in!
$comment = preg_replace(
$linkRegexp,
$linkMarker,
$comment,
1
);
}
return $comment;
},
$comment
);
}
/**
* Add a deferred link to the list and return its marker.
*
* @param callable $callback
* @return string
*/
private function addLinkMarker( $callback ) {
$nextId = count( $this->links );
if ( strlen( (string)$nextId ) > self::MAX_ID_SIZE ) {
throw new \RuntimeException( 'Too many links in comment batch' );
}
$this->links[] = $callback;
return sprintf( "\x1b%0" . self::MAX_ID_SIZE . 'd', $nextId );
}
/**
* Link to a LinkTarget. Return either HTML or a marker depending on whether
* existence checks are deferred.
*
* @param LinkTarget $target
* @param string $text
* @param string|false|null $wikiId
* @return string
*/
private function addPageLink( LinkTarget $target, $text, $wikiId ) {
if ( $wikiId !== null && $wikiId !== false && !$target->isExternal() ) {
// Handle links from a foreign wiki ID
return Linker::makeExternalLink(
\WikiMap::getForeignURL(
$wikiId,
$target->getNamespace() === 0
? $target->getDBkey()
: $this->namespaceInfo->getCanonicalName( $target->getNamespace() ) .
':' . $target->getDBkey(),
$target->getFragment()
),
$text,
/* escape = */ false // Already escaped
);
} elseif ( $this->linkCache->getGoodLinkID( $target ) ||
Title::newFromLinkTarget( $target )->isAlwaysKnown()
) {
// Already known
return $this->linkRenderer->makeKnownLink( $target, new HtmlArmor( $text ) );
} elseif ( $this->linkCache->isBadLink( $target ) ) {
// Already cached as unknown
return $this->linkRenderer->makeBrokenLink( $target, new HtmlArmor( $text ) );
}
// Defer page link
if ( !$this->linkBatch ) {
$this->linkBatch = $this->linkBatchFactory->newLinkBatch();
$this->linkBatch->setCaller( __METHOD__ );
}
$this->linkBatch->addObj( $target );
return $this->addLinkMarker( function () use ( $target, $text ) {
return $this->linkRenderer->makeLink( $target, new HtmlArmor( $text ) );
} );
}
/**
* Link to a file, returning a marker.
*
* @param LinkTarget $target The name of the file.
* @param string $html The inner HTML of the link
* @return string
*/
private function addFileLink( LinkTarget $target, $html ) {
$this->fileBatch[] = [
'title' => $target
];
return $this->addLinkMarker( function () use ( $target, $html ) {
return Linker::makeMediaLinkFile(
$target,
$this->files[$target->getDBkey()] ?? false,
$html
);
} );
}
/**
* Execute any pending link batch or file batch
*/
private function flushLinkBatches() {
if ( $this->linkBatch ) {
$this->linkBatch->execute();
$this->linkBatch = null;
}
if ( $this->fileBatch ) {
$this->files += $this->repoGroup->findFiles( $this->fileBatch );
$this->fileBatch = [];
}
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace MediaWiki\CommentFormatter;
use Language;
use LinkCache;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Linker\LinkRenderer;
use NamespaceInfo;
use RepoGroup;
use TitleParser;
/**
* @internal
*/
class CommentParserFactory {
/** @var LinkRenderer */
private $linkRenderer;
/** @var LinkBatchFactory */
private $linkBatchFactory;
/** @var LinkCache */
private $linkCache;
/** @var RepoGroup */
private $repoGroup;
/** @var Language */
private $userLang;
/** @var Language */
private $contLang;
/** @var TitleParser */
private $titleParser;
/** @var NamespaceInfo */
private $namespaceInfo;
/** @var HookContainer */
private $hookContainer;
/**
* @param LinkRenderer $linkRenderer
* @param LinkBatchFactory $linkBatchFactory
* @param LinkCache $linkCache
* @param RepoGroup $repoGroup
* @param Language $userLang
* @param Language $contLang
* @param TitleParser $titleParser
* @param NamespaceInfo $namespaceInfo
* @param HookContainer $hookContainer
*/
public function __construct(
LinkRenderer $linkRenderer,
LinkBatchFactory $linkBatchFactory,
LinkCache $linkCache,
RepoGroup $repoGroup,
Language $userLang,
Language $contLang,
TitleParser $titleParser,
NamespaceInfo $namespaceInfo,
HookContainer $hookContainer
) {
$this->linkRenderer = $linkRenderer;
$this->linkBatchFactory = $linkBatchFactory;
$this->linkCache = $linkCache;
$this->repoGroup = $repoGroup;
$this->userLang = $userLang;
$this->contLang = $contLang;
$this->titleParser = $titleParser;
$this->namespaceInfo = $namespaceInfo;
$this->hookContainer = $hookContainer;
}
/**
* @return CommentParser
*/
public function create() {
return new CommentParser(
$this->linkRenderer,
$this->linkBatchFactory,
$this->linkCache,
$this->repoGroup,
$this->userLang,
$this->contLang,
$this->titleParser,
$this->namespaceInfo,
$this->hookContainer
);
}
}

View File

@ -0,0 +1,133 @@
<?php
namespace MediaWiki\CommentFormatter;
use MediaWiki\Permissions\Authority;
use MediaWiki\Revision\RevisionRecord;
/**
* Fluent interface for revision comment batch inputs.
*
* @since 1.38
*/
class RevisionCommentBatch {
/** @var CommentFormatter */
private $formatter;
/** @var Authority|null */
private $authority;
/** @var iterable<RevisionRecord> */
private $revisions;
/** @var bool */
private $samePage = false;
/** @var bool */
private $isPublic = false;
/** @var bool */
private $useParentheses = false;
/** @var bool */
private $indexById = false;
/**
* @param CommentFormatter $formatter
*/
public function __construct( CommentFormatter $formatter ) {
$this->formatter = $formatter;
}
/**
* Set the authority to use for permission checks. This must be called
* prior to execute().
*
* @param Authority $authority
* @return $this
*/
public function authority( Authority $authority ) {
$this->authority = $authority;
return $this;
}
/**
* Set the revisions to extract comments from.
*
* @param iterable<RevisionRecord> $revisions
* @return $this
*/
public function revisions( $revisions ) {
$this->revisions = $revisions;
return $this;
}
/**
* Set the same-page option. If this is true, section links and fragment-
* only wikilinks are rendered with an href that is a fragment-only URL.
* If it is false (the default), such links go to the self link title.
*
* This is equivalent to $local in the old Linker methods.
*
* @param bool $samePage
* @return $this
*/
public function samePage( $samePage = true ) {
$this->samePage = $samePage;
return $this;
}
/**
* Wrap the comment with parentheses. This has no effect if the useBlock
* option is not enabled.
*
* Unlike the legacy Linker::commentBlock(), this option defaults to false
* if this method is not called, since that better preserves the fluent
* style.
*
* @param bool $useParentheses
* @return $this
*/
public function useParentheses( $useParentheses = true ) {
$this->useParentheses = $useParentheses;
return $this;
}
/**
* If this is true, show the comment only if all users can see it.
*
* We'll call it hideIfDeleted() since public is a keyword and isPublic()
* has an inappropriate verb.
*
* @param bool $isPublic
* @return $this
*/
public function hideIfDeleted( $isPublic = true ) {
$this->isPublic = $isPublic;
return $this;
}
/**
* If this is true, the array keys in the return value will be the revision
* IDs instead of the keys from the input array.
*
* @param bool $indexById
* @return $this
*/
public function indexById( $indexById = true ) {
$this->indexById = $indexById;
return $this;
}
/**
* Format the comments.
*
* @return string[] Formatted comments. The array key is either the field
* value specified by indexField(), or if that was not called, it is the
* key from the array passed to revisions().
*/
public function execute() {
return $this->formatter->formatRevisions(
$this->revisions,
$this->authority,
$this->samePage,
$this->isPublic,
$this->useParentheses,
$this->indexById
);
}
}

View File

@ -0,0 +1,88 @@
<?php
namespace MediaWiki\CommentFormatter;
use CommentStore;
use Traversable;
use Wikimedia\Rdbms\IResultWrapper;
/**
* This is basically a CommentFormatter with a CommentStore dependency, allowing
* it to retrieve comment texts directly from database result wrappers.
*
* @since 1.38
*/
class RowCommentFormatter extends CommentFormatter {
/** @var CommentStore */
private $commentStore;
/**
* @internal Use MediaWikiServices::getRowCommentFormatter()
*
* @param CommentParserFactory $commentParserFactory
* @param CommentStore $commentStore
*/
public function __construct(
CommentParserFactory $commentParserFactory,
CommentStore $commentStore
) {
parent::__construct( $commentParserFactory );
$this->commentStore = $commentStore;
}
/**
* Format DB rows using a fluent interface. Pass the return value of this
* function to CommentBatch::comments().
*
* Example:
* $comments = $rowCommentFormatter->createBatch()
* ->comments(
* $rowCommentFormatter->rows( $rows )
* ->commentField( 'img_comment' )
* )
* ->useBlock( true )
* ->execute();
*
* @param Traversable|array $rows
* @return RowCommentIterator
*/
public function rows( $rows ) {
return new RowCommentIterator( $this->commentStore, $rows );
}
/**
* Format DB rows using a parametric interface.
*
* @param iterable<\stdClass>|IResultWrapper $rows
* @param string $commentKey The comment key to pass through to CommentStore,
* typically a legacy field name.
* @param string|null $namespaceField The namespace field for the self-link
* target, or null to have no self-link target.
* @param string|null $titleField The title field for the self-link target,
* or null to have no self-link target.
* @param string|null $indexField The field to use for array keys in the
* result, or null to use the same keys as in the input $rows
* @param bool $useBlock Wrap the output in standard punctuation and
* formatting if it's non-empty.
* @param bool $useParentheses Wrap the output with parentheses. Has no
* effect if $useBlock is false.
* @return string[] The formatted comment. The key will be the value of the
* index field if an index field was specified, or the key from the
* corresponding element of $rows if no index field was specified.
*/
public function formatRows( $rows, $commentKey, $namespaceField = null, $titleField = null,
$indexField = null, $useBlock = false, $useParentheses = true
) {
return $this->createBatch()
->comments(
$this->rows( $rows )
->commentKey( $commentKey )
->namespaceField( $namespaceField )
->titleField( $titleField )
->indexField( $indexField )
)
->useBlock( $useBlock )
->useParentheses( $useParentheses )
->execute();
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace MediaWiki\CommentFormatter;
use ArrayIterator;
use CommentStore;
use IteratorIterator;
use TitleValue;
use Traversable;
/**
* An adaptor which converts a row iterator into a CommentItem iterator for
* batch formatting.
*
* Fluent-style mutators are provided to configure how comment text is extracted
* from rows.
*
* Using an iterator for this configuration, instead of putting the
* options in CommentBatch, allows CommentBatch to be a simple single
* class without a CommentStore dependency.
*
* @since 1.38
*/
class RowCommentIterator extends IteratorIterator {
/** @var CommentStore */
private $commentStore;
/** @var string|null */
private $commentKey;
/** @var string|null */
private $namespaceField;
/** @var string|null */
private $titleField;
/** @var string|null */
private $indexField;
/**
* @internal Use RowCommentFormatter::rows()
* @param CommentStore $commentStore
* @param Traversable|array $rows
*/
public function __construct( CommentStore $commentStore, $rows ) {
if ( is_array( $rows ) ) {
parent::__construct( new ArrayIterator( $rows ) );
} else {
parent::__construct( $rows );
}
$this->commentStore = $commentStore;
}
/**
* Set what CommentStore calls the key -- typically a legacy field name
* which once held a comment. This must be called before attempting
* iteration.
*
* @param string $key
* @return $this
*/
public function commentKey( $key ) {
$this->commentKey = $key;
return $this;
}
/**
* Set the namespace field. If this is not called, the item will not have
* a self-link target, although it may be provided by the batch.
*
* @param string $field
* @return $this
*/
public function namespaceField( $field ) {
$this->namespaceField = $field;
return $this;
}
/**
* Set the title field. If this is not called, the item will not have
* a self-link target, although it may be provided by the batch.
*
* @param string $field
* @return $this
*/
public function titleField( $field ) {
$this->titleField = $field;
return $this;
}
/**
* Set the index field. Values from this field will appear as array keys
* in the final formatted comment array. If unset, the array will be
* numerically indexed.
*
* @param string $field
* @return $this
*/
public function indexField( $field ) {
$this->indexField = $field;
return $this;
}
public function key(): string {
if ( $this->indexField ) {
return parent::current()->{$this->indexField};
} else {
return parent::key();
}
}
public function current(): CommentItem {
if ( $this->commentKey === null ) {
throw new \RuntimeException( __METHOD__ . ': commentKey must be specified' );
}
$row = parent::current();
$comment = $this->commentStore->getComment( $this->commentKey, $row );
$item = new CommentItem( (string)$comment->text );
if ( $this->namespaceField && $this->titleField ) {
$item->selfLinkTarget( new TitleValue(
(int)$row->{$this->namespaceField},
(string)$row->{$this->titleField}
) );
}
return $item;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\CommentFormatter;
use ArrayIterator;
/**
* An adaptor which converts an array of strings to an iterator of CommentItem
* objects.
*
* @since 1.38
*/
class StringCommentIterator extends ArrayIterator {
/**
* @internal Use CommentBatch::strings()
* @param string[] $strings
*/
public function __construct( $strings ) {
parent::__construct( $strings );
}
public function current(): CommentItem {
return new CommentItem( parent::current() );
}
}

View File

@ -0,0 +1,659 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\IDatabase;
/**
* @defgroup CommentStore CommentStore
*
* The Comment store in MediaWiki is responsible for storing edit summaries,
* log action comments and other such short strings (referred to as "comments").
*
* The CommentStore class handles the database abstraction for reading
* and writing comments, which are represented by CommentStoreComment objects.
*
* Data is internally stored in the `comment` table.
*/
/**
* Handle database storage of comments such as edit summaries and log reasons.
*
* @ingroup CommentStore
* @since 1.30
*/
class CommentStore {
/**
* Maximum length of a comment in UTF-8 characters. Longer comments will be truncated.
* @note This must be at least 255 and not greater than floor( MAX_DATA_LENGTH / 4 ).
*/
public const COMMENT_CHARACTER_LIMIT = 500;
/**
* Maximum length of serialized data in bytes. Longer data will result in an exception.
* @note This value is determined by the size of the underlying database field,
* currently BLOB in MySQL/MariaDB.
*/
public const MAX_DATA_LENGTH = 65535;
/**
* Define fields that use temporary tables for transitional purposes
* Array keys are field names, values are arrays with these possible fields:
* - table: Temporary table name
* - pk: Temporary table column referring to the main table's primary key
* - field: Temporary table column referring comment.comment_id
* - joinPK: Main table's primary key
* - stage: Migration stage
* - deprecatedIn: Version when using insertWithTempTable() was deprecated
*/
protected const TEMP_TABLES = [
'rev_comment' => [
'table' => 'revision_comment_temp',
'pk' => 'revcomment_rev',
'field' => 'revcomment_comment_id',
'joinPK' => 'rev_id',
'stage' => MIGRATION_OLD,
'deprecatedIn' => null,
],
'img_description' => [
'stage' => MIGRATION_NEW,
'deprecatedIn' => '1.32',
],
];
/**
* @var int One of the MIGRATION_* constants, or an appropriate combination
* of SCHEMA_COMPAT_* constants.
* @todo Deprecate and remove once extensions seem unlikely to need to use
* it for migration anymore.
*/
private $stage;
/** @var array[] Cache for `self::getJoin()` */
private $joinCache = [];
/** @var Language Language to use for comment truncation */
private $lang;
/**
* @param Language $lang Language to use for comment truncation. Defaults
* to content language.
* @param int $stage One of the MIGRATION_* constants, or an appropriate
* combination of SCHEMA_COMPAT_* constants. Always MIGRATION_NEW for
* MediaWiki core since 1.33.
*/
public function __construct( Language $lang, $stage ) {
if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
throw new InvalidArgumentException( '$stage must include a write mode' );
}
if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
throw new InvalidArgumentException( '$stage must include a read mode' );
}
$this->stage = $stage;
$this->lang = $lang;
}
/**
* @since 1.31
* @deprecated in 1.31 Use DI to inject a CommentStore instance into your class.
* @return CommentStore
*/
public static function getStore() {
return MediaWikiServices::getInstance()->getCommentStore();
}
/**
* Get SELECT fields for the comment key
*
* Each resulting row should be passed to `self::getCommentLegacy()` to get the
* actual comment.
*
* @note Use of this method may require a subsequent database query to
* actually fetch the comment. If possible, use `self::getJoin()` instead.
*
* @since 1.30
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @return string[] to include in the `$vars` to `IDatabase->select()`. All
* fields are aliased, so `+` is safe to use.
*/
public function getFields( $key ) {
$fields = [];
if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
$fields["{$key}_text"] = $key;
$fields["{$key}_data"] = 'NULL';
$fields["{$key}_cid"] = 'NULL';
} else { // READ_BOTH or READ_NEW
if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
$fields["{$key}_old"] = $key;
}
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
$fields["{$key}_pk"] = static::TEMP_TABLES[$key]['joinPK'];
}
if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) {
$fields["{$key}_id"] = "{$key}_id";
}
}
return $fields;
}
/**
* Get SELECT fields and joins for the comment key
*
* Each resulting row should be passed to `self::getComment()` to get the
* actual comment.
*
* @since 1.30
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @return array[] With three keys:
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
* All tables, fields, and joins are aliased, so `+` is safe to use.
* @phan-return array{tables:string[],fields:string[],joins:array}
*/
public function getJoin( $key ) {
if ( !array_key_exists( $key, $this->joinCache ) ) {
$tables = [];
$fields = [];
$joins = [];
if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
$fields["{$key}_text"] = $key;
$fields["{$key}_data"] = 'NULL';
$fields["{$key}_cid"] = 'NULL';
} else { // READ_BOTH or READ_NEW
$join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN';
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
$t = static::TEMP_TABLES[$key];
$alias = "temp_$key";
$tables[$alias] = $t['table'];
$joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
$joinField = "{$alias}.{$t['field']}";
} else {
// Nothing hits this code path for now, but will in the future when we set
// static::TEMP_TABLES['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
// merging revision_comment_temp into revision.
// @codeCoverageIgnoreStart
$joins[$alias][0] = 'LEFT JOIN';
$joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
throw new LogicException( 'Nothing should reach this code path at this time' );
// @codeCoverageIgnoreEnd
}
} else {
$joinField = "{$key}_id";
}
$alias = "comment_$key";
$tables[$alias] = 'comment';
$joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
$fields["{$key}_text"] = "{$alias}.comment_text";
} else {
$fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
}
$fields["{$key}_data"] = "{$alias}.comment_data";
$fields["{$key}_cid"] = "{$alias}.comment_id";
}
$this->joinCache[$key] = [
'tables' => $tables,
'fields' => $fields,
'joins' => $joins,
];
}
return $this->joinCache[$key];
}
/**
* Extract the comment from a row
*
* Shared implementation for getComment() and getCommentLegacy()
*
* @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment()
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @param stdClass|array $row
* @param bool $fallback
* @return CommentStoreComment
*/
private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) {
$row = (array)$row;
if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
$cid = $row["{$key}_cid"] ?? null;
$text = $row["{$key}_text"];
$data = $row["{$key}_data"];
} elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
$cid = null;
if ( $fallback && isset( $row[$key] ) ) {
wfLogWarning( "Using deprecated fallback handling for comment $key" );
$text = $row[$key];
} else {
wfLogWarning(
"Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
);
$text = '';
}
$data = null;
} else {
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
$row2 = null;
if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
if ( !$db ) {
throw new InvalidArgumentException(
"\$row does not contain fields needed for comment $key and getComment(), but "
. "does have fields for getCommentLegacy()"
);
}
$id = $row["{$key}_id"];
$row2 = $db->selectRow(
'comment',
[ 'comment_id', 'comment_text', 'comment_data' ],
[ 'comment_id' => $id ],
__METHOD__
);
}
if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
array_key_exists( "{$key}_pk", $row )
) {
if ( !$db ) {
throw new InvalidArgumentException(
"\$row does not contain fields needed for comment $key and getComment(), but "
. "does have fields for getCommentLegacy()"
);
}
$t = static::TEMP_TABLES[$key];
$id = $row["{$key}_pk"];
$row2 = $db->selectRow(
[ $t['table'], 'comment' ],
[ 'comment_id', 'comment_text', 'comment_data' ],
[ $t['pk'] => $id ],
__METHOD__,
[],
[ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
);
}
if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
wfLogWarning( "Using deprecated fallback handling for comment $key" );
$row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
}
if ( $row2 === null ) {
throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
}
if ( $row2 ) {
$cid = $row2->comment_id;
$text = $row2->comment_text;
$data = $row2->comment_data;
} elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
array_key_exists( "{$key}_old", $row )
) {
$cid = null;
$text = $row["{$key}_old"];
$data = null;
} else {
// @codeCoverageIgnoreStart
wfLogWarning( "Missing comment row for $key, id=$id" );
$cid = null;
$text = '';
$data = null;
// @codeCoverageIgnoreEnd
}
}
$msg = null;
if ( $data !== null ) {
$data = FormatJson::decode( $data, true );
if ( !is_array( $data ) ) {
// @codeCoverageIgnoreStart
wfLogWarning( "Invalid JSON object in comment: $data" );
$data = null;
// @codeCoverageIgnoreEnd
} else {
if ( isset( $data['_message'] ) ) {
$msg = self::decodeMessage( $data['_message'] )
->setInterfaceMessageFlag( true );
}
if ( !empty( $data['_null'] ) ) {
$data = null;
} else {
foreach ( $data as $k => $v ) {
if ( substr( $k, 0, 1 ) === '_' ) {
unset( $data[$k] );
}
}
}
}
}
return new CommentStoreComment( $cid, $text, $msg, $data );
}
/**
* Extract the comment from a row
*
* Use `self::getJoin()` to ensure the row contains the needed data.
*
* If you need to fake a comment in a row for some reason, set fields
* `{$key}_text` (string) and `{$key}_data` (JSON string or null).
*
* @since 1.30
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @param stdClass|array|null $row Result row.
* @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
* @return CommentStoreComment
*/
public function getComment( $key, $row = null, $fallback = false ) {
if ( $row === null ) {
// @codeCoverageIgnoreStart
throw new InvalidArgumentException( '$row must not be null' );
// @codeCoverageIgnoreEnd
}
return $this->getCommentInternal( null, $key, $row, $fallback );
}
/**
* Extract the comment from a row, with legacy lookups.
*
* If `$row` might have been generated using `self::getFields()` rather
* than `self::getJoin()`, use this. Prefer `self::getComment()` if you
* know callers used `self::getJoin()` for the row fetch.
*
* If you need to fake a comment in a row for some reason, set fields
* `{$key}_text` (string) and `{$key}_data` (JSON string or null).
*
* @since 1.30
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
* @param IDatabase $db Database handle to use for lookup
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @param stdClass|array|null $row Result row.
* @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
* @return CommentStoreComment
*/
public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
if ( $row === null ) {
// @codeCoverageIgnoreStart
throw new InvalidArgumentException( '$row must not be null' );
// @codeCoverageIgnoreEnd
}
return $this->getCommentInternal( $db, $key, $row, $fallback );
}
/**
* Create a new CommentStoreComment, inserting it into the database if necessary
*
* If a comment is going to be passed to `self::insert()` or the like
* multiple times, it will be more efficient to pass a CommentStoreComment
* once rather than making `self::insert()` do it every time through.
*
* @note When passing a CommentStoreComment, this may set `$comment->id` if
* it's not already set. If `$comment->id` is already set, it will not be
* verified that the specified comment actually exists or that it
* corresponds to the comment text, message, and/or data in the
* CommentStoreComment.
* @param IDatabase $dbw Database handle to insert on. Unused if `$comment`
* is a CommentStoreComment and `$comment->id` is set.
* @param string|Message|CommentStoreComment $comment Comment text or Message object, or
* a CommentStoreComment.
* @param array|null $data Structured data to store. Keys beginning with '_' are reserved.
* Ignored if $comment is a CommentStoreComment.
* @return CommentStoreComment
*/
public function createComment( IDatabase $dbw, $comment, array $data = null ) {
$comment = CommentStoreComment::newUnsavedComment( $comment, $data );
# Truncate comment in a Unicode-sensitive manner
$comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
$dbData = $comment->data;
if ( !$comment->message instanceof RawMessage ) {
if ( $dbData === null ) {
$dbData = [ '_null' => true ];
}
$dbData['_message'] = self::encodeMessage( $comment->message );
}
if ( $dbData !== null ) {
$dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
$len = strlen( $dbData );
if ( $len > self::MAX_DATA_LENGTH ) {
$max = self::MAX_DATA_LENGTH;
throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
}
}
$hash = self::hash( $comment->text, $dbData );
$commentId = $dbw->selectField(
'comment',
'comment_id',
[
'comment_hash' => $hash,
'comment_text' => $comment->text,
'comment_data' => $dbData,
],
__METHOD__
);
if ( !$commentId ) {
$dbw->insert(
'comment',
[
'comment_hash' => $hash,
'comment_text' => $comment->text,
'comment_data' => $dbData,
],
__METHOD__
);
$commentId = $dbw->insertId();
}
$comment->id = (int)$commentId;
}
return $comment;
}
/**
* Implementation for `self::insert()` and `self::insertWithTempTable()`
* @param IDatabase $dbw
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @param string|Message|CommentStoreComment $comment
* @param array|null $data
* @return array [ array $fields, callable $callback ]
*/
private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
$fields = [];
$callback = null;
$comment = $this->createComment( $dbw, $comment, $data );
if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
$fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
}
if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
$t = static::TEMP_TABLES[$key];
$func = __METHOD__;
$commentId = $comment->id;
$callback = static function ( $id ) use ( $dbw, $commentId, $t, $func ) {
$dbw->insert(
$t['table'],
[
$t['pk'] => $id,
$t['field'] => $commentId,
],
$func
);
};
}
if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
$fields["{$key}_id"] = $comment->id;
}
}
return [ $fields, $callback ];
}
/**
* Insert a comment in preparation for a row that references it
*
* @note It's recommended to include both the call to this method and the
* row insert in the same transaction.
*
* @since 1.30
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
* @param IDatabase $dbw Database handle to insert on
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @param string|Message|CommentStoreComment|null $comment As for `self::createComment()`
* @param array|null $data As for `self::createComment()`
* @return array Fields for the insert or update
*/
public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
if ( $comment === null ) {
// @codeCoverageIgnoreStart
throw new InvalidArgumentException( '$comment can not be null' );
// @codeCoverageIgnoreEnd
}
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
}
list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
return $fields;
}
/**
* Insert a comment in a temporary table in preparation for a row that references it
*
* This is currently needed for "rev_comment" and "img_description". In the
* future that requirement will be removed.
*
* @note It's recommended to include both the call to this method and the
* row insert in the same transaction.
*
* @since 1.30
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
* @param IDatabase $dbw Database handle to insert on
* @param string $key A key such as "rev_comment" identifying the comment
* field being fetched.
* @param string|Message|CommentStoreComment|null $comment As for `self::createComment()`
* @param array|null $data As for `self::createComment()`
* @return array Two values:
* - array Fields for the insert or update
* - callable Function to call when the primary key of the row being
* inserted/updated is known. Pass it that primary key.
*/
public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
if ( $comment === null ) {
// @codeCoverageIgnoreStart
throw new InvalidArgumentException( '$comment can not be null' );
// @codeCoverageIgnoreEnd
}
if ( !isset( static::TEMP_TABLES[$key] ) ) {
throw new InvalidArgumentException( "Must use insert() for $key" );
} elseif ( isset( static::TEMP_TABLES[$key]['deprecatedIn'] ) ) {
wfDeprecated( __METHOD__ . " for $key", static::TEMP_TABLES[$key]['deprecatedIn'] );
}
list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
if ( !$callback ) {
$callback = static function () {
// Do nothing.
};
}
return [ $fields, $callback ];
}
/**
* Encode a Message as a PHP data structure
* @param Message $msg
* @return array
*/
private static function encodeMessage( Message $msg ) {
$key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
$params = $msg->getParams();
foreach ( $params as &$param ) {
if ( $param instanceof Message ) {
$param = [
'message' => self::encodeMessage( $param )
];
}
}
array_unshift( $params, $key );
return $params;
}
/**
* Decode a message that was encoded by self::encodeMessage()
* @param array $data
* @return Message
*/
private static function decodeMessage( $data ) {
$key = array_shift( $data );
foreach ( $data as &$param ) {
if ( is_object( $param ) ) {
$param = (array)$param;
}
if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
$param = self::decodeMessage( $param['message'] );
}
}
return new Message( $key, $data );
}
/**
* Hashing function for comment storage
* @param string $text Comment text
* @param string|null $data Comment data
* @return int 32-bit signed integer
*/
public static function hash( $text, $data ) {
$hash = crc32( $text ) ^ crc32( (string)$data );
// 64-bit PHP returns an unsigned CRC, change it to signed for
// insertion into the database.
if ( $hash >= 0x80000000 ) {
$hash |= -1 << 32;
}
return $hash;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\MediaWikiServices;
/**
* Value object for a comment stored by CommentStore.
*
* The fields should be considered read-only.
*
* @ingroup CommentStore
* @since 1.30
*/
class CommentStoreComment {
/** @var int|null Comment ID, if any */
public $id;
/** @var string Text version of the comment */
public $text;
/** @var Message Message version of the comment. Might be a RawMessage */
public $message;
/** @var array|null Structured data of the comment */
public $data;
/**
* @internal For use by CommentStore only. Use self::newUnsavedComment() instead.
* @param int|null $id
* @param string $text
* @param Message|null $message
* @param array|null $data
*/
public function __construct( $id, $text, Message $message = null, array $data = null ) {
$this->id = $id;
$this->text = $text;
$this->message = $message ?: new RawMessage( '$1', [ Message::plaintextParam( $text ) ] );
$this->data = $data;
}
/**
* Create a new, unsaved CommentStoreComment
*
* @param string|Message|CommentStoreComment $comment Comment text or Message object.
* A CommentStoreComment is also accepted here, in which case it is returned unchanged.
* @param array|null $data Structured data to store. Keys beginning with '_' are reserved.
* Ignored if $comment is a CommentStoreComment.
* @return CommentStoreComment
*/
public static function newUnsavedComment( $comment, array $data = null ) {
if ( $comment instanceof CommentStoreComment ) {
return $comment;
}
if ( $data !== null ) {
foreach ( $data as $k => $v ) {
if ( substr( $k, 0, 1 ) === '_' ) {
throw new InvalidArgumentException( 'Keys in $data beginning with "_" are reserved' );
}
}
}
if ( $comment instanceof Message ) {
$message = clone $comment;
// Avoid $wgForceUIMsgAsContentMsg
$text = $message->inLanguage( MediaWikiServices::getInstance()->getContentLanguage() )
->setInterfaceMessageFlag( true )
->text();
return new CommentStoreComment( null, $text, $message, $data );
} else {
return new CommentStoreComment( null, $comment, null, $data );
}
}
}

View File

@ -0,0 +1,67 @@
<?php
/**
* A read-only mode service which does not depend on LoadBalancer.
* To obtain an instance, use MediaWikiServices::getInstance()->getConfiguredReadOnlyMode().
*
* @since 1.29
*/
class ConfiguredReadOnlyMode {
/** @var string|bool|null */
private $reason;
/** @var string|null */
private $reasonFile;
/**
* @param string|bool|null $reason Current reason for read-only mode, if known. null means look
* in $reasonFile instead.
* @param string|null $reasonFile A file to look in for a reason, if $reason is null. If it
* exists and is non-empty, its contents are treated as the reason for read-only mode.
* Otherwise, the wiki is not read-only.
*/
public function __construct( $reason, $reasonFile = null ) {
$this->reason = $reason;
$this->reasonFile = $reasonFile;
}
/**
* Check whether the wiki is in read-only mode.
*
* @return bool
*/
public function isReadOnly() {
return $this->getReason() !== false;
}
/**
* Get the value of $wgReadOnly or the contents of $wgReadOnlyFile.
*
* @return string|bool String when in read-only mode; false otherwise
*/
public function getReason() {
if ( $this->reason !== null ) {
return $this->reason;
}
if ( $this->reasonFile === null ) {
return false;
}
// Try the reason file
if ( is_file( $this->reasonFile ) && filesize( $this->reasonFile ) > 0 ) {
$this->reason = file_get_contents( $this->reasonFile );
}
// No need to try the reason file again
$this->reasonFile = null;
return $this->reason ?? false;
}
/**
* Set the read-only mode, which will apply for the remainder of the
* request or until a service reset.
*
* @param string|null $msg
*/
public function setReason( $msg ) {
$this->reason = $msg;
}
}

View File

@ -0,0 +1,594 @@
<?php
/**
* Handle sending Content-Security-Policy headers
*
* @see https://www.w3.org/TR/CSP2/
*
* Copyright © 20152018 Brian Wolff
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @since 1.32
* @file
*/
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\MediaWikiServices;
class ContentSecurityPolicy {
public const REPORT_ONLY_MODE = 1;
public const FULL_MODE = 2;
/** @var string The nonce to use for inline scripts (from OutputPage) */
private $nonce;
/** @var Config The site configuration object */
private $mwConfig;
/** @var WebResponse */
private $response;
/** @var array */
private $extraDefaultSrc = [];
/** @var array */
private $extraScriptSrc = [];
/** @var array */
private $extraStyleSrc = [];
/** @var HookRunner */
private $hookRunner;
/**
* @note As a general rule, you would not construct this class directly
* but use the instance from OutputPage::getCSP()
* @internal
* @param WebResponse $response
* @param Config $mwConfig
* @param HookContainer $hookContainer
* @since 1.35 Method signature changed
*/
public function __construct( WebResponse $response, Config $mwConfig,
HookContainer $hookContainer
) {
$this->response = $response;
$this->mwConfig = $mwConfig;
$this->hookRunner = new HookRunner( $hookContainer );
}
/**
* Send a single CSP header based on a given policy config.
*
* @note Most callers will probably want ContentSecurityPolicy::sendHeaders() instead.
* @internal
* @param array $csp ContentSecurityPolicy configuration
* @param int $reportOnly self::*_MODE constant
*/
public function sendCSPHeader( $csp, $reportOnly ) {
$policy = $this->makeCSPDirectives( $csp, $reportOnly );
$headerName = $this->getHeaderName( $reportOnly );
if ( $policy ) {
$this->response->header(
"$headerName: $policy"
);
}
}
/**
* Send CSP headers based on wiki config
*
* Main method that callers (OutputPage) are expected to use.
* As a general rule, you would never call this in an extension unless
* you have disabled OutputPage and are fully controlling the output.
*
* @since 1.35
*/
public function sendHeaders() {
$cspConfig = $this->mwConfig->get( 'CSPHeader' );
$cspConfigReportOnly = $this->mwConfig->get( 'CSPReportOnlyHeader' );
$this->sendCSPHeader( $cspConfig, self::FULL_MODE );
$this->sendCSPHeader( $cspConfigReportOnly, self::REPORT_ONLY_MODE );
// This used to insert a <meta> tag here, per advice at
// https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
// The goal was to prevent nonce from working after the page hit onready,
// This would help in old browsers that didn't support nonces, and
// also assist for varnish-cached pages which repeat nonces.
// However, this is incompatible with how resource loader storage works
// via mw.domEval() so it was removed.
}
/**
* @param int $reportOnly Either self::REPORT_ONLY_MODE or self::FULL_MODE
* @return string Name of http header
* @throws UnexpectedValueException
*/
private function getHeaderName( $reportOnly ) {
if ( $reportOnly === self::REPORT_ONLY_MODE ) {
return 'Content-Security-Policy-Report-Only';
}
if ( $reportOnly === self::FULL_MODE ) {
return 'Content-Security-Policy';
}
throw new UnexpectedValueException( "Mode '$reportOnly' not recognised" );
}
/**
* Determine what CSP policies to set for this page
*
* @param array|bool $policyConfig Policy configuration
* (Either $wgCSPHeader or $wgCSPReportOnlyHeader)
* @param int $mode self::REPORT_ONLY_MODE, self::FULL_MODE
* @return string Policy directives, or empty string for no policy.
*/
private function makeCSPDirectives( $policyConfig, $mode ) {
if ( $policyConfig === false ) {
// CSP is disabled
return '';
}
if ( $policyConfig === true ) {
$policyConfig = [];
}
$mwConfig = $this->mwConfig;
if (
!self::isNonceRequired( $mwConfig ) &&
self::isNonceRequiredArray( [ $policyConfig ] )
) {
// If the current policy requires a nonce, but the global state
// does not, that's bad. Throw an exception. This should never happen.
throw new LogicException( "Nonce requirement mismatch" );
}
$additionalSelfUrls = $this->getAdditionalSelfUrls();
$additionalSelfUrlsScript = $this->getAdditionalSelfUrlsScript();
// If no default-src is sent at all, it
// seems browsers (or at least some), interpret
// that as allow anything, but the spec seems
// to imply that data: and blob: should be
// blocked.
$defaultSrc = [ '*', 'data:', 'blob:' ];
$imgSrc = false;
$scriptSrc = [ "'unsafe-eval'", "blob:", "'self'" ];
if ( $policyConfig['useNonces'] ?? true ) {
$scriptSrc[] = "'nonce-" . $this->getNonce() . "'";
}
$scriptSrc = array_merge( $scriptSrc, $additionalSelfUrlsScript );
if ( isset( $policyConfig['script-src'] )
&& is_array( $policyConfig['script-src'] )
) {
foreach ( $policyConfig['script-src'] as $src ) {
$scriptSrc[] = $this->escapeUrlForCSP( $src );
}
}
// Note: default on if unspecified.
if ( $policyConfig['unsafeFallback'] ?? true ) {
// unsafe-inline should be ignored on browsers
// that support 'nonce-foo' sources.
// Some older versions of firefox don't follow this
// rule, but new browsers do. (Should be for at least
// firefox 40+).
$scriptSrc[] = "'unsafe-inline'";
}
// If default source option set to true or
// an array of urls, set a restrictive default-src.
// If set to false, we send a lenient default-src,
// see the code above where $defaultSrc is set initially.
if ( isset( $policyConfig['default-src'] )
&& $policyConfig['default-src'] !== false
) {
$defaultSrc = array_merge(
[ "'self'", 'data:', 'blob:' ],
$additionalSelfUrls
);
if ( is_array( $policyConfig['default-src'] ) ) {
foreach ( $policyConfig['default-src'] as $src ) {
$defaultSrc[] = $this->escapeUrlForCSP( $src );
}
}
}
if ( $policyConfig['includeCORS'] ?? true ) {
$CORSUrls = $this->getCORSSources();
if ( !in_array( '*', $defaultSrc ) ) {
$defaultSrc = array_merge( $defaultSrc, $CORSUrls );
}
// Unlikely to have * in scriptSrc, but doesn't
// hurt to check.
if ( !in_array( '*', $scriptSrc ) ) {
$scriptSrc = array_merge( $scriptSrc, $CORSUrls );
}
}
$defaultSrc = array_merge( $defaultSrc, $this->extraDefaultSrc );
$scriptSrc = array_merge( $scriptSrc, $this->extraScriptSrc );
$cssSrc = array_merge( $defaultSrc, $this->extraStyleSrc, [ "'unsafe-inline'" ] );
$this->hookRunner->onContentSecurityPolicyDefaultSource( $defaultSrc, $policyConfig, $mode );
$this->hookRunner->onContentSecurityPolicyScriptSource( $scriptSrc, $policyConfig, $mode );
if ( isset( $policyConfig['report-uri'] ) && $policyConfig['report-uri'] !== true ) {
if ( $policyConfig['report-uri'] === false ) {
$reportUri = false;
} else {
$reportUri = $this->escapeUrlForCSP( $policyConfig['report-uri'] );
}
} else {
$reportUri = $this->getReportUri( $mode );
}
// Only send an img-src, if we're sending a restrictive default.
if ( !is_array( $defaultSrc )
|| !in_array( '*', $defaultSrc )
|| !in_array( 'data:', $defaultSrc )
|| !in_array( 'blob:', $defaultSrc )
) {
// A future todo might be to make the allow options only
// add all the allowed sites to the header, instead of
// allowing all (Assuming there is a small number of sites).
// For now, the external image feature disables the limits
// CSP puts on external images.
if ( $mwConfig->get( 'AllowExternalImages' )
|| $mwConfig->get( 'AllowExternalImagesFrom' )
|| $mwConfig->get( 'AllowImageTag' )
) {
$imgSrc = [ '*', 'data:', 'blob:' ];
} elseif ( $mwConfig->get( 'EnableImageWhitelist' ) ) {
$whitelist = wfMessage( 'external_image_whitelist' )
->inContentLanguage()
->plain();
if ( preg_match( '/^\s*[^\s#]/m', $whitelist ) ) {
$imgSrc = [ '*', 'data:', 'blob:' ];
}
}
}
// Default value 'none'. true is none, false is nothing, string is single directive,
// array is list.
if ( !isset( $policyConfig['object-src'] ) || $policyConfig['object-src'] === true ) {
$objectSrc = [ "'none'" ];
} else {
$objectSrc = (array)( $policyConfig['object-src'] ?: [] );
}
$objectSrc = array_map( [ $this, 'escapeUrlForCSP' ], $objectSrc );
$directives = [];
if ( $scriptSrc ) {
$directives[] = 'script-src ' . implode( ' ', array_unique( $scriptSrc ) );
}
if ( $defaultSrc ) {
$directives[] = 'default-src ' . implode( ' ', array_unique( $defaultSrc ) );
}
if ( $cssSrc ) {
$directives[] = 'style-src ' . implode( ' ', array_unique( $cssSrc ) );
}
if ( $imgSrc ) {
$directives[] = 'img-src ' . implode( ' ', array_unique( $imgSrc ) );
}
if ( $objectSrc ) {
$directives[] = 'object-src ' . implode( ' ', $objectSrc );
}
if ( $reportUri ) {
$directives[] = 'report-uri ' . $reportUri;
}
$this->hookRunner->onContentSecurityPolicyDirectives( $directives, $policyConfig, $mode );
return implode( '; ', $directives );
}
/**
* Get the default report uri.
*
* @param int $mode self::*_MODE constant.
* @return string The URI to send reports to.
* @throws UnexpectedValueException if given invalid mode.
*/
private function getReportUri( $mode ) {
$apiArguments = [
'action' => 'cspreport',
'format' => 'json'
];
if ( $mode === self::REPORT_ONLY_MODE ) {
$apiArguments['reportonly'] = '1';
}
$reportUri = wfAppendQuery( wfScript( 'api' ), $apiArguments );
// Per spec, ';' and ',' must be hex-escaped in report URI
$reportUri = $this->escapeUrlForCSP( $reportUri );
return $reportUri;
}
/**
* Given a url, convert to form needed for CSP.
*
* Currently this does either scheme + host, or
* if protocol relative, just the host. Future versions
* could potentially preserve some of the path, if its determined
* that that would be a good idea.
*
* @note This does the extra escaping for CSP, but assumes the url
* has already had normal url escaping applied.
* @note This discards urls same as server name, as 'self' directive
* takes care of that.
* @param string $url
* @return string|bool Converted url or false on failure
*/
private function prepareUrlForCSP( $url ) {
$result = false;
if ( preg_match( '/^[a-z][a-z0-9+.-]*:$/i', $url ) ) {
// A schema source (e.g. blob: or data:)
return $url;
}
$bits = wfParseUrl( $url );
if ( !$bits && strpos( $url, '/' ) === false ) {
// probably something like example.com.
// try again protocol-relative.
$url = '//' . $url;
$bits = wfParseUrl( $url );
}
if ( $bits && isset( $bits['host'] )
&& $bits['host'] !== $this->mwConfig->get( 'ServerName' )
) {
$result = $bits['host'];
if ( $bits['scheme'] !== '' ) {
$result = $bits['scheme'] . $bits['delimiter'] . $result;
}
if ( isset( $bits['port'] ) ) {
$result .= ':' . $bits['port'];
}
$result = $this->escapeUrlForCSP( $result );
}
return $result;
}
/**
* @return array Additional sources for loading scripts from
*/
private function getAdditionalSelfUrlsScript() {
$additionalUrls = [];
// wgExtensionAssetsPath for ?debug=true mode
$pathVars = [ 'LoadScript', 'ExtensionAssetsPath', 'ResourceBasePath' ];
foreach ( $pathVars as $path ) {
$url = $this->mwConfig->get( $path );
$preparedUrl = $this->prepareUrlForCSP( $url );
if ( $preparedUrl ) {
$additionalUrls[] = $preparedUrl;
}
}
$RLSources = $this->mwConfig->get( 'ResourceLoaderSources' );
foreach ( $RLSources as $wiki => $sources ) {
foreach ( $sources as $id => $value ) {
$url = $this->prepareUrlForCSP( $value );
if ( $url ) {
$additionalUrls[] = $url;
}
}
}
return array_unique( $additionalUrls );
}
/**
* Get additional host names for the wiki (e.g. if static content loaded elsewhere)
*
* @note These are general load sources, not script sources
* @return string[] Array of other urls for wiki (for use in default-src)
*/
private function getAdditionalSelfUrls() {
// XXX on a foreign repo, the included description page can have anything on it,
// including inline scripts. But nobody does that.
// In principle, you can have even more complex configs... (e.g. The urlsByExt option)
$pathUrls = [];
$additionalSelfUrls = [];
// Future todo: The zone urls should never go into
// style-src. They should either be only in img-src, or if
// img-src unspecified they should be in default-src. Similarly,
// the DescriptionStylesheetUrl only needs to be in style-src
// (or default-src if style-src unspecified).
$callback = static function ( $repo, &$urls ) {
$urls[] = $repo->getZoneUrl( 'public' );
$urls[] = $repo->getZoneUrl( 'transcoded' );
$urls[] = $repo->getZoneUrl( 'thumb' );
$urls[] = $repo->getDescriptionStylesheetUrl();
};
$repoGroup = MediaWikiServices::getInstance()->getRepoGroup();
$localRepo = $repoGroup->getRepo( 'local' );
$callback( $localRepo, $pathUrls );
$repoGroup->forEachForeignRepo( $callback, [ &$pathUrls ] );
// Globals that might point to a different domain
$pathGlobals = [ 'LoadScript', 'ExtensionAssetsPath', 'StylePath', 'ResourceBasePath' ];
foreach ( $pathGlobals as $path ) {
$pathUrls[] = $this->mwConfig->get( $path );
}
foreach ( $pathUrls as $path ) {
$preparedUrl = $this->prepareUrlForCSP( $path );
if ( $preparedUrl !== false ) {
$additionalSelfUrls[] = $preparedUrl;
}
}
$RLSources = $this->mwConfig->get( 'ResourceLoaderSources' );
foreach ( $RLSources as $wiki => $sources ) {
foreach ( $sources as $id => $value ) {
$url = $this->prepareUrlForCSP( $value );
if ( $url ) {
$additionalSelfUrls[] = $url;
}
}
}
return array_unique( $additionalSelfUrls );
}
/**
* include domains that are allowed to send us CORS requests.
*
* Technically, $wgCrossSiteAJAXdomains lists things that are allowed to talk to us
* not things that we are allowed to talk to - but if something is allowed to talk to us,
* then there is a good chance that we should probably be allowed to talk to it.
*
* This is configurable with the 'includeCORS' key in the CSP config, and enabled
* by default.
* @note CORS domains with single character ('?') wildcards, are not included.
* @return array Additional hosts
*/
private function getCORSSources() {
$additionalUrls = [];
$CORSSources = $this->mwConfig->get( 'CrossSiteAJAXdomains' );
foreach ( $CORSSources as $source ) {
if ( strpos( $source, '?' ) !== false ) {
// CSP doesn't support single char wildcard
continue;
}
$url = $this->prepareUrlForCSP( $source );
if ( $url ) {
$additionalUrls[] = $url;
}
}
return $additionalUrls;
}
/**
* CSP spec says ',' and ';' are not allowed to appear in urls.
*
* @note This assumes that normal escaping has been applied to the url
* @param string $url URL (or possibly just part of one)
* @return string
*/
private function escapeUrlForCSP( $url ) {
return str_replace(
[ ';', ',' ],
[ '%3B', '%2C' ],
$url
);
}
/**
* Does this browser give false positive reports?
*
* Some versions of firefox (40-42) incorrectly report a csp
* violation for nonce sources, despite allowing them.
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1026520
* @param string $ua User-agent header
* @return bool
*/
public static function falsePositiveBrowser( $ua ) {
return (bool)preg_match( '!Firefox/4[0-2]\.!', $ua );
}
/**
* Should we set nonce attribute
*
* @param Config $config
* @return bool
*/
public static function isNonceRequired( Config $config ) {
$configs = [
$config->get( 'CSPHeader' ),
$config->get( 'CSPReportOnlyHeader' )
];
return self::isNonceRequiredArray( $configs );
}
/**
* Does a specific config require a nonce
*
* @param array $configs An array of CSP config arrays
* @return bool
*/
private static function isNonceRequiredArray( array $configs ) {
foreach ( $configs as $headerConfig ) {
if (
$headerConfig === true ||
( is_array( $headerConfig ) &&
!isset( $headerConfig['useNonces'] ) ) ||
( is_array( $headerConfig ) &&
isset( $headerConfig['useNonces'] ) &&
$headerConfig['useNonces'] )
) {
return true;
}
}
return false;
}
/**
* Get the nonce if nonce is in use
*
* @since 1.35
* @return bool|string A random (base64) string or false if not used.
*/
public function getNonce() {
if ( !self::isNonceRequired( $this->mwConfig ) ) {
return false;
}
if ( $this->nonce === null ) {
$rand = random_bytes( 15 );
$this->nonce = base64_encode( $rand );
}
return $this->nonce;
}
/**
* If possible you should use a more specific source type then default.
*
* So for example, if an extension added a special page that loaded something
* it might call $this->getOutput()->getCSP()->addDefaultSrc( '*.example.com' );
*
* @since 1.35
* @param string $source Source to add.
* e.g. blob:, *.example.com, %https://example.com, example.com/foo
*/
public function addDefaultSrc( $source ) {
$this->extraDefaultSrc[] = $this->prepareUrlForCSP( $source );
}
/**
* So for example, if an extension added a special page that loaded external CSS
* it might call $this->getOutput()->getCSP()->addStyleSrc( '*.example.com' );
*
* @since 1.35
* @param string $source Source to add.
* e.g. blob:, *.example.com, %https://example.com, example.com/foo
*/
public function addStyleSrc( $source ) {
$this->extraStyleSrc[] = $this->prepareUrlForCSP( $source );
}
/**
* So for example, if an extension added a special page that loaded something
* it might call $this->getOutput()->getCSP()->addScriptSrc( '*.example.com' );
*
* @since 1.35
* @warning Be careful including external scripts, as they can take over accounts.
* @param string $source Source to add.
* e.g. blob:, *.example.com, %https://example.com, example.com/foo
*/
public function addScriptSrc( $source ) {
$this->extraScriptSrc[] = $this->prepareUrlForCSP( $source );
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,316 @@
<?php
/**
* A few constants that might be needed during LocalSettings.php.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
require_once __DIR__ . '/libs/mime/defines.php';
require_once __DIR__ . '/libs/rdbms/defines.php';
use Wikimedia\Rdbms\IDatabase;
/**
* The running version of MediaWiki.
*
* This replaces the $wgVersion global found in earlier versions. When updating,
* remember to also bump the stand-alone duplicate of this in PHPVersionCheck.
*
* @since 1.35 (also backported to 1.33.3 and 1.34.1)
*/
define( 'MW_VERSION', '1.38.4' );
/** @{
* Obsolete IDatabase::makeList() constants
* These are also available as Database class constants
*/
define( 'LIST_COMMA', IDatabase::LIST_COMMA );
define( 'LIST_AND', IDatabase::LIST_AND );
define( 'LIST_SET', IDatabase::LIST_SET );
define( 'LIST_NAMES', IDatabase::LIST_NAMES );
define( 'LIST_OR', IDatabase::LIST_OR );
/** @} */
/** @{
* Virtual namespaces; don't appear in the page database
*/
define( 'NS_MEDIA', -2 );
define( 'NS_SPECIAL', -1 );
/** @} */
/** @{
* Real namespaces
*
* Number 100 and beyond are reserved for custom namespaces;
* DO NOT assign standard namespaces at 100 or beyond.
* DO NOT Change integer values as they are most probably hardcoded everywhere
* see T2696 which talked about that.
*/
define( 'NS_MAIN', 0 );
define( 'NS_TALK', 1 );
define( 'NS_USER', 2 );
define( 'NS_USER_TALK', 3 );
define( 'NS_PROJECT', 4 );
define( 'NS_PROJECT_TALK', 5 );
define( 'NS_FILE', 6 );
define( 'NS_FILE_TALK', 7 );
define( 'NS_MEDIAWIKI', 8 );
define( 'NS_MEDIAWIKI_TALK', 9 );
define( 'NS_TEMPLATE', 10 );
define( 'NS_TEMPLATE_TALK', 11 );
define( 'NS_HELP', 12 );
define( 'NS_HELP_TALK', 13 );
define( 'NS_CATEGORY', 14 );
define( 'NS_CATEGORY_TALK', 15 );
/** @} */
/** @{
* Cache type
*/
define( 'CACHE_ANYTHING', -1 ); // Use anything, as long as it works
define( 'CACHE_NONE', 0 ); // Do not cache
define( 'CACHE_DB', 1 ); // Store cache objects in the DB
define( 'CACHE_MEMCACHED', 'memcached-php' ); // Backwards-compatability alias for Memcached
define( 'CACHE_ACCEL', 3 ); // APC or WinCache
/** @} */
/** @{
* Antivirus result codes, for use in $wgAntivirusSetup.
*/
define( 'AV_NO_VIRUS', 0 ); # scan ok, no virus found
define( 'AV_VIRUS_FOUND', 1 ); # virus found!
define( 'AV_SCAN_ABORTED', -1 ); # scan aborted, the file is probably immune
define( 'AV_SCAN_FAILED', false ); # scan failed (scanner not found or error in scanner)
/** @} */
/** @{
* Date format selectors; used in user preference storage and by
* Language::date() and co.
*/
define( 'MW_DATE_DEFAULT', 'default' );
define( 'MW_DATE_MDY', 'mdy' );
define( 'MW_DATE_DMY', 'dmy' );
define( 'MW_DATE_YMD', 'ymd' );
define( 'MW_DATE_ISO', 'ISO 8601' );
/** @} */
/** @{
* RecentChange type identifiers
*/
define( 'RC_EDIT', 0 );
define( 'RC_NEW', 1 );
define( 'RC_LOG', 3 );
define( 'RC_EXTERNAL', 5 );
define( 'RC_CATEGORIZE', 6 );
/** @} */
/** @{
* Article edit flags
*/
define( 'EDIT_NEW', 1 );
define( 'EDIT_UPDATE', 2 );
define( 'EDIT_MINOR', 4 );
define( 'EDIT_SUPPRESS_RC', 8 );
define( 'EDIT_FORCE_BOT', 16 );
define( 'EDIT_DEFER_UPDATES', 32 ); // Unused since 1.27
define( 'EDIT_AUTOSUMMARY', 64 );
define( 'EDIT_INTERNAL', 128 );
/** @} */
/** @{
* Hook support constants
*/
define( 'MW_SUPPORTS_PARSERFIRSTCALLINIT', 1 );
define( 'MW_SUPPORTS_LOCALISATIONCACHE', 1 );
define( 'MW_SUPPORTS_CONTENTHANDLER', 1 );
define( 'MW_EDITFILTERMERGED_SUPPORTS_API', 1 );
/** @} */
/** Support for $wgResourceModules */
define( 'MW_SUPPORTS_RESOURCE_MODULES', 1 );
/** @{
* Allowed values for Parser::$mOutputType
* Parameter to Parser::startExternalParse().
* Use of Parser consts is preferred:
* - Parser::OT_HTML
* - Parser::OT_WIKI
* - Parser::OT_PREPROCESS
* - Parser::OT_MSG
* - Parser::OT_PLAIN
*/
define( 'OT_HTML', 1 );
define( 'OT_WIKI', 2 );
define( 'OT_PREPROCESS', 3 );
define( 'OT_MSG', 3 ); // b/c alias for OT_PREPROCESS
define( 'OT_PLAIN', 4 );
/** @} */
/** @{
* Flags for Parser::setFunctionHook
* Use of Parser consts is preferred:
* - Parser::SFH_NO_HASH
* - Parser::SFH_OBJECT_ARGS
*/
define( 'SFH_NO_HASH', 1 );
define( 'SFH_OBJECT_ARGS', 2 );
/** @} */
/** @{
* Autopromote conditions
*/
define( 'APCOND_EDITCOUNT', 1 );
define( 'APCOND_AGE', 2 );
define( 'APCOND_EMAILCONFIRMED', 3 );
define( 'APCOND_INGROUPS', 4 );
define( 'APCOND_ISIP', 5 );
define( 'APCOND_IPINRANGE', 6 );
define( 'APCOND_AGE_FROM_EDIT', 7 );
define( 'APCOND_BLOCKED', 8 );
define( 'APCOND_ISBOT', 9 );
/** @} */
/** @{
* Protocol constants for wfExpandUrl()
*/
define( 'PROTO_HTTP', 'http://' );
define( 'PROTO_HTTPS', 'https://' );
define( 'PROTO_RELATIVE', '//' );
define( 'PROTO_CURRENT', null );
define( 'PROTO_CANONICAL', 1 );
define( 'PROTO_INTERNAL', 2 );
/** @} */
/** @{
* Content model ids, used by Content and ContentHandler.
* These IDs will be exposed in the API and XML dumps.
*
* Extensions that define their own content model IDs should take
* care to avoid conflicts. Using the extension name as a prefix is recommended,
* for example 'myextension-somecontent'.
*/
define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' );
define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
define( 'CONTENT_MODEL_CSS', 'css' );
define( 'CONTENT_MODEL_TEXT', 'text' );
define( 'CONTENT_MODEL_JSON', 'json' );
define( 'CONTENT_MODEL_UNKNOWN', 'unknown' );
/** @} */
/** @{
* Content formats, used by Content and ContentHandler.
* These should be MIME types, and will be exposed in the API and XML dumps.
*
* Extensions are free to use the below formats, or define their own.
* It is recommended to stick with the conventions for MIME types.
*/
/** Wikitext */
define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' );
/** For JS pages */
define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' );
/** For CSS pages */
define( 'CONTENT_FORMAT_CSS', 'text/css' );
/** For future use, e.g. with some plain HTML messages. */
define( 'CONTENT_FORMAT_TEXT', 'text/plain' );
/** For future use, e.g. with some plain HTML messages. */
define( 'CONTENT_FORMAT_HTML', 'text/html' );
/** For future use with the API and for extensions */
define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' );
/** For future use with the API, and for use by extensions */
define( 'CONTENT_FORMAT_JSON', 'application/json' );
/** For future use with the API, and for use by extensions */
define( 'CONTENT_FORMAT_XML', 'application/xml' );
/** @} */
/** @{
* Max string length for shell invocations; based on binfmts.h
*/
define( 'SHELL_MAX_ARG_STRLEN', '100000' );
/** @} */
/** @{
* Schema compatibility flags.
*
* Used as flags in a bit field that indicates whether the old or new schema (or both)
* are read or written.
*
* - SCHEMA_COMPAT_WRITE_OLD: Whether information is written to the old schema.
* - SCHEMA_COMPAT_READ_OLD: Whether information stored in the old schema is read.
* - SCHEMA_COMPAT_WRITE_TEMP: Whether information is written to a temporary
* intermediate schema.
* - SCHEMA_COMPAT_READ_TEMP: Whether information is read from the temporary
* intermediate schema.
* - SCHEMA_COMPAT_WRITE_NEW: Whether information is written to the new schema
* - SCHEMA_COMPAT_READ_NEW: Whether information is read from the new schema
*/
define( 'SCHEMA_COMPAT_WRITE_OLD', 0x01 );
define( 'SCHEMA_COMPAT_READ_OLD', 0x02 );
define( 'SCHEMA_COMPAT_WRITE_TEMP', 0x10 );
define( 'SCHEMA_COMPAT_READ_TEMP', 0x20 );
define( 'SCHEMA_COMPAT_WRITE_NEW', 0x100 );
define( 'SCHEMA_COMPAT_READ_NEW', 0x200 );
define( 'SCHEMA_COMPAT_WRITE_MASK',
SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW );
define( 'SCHEMA_COMPAT_READ_MASK',
SCHEMA_COMPAT_READ_OLD | SCHEMA_COMPAT_READ_TEMP | SCHEMA_COMPAT_READ_NEW );
define( 'SCHEMA_COMPAT_WRITE_BOTH', SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_WRITE_NEW );
define( 'SCHEMA_COMPAT_WRITE_OLD_AND_TEMP', SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_WRITE_TEMP );
define( 'SCHEMA_COMPAT_WRITE_TEMP_AND_NEW', SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_WRITE_NEW );
define( 'SCHEMA_COMPAT_READ_BOTH', SCHEMA_COMPAT_READ_OLD | SCHEMA_COMPAT_READ_NEW );
define( 'SCHEMA_COMPAT_OLD', SCHEMA_COMPAT_WRITE_OLD | SCHEMA_COMPAT_READ_OLD );
define( 'SCHEMA_COMPAT_TEMP', SCHEMA_COMPAT_WRITE_TEMP | SCHEMA_COMPAT_READ_TEMP );
define( 'SCHEMA_COMPAT_NEW', SCHEMA_COMPAT_WRITE_NEW | SCHEMA_COMPAT_READ_NEW );
/** @} */
/** @{
* Schema change migration flags.
*
* Used as values of a feature flag for an orderly transition from an old
* schema to a new schema. The numeric values of these constants are compatible with the
* SCHEMA_COMPAT_XXX bitfield semantics. High bits are used to ensure that the numeric
* ordering follows the order in which the migration stages should be used.
*
* - MIGRATION_OLD: Only read and write the old schema. The new schema need not
* even exist. This is used from when the patch is merged until the schema
* change is actually applied to the database.
* - MIGRATION_WRITE_BOTH: Write both the old and new schema. Read the new
* schema preferentially, falling back to the old. This is used while the
* change is being tested, allowing easy roll-back to the old schema.
* - MIGRATION_WRITE_NEW: Write only the new schema. Read the new schema
* preferentially, falling back to the old. This is used while running the
* maintenance script to migrate existing entries in the old schema to the
* new schema.
* - MIGRATION_NEW: Only read and write the new schema. The old schema (and the
* feature flag) may now be removed.
*/
define( 'MIGRATION_OLD', 0x00000000 | SCHEMA_COMPAT_OLD );
define( 'MIGRATION_WRITE_BOTH', 0x10000000 | SCHEMA_COMPAT_READ_BOTH | SCHEMA_COMPAT_WRITE_BOTH );
define( 'MIGRATION_WRITE_NEW', 0x20000000 | SCHEMA_COMPAT_READ_BOTH | SCHEMA_COMPAT_WRITE_NEW );
define( 'MIGRATION_NEW', 0x30000000 | SCHEMA_COMPAT_NEW );
/** @} */
/** @{
* XML dump schema versions, for use with XmlDumpWriter.
* See also the corresponding export-nnnn.xsd files in the docs directory,
* which are also listed at <https://www.mediawiki.org/xml/>.
* Note that not all old schema versions are represented here, as several
* were already unsupported at the time these constants were introduced.
*/
define( 'XML_DUMP_SCHEMA_VERSION_10', '0.10' );
define( 'XML_DUMP_SCHEMA_VERSION_11', '0.11' );
/** @} */

View File

@ -0,0 +1,61 @@
<?php
/**
* Delayed loading of deprecated global objects.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Class to allow throwing wfDeprecated warnings
* when people use globals that we do not want them to.
*/
class DeprecatedGlobal extends StubObject {
protected $version;
/**
* @param string $name Global name
* @param callable|string $callback Factory function or class name to construct
* @param string|false $version Version global was deprecated in
*/
public function __construct( $name, $callback, $version = false ) {
parent::__construct( $name, $callback );
$this->version = $version;
}
// phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
public function _newObject() {
/*
* Put the caller offset for wfDeprecated as 6, as
* that gives the function that uses this object, since:
*
* 1 = this function ( _newObject )
* 2 = StubObject::_unstub
* 3 = StubObject::_call
* 4 = StubObject::__call
* 5 = DeprecatedGlobal::<method of global called>
* 6 = Actual function using the global.
* (the same applies to _get/__get or _set/__set instead of _call/__call)
*
* Of course its theoretically possible to have other call
* sequences for this method, but that seems to be
* rather unlikely.
*/
wfDeprecated( '$' . $this->global, $this->version, false, 6 );
return parent::_newObject();
}
}

View File

@ -0,0 +1,101 @@
<?php
/**
* Deal with importing all those nasty globals and things
*
* Copyright © 2003 Brion Vibber <brion@pobox.com>
* https://www.mediawiki.org/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Similar to FauxRequest, but only fakes URL parameters and method
* (POST or GET) and use the base request for the remaining stuff
* (cookies, session and headers).
*
* @newable
*
* @ingroup HTTP
* @since 1.19
*/
class DerivativeRequest extends FauxRequest {
private $base;
private $ip;
/**
* @stable to call
*
* @param WebRequest $base
* @param array $data Array of *non*-urlencoded key => value pairs, the
* fake GET/POST values
* @param bool $wasPosted Whether to treat the data as POST
*/
public function __construct( WebRequest $base, $data, $wasPosted = false ) {
$this->base = $base;
parent::__construct( $data, $wasPosted );
}
public function getCookie( $key, $prefix = null, $default = null ) {
return $this->base->getCookie( $key, $prefix, $default );
}
public function getHeader( $name, $flags = 0 ) {
return $this->base->getHeader( $name, $flags );
}
public function getAllHeaders() {
return $this->base->getAllHeaders();
}
public function getSession() {
return $this->base->getSession();
}
public function getSessionData( $key ) {
return $this->base->getSessionData( $key );
}
public function setSessionData( $key, $data ) {
$this->base->setSessionData( $key, $data );
}
public function getAcceptLang() {
return $this->base->getAcceptLang();
}
public function getIP() {
return $this->ip ?: $this->base->getIP();
}
public function setIP( $ip ) {
$this->ip = $ip;
}
public function getProtocol() {
return $this->base->getProtocol();
}
public function getUpload( $key ) {
// @phan-suppress-next-line PhanTypeMismatchReturnSuperType
return $this->base->getUpload( $key );
}
public function getElapsedTime() {
return $this->base->getElapsedTime();
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* Extra settings useful for MediaWiki development.
*
* To enable built-in debug and development settings, add the
* following to your LocalSettings.php file.
*
* require "$IP/includes/DevelopmentSettings.php";
*
* Alternatively, if running phpunit.php (or another Maintenance script),
* you can use the --mwdebug option to automatically load these settings.
*
* @file
*/
/**
* Debugging for PHP
*/
// Enable showing of errors
error_reporting( -1 );
ini_set( 'display_errors', 1 );
/**
* Debugging for MediaWiki
*/
global $wgDevelopmentWarnings, $wgShowExceptionDetails, $wgShowHostnames,
$wgDebugRawPage, $wgCommandLineMode, $wgDebugLogFile,
$wgDBerrorLog, $wgDebugLogGroups;
// Use of wfWarn() should cause tests to fail
$wgDevelopmentWarnings = true;
// Enable showing of errors
$wgShowExceptionDetails = true;
$wgShowHostnames = true;
$wgDebugRawPage = true; // T49960
// Enable log files
$logDir = getenv( 'MW_LOG_DIR' );
if ( $logDir ) {
if ( $wgCommandLineMode ) {
$wgDebugLogFile = "$logDir/mw-debug-cli.log";
} else {
$wgDebugLogFile = "$logDir/mw-debug-www.log";
}
$wgDBerrorLog = "$logDir/mw-dberror.log";
$wgDebugLogGroups['ratelimit'] = "$logDir/mw-ratelimit.log";
$wgDebugLogGroups['error'] = "$logDir/mw-error.log";
$wgDebugLogGroups['exception'] = "$logDir/mw-error.log";
}
unset( $logDir );
/**
* Make testing possible (or easier)
*/
global $wgRateLimits, $wgEnableJavaScriptTest, $wgRestAPIAdditionalRouteFiles,
$wgDeferredUpdateStrategy;
// Set almost infinite rate limits. This allows integration tests to run unthrottled
// in CI and for devs locally (T225796), but doesn't turn a large chunk of production
// code completely off during testing (T284804)
foreach ( $wgRateLimits as $right => &$limit ) {
foreach ( $limit as $group => &$groupLimit ) {
$groupLimit[0] = PHP_INT_MAX;
}
}
// Enable Special:JavaScriptTest and allow `npm run qunit` to work
// https://www.mediawiki.org/wiki/Manual:JavaScript_unit_testing
$wgEnableJavaScriptTest = true;
// Enable development/experimental endpoints
$wgRestAPIAdditionalRouteFiles = [ 'includes/Rest/coreDevelopmentRoutes.json' ];
// Greatly raise the limits on short/long term login attempts,
// so that automated tests run in parallel don't error.
$wgPasswordAttemptThrottle = [
[ 'count' => 1000, 'seconds' => 300 ],
[ 'count' => 100000, 'seconds' => 60 * 60 * 48 ],
];
// Run deferred updates before sending a response to the client.
// This ensures that in end-to-end tests, a GET request will see the
// effect of all previous POST requests (T230211).
// Caveat: this does not wait for jobs to be executed, and it does
// not wait for database replication to complete.
$wgForceDeferredUpdatesPreSend = true;
/**
* Experimental changes that may later become the default.
* (Must reference a Phabricator ticket)
*/
global $wgSQLMode, $wgLocalisationCacheConf,
$wgCacheDirectory, $wgEnableUploads, $wgCiteBookReferencing;
// Enable MariaDB/MySQL strict mode (T108255)
$wgSQLMode = 'TRADITIONAL';
// Localisation Cache to StaticArray (T218207)
$wgLocalisationCacheConf['store'] = 'array';
// Experimental Book Referencing feature (T236255)
$wgCiteBookReferencing = true;
// The default value is false, but for development it is useful to set this to the system temp
// directory by default (T218207)
$wgCacheDirectory = TempFSFile::getUsableTempDirectory() .
DIRECTORY_SEPARATOR .
rawurlencode( WikiMap::getCurrentWikiId() );
// Enable uploads for FileImporter browser tests (T190829)
$wgEnableUploads = true;
// Enable the new wikitext mode for browser testing (T270240)
$wgVisualEditorEnableWikitext = true;
// Currently the default, but repeated here for safety since it would break many source editor tests.
$wgDefaultUserOptions['visualeditor-newwikitext'] = 0;

View File

@ -0,0 +1,443 @@
<?php
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionRecord;
/**
* @since 1.18
*/
class DummyLinker {
public function link(
$target,
$html = null,
$customAttribs = [],
$query = [],
$options = []
) {
return Linker::link(
$target,
$html,
$customAttribs,
$query,
$options
);
}
public function linkKnown(
$target,
$html = null,
$customAttribs = [],
$query = [],
$options = [ 'known' ]
) {
return Linker::linkKnown(
$target,
$html,
$customAttribs,
$query,
$options
);
}
public function makeSelfLinkObj(
$nt,
$html = '',
$query = '',
$trail = '',
$prefix = ''
) {
return Linker::makeSelfLinkObj(
$nt,
$html,
$query,
$trail,
$prefix
);
}
public function getInvalidTitleDescription(
IContextSource $context,
$namespace,
$title
) {
return Linker::getInvalidTitleDescription(
$context,
$namespace,
$title
);
}
public function normaliseSpecialPage( Title $title ) {
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
return $linkRenderer->normalizeTarget( $title );
}
public function makeExternalImage( $url, $alt = '' ) {
return Linker::makeExternalImage( $url, $alt );
}
public function makeImageLink(
Parser $parser,
Title $title,
$file,
$frameParams = [],
$handlerParams = [],
$time = false,
$query = "",
$widthOption = null
) {
return Linker::makeImageLink(
$parser,
$title,
$file,
$frameParams,
$handlerParams,
$time,
$query,
$widthOption
);
}
public function makeThumbLinkObj(
Title $title,
$file,
$label = '',
$alt = '',
$align = 'right',
$params = [],
$framed = false,
$manualthumb = ""
) {
return Linker::makeThumbLinkObj(
$title,
$file,
$label,
$alt,
$align,
$params,
$framed,
$manualthumb
);
}
public function makeThumbLink2(
Title $title,
$file,
$frameParams = [],
$handlerParams = [],
$time = false,
$query = ""
) {
return Linker::makeThumbLink2(
$title,
$file,
$frameParams,
$handlerParams,
$time,
$query
);
}
public function processResponsiveImages( $file, $thumb, $hp ) {
Linker::processResponsiveImages(
$file,
$thumb,
$hp
);
}
public function makeBrokenImageLinkObj(
$title,
$label = '',
$query = '',
$unused1 = '',
$unused2 = '',
$time = false
) {
return Linker::makeBrokenImageLinkObj(
$title,
$label,
$query,
$unused1,
$unused2,
$time
);
}
public function makeMediaLinkObj( $title, $html = '', $time = false ) {
return Linker::makeMediaLinkObj(
$title,
$html,
$time
);
}
public function makeMediaLinkFile( Title $title, $file, $html = '' ) {
return Linker::makeMediaLinkFile(
$title,
$file,
$html
);
}
public function specialLink( $name, $key = '' ) {
return Linker::specialLink( $name, $key );
}
public function makeExternalLink(
$url,
$text,
$escape = true,
$linktype = '',
$attribs = [],
$title = null
) {
return Linker::makeExternalLink(
$url,
$text,
$escape,
$linktype,
$attribs,
$title
);
}
public function userLink( $userId, $userName, $altUserName = false ) {
return Linker::userLink(
$userId,
$userName,
$altUserName
);
}
public function userToolLinks(
$userId,
$userText,
$redContribsWhenNoEdits = false,
$flags = 0,
$edits = null
) {
return Linker::userToolLinks(
$userId,
$userText,
$redContribsWhenNoEdits,
$flags,
$edits
);
}
public function userToolLinksRedContribs( $userId, $userText, $edits = null ) {
return Linker::userToolLinksRedContribs(
$userId,
$userText,
$edits
);
}
public function userTalkLink( $userId, $userText ) {
return Linker::userTalkLink( $userId, $userText );
}
public function blockLink( $userId, $userText ) {
return Linker::blockLink( $userId, $userText );
}
public function emailLink( $userId, $userText ) {
return Linker::emailLink( $userId, $userText );
}
public function revUserLink( RevisionRecord $revRecord, $isPublic = false ) {
return Linker::revUserLink( $revRecord, $isPublic );
}
public function revUserTools( RevisionRecord $revRecord, $isPublic = false ) {
return Linker::revUserTools( $revRecord, $isPublic );
}
public function formatComment(
$comment,
$title = null,
$local = false,
$wikiId = null
) {
return Linker::formatComment(
$comment,
$title,
$local,
$wikiId
);
}
public function formatLinksInComment(
$comment,
$title = null,
$local = false,
$wikiId = null
) {
return Linker::formatLinksInComment(
$comment,
$title,
$local,
$wikiId
);
}
public function normalizeSubpageLink( $contextTitle, $target, &$text ) {
return Linker::normalizeSubpageLink(
$contextTitle,
$target,
$text
);
}
public function commentBlock(
$comment,
$title = null,
$local = false,
$wikiId = null
) {
return Linker::commentBlock(
$comment,
$title,
$local,
$wikiId
);
}
public function revComment( RevisionRecord $revRecord, $local = false, $isPublic = false ) {
return Linker::revComment( $revRecord, $local, $isPublic );
}
public function formatRevisionSize( $size ) {
return Linker::formatRevisionSize( $size );
}
public function tocIndent() {
return Linker::tocIndent();
}
public function tocUnindent( $level ) {
return Linker::tocUnindent( $level );
}
public function tocLine( $anchor, $tocline, $tocnumber, $level, $sectionIndex = false ) {
return Linker::tocLine(
$anchor,
$tocline,
$tocnumber,
$level,
$sectionIndex
);
}
public function tocLineEnd() {
return Linker::tocLineEnd();
}
public function tocList( $toc, Language $lang = null ) {
return Linker::tocList( $toc, $lang );
}
public function generateTOC( $tree, Language $lang = null ) {
return Linker::generateTOC( $tree, $lang );
}
public function makeHeadline(
$level,
$attribs,
$anchor,
$html,
$link,
$legacyAnchor = false
) {
return Linker::makeHeadline(
$level,
$attribs,
$anchor,
$html,
$link,
$legacyAnchor
);
}
public function splitTrail( $trail ) {
return Linker::splitTrail( $trail );
}
public function generateRollback(
RevisionRecord $revRecord,
IContextSource $context = null,
$options = [ 'verify' ]
) {
return Linker::generateRollback(
$revRecord,
$context,
$options
);
}
public function getRollbackEditCount( RevisionRecord $revRecord, $verify ) {
return Linker::getRollbackEditCount( $revRecord, $verify );
}
public function buildRollbackLink(
RevisionRecord $revRecord,
IContextSource $context = null,
$editCount = false
) {
return Linker::buildRollbackLink(
$revRecord,
$context,
$editCount
);
}
public function formatHiddenCategories( $hiddencats ) {
return Linker::formatHiddenCategories( $hiddencats );
}
public function titleAttrib( $name, $options = null, array $msgParams = [] ) {
return Linker::titleAttrib(
$name,
$options,
$msgParams
);
}
public function accesskey( $name ) {
return Linker::accesskey( $name );
}
public function getRevDeleteLink( User $user, RevisionRecord $revRecord, Title $title ) {
return Linker::getRevDeleteLink(
$user,
$revRecord,
$title
);
}
public function revDeleteLink( $query = [], $restricted = false, $delete = true ) {
return Linker::revDeleteLink(
$query,
$restricted,
$delete
);
}
public function revDeleteLinkDisabled( $delete = true ) {
return Linker::revDeleteLinkDisabled( $delete );
}
public function tooltipAndAccesskeyAttribs( $name, array $msgParams = [] ) {
return Linker::tooltipAndAccesskeyAttribs(
$name,
$msgParams
);
}
public function tooltip( $name, $options = null ) {
return Linker::tooltip( $name, $options );
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
<?php
namespace MediaWiki;
/**
* An interface to check for emptiness of an object
*/
interface Emptiable {
/**
* Check if object is empty
* @return bool Is it empty
*/
public function isEmpty();
}

View File

@ -0,0 +1,62 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Factory class for spawning EventRelayer objects using configuration
*
* @since 1.27
*/
class EventRelayerGroup {
/** @var array[] */
protected $configByChannel = [];
/** @var EventRelayer[] */
protected $relayers = [];
/**
* @param array[] $config Channel configuration
*/
public function __construct( array $config ) {
$this->configByChannel = $config;
}
/**
* @param string $channel
* @return EventRelayer Relayer instance that handles the given channel
*/
public function getRelayer( $channel ) {
$channelKey = isset( $this->configByChannel[$channel] )
? $channel
: 'default';
if ( !isset( $this->relayers[$channelKey] ) ) {
if ( !isset( $this->configByChannel[$channelKey] ) ) {
throw new UnexpectedValueException( "No config for '$channelKey'" );
}
$config = $this->configByChannel[$channelKey];
$class = $config['class'];
$this->relayers[$channelKey] = new $class( $config );
}
return $this->relayers[$channelKey];
}
}

View File

@ -0,0 +1,66 @@
<?php
namespace MediaWiki;
/**
* @since 1.35
*/
class ExtensionInfo {
/**
* Obtains the full path of a AUTHORS or CREDITS file if one exists.
*
* @param string $dir Path to the root directory
*
* @since 1.35
*
* @return bool|string False if no such file exists, otherwise returns
* a path to it.
*/
public static function getAuthorsFileName( $dir ) {
if ( !$dir ) {
return false;
}
foreach ( scandir( $dir ) as $file ) {
$fullPath = $dir . DIRECTORY_SEPARATOR . $file;
if ( preg_match( '/^(AUTHORS|CREDITS)(\.txt|\.wiki|\.mediawiki)?$/', $file ) &&
is_readable( $fullPath ) &&
is_file( $fullPath )
) {
return $fullPath;
}
}
return false;
}
/**
* Obtains the full paths of COPYING or LICENSE files if they exist.
*
* @param string $extDir Path to the extensions root directory
*
* @since 1.35
*
* @return string[] Returns an array of zero or more paths.
*/
public static function getLicenseFileNames( $extDir ) {
if ( !$extDir ) {
return [];
}
$licenseFiles = [];
foreach ( scandir( $extDir ) as $file ) {
$fullPath = $extDir . DIRECTORY_SEPARATOR . $file;
// Allow files like GPL-COPYING and MIT-LICENSE
if ( preg_match( '/^([\w\.-]+)?(COPYING|LICENSE)(\.txt)?$/', $file ) &&
is_readable( $fullPath ) &&
is_file( $fullPath )
) {
$licenseFiles[] = $fullPath;
}
}
return $licenseFiles;
}
}

View File

@ -0,0 +1,290 @@
<?php
/**
* Deal with importing all those nasty globals and things
*
* Copyright © 2003 Brion Vibber <brion@pobox.com>
* https://www.mediawiki.org/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Session\SessionManager;
/**
* WebRequest clone which takes values from a provided array.
*
* @newable
*
* @ingroup HTTP
*/
class FauxRequest extends WebRequest {
private $wasPosted = false;
private $requestUrl;
protected $cookies = [];
/** @var array */
private $uploadData = [];
/**
* @stable to call
*
* @param array $data Array of *non*-urlencoded key => value pairs, the
* fake GET/POST values
* @param bool $wasPosted Whether to treat the data as POST
* @param MediaWiki\Session\Session|array|null $session Session, session
* data array, or null
* @param string $protocol 'http' or 'https'
* @throws MWException
*/
public function __construct( $data = [], $wasPosted = false,
$session = null, $protocol = 'http'
) {
$this->requestTime = microtime( true );
if ( is_array( $data ) ) {
$this->data = $data;
} else {
throw new MWException( "FauxRequest() got bogus data" );
}
$this->wasPosted = $wasPosted;
if ( $session instanceof MediaWiki\Session\Session ) {
$this->sessionId = $session->getSessionId();
} elseif ( is_array( $session ) ) {
$mwsession = SessionManager::singleton()->getEmptySession( $this );
$this->sessionId = $mwsession->getSessionId();
foreach ( $session as $key => $value ) {
$mwsession->set( $key, $value );
}
} elseif ( $session !== null ) {
throw new MWException( "FauxRequest() got bogus session" );
}
$this->protocol = $protocol;
}
/**
* Initialise the header list
*/
protected function initHeaders() {
// Nothing to init
}
/**
* @param string $name
* @param string $default
* @return string
*/
public function getText( $name, $default = '' ) {
# Override; don't recode since we're using internal data
return (string)$this->getVal( $name, $default );
}
/**
* @return array
*/
public function getQueryValues() {
if ( $this->wasPosted ) {
return [];
} else {
return $this->data;
}
}
public function getMethod() {
return $this->wasPosted ? 'POST' : 'GET';
}
/**
* @return bool
*/
public function wasPosted() {
return $this->wasPosted;
}
public function getCookie( $key, $prefix = null, $default = null ) {
if ( $prefix === null ) {
$cookiePrefix = MediaWikiServices::getInstance()->getMainConfig()->get( 'CookiePrefix' );
$prefix = $cookiePrefix;
}
$name = $prefix . $key;
return $this->cookies[$name] ?? $default;
}
/**
* @since 1.26
* @param string $key Unprefixed name of the cookie to set
* @param string|null $value Value of the cookie to set
* @param string|null $prefix Cookie prefix. Defaults to $wgCookiePrefix
*/
public function setCookie( $key, $value, $prefix = null ) {
$this->setCookies( [ $key => $value ], $prefix );
}
/**
* @since 1.26
* @param array $cookies
* @param string|null $prefix Cookie prefix. Defaults to $wgCookiePrefix
*/
public function setCookies( $cookies, $prefix = null ) {
if ( $prefix === null ) {
$cookiePrefix = MediaWikiServices::getInstance()->getMainConfig()->get( 'CookiePrefix' );
$prefix = $cookiePrefix;
}
foreach ( $cookies as $key => $value ) {
$name = $prefix . $key;
$this->cookies[$name] = $value;
}
}
/**
* Set fake upload data for all files
*
* @since 1.37
* @param (array|WebRequestUpload)[] $uploadData
*/
public function setUploadData( $uploadData ) {
foreach ( $uploadData as $key => $data ) {
$this->setUpload( $key, $data );
}
}
/**
* Set fake upload data for one file with specific key
*
* @since 1.37
* @param string $key
* @param array|WebRequestUpload $data
*/
public function setUpload( $key, $data ) {
if ( $data instanceof WebRequestUpload ) {
// cannot reuse WebRequestUpload, because it contains the original web request object
$data = [
'name' => $data->getName(),
'type' => $data->getType(),
'tmp_name' => $data->getTempName(),
'size' => $data->getSize(),
'error' => $data->getError(),
];
}
// Check if everything is provided
if ( !is_array( $data ) ||
array_diff( WebRequestUpload::REQUIRED_FILEINFO_KEYS, array_keys( $data ) ) !== []
) {
throw new MWException( __METHOD__ . ' got bogus data' );
}
$this->uploadData[$key] = $data;
}
/**
* Return a FauxRequestUpload object corresponding to the key
*
* @param string $key
* @return FauxRequestUpload
*/
public function getUpload( $key ) {
return new FauxRequestUpload( $this->uploadData, $this, $key );
}
/**
* @since 1.25
* @param string $url
*/
public function setRequestURL( $url ) {
$this->requestUrl = $url;
}
/**
* @since 1.25 MWException( "getRequestURL not implemented" )
* no longer thrown.
* @return string
*/
public function getRequestURL() {
if ( $this->requestUrl === null ) {
throw new MWException( 'Request URL not set' );
}
return $this->requestUrl;
}
public function getProtocol() {
return $this->protocol;
}
/**
* @param string $name
* @param string $val
*/
public function setHeader( $name, $val ) {
$this->setHeaders( [ $name => $val ] );
}
/**
* @since 1.26
* @param array $headers
*/
public function setHeaders( $headers ) {
foreach ( $headers as $name => $val ) {
$name = strtoupper( $name );
$this->headers[$name] = $val;
}
}
/**
* @return array|null
*/
public function getSessionArray() {
if ( $this->sessionId !== null ) {
return iterator_to_array( $this->getSession() );
}
return null;
}
public function getPostValues() {
return $this->wasPosted ? $this->data : [];
}
/**
* FauxRequests shouldn't depend on raw request data (but that could be implemented here)
* @return string
*/
public function getRawQueryString() {
return '';
}
/**
* FauxRequests shouldn't depend on raw request data (but that could be implemented here)
* @return string
*/
public function getRawPostString() {
return '';
}
/**
* FauxRequests shouldn't depend on raw request data (but that could be implemented here)
* @return string
*/
public function getRawInput() {
return '';
}
/**
* @codeCoverageIgnore
* @return string
*/
protected function getRawIP() {
return '127.0.0.1';
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Object to access a fake $_FILES array for testing purpose
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
/**
* Object to fake the $_FILES array
*
* @ingroup HTTP
* @since 1.37
*/
class FauxRequestUpload extends WebRequestUpload {
/**
* Constructor. Should only be called by FauxRequest
*
* @param array $data Array of *non*-urlencoded key => value pairs, the
* fake (whole) FILES values
* @param FauxRequest $request The associated faux request
* @param string $key name of upload param
*/
public function __construct( $data, $request, $key ) {
$this->request = $request;
$this->doesExist = isset( $data[$key] );
if ( $this->doesExist ) {
$this->fileInfo = $data[$key];
}
}
}

View File

@ -0,0 +1,184 @@
<?php
/**
* Classes used to send headers and cookies back to the user
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\MediaWikiServices;
/**
* @ingroup HTTP
*/
class FauxResponse extends WebResponse {
private $headers;
private $cookies = [];
private $code;
/** @var ?Config */
private $cookieConfig = null;
/**
* Stores a HTTP header
* @param string $string Header to output
* @param bool $replace Replace current similar header
* @param null|int $http_response_code Forces the HTTP response code to the specified value.
*/
public function header( $string, $replace = true, $http_response_code = null ) {
if ( substr( $string, 0, 5 ) == 'HTTP/' ) {
$parts = explode( ' ', $string, 3 );
$this->code = intval( $parts[1] );
} else {
list( $key, $val ) = array_map( 'trim', explode( ":", $string, 2 ) );
$key = strtoupper( $key );
if ( $replace || !isset( $this->headers[$key] ) ) {
$this->headers[$key] = $val;
}
}
if ( $http_response_code !== null ) {
$this->code = intval( $http_response_code );
}
}
/**
* @since 1.26
* @param int $code Status code
*/
public function statusHeader( $code ) {
$this->code = intval( $code );
}
public function headersSent() {
return false;
}
/**
* @param string $key The name of the header to get (case insensitive).
* @return string|null The header value (if set); null otherwise.
*/
public function getHeader( $key ) {
$key = strtoupper( $key );
return $this->headers[$key] ?? null;
}
/**
* Get the HTTP response code, null if not set
*
* @return int|null
*/
public function getStatusCode() {
return $this->code;
}
/**
* @return Config
*/
private function getCookieConfig(): Config {
if ( !$this->cookieConfig ) {
$this->cookieConfig = MediaWikiServices::getInstance()->getMainConfig();
}
return $this->cookieConfig;
}
/**
* @param Config $cookieConfig
*/
public function setCookieConfig( Config $cookieConfig ): void {
$this->cookieConfig = $cookieConfig;
}
/**
* @param string $name The name of the cookie.
* @param string $value The value to be stored in the cookie.
* @param int|null $expire Ignored in this faux subclass.
* @param array $options Ignored in this faux subclass.
*/
public function setCookie( $name, $value, $expire = 0, $options = [] ) {
$cookieConfig = $this->getCookieConfig();
$cookiePath = $cookieConfig->get( 'CookiePath' );
$cookiePrefix = $cookieConfig->get( 'CookiePrefix' );
$cookieDomain = $cookieConfig->get( 'CookieDomain' );
$cookieSecure = $cookieConfig->get( 'CookieSecure' );
$cookieExpiration = $cookieConfig->get( 'CookieExpiration' );
$cookieHttpOnly = $cookieConfig->get( 'CookieHttpOnly' );
$options = array_filter( $options, static function ( $a ) {
return $a !== null;
} ) + [
'prefix' => $cookiePrefix,
'domain' => $cookieDomain,
'path' => $cookiePath,
'secure' => $cookieSecure,
'httpOnly' => $cookieHttpOnly,
'raw' => false,
];
if ( $expire === null ) {
$expire = 0; // Session cookie
} elseif ( $expire == 0 && $cookieExpiration != 0 ) {
$expire = time() + $cookieExpiration;
}
$this->cookies[$options['prefix'] . $name] = [
'value' => (string)$value,
'expire' => (int)$expire,
'path' => (string)$options['path'],
'domain' => (string)$options['domain'],
'secure' => (bool)$options['secure'],
'httpOnly' => (bool)$options['httpOnly'],
'raw' => (bool)$options['raw'],
];
}
/**
* @param string $name
* @return string|null
*/
public function getCookie( $name ) {
if ( isset( $this->cookies[$name] ) ) {
return $this->cookies[$name]['value'];
}
return null;
}
/**
* @param string $name
* @return array|null
*/
public function getCookieData( $name ) {
return $this->cookies[$name] ?? null;
}
/**
* @return array[]
*/
public function getCookies() {
return $this->cookies;
}
/**
* @inheritDoc
*/
public function hasCookies() {
return count( $this->cookies ) > 0;
}
}

View File

@ -0,0 +1,309 @@
<?php
/**
* Helper functions for feeds.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Feed
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
/**
* Helper functions for feeds
*
* @ingroup Feed
*/
class FeedUtils {
/**
* Check whether feeds can be used and that $type is a valid feed type
*
* @since 1.36 $output parameter added
*
* @param string $type Feed type, as requested by the user
* @param OutputPage|null $output Null falls back to $wgOut
* @return bool
*/
public static function checkFeedOutput( $type, $output = null ) {
$feed = MediaWikiServices::getInstance()->getMainConfig()->get( 'Feed' );
$feedClasses = MediaWikiServices::getInstance()->getMainConfig()->get( 'FeedClasses' );
if ( $output === null ) {
// Todo update GoogleNewsSitemap and deprecate
global $wgOut;
$output = $wgOut;
}
if ( !$feed ) {
$output->addWikiMsg( 'feed-unavailable' );
return false;
}
if ( !isset( $feedClasses[$type] ) ) {
$output->addWikiMsg( 'feed-invalid' );
return false;
}
return true;
}
/**
* Format a diff for the newsfeed
*
* @param stdClass $row Row from the recentchanges table, including fields as
* appropriate for CommentStore
* @param string|null $formattedComment rc_comment in HTML format, or null
* to format it on demand.
* @return string
*/
public static function formatDiff( $row, $formattedComment = null ) {
$titleObj = Title::makeTitle( $row->rc_namespace, $row->rc_title );
$timestamp = wfTimestamp( TS_MW, $row->rc_timestamp );
$actiontext = '';
if ( $row->rc_type == RC_LOG ) {
$rcRow = (array)$row; // newFromRow() only accepts arrays for RC rows
$actiontext = LogFormatter::newFromRow( $rcRow )->getActionText();
}
if ( $row->rc_deleted & RevisionRecord::DELETED_COMMENT ) {
$formattedComment = wfMessage( 'rev-deleted-comment' )->escaped();
} elseif ( $formattedComment === null ) {
$formattedComment = Linker::formatComment(
CommentStore::getStore()->getComment( 'rc_comment', $row )->text );
}
return self::formatDiffRow2( $titleObj,
$row->rc_last_oldid, $row->rc_this_oldid,
$timestamp,
$formattedComment,
$actiontext
);
}
/**
* Really format a diff for the newsfeed
*
* @deprecated since 1.38 use formatDiffRow2
*
* @param Title $title
* @param int $oldid Old revision's id
* @param int $newid New revision's id
* @param string $timestamp New revision's timestamp
* @param string $comment New revision's comment
* @param string $actiontext Text of the action; in case of log event
* @return string
*/
public static function formatDiffRow( $title, $oldid, $newid, $timestamp,
$comment, $actiontext = ''
) {
$formattedComment = MediaWikiServices::getInstance()->getCommentFormatter()
->format( $comment );
return self::formatDiffRow2( $title, $oldid, $newid, $timestamp,
$formattedComment, $actiontext );
}
/**
* Really really format a diff for the newsfeed. Same as formatDiffRow()
* except with preformatted comments.
*
* @param Title $title
* @param int $oldid Old revision's id
* @param int $newid New revision's id
* @param string $timestamp New revision's timestamp
* @param string $formattedComment New revision's comment in HTML format
* @param string $actiontext Text of the action; in case of log event
* @return string
*/
public static function formatDiffRow2( $title, $oldid, $newid, $timestamp,
$formattedComment, $actiontext = ''
) {
$feedDiffCutoff = MediaWikiServices::getInstance()->getMainConfig()->get( 'FeedDiffCutoff' );
// log entries
$unwrappedText = implode(
' ',
array_filter( [ $actiontext, $formattedComment ] )
);
$completeText = Html::rawElement( 'p', [], $unwrappedText ) . "\n";
// NOTE: Check permissions for anonymous users, not current user.
// No "privileged" version should end up in the cache.
// Most feed readers will not log in anyway.
$anon = new User();
$services = MediaWikiServices::getInstance();
$permManager = $services->getPermissionManager();
$accErrors = $permManager->getPermissionErrors(
'read',
$anon,
$title
);
// Can't diff special pages, unreadable pages or pages with no new revision
// to compare against: just return the text.
if ( $title->getNamespace() < 0 || $accErrors || !$newid ) {
return $completeText;
}
$revLookup = $services->getRevisionLookup();
$contentHandlerFactory = $services->getContentHandlerFactory();
if ( $oldid ) {
$diffText = '';
// Don't bother generating the diff if we won't be able to show it
if ( $feedDiffCutoff > 0 ) {
$revRecord = $revLookup->getRevisionById( $oldid );
if ( !$revRecord ) {
$diffText = false;
} else {
$mainContext = RequestContext::getMain();
$context = clone RequestContext::getMain();
$context->setTitle( $title );
$model = $revRecord->getSlot(
SlotRecord::MAIN,
RevisionRecord::RAW
)->getModel();
$contentHandler = $contentHandlerFactory->getContentHandler( $model );
$de = $contentHandler->createDifferenceEngine( $context, $oldid, $newid );
$lang = $mainContext->getLanguage();
$user = $mainContext->getUser();
$diffText = $de->getDiff(
$mainContext->msg( 'previousrevision' )->text(), // hack
$mainContext->msg( 'revisionasof',
$lang->userTimeAndDate( $timestamp, $user ),
$lang->userDate( $timestamp, $user ),
$lang->userTime( $timestamp, $user ) )->text() );
}
}
if ( $feedDiffCutoff <= 0 || ( strlen( $diffText ) > $feedDiffCutoff ) ) {
// Omit large diffs
$diffText = self::getDiffLink( $title, $newid, $oldid );
} elseif ( $diffText === false ) {
// Error in diff engine, probably a missing revision
$diffText = Html::rawElement(
'p',
[],
"Can't load revision $newid"
);
} else {
// Diff output fine, clean up any illegal UTF-8
$diffText = UtfNormal\Validator::cleanUp( $diffText );
$diffText = self::applyDiffStyle( $diffText );
}
} else {
$revRecord = $revLookup->getRevisionById( $newid );
if ( $feedDiffCutoff <= 0 || $revRecord === null ) {
$newContent = $contentHandlerFactory
->getContentHandler( $title->getContentModel() )
->makeEmptyContent();
} else {
$newContent = $revRecord->getContent( SlotRecord::MAIN );
}
if ( $newContent instanceof TextContent ) {
// only textual content has a "source view".
$text = $newContent->getText();
if ( $feedDiffCutoff <= 0 || strlen( $text ) > $feedDiffCutoff ) {
$html = null;
} else {
$html = nl2br( htmlspecialchars( $text ) );
}
} else {
// XXX: we could get an HTML representation of the content via getParserOutput, but that may
// contain JS magic and generally may not be suitable for inclusion in a feed.
// Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
// Compare also ApiFeedContributions::feedItemDesc
$html = null;
}
if ( $html === null ) {
// Omit large new page diffs, T31110
// Also use diff link for non-textual content
$diffText = self::getDiffLink( $title, $newid );
} else {
$diffText = Html::rawElement(
'p',
[],
Html::rawElement( 'b', [], wfMessage( 'newpage' )->text() )
);
$diffText .= Html::rawElement( 'div', [], $html );
}
}
$completeText .= $diffText;
return $completeText;
}
/**
* Generates a diff link. Used when the full diff is not wanted for example
* when $wgFeedDiffCutoff is 0.
*
* @param Title $title Title object: used to generate the diff URL
* @param int $newid Newid for this diff
* @param int|null $oldid Oldid for the diff. Null means it is a new article
* @return string
*/
protected static function getDiffLink( Title $title, $newid, $oldid = null ) {
$queryParameters = [ 'diff' => $newid ];
if ( $oldid != null ) {
$queryParameters['oldid'] = $oldid;
}
$diffUrl = $title->getFullURL( $queryParameters );
$diffLink = Html::element( 'a', [ 'href' => $diffUrl ],
wfMessage( 'showdiff' )->inContentLanguage()->text() );
return $diffLink;
}
/**
* Hacky application of diff styles for the feeds.
* Might be 'cleaner' to use DOM or XSLT or something,
* but *gack* it's a pain in the ass.
*
* @param string $text Diff's HTML output
* @return string Modified HTML
*/
public static function applyDiffStyle( $text ) {
$styles = [
'diff' => 'background-color: #fff; color: #202122;',
'diff-otitle' => 'background-color: #fff; color: #202122; text-align: center;',
'diff-ntitle' => 'background-color: #fff; color: #202122; text-align: center;',
'diff-addedline' => 'color: #202122; font-size: 88%; border-style: solid; '
. 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; '
. 'vertical-align: top; white-space: pre-wrap;',
'diff-deletedline' => 'color: #202122; font-size: 88%; border-style: solid; '
. 'border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; '
. 'vertical-align: top; white-space: pre-wrap;',
'diff-context' => 'background-color: #f8f9fa; color: #202122; font-size: 88%; '
. 'border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; '
. 'border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;',
'diffchange' => 'font-weight: bold; text-decoration: none;',
];
foreach ( $styles as $class => $style ) {
$text = preg_replace( '/(<\w+\b[^<>]*)\bclass=([\'"])(?:[^\'"]*\s)?' .
preg_quote( $class ) . '(?:\s[^\'"]*)?\2(?=[^<>]*>)/',
'$1style="' . $style . '"', $text );
}
return $text;
}
}

View File

@ -0,0 +1,160 @@
<?php
/**
* File deletion utilities.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @author Rob Church <robchur@gmail.com>
* @ingroup Media
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Page\DeletePage;
use MediaWiki\User\UserIdentity;
/**
* File deletion user interface
*
* @ingroup Media
*/
class FileDeleteForm {
/**
* Really delete the file
*
* @param Title $title
* @param LocalFile $file
* @param string|null $oldimage Archive name
* @param string $reason Reason of the deletion
* @param bool $suppress Whether to mark all deleted versions as restricted
* @param UserIdentity $user
* @param string[] $tags Tags to apply to the deletion action
* @param bool $deleteTalk
* @return Status The value can be an integer with the log ID of the deletion, or false in case of
* scheduled deletion.
* @throws MWException
*/
public static function doDelete( Title $title, LocalFile $file, ?string $oldimage, $reason,
$suppress, UserIdentity $user, $tags = [], bool $deleteTalk = false
): Status {
if ( $oldimage ) {
$page = null;
$status = $file->deleteOldFile( $oldimage, $reason, $user, $suppress );
if ( $status->isOK() ) {
// Need to do a log item
$logComment = wfMessage( 'deletedrevision', $oldimage )->inContentLanguage()->text();
if ( trim( $reason ) !== '' ) {
$logComment .= wfMessage( 'colon-separator' )
->inContentLanguage()->text() . $reason;
}
$logtype = $suppress ? 'suppress' : 'delete';
$logEntry = new ManualLogEntry( $logtype, 'delete' );
$logEntry->setPerformer( $user );
$logEntry->setTarget( $title );
$logEntry->setComment( $logComment );
$logEntry->addTags( $tags );
$logid = $logEntry->insert();
$logEntry->publish( $logid );
$status->value = $logid;
}
} else {
$status = Status::newFatal( 'cannotdelete',
wfEscapeWikiText( $title->getPrefixedText() )
);
$services = MediaWikiServices::getInstance();
$page = $services->getWikiPageFactory()->newFromTitle( $title );
'@phan-var WikiFilePage $page';
$deleter = $services->getUserFactory()->newFromUserIdentity( $user );
$deletePage = $services->getDeletePageFactory()->newDeletePage( $page, $deleter );
if ( $deleteTalk ) {
$checkStatus = $deletePage->canProbablyDeleteAssociatedTalk();
if ( !$checkStatus->isGood() ) {
return Status::wrap( $checkStatus );
}
$deletePage->setDeleteAssociatedTalk( true );
}
$dbw = wfGetDB( DB_PRIMARY );
$dbw->startAtomic( __METHOD__, $dbw::ATOMIC_CANCELABLE );
// delete the associated article first
$deleteStatus = $deletePage
->setSuppress( $suppress )
->setTags( $tags ?: [] )
->deleteIfAllowed( $reason );
// DeletePage returns a non-fatal error status if the page
// or revision is missing, so check for isOK() rather than isGood().
if ( $deleteStatus->isOK() ) {
$status = $file->deleteFile( $reason, $user, $suppress );
if ( $status->isOK() ) {
if ( $deletePage->deletionsWereScheduled()[DeletePage::PAGE_BASE] ) {
$status->value = false;
} else {
$deletedID = $deletePage->getSuccessfulDeletionsIDs()[DeletePage::PAGE_BASE];
if ( $deletedID !== null ) {
$status->value = $deletedID;
} else {
// Means that the page/revision didn't exist, so create a log entry here.
$logtype = $suppress ? 'suppress' : 'delete';
$logEntry = new ManualLogEntry( $logtype, 'delete' );
$logEntry->setPerformer( $user );
$logEntry->setTarget( $title );
$logEntry->setComment( $reason );
$logEntry->addTags( $tags );
$logid = $logEntry->insert();
$dbw->onTransactionPreCommitOrIdle(
static function () use ( $logEntry, $logid ) {
$logEntry->publish( $logid );
},
__METHOD__
);
$status->value = $logid;
}
}
$dbw->endAtomic( __METHOD__ );
} else {
// Page deleted but file still there? rollback page delete
$dbw->cancelAtomic( __METHOD__ );
}
} else {
$dbw->endAtomic( __METHOD__ );
}
}
if ( $status->isOK() ) {
$legacyUser = MediaWikiServices::getInstance()
->getUserFactory()
->newFromUserIdentity( $user );
Hooks::runner()->onFileDeleteComplete( $file, $oldimage, $page, $legacyUser, $reason );
}
return $status;
}
/**
* Is the provided `oldimage` value valid?
*
* @param string $oldimage
* @return bool
*/
public static function isValidOldSpec( $oldimage ) {
return strlen( $oldimage ) >= 16
&& strpos( $oldimage, '/' ) === false
&& strpos( $oldimage, '\\' ) === false;
}
}

View File

@ -0,0 +1,404 @@
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Maintenance
*/
use MediaWiki\MediaWikiServices;
use Wikimedia\AtEase\AtEase;
/**
* Manage foreign resources registered with ResourceLoader.
*
* @since 1.32
*/
class ForeignResourceManager {
private $defaultAlgo = 'sha384';
private $hasErrors = false;
private $registryFile;
private $libDir;
private $tmpParentDir;
private $cacheDir;
/**
* @var callable|Closure
* @phan-var callable(string):void
*/
private $infoPrinter;
/**
* @var callable|Closure
* @phan-var callable(string):void
*/
private $errorPrinter;
/**
* @var callable|Closure
* @phan-var callable(string):void
*/
private $verbosePrinter;
private $action;
/** @var array[] */
private $registry;
/**
* @param string $registryFile Path to YAML file
* @param string $libDir Path to a modules directory
* @param callable|null $infoPrinter Callback for printing info about the run.
* @param callable|null $errorPrinter Callback for printing errors from the run.
* @param callable|null $verbosePrinter Callback for printing extra verbose
* progress information from the run.
*/
public function __construct(
$registryFile,
$libDir,
callable $infoPrinter = null,
callable $errorPrinter = null,
callable $verbosePrinter = null
) {
$this->registryFile = $registryFile;
$this->libDir = $libDir;
$this->infoPrinter = $infoPrinter ?? static function ( $_ ) {
};
$this->errorPrinter = $errorPrinter ?? $this->infoPrinter;
$this->verbosePrinter = $verbosePrinter ?? static function ( $_ ) {
};
// Use a temporary directory under the destination directory instead
// of wfTempDir() because PHP's rename() does not work across file
// systems, and the user's /tmp and $IP may be on different filesystems.
$this->tmpParentDir = "{$this->libDir}/.foreign/tmp";
$cacheHome = getenv( 'XDG_CACHE_HOME' ) ? realpath( getenv( 'XDG_CACHE_HOME' ) ) : false;
$this->cacheDir = $cacheHome ? "$cacheHome/mw-foreign" : "{$this->libDir}/.foreign/cache";
}
/**
* @param string $action
* @param string $module
* @return bool
* @throws Exception
*/
public function run( $action, $module ) {
$actions = [ 'update', 'verify', 'make-sri' ];
if ( !in_array( $action, $actions ) ) {
$this->error( "Invalid action.\n\nMust be one of " . implode( ', ', $actions ) . '.' );
return false;
}
$this->action = $action;
$this->registry = $this->parseBasicYaml( file_get_contents( $this->registryFile ) );
if ( $module === 'all' ) {
$modules = $this->registry;
} elseif ( isset( $this->registry[ $module ] ) ) {
$modules = [ $module => $this->registry[ $module ] ];
} else {
$this->error( "Unknown module name.\n\nMust be one of:\n" .
wordwrap( implode( ', ', array_keys( $this->registry ) ), 80 ) .
'.'
);
return false;
}
foreach ( $modules as $moduleName => $info ) {
$this->verbose( "\n### {$moduleName}\n\n" );
$destDir = "{$this->libDir}/$moduleName";
if ( $this->action === 'update' ) {
$this->output( "... updating '{$moduleName}'\n" );
$this->verbose( "... emptying directory for $moduleName\n" );
wfRecursiveRemoveDir( $destDir );
} elseif ( $this->action === 'verify' ) {
$this->output( "... verifying '{$moduleName}'\n" );
} else {
$this->output( "... checking '{$moduleName}'\n" );
}
$this->verbose( "... preparing {$this->tmpParentDir}\n" );
wfRecursiveRemoveDir( $this->tmpParentDir );
if ( !wfMkdirParents( $this->tmpParentDir ) ) {
throw new Exception( "Unable to create {$this->tmpParentDir}" );
}
if ( !isset( $info['type'] ) ) {
throw new Exception( "Module '$moduleName' must have a 'type' key." );
}
switch ( $info['type'] ) {
case 'tar':
$this->handleTypeTar( $moduleName, $destDir, $info );
break;
case 'file':
$this->handleTypeFile( $moduleName, $destDir, $info );
break;
case 'multi-file':
$this->handleTypeMultiFile( $moduleName, $destDir, $info );
break;
default:
throw new Exception( "Unknown type '{$info['type']}' for '$moduleName'" );
}
}
$this->output( "\nDone!\n" );
$this->cleanUp();
if ( $this->hasErrors ) {
// The verify mode should check all modules/files and fail after, not during.
return false;
}
return true;
}
private function cacheKey( $src, $integrity ) {
$key = basename( $src ) . '_' . substr( $integrity, -12 );
$key = preg_replace( '/[.\/+?=_-]+/', '_', $key );
return rtrim( $key, '_' );
}
/**
* @param string $key
* @return string|false
*/
private function cacheGet( $key ) {
return AtEase::quietCall( 'file_get_contents', "{$this->cacheDir}/$key.data" );
}
private function cacheSet( $key, $data ) {
wfMkdirParents( $this->cacheDir );
file_put_contents( "{$this->cacheDir}/$key.data", $data, LOCK_EX );
}
private function fetch( $src, $integrity ) {
$key = $this->cacheKey( $src, $integrity );
$data = $this->cacheGet( $key );
if ( $data ) {
return $data;
}
$req = MediaWikiServices::getInstance()->getHttpRequestFactory()
->create( $src, [ 'method' => 'GET', 'followRedirects' => false ], __METHOD__ );
if ( !$req->execute()->isOK() ) {
throw new Exception( "Failed to download resource at {$src}" );
}
if ( $req->getStatus() !== 200 ) {
throw new Exception( "Unexpected HTTP {$req->getStatus()} response from {$src}" );
}
$data = $req->getContent();
$algo = $integrity === null ? $this->defaultAlgo : explode( '-', $integrity )[0];
$actualIntegrity = $algo . '-' . base64_encode( hash( $algo, $data, true ) );
if ( $integrity === $actualIntegrity ) {
$this->verbose( "... passed integrity check for {$src}\n" );
$this->cacheSet( $key, $data );
} elseif ( $this->action === 'make-sri' ) {
$this->output( "Integrity for {$src}\n\tintegrity: {$actualIntegrity}\n" );
} else {
$expectedIntegrity = $integrity ?? 'null';
throw new Exception( "Integrity check failed for {$src}\n" .
"\tExpected: {$expectedIntegrity}\n" .
"\tActual: {$actualIntegrity}"
);
}
return $data;
}
private function handleTypeFile( $moduleName, $destDir, array $info ) {
if ( !isset( $info['src'] ) ) {
throw new Exception( "Module '$moduleName' must have a 'src' key." );
}
$data = $this->fetch( $info['src'], $info['integrity'] ?? null );
$dest = $info['dest'] ?? basename( $info['src'] );
$path = "$destDir/$dest";
if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) {
throw new Exception( "File for '$moduleName' is different." );
}
if ( $this->action === 'update' ) {
wfMkdirParents( $destDir );
file_put_contents( "$destDir/$dest", $data );
}
}
private function handleTypeMultiFile( $moduleName, $destDir, array $info ) {
if ( !isset( $info['files'] ) ) {
throw new Exception( "Module '$moduleName' must have a 'files' key." );
}
foreach ( $info['files'] as $dest => $file ) {
if ( !isset( $file['src'] ) ) {
throw new Exception( "Module '$moduleName' file '$dest' must have a 'src' key." );
}
$data = $this->fetch( $file['src'], $file['integrity'] ?? null );
$path = "$destDir/$dest";
if ( $this->action === 'verify' && sha1_file( $path ) !== sha1( $data ) ) {
throw new Exception( "File '$dest' for '$moduleName' is different." );
} elseif ( $this->action === 'update' ) {
wfMkdirParents( $destDir );
file_put_contents( "$destDir/$dest", $data );
}
}
}
private function handleTypeTar( $moduleName, $destDir, array $info ) {
$info += [ 'src' => null, 'integrity' => null, 'dest' => null ];
if ( $info['src'] === null ) {
throw new Exception( "Module '$moduleName' must have a 'src' key." );
}
// Download the resource to a temporary file and open it
$data = $this->fetch( $info['src'], $info['integrity' ] );
$tmpFile = "{$this->tmpParentDir}/$moduleName.tar";
$this->verbose( "... writing '$moduleName' src to $tmpFile\n" );
file_put_contents( $tmpFile, $data );
$p = new PharData( $tmpFile );
$tmpDir = "{$this->tmpParentDir}/$moduleName";
$p->extractTo( $tmpDir );
unset( $data, $p );
if ( $info['dest'] === null ) {
// Default: Replace the entire directory
$toCopy = [ $tmpDir => $destDir ];
} else {
// Expand and normalise the 'dest' entries
$toCopy = [];
foreach ( $info['dest'] as $fromSubPath => $toSubPath ) {
// Use glob() to expand wildcards and check existence
$fromPaths = glob( "{$tmpDir}/{$fromSubPath}", GLOB_BRACE );
if ( !$fromPaths ) {
throw new Exception( "Path '$fromSubPath' of '$moduleName' not found." );
}
foreach ( $fromPaths as $fromPath ) {
$toCopy[$fromPath] = $toSubPath === null
? "$destDir/" . basename( $fromPath )
: "$destDir/$toSubPath/" . basename( $fromPath );
}
}
}
foreach ( $toCopy as $from => $to ) {
if ( $this->action === 'verify' ) {
$this->verbose( "... verifying $to\n" );
if ( is_dir( $from ) ) {
$rii = new RecursiveIteratorIterator( new RecursiveDirectoryIterator(
$from,
RecursiveDirectoryIterator::SKIP_DOTS
) );
/** @var SplFileInfo $file */
foreach ( $rii as $file ) {
$remote = $file->getPathname();
$local = strtr( $remote, [ $from => $to ] );
if ( sha1_file( $remote ) !== sha1_file( $local ) ) {
$this->error( "File '$local' is different." );
$this->hasErrors = true;
}
}
} elseif ( sha1_file( $from ) !== sha1_file( $to ) ) {
$this->error( "File '$to' is different." );
$this->hasErrors = true;
}
} elseif ( $this->action === 'update' ) {
$this->verbose( "... moving $from to $to\n" );
wfMkdirParents( dirname( $to ) );
if ( !rename( $from, $to ) ) {
throw new Exception( "Could not move $from to $to." );
}
}
}
}
private function verbose( $text ) {
( $this->verbosePrinter )( $text );
}
private function output( $text ) {
( $this->infoPrinter )( $text );
}
private function error( $text ) {
( $this->errorPrinter )( $text );
}
private function cleanUp() {
wfRecursiveRemoveDir( $this->tmpParentDir );
// Prune the cache of files we don't recognise.
$knownKeys = [];
foreach ( $this->registry as $info ) {
if ( $info['type'] === 'file' || $info['type'] === 'tar' ) {
$knownKeys[] = $this->cacheKey( $info['src'], $info['integrity'] );
} elseif ( $info['type'] === 'multi-file' ) {
foreach ( $info['files'] as $file ) {
$knownKeys[] = $this->cacheKey( $file['src'], $file['integrity'] );
}
}
}
foreach ( glob( "{$this->cacheDir}/*" ) as $cacheFile ) {
if ( !in_array( basename( $cacheFile, '.data' ), $knownKeys ) ) {
unlink( $cacheFile );
}
}
}
/**
* Basic YAML parser.
*
* Supports only string or object values, and 2 spaces indentation.
*
* @todo Just ship symfony/yaml.
* @param string $input
* @return array
*/
private function parseBasicYaml( $input ) {
$lines = explode( "\n", $input );
$root = [];
$stack = [ &$root ];
$prev = 0;
foreach ( $lines as $i => $text ) {
$line = $i + 1;
$trimmed = ltrim( $text, ' ' );
if ( $trimmed === '' || $trimmed[0] === '#' ) {
continue;
}
$indent = strlen( $text ) - strlen( $trimmed );
if ( $indent % 2 !== 0 ) {
throw new Exception( __METHOD__ . ": Odd indentation on line $line." );
}
$depth = $indent === 0 ? 0 : ( $indent / 2 );
if ( $depth < $prev ) {
// Close previous branches we can't re-enter
array_splice( $stack, $depth + 1 );
}
if ( !array_key_exists( $depth, $stack ) ) {
throw new Exception( __METHOD__ . ": Too much indentation on line $line." );
}
if ( strpos( $trimmed, ':' ) === false ) {
throw new Exception( __METHOD__ . ": Missing colon on line $line." );
}
$dest =& $stack[ $depth ];
if ( $dest === null ) {
// Promote from null to object
$dest = [];
}
list( $key, $val ) = explode( ':', $trimmed, 2 );
$val = ltrim( $val, ' ' );
if ( $val !== '' ) {
// Add string
$dest[ $key ] = $val;
} else {
// Add null (may become an object later)
$val = null;
$stack[] = &$val;
$dest[ $key ] = &$val;
}
$prev = $depth;
unset( $dest, $val );
}
return $root;
}
}

View File

@ -0,0 +1,214 @@
<?php
/**
* Class for managing forking command line scripts.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\MediaWikiServices;
/**
* Class for managing forking command line scripts.
* Currently just does forking and process control, but it could easily be extended
* to provide IPC and job dispatch.
*
* This class requires the posix and pcntl extensions.
*
* @ingroup Maintenance
*/
class ForkController {
/** @var array|null */
protected $children = [];
/** @var int */
protected $childNumber = 0;
/** @var bool */
protected $termReceived = false;
/** @var int */
protected $flags = 0;
/** @var int */
protected $procsToStart = 0;
protected static $RESTARTABLE_SIGNALS = [];
/**
* Pass this flag to __construct() to cause the class to automatically restart
* workers that exit with non-zero exit status or a signal such as SIGSEGV.
*/
private const RESTART_ON_ERROR = 1;
/**
* @param int $numProcs The number of worker processes to fork
* @param int $flags
*/
public function __construct( $numProcs, $flags = 0 ) {
if ( !wfIsCLI() ) {
throw new MWException( "ForkController cannot be used from the web." );
} elseif ( !extension_loaded( 'pcntl' ) ) {
throw new MWException( 'ForkController requires pcntl extension to be installed.' );
} elseif ( !extension_loaded( 'posix' ) ) {
throw new MWException( 'ForkController requires posix extension to be installed.' );
}
$this->procsToStart = $numProcs;
$this->flags = $flags;
// Define this only after confirming PCNTL support
self::$RESTARTABLE_SIGNALS = [
SIGFPE, SIGILL, SIGSEGV, SIGBUS,
SIGABRT, SIGSYS, SIGPIPE, SIGXCPU,SIGXFSZ,
];
}
/**
* Start the child processes.
*
* This should only be called from the command line. It should be called
* as early as possible during execution.
*
* This will return 'child' in the child processes. In the parent process,
* it will run until all the child processes exit or a TERM signal is
* received. It will then return 'done'.
* @return string
*/
public function start() {
// Trap SIGTERM
pcntl_signal( SIGTERM, [ $this, 'handleTermSignal' ], false );
do {
// Start child processes
if ( $this->procsToStart ) {
if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) {
return 'child';
}
$this->procsToStart = 0;
}
// Check child status
$status = false;
$deadPid = pcntl_wait( $status );
if ( $deadPid > 0 ) {
// Respond to child process termination
unset( $this->children[$deadPid] );
if ( $this->flags & self::RESTART_ON_ERROR ) {
if ( pcntl_wifsignaled( $status ) ) {
// Restart if the signal was abnormal termination
// Don't restart if it was deliberately killed
$signal = pcntl_wtermsig( $status );
if ( in_array( $signal, self::$RESTARTABLE_SIGNALS ) ) {
echo "Worker exited with signal $signal, restarting\n";
$this->procsToStart++;
}
} elseif ( pcntl_wifexited( $status ) ) {
// Restart on non-zero exit status
$exitStatus = pcntl_wexitstatus( $status );
if ( $exitStatus != 0 ) {
echo "Worker exited with status $exitStatus, restarting\n";
$this->procsToStart++;
} else {
echo "Worker exited normally\n";
}
}
}
// Throttle restarts
if ( $this->procsToStart ) {
usleep( 500000 );
}
}
// Run signal handlers
if ( function_exists( 'pcntl_signal_dispatch' ) ) {
pcntl_signal_dispatch();
} else {
declare( ticks = 1 ) {
// @phan-suppress-next-line PhanPluginDuplicateExpressionAssignment
$status = $status;
}
}
// Respond to TERM signal
if ( $this->termReceived ) {
foreach ( $this->children as $childPid => $unused ) {
posix_kill( $childPid, SIGTERM );
}
$this->termReceived = false;
}
} while ( count( $this->children ) );
pcntl_signal( SIGTERM, SIG_DFL );
return 'done';
}
/**
* Get the number of the child currently running. Note, this
* is not the pid, but rather which of the total number of children
* we are
* @return int
*/
public function getChildNumber() {
return $this->childNumber;
}
protected function prepareEnvironment() {
// Don't share DB, storage, or memcached connections
MediaWikiServices::resetChildProcessServices();
JobQueueGroup::destroySingletons();
ObjectCache::clear();
RedisConnectionPool::destroySingletons();
}
/**
* Fork a number of worker processes.
*
* @param int $numProcs
* @return string
*/
protected function forkWorkers( $numProcs ) {
$this->prepareEnvironment();
// Create the child processes
for ( $i = 0; $i < $numProcs; $i++ ) {
// Do the fork
$pid = pcntl_fork();
if ( $pid === -1 || $pid === false ) {
echo "Error creating child processes\n";
exit( 1 );
}
if ( !$pid ) {
$this->initChild();
$this->childNumber = $i;
return 'child';
} else {
// This is the parent process
$this->children[$pid] = true;
}
}
return 'parent';
}
protected function initChild() {
$this->children = null;
pcntl_signal( SIGTERM, SIG_DFL );
}
protected function handleTermSignal( $signal ) {
$this->termReceived = true;
}
}

View File

@ -0,0 +1,428 @@
<?php
/**
* Helper class to keep track of options when mixing links and form elements.
*
* Copyright © 2008, Niklas Laxström
* Copyright © 2011, Antoine Musso
* Copyright © 2013, Bartosz Dziewoński
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @author Niklas Laxström
* @author Antoine Musso
*/
/**
* Helper class to keep track of options when mixing links and form elements.
*
* @todo This badly needs some examples and tests :) The usage in SpecialRecentchanges class is a
* good ersatz in the meantime.
*/
class FormOptions implements ArrayAccess {
/** @name Type constants
* Used internally to map an option value to a WebRequest accessor
* @{
*/
/** Mark value for automatic detection (for simple data types only) */
public const AUTO = -1;
/** String type, maps guessType() to WebRequest::getText() */
public const STRING = 0;
/** Integer type, maps guessType() to WebRequest::getInt() */
public const INT = 1;
/** Float type, maps guessType() to WebRequest::getFloat()
* @since 1.23
*/
public const FLOAT = 4;
/** Boolean type, maps guessType() to WebRequest::getBool() */
public const BOOL = 2;
/** Integer type or null, maps to WebRequest::getIntOrNull()
* This is useful for the namespace selector.
*/
public const INTNULL = 3;
/** Array type, maps guessType() to WebRequest::getArray()
* @since 1.29
*/
public const ARR = 5;
/** @} */
/**
* Map of known option names to information about them.
*
* Each value is an array with the following keys:
* - 'default' - the default value as passed to add()
* - 'value' - current value, start with null, can be set by various functions
* - 'consumed' - true/false, whether the option was consumed using
* consumeValue() or consumeValues()
* - 'type' - one of the type constants (but never AUTO)
*/
protected $options = [];
# Setting up
/**
* Add an option to be handled by this FormOptions instance.
*
* @param string $name Request parameter name
* @param mixed $default Default value when the request parameter is not present
* @param int $type One of the type constants (optional, defaults to AUTO)
*/
public function add( $name, $default, $type = self::AUTO ) {
$option = [];
$option['default'] = $default;
$option['value'] = null;
$option['consumed'] = false;
if ( $type !== self::AUTO ) {
$option['type'] = $type;
} else {
$option['type'] = self::guessType( $default );
}
$this->options[$name] = $option;
}
/**
* Remove an option being handled by this FormOptions instance. This is the inverse of add().
*
* @param string $name Request parameter name
*/
public function delete( $name ) {
$this->validateName( $name, true );
unset( $this->options[$name] );
}
/**
* Used to find out which type the data is. All types are defined in the 'Type constants' section
* of this class.
*
* Detection of the INTNULL type is not supported; INT will be assumed if the data is an integer,
* MWException will be thrown if it's null.
*
* @param mixed $data Value to guess the type for
* @throws MWException If unable to guess the type
* @return int Type constant
*/
public static function guessType( $data ) {
if ( is_bool( $data ) ) {
return self::BOOL;
} elseif ( is_int( $data ) ) {
return self::INT;
} elseif ( is_float( $data ) ) {
return self::FLOAT;
} elseif ( is_string( $data ) ) {
return self::STRING;
} elseif ( is_array( $data ) ) {
return self::ARR;
} else {
throw new MWException( 'Unsupported datatype' );
}
}
# Handling values
/**
* Verify that the given option name exists.
*
* @param string $name Option name
* @param bool $strict Throw an exception when the option doesn't exist instead of returning false
* @throws MWException
* @return bool True if the option exists, false otherwise
*/
public function validateName( $name, $strict = false ) {
if ( !isset( $this->options[$name] ) ) {
if ( $strict ) {
throw new MWException( "Invalid option $name" );
} else {
return false;
}
}
return true;
}
/**
* Use to set the value of an option.
*
* @param string $name Option name
* @param mixed $value Value for the option
* @param bool $force Whether to set the value when it is equivalent to the default value for this
* option (default false).
*/
public function setValue( $name, $value, $force = false ) {
$this->validateName( $name, true );
if ( !$force && $value === $this->options[$name]['default'] ) {
// null default values as unchanged
$this->options[$name]['value'] = null;
} else {
$this->options[$name]['value'] = $value;
}
}
/**
* Get the value for the given option name. Uses getValueReal() internally.
*
* @param string $name Option name
* @return mixed
*/
public function getValue( $name ) {
$this->validateName( $name, true );
return $this->getValueReal( $this->options[$name] );
}
/**
* Return current option value, based on a structure taken from $options.
*
* @param array $option Array structure describing the option
* @return mixed Value, or the default value if it is null
*/
protected function getValueReal( $option ) {
if ( $option['value'] !== null ) {
return $option['value'];
} else {
return $option['default'];
}
}
/**
* Delete the option value.
* This will make future calls to getValue() return the default value.
* @param string $name Option name
*/
public function reset( $name ) {
$this->validateName( $name, true );
$this->options[$name]['value'] = null;
}
/**
* Get the value of given option and mark it as 'consumed'. Consumed options are not returned
* by getUnconsumedValues().
*
* @see consumeValues()
* @throws MWException If the option does not exist
* @param string $name Option name
* @return mixed Value, or the default value if it is null
*/
public function consumeValue( $name ) {
$this->validateName( $name, true );
$this->options[$name]['consumed'] = true;
return $this->getValueReal( $this->options[$name] );
}
/**
* Get the values of given options and mark them as 'consumed'. Consumed options are not returned
* by getUnconsumedValues().
*
* @see consumeValue()
* @throws MWException If any option does not exist
* @param string[] $names List of option names
* @return array Array of option values, or the default values if they are null
*/
public function consumeValues( $names ) {
$out = [];
foreach ( $names as $name ) {
$this->validateName( $name, true );
$this->options[$name]['consumed'] = true;
$out[] = $this->getValueReal( $this->options[$name] );
}
return $out;
}
/**
* @see validateBounds()
* @param string $name
* @param int $min
* @param int $max
*/
public function validateIntBounds( $name, $min, $max ) {
$this->validateBounds( $name, $min, $max );
}
/**
* Constrain a numeric value for a given option to a given range. The value will be altered to fit
* in the range.
*
* @since 1.23
*
* @param string $name Option name
* @param int|float $min Minimum value
* @param int|float $max Maximum value
* @throws MWException If option is not of type INT
*/
public function validateBounds( $name, $min, $max ) {
$this->validateName( $name, true );
$type = $this->options[$name]['type'];
if ( $type !== self::INT && $type !== self::FLOAT ) {
throw new MWException( "Option $name is not of type INT or FLOAT" );
}
$value = $this->getValueReal( $this->options[$name] );
$value = max( $min, min( $max, $value ) );
$this->setValue( $name, $value );
}
/**
* Get all remaining values which have not been consumed by consumeValue() or consumeValues().
*
* @param bool $all Whether to include unchanged options (default: false)
* @return array
*/
public function getUnconsumedValues( $all = false ) {
$values = [];
foreach ( $this->options as $name => $data ) {
if ( !$data['consumed'] ) {
if ( $all || $data['value'] !== null ) {
$values[$name] = $this->getValueReal( $data );
}
}
}
return $values;
}
/**
* Return options modified as an array ( name => value )
* @return array
*/
public function getChangedValues() {
$values = [];
foreach ( $this->options as $name => $data ) {
if ( $data['value'] !== null ) {
$values[$name] = $data['value'];
}
}
return $values;
}
/**
* Format options to an array ( name => value )
* @return array
*/
public function getAllValues() {
$values = [];
foreach ( $this->options as $name => $data ) {
$values[$name] = $this->getValueReal( $data );
}
return $values;
}
# Reading values
/**
* Fetch values for all options (or selected options) from the given WebRequest, making them
* available for accessing with getValue() or consumeValue() etc.
*
* @param WebRequest $r The request to fetch values from
* @param array|null $optionKeys Which options to fetch the values for (default:
* all of them). Note that passing an empty array will also result in
* values for all keys being fetched.
* @throws MWException If the type of any option is invalid
*/
public function fetchValuesFromRequest( WebRequest $r, $optionKeys = null ) {
if ( !$optionKeys ) {
$optionKeys = array_keys( $this->options );
}
foreach ( $optionKeys as $name ) {
$default = $this->options[$name]['default'];
$type = $this->options[$name]['type'];
switch ( $type ) {
case self::BOOL:
$value = $r->getBool( $name, $default );
break;
case self::INT:
$value = $r->getInt( $name, $default );
break;
case self::FLOAT:
$value = $r->getFloat( $name, $default );
break;
case self::STRING:
$value = $r->getText( $name, $default );
break;
case self::INTNULL:
$value = $r->getIntOrNull( $name );
break;
case self::ARR:
$value = $r->getArray( $name );
break;
default:
throw new MWException( 'Unsupported datatype' );
}
if ( $value !== null ) {
$this->options[$name]['value'] = $value === $default ? null : $value;
}
}
}
/***************************************************************************/
// region ArrayAccess functions
/** @name ArrayAccess functions
* These functions implement the ArrayAccess PHP interface.
* @see https://www.php.net/manual/en/class.arrayaccess.php
*/
/**
* Whether the option exists.
* @param string $name
* @return bool
*/
public function offsetExists( $name ): bool {
return isset( $this->options[$name] );
}
/**
* Retrieve an option value.
* @param string $name
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet( $name ) {
return $this->getValue( $name );
}
/**
* Set an option to given value.
* @param string $name
* @param mixed $value
*/
public function offsetSet( $name, $value ): void {
$this->setValue( $name, $value );
}
/**
* Delete the option.
* @param string $name
*/
public function offsetUnset( $name ): void {
$this->delete( $name );
}
// endregion -- end of ArrayAccess functions
}

View File

@ -0,0 +1,437 @@
<?php
/**
* A class to help return information about a git repo MediaWiki may be inside
* This is used by Special:Version and is also useful for the LocalSettings.php
* of anyone working on large branches in git to setup config that show up only
* when specific branches are currently checked out.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Shell\Shell;
use Wikimedia\AtEase\AtEase;
/**
* @newable
* @note marked as newable in 1.35 for lack of a better alternative,
* but should become a stateless service eventually.
*/
class GitInfo {
/**
* Singleton for the repo at $IP
*/
protected static $repo = null;
/**
* Location of the .git directory
*/
protected $basedir;
/**
* Location of the repository
*/
protected $repoDir;
/**
* Path to JSON cache file for pre-computed git information.
*/
protected $cacheFile;
/**
* Cached git information.
*/
protected $cache = [];
/**
* @var array|false Map of repo URLs to viewer URLs. Access via static method getViewers().
*/
private static $viewers = false;
/**
* @stable to call
* @param string $repoDir The root directory of the repo where .git can be found
* @param bool $usePrecomputed Use precomputed information if available
* @see precomputeValues
*/
public function __construct( $repoDir, $usePrecomputed = true ) {
$this->repoDir = $repoDir;
$this->cacheFile = self::getCacheFilePath( $repoDir );
wfDebugLog( 'gitinfo',
"Candidate cacheFile={$this->cacheFile} for {$repoDir}"
);
if ( $usePrecomputed &&
$this->cacheFile !== null &&
is_readable( $this->cacheFile )
) {
$this->cache = FormatJson::decode(
file_get_contents( $this->cacheFile ),
true
);
wfDebugLog( 'gitinfo', "Loaded git data from cache for {$repoDir}" );
}
if ( !$this->cacheIsComplete() ) {
wfDebugLog( 'gitinfo', "Cache incomplete for {$repoDir}" );
$this->basedir = $repoDir . DIRECTORY_SEPARATOR . '.git';
if ( is_readable( $this->basedir ) && !is_dir( $this->basedir ) ) {
$GITfile = file_get_contents( $this->basedir );
if ( strlen( $GITfile ) > 8 &&
substr( $GITfile, 0, 8 ) === 'gitdir: '
) {
$path = rtrim( substr( $GITfile, 8 ), "\r\n" );
if ( $path[0] === '/' || substr( $path, 1, 1 ) === ':' ) {
// Path from GITfile is absolute
$this->basedir = $path;
} else {
$this->basedir = $repoDir . DIRECTORY_SEPARATOR . $path;
}
}
}
}
}
/**
* Compute the path to the cache file for a given directory.
*
* @param string $repoDir The root directory of the repo where .git can be found
* @return string Path to GitInfo cache file in $wgGitInfoCacheDirectory or
* fallback in the extension directory itself
* @since 1.24
*/
protected static function getCacheFilePath( $repoDir ) {
$config = MediaWikiServices::getInstance()->getMainConfig();
$gitInfoCacheDirectory = $config->get( 'GitInfoCacheDirectory' );
$baseDir = $config->get( 'BaseDirectory' );
if ( $gitInfoCacheDirectory ) {
// Convert both $IP and $repoDir to canonical paths to protect against
// $IP having changed between the settings files and runtime.
$realIP = realpath( $baseDir );
$repoName = realpath( $repoDir );
if ( $repoName === false ) {
// Unit tests use fake path names
$repoName = $repoDir;
}
if ( strpos( $repoName, $realIP ) === 0 ) {
// Strip $IP from path
$repoName = substr( $repoName, strlen( $realIP ) );
}
// Transform path to git repo to something we can safely embed in
// a filename
$repoName = strtr( $repoName, DIRECTORY_SEPARATOR, '-' );
$fileName = 'info' . $repoName . '.json';
$cachePath = "{$gitInfoCacheDirectory}/{$fileName}";
if ( is_readable( $cachePath ) ) {
return $cachePath;
}
}
return "$repoDir/gitinfo.json";
}
/**
* Get the singleton for the repo at MW_INSTALL_PATH
*
* @return GitInfo
*/
public static function repo() {
if ( self::$repo === null ) {
self::$repo = new self( MW_INSTALL_PATH );
}
return self::$repo;
}
/**
* Check if a string looks like a hex encoded SHA1 hash
*
* @param string $str The string to check
* @return bool Whether or not the string looks like a SHA1
*/
public static function isSHA1( $str ) {
return (bool)preg_match( '/^[0-9A-F]{40}$/i', $str );
}
/**
* Get the HEAD of the repo (without any opening "ref: ")
*
* @return string|bool The HEAD (git reference or SHA1) or false
*/
public function getHead() {
if ( !isset( $this->cache['head'] ) ) {
$headFile = "{$this->basedir}/HEAD";
$head = false;
if ( is_readable( $headFile ) ) {
$head = file_get_contents( $headFile );
if ( preg_match( "/ref: (.*)/", $head, $m ) ) {
$head = rtrim( $m[1] );
} else {
$head = rtrim( $head );
}
}
$this->cache['head'] = $head;
}
return $this->cache['head'];
}
/**
* Get the SHA1 for the current HEAD of the repo
*
* @return string|bool A SHA1 or false
*/
public function getHeadSHA1() {
if ( !isset( $this->cache['headSHA1'] ) ) {
$head = $this->getHead();
$sha1 = false;
// If detached HEAD may be a SHA1
if ( self::isSHA1( $head ) ) {
$sha1 = $head;
} else {
// If not a SHA1 it may be a ref:
$refFile = "{$this->basedir}/{$head}";
$packedRefs = "{$this->basedir}/packed-refs";
$headRegex = preg_quote( $head, '/' );
if ( is_readable( $refFile ) ) {
$sha1 = rtrim( file_get_contents( $refFile ) );
} elseif ( is_readable( $packedRefs ) &&
preg_match( "/^([0-9A-Fa-f]{40}) $headRegex$/m", file_get_contents( $packedRefs ), $matches )
) {
$sha1 = $matches[1];
}
}
$this->cache['headSHA1'] = $sha1;
}
return $this->cache['headSHA1'];
}
/**
* Get the commit date of HEAD entry of the git code repository
*
* @since 1.22
* @return int|bool Commit date (UNIX timestamp) or false
*/
public function getHeadCommitDate() {
$gitBin = MediaWikiServices::getInstance()->getMainConfig()->get( 'GitBin' );
if ( !isset( $this->cache['headCommitDate'] ) ) {
$date = false;
// Suppress warnings about any open_basedir restrictions affecting $wgGitBin (T74445).
$isFile = AtEase::quietCall( 'is_file', $gitBin );
if ( $isFile &&
is_executable( $gitBin ) &&
!Shell::isDisabled() &&
$this->getHead() !== false
) {
$cmd = [
$gitBin,
'show',
'-s',
'--format=format:%ct',
'HEAD',
];
$gitDir = realpath( $this->basedir );
$result = Shell::command( $cmd )
->environment( [ 'GIT_DIR' => $gitDir ] )
->restrict( Shell::RESTRICT_DEFAULT | Shell::NO_NETWORK )
->whitelistPaths( [ $gitDir, $this->repoDir ] )
->execute();
if ( $result->getExitCode() === 0 ) {
$date = (int)$result->getStdout();
}
}
$this->cache['headCommitDate'] = $date;
}
return $this->cache['headCommitDate'];
}
/**
* Get the name of the current branch, or HEAD if not found
*
* @return string|bool The branch name, HEAD, or false
*/
public function getCurrentBranch() {
if ( !isset( $this->cache['branch'] ) ) {
$branch = $this->getHead();
if ( $branch &&
preg_match( "#^refs/heads/(.*)$#", $branch, $m )
) {
$branch = $m[1];
}
$this->cache['branch'] = $branch;
}
return $this->cache['branch'];
}
/**
* Get an URL to a web viewer link to the HEAD revision.
*
* @return string|bool String if a URL is available or false otherwise
*/
public function getHeadViewUrl() {
$url = $this->getRemoteUrl();
if ( $url === false ) {
return false;
}
foreach ( self::getViewers() as $repo => $viewer ) {
$pattern = '#^' . $repo . '$#';
if ( preg_match( $pattern, $url, $matches ) ) {
$viewerUrl = preg_replace( $pattern, $viewer, $url );
$headSHA1 = $this->getHeadSHA1();
$replacements = [
'%h' => substr( $headSHA1, 0, 7 ),
'%H' => $headSHA1,
'%r' => urlencode( $matches[1] ),
'%R' => $matches[1],
];
return strtr( $viewerUrl, $replacements );
}
}
return false;
}
/**
* Get the URL of the remote origin.
* @return string|bool String if a URL is available or false otherwise.
*/
protected function getRemoteUrl() {
if ( !isset( $this->cache['remoteURL'] ) ) {
$config = "{$this->basedir}/config";
$url = false;
if ( is_readable( $config ) ) {
AtEase::suppressWarnings();
$configArray = parse_ini_file( $config, true );
AtEase::restoreWarnings();
$remote = false;
// Use the "origin" remote repo if available or any other repo if not.
if ( isset( $configArray['remote origin'] ) ) {
$remote = $configArray['remote origin'];
} elseif ( is_array( $configArray ) ) {
foreach ( $configArray as $sectionName => $sectionConf ) {
if ( substr( $sectionName, 0, 6 ) == 'remote' ) {
$remote = $sectionConf;
}
}
}
if ( $remote !== false && isset( $remote['url'] ) ) {
$url = $remote['url'];
}
}
$this->cache['remoteURL'] = $url;
}
return $this->cache['remoteURL'];
}
/**
* Check to see if the current cache is fully populated.
*
* Note: This method is public only to make unit testing easier. There's
* really no strong reason that anything other than a test should want to
* call this method.
*
* @return bool True if all expected cache keys exist, false otherwise
*/
public function cacheIsComplete() {
return isset( $this->cache['head'] ) &&
isset( $this->cache['headSHA1'] ) &&
isset( $this->cache['headCommitDate'] ) &&
isset( $this->cache['branch'] ) &&
isset( $this->cache['remoteURL'] );
}
/**
* Precompute and cache git information.
*
* Creates a JSON file in the cache directory associated with this
* GitInfo instance. This cache file will be used by subsequent GitInfo objects referencing
* the same directory to avoid needing to examine the .git directory again.
*
* @since 1.24
*/
public function precomputeValues() {
if ( $this->cacheFile !== null ) {
// Try to completely populate the cache
$this->getHead();
$this->getHeadSHA1();
$this->getHeadCommitDate();
$this->getCurrentBranch();
$this->getRemoteUrl();
if ( !$this->cacheIsComplete() ) {
wfDebugLog( 'gitinfo',
"Failed to compute GitInfo for \"{$this->basedir}\""
);
return;
}
$cacheDir = dirname( $this->cacheFile );
if ( !file_exists( $cacheDir ) &&
!wfMkdirParents( $cacheDir, null, __METHOD__ )
) {
throw new MWException( "Unable to create GitInfo cache \"{$cacheDir}\"" );
}
file_put_contents( $this->cacheFile, FormatJson::encode( $this->cache ) );
}
}
/**
* @see self::getHeadSHA1
* @return string
*/
public static function headSHA1() {
return self::repo()->getHeadSHA1();
}
/**
* @see self::getCurrentBranch
* @return string
*/
public static function currentBranch() {
return self::repo()->getCurrentBranch();
}
/**
* @see self::getHeadViewUrl()
* @return bool|string
*/
public static function headViewUrl() {
return self::repo()->getHeadViewUrl();
}
/**
* Gets the list of repository viewers
* @return array
*/
protected static function getViewers() {
$gitRepositoryViewers = MediaWikiServices::getInstance()->getMainConfig()->get( 'GitRepositoryViewers' );
if ( self::$viewers === false ) {
self::$viewers = $gitRepositoryViewers;
Hooks::runner()->onGitViewers( self::$viewers );
}
return self::$viewers;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,115 @@
<?php
namespace MediaWiki;
/**
* @since 1.29
*/
class HeaderCallback {
private static $headersSentException;
private static $messageSent = false;
/**
* Register a callback to be called when headers are sent. There can only
* be one of these handlers active, so all relevant actions have to be in
* here.
*
* @since 1.29
*/
public static function register() {
// T261260 load the WebRequest class, which will be needed in callback().
// Autoloading seems unreliable in header callbacks, and in the case of a web
// request (ie. in all cases where the request might be performance-sensitive)
// it will have to be loaded at some point anyway.
// This can be removed once we require PHP 8.0+.
class_exists( \WebRequest::class );
header_register_callback( [ __CLASS__, 'callback' ] );
}
/**
* The callback, which is called by the transport
*
* @since 1.29
*/
public static function callback() {
// Prevent caching of responses with cookies (T127993)
$headers = [];
foreach ( headers_list() as $header ) {
$header = explode( ':', $header, 2 );
// Note: The code below (currently) does not care about value-less headers
if ( isset( $header[1] ) ) {
$headers[ strtolower( trim( $header[0] ) ) ][] = trim( $header[1] );
}
}
if ( isset( $headers['set-cookie'] ) ) {
$cacheControl = isset( $headers['cache-control'] )
? implode( ', ', $headers['cache-control'] )
: '';
if ( !preg_match( '/(?:^|,)\s*(?:private|no-cache|no-store)\s*(?:$|,)/i',
$cacheControl )
) {
header( 'Expires: Thu, 01 Jan 1970 00:00:00 GMT' );
header( 'Cache-Control: private, max-age=0, s-maxage=0' );
\MediaWiki\Logger\LoggerFactory::getInstance( 'cache-cookies' )->warning(
'Cookies set on {url} with Cache-Control "{cache-control}"', [
'url' => \WebRequest::getGlobalRequestURL(),
'set-cookie' => self::sanitizeSetCookie( $headers['set-cookie'] ),
'cache-control' => $cacheControl ?: '<not set>',
]
);
}
}
// Set the request ID on the response, so edge infrastructure can log it.
// FIXME this is not an ideal place to do it, but the most reliable for now.
if ( !isset( $headers['x-request-id'] ) ) {
header( 'X-Request-Id: ' . \WebRequest::getRequestId() );
}
// Save a backtrace for logging in case it turns out that headers were sent prematurely
self::$headersSentException = new \Exception( 'Headers already sent from this point' );
}
/**
* Log a warning message if headers have already been sent. This can be
* called before flushing the output.
*
* @since 1.29
*/
public static function warnIfHeadersSent() {
if ( headers_sent() && !self::$messageSent ) {
self::$messageSent = true;
\MWDebug::warning( 'Headers already sent, should send headers earlier than ' .
wfGetCaller( 3 ) );
$logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'headers-sent' );
$logger->error( 'Warning: headers were already sent from the location below', [
'exception' => self::$headersSentException,
'detection-trace' => new \Exception( 'Detected here' ),
] );
}
}
/**
* Sanitize Set-Cookie headers for logging.
* @param array $values List of header values.
* @return string
*/
public static function sanitizeSetCookie( array $values ) {
$sanitizedValues = [];
foreach ( $values as $value ) {
// Set-Cookie header format: <cookie-name>=<cookie-value>; <non-sensitive attributes>
$parts = explode( ';', $value );
list( $name, $value ) = explode( '=', $parts[0], 2 );
if ( strlen( $value ) > 8 ) {
$value = substr( $value, 0, 8 ) . '...';
$parts[0] = "$name=$value";
}
$sanitizedValues[] = implode( ';', $parts );
}
return implode( "\n", $sanitizedValues );
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace MediaWiki\Hook;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "AfterBuildFeedLinks" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface AfterBuildFeedLinksHook {
/**
* This hook is called in OutputPage.php after all feed links (atom,
* rss,...) are created. Use this hook to omit specific feeds from being outputted.
* You must not use this hook to add feeds; use OutputPage::addFeedLink() instead.
*
* @since 1.35
*
* @param string[] &$feedLinks Array of created feed links
* @return bool|void True or no return value to continue or false to abort
*/
public function onAfterBuildFeedLinks( &$feedLinks );
}

View File

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Hook;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "AfterFinalPageOutput" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface AfterFinalPageOutputHook {
/**
* This hook is called nearly at the end of OutputPage::output() but
* before OutputPage::sendCacheControl() and final ob_end_flush() which
* will send the buffered output to the client. This allows for last-minute
* modification of the output within the buffer by using ob_get_clean().
*
* @since 1.35
*
* @param OutputPage $output The OutputPage object where output() was called
* @return void This hook must not abort, it must return no value
*/
public function onAfterFinalPageOutput( $output ): void;
}

View File

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\Hook;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "AlternateEdit" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface AlternateEditHook {
/**
* This hook is called before checking if a user can edit a page and before showing
* the edit form ( EditPage::edit() ). This is triggered on &action=edit.
*
* @since 1.35
*
* @param EditPage $editPage
* @return bool|void True or no return value to continue or false to abort
*/
public function onAlternateEdit( $editPage );
}

View File

@ -0,0 +1,33 @@
<?php
namespace MediaWiki\Hook;
use Content;
use EditPage;
use ParserOutput;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "AlternateEditPreview" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface AlternateEditPreviewHook {
/**
* This hook is called before generating the preview of the page when editing
* ( EditPage::getPreviewText() ).
*
* @since 1.35
*
* @param EditPage $editPage
* @param Content &$content Content object for the text field from the edit page
* @param string &$previewHTML Text to be placed into the page for the preview
* @param ParserOutput &$parserOutput ParserOutput object for the preview
* @return bool|void True or no return value to continue, or false and set $previewHTML and
* $parserOutput to output custom page preview HTML
*/
public function onAlternateEditPreview( $editPage, &$content, &$previewHTML,
&$parserOutput
);
}

View File

@ -0,0 +1,24 @@
<?php
namespace MediaWiki\Hook;
use ApiMain;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "ApiBeforeMain" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface ApiBeforeMainHook {
/**
* This hook is called before calling ApiMain's execute() method in api.php.
*
* @since 1.35
*
* @param ApiMain &$main
* @return bool|void True or no return value to continue or false to abort
*/
public function onApiBeforeMain( &$main );
}

View File

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\Hook;
use Title;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "ArticleMergeComplete" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface ArticleMergeCompleteHook {
/**
* This hook is called after merging to article using Special:Mergehistory.
*
* @since 1.35
*
* @param Title $targetTitle
* @param Title $destTitle Destination title
* @return bool|void True or no return value to continue or false to abort
*/
public function onArticleMergeComplete( $targetTitle, $destTitle );
}

View File

@ -0,0 +1,29 @@
<?php
namespace MediaWiki\Hook;
use Article;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "ArticleUpdateBeforeRedirect" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface ArticleUpdateBeforeRedirectHook {
/**
* This hook is called after a page is updated (usually on save), before
* the user is redirected back to the page.
*
* @since 1.35
*
* @param Article $article
* @param string &$sectionanchor Section anchor link (e.g. "#overview" )
* @param string &$extraq Extra query parameters which can be added via hooked functions
* @return bool|void True or no return value to continue or false to abort
*/
public function onArticleUpdateBeforeRedirect( $article, &$sectionanchor,
&$extraq
);
}

View File

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\Hook;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "BadImage" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface BadImageHook {
/**
* This hook is called when checking against the bad image list. If an image is "bad",
* it is not rendered inline in wiki pages or galleries in category pages.
*
* @since 1.35
*
* @param string $name Image name being checked
* @param bool &$bad Whether or not the image is "bad"
* @return bool|void True or no return value to continue, or false and change $bad
* to override
*/
public function onBadImage( $name, &$bad );
}

View File

@ -0,0 +1,35 @@
<?php
namespace MediaWiki\Hook;
use MediaWiki;
use OutputPage;
use Title;
use User;
use WebRequest;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "BeforeInitialize" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface BeforeInitializeHook {
/**
* This hook is called before anything is initialized in MediaWiki::performRequest().
*
* @since 1.35
*
* @param Title $title Title being used for request
* @param null $unused
* @param OutputPage $output
* @param User $user
* @param WebRequest $request
* @param MediaWiki $mediaWiki
* @return bool|void True or no return value to continue or false to abort
*/
public function onBeforeInitialize( $title, $unused, $output, $user, $request,
$mediaWiki
);
}

View File

@ -0,0 +1,26 @@
<?php
namespace MediaWiki\Hook;
use OutputPage;
use Skin;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "BeforePageDisplay" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface BeforePageDisplayHook {
/**
* This hook is called prior to outputting a page.
*
* @since 1.35
*
* @param OutputPage $out
* @param Skin $skin
* @return void This hook must not abort, it must return no value
*/
public function onBeforePageDisplay( $out, $skin ): void;
}

View File

@ -0,0 +1,28 @@
<?php
namespace MediaWiki\Hook;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "BeforePageRedirect" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface BeforePageRedirectHook {
/**
* This hook is called prior to sending an HTTP redirect. Gives a chance to
* override how the redirect is output by modifying, or by returning false and
* taking over the output.
*
* @since 1.35
*
* @param OutputPage $out
* @param string &$redirect URL, modifiable
* @param string &$code HTTP code (eg '301' or '302'), modifiable
* @return bool|void True or no return value to continue or false to abort
*/
public function onBeforePageRedirect( $out, &$redirect, &$code );
}

View File

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Hook;
use Wikimedia\Rdbms\IResultWrapper;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "CategoryViewer::doCategoryQuery" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface CategoryViewer__doCategoryQueryHook {
/**
* This hook is called after querying for pages to be displayed in a Category page.
* Use this hook to batch load any related data about the pages.
*
* @since 1.35
*
* @param string $type Category type, either 'page', 'file', or 'subcat'
* @param IResultWrapper $res Query result from Wikimedia\Rdbms\IDatabase::select()
* @return bool|void True or no return value to continue or false to abort
*/
public function onCategoryViewer__doCategoryQuery( $type, $res );
}

View File

@ -0,0 +1,30 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use Title;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "CategoryViewer::generateLink" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface CategoryViewer__generateLinkHook {
/**
* This hook is called before generating an output link allow
* extensions opportunity to generate a more specific or relevant link.
*
* @since 1.35
*
* @param string $type Category type, either 'page', 'img', or 'subcat'
* @param Title $title Categorized page
* @param string $html Requested HTML content of anchor
* @param string &$link Returned value. When set to a non-null value by a hook subscriber,
* this value will be used as the anchor instead of Linker::link.
* @return bool|void True or no return value to continue or false to abort
*/
public function onCategoryViewer__generateLink( $type, $title, $html, &$link );
}

View File

@ -0,0 +1,29 @@
<?php
namespace MediaWiki\Hook;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "ContentSecurityPolicyDefaultSource" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface ContentSecurityPolicyDefaultSourceHook {
/**
* Use this hook to modify the allowed CSP load sources. This affects all
* directives except for the script directive. To add a script source, see
* ContentSecurityPolicyScriptSource hook.
*
* @since 1.35
*
* @param string[] &$defaultSrc Array of Content-Security-Policy allowed sources
* @param array $policyConfig Current configuration for the Content-Security-Policy header
* @param int $mode ContentSecurityPolicy::REPORT_ONLY_MODE or
* ContentSecurityPolicy::FULL_MODE depending on type of header
* @return bool|void True or no return value to continue or false to abort
*/
public function onContentSecurityPolicyDefaultSource( &$defaultSrc,
$policyConfig, $mode
);
}

View File

@ -0,0 +1,28 @@
<?php
namespace MediaWiki\Hook;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "ContentSecurityPolicyDirectives" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface ContentSecurityPolicyDirectivesHook {
/**
* If ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource
* do not meet your needs, use this hook to modify the content security policy directives.
*
* @since 1.35
*
* @param string[] &$directives Array of CSP directives
* @param array $policyConfig Current configuration for the CSP header
* @param int $mode ContentSecurityPolicy::REPORT_ONLY_MODE or
* ContentSecurityPolicy::FULL_MODE depending on type of header
* @return bool|void True or no return value to continue or false to abort
*/
public function onContentSecurityPolicyDirectives( &$directives, $policyConfig,
$mode
);
}

View File

@ -0,0 +1,29 @@
<?php
namespace MediaWiki\Hook;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "ContentSecurityPolicyScriptSource" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface ContentSecurityPolicyScriptSourceHook {
/**
* Use this hook to modify the allowed CSP script sources.
* Note that you also have to use ContentSecurityPolicyDefaultSource if you
* want non-script sources to be loaded from whatever you add.
*
* @since 1.35
*
* @param string[] &$scriptSrc Array of CSP directives
* @param array $policyConfig Current configuration for the CSP header
* @param int $mode ContentSecurityPolicy::REPORT_ONLY_MODE or
* ContentSecurityPolicy::FULL_MODE depending on type of header
* @return bool|void True or no return value to continue or false to abort
*/
public function onContentSecurityPolicyScriptSource( &$scriptSrc,
$policyConfig, $mode
);
}

View File

@ -0,0 +1,32 @@
<?php
namespace MediaWiki\Hook;
use Wikimedia\Rdbms\IDatabase;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "DeleteUnknownPreferences" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface DeleteUnknownPreferencesHook {
/**
* This hook is called by the cleanupPreferences.php maintenance script
* to build a WHERE clause with which to delete preferences that are not
* known about. This hook is used by extensions that have dynamically-named
* preferences that should not be deleted in the usual cleanup process.
* For example, the Gadgets extension creates preferences prefixed with
* 'gadget-', so anything with that prefix is excluded from the deletion.
*
* @since 1.35
*
* @param array &$where Array that will be passed as the $cond parameter to
* IDatabase::select() to determine what will be deleted from the user_properties
* table
* @param IDatabase $db IDatabase object, useful for accessing $db->buildLike() etc.
* @return bool|void True or no return value to continue or false to abort
*/
public function onDeleteUnknownPreferences( &$where, $db );
}

View File

@ -0,0 +1,28 @@
<?php
namespace MediaWiki\Hook;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditFilter" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditFilterHook {
/**
* Use this hook to perform checks on an edit.
*
* @since 1.35
*
* @param EditPage $editor Edit form (see includes/EditPage.php)
* @param string $text Contents of the edit box
* @param string $section Section being edited
* @param string &$error Error message to return
* @param string $summary Edit summary for page
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditFilter( $editor, $text, $section, &$error, $summary );
}

View File

@ -0,0 +1,41 @@
<?php
namespace MediaWiki\Hook;
use Content;
use IContextSource;
use Status;
use User;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditFilterMergedContent" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditFilterMergedContentHook {
/**
* Use this hook for a post-section-merge edit filter. This may be triggered by
* the EditPage or any other facility that modifies page content. Use the return value
* to indicate whether the edit should be allowed, and use the $status object to provide
* a reason for disallowing it. $status->apiHookResult can be set to an array to be returned
* by api.php action=edit. This is used to deliver captchas.
*
* @since 1.35
*
* @param IContextSource $context
* @param Content $content Content of the edit box
* @param Status $status Status object to represent errors, etc.
* @param string $summary Edit summary for page
* @param User $user User whois performing the edit
* @param bool $minoredit Whether the edit was marked as minor by the user.
* @return bool|void False or no return value with not $status->isOK() to abort the edit
* and show the edit form, true to continue. But because multiple triggers of this hook
* may have different behavior in different version (T273354), you'd better return false
* and set $status->value to EditPage::AS_HOOK_ERROR_EXPECTED or any other customized value.
*/
public function onEditFilterMergedContent( IContextSource $context, Content $content, Status $status,
$summary, User $user, $minoredit
);
}

View File

@ -0,0 +1,24 @@
<?php
namespace MediaWiki\Hook;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditFormInitialText" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditFormInitialTextHook {
/**
* Use this hook to modify the edit form when editing existing pages.
*
* @since 1.35
*
* @param EditPage $editPage
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditFormInitialText( $editPage );
}

View File

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\Hook;
use Title;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditFormPreloadText" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditFormPreloadTextHook {
/**
* Use this hook to populate the edit form when creating pages.
*
* @since 1.35
*
* @param string &$text Text to preload with
* @param Title $title Page being created
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditFormPreloadText( &$text, $title );
}

View File

@ -0,0 +1,28 @@
<?php
namespace MediaWiki\Hook;
use EditPage;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageBeforeConflictDiff" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageBeforeConflictDiffHook {
/**
* Use this hook to modify the EditPage object and output when there's an edit conflict.
*
* @since 1.35
*
* @param EditPage $editor
* @param OutputPage $out
* @return bool|void True or no return value to continue. False to halt normal diff output;
* in this case you're responsible for computing and outputting the entire "conflict" part,
* i.e., the "difference between revisions" and "your text" headers and sections.
*/
public function onEditPageBeforeConflictDiff( $editor, $out );
}

View File

@ -0,0 +1,26 @@
<?php
namespace MediaWiki\Hook;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageBeforeEditButtons" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageBeforeEditButtonsHook {
/**
* Use this hook to modify the edit buttons below the textarea in the edit form.
*
* @since 1.35
*
* @param EditPage $editpage Current EditPage object
* @param array &$buttons Array of edit buttons, "Save", "Preview", "Live", and "Diff"
* @param int &$tabindex HTML tabindex of the last edit check/button
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPageBeforeEditButtons( $editpage, &$buttons, &$tabindex );
}

View File

@ -0,0 +1,23 @@
<?php
namespace MediaWiki\Hook;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageBeforeEditToolbar" to register handlers implementing this interface.
*
* @deprecated since 1.36 Use one of the many other EditPage hooks instead
* @ingroup Hooks
*/
interface EditPageBeforeEditToolbarHook {
/**
* Use this hook to add an edit toolbar above the textarea in the edit form.
*
* @since 1.35
*
* @param string &$toolbar Toolbar HTML, initially an empty `<div id="toolbar"></div>`
* @return bool|void True or no return value to continue, or false to have
* no toolbar HTML be loaded
*/
public function onEditPageBeforeEditToolbar( &$toolbar );
}

View File

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Hook;
use Title;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageCopyrightWarning" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageCopyrightWarningHook {
/**
* Use this hook for site and per-namespace customization of contribution/copyright notice.
*
* @since 1.35
*
* @param Title $title Title of page being edited
* @param array &$msg An array of arguments to wfMessage(), overridable.
* The default is an array containing either 'copyrightwarning' or
* 'copyrightwarning2' as the first element (the message key).
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPageCopyrightWarning( $title, &$msg );
}

View File

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Hook;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageGetCheckboxesDefinition" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageGetCheckboxesDefinitionHook {
/**
* Use this hook to modify the edit checkboxes and other form fields
* below the textarea in the edit form.
*
* @since 1.35
*
* @param EditPage $editpage Current EditPage object
* @param array &$checkboxes Array of checkbox definitions. See
* EditPage::getCheckboxesDefinition() for the format.
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPageGetCheckboxesDefinition( $editpage, &$checkboxes );
}

View File

@ -0,0 +1,28 @@
<?php
namespace MediaWiki\Hook;
use Content;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageGetDiffContent" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageGetDiffContentHook {
/**
* Use this hook to modify the wikitext that will be used in "Show changes".
* Note that it is preferable to implement diff handling for different data types
* using the ContentHandler facility.
*
* @since 1.35
*
* @param EditPage $editPage
* @param Content &$newtext Content that will be used in place of "Show changes"
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPageGetDiffContent( $editPage, &$newtext );
}

View File

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Hook;
use Content;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageGetPreviewContent" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageGetPreviewContentHook {
/**
* Use this hook to modify the wikitext that will be previewed. Note that it is preferable
* to implement previews for different data types using the ContentHandler facility.
*
* @since 1.35
*
* @param EditPage $editPage
* @param Content &$content Content object to be previewed (may be replaced by hook function)
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPageGetPreviewContent( $editPage, &$content );
}

View File

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\Hook;
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageNoSuchSection" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageNoSuchSectionHook {
/**
* This hook is called when a section edit request is given for an non-existent section.
*
* @since 1.35
*
* @param EditPage $editpage Current EditPage object
* @param string &$res HTML of the error text
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPageNoSuchSection( $editpage, &$res );
}

View File

@ -0,0 +1,26 @@
<?php
namespace MediaWiki\Hook;
use Title;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPageTosSummary" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPageTosSummaryHook {
/**
* Use this hook for site and per-namespace customizations of terms of service summary link
* that might exist separately from the copyright notice.
*
* @since 1.35
*
* @param Title $title Title of page being edited
* @param string &$msg Localization message name, overridable. Defaults to 'editpage-tos-summary'
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPageTosSummary( $title, &$msg );
}

View File

@ -0,0 +1,25 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use EditPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPage::attemptSave" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPage__attemptSaveHook {
/**
* This hook is called before an article is saved, before WikiPage::doUserEditContent() is called.
*
* @since 1.35
*
* @param EditPage $editpage_Obj Current EditPage object
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPage__attemptSave( $editpage_Obj );
}

View File

@ -0,0 +1,30 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use EditPage;
use Status;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPage::attemptSave:after" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPage__attemptSave_afterHook {
/**
* This hook is called after an article save attempt.
*
* @since 1.35
*
* @param EditPage $editpage_Obj Current EditPage object
* @param Status $status Resulting Status object
* @param array $resultDetails Result details array
* @return bool|void True or no return value to continue or false to abort
*/
public function onEditPage__attemptSave_after( $editpage_Obj, $status,
$resultDetails
);
}

View File

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use EditPage;
use WebRequest;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPage::importFormData" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPage__importFormDataHook {
/**
* Use this hook to read additional data posted in the form.
*
* @since 1.35
*
* @param EditPage $editpage
* @param WebRequest $request
* @return bool|void Return value is ignored; this hook should always return true
*/
public function onEditPage__importFormData( $editpage, $request );
}

View File

@ -0,0 +1,27 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use EditPage;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPage::showEditForm:fields" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPage__showEditForm_fieldsHook {
/**
* Use this hook to inject form field into edit form.
*
* @since 1.35
*
* @param EditPage $editor EditPage instance for reference
* @param OutputPage $out OutputPage instance to write to
* @return bool|void Return value is ignored; this hook should always return true
*/
public function onEditPage__showEditForm_fields( $editor, $out );
}

View File

@ -0,0 +1,30 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use EditPage;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPage::showEditForm:initial" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPage__showEditForm_initialHook {
/**
* This hook is called before showing the edit form.
*
* @since 1.35
*
* @param EditPage $editor
* @param OutputPage $out OutputPage instance to write to
* @return bool|void True or no return value without altering $error to allow the
* edit to continue. Modifying $error and returning true will cause the contents
* of $error to be echoed at the top of the edit form as wikitext. Return false
* to halt editing; you'll need to handle error messages, etc. yourself.
*/
public function onEditPage__showEditForm_initial( $editor, $out );
}

View File

@ -0,0 +1,28 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use EditPage;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPage::showReadOnlyForm:initial" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPage__showReadOnlyForm_initialHook {
/**
* This hook is similar to EditPage::showEditForm:initial
* but for the read-only 'view source' variant of the edit form.
*
* @since 1.35
*
* @param EditPage $editor
* @param OutputPage $out OutputPage instance to write to
* @return bool|void Return value is ignored; this hook should always return true
*/
public function onEditPage__showReadOnlyForm_initial( $editor, $out );
}

View File

@ -0,0 +1,30 @@
<?php
namespace MediaWiki\Hook;
// phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps
use EditPage;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "EditPage::showStandardInputs:options" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface EditPage__showStandardInputs_optionsHook {
/**
* Use this hook to inject form fields into the editOptions area.
*
* @since 1.35
*
* @param EditPage $editor
* @param OutputPage $out OutputPage instance to write to
* @param int &$tabindex HTML tabindex of the last edit check/button
* @return bool|void Return value is ignored; this hook should always return true
*/
public function onEditPage__showStandardInputs_options( $editor, $out,
&$tabindex
);
}

View File

@ -0,0 +1,33 @@
<?php
namespace MediaWiki\Hook;
use LocalFile;
use User;
use WikiFilePage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "FileDeleteComplete" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface FileDeleteCompleteHook {
/**
* This hook is called when a file is deleted.
*
* @since 1.35
*
* @param LocalFile $file Reference to the deleted file
* @param string|null $oldimage In case of the deletion of an old image, the name of the old file
* @param WikiFilePage|null $article In case all revisions of the file are deleted, a reference to
* the WikiFilePage associated with the file
* @param User $user User who performed the deletion
* @param string $reason
* @return bool|void True or no return value to continue or false to abort
*/
public function onFileDeleteComplete( $file, $oldimage, $article, $user,
$reason
);
}

View File

@ -0,0 +1,35 @@
<?php
namespace MediaWiki\Hook;
use Title;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "FormatAutocomments" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface FormatAutocommentsHook {
/**
* This hook is called when an autocomment is formatted by the Linker.
*
* @since 1.35
*
* @param string|null &$comment Reference to the accumulated comment.
* Initially null, when set the default code will be skipped.
* @param bool $pre True if there is text before this autocomment
* @param string $auto Extracted part of the parsed comment before the call to the hook
* @param bool $post True if there is text after this autocomment
* @param Title|null $title Optional title object used to links to sections
* @param bool $local Whether section links should refer to local page
* @param string|null $wikiId ID (as used by WikiMap) of the wiki from which the
* autocomment originated; null for the local wiki. Added in 1.26, should default
* to null in handler functions, for backwards compatibility.
* @return bool|void True or no return value to continue or false to abort
*/
public function onFormatAutocomments( &$comment, $pre, $auto, $post, $title,
$local, $wikiId
);
}

View File

@ -0,0 +1,26 @@
<?php
namespace MediaWiki\Hook;
use OutputPage;
/**
* This is a hook handler interface, see docs/Hooks.md.
* Use the hook name "GetCacheVaryCookies" to register handlers implementing this interface.
*
* @stable to implement
* @ingroup Hooks
*/
interface GetCacheVaryCookiesHook {
/**
* Use this hook to get cookies that should vary cache options.
*
* @since 1.35
*
* @param OutputPage $out
* @param string[] &$cookies Array of cookie names. Add a value to it if you
* want to add a cookie that has to vary cache options.
* @return bool|void True or no return value to continue or false to abort
*/
public function onGetCacheVaryCookies( $out, &$cookies );
}

Some files were not shown because too many files have changed in this diff Show More