MediaWiki REL1_37
XmlDumpWriter.php
Go to the documentation of this file.
1<?php
33use Wikimedia\Assert\Assert;
34use Wikimedia\IPUtils;
35
40
42 public const WRITE_CONTENT = 0;
43
45 public const WRITE_STUB = 1;
46
51 private const WRITE_STUB_DELETED = 2;
52
57 public static $supportedSchemas = [
60 ];
61
68
74 private $currentTitle = null;
75
79 private $contentMode;
80
82 private $hookRunner;
83
90 public function __construct(
91 $contentMode = self::WRITE_CONTENT,
92 $schemaVersion = XML_DUMP_SCHEMA_VERSION_11
93 ) {
94 Assert::parameter(
95 in_array( $contentMode, [ self::WRITE_CONTENT, self::WRITE_STUB ], true ),
96 '$contentMode',
97 'must be one of the following constants: WRITE_CONTENT or WRITE_STUB.'
98 );
99
100 Assert::parameter(
101 in_array( $schemaVersion, self::$supportedSchemas, true ),
102 '$schemaVersion',
103 'must be one of the following schema versions: '
104 . implode( ',', self::$supportedSchemas )
105 );
106
107 $this->contentMode = $contentMode;
108 $this->schemaVersion = $schemaVersion;
109 $this->hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() );
110 }
111
122 public function openStream() {
123 $ver = $this->schemaVersion;
124 return Xml::element( 'mediawiki', [
125 'xmlns' => "http://www.mediawiki.org/xml/export-$ver/",
126 'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
127 /*
128 * When a new version of the schema is created, it needs staging on mediawiki.org.
129 * This requires a change in the operations/mediawiki-config git repo.
130 *
131 * Create a changeset like https://gerrit.wikimedia.org/r/#/c/149643/ in which
132 * you copy in the new xsd file.
133 *
134 * After it is reviewed, merged and deployed (sync-docroot), the index.html needs purging.
135 * echo "https://www.mediawiki.org/xml/index.html" | mwscript purgeList.php --wiki=aawiki
136 */
137 'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
138 "http://www.mediawiki.org/xml/export-$ver.xsd",
139 'version' => $ver,
140 'xml:lang' => MediaWikiServices::getInstance()->getContentLanguage()->getHtmlCode() ],
141 null ) .
142 "\n" .
143 $this->siteInfo();
144 }
145
149 private function siteInfo() {
150 $info = [
151 $this->sitename(),
152 $this->dbname(),
153 $this->homelink(),
154 $this->generator(),
155 $this->caseSetting(),
156 $this->namespaces() ];
157 return " <siteinfo>\n " .
158 implode( "\n ", $info ) .
159 "\n </siteinfo>\n";
160 }
161
165 private function sitename() {
166 global $wgSitename;
167 return Xml::element( 'sitename', [], $wgSitename );
168 }
169
173 private function dbname() {
174 global $wgDBname;
175 return Xml::element( 'dbname', [], $wgDBname );
176 }
177
181 private function generator() {
182 return Xml::element( 'generator', [], 'MediaWiki ' . MW_VERSION );
183 }
184
188 private function homelink() {
189 return Xml::element( 'base', [], Title::newMainPage()->getCanonicalURL() );
190 }
191
195 private function caseSetting() {
196 global $wgCapitalLinks;
197 // "case-insensitive" option is reserved for future
198 $sensitivity = $wgCapitalLinks ? 'first-letter' : 'case-sensitive';
199 return Xml::element( 'case', [], $sensitivity );
200 }
201
205 private function namespaces() {
206 $spaces = "<namespaces>\n";
207 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
208 foreach (
209 MediaWikiServices::getInstance()->getContentLanguage()->getFormattedNamespaces()
210 as $ns => $title
211 ) {
212 $spaces .= ' ' .
213 Xml::element( 'namespace',
214 [
215 'key' => $ns,
216 'case' => $nsInfo->isCapitalized( $ns )
217 ? 'first-letter' : 'case-sensitive',
218 ], $title ) . "\n";
219 }
220 $spaces .= " </namespaces>";
221 return $spaces;
222 }
223
230 public function closeStream() {
231 return "</mediawiki>\n";
232 }
233
241 public function openPage( $row ) {
242 $out = " <page>\n";
243 $this->currentTitle = Title::newFromRow( $row );
244 $canonicalTitle = self::canonicalTitle( $this->currentTitle );
245 $out .= ' ' . Xml::elementClean( 'title', [], $canonicalTitle ) . "\n";
246 $out .= ' ' . Xml::element( 'ns', [], strval( $row->page_namespace ) ) . "\n";
247 $out .= ' ' . Xml::element( 'id', [], strval( $row->page_id ) ) . "\n";
248 if ( $row->page_is_redirect ) {
249 $page = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $this->currentTitle );
250 $redirect = $this->invokeLenient(
251 static function () use ( $page ) {
252 return $page->getRedirectTarget();
253 },
254 'Failed to get redirect target of page ' . $page->getId()
255 );
256 if ( $redirect instanceof Title && $redirect->isValidRedirectTarget() ) {
257 $out .= ' ';
258 $out .= Xml::element( 'redirect', [ 'title' => self::canonicalTitle( $redirect ) ] );
259 $out .= "\n";
260 }
261 }
262
263 if ( $row->page_restrictions != '' ) {
264 $out .= ' ' . Xml::element( 'restrictions', [],
265 strval( $row->page_restrictions ) ) . "\n";
266 }
267
268 $this->hookRunner->onXmlDumpWriterOpenPage( $this, $out, $row, $this->currentTitle );
269
270 return $out;
271 }
272
279 public function closePage() {
280 if ( $this->currentTitle !== null ) {
281 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
282 // In rare cases, link cache has the same key for some pages which
283 // might be read as part of the same batch. T220424 and T220316
284 $linkCache->clearLink( $this->currentTitle );
285 }
286 return " </page>\n";
287 }
288
292 private function getRevisionStore() {
293 return MediaWikiServices::getInstance()->getRevisionStore();
294 }
295
299 private function getBlobStore() {
300 // @phan-suppress-next-line PhanTypeMismatchReturnSuperType
301 return MediaWikiServices::getInstance()->getBlobStore();
302 }
303
315 private function invokeLenient( $callback, $warning ) {
316 try {
317 return $callback();
318 } catch ( SuppressedDataException $ex ) {
319 return null;
320 } catch ( Exception $ex ) {
321 if ( $ex instanceof MWException || $ex instanceof RuntimeException ||
322 $ex instanceof InvalidArgumentException ) {
323 MWDebug::warning( $warning . ': ' . $ex->getMessage() );
324 return null;
325 } else {
326 throw $ex;
327 }
328 }
329 }
330
342 public function writeRevision( $row, $slotRows = null ) {
343 $rev = $this->getRevisionStore()->newRevisionFromRowAndSlots(
344 $row,
345 $slotRows,
346 0,
347 $this->currentTitle
348 );
349
350 $out = " <revision>\n";
351 $out .= " " . Xml::element( 'id', null, strval( $rev->getId() ) ) . "\n";
352
353 if ( $rev->getParentId() ) {
354 $out .= " " . Xml::element( 'parentid', null, strval( $rev->getParentId() ) ) . "\n";
355 }
356
357 $out .= $this->writeTimestamp( $rev->getTimestamp() );
358
359 if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
360 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
361 } else {
362 // empty values get written out as uid 0, see T224221
363 $user = $rev->getUser();
364 $out .= $this->writeContributor(
365 $user ? $user->getId() : 0,
366 $user ? $user->getName() : ''
367 );
368 }
369
370 if ( $rev->isMinor() ) {
371 $out .= " <minor/>\n";
372 }
373 if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
374 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
375 } else {
376 if ( $rev->getComment()->text != '' ) {
377 $out .= " "
378 // @phan-suppress-next-line SecurityCheck-DoubleEscaped getComment is polluted by truncate
379 . Xml::elementClean( 'comment', [], strval( $rev->getComment()->text ) )
380 . "\n";
381 }
382 }
383
384 $contentMode = $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ? self::WRITE_STUB_DELETED
385 : $this->contentMode;
386
387 $slots = $rev->getSlots()->getSlots();
388
389 // use predictable order, put main slot first
390 ksort( $slots );
391 $out .= $this->writeSlot( $slots[SlotRecord::MAIN], $contentMode );
392
393 foreach ( $slots as $role => $slot ) {
394 if ( $role === SlotRecord::MAIN ) {
395 continue;
396 }
397 $out .= $this->writeSlot( $slot, $contentMode );
398 }
399
400 if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
401 $out .= " <sha1/>\n";
402 } else {
403 $sha1 = $this->invokeLenient(
404 static function () use ( $rev ) {
405 return $rev->getSha1();
406 },
407 'failed to determine sha1 for revision ' . $rev->getId()
408 );
409 $out .= " " . Xml::element( 'sha1', null, strval( $sha1 ) ) . "\n";
410 }
411
412 // Avoid PHP 7.1 warning from passing $this by reference
413 $writer = $this;
414 $text = '';
415 if ( $contentMode === self::WRITE_CONTENT ) {
417 $content = $this->invokeLenient(
418 static function () use ( $rev ) {
419 return $rev->getContent( SlotRecord::MAIN, RevisionRecord::RAW );
420 },
421 'Failed to load main slot content of revision ' . $rev->getId()
422 );
423
424 $text = $content ? $content->serialize() : '';
425 }
426 $this->hookRunner->onXmlDumpWriterWriteRevision( $writer, $out, $row, $text, $rev );
427
428 $out .= " </revision>\n";
429
430 return $out;
431 }
432
439 private function writeSlot( SlotRecord $slot, $contentMode ) {
440 $isMain = $slot->getRole() === SlotRecord::MAIN;
441 $isV11 = $this->schemaVersion >= XML_DUMP_SCHEMA_VERSION_11;
442
443 if ( !$isV11 && !$isMain ) {
444 // ignore extra slots
445 return '';
446 }
447
448 $out = '';
449 $indent = ' ';
450
451 if ( !$isMain ) {
452 // non-main slots are wrapped into an additional element.
453 $out .= ' ' . Xml::openElement( 'content' ) . "\n";
454 $indent .= ' ';
455 $out .= $indent . Xml::element( 'role', null, strval( $slot->getRole() ) ) . "\n";
456 }
457
458 if ( $isV11 ) {
459 $out .= $indent . Xml::element( 'origin', null, strval( $slot->getOrigin() ) ) . "\n";
460 }
461
462 $contentModel = $slot->getModel();
463 $contentHandler = MediaWikiServices::getInstance()
464 ->getContentHandlerFactory()
465 ->getContentHandler( $contentModel );
466 $contentFormat = $contentHandler->getDefaultFormat();
467
468 // XXX: The content format is only relevant when actually outputting serialized content.
469 // It should probably be an attribute on the text tag.
470 $out .= $indent . Xml::element( 'model', null, strval( $contentModel ) ) . "\n";
471 $out .= $indent . Xml::element( 'format', null, strval( $contentFormat ) ) . "\n";
472
473 $textAttributes = [
474 'bytes' => $this->invokeLenient(
475 static function () use ( $slot ) {
476 return $slot->getSize();
477 },
478 'failed to determine size for slot ' . $slot->getRole() . ' of revision '
479 . $slot->getRevision()
480 ) ?: '0'
481 ];
482
483 if ( $isV11 ) {
484 $textAttributes['sha1'] = $this->invokeLenient(
485 static function () use ( $slot ) {
486 return $slot->getSha1();
487 },
488 'failed to determine sha1 for slot ' . $slot->getRole() . ' of revision '
489 . $slot->getRevision()
490 ) ?: '';
491 }
492
493 if ( $contentMode === self::WRITE_CONTENT ) {
494 $content = $this->invokeLenient(
495 static function () use ( $slot ) {
496 return $slot->getContent();
497 },
498 'failed to load content for slot ' . $slot->getRole() . ' of revision '
499 . $slot->getRevision()
500 );
501
502 if ( $content === null ) {
503 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
504 } else {
505 $out .= $this->writeText( $content, $textAttributes, $indent );
506 }
507 } elseif ( $contentMode === self::WRITE_STUB_DELETED ) {
508 // write <text> placeholder tag
509 $textAttributes['deleted'] = 'deleted';
510 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
511 } else {
512 // write <text> stub tag
513 if ( $isV11 ) {
514 $textAttributes['location'] = $slot->getAddress();
515 }
516
517 if ( $isMain ) {
518 // Output the numerical text ID if possible, for backwards compatibility.
519 // Note that this is currently the ONLY reason we have a BlobStore here at all.
520 // When removing this line, check whether the BlobStore has become unused.
521 try {
522 // NOTE: this will only work for addresses of the form "tt:12345".
523 // If we want to support other kinds of addresses in the future,
524 // we will have to silently ignore failures here.
525 // For now, this fails for "tt:0", which is present in the WMF production
526 // database as of July 2019, due to data corruption.
527 $textId = $this->getBlobStore()->getTextIdFromAddress( $slot->getAddress() );
528 } catch ( InvalidArgumentException $ex ) {
529 MWDebug::warning( 'Bad content address for slot ' . $slot->getRole()
530 . ' of revision ' . $slot->getRevision() . ': ' . $ex->getMessage() );
531 $textId = 0;
532 }
533
534 if ( is_int( $textId ) ) {
535 $textAttributes['id'] = $textId;
536 }
537 }
538
539 $out .= $indent . Xml::element( 'text', $textAttributes ) . "\n";
540 }
541
542 if ( !$isMain ) {
543 $out .= ' ' . Xml::closeElement( 'content' ) . "\n";
544 }
545
546 return $out;
547 }
548
556 private function writeText( Content $content, $textAttributes, $indent ) {
557 $out = '';
558
559 $contentHandler = $content->getContentHandler();
560 $contentFormat = $contentHandler->getDefaultFormat();
561
562 if ( $content instanceof TextContent ) {
563 // HACK: For text based models, bypass the serialization step. This allows extensions (like Flow)
564 // that use incompatible combinations of serialization format and content model.
565 $data = $content->getNativeData();
566 } else {
567 $data = $content->serialize( $contentFormat );
568 }
569
570 $data = $contentHandler->exportTransform( $data, $contentFormat );
571 $textAttributes['bytes'] = $size = strlen( $data ); // make sure to use the actual size
572 $textAttributes['xml:space'] = 'preserve';
573 $out .= $indent . Xml::elementClean( 'text', $textAttributes, strval( $data ) ) . "\n";
574
575 return $out;
576 }
577
585 public function writeLogItem( $row ) {
586 $out = " <logitem>\n";
587 $out .= " " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
588
589 $out .= $this->writeTimestamp( $row->log_timestamp, " " );
590
591 if ( $row->log_deleted & LogPage::DELETED_USER ) {
592 $out .= " " . Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
593 } else {
594 $out .= $this->writeContributor( $row->actor_user, $row->actor_name, " " );
595 }
596
597 if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
598 $out .= " " . Xml::element( 'comment', [ 'deleted' => 'deleted' ] ) . "\n";
599 } else {
600 $comment = CommentStore::getStore()->getComment( 'log_comment', $row )->text;
601 if ( $comment != '' ) {
602 // @phan-suppress-next-line SecurityCheck-DoubleEscaped CommentStore is polluted by truncate
603 $out .= " " . Xml::elementClean( 'comment', null, strval( $comment ) ) . "\n";
604 }
605 }
606
607 $out .= " " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
608 $out .= " " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
609
610 if ( $row->log_deleted & LogPage::DELETED_ACTION ) {
611 $out .= " " . Xml::element( 'text', [ 'deleted' => 'deleted' ] ) . "\n";
612 } else {
613 $title = Title::makeTitle( $row->log_namespace, $row->log_title );
614 $out .= " " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
615 $out .= " " . Xml::elementClean( 'params',
616 [ 'xml:space' => 'preserve' ],
617 strval( $row->log_params ) ) . "\n";
618 }
619
620 $out .= " </logitem>\n";
621
622 return $out;
623 }
624
630 public function writeTimestamp( $timestamp, $indent = " " ) {
631 $ts = wfTimestamp( TS_ISO_8601, $timestamp );
632 return $indent . Xml::element( 'timestamp', null, $ts ) . "\n";
633 }
634
641 public function writeContributor( $id, $text, $indent = " " ) {
642 $out = $indent . "<contributor>\n";
643 if ( $id || !IPUtils::isValid( $text ) ) {
644 $out .= $indent . " " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
645 $out .= $indent . " " . Xml::element( 'id', null, strval( $id ) ) . "\n";
646 } else {
647 $out .= $indent . " " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
648 }
649 $out .= $indent . "</contributor>\n";
650 return $out;
651 }
652
659 public function writeUploads( $row, $dumpContents = false ) {
660 if ( $row->page_namespace == NS_FILE ) {
661 $img = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
662 ->newFile( $row->page_title );
663 if ( $img && $img->exists() ) {
664 $out = '';
665 foreach ( array_reverse( $img->getHistory() ) as $ver ) {
666 $out .= $this->writeUpload( $ver, $dumpContents );
667 }
668 $out .= $this->writeUpload( $img, $dumpContents );
669 return $out;
670 }
671 }
672 return '';
673 }
674
680 private function writeUpload( $file, $dumpContents = false ) {
681 if ( $file->isOld() ) {
683 '@phan-var OldLocalFile $file';
684 $archiveName = " " .
685 Xml::element( 'archivename', null, $file->getArchiveName() ) . "\n";
686 } else {
687 $archiveName = '';
688 }
689 if ( $dumpContents ) {
690 $be = $file->getRepo()->getBackend();
691 # Dump file as base64
692 # Uses only XML-safe characters, so does not need escaping
693 # @todo Too bad this loads the contents into memory (script might swap)
694 $contents = ' <contents encoding="base64">' .
695 chunk_split( base64_encode(
696 $be->getFileContents( [ 'src' => $file->getPath() ] ) ) ) .
697 " </contents>\n";
698 } else {
699 $contents = '';
700 }
701 $uploader = $file->getUploader( File::FOR_PUBLIC );
702 if ( $uploader ) {
703 $uploader = $this->writeContributor( $uploader->getId(), $uploader->getName() );
704 } else {
705 $uploader = Xml::element( 'contributor', [ 'deleted' => 'deleted' ] ) . "\n";
706 }
707 $comment = $file->getDescription( File::FOR_PUBLIC );
708 if ( $comment ) {
709 $comment = Xml::elementClean( 'comment', null, $comment );
710 } else {
711 $comment = Xml::element( 'comment', [ 'deleted' => 'deleted' ] );
712 }
713 return " <upload>\n" .
714 $this->writeTimestamp( $file->getTimestamp() ) .
715 $uploader .
716 " " . $comment . "\n" .
717 " " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
718 $archiveName .
719 " " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
720 " " . Xml::element( 'size', null, $file->getSize() ) . "\n" .
721 " " . Xml::element( 'sha1base36', null, $file->getSha1() ) . "\n" .
722 " " . Xml::element( 'rel', null, $file->getRel() ) . "\n" .
723 $contents .
724 " </upload>\n";
725 }
726
737 public static function canonicalTitle( Title $title ) {
738 if ( $title->isExternal() ) {
739 return $title->getPrefixedText();
740 }
741
742 $prefix = MediaWikiServices::getInstance()->getContentLanguage()->
743 getFormattedNsText( $title->getNamespace() );
744
745 // @todo Emit some kind of warning to the user if $title->getNamespace() !==
746 // NS_MAIN and $prefix === '' (viz. pages in an unregistered namespace)
747
748 if ( $prefix !== '' ) {
749 $prefix .= ':';
750 }
751
752 return $prefix . $title->getText();
753 }
754}
$wgCapitalLinks
Set this to false to avoid forcing the first letter of links to capitals.
$wgDBname
Current wiki database name.
$wgSitename
Name of the site.
const NS_FILE
Definition Defines.php:70
const MW_VERSION
The running version of MediaWiki.
Definition Defines.php:36
const XML_DUMP_SCHEMA_VERSION_11
Definition Defines.php:314
const XML_DUMP_SCHEMA_VERSION_10
Definition Defines.php:313
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const DELETED_USER
Definition LogPage.php:41
const DELETED_COMMENT
Definition LogPage.php:40
const DELETED_ACTION
Definition LogPage.php:39
MediaWiki exception.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
MediaWikiServices is the service locator for the application scope of MediaWiki.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
getContent()
Returns the Content of the given slot.
getRole()
Returns the role of the slot.
getSha1()
Returns the content size.
getSize()
Returns the content size.
getAddress()
Returns the address of this slot's content.
getModel()
Returns the content model.
getOrigin()
Returns the revision ID of the revision that originated the slot's content.
getRevision()
Returns the ID of the revision this slot is associated with.
Exception raised in response to an audience check when attempting to access suppressed information wi...
Service for storing and loading Content objects.
Content object implementation for representing flat text.
Represents a title within MediaWiki.
Definition Title.php:48
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition Title.php:3896
closeStream()
Closes the output stream with the closing root element.
__construct( $contentMode=self::WRITE_CONTENT, $schemaVersion=XML_DUMP_SCHEMA_VERSION_11)
static string[] $supportedSchemas
the schema versions supported for output @final
const WRITE_STUB_DELETED
Only output subs for revision content, indicating that the content has been deleted/suppressed.
static canonicalTitle(Title $title)
Return prefixed text form of title, but using the content language's canonical namespace.
int $contentMode
Whether to output revision content or just stubs.
const WRITE_STUB
Only output subs for revision content.
string $schemaVersion
which schema version the generated XML should comply to.
writeUpload( $file, $dumpContents=false)
invokeLenient( $callback, $warning)
Invokes the given callback, catching and logging any storage related exceptions.
HookRunner $hookRunner
writeLogItem( $row)
Dumps a "<logitem>" section on the output stream, with data filled in from the given database row.
writeTimestamp( $timestamp, $indent=" ")
const WRITE_CONTENT
Output serialized revision content.
writeUploads( $row, $dumpContents=false)
Warning! This data is potentially inconsistent.
closePage()
Closes a "<page>" section on the output stream.
openStream()
Opens the XML output stream's root "<mediawiki>" element.
writeRevision( $row, $slotRows=null)
Dumps a "<revision>" section on the output stream, with data filled in from the given database row.
openPage( $row)
Opens a "<page>" section on the output stream, with data from the given database row.
writeText(Content $content, $textAttributes, $indent)
Title null $currentTitle
Title of the currently processed page.
writeSlot(SlotRecord $slot, $contentMode)
writeContributor( $id, $text, $indent=" ")
Base interface for content objects.
Definition Content.php:35
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42