/images/timeline
## Extension:Score
/images/lilypond
+## Extension:TimedMediaHandler
+/images/transcoded
/images/tmp
/maintenance/.mweval_history
/maintenance/.mwsql_history
* (T187750) New language support: Spanish formal address (es-formal).
=== Other changes in 1.31 ===
+* Browser support for Internet Explorer 10 was lowered from Grade A to Grade C.
* Introducing multi-content-revision capability into the storage layer. For details,
see <https://www.mediawiki.org/wiki/Requests_for_comment/Multi-Content_Revisions>.
* The Revision class was deprecated in favor of RevisionStore, BlobStore, and
'ImportStringSource' => __DIR__ . '/includes/import/ImportStringSource.php',
'ImportTextFiles' => __DIR__ . '/maintenance/importTextFiles.php',
'ImportTitleFactory' => __DIR__ . '/includes/title/ImportTitleFactory.php',
+ 'ImportableOldRevision' => __DIR__ . '/includes/import/ImportableOldRevision.php',
+ 'ImportableOldRevisionImporter' => __DIR__ . '/includes/import/ImportableOldRevisionImporter.php',
+ 'ImportableUploadRevision' => __DIR__ . '/includes/import/ImportableUploadRevision.php',
+ 'ImportableUploadRevisionImporter' => __DIR__ . '/includes/import/ImportableUploadRevisionImporter.php',
'IncludableSpecialPage' => __DIR__ . '/includes/specialpage/IncludableSpecialPage.php',
'IndexPager' => __DIR__ . '/includes/pager/IndexPager.php',
'InfoAction' => __DIR__ . '/includes/actions/InfoAction.php',
'ObjectFactory' => __DIR__ . '/includes/compat/ObjectFactory.php',
'OldChangesList' => __DIR__ . '/includes/changes/OldChangesList.php',
'OldLocalFile' => __DIR__ . '/includes/filerepo/file/OldLocalFile.php',
+ 'OldRevisionImporter' => __DIR__ . '/includes/import/OldRevisionImporter.php',
'OracleInstaller' => __DIR__ . '/includes/installer/OracleInstaller.php',
'OracleUpdater' => __DIR__ . '/includes/installer/OracleUpdater.php',
'OrderedStreamingForkController' => __DIR__ . '/includes/OrderedStreamingForkController.php',
'UploadFromStash' => __DIR__ . '/includes/upload/UploadFromStash.php',
'UploadFromUrl' => __DIR__ . '/includes/upload/UploadFromUrl.php',
'UploadLogFormatter' => __DIR__ . '/includes/logging/UploadLogFormatter.php',
+ 'UploadRevisionImporter' => __DIR__ . '/includes/import/UploadRevisionImporter.php',
'UploadSourceAdapter' => __DIR__ . '/includes/import/UploadSourceAdapter.php',
'UploadSourceField' => __DIR__ . '/includes/specials/formfields/UploadSourceField.php',
'UploadStash' => __DIR__ . '/includes/upload/UploadStash.php',
return $this->getService( 'ReadOnlyMode' );
}
+ /**
+ * @since 1.31
+ * @return \UploadRevisionImporter
+ */
+ public function getWikiRevisionUploadImporter() {
+ return $this->getService( 'UploadRevisionImporter' );
+ }
+
+ /**
+ * @since 1.31
+ * @return \OldRevisionImporter
+ */
+ public function getWikiRevisionOldRevisionImporter() {
+ return $this->getService( 'OldRevisionImporter' );
+ }
+
+ /**
+ * @since 1.31
+ * @return \OldRevisionImporter
+ */
+ public function getWikiRevisionOldRevisionImporterNoUpdates() {
+ return $this->getService( 'WikiRevisionOldRevisionImporterNoUpdates' );
+ }
+
/**
* @since 1.30
* @return CommandFactory
);
},
+ 'UploadRevisionImporter' => function ( MediaWikiServices $services ) {
+ return new ImportableUploadRevisionImporter(
+ $services->getMainConfig()->get( 'EnableUploads' ),
+ LoggerFactory::getInstance( 'UploadRevisionImporter' )
+ );
+ },
+
+ 'OldRevisionImporter' => function ( MediaWikiServices $services ) {
+ return new ImportableOldRevisionImporter(
+ true,
+ LoggerFactory::getInstance( 'OldRevisionImporter' ),
+ $services->getDBLoadBalancer()
+ );
+ },
+
+ 'WikiRevisionOldRevisionImporterNoUpdates' => function ( MediaWikiServices $services ) {
+ return new ImportableOldRevisionImporter(
+ false,
+ LoggerFactory::getInstance( 'OldRevisionImporter' ),
+ $services->getDBLoadBalancer()
+ );
+ },
+
'ShellCommandFactory' => function ( MediaWikiServices $services ) {
$config = $services->getMainConfig();
return $msg;
}
+ /**
+ * Get truncated message for the tag's long description.
+ *
+ * @param string $tag Tag name.
+ * @param int $length Maximum length of truncated message, including ellipsis.
+ * @param IContextSource $context
+ *
+ * @return string Truncated long tag description.
+ */
+ public static function truncateTagDescription( $tag, $length, IContextSource $context ) {
+ $originalDesc = self::tagLongDescriptionMessage( $tag, $context );
+ // If there is no tag description, return empty string
+ if ( !$originalDesc ) {
+ return '';
+ }
+
+ $taglessDesc = Sanitizer::stripAllTags( $originalDesc->parse() );
+ $escapedDesc = Sanitizer::escapeHtmlAllowEntities( $taglessDesc );
+
+ return $context->getLanguage()->truncateForVisual( $escapedDesc, $length );
+ }
+
/**
* Add tags to a change given its rc_id, rev_id and/or log_id
*
--- /dev/null
+<?php
+
+/**
+ * @since 1.31
+ */
+interface ImportableOldRevision {
+
+ /**
+ * @since 1.31
+ * @return User
+ */
+ public function getUserObj();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getUser();
+
+ /**
+ * @since 1.31
+ * @return Title
+ */
+ public function getTitle();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getTimestamp();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getComment();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getModel();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getFormat();
+
+ /**
+ * @since 1.31
+ * @return Content
+ */
+ public function getContent();
+
+ /**
+ * @since 1.31
+ * @return bool
+ */
+ public function getMinor();
+
+ /**
+ * @since 1.31
+ * @return bool|string
+ */
+ public function getSha1Base36();
+
+}
--- /dev/null
+<?php
+
+use Psr\Log\LoggerInterface;
+use Wikimedia\Rdbms\LoadBalancer;
+
+/**
+ * @since 1.31
+ */
+class ImportableOldRevisionImporter implements OldRevisionImporter {
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @var bool
+ */
+ private $doUpdates;
+
+ /**
+ * @var LoadBalancer
+ */
+ private $loadBalancer;
+
+ /**
+ * @param bool $doUpdates
+ * @param LoggerInterface $logger
+ * @param LoadBalancer $loadBalancer
+ */
+ public function __construct(
+ $doUpdates,
+ LoggerInterface $logger,
+ LoadBalancer $loadBalancer
+ ) {
+ $this->doUpdates = $doUpdates;
+ $this->logger = $logger;
+ $this->loadBalancer = $loadBalancer;
+ }
+
+ public function import( ImportableOldRevision $importableRevision, $doUpdates = true ) {
+ $dbw = $this->loadBalancer->getConnectionRef( DB_MASTER );
+
+ # Sneak a single revision into place
+ $user = $importableRevision->getUserObj() ?: User::newFromName( $importableRevision->getUser() );
+ if ( $user ) {
+ $userId = intval( $user->getId() );
+ $userText = $user->getName();
+ } else {
+ $userId = 0;
+ $userText = $importableRevision->getUser();
+ $user = new User;
+ }
+
+ // avoid memory leak...?
+ Title::clearCaches();
+
+ $page = WikiPage::factory( $importableRevision->getTitle() );
+ $page->loadPageData( 'fromdbmaster' );
+ if ( !$page->exists() ) {
+ // must create the page...
+ $pageId = $page->insertOn( $dbw );
+ $created = true;
+ $oldcountable = null;
+ } else {
+ $pageId = $page->getId();
+ $created = false;
+
+ // Note: sha1 has been in XML dumps since 2012. If you have an
+ // older dump, the duplicate detection here won't work.
+ $prior = $dbw->selectField( 'revision', '1',
+ [ 'rev_page' => $pageId,
+ 'rev_timestamp' => $dbw->timestamp( $importableRevision->getTimestamp() ),
+ 'rev_sha1' => $importableRevision->getSha1Base36() ],
+ __METHOD__
+ );
+ if ( $prior ) {
+ // @todo FIXME: This could fail slightly for multiple matches :P
+ $this->logger->debug( __METHOD__ . ": skipping existing revision for [[" .
+ $importableRevision->getTitle()->getPrefixedText() . "]], timestamp " .
+ $importableRevision->getTimestamp() . "\n" );
+ return false;
+ }
+ }
+
+ if ( !$pageId ) {
+ // This seems to happen if two clients simultaneously try to import the
+ // same page
+ $this->logger->debug( __METHOD__ . ': got invalid $pageId when importing revision of [[' .
+ $importableRevision->getTitle()->getPrefixedText() . ']], timestamp ' .
+ $importableRevision->getTimestamp() . "\n" );
+ return false;
+ }
+
+ // Select previous version to make size diffs correct
+ // @todo This assumes that multiple revisions of the same page are imported
+ // in order from oldest to newest.
+ $prevId = $dbw->selectField( 'revision', 'rev_id',
+ [
+ 'rev_page' => $pageId,
+ 'rev_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $importableRevision->getTimestamp() ) ),
+ ],
+ __METHOD__,
+ [ 'ORDER BY' => [
+ 'rev_timestamp DESC',
+ 'rev_id DESC', // timestamp is not unique per page
+ ]
+ ]
+ );
+
+ # @todo FIXME: Use original rev_id optionally (better for backups)
+ # Insert the row
+ $revision = new Revision( [
+ 'title' => $importableRevision->getTitle(),
+ 'page' => $pageId,
+ 'content_model' => $importableRevision->getModel(),
+ 'content_format' => $importableRevision->getFormat(),
+ // XXX: just set 'content' => $wikiRevision->getContent()?
+ 'text' => $importableRevision->getContent()->serialize( $importableRevision->getFormat() ),
+ 'comment' => $importableRevision->getComment(),
+ 'user' => $userId,
+ 'user_text' => $userText,
+ 'timestamp' => $importableRevision->getTimestamp(),
+ 'minor_edit' => $importableRevision->getMinor(),
+ 'parent_id' => $prevId,
+ ] );
+ $revision->insertOn( $dbw );
+ $changed = $page->updateIfNewerOn( $dbw, $revision );
+
+ if ( $changed !== false && $this->doUpdates ) {
+ $this->logger->debug( __METHOD__ . ": running updates\n" );
+ // countable/oldcountable stuff is handled in WikiImporter::finishImportPage
+ $page->doEditUpdates(
+ $revision,
+ $user,
+ [ 'created' => $created, 'oldcountable' => 'no-change' ]
+ );
+ }
+
+ return true;
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @since 1.31
+ */
+interface ImportableUploadRevision {
+
+ /**
+ * @since 1.31
+ * @return string Archive name of a revision if archived.
+ */
+ public function getArchiveName();
+
+ /**
+ * @since 1.31
+ * @return Title
+ */
+ public function getTitle();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getTimestamp();
+
+ /**
+ * @since 1.31
+ * @return string|null HTTP source of revision to be used for downloading.
+ */
+ public function getSrc();
+
+ /**
+ * @since 1.31
+ * @return string Local file source of the revision.
+ */
+ public function getFileSrc();
+
+ /**
+ * @since 1.31
+ * @return bool Is the return of getFileSrc only temporary?
+ */
+ public function isTempSrc();
+
+ /**
+ * @since 1.31
+ * @return string|bool sha1 of the revision, false if not set or errors occour.
+ */
+ public function getSha1();
+
+ /**
+ * @since 1.31
+ * @return User
+ */
+ public function getUserObj();
+
+ /**
+ * @since 1.31
+ * @return string The username of the user that created this revision
+ */
+ public function getUser();
+
+ /**
+ * @since 1.31
+ * @return string
+ */
+ public function getComment();
+
+}
--- /dev/null
+<?php
+
+use Psr\Log\LoggerInterface;
+
+/**
+ * @since 1.31
+ */
+class ImportableUploadRevisionImporter implements UploadRevisionImporter {
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @var bool
+ */
+ private $enableUploads;
+
+ /**
+ * @param bool $enableUploads
+ * @param LoggerInterface $logger
+ */
+ public function __construct(
+ $enableUploads,
+ LoggerInterface $logger
+ ) {
+ $this->enableUploads = $enableUploads;
+ $this->logger = $logger;
+ }
+
+ /**
+ * @return StatusValue
+ */
+ private function newNotOkStatus() {
+ $statusValue = new StatusValue();
+ $statusValue->setOK( false );
+ return $statusValue;
+ }
+
+ public function import( ImportableUploadRevision $importableRevision ) {
+ # Construct a file
+ $archiveName = $importableRevision->getArchiveName();
+ if ( $archiveName ) {
+ $this->logger->debug( __METHOD__ . "Importing archived file as $archiveName\n" );
+ $file = OldLocalFile::newFromArchiveName( $importableRevision->getTitle(),
+ RepoGroup::singleton()->getLocalRepo(), $archiveName );
+ } else {
+ $file = wfLocalFile( $importableRevision->getTitle() );
+ $file->load( File::READ_LATEST );
+ $this->logger->debug( __METHOD__ . 'Importing new file as ' . $file->getName() . "\n" );
+ if ( $file->exists() && $file->getTimestamp() > $importableRevision->getTimestamp() ) {
+ $archiveName = $file->getTimestamp() . '!' . $file->getName();
+ $file = OldLocalFile::newFromArchiveName( $importableRevision->getTitle(),
+ RepoGroup::singleton()->getLocalRepo(), $archiveName );
+ $this->logger->debug( __METHOD__ . "File already exists; importing as $archiveName\n" );
+ }
+ }
+ if ( !$file ) {
+ $this->logger->debug( __METHOD__ . ': Bad file for ' . $importableRevision->getTitle() . "\n" );
+ return $this->newNotOkStatus();
+ }
+
+ # Get the file source or download if necessary
+ $source = $importableRevision->getFileSrc();
+ $autoDeleteSource = $importableRevision->isTempSrc();
+ if ( !strlen( $source ) ) {
+ $source = $this->downloadSource( $importableRevision );
+ $autoDeleteSource = true;
+ }
+ if ( !strlen( $source ) ) {
+ $this->logger->debug( __METHOD__ . ": Could not fetch remote file.\n" );
+ return $this->newNotOkStatus();
+ }
+
+ $tmpFile = new TempFSFile( $source );
+ if ( $autoDeleteSource ) {
+ $tmpFile->autocollect();
+ }
+
+ $sha1File = ltrim( sha1_file( $source ), '0' );
+ $sha1 = $importableRevision->getSha1();
+ if ( $sha1 && ( $sha1 !== $sha1File ) ) {
+ $this->logger->debug( __METHOD__ . ": Corrupt file $source.\n" );
+ return $this->newNotOkStatus();
+ }
+
+ $user = $importableRevision->getUserObj() ?: User::newFromName( $importableRevision->getUser() );
+
+ # Do the actual upload
+ if ( $archiveName ) {
+ $status = $file->uploadOld( $source, $archiveName,
+ $importableRevision->getTimestamp(), $importableRevision->getComment(), $user );
+ } else {
+ $flags = 0;
+ $status = $file->upload(
+ $source,
+ $importableRevision->getComment(),
+ $importableRevision->getComment(),
+ $flags,
+ false,
+ $importableRevision->getTimestamp(),
+ $user
+ );
+ }
+
+ if ( $status->isGood() ) {
+ $this->logger->debug( __METHOD__ . ": Successful\n" );
+ } else {
+ $this->logger->debug( __METHOD__ . ': failed: ' . $status->getHTML() . "\n" );
+ }
+
+ return $status;
+ }
+
+ /**
+ * @deprecated DO NOT CALL ME.
+ * This method was introduced when factoring UploadImporter out of WikiRevision.
+ * It only has 1 use by the deprecated downloadSource method in WikiRevision.
+ * Do not use this in new code.
+ *
+ * @param ImportableUploadRevision $wikiRevision
+ *
+ * @return bool|string
+ */
+ public function downloadSource( ImportableUploadRevision $wikiRevision ) {
+ if ( !$this->enableUploads ) {
+ return false;
+ }
+
+ $tempo = tempnam( wfTempDir(), 'download' );
+ $f = fopen( $tempo, 'wb' );
+ if ( !$f ) {
+ $this->logger->debug( "IMPORT: couldn't write to temp file $tempo\n" );
+ return false;
+ }
+
+ // @todo FIXME!
+ $src = $wikiRevision->getSrc();
+ $data = Http::get( $src, [], __METHOD__ );
+ if ( !$data ) {
+ $this->logger->debug( "IMPORT: couldn't fetch source $src\n" );
+ fclose( $f );
+ unlink( $tempo );
+ return false;
+ }
+
+ fwrite( $f, $data );
+ fclose( $f );
+
+ return $tempo;
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @since 1.31
+ */
+interface OldRevisionImporter {
+
+ /**
+ * @since 1.31
+ *
+ * @param ImportableOldRevision $importableRevision
+ *
+ * @return bool Success
+ */
+ public function import( ImportableOldRevision $importableRevision );
+
+}
--- /dev/null
+<?php
+
+/**
+ * @since 1.31
+ */
+interface UploadRevisionImporter {
+
+ /**
+ * @since 1.31
+ *
+ * @param ImportableUploadRevision $importableUploadRevision
+ *
+ * @return StatusValue On success, the value member contains the
+ * archive name, or an empty string if it was a new file.
+ */
+ public function import( ImportableUploadRevision $importableUploadRevision );
+
+}
* @file
* @ingroup SpecialPage
*/
+use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
/**
* Represents a revision, log entry or upload during the import process.
*
* @ingroup SpecialPage
*/
-class WikiRevision {
+class WikiRevision implements ImportableUploadRevision, ImportableOldRevision {
/**
* @since 1.17
/**
* @since 1.12.2
- * @var mixed
+ * @var string|null
*/
- protected $src;
+ protected $src = null;
/**
* @since 1.18
/**
* @since 1.12.2
- * @param mixed $src
+ * @param string|null $src
*/
public function setSrc( $src ) {
$this->src = $src;
/**
* @since 1.12.2
- * @return mixed
+ * @return string|null
*/
public function getSrc() {
return $this->src;
return false;
}
+ /**
+ * @since 1.31
+ * @return bool|string
+ */
+ public function getSha1Base36() {
+ if ( $this->sha1base36 ) {
+ return $this->sha1base36;
+ }
+ return false;
+ }
+
/**
* @since 1.17
* @return string
/**
* @since 1.4.1
+ * @deprecated in 1.31. Use OldRevisionImporter::import
* @return bool
*/
public function importOldRevision() {
- $dbw = wfGetDB( DB_MASTER );
-
- # Sneak a single revision into place
- $user = $this->getUserObj() ?: User::newFromName( $this->getUser() );
- if ( $user ) {
- $userId = intval( $user->getId() );
- $userText = $user->getName();
- } else {
- $userId = 0;
- $userText = $this->getUser();
- $user = new User;
- }
-
- // avoid memory leak...?
- Title::clearCaches();
-
- $page = WikiPage::factory( $this->title );
- $page->loadPageData( 'fromdbmaster' );
- if ( !$page->exists() ) {
- // must create the page...
- $pageId = $page->insertOn( $dbw );
- $created = true;
- $oldcountable = null;
+ if ( $this->mNoUpdates ) {
+ $importer = MediaWikiServices::getInstance()->getWikiRevisionOldRevisionImporterNoUpdates();
} else {
- $pageId = $page->getId();
- $created = false;
-
- // Note: sha1 has been in XML dumps since 2012. If you have an
- // older dump, the duplicate detection here won't work.
- $prior = $dbw->selectField( 'revision', '1',
- [ 'rev_page' => $pageId,
- 'rev_timestamp' => $dbw->timestamp( $this->timestamp ),
- 'rev_sha1' => $this->sha1base36 ],
- __METHOD__
- );
- if ( $prior ) {
- // @todo FIXME: This could fail slightly for multiple matches :P
- wfDebug( __METHOD__ . ": skipping existing revision for [[" .
- $this->title->getPrefixedText() . "]], timestamp " . $this->timestamp . "\n" );
- return false;
- }
- }
-
- if ( !$pageId ) {
- // This seems to happen if two clients simultaneously try to import the
- // same page
- wfDebug( __METHOD__ . ': got invalid $pageId when importing revision of [[' .
- $this->title->getPrefixedText() . ']], timestamp ' . $this->timestamp . "\n" );
- return false;
+ $importer = MediaWikiServices::getInstance()->getWikiRevisionOldRevisionImporter();
}
-
- // Select previous version to make size diffs correct
- // @todo This assumes that multiple revisions of the same page are imported
- // in order from oldest to newest.
- $prevId = $dbw->selectField( 'revision', 'rev_id',
- [
- 'rev_page' => $pageId,
- 'rev_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $this->timestamp ) ),
- ],
- __METHOD__,
- [ 'ORDER BY' => [
- 'rev_timestamp DESC',
- 'rev_id DESC', // timestamp is not unique per page
- ]
- ]
- );
-
- # @todo FIXME: Use original rev_id optionally (better for backups)
- # Insert the row
- $revision = new Revision( [
- 'title' => $this->title,
- 'page' => $pageId,
- 'content_model' => $this->getModel(),
- 'content_format' => $this->getFormat(),
- // XXX: just set 'content' => $this->getContent()?
- 'text' => $this->getContent()->serialize( $this->getFormat() ),
- 'comment' => $this->getComment(),
- 'user' => $userId,
- 'user_text' => $userText,
- 'timestamp' => $this->timestamp,
- 'minor_edit' => $this->minor,
- 'parent_id' => $prevId,
- ] );
- $revision->insertOn( $dbw );
- $changed = $page->updateIfNewerOn( $dbw, $revision );
-
- if ( $changed !== false && !$this->mNoUpdates ) {
- wfDebug( __METHOD__ . ": running updates\n" );
- // countable/oldcountable stuff is handled in WikiImporter::finishImportPage
- $page->doEditUpdates(
- $revision,
- $user,
- [ 'created' => $created, 'oldcountable' => 'no-change' ]
- );
- }
-
- return true;
+ return $importer->import( $this );
}
/**
/**
* @since 1.12.2
+ * @deprecated in 1.31. Use UploadImporter::import
* @return bool
*/
public function importUpload() {
- # Construct a file
- $archiveName = $this->getArchiveName();
- if ( $archiveName ) {
- wfDebug( __METHOD__ . "Importing archived file as $archiveName\n" );
- $file = OldLocalFile::newFromArchiveName( $this->getTitle(),
- RepoGroup::singleton()->getLocalRepo(), $archiveName );
- } else {
- $file = wfLocalFile( $this->getTitle() );
- $file->load( File::READ_LATEST );
- wfDebug( __METHOD__ . 'Importing new file as ' . $file->getName() . "\n" );
- if ( $file->exists() && $file->getTimestamp() > $this->getTimestamp() ) {
- $archiveName = $file->getTimestamp() . '!' . $file->getName();
- $file = OldLocalFile::newFromArchiveName( $this->getTitle(),
- RepoGroup::singleton()->getLocalRepo(), $archiveName );
- wfDebug( __METHOD__ . "File already exists; importing as $archiveName\n" );
- }
- }
- if ( !$file ) {
- wfDebug( __METHOD__ . ': Bad file for ' . $this->getTitle() . "\n" );
- return false;
- }
-
- # Get the file source or download if necessary
- $source = $this->getFileSrc();
- $autoDeleteSource = $this->isTempSrc();
- if ( !strlen( $source ) ) {
- $source = $this->downloadSource();
- $autoDeleteSource = true;
- }
- if ( !strlen( $source ) ) {
- wfDebug( __METHOD__ . ": Could not fetch remote file.\n" );
- return false;
- }
-
- $tmpFile = new TempFSFile( $source );
- if ( $autoDeleteSource ) {
- $tmpFile->autocollect();
- }
-
- $sha1File = ltrim( sha1_file( $source ), '0' );
- $sha1 = $this->getSha1();
- if ( $sha1 && ( $sha1 !== $sha1File ) ) {
- wfDebug( __METHOD__ . ": Corrupt file $source.\n" );
- return false;
- }
-
- $user = $this->getUserObj() ?: User::newFromName( $this->getUser() );
-
- # Do the actual upload
- if ( $archiveName ) {
- $status = $file->uploadOld( $source, $archiveName,
- $this->getTimestamp(), $this->getComment(), $user );
- } else {
- $flags = 0;
- $status = $file->upload( $source, $this->getComment(), $this->getComment(),
- $flags, false, $this->getTimestamp(), $user );
- }
-
- if ( $status->isGood() ) {
- wfDebug( __METHOD__ . ": Successful\n" );
- return true;
- } else {
- wfDebug( __METHOD__ . ': failed: ' . $status->getHTML() . "\n" );
- return false;
- }
+ $importer = MediaWikiServices::getInstance()->getWikiRevisionUploadImporter();
+ $statusValue = $importer->import( $this );
+ return $statusValue->isGood();
}
/**
* @since 1.12.2
+ * @deprecated in 1.31. Use UploadImporter::downloadSource
* @return bool|string
*/
public function downloadSource() {
- if ( !$this->config->get( 'EnableUploads' ) ) {
- return false;
- }
-
- $tempo = tempnam( wfTempDir(), 'download' );
- $f = fopen( $tempo, 'wb' );
- if ( !$f ) {
- wfDebug( "IMPORT: couldn't write to temp file $tempo\n" );
- return false;
- }
-
- // @todo FIXME!
- $src = $this->getSrc();
- $data = Http::get( $src, [], __METHOD__ );
- if ( !$data ) {
- wfDebug( "IMPORT: couldn't fetch source $src\n" );
- fclose( $f );
- unlink( $tempo );
- return false;
- }
-
- fwrite( $f, $data );
- fclose( $f );
-
- return $tempo;
+ $importer = new ImportableUploadRevisionImporter(
+ $this->config->get( 'EnableUploads' ),
+ LoggerFactory::getInstance( 'UploadRevisionImporter' )
+ );
+ return $importer->downloadSource( $this );
}
}
$rows = [ $rows ];
}
- $affectedRowCount = 0;
- foreach ( $rows as $row ) {
- // Delete rows which collide with this one
- $indexWhereClauses = [];
- foreach ( $uniqueIndexes as $index ) {
- $indexColumns = (array)$index;
- $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
- if ( count( $indexRowValues ) != count( $indexColumns ) ) {
- throw new DBUnexpectedError(
- $this,
- 'New record does not provide all values for unique key (' .
+ $useTrx = !$this->trxLevel;
+ if ( $useTrx ) {
+ $this->begin( $fname, self::TRANSACTION_INTERNAL );
+ }
+ try {
+ $affectedRowCount = 0;
+ foreach ( $rows as $row ) {
+ // Delete rows which collide with this one
+ $indexWhereClauses = [];
+ foreach ( $uniqueIndexes as $index ) {
+ $indexColumns = (array)$index;
+ $indexRowValues = array_intersect_key( $row, array_flip( $indexColumns ) );
+ if ( count( $indexRowValues ) != count( $indexColumns ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'New record does not provide all values for unique key (' .
implode( ', ', $indexColumns ) . ')'
- );
- } elseif ( in_array( null, $indexRowValues, true ) ) {
- throw new DBUnexpectedError(
- $this,
- 'New record has a null value for unique key (' .
+ );
+ } elseif ( in_array( null, $indexRowValues, true ) ) {
+ throw new DBUnexpectedError(
+ $this,
+ 'New record has a null value for unique key (' .
implode( ', ', $indexColumns ) . ')'
- );
+ );
+ }
+ $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
}
- $indexWhereClauses[] = $this->makeList( $indexRowValues, LIST_AND );
- }
- if ( $indexWhereClauses ) {
- $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
+ if ( $indexWhereClauses ) {
+ $this->delete( $table, $this->makeList( $indexWhereClauses, LIST_OR ), $fname );
+ $affectedRowCount += $this->affectedRows();
+ }
+
+ // Now insert the row
+ $this->insert( $table, $row, $fname );
$affectedRowCount += $this->affectedRows();
}
-
- // Now insert the row
- $this->insert( $table, $row, $fname );
- $affectedRowCount += $this->affectedRows();
+ } catch ( Exception $e ) {
+ if ( $useTrx ) {
+ $this->rollback( $fname, self::FLUSHING_INTERNAL );
+ }
+ throw $e;
+ }
+ if ( $useTrx ) {
+ $this->commit( $fname, self::FLUSHING_INTERNAL );
}
$this->affectedRowCount = $affectedRowCount;
// this is just a sanity check
throw new MWException( 'Too many jpeg segments. Aborting' );
}
- while ( $buffer !== "\xFF" ) {
+ while ( $buffer !== "\xFF" && !feof( $fh ) ) {
// In theory JPEG files are not allowed to contain anything between the sections,
// but in practice they sometimes do. It's customary to ignore the garbage data.
$buffer = fread( $fh, 1 );
* @ingroup SpecialPage
*/
abstract class ChangesListSpecialPage extends SpecialPage {
+ /**
+ * Maximum length of a tag description in UTF-8 characters.
+ * Longer descriptions will be truncated.
+ */
+ const TAG_DESC_CHARACTER_LIMIT = 120;
+
/**
* Preference name for saved queries. Subclasses that use saved queries should override this.
* @var string
isset( $explicitlyDefinedTags[ $tagName ] ) ||
isset( $softwareActivatedTags[ $tagName ] )
) {
- // Parse description
- $desc = ChangeTags::tagLongDescriptionMessage( $tagName, $context );
-
$result[] = [
'name' => $tagName,
'label' => Sanitizer::stripAllTags(
ChangeTags::tagDescription( $tagName, $context )
),
- 'description' => $desc ? Sanitizer::stripAllTags( $desc->parse() ) : '',
+ 'description' =>
+ ChangeTags::truncateTagDescription(
+ $tagName, self::TAG_DESC_CHARACTER_LIMIT, $context
+ ),
'cssClass' => Sanitizer::escapeClass( 'mw-tag-' . $tagName ),
'hits' => $hits,
];
$this->setHeaders();
$this->outputHeader();
$out = $this->getOutput();
+ // Modules required for viewing the list of contributions (also when included on other pages)
$out->addModuleStyles( [
'mediawiki.special',
'mediawiki.special.changeslist',
- 'mediawiki.widgets.DateInputWidget.styles',
] );
- $out->addModules( 'mediawiki.special.contributions' );
$this->addHelpLink( 'Help:User contributions' );
- $out->enableOOUI();
$this->opts = [];
$request = $this->getRequest();
$this->opts['hideMinor'] = false;
}
+ // Modules required only for the form
+ $this->getOutput()->addModules( [
+ 'mediawiki.userSuggest',
+ 'mediawiki.special.contributions',
+ ] );
+ $this->getOutput()->addModuleStyles( 'mediawiki.widgets.DateInputWidget.styles' );
+ $this->getOutput()->enableOOUI();
+
$form = Html::openElement(
'form',
[
$filterSelection = Html::rawElement( 'div', [], '' );
}
- $this->getOutput()->addModules( 'mediawiki.userSuggest' );
-
$labelNewbies = Xml::radioLabel(
$this->msg( 'sp-contributions-newbies' )->text(),
'contribs',
}
/**
- * Truncate a string to a specified length in bytes, appending an optional
- * string (e.g. for ellipses)
+ * This method is deprecated since 1.31 and kept as alias for truncateForDatabase, which
+ * has replaced it. This method provides truncation suitable for DB.
*
* The database offers limited byte lengths for some columns in the database;
* multi-byte character sets mean we need to ensure that only whole characters
- * are included, otherwise broken characters can be passed to the user
+ * are included, otherwise broken characters can be passed to the user.
*
- * If $length is negative, the string will be truncated from the beginning
+ * @deprecated since 1.31, use truncateForDatabase or truncateForVisual as appropriate.
*
* @param string $string String to truncate
- * @param int $length Maximum length (including ellipses)
+ * @param int $length Maximum length (including ellipsis)
* @param string $ellipsis String to append to the truncated text
* @param bool $adjustLength Subtract length of ellipsis from $length.
* $adjustLength was introduced in 1.18, before that behaved as if false.
* @return string
*/
function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
+ return $this->truncateForDatabase( $string, $length, $ellipsis, $adjustLength );
+ }
+
+ /**
+ * Truncate a string to a specified length in bytes, appending an optional
+ * string (e.g. for ellipsis)
+ *
+ * If $length is negative, the string will be truncated from the beginning
+ *
+ * @since 1.31
+ *
+ * @param string $string String to truncate
+ * @param int $length Maximum length in bytes
+ * @param string $ellipsis String to append to the end of truncated text
+ * @param bool $adjustLength Subtract length of ellipsis from $length
+ *
+ * @return string
+ */
+ function truncateForDatabase( $string, $length, $ellipsis = '...', $adjustLength = true ) {
+ return $this->truncateInternal(
+ $string, $length, $ellipsis, $adjustLength, 'strlen', 'substr'
+ );
+ }
+
+ /**
+ * Truncate a string to a specified number of characters, appending an optional
+ * string (e.g. for ellipsis).
+ *
+ * This provides multibyte version of truncate() method of this class, suitable for truncation
+ * based on number of characters, instead of number of bytes.
+ *
+ * If $length is negative, the string will be truncated from the beginning.
+ *
+ * @since 1.31
+ *
+ * @param string $string String to truncate
+ * @param int $length Maximum number of characters
+ * @param string $ellipsis String to append to the end of truncated text
+ * @param bool $adjustLength Subtract length of ellipsis from $length
+ *
+ * @return string
+ */
+ function truncateForVisual( $string, $length, $ellipsis = '...', $adjustLength = true ) {
+ // Passing encoding to mb_strlen and mb_substr is optional.
+ // Encoding defaults to mb_internal_encoding(), which is set to UTF-8 in Setup.php, so
+ // explicit specification of encoding is skipped.
+ // Note: Both multibyte methods are callables invoked in truncateInternal.
+ return $this->truncateInternal(
+ $string, $length, $ellipsis, $adjustLength, 'mb_strlen', 'mb_substr'
+ );
+ }
+
+ /**
+ * Internal method used for truncation. This method abstracts text truncation into
+ * one common method, allowing users to provide length measurement function and
+ * function for finding substring.
+ *
+ * For usages, see truncateForDatabase and truncateForVisual.
+ *
+ * @param string $string String to truncate
+ * @param int $length Maximum length of final text
+ * @param string $ellipsis String to append to the end of truncated text
+ * @param bool $adjustLength Subtract length of ellipsis from $length
+ * @param callable $measureLength Callable function used for determining the length of text
+ * @param callable $getSubstring Callable function used for getting the substrings
+ *
+ * @return string
+ */
+ private function truncateInternal(
+ $string, $length, $ellipsis = '...', $adjustLength = true, $measureLength, $getSubstring
+ ) {
+ if ( !is_callable( $measureLength ) || !is_callable( $getSubstring ) ) {
+ throw new InvalidArgumentException( 'Invalid callback provided' );
+ }
+
# Check if there is no need to truncate
- if ( strlen( $string ) <= abs( $length ) ) {
+ if ( $measureLength( $string ) <= abs( $length ) ) {
return $string; // no need to truncate
}
+
# Use the localized ellipsis character
if ( $ellipsis == '...' ) {
$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
if ( $length == 0 ) {
return $ellipsis; // convention
}
+
$stringOriginal = $string;
# If ellipsis length is >= $length then we can't apply $adjustLength
- if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
+ if ( $adjustLength && $measureLength( $ellipsis ) >= abs( $length ) ) {
$string = $ellipsis; // this can be slightly unexpected
# Otherwise, truncate and add ellipsis...
} else {
- $eLength = $adjustLength ? strlen( $ellipsis ) : 0;
+ $ellipsisLength = $adjustLength ? $measureLength( $ellipsis ) : 0;
if ( $length > 0 ) {
- $length -= $eLength;
- $string = substr( $string, 0, $length ); // xyz...
+ $length -= $ellipsisLength;
+ $string = $getSubstring( $string, 0, $length ); // xyz...
$string = $this->removeBadCharLast( $string );
$string = rtrim( $string );
$string = $string . $ellipsis;
} else {
- $length += $eLength;
- $string = substr( $string, $length ); // ...xyz
+ $length += $ellipsisLength;
+ $string = $getSubstring( $string, $length ); // ...xyz
$string = $this->removeBadCharFirst( $string );
$string = ltrim( $string );
$string = $ellipsis . $string;
}
}
+
# Do not truncate if the ellipsis makes the string longer/equal (T24181).
# This check is *not* redundant if $adjustLength, due to the single case where
# LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
- if ( strlen( $string ) < strlen( $stringOriginal ) ) {
+ if ( $measureLength( $string ) < $measureLength( $stringOriginal ) ) {
return $string;
} else {
return $stringOriginal;
'ky' => 'Кыргызча', # Kirghiz
'la' => 'Latina', # Latin
'lad' => 'Ladino', # Ladino
- 'lb' => 'Lëtzebuergesch', # Luxemburguish
+ 'lb' => 'Lëtzebuergesch', # Luxembourgish
'lbe' => 'лакку', # Lak
'lez' => 'лезги', # Lezgi
'lfn' => 'Lingua Franca Nova', # Lingua Franca Nova
'Takes a file name containing SQL as argument or runs interactively.' );
$this->addOption( 'query',
'Run a single query instead of running interactively', false, true );
+ $this->addOption( 'json', 'Output the results as JSON instead of PHP objects' );
$this->addOption( 'cluster', 'Use an external cluster by name', false, true );
$this->addOption( 'wikidb',
'The database wiki ID to use if not the current one', false, true );
// Do nothing
return;
} elseif ( is_object( $res ) && $res->numRows() ) {
+ $out = '';
foreach ( $res as $row ) {
- $this->output( print_r( $row, true ) );
+ $out .= print_r( $row, true );
+ $rows[] = $row;
}
+ if ( $this->hasOption( 'json' ) ) {
+ $out = json_encode( $rows, JSON_PRETTY_PRINT );
+ }
+ $this->output( $out . "\n" );
} else {
$affected = $db->affectedRows();
$this->output( "Query OK, $affected row(s) affected\n" );
*
* Browsers we support in our modern run-time (Grade A):
* - Chrome 13+
- * - IE 10+
+ * - IE 11+
* - Firefox 4+
* - Safari 5+
* - Opera 15+
// support in the modern run-time.
// Note: Please extend the regex instead of adding new ones
!(
- ua.match( /webOS\/1\.[0-4]|SymbianOS|Series60|NetFront|Opera Mini|S40OviBrowser|MeeGo|Android.+Glass|^Mozilla\/5\.0 .+ Gecko\/$|googleweblight/ ) ||
+ ua.match( /MSIE 10|webOS\/1\.[0-4]|SymbianOS|Series60|NetFront|Opera Mini|S40OviBrowser|MeeGo|Android.+Glass|^Mozilla\/5\.0 .+ Gecko\/$|googleweblight/ ) ||
ua.match( /PlayStation/i )
)
);
'uniqueIndexes' => [ 'field' ],
'rows' => [ 'field' => 'text', 'field2' => 'text2' ],
],
- "DELETE FROM replace_table " .
+ "BEGIN; DELETE FROM replace_table " .
"WHERE (field = 'text'); " .
"INSERT INTO replace_table " .
"(field,field2) " .
- "VALUES ('text','text2')"
+ "VALUES ('text','text2'); COMMIT"
],
[
[
'md_deps' => 'deps',
],
],
- "DELETE FROM module_deps " .
+ "BEGIN; DELETE FROM module_deps " .
"WHERE (md_module = 'module' AND md_skin = 'skin'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
- "VALUES ('module','skin','deps')"
+ "VALUES ('module','skin','deps'); COMMIT"
],
[
[
],
],
],
- "DELETE FROM module_deps " .
+ "BEGIN; DELETE FROM module_deps " .
"WHERE (md_module = 'module' AND md_skin = 'skin'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
"WHERE (md_module = 'module2' AND md_skin = 'skin2'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
- "VALUES ('module2','skin2','deps2')"
+ "VALUES ('module2','skin2','deps2'); COMMIT"
],
[
[
],
],
],
- "DELETE FROM module_deps " .
+ "BEGIN; DELETE FROM module_deps " .
"WHERE (md_module = 'module') OR (md_skin = 'skin'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
"WHERE (md_module = 'module2') OR (md_skin = 'skin2'); " .
"INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
- "VALUES ('module2','skin2','deps2')"
+ "VALUES ('module2','skin2','deps2'); COMMIT"
],
[
[
'md_deps' => 'deps',
],
],
- "INSERT INTO module_deps " .
+ "BEGIN; INSERT INTO module_deps " .
"(md_module,md_skin,md_deps) " .
- "VALUES ('module','skin','deps')"
+ "VALUES ('module','skin','deps'); COMMIT"
],
];
}
$expected = 'BE';
$this->assertEquals( $expected, $res['byteOrder'] );
}
+
+ public function testInfiniteRead() {
+ // Should get past infinite loop and throw in wfUnpack()
+ $this->setExpectedException( 'MWException' );
+ $res = JpegMetadataExtractor::segmentSplitter( $this->filePath . 'jpeg-xmp-loop.jpg' );
+ }
}
}
/**
- * @covers Language::truncate
+ * @covers Language::truncateForDatabase
+ * @covers Language::truncateInternal
*/
- public function testTruncate() {
+ public function testTruncateForDatabase() {
$this->assertEquals(
"XXX",
- $this->getLang()->truncate( "1234567890", 0, 'XXX' ),
+ $this->getLang()->truncateForDatabase( "1234567890", 0, 'XXX' ),
'truncate prefix, len 0, small ellipsis'
);
$this->assertEquals(
"12345XXX",
- $this->getLang()->truncate( "1234567890", 8, 'XXX' ),
+ $this->getLang()->truncateForDatabase( "1234567890", 8, 'XXX' ),
'truncate prefix, small ellipsis'
);
$this->assertEquals(
"123456789",
- $this->getLang()->truncate( "123456789", 5, 'XXXXXXXXXXXXXXX' ),
+ $this->getLang()->truncateForDatabase( "123456789", 5, 'XXXXXXXXXXXXXXX' ),
'truncate prefix, large ellipsis'
);
$this->assertEquals(
"XXX67890",
- $this->getLang()->truncate( "1234567890", -8, 'XXX' ),
+ $this->getLang()->truncateForDatabase( "1234567890", -8, 'XXX' ),
'truncate suffix, small ellipsis'
);
$this->assertEquals(
"123456789",
- $this->getLang()->truncate( "123456789", -5, 'XXXXXXXXXXXXXXX' ),
+ $this->getLang()->truncateForDatabase( "123456789", -5, 'XXXXXXXXXXXXXXX' ),
'truncate suffix, large ellipsis'
);
$this->assertEquals(
"123XXX",
- $this->getLang()->truncate( "123 ", 9, 'XXX' ),
+ $this->getLang()->truncateForDatabase( "123 ", 9, 'XXX' ),
'truncate prefix, with spaces'
);
$this->assertEquals(
"12345XXX",
- $this->getLang()->truncate( "12345 8", 11, 'XXX' ),
+ $this->getLang()->truncateForDatabase( "12345 8", 11, 'XXX' ),
'truncate prefix, with spaces and non-space ending'
);
$this->assertEquals(
"XXX234",
- $this->getLang()->truncate( "1 234", -8, 'XXX' ),
+ $this->getLang()->truncateForDatabase( "1 234", -8, 'XXX' ),
'truncate suffix, with spaces'
);
$this->assertEquals(
"12345XXX",
- $this->getLang()->truncate( "1234567890", 5, 'XXX', false ),
+ $this->getLang()->truncateForDatabase( "1234567890", 5, 'XXX', false ),
'truncate without adjustment'
);
$this->assertEquals(
"泰乐菌...",
- $this->getLang()->truncate( "泰乐菌素123456789", 11, '...', false ),
+ $this->getLang()->truncateForDatabase( "泰乐菌素123456789", 11, '...', false ),
'truncate does not chop Unicode characters in half'
);
$this->assertEquals(
"\n泰乐菌...",
- $this->getLang()->truncate( "\n泰乐菌素123456789", 12, '...', false ),
+ $this->getLang()->truncateForDatabase( "\n泰乐菌素123456789", 12, '...', false ),
'truncate does not chop Unicode characters in half if there is a preceding newline'
);
}
+ /**
+ * @dataProvider provideTruncateData
+ * @covers Language::truncateForVisual
+ * @covers Language::truncateInternal
+ */
+ public function testTruncateForVisual(
+ $expected, $string, $length, $ellipsis = '...', $adjustLength = true
+ ) {
+ $this->assertEquals(
+ $expected,
+ $this->getLang()->truncateForVisual( $string, $length, $ellipsis, $adjustLength )
+ );
+ }
+
+ /**
+ * @return array Format is ($expected, $string, $length, $ellipsis, $adjustLength)
+ */
+ public static function provideTruncateData() {
+ return [
+ [ "XXX", "тестирам да ли ради", 0, "XXX" ],
+ [ "testnXXX", "testni scenarij", 8, "XXX" ],
+ [ "حالة اختبار", "حالة اختبار", 5, "XXXXXXXXXXXXXXX" ],
+ [ "XXXедент", "прецедент", -8, "XXX" ],
+ [ "XXപിൾ", "ആപ്പിൾ", -5, "XX" ],
+ [ "神秘XXX", "神秘 ", 9, "XXX" ],
+ [ "ΔημιουργXXX", "Δημιουργία Σύμπαντος", 11, "XXX" ],
+ [ "XXXの家です", "地球は私たちの唯 の家です", -8, "XXX" ],
+ [ "زندگیXXX", "زندگی زیباست", 6, "XXX", false ],
+ [ "ცხოვრება...", "ცხოვრება არის საოცარი", 8, "...", false ],
+ [ "\nທ່ານ...", "\nທ່ານບໍ່ຮູ້ຫນັງສື", 5, "...", false ],
+ ];
+ }
+
/**
* @dataProvider provideHTMLTruncateData
* @covers Language::truncateHTML
'Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36 OPR/15.0.1147.153',
'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36 OPR/16.0.1196.62',
'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36 OPR/23.0.1522.75',
- // Internet Explorer 10+
- 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
+ // Internet Explorer 11
'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko',
- // IE Mobile
- 'Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0; NOKIA; Lumia 800)',
// Edge
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',
// Edge Mobile
blacklisted: [
/* Grade C */
+ // Internet Explorer 10
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)',
+ // IE Mobile 10
+ 'Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; HTC; Windows Phone 8X by HTC)',
// PlayStation
'Mozilla/5.0 (PLAYSTATION 3; 1.10)',
'Mozilla/5.0 (PLAYSTATION 3; 3.55)',
The runner reads the config file `wdio.conf.js` and runs the spec listed in
`page.js`.
-The defaults in the configuration files aim are targetting a MediaWiki-Vagrant
-installation on installation on http://127.0.0.1:8080 with a user Admin and
-password 'vagrant'. Those settings can be overriden using environment
+The defaults in the configuration files aim are targeting a MediaWiki-Vagrant
+installation on http://127.0.0.1:8080 with a user Admin and
+password 'vagrant'. Those settings can be overridden using environment
variables:
`MW_SERVER`: to be set to the value of your $wgServer
-`MW_SCRIPT_PATH`: ditto with $wgScriptPath
-`MEDIAWIKI_USER`: username of an account that can create users on the wiki.
+`MW_SCRIPT_PATH`: ditto with $wgScriptPath
+`MEDIAWIKI_USER`: username of an account that can create users on the wiki
`MEDIAWIKI_PASSWORD`: password for above user
Example: