MediaWiki  master
ApiQueryWatchlist.php
Go to the documentation of this file.
1 <?php
32 
40 
41  private CommentStore $commentStore;
42  private WatchedItemQueryService $watchedItemQueryService;
43  private Language $contentLanguage;
44  private NamespaceInfo $namespaceInfo;
45  private GenderCache $genderCache;
46  private CommentFormatter $commentFormatter;
47 
58  public function __construct(
59  ApiQuery $query,
60  $moduleName,
61  CommentStore $commentStore,
62  WatchedItemQueryService $watchedItemQueryService,
63  Language $contentLanguage,
64  NamespaceInfo $namespaceInfo,
65  GenderCache $genderCache,
66  CommentFormatter $commentFormatter
67  ) {
68  parent::__construct( $query, $moduleName, 'wl' );
69  $this->commentStore = $commentStore;
70  $this->watchedItemQueryService = $watchedItemQueryService;
71  $this->contentLanguage = $contentLanguage;
72  $this->namespaceInfo = $namespaceInfo;
73  $this->genderCache = $genderCache;
74  $this->commentFormatter = $commentFormatter;
75  }
76 
77  public function execute() {
78  $this->run();
79  }
80 
81  public function executeGenerator( $resultPageSet ) {
82  $this->run( $resultPageSet );
83  }
84 
85  private $fld_ids = false, $fld_title = false, $fld_patrol = false,
86  $fld_flags = false, $fld_timestamp = false, $fld_user = false,
87  $fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false,
90 
92  private $fld_expiry = false;
93 
98  private function run( $resultPageSet = null ) {
99  $params = $this->extractRequestParams();
100 
101  $user = $this->getUser();
102  $wlowner = $this->getWatchlistUser( $params );
103 
104  if ( $params['prop'] !== null && $resultPageSet === null ) {
105  $prop = array_fill_keys( $params['prop'], true );
106 
107  $this->fld_ids = isset( $prop['ids'] );
108  $this->fld_title = isset( $prop['title'] );
109  $this->fld_flags = isset( $prop['flags'] );
110  $this->fld_user = isset( $prop['user'] );
111  $this->fld_userid = isset( $prop['userid'] );
112  $this->fld_comment = isset( $prop['comment'] );
113  $this->fld_parsedcomment = isset( $prop['parsedcomment'] );
114  $this->fld_timestamp = isset( $prop['timestamp'] );
115  $this->fld_sizes = isset( $prop['sizes'] );
116  $this->fld_patrol = isset( $prop['patrol'] );
117  $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] );
118  $this->fld_loginfo = isset( $prop['loginfo'] );
119  $this->fld_tags = isset( $prop['tags'] );
120  $this->fld_expiry = isset( $prop['expiry'] );
121 
122  if ( $this->fld_patrol && !$user->useRCPatrol() && !$user->useNPPatrol() ) {
123  $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'patrol' );
124  }
125  }
126 
127  $options = [
128  'dir' => $params['dir'] === 'older'
131  ];
132 
133  if ( $resultPageSet === null ) {
134  $options['includeFields'] = $this->getFieldsToInclude();
135  } else {
136  $options['usedInGenerator'] = true;
137  }
138 
139  if ( $params['start'] ) {
140  $options['start'] = $params['start'];
141  }
142  if ( $params['end'] ) {
143  $options['end'] = $params['end'];
144  }
145 
146  $startFrom = null;
147  if ( $params['continue'] !== null ) {
148  $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string', 'int' ] );
149  $startFrom = $cont;
150  }
151 
152  if ( $wlowner !== $user ) {
153  $options['watchlistOwner'] = $wlowner;
154  $options['watchlistOwnerToken'] = $params['token'];
155  }
156 
157  if ( $params['namespace'] !== null ) {
158  $options['namespaceIds'] = $params['namespace'];
159  }
160 
161  if ( $params['allrev'] ) {
162  $options['allRevisions'] = true;
163  }
164 
165  if ( $params['show'] !== null ) {
166  $show = array_fill_keys( $params['show'], true );
167 
168  /* Check for conflicting parameters. */
169  if ( $this->showParamsConflicting( $show ) ) {
170  $this->dieWithError( 'apierror-show' );
171  }
172 
173  // Check permissions.
174  if ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
176  ) {
177  if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) {
178  $this->dieWithError( 'apierror-permissiondenied-patrolflag', 'permissiondenied' );
179  }
180  }
181 
182  $options['filters'] = array_keys( $show );
183  }
184 
185  if ( $params['type'] !== null ) {
186  try {
187  $rcTypes = RecentChange::parseToRCType( $params['type'] );
188  if ( $rcTypes ) {
189  $options['rcTypes'] = $rcTypes;
190  }
191  } catch ( Exception $e ) {
192  ApiBase::dieDebug( __METHOD__, $e->getMessage() );
193  }
194  }
195 
196  $this->requireMaxOneParameter( $params, 'user', 'excludeuser' );
197  if ( $params['user'] !== null ) {
198  $options['onlyByUser'] = $params['user'];
199  }
200  if ( $params['excludeuser'] !== null ) {
201  $options['notByUser'] = $params['excludeuser'];
202  }
203 
204  $options['limit'] = $params['limit'];
205 
206  $this->getHookRunner()->onApiQueryWatchlistPrepareWatchedItemQueryServiceOptions(
207  $this, $params, $options );
208 
209  $ids = [];
210  $items = $this->watchedItemQueryService->getWatchedItemsWithRecentChangeInfo( $wlowner, $options, $startFrom );
211 
212  // Get gender information
213  if ( $items !== [] && $resultPageSet === null && $this->fld_title &&
214  $this->contentLanguage->needsGenderDistinction()
215  ) {
216  $usernames = [];
217  foreach ( $items as [ $watchedItem, ] ) {
219  $linkTarget = $watchedItem->getTarget();
220  if ( $this->namespaceInfo->hasGenderDistinction( $linkTarget->getNamespace() ) ) {
221  $usernames[] = $linkTarget->getText();
222  }
223  }
224  if ( $usernames !== [] ) {
225  $this->genderCache->doQuery( $usernames, __METHOD__ );
226  }
227  }
228 
229  foreach ( $items as [ $watchedItem, $recentChangeInfo ] ) {
231  if ( $resultPageSet === null ) {
232  $vals = $this->extractOutputData( $watchedItem, $recentChangeInfo );
233  $fit = $this->getResult()->addValue( [ 'query', $this->getModuleName() ], null, $vals );
234  if ( !$fit ) {
235  $startFrom = [ $recentChangeInfo['rc_timestamp'], $recentChangeInfo['rc_id'] ];
236  break;
237  }
238  } elseif ( $params['allrev'] ) {
239  $ids[] = (int)$recentChangeInfo['rc_this_oldid'];
240  } else {
241  $ids[] = (int)$recentChangeInfo['rc_cur_id'];
242  }
243  }
244 
245  if ( $startFrom !== null ) {
246  $this->setContinueEnumParameter( 'continue', implode( '|', $startFrom ) );
247  }
248 
249  if ( $resultPageSet === null ) {
250  $this->getResult()->addIndexedTagName(
251  [ 'query', $this->getModuleName() ],
252  'item'
253  );
254  } elseif ( $params['allrev'] ) {
255  $resultPageSet->populateFromRevisionIDs( $ids );
256  } else {
257  $resultPageSet->populateFromPageIDs( $ids );
258  }
259  }
260 
261  private function getFieldsToInclude() {
262  $includeFields = [];
263  if ( $this->fld_flags ) {
264  $includeFields[] = WatchedItemQueryService::INCLUDE_FLAGS;
265  }
266  if ( $this->fld_user || $this->fld_userid || $this->fld_loginfo ) {
267  $includeFields[] = WatchedItemQueryService::INCLUDE_USER_ID;
268  }
269  if ( $this->fld_user || $this->fld_loginfo ) {
270  $includeFields[] = WatchedItemQueryService::INCLUDE_USER;
271  }
272  if ( $this->fld_comment || $this->fld_parsedcomment ) {
273  $includeFields[] = WatchedItemQueryService::INCLUDE_COMMENT;
274  }
275  if ( $this->fld_patrol ) {
278  }
279  if ( $this->fld_sizes ) {
280  $includeFields[] = WatchedItemQueryService::INCLUDE_SIZES;
281  }
282  if ( $this->fld_loginfo ) {
284  }
285  if ( $this->fld_tags ) {
286  $includeFields[] = WatchedItemQueryService::INCLUDE_TAGS;
287  }
288  return $includeFields;
289  }
290 
291  private function showParamsConflicting( array $show ) {
292  return ( isset( $show[WatchedItemQueryService::FILTER_MINOR] )
293  && isset( $show[WatchedItemQueryService::FILTER_NOT_MINOR] ) )
294  || ( isset( $show[WatchedItemQueryService::FILTER_BOT] )
295  && isset( $show[WatchedItemQueryService::FILTER_NOT_BOT] ) )
296  || ( isset( $show[WatchedItemQueryService::FILTER_ANON] )
297  && isset( $show[WatchedItemQueryService::FILTER_NOT_ANON] ) )
298  || ( isset( $show[WatchedItemQueryService::FILTER_PATROLLED] )
304  || ( isset( $show[WatchedItemQueryService::FILTER_UNREAD] )
305  && isset( $show[WatchedItemQueryService::FILTER_NOT_UNREAD] ) );
306  }
307 
308  private function extractOutputData( WatchedItem $watchedItem, array $recentChangeInfo ) {
309  /* Determine the title of the page that has been changed. */
310  $target = $watchedItem->getTarget();
311  if ( $target instanceof LinkTarget ) {
312  $title = Title::newFromLinkTarget( $target );
313  } else {
314  $title = Title::newFromPageIdentity( $target );
315  }
316  $user = $this->getUser();
317 
318  /* Our output data. */
319  $vals = [];
320  $type = (int)$recentChangeInfo['rc_type'];
321  $vals['type'] = RecentChange::parseFromRCType( $type );
322  $anyHidden = false;
323 
324  /* Create a new entry in the result for the title. */
325  if ( $this->fld_title || $this->fld_ids ) {
326  // These should already have been filtered out of the query, but just in case.
327  if ( $type === RC_LOG && ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) ) {
328  $vals['actionhidden'] = true;
329  $anyHidden = true;
330  }
331  if ( $type !== RC_LOG ||
333  $recentChangeInfo['rc_deleted'],
335  $user
336  )
337  ) {
338  if ( $this->fld_title ) {
339  ApiQueryBase::addTitleInfo( $vals, $title );
340  }
341  if ( $this->fld_ids ) {
342  $vals['pageid'] = (int)$recentChangeInfo['rc_cur_id'];
343  $vals['revid'] = (int)$recentChangeInfo['rc_this_oldid'];
344  $vals['old_revid'] = (int)$recentChangeInfo['rc_last_oldid'];
345  }
346  }
347  }
348 
349  /* Add user data and 'anon' flag, if user is anonymous. */
350  if ( $this->fld_user || $this->fld_userid ) {
351  if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_USER ) {
352  $vals['userhidden'] = true;
353  $anyHidden = true;
354  }
355  if ( RevisionRecord::userCanBitfield(
356  $recentChangeInfo['rc_deleted'],
357  RevisionRecord::DELETED_USER,
358  $user
359  ) ) {
360  if ( $this->fld_userid ) {
361  $vals['userid'] = (int)$recentChangeInfo['rc_user'];
362  // for backwards compatibility
363  $vals['user'] = (int)$recentChangeInfo['rc_user'];
364  }
365 
366  if ( $this->fld_user ) {
367  $vals['user'] = $recentChangeInfo['rc_user_text'];
368  }
369 
370  $vals['anon'] = !$recentChangeInfo['rc_user'];
371  }
372  }
373 
374  /* Add flags, such as new, minor, bot. */
375  if ( $this->fld_flags ) {
376  $vals['bot'] = (bool)$recentChangeInfo['rc_bot'];
377  $vals['new'] = $recentChangeInfo['rc_type'] == RC_NEW;
378  $vals['minor'] = (bool)$recentChangeInfo['rc_minor'];
379  }
380 
381  /* Add sizes of each revision. (Only available on 1.10+) */
382  if ( $this->fld_sizes ) {
383  $vals['oldlen'] = (int)$recentChangeInfo['rc_old_len'];
384  $vals['newlen'] = (int)$recentChangeInfo['rc_new_len'];
385  }
386 
387  /* Add the timestamp. */
388  if ( $this->fld_timestamp ) {
389  $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $recentChangeInfo['rc_timestamp'] );
390  }
391 
392  if ( $this->fld_notificationtimestamp ) {
393  $vals['notificationtimestamp'] = ( $watchedItem->getNotificationTimestamp() == null )
394  ? ''
395  : wfTimestamp( TS_ISO_8601, $watchedItem->getNotificationTimestamp() );
396  }
397 
398  /* Add edit summary / log summary. */
399  if ( $this->fld_comment || $this->fld_parsedcomment ) {
400  if ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_COMMENT ) {
401  $vals['commenthidden'] = true;
402  $anyHidden = true;
403  }
404  if ( RevisionRecord::userCanBitfield(
405  $recentChangeInfo['rc_deleted'],
406  RevisionRecord::DELETED_COMMENT,
407  $user
408  ) ) {
409  $comment = $this->commentStore->getComment( 'rc_comment', $recentChangeInfo )->text;
410  if ( $this->fld_comment ) {
411  $vals['comment'] = $comment;
412  }
413 
414  if ( $this->fld_parsedcomment ) {
415  $vals['parsedcomment'] = $this->commentFormatter->format( $comment, $title );
416  }
417  }
418  }
419 
420  /* Add the patrolled flag */
421  if ( $this->fld_patrol ) {
422  $vals['patrolled'] = $recentChangeInfo['rc_patrolled'] != RecentChange::PRC_UNPATROLLED;
423  $vals['unpatrolled'] = ChangesList::isUnpatrolled( (object)$recentChangeInfo, $user );
424  $vals['autopatrolled'] = $recentChangeInfo['rc_patrolled'] == RecentChange::PRC_AUTOPATROLLED;
425  }
426 
427  if ( $this->fld_loginfo && $recentChangeInfo['rc_type'] == RC_LOG ) {
428  if ( $recentChangeInfo['rc_deleted'] & LogPage::DELETED_ACTION ) {
429  $vals['actionhidden'] = true;
430  $anyHidden = true;
431  }
433  $recentChangeInfo['rc_deleted'],
435  $user
436  ) ) {
437  $vals['logid'] = (int)$recentChangeInfo['rc_logid'];
438  $vals['logtype'] = $recentChangeInfo['rc_log_type'];
439  $vals['logaction'] = $recentChangeInfo['rc_log_action'];
440 
441  $logFormatter = LogFormatter::newFromRow( $recentChangeInfo );
442  $vals['logparams'] = $logFormatter->formatParametersForApi();
443  $vals['logdisplay'] = $logFormatter->getActionText();
444  }
445  }
446 
447  if ( $this->fld_tags ) {
448  if ( $recentChangeInfo['rc_tags'] ) {
449  $tags = explode( ',', $recentChangeInfo['rc_tags'] );
450  ApiResult::setIndexedTagName( $tags, 'tag' );
451  $vals['tags'] = $tags;
452  } else {
453  $vals['tags'] = [];
454  }
455  }
456 
457  if ( $this->fld_expiry ) {
458  // Add expiration, T263796
459  $expiry = $watchedItem->getExpiry( TS_ISO_8601 );
460  $vals['expiry'] = ( $expiry ?? false );
461  }
462 
463  if ( $anyHidden && ( $recentChangeInfo['rc_deleted'] & RevisionRecord::DELETED_RESTRICTED ) ) {
464  $vals['suppressed'] = true;
465  }
466 
467  $this->getHookRunner()->onApiQueryWatchlistExtractOutputData(
468  $this, $watchedItem, $recentChangeInfo, $vals );
469 
470  return $vals;
471  }
472 
473  public function getAllowedParams() {
474  return [
475  'allrev' => false,
476  'start' => [
477  ParamValidator::PARAM_TYPE => 'timestamp'
478  ],
479  'end' => [
480  ParamValidator::PARAM_TYPE => 'timestamp'
481  ],
482  'namespace' => [
483  ParamValidator::PARAM_ISMULTI => true,
484  ParamValidator::PARAM_TYPE => 'namespace'
485  ],
486  'user' => [
487  ParamValidator::PARAM_TYPE => 'user',
488  UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
489  ],
490  'excludeuser' => [
491  ParamValidator::PARAM_TYPE => 'user',
492  UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'id', 'interwiki' ],
493  ],
494  'dir' => [
495  ParamValidator::PARAM_DEFAULT => 'older',
496  ParamValidator::PARAM_TYPE => [
497  'newer',
498  'older'
499  ],
500  ApiBase::PARAM_HELP_MSG => 'api-help-param-direction',
502  'newer' => 'api-help-paramvalue-direction-newer',
503  'older' => 'api-help-paramvalue-direction-older',
504  ],
505  ],
506  'limit' => [
507  ParamValidator::PARAM_DEFAULT => 10,
508  ParamValidator::PARAM_TYPE => 'limit',
509  IntegerDef::PARAM_MIN => 1,
510  IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
511  IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
512  ],
513  'prop' => [
514  ParamValidator::PARAM_ISMULTI => true,
515  ParamValidator::PARAM_DEFAULT => 'ids|title|flags',
517  ParamValidator::PARAM_TYPE => [
518  'ids',
519  'title',
520  'flags',
521  'user',
522  'userid',
523  'comment',
524  'parsedcomment',
525  'timestamp',
526  'patrol',
527  'sizes',
528  'notificationtimestamp',
529  'loginfo',
530  'tags',
531  'expiry',
532  ]
533  ],
534  'show' => [
535  ParamValidator::PARAM_ISMULTI => true,
536  ParamValidator::PARAM_TYPE => [
549  ]
550  ],
551  'type' => [
552  ParamValidator::PARAM_DEFAULT => 'edit|new|log|categorize',
553  ParamValidator::PARAM_ISMULTI => true,
555  ParamValidator::PARAM_TYPE => RecentChange::getChangeTypes()
556  ],
557  'owner' => [
558  ParamValidator::PARAM_TYPE => 'user',
559  UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name' ],
560  ],
561  'token' => [
562  ParamValidator::PARAM_TYPE => 'string',
563  ParamValidator::PARAM_SENSITIVE => true,
564  ],
565  'continue' => [
566  ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
567  ],
568  ];
569  }
570 
571  protected function getExamplesMessages() {
572  return [
573  'action=query&list=watchlist'
574  => 'apihelp-query+watchlist-example-simple',
575  'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment'
576  => 'apihelp-query+watchlist-example-props',
577  'action=query&list=watchlist&wlprop=ids|title|timestamp|user|comment|expiry'
578  => 'apihelp-query+watchlist-example-expiry',
579  'action=query&list=watchlist&wlallrev=&wlprop=ids|title|timestamp|user|comment'
580  => 'apihelp-query+watchlist-example-allrev',
581  'action=query&generator=watchlist&prop=info'
582  => 'apihelp-query+watchlist-example-generator',
583  'action=query&generator=watchlist&gwlallrev=&prop=revisions&rvprop=timestamp|user'
584  => 'apihelp-query+watchlist-example-generator-rev',
585  'action=query&list=watchlist&wlowner=Example&wltoken=123ABC'
586  => 'apihelp-query+watchlist-example-wlowner',
587  ];
588  }
589 
590  public function getHelpUrls() {
591  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Watchlist';
592  }
593 }
const RC_NEW
Definition: Defines.php:117
const RC_LOG
Definition: Defines.php:118
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1515
getWatchlistUser( $params)
Gets the user for whom to get the watchlist.
Definition: ApiBase.php:1228
static dieDebug( $method, $message)
Internal code errors should be reported with this method.
Definition: ApiBase.php:1759
parseContinueParamOrDie(string $continue, array $types)
Parse the 'continue' parameter in the usual format and validate the types of each part,...
Definition: ApiBase.php:1706
const PARAM_HELP_MSG_PER_VALUE
((string|array|Message)[]) When PARAM_TYPE is an array, or 'string' with PARAM_ISMULTI,...
Definition: ApiBase.php:209
const LIMIT_BIG1
Fast query, standard limit.
Definition: ApiBase.php:234
requireMaxOneParameter( $params,... $required)
Dies if more than one parameter from a certain set of parameters are set and not false.
Definition: ApiBase.php:981
getResult()
Get the result object.
Definition: ApiBase.php:667
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:807
const PARAM_HELP_MSG
(string|array|Message) Specify an alternative i18n documentation message for this parameter.
Definition: ApiBase.php:169
const LIMIT_BIG2
Fast query, apihighlimits limit.
Definition: ApiBase.php:236
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:528
getHookRunner()
Get an ApiHookRunner for running core API hooks.
Definition: ApiBase.php:752
static addTitleInfo(&$arr, $title, $prefix='')
Add information (title and namespace) about a Title object to a result array.
setContinueEnumParameter( $paramName, $paramValue)
Overridden to set the generator param if in generator mode.
This query action allows clients to retrieve a list of recently modified pages that are part of the l...
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
__construct(ApiQuery $query, $moduleName, CommentStore $commentStore, WatchedItemQueryService $watchedItemQueryService, Language $contentLanguage, NamespaceInfo $namespaceInfo, GenderCache $genderCache, CommentFormatter $commentFormatter)
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getExamplesMessages()
Returns usage examples for this module.
getHelpUrls()
Return links to more detailed help pages about the module.
executeGenerator( $resultPageSet)
Execute this module as a generator.
This is the main query class.
Definition: ApiQuery.php:43
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
static isUnpatrolled( $rc, User $user)
Look up "gender" user preference.
Definition: GenderCache.php:39
Base class for language-specific code.
Definition: Language.php:63
static userCanBitfield( $bitfield, $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this log row,...
static newFromRow( $row)
Handy shortcut for constructing a formatter directly from database row.
const DELETED_ACTION
Definition: LogPage.php:44
This is the main service interface for converting single-line comments from various DB comment fields...
Handle database storage of comments such as edit summaries and log reasons.
Page revision base class.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition: Title.php:76
const PRC_UNPATROLLED
static parseToRCType( $type)
Parsing text to RC_* constants.
static getChangeTypes()
Get an array of all change types.
static parseFromRCType( $rcType)
Parsing RC_* constants to human-readable test.
const PRC_AUTOPATROLLED
Representation of a pair of user and title for watchlist entries.
Definition: WatchedItem.php:38
getExpiry(?int $style=TS_MW)
When the watched item will expire.
getNotificationTimestamp()
Get the notification timestamp of this entry.
Service for formatting and validating API parameters.
Type definition for integer types.
Definition: IntegerDef.php:23
Represents the target of a wiki link.
Definition: LinkTarget.php:30