MediaWiki  master
SignatureValidator.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Preferences;
22 
23 use Html;
30 use MultiHttpClient;
31 use Parser;
32 use ParserOptions;
34 use SpecialPage;
35 use TitleFactory;
37 
42 
44  private const CONSTRUCTOR_OPTIONS = [
45  'SignatureAllowedLintErrors',
46  'VirtualRestConfig',
47  ];
48 
50  private $user;
52  private $localizer;
54  private $popts;
56  private $parser;
58  private $serviceOptions;
62  private $titleFactory;
63 
70  $this->user = $user;
71  $this->localizer = $localizer;
72  $this->popts = $popts;
73 
74  // TODO inject these
75  $services = MediaWikiServices::getInstance();
76  // Fetch the parser, will be used to create a new parser via getFreshParser() when needed
77  $this->parser = $services->getParser();
78  // Configuration
79  $this->serviceOptions = new ServiceOptions(
80  self::CONSTRUCTOR_OPTIONS,
81  $services->getMainConfig()
82  );
83  $this->serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
84  // Services
85  $this->specialPageFactory = $services->getSpecialPageFactory();
86  $this->titleFactory = $services->getTitleFactory();
87 
88  // TODO SpecialPage::getTitleFor should also be available via SpecialPageFactory
89  }
90 
96  public function validateSignature( string $signature ) {
97  $pstSignature = $this->applyPreSaveTransform( $signature );
98  if ( $pstSignature === false ) {
99  // Return early because the rest of the validation uses wikitext parsing, which requires
100  // the pre-save transform to be applied first, and we just found out that the result of the
101  // pre-save transform would require *another* pre-save transform, which is crazy
102  if ( $this->localizer ) {
103  return [ $this->localizer->msg( 'badsigsubst' )->parse() ];
104  }
105  return true;
106  }
107 
108  $pstWasApplied = false;
109  if ( $pstSignature !== $signature ) {
110  $pstWasApplied = true;
111  $signature = $pstSignature;
112  }
113 
114  $errors = $this->localizer ? [] : false;
115 
116  $lintErrors = $this->checkLintErrors( $signature );
117  if ( $lintErrors ) {
118  $allowedLintErrors = $this->serviceOptions->get( 'SignatureAllowedLintErrors' );
119  $messages = '';
120 
121  foreach ( $lintErrors as $error ) {
122  if ( $error['type'] === 'multiple-unclosed-formatting-tags' ) {
123  // Always appears with 'missing-end-tag', we can ignore it to simplify the error message
124  continue;
125  }
126  if ( in_array( $error['type'], $allowedLintErrors, true ) ) {
127  continue;
128  }
129  if ( !$this->localizer ) {
130  $errors = true;
131  break;
132  }
133 
134  $details = $this->getLintErrorDetails( $error );
135  $location = $this->getLintErrorLocation( $error );
136  // Messages used here:
137  // * linterror-bogus-image-options
138  // * linterror-deletable-table-tag
139  // * linterror-html5-misnesting
140  // * linterror-misc-tidy-replacement-issues
141  // * linterror-misnested-tag
142  // * linterror-missing-end-tag
143  // * linterror-multi-colon-escape
144  // * linterror-multiline-html-table-in-list
145  // * linterror-multiple-unclosed-formatting-tags
146  // * linterror-obsolete-tag
147  // * linterror-pwrap-bug-workaround
148  // * linterror-self-closed-tag
149  // * linterror-stripped-tag
150  // * linterror-tidy-font-bug
151  // * linterror-tidy-whitespace-bug
152  // * linterror-unclosed-quotes-in-heading
153  $label = $this->localizer->msg( "linterror-{$error['type']}" )->parse();
154  $docsLink = new \OOUI\ButtonWidget( [
155  'href' =>
156  "https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Lint_errors/{$error['type']}",
157  'target' => '_blank',
158  'label' => $this->localizer->msg( 'prefs-signature-error-details' )->text(),
159  ] );
160 
161  // If pre-save transform was applied (i.e., the signature has 'subst:' syntax),
162  // error locations will be incorrect, because Parsoid can't expand templates.
163  // Don't display them.
164  $encLocation = $pstWasApplied ? null : json_encode( $location );
165 
166  $messages .= Html::rawElement(
167  'li',
168  [ 'data-mw-lint-error-location' => $encLocation ],
169  $label . $this->localizer->msg( 'colon-separator' )->escaped() .
170  $details . ' ' . $docsLink
171  );
172  }
173 
174  if ( $messages && $this->localizer ) {
175  $errors[] = $this->localizer->msg( 'badsightml' )->parse() .
176  Html::rawElement( 'ol', [], $messages );
177  }
178  }
179 
180  if ( !$this->checkUserLinks( $signature ) ) {
181  if ( $this->localizer ) {
182  $userText = wfEscapeWikiText( $this->user->getName() );
183  $linkWikitext = $this->localizer->msg( 'signature', $userText, $userText )->inContentLanguage()->text();
184  $errors[] = $this->localizer->msg( 'badsiglinks', wfEscapeWikiText( $linkWikitext ) )->parse();
185  } else {
186  $errors = true;
187  }
188  }
189 
190  if ( !$this->checkLineBreaks( $signature ) ) {
191  if ( $this->localizer ) {
192  $errors[] = $this->localizer->msg( 'badsiglinebreak' )->parse();
193  } else {
194  $errors = true;
195  }
196  }
197 
198  return $errors;
199  }
200 
206  protected function applyPreSaveTransform( string $signature ) {
207  // This may be called by the Parser when it's displaying a signature, so we need a new instance
208  $parser = $this->parser->getFreshParser();
209 
210  $pstSignature = $parser->preSaveTransform(
211  $signature,
212  SpecialPage::getTitleFor( 'Preferences' ),
213  $this->user,
214  $this->popts
215  );
216 
217  // The signature wikitext contains another '~~~~' or similar (T230652)
218  if ( $parser->getOutput()->getOutputFlag( ParserOutputFlags::USER_SIGNATURE ) ) {
219  return false;
220  }
221 
222  // The signature wikitext contains '{{subst:...}}' markup that produces another subst (T230652)
223  $pstPstSignature = $parser->preSaveTransform(
224  $pstSignature,
225  SpecialPage::getTitleFor( 'Preferences' ),
226  $this->user,
227  $this->popts
228  );
229  if ( $pstPstSignature !== $pstSignature ) {
230  return false;
231  }
232 
233  return $pstSignature;
234  }
235 
240  protected function checkLintErrors( string $signature ): array {
241  // Real check for mismatched HTML tags in the *output*.
242  // This has to use Parsoid because PHP Parser doesn't produce this information,
243  // it just fixes up the result quietly.
244 
245  // This request is not cached, but that's okay, because $signature is short (other code checks
246  // the length against $wgMaxSigChars).
247 
248  $vrsConfig = $this->serviceOptions->get( 'VirtualRestConfig' );
249  if ( isset( $vrsConfig['modules']['parsoid'] ) ) {
250  $params = $vrsConfig['modules']['parsoid'];
251  if ( isset( $vrsConfig['global'] ) ) {
252  $params = array_merge( $vrsConfig['global'], $params );
253  }
254  $parsoidVrs = new ParsoidVirtualRESTService( $params );
255 
256  $vrsClient = new VirtualRESTServiceClient( new MultiHttpClient( [] ) );
257  $vrsClient->mount( '/parsoid/', $parsoidVrs );
258 
259  $request = [
260  'method' => 'POST',
261  'url' => '/parsoid/local/v3/transform/wikitext/to/lint',
262  'body' => [
263  'wikitext' => $signature,
264  ],
265  'headers' => [
266  // Are both of these are really needed?
267  'User-Agent' => 'MediaWiki/' . MW_VERSION,
268  'Api-User-Agent' => 'MediaWiki/' . MW_VERSION,
269  ],
270  ];
271 
272  $response = $vrsClient->run( $request );
273  if ( $response['code'] === 200 ) {
274  $json = json_decode( $response['body'], true );
275  // $json is an array of error objects
276  if ( $json ) {
277  return $json;
278  }
279  }
280  }
281 
282  return [];
283  }
284 
289  protected function checkUserLinks( string $signature ): bool {
290  // This may be called by the Parser when it's displaying a signature, so we need a new instance
291  $parser = $this->parser->getFreshParser();
292 
293  // Check for required links. This one's easier to do with the PHP Parser.
294  $pout = $parser->parse(
295  $signature,
296  SpecialPage::getTitleFor( 'Preferences' ),
297  $this->popts
298  );
299 
300  // Checking user or talk links is easy
301  $links = $pout->getLinks();
302  $username = $this->user->getName();
303  if (
304  isset( $links[ NS_USER ][ strtr( $username, ' ', '_' ) ] ) ||
305  isset( $links[ NS_USER_TALK ][ strtr( $username, ' ', '_' ) ] )
306  ) {
307  return true;
308  }
309 
310  // Checking the contributions link is harder, because the special page name and the username in
311  // the "subpage parameter" are not normalized for us.
312  $splinks = $pout->getLinksSpecial();
313  foreach ( $splinks as $dbkey => $unused ) {
314  list( $name, $subpage ) = $this->specialPageFactory->resolveAlias( $dbkey );
315  if ( $name === 'Contributions' && $subpage ) {
316  $userTitle = $this->titleFactory->makeTitleSafe( NS_USER, $subpage );
317  if ( $userTitle && $userTitle->getText() === $username ) {
318  return true;
319  }
320  }
321  }
322 
323  return false;
324  }
325 
330  protected function checkLineBreaks( string $signature ): bool {
331  return !preg_match( "/[\r\n]/", $signature );
332  }
333 
334  // Adapted from the Linter extension
335  private function getLintErrorLocation( array $lintError ): array {
336  return array_slice( $lintError['dsr'], 0, 2 );
337  }
338 
339  // Adapted from the Linter extension
340  private function getLintErrorDetails( array $lintError ): string {
341  [ 'type' => $type, 'params' => $params ] = $lintError;
342 
343  if ( $type === 'bogus-image-options' && isset( $params['items'] ) ) {
344  $list = array_map( static function ( $in ) {
345  return Html::element( 'code', [], $in );
346  }, $params['items'] );
347  return implode(
348  $this->localizer->msg( 'comma-separator' )->escaped(),
349  $list
350  );
351  } elseif ( $type === 'pwrap-bug-workaround' &&
352  isset( $params['root'] ) &&
353  isset( $params['child'] ) ) {
354  return Html::element( 'code', [],
355  $params['root'] . " > " . $params['child'] );
356  } elseif ( $type === 'tidy-whitespace-bug' &&
357  isset( $params['node'] ) &&
358  isset( $params['sibling'] ) ) {
359  return Html::element( 'code', [],
360  $params['node'] . " + " . $params['sibling'] );
361  } elseif ( $type === 'multi-colon-escape' &&
362  isset( $params['href'] ) ) {
363  return Html::element( 'code', [], $params['href'] );
364  } elseif ( $type === 'multiline-html-table-in-list' ) {
365  /* ancestor and name will be set */
366  return Html::element( 'code', [],
367  $params['ancestorName'] . " > " . $params['name'] );
368  } elseif ( $type === 'misc-tidy-replacement-issues' ) {
369  /* There will be a 'subtype' param to disambiguate */
370  return Html::element( 'code', [], $params['subtype'] );
371  } elseif ( $type === 'missing-end-tag' ) {
372  return Html::element( 'code', [], '</' . $params['name'] . '>' );
373  } elseif ( isset( $params['name'] ) ) {
374  return Html::element( 'code', [], $params['name'] );
375  }
376 
377  return '';
378  }
379 
380 }
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:45
MultiHttpClient
Class to handle multiple HTTP requests.
Definition: MultiHttpClient.php:55
MediaWiki\Preferences\SignatureValidator\applyPreSaveTransform
applyPreSaveTransform(string $signature)
Definition: SignatureValidator.php:206
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:200
MediaWiki\Preferences\SignatureValidator\$serviceOptions
ServiceOptions $serviceOptions
Definition: SignatureValidator.php:58
Parser\ParserOutputFlags
Definition: ParserOutputFlags.php:41
MediaWiki\SpecialPage\SpecialPageFactory
Factory for handling the special page list and generating SpecialPage objects.
Definition: SpecialPageFactory.php:64
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:36
MediaWiki\Preferences\SignatureValidator\checkLineBreaks
checkLineBreaks(string $signature)
Definition: SignatureValidator.php:330
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:107
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
MessageLocalizer
Interface for localizing messages in MediaWiki.
Definition: MessageLocalizer.php:29
Parser\preSaveTransform
preSaveTransform( $text, PageReference $page, UserIdentity $user, ParserOptions $options, $clearState=true)
Transform wiki markup when saving a page by doing "\\r\\n" -> "\\n" conversion, substituting signatur...
Definition: Parser.php:4509
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:261
MediaWiki\Preferences\SignatureValidator\checkLintErrors
checkLintErrors(string $signature)
Definition: SignatureValidator.php:240
MediaWiki\Preferences
Definition: DefaultPreferencesFactory.php:21
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
VirtualRESTServiceClient
Virtual HTTP service client loosely styled after a Virtual File System.
Definition: VirtualRESTServiceClient.php:45
MediaWiki\Preferences\SignatureValidator\validateSignature
validateSignature(string $signature)
Definition: SignatureValidator.php:96
Parser\getFreshParser
getFreshParser()
Return this parser if it is not doing anything, otherwise get a fresh parser.
Definition: Parser.php:6352
MediaWiki\Preferences\SignatureValidator\$parser
Parser $parser
Definition: SignatureValidator.php:56
MediaWiki\Preferences\SignatureValidator\__construct
__construct(UserIdentity $user, ?MessageLocalizer $localizer, ParserOptions $popts)
Definition: SignatureValidator.php:69
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:43
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1456
MediaWiki\Preferences\SignatureValidator\checkUserLinks
checkUserLinks(string $signature)
Definition: SignatureValidator.php:289
MediaWiki\Preferences\SignatureValidator\$specialPageFactory
SpecialPageFactory $specialPageFactory
Definition: SignatureValidator.php:60
MediaWiki\Preferences\SignatureValidator\getLintErrorLocation
getLintErrorLocation(array $lintError)
Definition: SignatureValidator.php:335
NS_USER
const NS_USER
Definition: Defines.php:66
Parser
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:92
ParsoidVirtualRESTService
Virtual HTTP service client for Parsoid.
Definition: ParsoidVirtualRESTService.php:25
MediaWiki\Preferences\SignatureValidator
Definition: SignatureValidator.php:41
MediaWiki\Preferences\SignatureValidator\$titleFactory
TitleFactory $titleFactory
Definition: SignatureValidator.php:62
MediaWiki\Preferences\SignatureValidator\getLintErrorDetails
getLintErrorDetails(array $lintError)
Definition: SignatureValidator.php:340
TitleFactory
Creates Title objects.
Definition: TitleFactory.php:35
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
Parser\parse
parse( $text, PageReference $page, ParserOptions $options, $linestart=true, $clearState=true, $revid=null)
Convert wikitext to HTML Do not call this function recursively.
Definition: Parser.php:661
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
Parser\getOutput
getOutput()
Definition: Parser.php:1084
MediaWiki\Preferences\SignatureValidator\$localizer
MessageLocalizer null $localizer
Definition: SignatureValidator.php:52
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
MediaWiki\Preferences\SignatureValidator\$popts
ParserOptions $popts
Definition: SignatureValidator.php:54
MediaWiki\Preferences\SignatureValidator\$user
UserIdentity $user
Definition: SignatureValidator.php:43
Html
This class is a collection of static functions that serve two purposes:
Definition: Html.php:49
$type
$type
Definition: testCompression.php:52