Initial Commit
This commit is contained in:
parent
d6ec1121f3
commit
7227036840
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
$mtDbSuffix = '_mw1384';
|
||||
$mtReadOnly = false;
|
|
@ -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' );
|
||||
};
|
||||
};
|
||||
|
|
@ -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);
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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";
|
|
@ -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"
|
||||
];
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
//$mtDBSuffix = "other_";
|
||||
$mtReadOnly = false;
|
||||
//$mtReadOnlyMessage = "This is a separate romsg";
|
||||
$wgShowExceptionDetails = true;
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
$wgGroupPermissions['*']['createaccount'] = false;
|
||||
$wgGroupPermissions['*']['edit'] = false;
|
||||
$wgGroupPermissions['user']['edit'] = true;
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
$wgAuthenticationTokenVersion = "1";
|
||||
|
||||
$wgDBpassword = "";
|
||||
|
||||
$wgSecretKey = "";
|
||||
$wgUpgradeKey = "";
|
|
@ -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;
|
|
@ -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.
|
|
@ -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");
|
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extra": {
|
||||
"merge-plugin": {
|
||||
"include": [
|
||||
"extensions/*/composer.json",
|
||||
"skins/*/composer.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
] );
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Deny from all
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -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' ] );
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 ) );
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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( '[[', '[[', $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] ),
|
||||
[ '<' => '<', '>' => '>' ]
|
||||
);
|
||||
}
|
||||
|
||||
// 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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() );
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,594 @@
|
|||
<?php
|
||||
/**
|
||||
* Handle sending Content-Security-Policy headers
|
||||
*
|
||||
* @see https://www.w3.org/TR/CSP2/
|
||||
*
|
||||
* Copyright © 2015–2018 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
|
@ -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' );
|
||||
/** @} */
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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
|
@ -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();
|
||||
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
@ -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 );
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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 );
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue