MediaWiki  master
SpecialWhatLinksHere.php
Go to the documentation of this file.
1 <?php
26 
34  protected $opts;
35 
36  protected $selfTitle;
37 
39  protected $target;
40 
41  protected $limits = [ 20, 50, 100, 250, 500 ];
42 
43  public function __construct() {
44  parent::__construct( 'Whatlinkshere' );
45  }
46 
47  function execute( $par ) {
48  $out = $this->getOutput();
49 
50  $this->setHeaders();
51  $this->outputHeader();
52  $this->addHelpLink( 'Help:What links here' );
53 
54  $opts = new FormOptions();
55 
56  $opts->add( 'target', '' );
57  $opts->add( 'namespace', '', FormOptions::INTNULL );
58  $opts->add( 'limit', $this->getConfig()->get( 'QueryPageDefaultLimit' ) );
59  $opts->add( 'from', 0 );
60  $opts->add( 'back', 0 );
61  $opts->add( 'hideredirs', false );
62  $opts->add( 'hidetrans', false );
63  $opts->add( 'hidelinks', false );
64  $opts->add( 'hideimages', false );
65  $opts->add( 'invert', false );
66 
68  $opts->validateIntBounds( 'limit', 0, 5000 );
69 
70  // Give precedence to subpage syntax
71  if ( $par !== null ) {
72  $opts->setValue( 'target', $par );
73  }
74 
75  // Bind to member variable
76  $this->opts = $opts;
77 
78  $this->target = Title::newFromText( $opts->getValue( 'target' ) );
79  if ( !$this->target ) {
80  if ( !$this->including() ) {
81  $out->addHTML( $this->whatlinkshereForm() );
82  }
83 
84  return;
85  }
86 
87  $this->getSkin()->setRelevantTitle( $this->target );
88 
89  $this->selfTitle = $this->getPageTitle( $this->target->getPrefixedDBkey() );
90 
91  $out->setPageTitle( $this->msg( 'whatlinkshere-title', $this->target->getPrefixedText() ) );
92  $out->addBacklinkSubtitle( $this->target );
93  $this->showIndirectLinks(
94  0,
95  $this->target,
96  $opts->getValue( 'limit' ),
97  $opts->getValue( 'from' ),
98  $opts->getValue( 'back' )
99  );
100  }
101 
109  function showIndirectLinks( $level, $target, $limit, $from = 0, $back = 0 ) {
110  $out = $this->getOutput();
111  $dbr = wfGetDB( DB_REPLICA );
112 
113  $hidelinks = $this->opts->getValue( 'hidelinks' );
114  $hideredirs = $this->opts->getValue( 'hideredirs' );
115  $hidetrans = $this->opts->getValue( 'hidetrans' );
116  $hideimages = $target->getNamespace() != NS_FILE || $this->opts->getValue( 'hideimages' );
117 
118  $fetchlinks = ( !$hidelinks || !$hideredirs );
119 
120  // Build query conds in concert for all three tables...
121  $conds = [];
122  $conds['pagelinks'] = [
123  'pl_namespace' => $target->getNamespace(),
124  'pl_title' => $target->getDBkey(),
125  ];
126  $conds['templatelinks'] = [
127  'tl_namespace' => $target->getNamespace(),
128  'tl_title' => $target->getDBkey(),
129  ];
130  $conds['imagelinks'] = [
131  'il_to' => $target->getDBkey(),
132  ];
133 
134  $namespace = $this->opts->getValue( 'namespace' );
135  $invert = $this->opts->getValue( 'invert' );
136  $nsComparison = ( $invert ? '!= ' : '= ' ) . $dbr->addQuotes( $namespace );
137  if ( is_int( $namespace ) ) {
138  $conds['pagelinks'][] = "pl_from_namespace $nsComparison";
139  $conds['templatelinks'][] = "tl_from_namespace $nsComparison";
140  $conds['imagelinks'][] = "il_from_namespace $nsComparison";
141  }
142 
143  if ( $from ) {
144  $conds['templatelinks'][] = "tl_from >= $from";
145  $conds['pagelinks'][] = "pl_from >= $from";
146  $conds['imagelinks'][] = "il_from >= $from";
147  }
148 
149  if ( $hideredirs ) {
150  $conds['pagelinks']['rd_from'] = null;
151  } elseif ( $hidelinks ) {
152  $conds['pagelinks'][] = 'rd_from is NOT NULL';
153  }
154 
155  $queryFunc = function ( IDatabase $dbr, $table, $fromCol ) use (
156  $conds, $target, $limit
157  ) {
158  // Read an extra row as an at-end check
159  $queryLimit = $limit + 1;
160  $on = [
161  "rd_from = $fromCol",
162  'rd_title' => $target->getDBkey(),
163  'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL'
164  ];
165  $on['rd_namespace'] = $target->getNamespace();
166  // Inner LIMIT is 2X in case of stale backlinks with wrong namespaces
167  $subQuery = $dbr->buildSelectSubquery(
168  [ $table, 'redirect', 'page' ],
169  [ $fromCol, 'rd_from' ],
170  $conds[$table],
171  __CLASS__ . '::showIndirectLinks',
172  // Force JOIN order per T106682 to avoid large filesorts
173  [ 'ORDER BY' => $fromCol, 'LIMIT' => 2 * $queryLimit, 'STRAIGHT_JOIN' ],
174  [
175  'page' => [ 'JOIN', "$fromCol = page_id" ],
176  'redirect' => [ 'LEFT JOIN', $on ]
177  ]
178  );
179  return $dbr->select(
180  [ 'page', 'temp_backlink_range' => $subQuery ],
181  [ 'page_id', 'page_namespace', 'page_title', 'rd_from', 'page_is_redirect' ],
182  [],
183  __CLASS__ . '::showIndirectLinks',
184  [ 'ORDER BY' => 'page_id', 'LIMIT' => $queryLimit ],
185  [ 'page' => [ 'JOIN', "$fromCol = page_id" ] ]
186  );
187  };
188 
189  if ( $fetchlinks ) {
190  $plRes = $queryFunc( $dbr, 'pagelinks', 'pl_from' );
191  }
192 
193  if ( !$hidetrans ) {
194  $tlRes = $queryFunc( $dbr, 'templatelinks', 'tl_from' );
195  }
196 
197  if ( !$hideimages ) {
198  $ilRes = $queryFunc( $dbr, 'imagelinks', 'il_from' );
199  }
200 
201  if ( ( !$fetchlinks || !$plRes->numRows() )
202  && ( $hidetrans || !$tlRes->numRows() )
203  && ( $hideimages || !$ilRes->numRows() )
204  ) {
205  if ( $level == 0 && !$this->including() ) {
206  $out->addHTML( $this->whatlinkshereForm() );
207 
208  // Show filters only if there are links
209  if ( $hidelinks || $hidetrans || $hideredirs || $hideimages ) {
210  $out->addHTML( $this->getFilterPanel() );
211  }
212  $msgKey = is_int( $namespace ) ? 'nolinkshere-ns' : 'nolinkshere';
213  $link = $this->getLinkRenderer()->makeLink(
214  $this->target,
215  null,
216  [],
217  $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
218  );
219 
220  $errMsg = $this->msg( $msgKey )
221  ->params( $this->target->getPrefixedText() )
222  ->rawParams( $link )
223  ->parseAsBlock();
224  $out->addHTML( $errMsg );
225  $out->setStatusCode( 404 );
226  }
227 
228  return;
229  }
230 
231  // Read the rows into an array and remove duplicates
232  // templatelinks comes second so that the templatelinks row overwrites the
233  // pagelinks row, so we get (inclusion) rather than nothing
234  $rows = [];
235  if ( $fetchlinks ) {
236  foreach ( $plRes as $row ) {
237  $row->is_template = 0;
238  $row->is_image = 0;
239  $rows[$row->page_id] = $row;
240  }
241  }
242  if ( !$hidetrans ) {
243  foreach ( $tlRes as $row ) {
244  $row->is_template = 1;
245  $row->is_image = 0;
246  $rows[$row->page_id] = $row;
247  }
248  }
249  if ( !$hideimages ) {
250  foreach ( $ilRes as $row ) {
251  $row->is_template = 0;
252  $row->is_image = 1;
253  $rows[$row->page_id] = $row;
254  }
255  }
256 
257  // Sort by key and then change the keys to 0-based indices
258  ksort( $rows );
259  $rows = array_values( $rows );
260 
261  $numRows = count( $rows );
262 
263  // Work out the start and end IDs, for prev/next links
264  if ( $numRows > $limit ) {
265  // More rows available after these ones
266  // Get the ID from the last row in the result set
267  $nextId = $rows[$limit]->page_id;
268  // Remove undisplayed rows
269  $rows = array_slice( $rows, 0, $limit );
270  } else {
271  // No more rows after
272  $nextId = false;
273  }
274  $prevId = $from;
275 
276  // use LinkBatch to make sure, that all required data (associated with Titles)
277  // is loaded in one query
278  $lb = new LinkBatch();
279  foreach ( $rows as $row ) {
280  $lb->add( $row->page_namespace, $row->page_title );
281  }
282  $lb->execute();
283 
284  if ( $level == 0 && !$this->including() ) {
285  $out->addHTML( $this->whatlinkshereForm() );
286  $out->addHTML( $this->getFilterPanel() );
287 
288  $link = $this->getLinkRenderer()->makeLink(
289  $this->target,
290  null,
291  [],
292  $this->target->isRedirect() ? [ 'redirect' => 'no' ] : []
293  );
294 
295  $msg = $this->msg( 'linkshere' )
296  ->params( $this->target->getPrefixedText() )
297  ->rawParams( $link )
298  ->parseAsBlock();
299  $out->addHTML( $msg );
300 
301  $prevnext = $this->getPrevNext( $prevId, $nextId );
302  $out->addHTML( $prevnext );
303  }
304  $out->addHTML( $this->listStart( $level ) );
305  foreach ( $rows as $row ) {
306  $nt = Title::makeTitle( $row->page_namespace, $row->page_title );
307 
308  if ( $row->rd_from && $level < 2 ) {
309  $out->addHTML( $this->listItem( $row, $nt, $target, true ) );
310  $this->showIndirectLinks(
311  $level + 1,
312  $nt,
313  $this->getConfig()->get( 'MaxRedirectLinksRetrieved' )
314  );
315  $out->addHTML( Xml::closeElement( 'li' ) );
316  } else {
317  $out->addHTML( $this->listItem( $row, $nt, $target ) );
318  }
319  }
320 
321  $out->addHTML( $this->listEnd() );
322 
323  if ( $level == 0 && !$this->including() ) {
324  $out->addHTML( $prevnext );
325  }
326  }
327 
328  protected function listStart( $level ) {
329  return Xml::openElement( 'ul', ( $level ? [] : [ 'id' => 'mw-whatlinkshere-list' ] ) );
330  }
331 
332  protected function listItem( $row, $nt, $target, $notClose = false ) {
333  $dirmark = $this->getLanguage()->getDirMark();
334 
335  # local message cache
336  static $msgcache = null;
337  if ( $msgcache === null ) {
338  static $msgs = [ 'isredirect', 'istemplate', 'semicolon-separator',
339  'whatlinkshere-links', 'isimage', 'editlink' ];
340  $msgcache = [];
341  foreach ( $msgs as $msg ) {
342  $msgcache[$msg] = $this->msg( $msg )->escaped();
343  }
344  }
345 
346  if ( $row->rd_from ) {
347  $query = [ 'redirect' => 'no' ];
348  } else {
349  $query = [];
350  }
351 
352  $link = $this->getLinkRenderer()->makeKnownLink(
353  $nt,
354  null,
355  $row->page_is_redirect ? [ 'class' => 'mw-redirect' ] : [],
356  $query
357  );
358 
359  // Display properties (redirect or template)
360  $propsText = '';
361  $props = [];
362  if ( $row->rd_from ) {
363  $props[] = $msgcache['isredirect'];
364  }
365  if ( $row->is_template ) {
366  $props[] = $msgcache['istemplate'];
367  }
368  if ( $row->is_image ) {
369  $props[] = $msgcache['isimage'];
370  }
371 
372  Hooks::run( 'WhatLinksHereProps', [ $row, $nt, $target, &$props ] );
373 
374  if ( count( $props ) ) {
375  $propsText = $this->msg( 'parentheses' )
376  ->rawParams( implode( $msgcache['semicolon-separator'], $props ) )->escaped();
377  }
378 
379  # Space for utilities links, with a what-links-here link provided
380  $wlhLink = $this->wlhLink( $nt, $msgcache['whatlinkshere-links'], $msgcache['editlink'] );
381  $wlh = Xml::wrapClass(
382  $this->msg( 'parentheses' )->rawParams( $wlhLink )->escaped(),
383  'mw-whatlinkshere-tools'
384  );
385 
386  return $notClose ?
387  Xml::openElement( 'li' ) . "$link $propsText $dirmark $wlh\n" :
388  Xml::tags( 'li', null, "$link $propsText $dirmark $wlh" ) . "\n";
389  }
390 
391  protected function listEnd() {
392  return Xml::closeElement( 'ul' );
393  }
394 
395  protected function wlhLink( Title $target, $text, $editText ) {
396  static $title = null;
397  if ( $title === null ) {
398  $title = $this->getPageTitle();
399  }
400 
401  $linkRenderer = $this->getLinkRenderer();
402 
403  if ( $text !== null ) {
404  $text = new HtmlArmor( $text );
405  }
406 
407  // always show a "<- Links" link
408  $links = [
409  'links' => $linkRenderer->makeKnownLink(
410  $title,
411  $text,
412  [],
413  [ 'target' => $target->getPrefixedText() ]
414  ),
415  ];
416 
417  // if the page is editable, add an edit link
418  if (
419  // check user permissions
420  MediaWikiServices::getInstance()
421  ->getPermissionManager()
422  ->userHasRight( $this->getUser(), 'edit' ) &&
423  // check, if the content model is editable through action=edit
424  ContentHandler::getForTitle( $target )->supportsDirectEditing()
425  ) {
426  if ( $editText !== null ) {
427  $editText = new HtmlArmor( $editText );
428  }
429 
430  $links['edit'] = $linkRenderer->makeKnownLink(
431  $target,
432  $editText,
433  [],
434  [ 'action' => 'edit' ]
435  );
436  }
437 
438  // build the links html
439  return $this->getLanguage()->pipeList( $links );
440  }
441 
442  function makeSelfLink( $text, $query ) {
443  if ( $text !== null ) {
444  $text = new HtmlArmor( $text );
445  }
446 
447  return $this->getLinkRenderer()->makeKnownLink(
448  $this->selfTitle,
449  $text,
450  [],
451  $query
452  );
453  }
454 
455  function getPrevNext( $prevId, $nextId ) {
456  $currentLimit = $this->opts->getValue( 'limit' );
457  $prev = $this->msg( 'whatlinkshere-prev' )->numParams( $currentLimit )->escaped();
458  $next = $this->msg( 'whatlinkshere-next' )->numParams( $currentLimit )->escaped();
459 
460  $changed = $this->opts->getChangedValues();
461  unset( $changed['target'] ); // Already in the request title
462 
463  if ( $prevId != 0 ) {
464  $overrides = [ 'from' => $this->opts->getValue( 'back' ) ];
465  $prev = $this->makeSelfLink( $prev, array_merge( $changed, $overrides ) );
466  }
467  if ( $nextId != 0 ) {
468  $overrides = [ 'from' => $nextId, 'back' => $prevId ];
469  $next = $this->makeSelfLink( $next, array_merge( $changed, $overrides ) );
470  }
471 
472  $limitLinks = [];
473  $lang = $this->getLanguage();
474  foreach ( $this->limits as $limit ) {
475  $prettyLimit = htmlspecialchars( $lang->formatNum( $limit ) );
476  $overrides = [ 'limit' => $limit ];
477  $limitLinks[] = $this->makeSelfLink( $prettyLimit, array_merge( $changed, $overrides ) );
478  }
479 
480  $nums = $lang->pipeList( $limitLinks );
481 
482  return $this->msg( 'viewprevnext' )->rawParams( $prev, $next, $nums )->escaped();
483  }
484 
485  function whatlinkshereForm() {
486  // We get nicer value from the title object
487  $this->opts->consumeValue( 'target' );
488  // Reset these for new requests
489  $this->opts->consumeValues( [ 'back', 'from' ] );
490 
491  $target = $this->target ? $this->target->getPrefixedText() : '';
492  $namespace = $this->opts->consumeValue( 'namespace' );
493  $nsinvert = $this->opts->consumeValue( 'invert' );
494 
495  # Build up the form
496  $f = Xml::openElement( 'form', [ 'action' => wfScript() ] );
497 
498  # Values that should not be forgotten
499  $f .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() );
500  foreach ( $this->opts->getUnconsumedValues() as $name => $value ) {
501  $f .= Html::hidden( $name, $value );
502  }
503 
504  $f .= Xml::fieldset( $this->msg( 'whatlinkshere' )->text() );
505 
506  # Target input (.mw-searchInput enables suggestions)
507  $f .= Xml::inputLabel( $this->msg( 'whatlinkshere-page' )->text(), 'target',
508  'mw-whatlinkshere-target', 40, $target, [ 'class' => 'mw-searchInput' ] );
509 
510  $f .= ' ';
511 
512  # Namespace selector
514  [
515  'selected' => $namespace,
516  'all' => '',
517  'label' => $this->msg( 'namespace' )->text(),
518  'in-user-lang' => true,
519  ], [
520  'name' => 'namespace',
521  'id' => 'namespace',
522  'class' => 'namespaceselector',
523  ]
524  );
525 
526  $f .= "\u{00A0}" .
528  $this->msg( 'invert' )->text(),
529  'invert',
530  'nsinvert',
531  $nsinvert,
532  [ 'title' => $this->msg( 'tooltip-whatlinkshere-invert' )->text() ]
533  );
534 
535  $f .= ' ';
536 
537  # Submit
538  $f .= Xml::submitButton( $this->msg( 'whatlinkshere-submit' )->text() );
539 
540  # Close
541  $f .= Xml::closeElement( 'fieldset' ) . Xml::closeElement( 'form' ) . "\n";
542 
543  return $f;
544  }
545 
551  function getFilterPanel() {
552  $show = $this->msg( 'show' )->escaped();
553  $hide = $this->msg( 'hide' )->escaped();
554 
555  $changed = $this->opts->getChangedValues();
556  unset( $changed['target'] ); // Already in the request title
557 
558  $links = [];
559  $types = [ 'hidetrans', 'hidelinks', 'hideredirs' ];
560  if ( $this->target->getNamespace() == NS_FILE ) {
561  $types[] = 'hideimages';
562  }
563 
564  // Combined message keys: 'whatlinkshere-hideredirs', 'whatlinkshere-hidetrans',
565  // 'whatlinkshere-hidelinks', 'whatlinkshere-hideimages'
566  // To be sure they will be found by grep
567  foreach ( $types as $type ) {
568  $chosen = $this->opts->getValue( $type );
569  $msg = $chosen ? $show : $hide;
570  $overrides = [ $type => !$chosen ];
571  $links[] = $this->msg( "whatlinkshere-{$type}" )->rawParams(
572  $this->makeSelfLink( $msg, array_merge( $changed, $overrides ) ) )->escaped();
573  }
574 
575  return Xml::fieldset(
576  $this->msg( 'whatlinkshere-filters' )->text(),
577  $this->getLanguage()->pipeList( $links )
578  );
579  }
580 
589  public function prefixSearchSubpages( $search, $limit, $offset ) {
590  return $this->prefixSearchString( $search, $limit, $offset );
591  }
592 
593  protected function getGroupName() {
594  return 'pagetools';
595  }
596 }
wlhLink(Title $target, $text, $editText)
Helper class to keep track of options when mixing links and form elements.
Definition: FormOptions.php:35
buildSelectSubquery( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Equivalent to IDatabase::selectSQLText() except wraps the result in Subqyery.
including( $x=null)
Whether the special page is being evaluated via transclusion.
const INTNULL
Integer type or null, maps to WebRequest::getIntOrNull() This is useful for the namespace selector...
Definition: FormOptions.php:55
if(!isset( $args[0])) $lang
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
getFilterPanel()
Create filter panel.
getOutput()
Get the OutputPage being used for this instance.
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1853
static wrapClass( $text, $class, $tag='span', $attribs=[])
Shortcut to make a specific element with a class attribute.
Definition: Xml.php:262
add( $name, $default, $type=self::AUTO)
Add an option to be handled by this FormOptions instance.
Definition: FormOptions.php:83
validateIntBounds( $name, $min, $max)
fetchValuesFromRequest(WebRequest $r, $optionKeys=null)
Fetch values for all options (or selected options) from the given WebRequest, making them available f...
wfScript( $script='index')
Get the path to a specified script file, respecting file extensions; this is a wrapper around $wgScri...
Class representing a list of titles The execute() method checks them all for existence and adds them ...
Definition: LinkBatch.php:34
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
prefixSearchString( $search, $limit, $offset)
Perform a regular substring search for prefixSearchSubpages.
Shortcut to construct an includable special page.
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
getDBkey()
Get the main part with underscores.
Definition: Title.php:1014
static fieldset( $legend=false, $content=false, $attribs=[])
Shortcut for creating fieldsets.
Definition: Xml.php:609
static submitButton( $value, $attribs=[])
Convenience function to build an HTML submit button When $wgUseMediaWikiUIEverywhere is true it will ...
Definition: Xml.php:459
showIndirectLinks( $level, $target, $limit, $from=0, $back=0)
getSkin()
Shortcut to get the skin being used for this instance.
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:130
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes! ...
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1035
const NS_FILE
Definition: Defines.php:66
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:117
getPrevNext( $prevId, $nextId)
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
setValue( $name, $value, $force=false)
Use to set the value of an option.
getUser()
Shortcut to get the User executing this instance.
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
getConfig()
Shortcut to get main config object.
getLanguage()
Shortcut to get user&#39;s language.
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
static inputLabel( $label, $name, $id, $size=false, $value=false, $attribs=[])
Convenience function to build an HTML text input field with a label.
Definition: Xml.php:380
const DB_REPLICA
Definition: defines.php:25
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:419
getRequest()
Get the WebRequest being used for this instance.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
listItem( $row, $nt, $target, $notClose=false)
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages Per default the message key is the canonical name o...
getPageTitle( $subpage=false)
Get a self-referential title object.
Implements Special:Whatlinkshere.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
getValue( $name)
Get the value for the given option name.
MediaWiki Linker LinkRenderer null $linkRenderer
Definition: SpecialPage.php:67
static namespaceSelector(array $params=[], array $selectAttribs=[])
Build a drop-down box for selecting a namespace.
Definition: Html.php:892
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:317