MediaWiki  master
SignatureValidator.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Preferences;
22 
23 use Html;
26 use MultiHttpClient;
27 use ParserOptions;
29 use SpecialPage;
30 use Title;
31 use User;
33 
38 
40  private $user;
42  private $localizer;
44  private $popts;
45 
47  $this->user = $user;
48  $this->localizer = $localizer;
49  $this->popts = $popts;
50  }
51 
57  public function validateSignature( string $signature ) {
58  $pstSignature = $this->applyPreSaveTransform( $signature );
59  if ( $pstSignature === false ) {
60  // Return early because the rest of the validation uses wikitext parsing, which requires
61  // the pre-save transform to be applied first, and we just found out that the result of the
62  // pre-save transform would require *another* pre-save transform, which is crazy
63  if ( $this->localizer ) {
64  return [ $this->localizer->msg( 'badsigsubst' )->parse() ];
65  }
66  return true;
67  }
68 
69  $pstWasApplied = false;
70  if ( $pstSignature !== $signature ) {
71  $pstWasApplied = true;
72  $signature = $pstSignature;
73  }
74 
75  $errors = $this->localizer ? [] : false;
76 
77  $lintErrors = $this->checkLintErrors( $signature );
78  if ( $lintErrors ) {
79  $config = MediaWikiServices::getInstance()->getMainConfig();
80  $allowedLintErrors = $config->get( 'SignatureAllowedLintErrors' );
81  $messages = '';
82 
83  foreach ( $lintErrors as $error ) {
84  if ( $error['type'] === 'multiple-unclosed-formatting-tags' ) {
85  // Always appears with 'missing-end-tag', we can ignore it to simplify the error message
86  continue;
87  }
88  if ( in_array( $error['type'], $allowedLintErrors, true ) ) {
89  continue;
90  }
91  if ( !$this->localizer ) {
92  $errors = true;
93  break;
94  }
95 
96  $details = $this->getLintErrorDetails( $error );
97  $location = $this->getLintErrorLocation( $error );
98  // Messages used here:
99  // * linter-pager-bogus-image-options-details
100  // * linter-pager-deletable-table-tag-details
101  // * linter-pager-html5-misnesting-details
102  // * linter-pager-misc-tidy-replacement-issues-details
103  // * linter-pager-misnested-tag-details
104  // * linter-pager-missing-end-tag-details
105  // * linter-pager-multi-colon-escape-details
106  // * linter-pager-multiline-html-table-in-list-details
107  // * linter-pager-multiple-unclosed-formatting-tags-details
108  // * linter-pager-obsolete-tag-details
109  // * linter-pager-pwrap-bug-workaround-details
110  // * linter-pager-self-closed-tag-details
111  // * linter-pager-stripped-tag-details
112  // * linter-pager-tidy-font-bug-details
113  // * linter-pager-tidy-whitespace-bug-details
114  // * linter-pager-unclosed-quotes-in-heading-details
115  $label = $this->localizer->msg( "linter-pager-{$error['type']}-details" )->parse();
116  $docsLink = new \OOUI\ButtonWidget( [
117  'href' =>
118  "https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Lint_errors/{$error['type']}",
119  'target' => '_blank',
120  'label' => $this->localizer->msg( 'prefs-signature-error-details' )->text(),
121  ] );
122 
123  // If pre-save transform was applied (i.e., the signature has 'subst:' syntax),
124  // error locations will be incorrect, because Parsoid can't expand templates.
125  // Don't display them.
126  $encLocation = $pstWasApplied ? null : json_encode( $location );
127 
128  $messages .= Html::rawElement(
129  'li',
130  [ 'data-mw-lint-error-location' => $encLocation ],
131  $label . $this->localizer->msg( 'colon-separator' )->escaped() .
132  $details . ' ' . $docsLink
133  );
134  }
135 
136  if ( $messages && $this->localizer ) {
137  $errors[] = $this->localizer->msg( 'badsightml' )->parse() .
138  Html::rawElement( 'ol', [], $messages );
139  }
140  }
141 
142  if ( !$this->checkUserLinks( $signature ) ) {
143  if ( $this->localizer ) {
144  $userText = wfEscapeWikiText( $this->user->getName() );
145  $linkWikitext = $this->localizer->msg( 'signature', $userText, $userText )->inContentLanguage()->text();
146  $errors[] = $this->localizer->msg( 'badsiglinks', wfEscapeWikiText( $linkWikitext ) )->parse();
147  } else {
148  $errors = true;
149  }
150  }
151 
152  return $errors;
153  }
154 
160  protected function applyPreSaveTransform( string $signature ) {
161  // This may be called by the Parser when it's displaying a signature, so we need a new instance
162  $parser = MediaWikiServices::getInstance()->getParser()->getFreshParser();
163 
164  $pstSignature = $parser->preSaveTransform(
165  $signature,
166  SpecialPage::getTitleFor( 'Preferences' ),
167  $this->user,
168  $this->popts
169  );
170 
171  // The signature wikitext contains another '~~~~' or similar (T230652)
172  if ( $parser->getOutput()->getFlag( 'user-signature' ) ) {
173  return false;
174  }
175 
176  // The signature wikitext contains '{{subst:...}}' markup that produces another subst (T230652)
177  $pstPstSignature = $parser->preSaveTransform(
178  $pstSignature,
179  SpecialPage::getTitleFor( 'Preferences' ),
180  $this->user,
181  $this->popts
182  );
183  if ( $pstPstSignature !== $pstSignature ) {
184  return false;
185  }
186 
187  return $pstSignature;
188  }
189 
194  protected function checkLintErrors( string $signature ) : array {
195  // Real check for mismatched HTML tags in the *output*.
196  // This has to use Parsoid because PHP Parser doesn't produce this information,
197  // it just fixes up the result quietly.
198 
199  // This request is not cached, but that's okay, because $signature is short (other code checks
200  // the length against $wgMaxSigChars).
201 
202  $config = MediaWikiServices::getInstance()->getMainConfig();
203  $vrsConfig = $config->get( 'VirtualRestConfig' );
204  if ( isset( $vrsConfig['modules']['parsoid'] ) ) {
205  $params = $vrsConfig['modules']['parsoid'];
206  if ( isset( $vrsConfig['global'] ) ) {
207  $params = array_merge( $vrsConfig['global'], $params );
208  }
209  $parsoidVrs = new ParsoidVirtualRESTService( $params );
210 
211  $vrsClient = new VirtualRESTServiceClient( new MultiHttpClient( [] ) );
212  $vrsClient->mount( '/parsoid/', $parsoidVrs );
213 
214  $request = [
215  'method' => 'POST',
216  'url' => '/parsoid/local/v3/transform/wikitext/to/lint',
217  'body' => [
218  'wikitext' => $signature,
219  ],
220  'headers' => [
221  // Are both of these are really needed?
222  'User-Agent' => 'MediaWiki/' . MW_VERSION,
223  'Api-User-Agent' => 'MediaWiki/' . MW_VERSION,
224  ],
225  ];
226 
227  $response = $vrsClient->run( $request );
228  if ( $response['code'] === 200 ) {
229  $json = json_decode( $response['body'], true );
230  // $json is an array of error objects
231  if ( $json ) {
232  return $json;
233  }
234  }
235  }
236 
237  return [];
238  }
239 
244  protected function checkUserLinks( string $signature ) : bool {
245  // This may be called by the Parser when it's displaying a signature, so we need a new instance
246  $parser = MediaWikiServices::getInstance()->getParser()->getFreshParser();
247 
248  // Check for required links. This one's easier to do with the PHP Parser.
249  $pout = $parser->parse(
250  $signature,
251  SpecialPage::getTitleFor( 'Preferences' ),
252  $this->popts
253  );
254 
255  // Checking user or talk links is easy
256  $links = $pout->getLinks();
257  $username = $this->user->getName();
258  if (
259  isset( $links[ NS_USER ][ strtr( $username, ' ', '_' ) ] ) ||
260  isset( $links[ NS_USER_TALK ][ strtr( $username, ' ', '_' ) ] )
261  ) {
262  return true;
263  }
264 
265  // Checking the contributions link is harder, because the special page name and the username in
266  // the "subpage parameter" are not normalized for us.
267  $splinks = $pout->getLinksSpecial();
268  $specialPageFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
269  foreach ( $splinks as $dbkey => $unused ) {
270  list( $name, $subpage ) = $specialPageFactory->resolveAlias( $dbkey );
271  if ( $name === 'Contributions' && $subpage ) {
272  $userTitle = Title::makeTitleSafe( NS_USER, $subpage );
273  if ( $userTitle && $userTitle->getText() === $username ) {
274  return true;
275  }
276  }
277  }
278 
279  return false;
280  }
281 
282  // Adapted from the Linter extension
283  private function getLintErrorLocation( array $lintError ) : array {
284  return array_slice( $lintError['dsr'], 0, 2 );
285  }
286 
287  // Adapted from the Linter extension
288  private function getLintErrorDetails( array $lintError ) : string {
289  [ 'type' => $type, 'params' => $params ] = $lintError;
290 
291  if ( $type === 'bogus-image-options' && isset( $params['items'] ) ) {
292  $list = array_map( function ( $in ) {
293  return Html::element( 'code', [], $in );
294  }, $params['items'] );
295  return implode(
296  $this->localizer->msg( 'comma-separator' )->escaped(),
297  $list
298  );
299  } elseif ( $type === 'pwrap-bug-workaround' &&
300  isset( $params['root'] ) &&
301  isset( $params['child'] ) ) {
302  return Html::element( 'code', [],
303  $params['root'] . " > " . $params['child'] );
304  } elseif ( $type === 'tidy-whitespace-bug' &&
305  isset( $params['node'] ) &&
306  isset( $params['sibling'] ) ) {
307  return Html::element( 'code', [],
308  $params['node'] . " + " . $params['sibling'] );
309  } elseif ( $type === 'multi-colon-escape' &&
310  isset( $params['href'] ) ) {
311  return Html::element( 'code', [], $params['href'] );
312  } elseif ( $type === 'multiline-html-table-in-list' ) {
313  /* ancestor and name will be set */
314  return Html::element( 'code', [],
315  $params['ancestorName'] . " > " . $params['name'] );
316  } elseif ( $type === 'misc-tidy-replacement-issues' ) {
317  /* There will be a 'subtype' param to disambiguate */
318  return Html::element( 'code', [], $params['subtype'] );
319  } elseif ( $type === 'missing-end-tag' ) {
320  return Html::element( 'code', [], '</' . $params['name'] . '>' );
321  } elseif ( isset( $params['name'] ) ) {
322  return Html::element( 'code', [], $params['name'] );
323  }
324 
325  return '';
326  }
327 
328 }
ParserOptions
Set options of the Parser.
Definition: ParserOptions.php:44
MultiHttpClient
Class to handle multiple HTTP requests.
Definition: MultiHttpClient.php:55
MediaWiki\Preferences\SignatureValidator\applyPreSaveTransform
applyPreSaveTransform(string $signature)
Definition: SignatureValidator.php:160
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:154
MediaWiki\Preferences\SignatureValidator\$user
User $user
Definition: SignatureValidator.php:40
MW_VERSION
const MW_VERSION
The running version of MediaWiki.
Definition: Defines.php:39
MediaWiki\Preferences\SignatureValidator\__construct
__construct(User $user, ?MessageLocalizer $localizer, ParserOptions $popts)
Definition: SignatureValidator.php:46
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:92
MessageLocalizer
Interface for localizing messages in MediaWiki.
Definition: MessageLocalizer.php:29
MediaWiki\MediaWikiServices\getInstance
static getInstance()
Returns the global default instance of the top level service locator.
Definition: MediaWikiServices.php:185
MediaWiki\Preferences\SignatureValidator\checkLintErrors
checkLintErrors(string $signature)
Definition: SignatureValidator.php:194
MediaWiki\Preferences
Definition: DefaultPreferencesFactory.php:21
Config\get
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
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:57
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:618
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:72
SpecialPage
Parent class for all special pages.
Definition: SpecialPage.php:41
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1487
MediaWiki\Preferences\SignatureValidator\checkUserLinks
checkUserLinks(string $signature)
Definition: SignatureValidator.php:244
MediaWiki\Preferences\SignatureValidator\getLintErrorLocation
getLintErrorLocation(array $lintError)
Definition: SignatureValidator.php:283
ParsoidVirtualRESTService
Virtual HTTP service client for Parsoid.
Definition: ParsoidVirtualRESTService.php:25
Title
Represents a title within MediaWiki.
Definition: Title.php:42
MediaWiki\Preferences\SignatureValidator
Definition: SignatureValidator.php:37
MediaWiki\Preferences\SignatureValidator\getLintErrorDetails
getLintErrorDetails(array $lintError)
Definition: SignatureValidator.php:288
MediaWiki\$config
Config $config
Definition: MediaWiki.php:42
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
NS_USER
const NS_USER
Definition: Defines.php:71
MediaWiki\Preferences\SignatureValidator\$localizer
MessageLocalizer null $localizer
Definition: SignatureValidator.php:42
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:55
MediaWiki\Preferences\SignatureValidator\$popts
ParserOptions $popts
Definition: SignatureValidator.php:44
Html
This class is a collection of static functions that serve two purposes:
Definition: Html.php:49
$type
$type
Definition: testCompression.php:52