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