From: jenkins-bot Date: Sat, 15 Oct 2016 06:45:26 +0000 (+0000) Subject: Merge "Only set memcache hash when saving messages to memcache" X-Git-Tag: 1.31.0-rc.0~5104 X-Git-Url: https://git.cyclocoop.org/%28%28?a=commitdiff_plain;h=296c00a6e1a0a174d16117f377d554d8c776b95b;hp=aa5be0c1e03dd70eeaa7a083088f2a0eee2b29d4;p=lhc%2Fweb%2Fwiklou.git Merge "Only set memcache hash when saving messages to memcache" --- diff --git a/CREDITS b/CREDITS index dca597ef0d..46d5c9ca0b 100644 --- a/CREDITS +++ b/CREDITS @@ -33,6 +33,7 @@ following names for their contribution to the product. * David McCabe * Derk-Jan Hartman * Domas Mituzas +* Ed Sanders * Emufarmers * Fran Rogers * Greg Sabino Mullane @@ -51,6 +52,7 @@ following names for their contribution to the product. * John Du Hart * Jon Harald Søby * Juliano F. Ravasi +* JuneHyeon Bae * Leo Koppelkamm * Leon Weber * Leslie Hoare @@ -157,7 +159,6 @@ following names for their contribution to the product. * Jimmy Xu * John N * Jonathan Wiltshire -* JuneHyeon Bae * Jure Kajzer * Karun Dambiec * Katie Filbert diff --git a/RELEASE-NOTES-1.28 b/RELEASE-NOTES-1.28 index b9278d058d..75fc139b58 100644 --- a/RELEASE-NOTES-1.28 +++ b/RELEASE-NOTES-1.28 @@ -227,8 +227,8 @@ changes to languages because of Phabricator reports. migrate to using the same functions on a ProxyLookup instance, obtainable from MediaWikiServices. * The ArticleAfterFetchContent, ArticleInsertComplete, ArticleSave, ArticleSaveComplete, - ArticleViewCustom, EditPageGetDiffText, EditPageGetPreviewText and ShowRawCssJs hooks - will now emit deprecation warnings if used. + ArticleViewCustom, EditFilterMerged, EditPageGetDiffText, EditPageGetPreviewText and + ShowRawCssJs hooks will now emit deprecation warnings if used. == Compatibility == diff --git a/autoload.php b/autoload.php index 748d954e30..636cb592fd 100644 --- a/autoload.php +++ b/autoload.php @@ -586,7 +586,7 @@ $wgAutoloadLocalClasses = [ 'IContextSource' => __DIR__ . '/includes/context/IContextSource.php', 'IDBAccessObject' => __DIR__ . '/includes/dao/IDBAccessObject.php', 'IDatabase' => __DIR__ . '/includes/libs/rdbms/database/IDatabase.php', - 'IEContentAnalyzer' => __DIR__ . '/includes/libs/IEContentAnalyzer.php', + 'IEContentAnalyzer' => __DIR__ . '/includes/libs/mime/IEContentAnalyzer.php', 'IEUrlExtension' => __DIR__ . '/includes/libs/IEUrlExtension.php', 'IExpiringStore' => __DIR__ . '/includes/libs/objectcache/IExpiringStore.php', 'IJobSpecification' => __DIR__ . '/includes/jobqueue/JobSpecification.php', @@ -781,7 +781,7 @@ $wgAutoloadLocalClasses = [ 'MWExceptionHandler' => __DIR__ . '/includes/exception/MWExceptionHandler.php', 'MWExceptionRenderer' => __DIR__ . '/includes/exception/MWExceptionRenderer.php', 'MWFileProps' => __DIR__ . '/includes/utils/MWFileProps.php', - 'MWGrants' => __DIR__ . '/includes/utils/MWGrants.php', + 'MWGrants' => __DIR__ . '/includes/MWGrants.php', 'MWHttpRequest' => __DIR__ . '/includes/http/MWHttpRequest.php', 'MWLBFactory' => __DIR__ . '/includes/db/MWLBFactory.php', 'MWMemcached' => __DIR__ . '/includes/compat/MemcachedClientCompat.php', @@ -943,6 +943,7 @@ $wgAutoloadLocalClasses = [ 'MessageSpecifier' => __DIR__ . '/includes/libs/MessageSpecifier.php', 'MigrateFileRepoLayout' => __DIR__ . '/maintenance/migrateFileRepoLayout.php', 'MigrateUserGroup' => __DIR__ . '/maintenance/migrateUserGroup.php', + 'MimeAnalyzer' => __DIR__ . '/includes/libs/mime/MimeAnalyzer.php', 'MimeMagic' => __DIR__ . '/includes/MimeMagic.php', 'MinifyScript' => __DIR__ . '/maintenance/minify.php', 'MostcategoriesPage' => __DIR__ . '/includes/specials/SpecialMostcategories.php', @@ -1580,7 +1581,7 @@ $wgAutoloadLocalClasses = [ 'XmlDumpWriter' => __DIR__ . '/includes/export/XmlDumpWriter.php', 'XmlJsCode' => __DIR__ . '/includes/Xml.php', 'XmlSelect' => __DIR__ . '/includes/XmlSelect.php', - 'XmlTypeCheck' => __DIR__ . '/includes/libs/XmlTypeCheck.php', + 'XmlTypeCheck' => __DIR__ . '/includes/libs/mime/XmlTypeCheck.php', 'ZhConverter' => __DIR__ . '/languages/classes/LanguageZh.php', 'ZipDirectoryReader' => __DIR__ . '/includes/utils/ZipDirectoryReader.php', 'ZipDirectoryReaderError' => __DIR__ . '/includes/utils/ZipDirectoryReader.php', diff --git a/docs/hooks.txt b/docs/hooks.txt index 3b3741a0da..562d7b4b2f 100644 --- a/docs/hooks.txt +++ b/docs/hooks.txt @@ -3791,6 +3791,10 @@ $content: the Content to generate updates for (or null, if the Content could not due to an error) &$updates: the array of DataUpdate objects. Hook function may want to add to it. +'WikiPageFactory': Override WikiPage class used for a title +$title: Title of the page +&$page: Variable to set the created WikiPage to. + 'XmlDumpWriterOpenPage': Called at the end of XmlDumpWriter::openPage, to allow extra metadata to be added. $obj: The XmlDumpWriter object. diff --git a/includes/Block.php b/includes/Block.php index 098d51c808..a11ba26484 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -692,11 +692,13 @@ class Block { public static function isWhitelistedFromAutoblocks( $ip ) { // Try to get the autoblock_whitelist from the cache, as it's faster // than getting the msg raw and explode()'ing it. - $cache = ObjectCache::getMainWANInstance(); + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); $lines = $cache->getWithSetCallback( wfMemcKey( 'ipb', 'autoblock', 'whitelist' ), $cache::TTL_DAY, - function () { + function ( $curValue, &$ttl, array &$setOpts ) { + $setOpts += Database::getCacheSetOptions( wfGetDB( DB_REPLICA ) ); + return explode( "\n", wfMessage( 'autoblock_whitelist' )->inContentLanguage()->plain() ); } diff --git a/includes/Defines.php b/includes/Defines.php index 02930ea815..06168980ce 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -97,32 +97,7 @@ define( 'CACHE_MEMCACHED', 2 ); // MemCached, must specify servers in $wgMemCac define( 'CACHE_ACCEL', 3 ); // APC, XCache or WinCache /**@}*/ -/**@{ - * Media types. - * This defines constants for the value returned by File::getMediaType() - */ -// unknown format -define( 'MEDIATYPE_UNKNOWN', 'UNKNOWN' ); -// some bitmap image or image source (like psd, etc). Can't scale up. -define( 'MEDIATYPE_BITMAP', 'BITMAP' ); -// some vector drawing (SVG, WMF, PS, ...) or image source (oo-draw, etc). Can scale up. -define( 'MEDIATYPE_DRAWING', 'DRAWING' ); -// simple audio file (ogg, mp3, wav, midi, whatever) -define( 'MEDIATYPE_AUDIO', 'AUDIO' ); -// simple video file (ogg, mpg, etc; -// no not include formats here that may contain executable sections or scripts!) -define( 'MEDIATYPE_VIDEO', 'VIDEO' ); -// Scriptable Multimedia (flash, advanced video container formats, etc) -define( 'MEDIATYPE_MULTIMEDIA', 'MULTIMEDIA' ); -// Office Documents, Spreadsheets (office formats possibly containing apples, scripts, etc) -define( 'MEDIATYPE_OFFICE', 'OFFICE' ); -// Plain text (possibly containing program code or scripts) -define( 'MEDIATYPE_TEXT', 'TEXT' ); -// binary executable -define( 'MEDIATYPE_EXECUTABLE', 'EXECUTABLE' ); -// archive file (zip, tar, etc) -define( 'MEDIATYPE_ARCHIVE', 'ARCHIVE' ); -/**@}*/ +require_once __DIR__ . '/libs/mime/defines.php'; /**@{ * Antivirus result codes, for use in $wgAntivirusSetup. diff --git a/includes/EditPage.php b/includes/EditPage.php index 8226da5a78..1c13d56e2f 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -1613,7 +1613,8 @@ class EditPage { protected function runPostMergeFilters( Content $content, Status $status, User $user ) { // Run old style post-section-merge edit filter if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', - [ $this, $content, &$this->hookError, $this->summary ] + [ $this, $content, &$this->hookError, $this->summary ], + '1.21' ) ) { # Error messages etc. could be handled within the hook... $status->fatal( 'hookaborted' ); @@ -1639,7 +1640,7 @@ class EditPage { // being set. This is used by ConfirmEdit to display a captcha // without any error message cruft. } else { - $this->hookError = $status->getWikiText(); + $this->hookError = $this->formatStatusErrors( $status ); } // Use the existing $status->value if the hook set it if ( !$status->value ) { @@ -1649,7 +1650,7 @@ class EditPage { } elseif ( !$status->isOK() ) { # ...or the hook could be expecting us to produce an error // FIXME this sucks, we should just use the Status object throughout - $this->hookError = $status->getWikiText(); + $this->hookError = $this->formatStatusErrors( $status ); $status->fatal( 'hookaborted' ); $status->value = self::AS_HOOK_ERROR_EXPECTED; return false; @@ -1658,6 +1659,26 @@ class EditPage { return true; } + /** + * Wrap status errors in an errorbox for increased visiblity + * + * @param Status $status + * @return string + */ + private function formatStatusErrors( Status $status ) { + $errmsg = $status->getHTML( + 'edit-error-short', + 'edit-error-long', + $this->context->getLanguage() + ); + return << +{$errmsg} + +
+ERROR; + } + /** * Return the summary to be used for a new section. * diff --git a/includes/MWGrants.php b/includes/MWGrants.php new file mode 100644 index 0000000000..58efdc7278 --- /dev/null +++ b/includes/MWGrants.php @@ -0,0 +1,214 @@ + array of rights + */ + public static function getRightsByGrant() { + global $wgGrantPermissions; + + $res = []; + foreach ( $wgGrantPermissions as $grant => $rights ) { + $res[$grant] = array_keys( array_filter( $rights ) ); + } + return $res; + } + + /** + * Fetch the display name of the grant + * @param string $grant + * @param Language|string|null $lang + * @return string Grant description + */ + public static function grantName( $grant, $lang = null ) { + // Give grep a chance to find the usages: + // grant-blockusers, grant-createeditmovepage, grant-delete, + // grant-editinterface, grant-editmycssjs, grant-editmywatchlist, + // grant-editpage, grant-editprotected, grant-highvolume, + // grant-oversight, grant-patrol, grant-protect, grant-rollback, + // grant-sendemail, grant-uploadeditmovefile, grant-uploadfile, + // grant-basic, grant-viewdeleted, grant-viewmywatchlist, + // grant-createaccount + $msg = wfMessage( "grant-$grant" ); + if ( $lang !== null ) { + if ( is_string( $lang ) ) { + $lang = Language::factory( $lang ); + } + $msg->inLanguage( $lang ); + } + if ( !$msg->exists() ) { + $msg = wfMessage( 'grant-generic', $grant ); + if ( $lang ) { + $msg->inLanguage( $lang ); + } + } + return $msg->text(); + } + + /** + * Fetch the display names for the grants. + * @param string[] $grants + * @param Language|string|null $lang + * @return string[] Corresponding grant descriptions + */ + public static function grantNames( array $grants, $lang = null ) { + if ( $lang !== null ) { + if ( is_string( $lang ) ) { + $lang = Language::factory( $lang ); + } + } + + $ret = []; + foreach ( $grants as $grant ) { + $ret[] = self::grantName( $grant, $lang ); + } + return $ret; + } + + /** + * Fetch the rights allowed by a set of grants. + * @param string[]|string $grants + * @return string[] + */ + public static function getGrantRights( $grants ) { + global $wgGrantPermissions; + + $rights = []; + foreach ( (array)$grants as $grant ) { + if ( isset( $wgGrantPermissions[$grant] ) ) { + $rights = array_merge( $rights, array_keys( array_filter( $wgGrantPermissions[$grant] ) ) ); + } + } + return array_unique( $rights ); + } + + /** + * Test that all grants in the list are known. + * @param string[] $grants + * @return bool + */ + public static function grantsAreValid( array $grants ) { + return array_diff( $grants, self::getValidGrants() ) === []; + } + + /** + * Divide the grants into groups. + * @param string[]|null $grantsFilter + * @return array Map of (group => (grant list)) + */ + public static function getGrantGroups( $grantsFilter = null ) { + global $wgGrantPermissions, $wgGrantPermissionGroups; + + if ( is_array( $grantsFilter ) ) { + $grantsFilter = array_flip( $grantsFilter ); + } + + $groups = []; + foreach ( $wgGrantPermissions as $grant => $rights ) { + if ( $grantsFilter !== null && !isset( $grantsFilter[$grant] ) ) { + continue; + } + if ( isset( $wgGrantPermissionGroups[$grant] ) ) { + $groups[$wgGrantPermissionGroups[$grant]][] = $grant; + } else { + $groups['other'][] = $grant; + } + } + + return $groups; + } + + /** + * Get the list of grants that are hidden and should always be granted + * @return string[] + */ + public static function getHiddenGrants() { + global $wgGrantPermissionGroups; + + $grants = []; + foreach ( $wgGrantPermissionGroups as $grant => $group ) { + if ( $group === 'hidden' ) { + $grants[] = $grant; + } + } + return $grants; + } + + /** + * Generate a link to Special:ListGrants for a particular grant name. + * + * This should be used to link end users to a full description of what + * rights they are giving when they authorize a grant. + * + * @param string $grant the grant name + * @param Language|string|null $lang + * @return string (proto-relative) HTML link + */ + public static function getGrantsLink( $grant, $lang = null ) { + return \Linker::linkKnown( + \SpecialPage::getTitleFor( 'Listgrants', false, $grant ), + htmlspecialchars( self::grantName( $grant, $lang ) ) + ); + } + + /** + * Generate wikitext to display a list of grants + * @param string[]|null $grantsFilter If non-null, only display these grants. + * @param Language|string|null $lang + * @return string Wikitext + */ + public static function getGrantsWikiText( $grantsFilter, $lang = null ) { + global $wgContLang; + + if ( is_string( $lang ) ) { + $lang = Language::factory( $lang ); + } elseif ( $lang === null ) { + $lang = $wgContLang; + } + + $s = ''; + foreach ( self::getGrantGroups( $grantsFilter ) as $group => $grants ) { + if ( $group === 'hidden' ) { + continue; // implicitly granted + } + $s .= "*" . + wfMessage( "grant-group-$group" )->inLanguage( $lang )->text() . "\n"; + $s .= ":" . $lang->semicolonList( self::grantNames( $grants, $lang ) ) . "\n"; + } + return "$s\n"; + } + +} diff --git a/includes/MediaWikiServices.php b/includes/MediaWikiServices.php index f91bbaed8a..7f94ced2c9 100644 --- a/includes/MediaWikiServices.php +++ b/includes/MediaWikiServices.php @@ -19,6 +19,7 @@ use MediaWiki\Services\SalvageableService; use MediaWiki\Services\ServiceContainer; use MediaWiki\Services\NoSuchServiceException; use MWException; +use MimeAnalyzer; use ObjectCache; use ProxyLookup; use SearchEngine; @@ -539,6 +540,14 @@ class MediaWikiServices extends ServiceContainer { return $this->getService( 'MediaHandlerFactory' ); } + /** + * @since 1.28 + * @return MimeAnalyzer + */ + public function getMimeAnalyzer() { + return $this->getService( 'MimeAnalyzer' ); + } + /** * @since 1.28 * @return ProxyLookup diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 54d58d2d44..c03bce77f0 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -1,7 +1,5 @@ ext - * map. Each line contains a MIME type followed by a space separated list of - * extensions. If multiple extensions for a single MIME type exist or if - * multiple MIME types exist for a single extension then in most cases - * MediaWiki assumes that the first extension following the MIME type is the - * canonical extension, and the first time a MIME type appears for a certain - * extension is considered the canonical MIME type. - * - * (Note that appending $wgMimeTypeFile to the end of MM_WELL_KNOWN_MIME_TYPES - * sucks because you can't redefine canonical types. This could be fixed by - * appending MM_WELL_KNOWN_MIME_TYPES behind $wgMimeTypeFile, but who knows - * what will break? In practice this probably isn't a problem anyway -- Bryan) - */ -define( 'MM_WELL_KNOWN_MIME_TYPES', <<makeConfig( 'main' ); - } - $this->mConfig = $config; - - /** - * --- load mime.types --- - */ - - global $IP; - - # Allow media handling extensions adding MIME-types and MIME-info - Hooks::run( 'MimeMagicInit', [ $this ] ); - - $types = MM_WELL_KNOWN_MIME_TYPES; - - $mimeTypeFile = $this->mConfig->get( 'MimeTypeFile' ); - if ( $mimeTypeFile == 'includes/mime.types' ) { - $mimeTypeFile = "$IP/$mimeTypeFile"; - } - - if ( $mimeTypeFile ) { - if ( is_file( $mimeTypeFile ) && is_readable( $mimeTypeFile ) ) { - wfDebug( __METHOD__ . ": loading mime types from $mimeTypeFile\n" ); - $types .= "\n"; - $types .= file_get_contents( $mimeTypeFile ); - } else { - wfDebug( __METHOD__ . ": can't load mime types from $mimeTypeFile\n" ); - } - } else { - wfDebug( __METHOD__ . ": no mime types file defined, using built-ins only.\n" ); - } - - $types .= "\n" . $this->mExtraTypes; - - $types = str_replace( [ "\r\n", "\n\r", "\n\n", "\r\r", "\r" ], "\n", $types ); - $types = str_replace( "\t", " ", $types ); - - $this->mMimeToExt = []; - $this->mExtToMime = []; - - $lines = explode( "\n", $types ); - foreach ( $lines as $s ) { - $s = trim( $s ); - if ( empty( $s ) ) { - continue; - } - if ( strpos( $s, '#' ) === 0 ) { - continue; - } - - $s = strtolower( $s ); - $i = strpos( $s, ' ' ); - - if ( $i === false ) { - continue; - } - - $mime = substr( $s, 0, $i ); - $ext = trim( substr( $s, $i + 1 ) ); - - if ( empty( $ext ) ) { - continue; - } - - if ( !empty( $this->mMimeToExt[$mime] ) ) { - $this->mMimeToExt[$mime] .= ' ' . $ext; - } else { - $this->mMimeToExt[$mime] = $ext; - } - - $extensions = explode( ' ', $ext ); - - foreach ( $extensions as $e ) { - $e = trim( $e ); - if ( empty( $e ) ) { - continue; - } - - if ( !empty( $this->mExtToMime[$e] ) ) { - $this->mExtToMime[$e] .= ' ' . $mime; - } else { - $this->mExtToMime[$e] = $mime; - } - } - } - - /** - * --- load mime.info --- - */ - - $mimeInfoFile = $this->mConfig->get( 'MimeInfoFile' ); - if ( $mimeInfoFile == 'includes/mime.info' ) { - $mimeInfoFile = "$IP/$mimeInfoFile"; - } - - $info = MM_WELL_KNOWN_MIME_INFO; - - if ( $mimeInfoFile ) { - if ( is_file( $mimeInfoFile ) && is_readable( $mimeInfoFile ) ) { - wfDebug( __METHOD__ . ": loading mime info from $mimeInfoFile\n" ); - $info .= "\n"; - $info .= file_get_contents( $mimeInfoFile ); - } else { - wfDebug( __METHOD__ . ": can't load mime info from $mimeInfoFile\n" ); - } - } else { - wfDebug( __METHOD__ . ": no mime info file defined, using built-ins only.\n" ); - } - - $info .= "\n" . $this->mExtraInfo; - - $info = str_replace( [ "\r\n", "\n\r", "\n\n", "\r\r", "\r" ], "\n", $info ); - $info = str_replace( "\t", " ", $info ); - - $this->mMimeTypeAliases = []; - $this->mMediaTypes = []; - - $lines = explode( "\n", $info ); - foreach ( $lines as $s ) { - $s = trim( $s ); - if ( empty( $s ) ) { - continue; - } - if ( strpos( $s, '#' ) === 0 ) { - continue; - } - - $s = strtolower( $s ); - $i = strpos( $s, ' ' ); - - if ( $i === false ) { - continue; - } - - # print "processing MIME INFO line $s
"; - - $match = []; - if ( preg_match( '!\[\s*(\w+)\s*\]!', $s, $match ) ) { - $s = preg_replace( '!\[\s*(\w+)\s*\]!', '', $s ); - $mtype = trim( strtoupper( $match[1] ) ); - } else { - $mtype = MEDIATYPE_UNKNOWN; - } - - $m = explode( ' ', $s ); - - if ( !isset( $this->mMediaTypes[$mtype] ) ) { - $this->mMediaTypes[$mtype] = []; - } - - foreach ( $m as $mime ) { - $mime = trim( $mime ); - if ( empty( $mime ) ) { - continue; - } - - $this->mMediaTypes[$mtype][] = $mime; - } - - if ( count( $m ) > 1 ) { - $main = $m[0]; - $mCount = count( $m ); - for ( $i = 1; $i < $mCount; $i += 1 ) { - $mime = $m[$i]; - $this->mMimeTypeAliases[$mime] = $main; - } - } - } - } - +class MimeMagic extends MimeAnalyzer { /** * Get an instance of this class * @return MimeMagic + * @deprecated since 1.28 */ public static function singleton() { - if ( self::$instance === null ) { - self::$instance = new MimeMagic( - ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) - ); - } - return self::$instance; - } - - /** - * Adds to the list mapping MIME to file extensions. - * As an extension author, you are encouraged to submit patches to - * MediaWiki's core to add new MIME types to mime.types. - * @param string $types - */ - public function addExtraTypes( $types ) { - $this->mExtraTypes .= "\n" . $types; - } - - /** - * Adds to the list mapping MIME to media type. - * As an extension author, you are encouraged to submit patches to - * MediaWiki's core to add new MIME info to mime.info. - * @param string $info - */ - public function addExtraInfo( $info ) { - $this->mExtraInfo .= "\n" . $info; - } - - /** - * Returns a list of file extensions for a given MIME type as a space - * separated string or null if the MIME type was unrecognized. Resolves - * MIME type aliases. - * - * @param string $mime - * @return string|null - */ - public function getExtensionsForType( $mime ) { - $mime = strtolower( $mime ); - - // Check the mime-to-ext map - if ( isset( $this->mMimeToExt[$mime] ) ) { - return $this->mMimeToExt[$mime]; - } - - // Resolve the MIME type to the canonical type - if ( isset( $this->mMimeTypeAliases[$mime] ) ) { - $mime = $this->mMimeTypeAliases[$mime]; - if ( isset( $this->mMimeToExt[$mime] ) ) { - return $this->mMimeToExt[$mime]; - } - } - - return null; + return MediaWikiServices::getInstance()->getMIMEAnalyzer(); } /** - * Returns a list of MIME types for a given file extension as a space - * separated string or null if the extension was unrecognized. - * - * @param string $ext - * @return string|null - */ - public function getTypesForExtension( $ext ) { - $ext = strtolower( $ext ); - - $r = isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null; - return $r; - } - - /** - * Returns a single MIME type for a given file extension or null if unknown. - * This is always the first type from the list returned by getTypesForExtension($ext). - * - * @param string $ext - * @return string|null - */ - public function guessTypesForExtension( $ext ) { - $m = $this->getTypesForExtension( $ext ); - if ( is_null( $m ) ) { - return null; - } - - // TODO: Check if this is needed; strtok( $m, ' ' ) should be sufficient - $m = trim( $m ); - $m = preg_replace( '/\s.*$/', '', $m ); - - return $m; - } - - /** - * Tests if the extension matches the given MIME type. Returns true if a - * match was found, null if the MIME type is unknown, and false if the - * MIME type is known but no matches where found. - * - * @param string $extension - * @param string $mime - * @return bool|null - */ - public function isMatchingExtension( $extension, $mime ) { - $ext = $this->getExtensionsForType( $mime ); - - if ( !$ext ) { - return null; // Unknown MIME type - } - - $ext = explode( ' ', $ext ); - - $extension = strtolower( $extension ); - return in_array( $extension, $ext ); - } - - /** - * Returns true if the MIME type is known to represent an image format - * supported by the PHP GD library. - * - * @param string $mime - * - * @return bool - */ - public function isPHPImageType( $mime ) { - // As defined by imagegetsize and image_type_to_mime - static $types = [ - 'image/gif', 'image/jpeg', 'image/png', - 'image/x-bmp', 'image/xbm', 'image/tiff', - 'image/jp2', 'image/jpeg2000', 'image/iff', - 'image/xbm', 'image/x-xbitmap', - 'image/vnd.wap.wbmp', 'image/vnd.xiff', - 'image/x-photoshop', - 'application/x-shockwave-flash', - ]; - - return in_array( $mime, $types ); - } - - /** - * Returns true if the extension represents a type which can - * be reliably detected from its content. Use this to determine - * whether strict content checks should be applied to reject - * invalid uploads; if we can't identify the type we won't - * be able to say if it's invalid. - * - * @todo Be more accurate when using fancy MIME detector plugins; - * right now this is the bare minimum getimagesize() list. - * @param string $extension - * @return bool - */ - function isRecognizableExtension( $extension ) { - static $types = [ - // Types recognized by getimagesize() - 'gif', 'jpeg', 'jpg', 'png', 'swf', 'psd', - 'bmp', 'tiff', 'tif', 'jpc', 'jp2', - 'jpx', 'jb2', 'swc', 'iff', 'wbmp', - 'xbm', - - // Formats we recognize magic numbers for - 'djvu', 'ogx', 'ogg', 'ogv', 'oga', 'spx', - 'mid', 'pdf', 'wmf', 'xcf', 'webm', 'mkv', 'mka', - 'webp', - - // XML formats we sure hope we recognize reliably - 'svg', - ]; - return in_array( strtolower( $extension ), $types ); - } - - /** - * Improves a MIME type using the file extension. Some file formats are very generic, - * so their MIME type is not very meaningful. A more useful MIME type can be derived - * by looking at the file extension. Typically, this method would be called on the - * result of guessMimeType(). - * - * @param string $mime The MIME type, typically guessed from a file's content. - * @param string $ext The file extension, as taken from the file name - * - * @return string The MIME type - */ - public function improveTypeFromExtension( $mime, $ext ) { - if ( $mime === 'unknown/unknown' ) { - if ( $this->isRecognizableExtension( $ext ) ) { - wfDebug( __METHOD__ . ': refusing to guess mime type for .' . - "$ext file, we should have recognized it\n" ); - } else { - // Not something we can detect, so simply - // trust the file extension - $mime = $this->guessTypesForExtension( $ext ); - } - } elseif ( $mime === 'application/x-opc+zip' ) { - if ( $this->isMatchingExtension( $ext, $mime ) ) { - // A known file extension for an OPC file, - // find the proper MIME type for that file extension - $mime = $this->guessTypesForExtension( $ext ); - } else { - wfDebug( __METHOD__ . ": refusing to guess better type for $mime file, " . - ".$ext is not a known OPC extension.\n" ); - $mime = 'application/zip'; - } - } elseif ( $mime === 'text/plain' && $this->findMediaType( ".$ext" ) === MEDIATYPE_TEXT ) { - // Textual types are sometimes not recognized properly. - // If detected as text/plain, and has an extension which is textual - // improve to the extension's type. For example, csv and json are often - // misdetected as text/plain. - $mime = $this->guessTypesForExtension( $ext ); - } - - # Media handling extensions can improve the MIME detected - Hooks::run( 'MimeMagicImproveFromExtension', [ $this, $ext, &$mime ] ); - - if ( isset( $this->mMimeTypeAliases[$mime] ) ) { - $mime = $this->mMimeTypeAliases[$mime]; - } - - wfDebug( __METHOD__ . ": improved mime type for .$ext: $mime\n" ); - return $mime; - } - - /** - * MIME type detection. This uses detectMimeType to detect the MIME type - * of the file, but applies additional checks to determine some well known - * file formats that may be missed or misinterpreted by the default MIME - * detection (namely XML based formats like XHTML or SVG, as well as ZIP - * based formats like OPC/ODF files). - * - * @param string $file The file to check - * @param string|bool $ext The file extension, or true (default) to extract it from the filename. - * Set it to false to ignore the extension. DEPRECATED! Set to false, use - * improveTypeFromExtension($mime, $ext) later to improve MIME type. - * - * @return string The MIME type of $file - */ - public function guessMimeType( $file, $ext = true ) { - if ( $ext ) { // TODO: make $ext default to false. Or better, remove it. - wfDebug( __METHOD__ . ": WARNING: use of the \$ext parameter is deprecated. " . - "Use improveTypeFromExtension(\$mime, \$ext) instead.\n" ); - } - - $mime = $this->doGuessMimeType( $file, $ext ); - - if ( !$mime ) { - wfDebug( __METHOD__ . ": internal type detection failed for $file (.$ext)...\n" ); - $mime = $this->detectMimeType( $file, $ext ); - } - - if ( isset( $this->mMimeTypeAliases[$mime] ) ) { - $mime = $this->mMimeTypeAliases[$mime]; - } - - wfDebug( __METHOD__ . ": guessed mime type of $file: $mime\n" ); - return $mime; - } - - /** - * Guess the MIME type from the file contents. - * - * @todo Remove $ext param - * - * @param string $file - * @param mixed $ext - * @return bool|string - * @throws MWException + * @param array $params + * @param Config $mainConfig + * @return array */ - private function doGuessMimeType( $file, $ext ) { - // Read a chunk of the file - MediaWiki\suppressWarnings(); - $f = fopen( $file, 'rb' ); - MediaWiki\restoreWarnings(); - - if ( !$f ) { - return 'unknown/unknown'; - } - - $fsize = filesize( $file ); - if ( $fsize === false ) { - return 'unknown/unknown'; - } - - $head = fread( $f, 1024 ); - $tailLength = min( 65558, $fsize ); // 65558 = maximum size of a zip EOCDR - if ( fseek( $f, -1 * $tailLength, SEEK_END ) === -1 ) { - throw new MWException( - "Seeking $tailLength bytes from EOF failed in " . __METHOD__ ); - } - $tail = $tailLength ? fread( $f, $tailLength ) : ''; - fclose( $f ); - - wfDebug( __METHOD__ . ": analyzing head and tail of $file for magic numbers.\n" ); - - // Hardcode a few magic number checks... - $headers = [ - // Multimedia... - 'MThd' => 'audio/midi', - 'OggS' => 'application/ogg', - - // Image formats... - // Note that WMF may have a bare header, no magic number. - "\x01\x00\x09\x00" => 'application/x-msmetafile', // Possibly prone to false positives? - "\xd7\xcd\xc6\x9a" => 'application/x-msmetafile', - '%PDF' => 'application/pdf', - 'gimp xcf' => 'image/x-xcf', - - // Some forbidden fruit... - 'MZ' => 'application/octet-stream', // DOS/Windows executable - "\xca\xfe\xba\xbe" => 'application/octet-stream', // Mach-O binary - "\x7fELF" => 'application/octet-stream', // ELF binary - ]; - - foreach ( $headers as $magic => $candidate ) { - if ( strncmp( $head, $magic, strlen( $magic ) ) == 0 ) { - wfDebug( __METHOD__ . ": magic header in $file recognized as $candidate\n" ); - return $candidate; - } - } - - /* Look for WebM and Matroska files */ - if ( strncmp( $head, pack( "C4", 0x1a, 0x45, 0xdf, 0xa3 ), 4 ) == 0 ) { - $doctype = strpos( $head, "\x42\x82" ); - if ( $doctype ) { - // Next byte is datasize, then data (sizes larger than 1 byte are very stupid muxers) - $data = substr( $head, $doctype + 3, 8 ); - if ( strncmp( $data, "matroska", 8 ) == 0 ) { - wfDebug( __METHOD__ . ": recognized file as video/x-matroska\n" ); - return "video/x-matroska"; - } elseif ( strncmp( $data, "webm", 4 ) == 0 ) { - wfDebug( __METHOD__ . ": recognized file as video/webm\n" ); - return "video/webm"; - } - } - wfDebug( __METHOD__ . ": unknown EBML file\n" ); - return "unknown/unknown"; - } - - /* Look for WebP */ - if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 7 ), "WEBPVP8", 7 ) == 0 ) { - wfDebug( __METHOD__ . ": recognized file as image/webp\n" ); - return "image/webp"; - } - - /** - * Look for PHP. Check for this before HTML/XML... Warning: this is a - * heuristic, and won't match a file with a lot of non-PHP before. It - * will also match text files which could be PHP. :) - * - * @todo FIXME: For this reason, the check is probably useless -- an attacker - * could almost certainly just pad the file with a lot of nonsense to - * circumvent the check in any case where it would be a security - * problem. On the other hand, it causes harmful false positives (bug - * 16583). The heuristic has been cut down to exclude three-character - * strings like "wellFormed ) { - $xmlMimeTypes = $this->mConfig->get( 'XMLMimeTypes' ); - if ( isset( $xmlMimeTypes[$xml->getRootElement()] ) ) { - return $xmlMimeTypes[$xml->getRootElement()]; - } else { - return 'application/xml'; - } - } - - /** - * look for shell scripts - */ - $script_type = null; - - # detect by shebang - if ( substr( $head, 0, 2 ) == "#!" ) { - $script_type = "ASCII"; - } elseif ( substr( $head, 0, 5 ) == "\xef\xbb\xbf#!" ) { - $script_type = "UTF-8"; - } elseif ( substr( $head, 0, 7 ) == "\xfe\xff\x00#\x00!" ) { - $script_type = "UTF-16BE"; - } elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) { - $script_type = "UTF-16LE"; - } - - if ( $script_type ) { - if ( $script_type !== "UTF-8" && $script_type !== "ASCII" ) { - // Quick and dirty fold down to ASCII! - $pack = [ 'UTF-16BE' => 'n*', 'UTF-16LE' => 'v*' ]; - $chars = unpack( $pack[$script_type], substr( $head, 2 ) ); - $head = ''; - foreach ( $chars as $codepoint ) { - if ( $codepoint < 128 ) { - $head .= chr( $codepoint ); - } else { - $head .= '?'; + public static function applyDefaultParameters( array $params, Config $mainConfig ) { + $logger = LoggerFactory::getInstance( 'Mime' ); + $params += [ + 'typeFile' => $mainConfig->get( 'MimeTypeFile' ), + 'infoFile' => $mainConfig->get( 'MimeInfoFile' ), + 'xmlTypes' => $mainConfig->get( 'XMLMimeTypes' ), + 'guessCallback' => + function ( $mimeAnalyzer, &$head, &$tail, $file, &$mime ) use ( $logger ) { + // Also test DjVu + $deja = new DjVuImage( $file ); + if ( $deja->isValid() ) { + $logger->info( __METHOD__ . ": detected $file as image/vnd.djvu\n" ); + $mime = 'image/vnd.djvu'; + + return; } - } - } - - $match = []; - - if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) { - $mime = "application/x-{$match[2]}"; - wfDebug( __METHOD__ . ": shell script recognized as $mime\n" ); - return $mime; - } - } - - // Check for ZIP variants (before getimagesize) - if ( strpos( $tail, "PK\x05\x06" ) !== false ) { - wfDebug( __METHOD__ . ": ZIP header present in $file\n" ); - return $this->detectZipType( $head, $tail, $ext ); - } - - MediaWiki\suppressWarnings(); - $gis = getimagesize( $file ); - MediaWiki\restoreWarnings(); - - if ( $gis && isset( $gis['mime'] ) ) { - $mime = $gis['mime']; - wfDebug( __METHOD__ . ": getimagesize detected $file as $mime\n" ); - return $mime; - } - - // Also test DjVu - $deja = new DjVuImage( $file ); - if ( $deja->isValid() ) { - wfDebug( __METHOD__ . ": detected $file as image/vnd.djvu\n" ); - return 'image/vnd.djvu'; - } - - # Media handling extensions can guess the MIME by content - # It's intentionally here so that if core is wrong about a type (false positive), - # people will hopefully nag and submit patches :) - $mime = false; - # Some strings by reference for performance - assuming well-behaved hooks - Hooks::run( - 'MimeMagicGuessFromContent', - [ $this, &$head, &$tail, $file, &$mime ] - ); - - return $mime; - } - - /** - * Detect application-specific file type of a given ZIP file from its - * header data. Currently works for OpenDocument and OpenXML types... - * If can't tell, returns 'application/zip'. - * - * @param string $header Some reasonably-sized chunk of file header - * @param string|null $tail The tail of the file - * @param string|bool $ext The file extension, or true to extract it from the filename. - * Set it to false (default) to ignore the extension. DEPRECATED! Set to false, - * use improveTypeFromExtension($mime, $ext) later to improve MIME type. - * - * @return string - */ - function detectZipType( $header, $tail = null, $ext = false ) { - if ( $ext ) { # TODO: remove $ext param - wfDebug( __METHOD__ . ": WARNING: use of the \$ext parameter is deprecated. " . - "Use improveTypeFromExtension(\$mime, \$ext) instead.\n" ); - } - - $mime = 'application/zip'; - $opendocTypes = [ - 'chart-template', - 'chart', - 'formula-template', - 'formula', - 'graphics-template', - 'graphics', - 'image-template', - 'image', - 'presentation-template', - 'presentation', - 'spreadsheet-template', - 'spreadsheet', - 'text-template', - 'text-master', - 'text-web', - 'text' ]; - - // http://lists.oasis-open.org/archives/office/200505/msg00006.html - $types = '(?:' . implode( '|', $opendocTypes ) . ')'; - $opendocRegex = "/^mimetype(application\/vnd\.oasis\.opendocument\.$types)/"; - - $openxmlRegex = "/^\[Content_Types\].xml/"; - - if ( preg_match( $opendocRegex, substr( $header, 30 ), $matches ) ) { - $mime = $matches[1]; - wfDebug( __METHOD__ . ": detected $mime from ZIP archive\n" ); - } elseif ( preg_match( $openxmlRegex, substr( $header, 30 ) ) ) { - $mime = "application/x-opc+zip"; - # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere - if ( $ext !== true && $ext !== false ) { - // These MIME's are stored in the database, where we don't really want - // x-opc+zip, because we use it only for internal purposes - if ( $this->isMatchingExtension( $ext, $mime ) ) { - /* A known file extension for an OPC file, - * find the proper mime type for that file extension - */ - $mime = $this->guessTypesForExtension( $ext ); - } else { - $mime = "application/zip"; - } - } - wfDebug( __METHOD__ . ": detected an Open Packaging Conventions archive: $mime\n" ); - } elseif ( substr( $header, 0, 8 ) == "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" && - ( $headerpos = strpos( $tail, "PK\x03\x04" ) ) !== false && - preg_match( $openxmlRegex, substr( $tail, $headerpos + 30 ) ) ) { - if ( substr( $header, 512, 4 ) == "\xEC\xA5\xC1\x00" ) { - $mime = "application/msword"; - } - switch ( substr( $header, 512, 6 ) ) { - case "\xEC\xA5\xC1\x00\x0E\x00": - case "\xEC\xA5\xC1\x00\x1C\x00": - case "\xEC\xA5\xC1\x00\x43\x00": - $mime = "application/vnd.ms-powerpoint"; - break; - case "\xFD\xFF\xFF\xFF\x10\x00": - case "\xFD\xFF\xFF\xFF\x1F\x00": - case "\xFD\xFF\xFF\xFF\x22\x00": - case "\xFD\xFF\xFF\xFF\x23\x00": - case "\xFD\xFF\xFF\xFF\x28\x00": - case "\xFD\xFF\xFF\xFF\x29\x00": - case "\xFD\xFF\xFF\xFF\x10\x02": - case "\xFD\xFF\xFF\xFF\x1F\x02": - case "\xFD\xFF\xFF\xFF\x22\x02": - case "\xFD\xFF\xFF\xFF\x23\x02": - case "\xFD\xFF\xFF\xFF\x28\x02": - case "\xFD\xFF\xFF\xFF\x29\x02": - $mime = "application/vnd.msexcel"; - break; - } - - wfDebug( __METHOD__ . ": detected a MS Office document with OPC trailer\n" ); - } else { - wfDebug( __METHOD__ . ": unable to identify type of ZIP archive\n" ); - } - return $mime; - } - - /** - * Internal MIME type detection. Detection is done using an external - * program, if $wgMimeDetectorCommand is set. Otherwise, the fileinfo - * extension is tried if it is available. If detection fails and $ext - * is not false, the MIME type is guessed from the file extension, - * using guessTypesForExtension. - * - * If the MIME type is still unknown, getimagesize is used to detect the - * MIME type if the file is an image. If no MIME type can be determined, - * this function returns 'unknown/unknown'. - * - * @param string $file The file to check - * @param string|bool $ext The file extension, or true (default) to extract it from the filename. - * Set it to false to ignore the extension. DEPRECATED! Set to false, use - * improveTypeFromExtension($mime, $ext) later to improve MIME type. - * - * @return string The MIME type of $file - */ - private function detectMimeType( $file, $ext = true ) { - /** @todo Make $ext default to false. Or better, remove it. */ - if ( $ext ) { - wfDebug( __METHOD__ . ": WARNING: use of the \$ext parameter is deprecated. " - . "Use improveTypeFromExtension(\$mime, \$ext) instead.\n" ); - } - - $mimeDetectorCommand = $this->mConfig->get( 'MimeDetectorCommand' ); - $m = null; - if ( $mimeDetectorCommand ) { - $args = wfEscapeShellArg( $file ); - $m = wfShellExec( "$mimeDetectorCommand $args" ); - } elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) { - $mime_magic_resource = finfo_open( FILEINFO_MIME ); - - if ( $mime_magic_resource ) { - $m = finfo_file( $mime_magic_resource, $file ); - finfo_close( $mime_magic_resource ); - } else { - wfDebug( __METHOD__ . ": finfo_open failed on " . FILEINFO_MIME . "!\n" ); - } - } else { - wfDebug( __METHOD__ . ": no magic mime detector found!\n" ); - } - - if ( $m ) { - # normalize - $m = preg_replace( '![;, ].*$!', '', $m ); # strip charset, etc - $m = trim( $m ); - $m = strtolower( $m ); - - if ( strpos( $m, 'unknown' ) !== false ) { - $m = null; - } else { - wfDebug( __METHOD__ . ": magic mime type of $file: $m\n" ); - return $m; - } - } - - // If desired, look at extension as a fallback. - if ( $ext === true ) { - $i = strrpos( $file, '.' ); - $ext = strtolower( $i ? substr( $file, $i + 1 ) : '' ); - } - if ( $ext ) { - if ( $this->isRecognizableExtension( $ext ) ) { - wfDebug( __METHOD__ . ": refusing to guess mime type for .$ext file, " - . "we should have recognized it\n" ); - } else { - $m = $this->guessTypesForExtension( $ext ); - if ( $m ) { - wfDebug( __METHOD__ . ": extension mime type of $file: $m\n" ); - return $m; - } - } - } - - // Unknown type - wfDebug( __METHOD__ . ": failed to guess mime type for $file!\n" ); - return 'unknown/unknown'; - } - - /** - * Determine the media type code for a file, using its MIME type, name and - * possibly its contents. - * - * This function relies on the findMediaType(), mapping extensions and MIME - * types to media types. - * - * @todo analyse file if need be - * @todo look at multiple extension, separately and together. - * - * @param string $path Full path to the image file, in case we have to look at the contents - * (if null, only the MIME type is used to determine the media type code). - * @param string $mime MIME type. If null it will be guessed using guessMimeType. - * - * @return string A value to be used with the MEDIATYPE_xxx constants. - */ - function getMediaType( $path = null, $mime = null ) { - if ( !$mime && !$path ) { - return MEDIATYPE_UNKNOWN; - } - - // If MIME type is unknown, guess it - if ( !$mime ) { - $mime = $this->guessMimeType( $path, false ); - } - - // Special code for ogg - detect if it's video (theora), - // else label it as sound. - if ( $mime == 'application/ogg' && file_exists( $path ) ) { - - // Read a chunk of the file - $f = fopen( $path, "rt" ); - if ( !$f ) { - return MEDIATYPE_UNKNOWN; - } - $head = fread( $f, 256 ); - fclose( $f ); - - $head = str_replace( 'ffmpeg2theora', '', strtolower( $head ) ); - - // This is an UGLY HACK, file should be parsed correctly - if ( strpos( $head, 'theora' ) !== false ) { - return MEDIATYPE_VIDEO; - } elseif ( strpos( $head, 'vorbis' ) !== false ) { - return MEDIATYPE_AUDIO; - } elseif ( strpos( $head, 'flac' ) !== false ) { - return MEDIATYPE_AUDIO; - } elseif ( strpos( $head, 'speex' ) !== false ) { - return MEDIATYPE_AUDIO; - } else { - return MEDIATYPE_MULTIMEDIA; - } - } - - $type = null; - // Check for entry for full MIME type - if ( $mime ) { - $type = $this->findMediaType( $mime ); - if ( $type !== MEDIATYPE_UNKNOWN ) { - return $type; - } - } - - // Check for entry for file extension - if ( $path ) { - $i = strrpos( $path, '.' ); - $e = strtolower( $i ? substr( $path, $i + 1 ) : '' ); - - // TODO: look at multi-extension if this fails, parse from full path - $type = $this->findMediaType( '.' . $e ); - if ( $type !== MEDIATYPE_UNKNOWN ) { - return $type; - } - } - - // Check major MIME type - if ( $mime ) { - $i = strpos( $mime, '/' ); - if ( $i !== false ) { - $major = substr( $mime, 0, $i ); - $type = $this->findMediaType( $major ); - if ( $type !== MEDIATYPE_UNKNOWN ) { - return $type; - } - } - } + // Some strings by reference for performance - assuming well-behaved hooks + Hooks::run( + 'MimeMagicGuessFromContent', + [ $mimeAnalyzer, &$head, &$tail, $file, &$mime ] + ); + }, + 'extCallback' => function ( $mimeAnalyzer, $ext, &$mime ) { + // Media handling extensions can improve the MIME detected + Hooks::run( 'MimeMagicImproveFromExtension', [ $mimeAnalyzer, $ext, &$mime ] ); + }, + 'initCallback' => function ( $mimeAnalyzer ) { + // Allow media handling extensions adding MIME-types and MIME-info + Hooks::run( 'MimeMagicInit', [ $mimeAnalyzer ] ); + }, + 'logger' => $logger + ]; - if ( !$type ) { - $type = MEDIATYPE_UNKNOWN; + if ( $params['infoFile'] === 'includes/mime.info' ) { + $params['infoFile'] = __DIR__ . "/libs/mime/mime.info"; } - return $type; - } - - /** - * Returns a media code matching the given MIME type or file extension. - * File extensions are represented by a string starting with a dot (.) to - * distinguish them from MIME types. - * - * This function relies on the mapping defined by $this->mMediaTypes - * @access private - * @param string $extMime - * @return int|string - */ - function findMediaType( $extMime ) { - if ( strpos( $extMime, '.' ) === 0 ) { - // If it's an extension, look up the MIME types - $m = $this->getTypesForExtension( substr( $extMime, 1 ) ); - if ( !$m ) { - return MEDIATYPE_UNKNOWN; - } - - $m = explode( ' ', $m ); - } else { - // Normalize MIME type - if ( isset( $this->mMimeTypeAliases[$extMime] ) ) { - $extMime = $this->mMimeTypeAliases[$extMime]; - } - - $m = [ $extMime ]; + if ( $params['typeFile'] === 'includes/mime.types' ) { + $params['typeFile'] = __DIR__ . "/libs/mime/mime.types"; } - foreach ( $m as $mime ) { - foreach ( $this->mMediaTypes as $type => $codes ) { - if ( in_array( $mime, $codes, true ) ) { - return $type; - } - } + $detectorCmd = $mainConfig->get( 'MimeDetectorCommand' ); + if ( $detectorCmd ) { + $params['detectCallback'] = function ( $file ) use ( $detectorCmd ) { + return wfShellExec( "$detectorCmd " . wfEscapeShellArg( $file ) ); + }; } - return MEDIATYPE_UNKNOWN; - } - - /** - * Get the MIME types that various versions of Internet Explorer would - * detect from a chunk of the content. - * - * @param string $fileName The file name (unused at present) - * @param string $chunk The first 256 bytes of the file - * @param string $proposed The MIME type proposed by the server - * @return array - */ - public function getIEMimeTypes( $fileName, $chunk, $proposed ) { - $ca = $this->getIEContentAnalyzer(); - return $ca->getRealMimesFromData( $fileName, $chunk, $proposed ); - } - - /** - * Get a cached instance of IEContentAnalyzer - * - * @return IEContentAnalyzer - */ - protected function getIEContentAnalyzer() { - if ( is_null( $this->mIEAnalyzer ) ) { - $this->mIEAnalyzer = new IEContentAnalyzer; - } - return $this->mIEAnalyzer; + return $params; } } diff --git a/includes/Revision.php b/includes/Revision.php index 208652fd22..8f337f955c 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -20,6 +20,7 @@ * @file */ use MediaWiki\Linker\LinkTarget; +use MediaWiki\MediaWikiServices; /** * @todo document @@ -1902,7 +1903,7 @@ class Revision implements IDBAccessObject { * @since 1.28 */ public static function newKnownCurrent( IDatabase $db, $pageId, $revId ) { - $cache = ObjectCache::getMainWANInstance(); + $cache = MediaWikiServices::getInstance()->getMainWANObjectCache(); return $cache->getWithSetCallback( // Page/rev IDs passed in from DB to reflect history merges $cache->makeGlobalKey( 'revision', $db->getWikiID(), $pageId, $revId ), diff --git a/includes/ServiceWiring.php b/includes/ServiceWiring.php index 42b75f042b..49183e5705 100644 --- a/includes/ServiceWiring.php +++ b/includes/ServiceWiring.php @@ -190,6 +190,15 @@ return [ ); }, + 'MimeAnalyzer' => function( MediaWikiServices $services ) { + return new MimeMagic( + MimeMagic::applyDefaultParameters( + [], + $services->getMainConfig() + ) + ); + }, + 'ProxyLookup' => function( MediaWikiServices $services ) { $mainConfig = $services->getMainConfig(); return new ProxyLookup( diff --git a/includes/actions/InfoAction.php b/includes/actions/InfoAction.php index 4d80a1cd33..c039388783 100644 --- a/includes/actions/InfoAction.php +++ b/includes/actions/InfoAction.php @@ -690,7 +690,6 @@ class InfoAction extends FormlessAction { $dbr = wfGetDB( DB_REPLICA ); $dbrWatchlist = wfGetDB( DB_REPLICA, 'watchlist' ); - $setOpts += Database::getCacheSetOptions( $dbr, $dbrWatchlist ); $watchedItemStore = MediaWikiServices::getInstance()->getWatchedItemStore(); diff --git a/includes/api/i18n/bg.json b/includes/api/i18n/bg.json index 4156395500..29c06059a9 100644 --- a/includes/api/i18n/bg.json +++ b/includes/api/i18n/bg.json @@ -2,7 +2,8 @@ "@metadata": { "authors": [ "Vodnokon4e", - "StanProg" + "StanProg", + "Spas.Z.Spasov" ] }, "apihelp-main-param-action": "Кое действие да се извърши.", @@ -30,7 +31,7 @@ "apihelp-feedcontributions-param-year": "От година (и по-рано).", "apihelp-feedcontributions-param-month": "От месец (и по-рано).", "apihelp-feedcontributions-param-tagfilter": "Филтриране на приноси, които имат тези етикети.", - "apihelp-feedcontributions-param-deletedonly": "Покажи само изтритите редакции.", + "apihelp-feedcontributions-param-deletedonly": "Покажи само изтритите приноси.", "apihelp-feedcontributions-param-newonly": "Показване само на редакции за създаване на страници.", "apihelp-feedcontributions-param-hideminor": "Скриване на малки промени.", "apihelp-feedcontributions-param-showsizediff": "Показване на размера на разликите между версиите.", diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 05f606d66c..c20ed5d31f 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -101,7 +101,7 @@ "apihelp-edit-param-tags": "Change tags to apply to the revision.", "apihelp-edit-param-minor": "Minor edit.", "apihelp-edit-param-notminor": "Non-minor edit.", - "apihelp-edit-param-bot": "Mark this edit as bot.", + "apihelp-edit-param-bot": "Mark this edit as a bot edit.", "apihelp-edit-param-basetimestamp": "Timestamp of the base revision, used to detect edit conflicts. May be obtained through [[Special:ApiHelp/query+revisions|action=query&prop=revisions&rvprop=timestamp]].", "apihelp-edit-param-starttimestamp": "Timestamp when the editing process began, used to detect edit conflicts. An appropriate value may be obtained using [[Special:ApiHelp/main|curtimestamp]] when beginning the edit process (e.g. when loading the page content to edit).", "apihelp-edit-param-recreate": "Override any errors about the page having been deleted in the meantime.", @@ -130,7 +130,7 @@ "apihelp-emailuser-param-ccme": "Send a copy of this mail to me.", "apihelp-emailuser-example-email": "Send an email to user WikiSysop with the text Content.", - "apihelp-expandtemplates-description": "Expands all templates in wikitext.", + "apihelp-expandtemplates-description": "Expands all templates within wikitext.", "apihelp-expandtemplates-param-title": "Title of page.", "apihelp-expandtemplates-param-text": "Wikitext to convert.", "apihelp-expandtemplates-param-revid": "Revision ID, for {{REVISIONID}} and similar variables.", @@ -156,7 +156,7 @@ "apihelp-feedcontributions-param-month": "From month (and earlier).", "apihelp-feedcontributions-param-tagfilter": "Filter contributions that have these tags.", "apihelp-feedcontributions-param-deletedonly": "Show only deleted contributions.", - "apihelp-feedcontributions-param-toponly": "Only show edits that are latest revisions.", + "apihelp-feedcontributions-param-toponly": "Only show edits that are the latest revisions.", "apihelp-feedcontributions-param-newonly": "Only show edits that are page creations.", "apihelp-feedcontributions-param-hideminor": "Hide minor edits.", "apihelp-feedcontributions-param-showsizediff": "Show the size difference between revisions.", diff --git a/includes/api/i18n/fr.json b/includes/api/i18n/fr.json index d535a5d1a9..435c6b4906 100644 --- a/includes/api/i18n/fr.json +++ b/includes/api/i18n/fr.json @@ -37,7 +37,7 @@ "apihelp-main-param-smaxage": "Fixer l’entête HTTP de contrôle de cache s-maxage à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.", "apihelp-main-param-maxage": "Fixer l’entête HTTP de contrôle de cache max-age à ce nombre de secondes. Les erreurs ne sont jamais mises en cache.", "apihelp-main-param-assert": "Vérifier si l’utilisateur est connecté si positionné à user, ou s'il a le droit d'un utilisateur robot si positionné à bot.", - "apihelp-main-param-assertuser": "Vérifier que l’utilisateur courant est l’utilisateur nommé.", + "apihelp-main-param-assertuser": "Vérifier que l’utilisateur actuel est l’utilisateur nommé.", "apihelp-main-param-requestid": "Toute valeur fournie ici sera incluse dans la réponse. Peut être utilisé pour distinguer des demandes.", "apihelp-main-param-servedby": "Inclure le nom d’hôte qui a renvoyé la requête dans les résultats.", "apihelp-main-param-curtimestamp": "Inclure l’horodatage actuel dans le résultat.", diff --git a/includes/api/i18n/pl.json b/includes/api/i18n/pl.json index 7e45e55b42..c28ac642ba 100644 --- a/includes/api/i18n/pl.json +++ b/includes/api/i18n/pl.json @@ -395,10 +395,10 @@ "apihelp-xml-param-xslt": "Jeśli określony, dodaje podaną stronę jako arkusz styli XSL. Powinna to być strona wiki w przestrzeni nazw MediaWiki, której nazwa kończy się na .xsl.", "apihelp-xmlfm-description": "Dane wyjściowe w formacie XML (prawidłowo wyświetlane w HTML).", "api-format-title": "Wynik MediaWiki API", - "api-pageset-param-titles": "Lista tytułów, nad którymi trzeba pracować.", - "api-pageset-param-pageids": "Lista identyfikatorów stron, nad którymi trzeba pracować.", - "api-pageset-param-revids": "Lista identyfikatorów wersji, nad którymi trzeba pracować.", - "api-pageset-param-generator": "Pobierz listę stron, nad którymi trzeba pracować poprzez wykonanie określonego modułu zapytań.\n\nUwaga: Nazwy parametrów generatora musi poprzedzać prefiks „g”. Zobacz przykłady.", + "api-pageset-param-titles": "Lista tytułów, z którymi pracować.", + "api-pageset-param-pageids": "Lista identyfikatorów stron, z którymi pracować.", + "api-pageset-param-revids": "Lista identyfikatorów wersji, z którymi pracować.", + "api-pageset-param-generator": "Pobierz listę stron, z którymi pracować poprzez wykonanie określonego modułu zapytań.\n\nUwaga: Nazwy parametrów generatora musi poprzedzać prefiks „g”. Zobacz przykłady.", "api-pageset-param-redirects-generator": "Automatycznie rozwiązuj przekierowania ze stron podanych w $1titles, $1pageids, oraz $1revids, a także ze stron zwróconych przez $1generator.", "api-pageset-param-converttitles": "Konwertuj tytuły do innych wariantów, jeżeli trzeba. Będzie działać tylko wtedy, gdy język zawartości wiki będzie wspierał konwersje wariantów. Języki, które wspierają konwersję wariantów to m.in. $1.", "api-help-title": "Pomoc MediaWiki API", diff --git a/includes/api/i18n/pt.json b/includes/api/i18n/pt.json index 7e6977e6c0..a9fe78455a 100644 --- a/includes/api/i18n/pt.json +++ b/includes/api/i18n/pt.json @@ -8,7 +8,7 @@ "Hamilton Abreu" ] }, - "apihelp-main-description": "
\n* [[mw:API:Main_page|Documentação]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discussão]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anúncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitações]\n
\nEstado: Todas as funcionalidades mostradas nesta página deveriam estar a funcionar, mas a API ainda está em activo desenvolvimento, e pode ser alterada a qualquer momento. Inscreva-se na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discussão mediawiki-api-announce] para ser informado acerca das actualizações.\n\nSolicitações erradas: Quando solicitações erradas são enviadas à API, um cabeçalho em HTTP será enviado com a chave \"MediaWiki-API-Error\" e, em seguida, tanto o valor do cabeçalho quanto o código de erro retornado serão definidos com o mesmo valor. Para mais informação, consulte [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\nTestes: Para facilitar os testes de solicitações à API, consulte [[Special:ApiSandbox]].", + "apihelp-main-description": "
\n* [[mw:API:Main_page|Documentação]]\n* [[mw:API:FAQ|FAQ]]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api Lista de discussão]\n* [https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce Anúncios da API]\n* [https://phabricator.wikimedia.org/maniphest/query/GebfyV4uCaLd/#R Erros e solicitações]\n
\nEstado: Todas as funcionalidades mostradas nesta página deveriam estar a funcionar, mas a API ainda está em desenvolvimento ativo, e pode ser alterada a qualquer momento. Inscreva-se na [https://lists.wikimedia.org/pipermail/mediawiki-api-announce/ lista de discussão mediawiki-api-announce] para ser informado acerca das atualizações.\n\nPedidos incorretos: Quando são enviados pedidos incorretos à API, um cabeçalho de HTTP será enviado com a chave \"MediaWiki-API-Error\" e, em seguida, tanto o valor do cabeçalho quanto o código de erro retornado serão definidos com o mesmo valor. Para mais informação, consulte [[mw:API:Errors_and_warnings|API: Errors and warnings]].\n\nTestes: Para facilitar os testes de pedidos à API, consulte [[Special:ApiSandbox]].", "apihelp-main-param-action": "Qual acção a executar.", "apihelp-main-param-format": "O formato de saída.", "apihelp-main-param-origin": "Ao aceder à API usando um pedido AJAX entre domínios (CORS), coloque aqui o domínio de origem. Isto tem de ser incluído em todas as verificações prévias, e portanto tem de fazer parte da URI do pedido (e não do conteúdo do POST).\n\nPara pedidos autenticados, este valor tem de corresponder de forma exata a um dos cabeçalhos Origin, portanto tem de ser algo como https://en.wikipedia.org ou https://meta.wikimedia.org. Se este parâmetro não for igual ao cabeçalho Origin, será devolvida a resposta 403. Se este parâmetro for igual ao cabeçalho Origin e a origem for permitida (white-listed) os cabeçalhos Access-Control-Allow-Origin e Access-Control-Allow-Credentials serão preenchidos.\n\nPara pedidos não autenticados, especifique o valor *. Isto fará com que o cabeçalho Access-Control-Allow-Origin\nseja preenchido, mas Access-Control-Allow-Credentials terá o valor false e todos os dados específicos do utilizador serão restringidos.", diff --git a/includes/auth/AuthManager.php b/includes/auth/AuthManager.php index e223e16e69..eeb233e465 100644 --- a/includes/auth/AuthManager.php +++ b/includes/auth/AuthManager.php @@ -1678,7 +1678,7 @@ class AuthManager implements LoggerAwareInterface { // Ignore warnings about master connections/writes...hard to avoid here $trxProfiler = \Profiler::instance()->getTransactionProfiler(); - $trxProfiler->setSilenced( true ); + $old = $trxProfiler->setSilenced( true ); try { $status = $user->addToDatabase(); if ( !$status->isOK() ) { @@ -1704,7 +1704,7 @@ class AuthManager implements LoggerAwareInterface { return $status; } } catch ( \Exception $ex ) { - $trxProfiler->setSilenced( false ); + $trxProfiler->setSilenced( $old ); $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [ 'username' => $username, 'exception' => $ex, @@ -1743,7 +1743,7 @@ class AuthManager implements LoggerAwareInterface { $logEntry->insert(); } - $trxProfiler->setSilenced( false ); + $trxProfiler->setSilenced( $old ); if ( $login ) { $this->setSessionDataForUser( $user ); diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php index cd644cb38e..16e9a443ff 100644 --- a/includes/diff/DifferenceEngine.php +++ b/includes/diff/DifferenceEngine.php @@ -21,7 +21,7 @@ * @ingroup DifferenceEngine */ -// Deprecated, use class constant instead +/** @deprecated use class constant instead */ define( 'MW_DIFF_VERSION', '1.11a' ); /** @@ -176,7 +176,7 @@ class DifferenceEngine extends ContextSource { * * @param int $id Revision ID * - * @return mixed URL or false + * @return string|bool Link HTML or false */ public function deletedLink( $id ) { if ( $this->getUser()->isAllowed( 'deletedhistory' ) ) { diff --git a/includes/htmlform/fields/HTMLDateTimeField.php b/includes/htmlform/fields/HTMLDateTimeField.php index 66f89f9564..3390a56771 100644 --- a/includes/htmlform/fields/HTMLDateTimeField.php +++ b/includes/htmlform/fields/HTMLDateTimeField.php @@ -126,6 +126,9 @@ class HTMLDateTimeField extends HTMLTextField { protected function parseDate( $value ) { $value = trim( $value ); + if ( $value === '' ) { + return false; + } if ( $this->mType === 'date' ) { $value .= ' T00:00:00+0000'; @@ -138,7 +141,7 @@ class HTMLDateTimeField extends HTMLTextField { $date = new DateTime( $value, new DateTimeZone( 'GMT' ) ); return $date->getTimestamp(); } catch ( Exception $ex ) { - return 0; + return false; } } diff --git a/includes/installer/i18n/bg.json b/includes/installer/i18n/bg.json index 8b1ca188c2..5cdb83fc50 100644 --- a/includes/installer/i18n/bg.json +++ b/includes/installer/i18n/bg.json @@ -62,6 +62,7 @@ "config-memory-bad": "Предупреждение: memory_limit на PHP е $1.\nСтойността вероятно е твърде ниска.\nВъзможно е инсталацията да се провали!", "config-xcache": "[http://xcache.lighttpd.net/ XCache] е инсталиран", "config-apc": "[http://www.php.net/apc APC] е инсталиран", + "config-apcu": "[http://www.php.net/apc APC] е инсталиран", "config-wincache": "[http://www.iis.net/download/WinCacheForPhp WinCache] е инсталиран", "config-mod-security": "Предупреждение: [http://modsecurity.org/ mod_security]/mod_security2 е включено на вашия уеб сървър. Много от обичайните му конфигурации пораждат проблеми с МедияУики и друг софтуер, който позволява публикуване на произволно съдържание.\nАко е възможно, моля изключете го. В противен случай се обърнете към [http://modsecurity.org/documentation/ документацията на mod_security] или се свържете с поддръжката на хостинга си, ако се сблъскате със случайни грешки.", "config-diff3-bad": "GNU diff3 не беше намерен.", @@ -242,7 +243,7 @@ "config-cache-options": "Настройки за обектното кеширане:", "config-cache-help": "Обектното кеширане се използва за подобряване на скоростта на МедияУики чрез кеширане на често използваните данни.\nСилно препоръчително е на средните и големите сайтове да включат тази настройка, но малките също могат да се възползват от нея.", "config-cache-none": "Без кеширане (не се премахва от функционалността, но това влияе на скоростта на по-големи уикита)", - "config-cache-accel": "PHP обектно кеширане (APC, XCache или WinCache)", + "config-cache-accel": "PHP обектно кеширане (APCu, XCache или WinCache)", "config-cache-memcached": "Използване на Memcached (изисква допълнителни настройки и конфигуриране)", "config-memcached-servers": "Memcached сървъри:", "config-memcached-help": "Списък с IP адреси за използване за Memcached.\nНеобходимо е да бъдат разделени по един на ред, както и да е посочен порта. Пример:\n127.0.0.1:11211\n192.168.1.25:1234", @@ -299,6 +300,7 @@ "config-install-done-path": "Поздравления!\nИнсталирането на МедияУики приключи успешно.\n\nИнсталаторът създаде файл LocalSettings.php.\nТой съдържа всички ваши настройки.\n\nНеобходимо е той да бъде изтеглен и поставен в $4. Изтеглянето би трябвало да започне автоматично.\n\nАко изтеглянето не започне автоматично или е било прекратено, файлът може да бъде изтеглен чрез щракване на препратката по-долу:\n\n$3\n\nЗабележка: Ако това не бъде направено сега, генерираният конфигурационен файл няма да е достъпен на по-късен етап ако не бъде изтеглен сега или инсталацията приключи без изтеглянето му.\n\nКогато файлът вече е в основната директория, [$2 уикито ще е достъпно на този адрес].", "config-download-localsettings": "Изтегляне на LocalSettings.php", "config-help": "помощ", + "config-help-tooltip": "Щракнете за разширяване", "config-nofile": "Файлът „$1“ не може да бъде открит. Да не е бил изтрит?", "config-extension-link": "Знаете ли, че това уики поддържа [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Extensions разширения]?\n\nМожете да разгледате [https://www.mediawiki.org/wiki/Special:MyLanguage/Category:Extensions_by_category разширенията по категория] или [https://www.mediawiki.org/wiki/Extension_Matrix Матрицата на разширенията] за пълен списък на разширенията.", "mainpagetext": "МедияУики беше успешно инсталирано.", diff --git a/includes/libs/IEContentAnalyzer.php b/includes/libs/IEContentAnalyzer.php deleted file mode 100644 index 0d1e527ba6..0000000000 --- a/includes/libs/IEContentAnalyzer.php +++ /dev/null @@ -1,851 +0,0 @@ - [ - 'text/plain', - 'application/octet-stream', - 'application/x-netcdf', // [sic] - ], - 'text' /*3*/ => [ - 'text/richtext', 'image/x-bitmap', 'application/postscript', 'application/base64', - 'application/macbinhex40', 'application/x-cdf', 'text/scriptlet' - ], - 'binary' /*4*/ => [ - 'application/pdf', 'audio/x-aiff', 'audio/basic', 'audio/wav', 'image/gif', - 'image/pjpeg', 'image/jpeg', 'image/tiff', 'image/x-png', 'image/png', 'image/bmp', - 'image/x-jg', 'image/x-art', 'image/x-emf', 'image/x-wmf', 'video/avi', - 'video/x-msvideo', 'video/mpeg', 'application/x-compressed', - 'application/x-zip-compressed', 'application/x-gzip-compressed', 'application/java', - 'application/x-msdownload' - ], - 'html' /*5*/ => [ 'text/html' ], - ]; - - /** - * Changes to the type table in later versions of IE - */ - protected $addedTypes = [ - 'ie07' => [ - 'text' => [ 'text/xml', 'application/xml' ] - ], - ]; - - /** - * An approximation of the "Content Type" values in HKEY_CLASSES_ROOT in a - * typical Windows installation. - * - * Used for extension to MIME type mapping if detection fails. - */ - protected $registry = [ - '.323' => 'text/h323', - '.3g2' => 'video/3gpp2', - '.3gp' => 'video/3gpp', - '.3gp2' => 'video/3gpp2', - '.3gpp' => 'video/3gpp', - '.aac' => 'audio/aac', - '.ac3' => 'audio/ac3', - '.accda' => 'application/msaccess', - '.accdb' => 'application/msaccess', - '.accdc' => 'application/msaccess', - '.accde' => 'application/msaccess', - '.accdr' => 'application/msaccess', - '.accdt' => 'application/msaccess', - '.ade' => 'application/msaccess', - '.adp' => 'application/msaccess', - '.adts' => 'audio/aac', - '.ai' => 'application/postscript', - '.aif' => 'audio/aiff', - '.aifc' => 'audio/aiff', - '.aiff' => 'audio/aiff', - '.amc' => 'application/x-mpeg', - '.application' => 'application/x-ms-application', - '.asf' => 'video/x-ms-asf', - '.asx' => 'video/x-ms-asf', - '.au' => 'audio/basic', - '.avi' => 'video/avi', - '.bmp' => 'image/bmp', - '.caf' => 'audio/x-caf', - '.cat' => 'application/vnd.ms-pki.seccat', - '.cbo' => 'application/sha', - '.cdda' => 'audio/aiff', - '.cer' => 'application/x-x509-ca-cert', - '.conf' => 'text/plain', - '.crl' => 'application/pkix-crl', - '.crt' => 'application/x-x509-ca-cert', - '.css' => 'text/css', - '.csv' => 'application/vnd.ms-excel', - '.der' => 'application/x-x509-ca-cert', - '.dib' => 'image/bmp', - '.dif' => 'video/x-dv', - '.dll' => 'application/x-msdownload', - '.doc' => 'application/msword', - '.docm' => 'application/vnd.ms-word.document.macroEnabled.12', - '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - '.dot' => 'application/msword', - '.dotm' => 'application/vnd.ms-word.template.macroEnabled.12', - '.dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - '.dv' => 'video/x-dv', - '.dwfx' => 'model/vnd.dwfx+xps', - '.edn' => 'application/vnd.adobe.edn', - '.eml' => 'message/rfc822', - '.eps' => 'application/postscript', - '.etd' => 'application/x-ebx', - '.exe' => 'application/x-msdownload', - '.fdf' => 'application/vnd.fdf', - '.fif' => 'application/fractals', - '.gif' => 'image/gif', - '.gsm' => 'audio/x-gsm', - '.hqx' => 'application/mac-binhex40', - '.hta' => 'application/hta', - '.htc' => 'text/x-component', - '.htm' => 'text/html', - '.html' => 'text/html', - '.htt' => 'text/webviewhtml', - '.hxa' => 'application/xml', - '.hxc' => 'application/xml', - '.hxd' => 'application/octet-stream', - '.hxe' => 'application/xml', - '.hxf' => 'application/xml', - '.hxh' => 'application/octet-stream', - '.hxi' => 'application/octet-stream', - '.hxk' => 'application/xml', - '.hxq' => 'application/octet-stream', - '.hxr' => 'application/octet-stream', - '.hxs' => 'application/octet-stream', - '.hxt' => 'application/xml', - '.hxv' => 'application/xml', - '.hxw' => 'application/octet-stream', - '.ico' => 'image/x-icon', - '.iii' => 'application/x-iphone', - '.ins' => 'application/x-internet-signup', - '.iqy' => 'text/x-ms-iqy', - '.isp' => 'application/x-internet-signup', - '.jfif' => 'image/jpeg', - '.jnlp' => 'application/x-java-jnlp-file', - '.jpe' => 'image/jpeg', - '.jpeg' => 'image/jpeg', - '.jpg' => 'image/jpeg', - '.jtx' => 'application/x-jtx+xps', - '.latex' => 'application/x-latex', - '.log' => 'text/plain', - '.m1v' => 'video/mpeg', - '.m2v' => 'video/mpeg', - '.m3u' => 'audio/x-mpegurl', - '.mac' => 'image/x-macpaint', - '.man' => 'application/x-troff-man', - '.mda' => 'application/msaccess', - '.mdb' => 'application/msaccess', - '.mde' => 'application/msaccess', - '.mfp' => 'application/x-shockwave-flash', - '.mht' => 'message/rfc822', - '.mhtml' => 'message/rfc822', - '.mid' => 'audio/mid', - '.midi' => 'audio/mid', - '.mod' => 'video/mpeg', - '.mov' => 'video/quicktime', - '.mp2' => 'video/mpeg', - '.mp2v' => 'video/mpeg', - '.mp3' => 'audio/mpeg', - '.mp4' => 'video/mp4', - '.mpa' => 'video/mpeg', - '.mpe' => 'video/mpeg', - '.mpeg' => 'video/mpeg', - '.mpf' => 'application/vnd.ms-mediapackage', - '.mpg' => 'video/mpeg', - '.mpv2' => 'video/mpeg', - '.mqv' => 'video/quicktime', - '.NMW' => 'application/nmwb', - '.nws' => 'message/rfc822', - '.odc' => 'text/x-ms-odc', - '.ols' => 'application/vnd.ms-publisher', - '.p10' => 'application/pkcs10', - '.p12' => 'application/x-pkcs12', - '.p7b' => 'application/x-pkcs7-certificates', - '.p7c' => 'application/pkcs7-mime', - '.p7m' => 'application/pkcs7-mime', - '.p7r' => 'application/x-pkcs7-certreqresp', - '.p7s' => 'application/pkcs7-signature', - '.pct' => 'image/pict', - '.pdf' => 'application/pdf', - '.pdx' => 'application/vnd.adobe.pdx', - '.pfx' => 'application/x-pkcs12', - '.pic' => 'image/pict', - '.pict' => 'image/pict', - '.pinstall' => 'application/x-picasa-detect', - '.pko' => 'application/vnd.ms-pki.pko', - '.png' => 'image/png', - '.pnt' => 'image/x-macpaint', - '.pntg' => 'image/x-macpaint', - '.pot' => 'application/vnd.ms-powerpoint', - '.potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12', - '.potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', - '.ppa' => 'application/vnd.ms-powerpoint', - '.ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', - '.pps' => 'application/vnd.ms-powerpoint', - '.ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', - '.ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', - '.ppt' => 'application/vnd.ms-powerpoint', - '.pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', - '.pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - '.prf' => 'application/pics-rules', - '.ps' => 'application/postscript', - '.pub' => 'application/vnd.ms-publisher', - '.pwz' => 'application/vnd.ms-powerpoint', - '.py' => 'text/plain', - '.pyw' => 'text/plain', - '.qht' => 'text/x-html-insertion', - '.qhtm' => 'text/x-html-insertion', - '.qt' => 'video/quicktime', - '.qti' => 'image/x-quicktime', - '.qtif' => 'image/x-quicktime', - '.qtl' => 'application/x-quicktimeplayer', - '.rat' => 'application/rat-file', - '.rmf' => 'application/vnd.adobe.rmf', - '.rmi' => 'audio/mid', - '.rqy' => 'text/x-ms-rqy', - '.rtf' => 'application/msword', - '.sct' => 'text/scriptlet', - '.sd2' => 'audio/x-sd2', - '.sdp' => 'application/sdp', - '.shtml' => 'text/html', - '.sit' => 'application/x-stuffit', - '.sldm' => 'application/vnd.ms-powerpoint.slide.macroEnabled.12', - '.sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', - '.slk' => 'application/vnd.ms-excel', - '.snd' => 'audio/basic', - '.so' => 'application/x-apachemodule', - '.sol' => 'text/plain', - '.sor' => 'text/plain', - '.spc' => 'application/x-pkcs7-certificates', - '.spl' => 'application/futuresplash', - '.sst' => 'application/vnd.ms-pki.certstore', - '.stl' => 'application/vnd.ms-pki.stl', - '.swf' => 'application/x-shockwave-flash', - '.thmx' => 'application/vnd.ms-officetheme', - '.tif' => 'image/tiff', - '.tiff' => 'image/tiff', - '.txt' => 'text/plain', - '.uls' => 'text/iuls', - '.vcf' => 'text/x-vcard', - '.vdx' => 'application/vnd.ms-visio.viewer', - '.vsd' => 'application/vnd.ms-visio.viewer', - '.vss' => 'application/vnd.ms-visio.viewer', - '.vst' => 'application/vnd.ms-visio.viewer', - '.vsx' => 'application/vnd.ms-visio.viewer', - '.vtx' => 'application/vnd.ms-visio.viewer', - '.wav' => 'audio/wav', - '.wax' => 'audio/x-ms-wax', - '.wbk' => 'application/msword', - '.wdp' => 'image/vnd.ms-photo', - '.wiz' => 'application/msword', - '.wm' => 'video/x-ms-wm', - '.wma' => 'audio/x-ms-wma', - '.wmd' => 'application/x-ms-wmd', - '.wmv' => 'video/x-ms-wmv', - '.wmx' => 'video/x-ms-wmx', - '.wmz' => 'application/x-ms-wmz', - '.wpl' => 'application/vnd.ms-wpl', - '.wsc' => 'text/scriptlet', - '.wvx' => 'video/x-ms-wvx', - '.xaml' => 'application/xaml+xml', - '.xbap' => 'application/x-ms-xbap', - '.xdp' => 'application/vnd.adobe.xdp+xml', - '.xfdf' => 'application/vnd.adobe.xfdf', - '.xht' => 'application/xhtml+xml', - '.xhtml' => 'application/xhtml+xml', - '.xla' => 'application/vnd.ms-excel', - '.xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', - '.xlk' => 'application/vnd.ms-excel', - '.xll' => 'application/vnd.ms-excel', - '.xlm' => 'application/vnd.ms-excel', - '.xls' => 'application/vnd.ms-excel', - '.xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', - '.xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', - '.xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - '.xlt' => 'application/vnd.ms-excel', - '.xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', - '.xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - '.xlw' => 'application/vnd.ms-excel', - '.xml' => 'text/xml', - '.xps' => 'application/vnd.ms-xpsdocument', - '.xsl' => 'text/xml', - ]; - - /** - * IE versions which have been analysed to bring you this class, and for - * which some substantive difference exists. These will appear as keys - * in the return value of getRealMimesFromData(). The names are chosen to sort correctly. - */ - protected $versions = [ 'ie05', 'ie06', 'ie07', 'ie07.strict', 'ie07.nohtml' ]; - - /** - * Type table with versions expanded - */ - protected $typeTable = []; - - /** constructor */ - function __construct() { - // Construct versioned type arrays from the base type array plus additions - $types = $this->baseTypeTable; - foreach ( $this->versions as $version ) { - if ( isset( $this->addedTypes[$version] ) ) { - foreach ( $this->addedTypes[$version] as $format => $addedTypes ) { - $types[$format] = array_merge( $types[$format], $addedTypes ); - } - } - $this->typeTable[$version] = $types; - } - } - - /** - * Get the MIME types from getMimesFromData(), but convert the result from IE's - * idiosyncratic private types into something other apps will understand. - * - * @param string $fileName the file name (unused at present) - * @param string $chunk the first 256 bytes of the file - * @param string $proposed the MIME type proposed by the server - * - * @return Array: map of IE version to detected MIME type - */ - public function getRealMimesFromData( $fileName, $chunk, $proposed ) { - $types = $this->getMimesFromData( $fileName, $chunk, $proposed ); - $types = array_map( [ $this, 'translateMimeType' ], $types ); - return $types; - } - - /** - * Translate a MIME type from IE's idiosyncratic private types into - * more commonly understood type strings - * @param $type - * @return string - */ - public function translateMimeType( $type ) { - static $table = [ - 'image/pjpeg' => 'image/jpeg', - 'image/x-png' => 'image/png', - 'image/x-wmf' => 'application/x-msmetafile', - 'image/bmp' => 'image/x-bmp', - 'application/x-zip-compressed' => 'application/zip', - 'application/x-compressed' => 'application/x-compress', - 'application/x-gzip-compressed' => 'application/x-gzip', - 'audio/mid' => 'audio/midi', - ]; - if ( isset( $table[$type] ) ) { - $type = $table[$type]; - } - return $type; - } - - /** - * Get the untranslated MIME types for all known versions - * - * @param string $fileName the file name (unused at present) - * @param string $chunk the first 256 bytes of the file - * @param string $proposed the MIME type proposed by the server - * - * @return Array: map of IE version to detected MIME type - */ - public function getMimesFromData( $fileName, $chunk, $proposed ) { - $types = []; - foreach ( $this->versions as $version ) { - $types[$version] = $this->getMimeTypeForVersion( $version, $fileName, $chunk, $proposed ); - } - return $types; - } - - /** - * Get the MIME type for a given named version - * @param $version - * @param $fileName - * @param $chunk - * @param $proposed - * @return bool|string - */ - protected function getMimeTypeForVersion( $version, $fileName, $chunk, $proposed ) { - // Strip text after a semicolon - $semiPos = strpos( $proposed, ';' ); - if ( $semiPos !== false ) { - $proposed = substr( $proposed, 0, $semiPos ); - } - - $proposedFormat = $this->getDataFormat( $version, $proposed ); - if ( $proposedFormat == 'unknown' - && $proposed != 'multipart/mixed' - && $proposed != 'multipart/x-mixed-replace' ) - { - return $proposed; - } - if ( strval( $chunk ) === '' ) { - return $proposed; - } - - // Truncate chunk at 255 bytes - $chunk = substr( $chunk, 0, 255 ); - - // IE does the Check*Headers() calls last, and instead does the following image - // type checks by directly looking for the magic numbers. What I do here should - // have the same effect since the magic number checks are identical in both cases. - $result = $this->sampleData( $version, $chunk ); - $sampleFound = $result['found']; - $counters = $result['counters']; - $binaryType = $this->checkBinaryHeaders( $version, $chunk ); - $textType = $this->checkTextHeaders( $version, $chunk ); - - if ( $proposed == 'text/html' && isset( $sampleFound['html'] ) ) { - return 'text/html'; - } - if ( $proposed == 'image/gif' && $binaryType == 'image/gif' ) { - return 'image/gif'; - } - if ( ( $proposed == 'image/pjpeg' || $proposed == 'image/jpeg' ) - && $binaryType == 'image/pjpeg' ) - { - return $proposed; - } - // PNG check added in IE 7 - if ( $version >= 'ie07' - && ( $proposed == 'image/x-png' || $proposed == 'image/png' ) - && $binaryType == 'image/x-png' ) - { - return $proposed; - } - - // CDF was removed in IE 7 so it won't be in $sampleFound for later versions - if ( isset( $sampleFound['cdf'] ) ) { - return 'application/x-cdf'; - } - - // RSS and Atom were added in IE 7 so they won't be in $sampleFound for - // previous versions - if ( isset( $sampleFound['rss'] ) ) { - return 'application/rss+xml'; - } - if ( isset( $sampleFound['rdf-tag'] ) - && isset( $sampleFound['rdf-url'] ) - && isset( $sampleFound['rdf-purl'] ) ) - { - return 'application/rss+xml'; - } - if ( isset( $sampleFound['atom'] ) ) { - return 'application/atom+xml'; - } - - if ( isset( $sampleFound['xml'] ) ) { - // TODO: I'm not sure under what circumstances this flag is enabled - if ( strpos( $version, 'strict' ) !== false ) { - if ( $proposed == 'text/html' || $proposed == 'text/xml' ) { - return 'text/xml'; - } - } else { - return 'text/xml'; - } - } - if ( isset( $sampleFound['html'] ) ) { - // TODO: I'm not sure under what circumstances this flag is enabled - if ( strpos( $version, 'nohtml' ) !== false ) { - if ( $proposed == 'text/plain' ) { - return 'text/html'; - } - } else { - return 'text/html'; - } - } - if ( isset( $sampleFound['xbm'] ) ) { - return 'image/x-bitmap'; - } - if ( isset( $sampleFound['binhex'] ) ) { - return 'application/macbinhex40'; - } - if ( isset( $sampleFound['scriptlet'] ) ) { - if ( strpos( $version, 'strict' ) !== false ) { - if ( $proposed == 'text/plain' || $proposed == 'text/scriptlet' ) { - return 'text/scriptlet'; - } - } else { - return 'text/scriptlet'; - } - } - - // Freaky heuristics to determine if the data is text or binary - // The heuristic is of course broken for non-ASCII text - if ( $counters['ctrl'] != 0 && ( $counters['ff'] + $counters['low'] ) - < ( $counters['ctrl'] + $counters['high'] ) * 16 ) - { - $kindOfBinary = true; - $type = $binaryType ? $binaryType : $textType; - if ( $type === false ) { - $type = 'application/octet-stream'; - } - } else { - $kindOfBinary = false; - $type = $textType ? $textType : $binaryType; - if ( $type === false ) { - $type = 'text/plain'; - } - } - - // Check if the output format is ambiguous - // This generally means that detection failed, real types aren't ambiguous - $detectedFormat = $this->getDataFormat( $version, $type ); - if ( $detectedFormat != 'ambiguous' ) { - return $type; - } - - if ( $proposedFormat != 'ambiguous' ) { - // FormatAgreesWithData() - if ( $proposedFormat == 'text' && !$kindOfBinary ) { - return $proposed; - } - if ( $proposedFormat == 'binary' && $kindOfBinary ) { - return $proposed; - } - if ( $proposedFormat == 'html' ) { - return $proposed; - } - } - - // Find a MIME type by searching the registry for the file extension. - $dotPos = strrpos( $fileName, '.' ); - if ( $dotPos === false ) { - return $type; - } - $ext = substr( $fileName, $dotPos ); - if ( isset( $this->registry[$ext] ) ) { - return $this->registry[$ext]; - } - - // TODO: If the extension has an application registered to it, IE will return - // application/octet-stream. We'll skip that, so we could erroneously - // return text/plain or application/x-netcdf where application/octet-stream - // would be correct. - - return $type; - } - - /** - * Check for text headers at the start of the chunk - * Confirmed same in 5 and 7. - * @param $version - * @param $chunk - * @return bool|string - */ - private function checkTextHeaders( $version, $chunk ) { - $chunk2 = substr( $chunk, 0, 2 ); - $chunk4 = substr( $chunk, 0, 4 ); - $chunk5 = substr( $chunk, 0, 5 ); - if ( $chunk4 == '%PDF' ) { - return 'application/pdf'; - } - if ( $chunk2 == '%!' ) { - return 'application/postscript'; - } - if ( $chunk5 == '{\\rtf' ) { - return 'text/richtext'; - } - if ( $chunk5 == 'begin' ) { - return 'application/base64'; - } - return false; - } - - /** - * Check for binary headers at the start of the chunk - * Confirmed same in 5 and 7. - * @param $version - * @param $chunk - * @return bool|string - */ - private function checkBinaryHeaders( $version, $chunk ) { - $chunk2 = substr( $chunk, 0, 2 ); - $chunk3 = substr( $chunk, 0, 3 ); - $chunk4 = substr( $chunk, 0, 4 ); - $chunk5 = substr( $chunk, 0, 5 ); - $chunk5uc = strtoupper( $chunk5 ); - $chunk8 = substr( $chunk, 0, 8 ); - if ( $chunk5uc == 'GIF87' || $chunk5uc == 'GIF89' ) { - return 'image/gif'; - } - if ( $chunk2 == "\xff\xd8" ) { - return 'image/pjpeg'; // actually plain JPEG but this is what IE returns - } - - if ( $chunk2 == 'BM' - && substr( $chunk, 6, 2 ) == "\000\000" - && substr( $chunk, 8, 2 ) == "\000\000" ) - { - return 'image/bmp'; // another non-standard MIME - } - if ( $chunk4 == 'RIFF' - && substr( $chunk, 8, 4 ) == 'WAVE' ) - { - return 'audio/wav'; - } - // These were integer literals in IE - // Perhaps the author was not sure what the target endianness was - if ( $chunk4 == ".sd\000" - || $chunk4 == ".snd" - || $chunk4 == "\000ds." - || $chunk4 == "dns." ) - { - return 'audio/basic'; - } - if ( $chunk3 == "MM\000" ) { - return 'image/tiff'; - } - if ( $chunk2 == 'MZ' ) { - return 'application/x-msdownload'; - } - if ( $chunk8 == "\x89PNG\x0d\x0a\x1a\x0a" ) { - return 'image/x-png'; // [sic] - } - if ( strlen( $chunk ) >= 5 ) { - $byte2 = ord( $chunk[2] ); - $byte4 = ord( $chunk[4] ); - if ( $byte2 >= 3 && $byte2 <= 31 && $byte4 == 0 && $chunk2 == 'JG' ) { - return 'image/x-jg'; - } - } - // More endian confusion? - if ( $chunk4 == 'MROF' ) { - return 'audio/x-aiff'; - } - $chunk4_8 = substr( $chunk, 8, 4 ); - if ( $chunk4 == 'FORM' && ( $chunk4_8 == 'AIFF' || $chunk4_8 == 'AIFC' ) ) { - return 'audio/x-aiff'; - } - if ( $chunk4 == 'RIFF' && $chunk4_8 == 'AVI ' ) { - return 'video/avi'; - } - if ( $chunk4 == "\x00\x00\x01\xb3" || $chunk4 == "\x00\x00\x01\xba" ) { - return 'video/mpeg'; - } - if ( $chunk4 == "\001\000\000\000" - && substr( $chunk, 40, 4 ) == ' EMF' ) - { - return 'image/x-emf'; - } - if ( $chunk4 == "\xd7\xcd\xc6\x9a" ) { - return 'image/x-wmf'; - } - if ( $chunk4 == "\xca\xfe\xba\xbe" ) { - return 'application/java'; - } - if ( $chunk2 == 'PK' ) { - return 'application/x-zip-compressed'; - } - if ( $chunk2 == "\x1f\x9d" ) { - return 'application/x-compressed'; - } - if ( $chunk2 == "\x1f\x8b" ) { - return 'application/x-gzip-compressed'; - } - // Skip redundant check for ZIP - if ( $chunk5 == "MThd\000" ) { - return 'audio/mid'; - } - if ( $chunk4 == '%PDF' ) { - return 'application/pdf'; - } - return false; - } - - /** - * Do heuristic checks on the bulk of the data sample. - * Search for HTML tags. - * @param $version - * @param $chunk - * @return array - */ - protected function sampleData( $version, $chunk ) { - $found = []; - $counters = [ - 'ctrl' => 0, - 'high' => 0, - 'low' => 0, - 'lf' => 0, - 'cr' => 0, - 'ff' => 0 - ]; - $htmlTags = [ - 'html', - 'head', - 'title', - 'body', - 'script', - 'a href', - 'pre', - 'img', - 'plaintext', - 'table' - ]; - $rdfUrl = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; - $rdfPurl = 'http://purl.org/rss/1.0/'; - $xbmMagic1 = '#define'; - $xbmMagic2 = '_width'; - $xbmMagic3 = '_bits'; - $binhexMagic = 'converted with BinHex'; - $chunkLength = strlen( $chunk ); - - for ( $offset = 0; $offset < $chunkLength; $offset++ ) { - $curChar = $chunk[$offset]; - if ( $curChar == "\x0a" ) { - $counters['lf']++; - continue; - } elseif ( $curChar == "\x0d" ) { - $counters['cr']++; - continue; - } elseif ( $curChar == "\x0c" ) { - $counters['ff']++; - continue; - } elseif ( $curChar == "\t" ) { - $counters['low']++; - continue; - } elseif ( ord( $curChar ) < 32 ) { - $counters['ctrl']++; - continue; - } elseif ( ord( $curChar ) >= 128 ) { - $counters['high']++; - continue; - } - - $counters['low']++; - if ( $curChar == '<' ) { - // XML - $remainder = substr( $chunk, $offset + 1 ); - if ( !strncasecmp( $remainder, '?XML', 4 ) ) { - $nextChar = substr( $chunk, $offset + 5, 1 ); - if ( $nextChar == ':' || $nextChar == ' ' || $nextChar == "\t" ) { - $found['xml'] = true; - } - } - // Scriptlet (JSP) - if ( !strncasecmp( $remainder, 'SCRIPTLET', 9 ) ) { - $found['scriptlet'] = true; - break; - } - // HTML - foreach ( $htmlTags as $tag ) { - if ( !strncasecmp( $remainder, $tag, strlen( $tag ) ) ) { - $found['html'] = true; - } - } - // Skip broken check for additional tags (HR etc.) - - // CHANNEL replaced by RSS, RDF and FEED in IE 7 - if ( $version < 'ie07' ) { - if ( !strncasecmp( $remainder, 'CHANNEL', 7 ) ) { - $found['cdf'] = true; - } - } else { - // RSS - if ( !strncasecmp( $remainder, 'RSS', 3 ) ) { - $found['rss'] = true; - break; // return from SampleData - } - if ( !strncasecmp( $remainder, 'rdf:RDF', 7 ) ) { - $found['rdf-tag'] = true; - // no break - } - if ( !strncasecmp( $remainder, 'FEED', 4 ) ) { - $found['atom'] = true; - break; - } - } - continue; - } - // Skip broken check for --> - - // RSS URL checks - // For some reason both URLs must appear before it is recognised - $remainder = substr( $chunk, $offset ); - if ( !strncasecmp( $remainder, $rdfUrl, strlen( $rdfUrl ) ) ) { - $found['rdf-url'] = true; - if ( isset( $found['rdf-tag'] ) - && isset( $found['rdf-purl'] ) ) // [sic] - { - break; - } - continue; - } - - if ( !strncasecmp( $remainder, $rdfPurl, strlen( $rdfPurl ) ) ) { - if ( isset( $found['rdf-tag'] ) - && isset( $found['rdf-url'] ) ) // [sic] - { - break; - } - continue; - } - - // XBM checks - if ( !strncasecmp( $remainder, $xbmMagic1, strlen( $xbmMagic1 ) ) ) { - $found['xbm1'] = true; - continue; - } - if ( $curChar == '_' ) { - if ( isset( $found['xbm2'] ) ) { - if ( !strncasecmp( $remainder, $xbmMagic3, strlen( $xbmMagic3 ) ) ) { - $found['xbm'] = true; - break; - } - } elseif ( isset( $found['xbm1'] ) ) { - if ( !strncasecmp( $remainder, $xbmMagic2, strlen( $xbmMagic2 ) ) ) { - $found['xbm2'] = true; - } - } - } - - // BinHex - if ( !strncmp( $remainder, $binhexMagic, strlen( $binhexMagic ) ) ) { - $found['binhex'] = true; - } - } - return [ 'found' => $found, 'counters' => $counters ]; - } - - /** - * @param $version - * @param $type - * @return int|string - */ - protected function getDataFormat( $version, $type ) { - $types = $this->typeTable[$version]; - if ( $type == '(null)' || strval( $type ) === '' ) { - return 'ambiguous'; - } - foreach ( $types as $format => $list ) { - if ( in_array( $type, $list ) ) { - return $format; - } - } - return 'unknown'; - } -} diff --git a/includes/libs/XmlTypeCheck.php b/includes/libs/XmlTypeCheck.php deleted file mode 100644 index f057140a6b..0000000000 --- a/includes/libs/XmlTypeCheck.php +++ /dev/null @@ -1,347 +0,0 @@ - '', - ]; - - /** - * @param string $input a filename or string containing the XML element - * @param callable $filterCallback (optional) - * Function to call to do additional custom validity checks from the - * SAX element handler event. This gives you access to the element - * namespace, name, attributes, and text contents. - * Filter should return 'true' to toggle on $this->filterMatch - * @param bool $isFile (optional) indicates if the first parameter is a - * filename (default, true) or if it is a string (false) - * @param array $options list of additional parsing options: - * processing_instruction_handler: Callback for xml_set_processing_instruction_handler - */ - function __construct( $input, $filterCallback = null, $isFile = true, $options = [] ) { - $this->filterCallback = $filterCallback; - $this->parserOptions = array_merge( $this->parserOptions, $options ); - $this->validateFromInput( $input, $isFile ); - } - - /** - * Alternative constructor: from filename - * - * @param string $fname the filename of an XML document - * @param callable $filterCallback (optional) - * Function to call to do additional custom validity checks from the - * SAX element handler event. This gives you access to the element - * namespace, name, and attributes, but not to text contents. - * Filter should return 'true' to toggle on $this->filterMatch - * @return XmlTypeCheck - */ - public static function newFromFilename( $fname, $filterCallback = null ) { - return new self( $fname, $filterCallback, true ); - } - - /** - * Alternative constructor: from string - * - * @param string $string a string containing an XML element - * @param callable $filterCallback (optional) - * Function to call to do additional custom validity checks from the - * SAX element handler event. This gives you access to the element - * namespace, name, and attributes, but not to text contents. - * Filter should return 'true' to toggle on $this->filterMatch - * @return XmlTypeCheck - */ - public static function newFromString( $string, $filterCallback = null ) { - return new self( $string, $filterCallback, false ); - } - - /** - * Get the root element. Simple accessor to $rootElement - * - * @return string - */ - public function getRootElement() { - return $this->rootElement; - } - - /** - * @param string $fname the filename - */ - private function validateFromInput( $xml, $isFile ) { - $reader = new XMLReader(); - if ( $isFile ) { - $s = $reader->open( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING ); - } else { - $s = $reader->XML( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING ); - } - if ( $s !== true ) { - // Couldn't open the XML - $this->wellFormed = false; - } else { - $oldDisable = libxml_disable_entity_loader( true ); - $reader->setParserProperty( XMLReader::SUBST_ENTITIES, true ); - try { - $this->validate( $reader ); - } catch ( Exception $e ) { - // Calling this malformed, because we didn't parse the whole - // thing. Maybe just an external entity refernce. - $this->wellFormed = false; - $reader->close(); - libxml_disable_entity_loader( $oldDisable ); - throw $e; - } - $reader->close(); - libxml_disable_entity_loader( $oldDisable ); - } - } - - private function readNext( XMLReader $reader ) { - set_error_handler( [ $this, 'XmlErrorHandler' ] ); - $ret = $reader->read(); - restore_error_handler(); - return $ret; - } - - public function XmlErrorHandler( $errno, $errstr ) { - $this->wellFormed = false; - } - - private function validate( $reader ) { - - // First, move through anything that isn't an element, and - // handle any processing instructions with the callback - do { - if ( !$this->readNext( $reader ) ) { - // Hit the end of the document before any elements - $this->wellFormed = false; - return; - } - if ( $reader->nodeType === XMLReader::PI ) { - $this->processingInstructionHandler( $reader->name, $reader->value ); - } - } while ( $reader->nodeType != XMLReader::ELEMENT ); - - // Process the rest of the document - do { - switch ( $reader->nodeType ) { - case XMLReader::ELEMENT: - $name = $this->expandNS( - $reader->name, - $reader->namespaceURI - ); - if ( $this->rootElement === '' ) { - $this->rootElement = $name; - } - $empty = $reader->isEmptyElement; - $attrs = $this->getAttributesArray( $reader ); - $this->elementOpen( $name, $attrs ); - if ( $empty ) { - $this->elementClose(); - } - break; - - case XMLReader::END_ELEMENT: - $this->elementClose(); - break; - - case XMLReader::WHITESPACE: - case XMLReader::SIGNIFICANT_WHITESPACE: - case XMLReader::CDATA: - case XMLReader::TEXT: - $this->elementData( $reader->value ); - break; - - case XMLReader::ENTITY_REF: - // Unexpanded entity (maybe external?), - // don't send to the filter (xml_parse didn't) - break; - - case XMLReader::COMMENT: - // Don't send to the filter (xml_parse didn't) - break; - - case XMLReader::PI: - // Processing instructions can happen after the header too - $this->processingInstructionHandler( - $reader->name, - $reader->value - ); - break; - default: - // One of DOC, DOC_TYPE, ENTITY, END_ENTITY, - // NOTATION, or XML_DECLARATION - // xml_parse didn't send these to the filter, so we won't. - } - - } while ( $this->readNext( $reader ) ); - - if ( $this->stackDepth !== 0 ) { - $this->wellFormed = false; - } elseif ( $this->wellFormed === null ) { - $this->wellFormed = true; - } - - } - - /** - * Get all of the attributes for an XMLReader's current node - * @param $r XMLReader - * @return array of attributes - */ - private function getAttributesArray( XMLReader $r ) { - $attrs = []; - while ( $r->moveToNextAttribute() ) { - if ( $r->namespaceURI === 'http://www.w3.org/2000/xmlns/' ) { - // XMLReader treats xmlns attributes as normal - // attributes, while xml_parse doesn't - continue; - } - $name = $this->expandNS( $r->name, $r->namespaceURI ); - $attrs[$name] = $r->value; - } - return $attrs; - } - - /** - * @param $name element or attribute name, maybe with a full or short prefix - * @param $namespaceURI the namespaceURI - * @return string the name prefixed with namespaceURI - */ - private function expandNS( $name, $namespaceURI ) { - if ( $namespaceURI ) { - $parts = explode( ':', $name ); - $localname = array_pop( $parts ); - return "$namespaceURI:$localname"; - } - return $name; - } - - /** - * @param $name - * @param $attribs - */ - private function elementOpen( $name, $attribs ) { - $this->elementDataContext[] = [ $name, $attribs ]; - $this->elementData[] = ''; - $this->stackDepth++; - } - - /** - */ - private function elementClose() { - list( $name, $attribs ) = array_pop( $this->elementDataContext ); - $data = array_pop( $this->elementData ); - $this->stackDepth--; - $callbackReturn = false; - - if ( is_callable( $this->filterCallback ) ) { - $callbackReturn = call_user_func( - $this->filterCallback, - $name, - $attribs, - $data - ); - } - if ( $callbackReturn ) { - // Filter hit! - $this->filterMatch = true; - $this->filterMatchType = $callbackReturn; - } - } - - /** - * @param $data - */ - private function elementData( $data ) { - // Collect any data here, and we'll run the callback in elementClose - $this->elementData[ $this->stackDepth - 1 ] .= trim( $data ); - } - - /** - * @param $target - * @param $data - */ - private function processingInstructionHandler( $target, $data ) { - $callbackReturn = false; - if ( $this->parserOptions['processing_instruction_handler'] ) { - $callbackReturn = call_user_func( - $this->parserOptions['processing_instruction_handler'], - $target, - $data - ); - } - if ( $callbackReturn ) { - // Filter hit! - $this->filterMatch = true; - $this->filterMatchType = $callbackReturn; - } - } -} diff --git a/includes/libs/mime/IEContentAnalyzer.php b/includes/libs/mime/IEContentAnalyzer.php new file mode 100644 index 0000000000..0d1e527ba6 --- /dev/null +++ b/includes/libs/mime/IEContentAnalyzer.php @@ -0,0 +1,851 @@ + [ + 'text/plain', + 'application/octet-stream', + 'application/x-netcdf', // [sic] + ], + 'text' /*3*/ => [ + 'text/richtext', 'image/x-bitmap', 'application/postscript', 'application/base64', + 'application/macbinhex40', 'application/x-cdf', 'text/scriptlet' + ], + 'binary' /*4*/ => [ + 'application/pdf', 'audio/x-aiff', 'audio/basic', 'audio/wav', 'image/gif', + 'image/pjpeg', 'image/jpeg', 'image/tiff', 'image/x-png', 'image/png', 'image/bmp', + 'image/x-jg', 'image/x-art', 'image/x-emf', 'image/x-wmf', 'video/avi', + 'video/x-msvideo', 'video/mpeg', 'application/x-compressed', + 'application/x-zip-compressed', 'application/x-gzip-compressed', 'application/java', + 'application/x-msdownload' + ], + 'html' /*5*/ => [ 'text/html' ], + ]; + + /** + * Changes to the type table in later versions of IE + */ + protected $addedTypes = [ + 'ie07' => [ + 'text' => [ 'text/xml', 'application/xml' ] + ], + ]; + + /** + * An approximation of the "Content Type" values in HKEY_CLASSES_ROOT in a + * typical Windows installation. + * + * Used for extension to MIME type mapping if detection fails. + */ + protected $registry = [ + '.323' => 'text/h323', + '.3g2' => 'video/3gpp2', + '.3gp' => 'video/3gpp', + '.3gp2' => 'video/3gpp2', + '.3gpp' => 'video/3gpp', + '.aac' => 'audio/aac', + '.ac3' => 'audio/ac3', + '.accda' => 'application/msaccess', + '.accdb' => 'application/msaccess', + '.accdc' => 'application/msaccess', + '.accde' => 'application/msaccess', + '.accdr' => 'application/msaccess', + '.accdt' => 'application/msaccess', + '.ade' => 'application/msaccess', + '.adp' => 'application/msaccess', + '.adts' => 'audio/aac', + '.ai' => 'application/postscript', + '.aif' => 'audio/aiff', + '.aifc' => 'audio/aiff', + '.aiff' => 'audio/aiff', + '.amc' => 'application/x-mpeg', + '.application' => 'application/x-ms-application', + '.asf' => 'video/x-ms-asf', + '.asx' => 'video/x-ms-asf', + '.au' => 'audio/basic', + '.avi' => 'video/avi', + '.bmp' => 'image/bmp', + '.caf' => 'audio/x-caf', + '.cat' => 'application/vnd.ms-pki.seccat', + '.cbo' => 'application/sha', + '.cdda' => 'audio/aiff', + '.cer' => 'application/x-x509-ca-cert', + '.conf' => 'text/plain', + '.crl' => 'application/pkix-crl', + '.crt' => 'application/x-x509-ca-cert', + '.css' => 'text/css', + '.csv' => 'application/vnd.ms-excel', + '.der' => 'application/x-x509-ca-cert', + '.dib' => 'image/bmp', + '.dif' => 'video/x-dv', + '.dll' => 'application/x-msdownload', + '.doc' => 'application/msword', + '.docm' => 'application/vnd.ms-word.document.macroEnabled.12', + '.docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.dot' => 'application/msword', + '.dotm' => 'application/vnd.ms-word.template.macroEnabled.12', + '.dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + '.dv' => 'video/x-dv', + '.dwfx' => 'model/vnd.dwfx+xps', + '.edn' => 'application/vnd.adobe.edn', + '.eml' => 'message/rfc822', + '.eps' => 'application/postscript', + '.etd' => 'application/x-ebx', + '.exe' => 'application/x-msdownload', + '.fdf' => 'application/vnd.fdf', + '.fif' => 'application/fractals', + '.gif' => 'image/gif', + '.gsm' => 'audio/x-gsm', + '.hqx' => 'application/mac-binhex40', + '.hta' => 'application/hta', + '.htc' => 'text/x-component', + '.htm' => 'text/html', + '.html' => 'text/html', + '.htt' => 'text/webviewhtml', + '.hxa' => 'application/xml', + '.hxc' => 'application/xml', + '.hxd' => 'application/octet-stream', + '.hxe' => 'application/xml', + '.hxf' => 'application/xml', + '.hxh' => 'application/octet-stream', + '.hxi' => 'application/octet-stream', + '.hxk' => 'application/xml', + '.hxq' => 'application/octet-stream', + '.hxr' => 'application/octet-stream', + '.hxs' => 'application/octet-stream', + '.hxt' => 'application/xml', + '.hxv' => 'application/xml', + '.hxw' => 'application/octet-stream', + '.ico' => 'image/x-icon', + '.iii' => 'application/x-iphone', + '.ins' => 'application/x-internet-signup', + '.iqy' => 'text/x-ms-iqy', + '.isp' => 'application/x-internet-signup', + '.jfif' => 'image/jpeg', + '.jnlp' => 'application/x-java-jnlp-file', + '.jpe' => 'image/jpeg', + '.jpeg' => 'image/jpeg', + '.jpg' => 'image/jpeg', + '.jtx' => 'application/x-jtx+xps', + '.latex' => 'application/x-latex', + '.log' => 'text/plain', + '.m1v' => 'video/mpeg', + '.m2v' => 'video/mpeg', + '.m3u' => 'audio/x-mpegurl', + '.mac' => 'image/x-macpaint', + '.man' => 'application/x-troff-man', + '.mda' => 'application/msaccess', + '.mdb' => 'application/msaccess', + '.mde' => 'application/msaccess', + '.mfp' => 'application/x-shockwave-flash', + '.mht' => 'message/rfc822', + '.mhtml' => 'message/rfc822', + '.mid' => 'audio/mid', + '.midi' => 'audio/mid', + '.mod' => 'video/mpeg', + '.mov' => 'video/quicktime', + '.mp2' => 'video/mpeg', + '.mp2v' => 'video/mpeg', + '.mp3' => 'audio/mpeg', + '.mp4' => 'video/mp4', + '.mpa' => 'video/mpeg', + '.mpe' => 'video/mpeg', + '.mpeg' => 'video/mpeg', + '.mpf' => 'application/vnd.ms-mediapackage', + '.mpg' => 'video/mpeg', + '.mpv2' => 'video/mpeg', + '.mqv' => 'video/quicktime', + '.NMW' => 'application/nmwb', + '.nws' => 'message/rfc822', + '.odc' => 'text/x-ms-odc', + '.ols' => 'application/vnd.ms-publisher', + '.p10' => 'application/pkcs10', + '.p12' => 'application/x-pkcs12', + '.p7b' => 'application/x-pkcs7-certificates', + '.p7c' => 'application/pkcs7-mime', + '.p7m' => 'application/pkcs7-mime', + '.p7r' => 'application/x-pkcs7-certreqresp', + '.p7s' => 'application/pkcs7-signature', + '.pct' => 'image/pict', + '.pdf' => 'application/pdf', + '.pdx' => 'application/vnd.adobe.pdx', + '.pfx' => 'application/x-pkcs12', + '.pic' => 'image/pict', + '.pict' => 'image/pict', + '.pinstall' => 'application/x-picasa-detect', + '.pko' => 'application/vnd.ms-pki.pko', + '.png' => 'image/png', + '.pnt' => 'image/x-macpaint', + '.pntg' => 'image/x-macpaint', + '.pot' => 'application/vnd.ms-powerpoint', + '.potm' => 'application/vnd.ms-powerpoint.template.macroEnabled.12', + '.potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + '.ppa' => 'application/vnd.ms-powerpoint', + '.ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + '.pps' => 'application/vnd.ms-powerpoint', + '.ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + '.ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + '.ppt' => 'application/vnd.ms-powerpoint', + '.pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + '.pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.prf' => 'application/pics-rules', + '.ps' => 'application/postscript', + '.pub' => 'application/vnd.ms-publisher', + '.pwz' => 'application/vnd.ms-powerpoint', + '.py' => 'text/plain', + '.pyw' => 'text/plain', + '.qht' => 'text/x-html-insertion', + '.qhtm' => 'text/x-html-insertion', + '.qt' => 'video/quicktime', + '.qti' => 'image/x-quicktime', + '.qtif' => 'image/x-quicktime', + '.qtl' => 'application/x-quicktimeplayer', + '.rat' => 'application/rat-file', + '.rmf' => 'application/vnd.adobe.rmf', + '.rmi' => 'audio/mid', + '.rqy' => 'text/x-ms-rqy', + '.rtf' => 'application/msword', + '.sct' => 'text/scriptlet', + '.sd2' => 'audio/x-sd2', + '.sdp' => 'application/sdp', + '.shtml' => 'text/html', + '.sit' => 'application/x-stuffit', + '.sldm' => 'application/vnd.ms-powerpoint.slide.macroEnabled.12', + '.sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + '.slk' => 'application/vnd.ms-excel', + '.snd' => 'audio/basic', + '.so' => 'application/x-apachemodule', + '.sol' => 'text/plain', + '.sor' => 'text/plain', + '.spc' => 'application/x-pkcs7-certificates', + '.spl' => 'application/futuresplash', + '.sst' => 'application/vnd.ms-pki.certstore', + '.stl' => 'application/vnd.ms-pki.stl', + '.swf' => 'application/x-shockwave-flash', + '.thmx' => 'application/vnd.ms-officetheme', + '.tif' => 'image/tiff', + '.tiff' => 'image/tiff', + '.txt' => 'text/plain', + '.uls' => 'text/iuls', + '.vcf' => 'text/x-vcard', + '.vdx' => 'application/vnd.ms-visio.viewer', + '.vsd' => 'application/vnd.ms-visio.viewer', + '.vss' => 'application/vnd.ms-visio.viewer', + '.vst' => 'application/vnd.ms-visio.viewer', + '.vsx' => 'application/vnd.ms-visio.viewer', + '.vtx' => 'application/vnd.ms-visio.viewer', + '.wav' => 'audio/wav', + '.wax' => 'audio/x-ms-wax', + '.wbk' => 'application/msword', + '.wdp' => 'image/vnd.ms-photo', + '.wiz' => 'application/msword', + '.wm' => 'video/x-ms-wm', + '.wma' => 'audio/x-ms-wma', + '.wmd' => 'application/x-ms-wmd', + '.wmv' => 'video/x-ms-wmv', + '.wmx' => 'video/x-ms-wmx', + '.wmz' => 'application/x-ms-wmz', + '.wpl' => 'application/vnd.ms-wpl', + '.wsc' => 'text/scriptlet', + '.wvx' => 'video/x-ms-wvx', + '.xaml' => 'application/xaml+xml', + '.xbap' => 'application/x-ms-xbap', + '.xdp' => 'application/vnd.adobe.xdp+xml', + '.xfdf' => 'application/vnd.adobe.xfdf', + '.xht' => 'application/xhtml+xml', + '.xhtml' => 'application/xhtml+xml', + '.xla' => 'application/vnd.ms-excel', + '.xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + '.xlk' => 'application/vnd.ms-excel', + '.xll' => 'application/vnd.ms-excel', + '.xlm' => 'application/vnd.ms-excel', + '.xls' => 'application/vnd.ms-excel', + '.xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + '.xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', + '.xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xlt' => 'application/vnd.ms-excel', + '.xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', + '.xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + '.xlw' => 'application/vnd.ms-excel', + '.xml' => 'text/xml', + '.xps' => 'application/vnd.ms-xpsdocument', + '.xsl' => 'text/xml', + ]; + + /** + * IE versions which have been analysed to bring you this class, and for + * which some substantive difference exists. These will appear as keys + * in the return value of getRealMimesFromData(). The names are chosen to sort correctly. + */ + protected $versions = [ 'ie05', 'ie06', 'ie07', 'ie07.strict', 'ie07.nohtml' ]; + + /** + * Type table with versions expanded + */ + protected $typeTable = []; + + /** constructor */ + function __construct() { + // Construct versioned type arrays from the base type array plus additions + $types = $this->baseTypeTable; + foreach ( $this->versions as $version ) { + if ( isset( $this->addedTypes[$version] ) ) { + foreach ( $this->addedTypes[$version] as $format => $addedTypes ) { + $types[$format] = array_merge( $types[$format], $addedTypes ); + } + } + $this->typeTable[$version] = $types; + } + } + + /** + * Get the MIME types from getMimesFromData(), but convert the result from IE's + * idiosyncratic private types into something other apps will understand. + * + * @param string $fileName the file name (unused at present) + * @param string $chunk the first 256 bytes of the file + * @param string $proposed the MIME type proposed by the server + * + * @return Array: map of IE version to detected MIME type + */ + public function getRealMimesFromData( $fileName, $chunk, $proposed ) { + $types = $this->getMimesFromData( $fileName, $chunk, $proposed ); + $types = array_map( [ $this, 'translateMimeType' ], $types ); + return $types; + } + + /** + * Translate a MIME type from IE's idiosyncratic private types into + * more commonly understood type strings + * @param $type + * @return string + */ + public function translateMimeType( $type ) { + static $table = [ + 'image/pjpeg' => 'image/jpeg', + 'image/x-png' => 'image/png', + 'image/x-wmf' => 'application/x-msmetafile', + 'image/bmp' => 'image/x-bmp', + 'application/x-zip-compressed' => 'application/zip', + 'application/x-compressed' => 'application/x-compress', + 'application/x-gzip-compressed' => 'application/x-gzip', + 'audio/mid' => 'audio/midi', + ]; + if ( isset( $table[$type] ) ) { + $type = $table[$type]; + } + return $type; + } + + /** + * Get the untranslated MIME types for all known versions + * + * @param string $fileName the file name (unused at present) + * @param string $chunk the first 256 bytes of the file + * @param string $proposed the MIME type proposed by the server + * + * @return Array: map of IE version to detected MIME type + */ + public function getMimesFromData( $fileName, $chunk, $proposed ) { + $types = []; + foreach ( $this->versions as $version ) { + $types[$version] = $this->getMimeTypeForVersion( $version, $fileName, $chunk, $proposed ); + } + return $types; + } + + /** + * Get the MIME type for a given named version + * @param $version + * @param $fileName + * @param $chunk + * @param $proposed + * @return bool|string + */ + protected function getMimeTypeForVersion( $version, $fileName, $chunk, $proposed ) { + // Strip text after a semicolon + $semiPos = strpos( $proposed, ';' ); + if ( $semiPos !== false ) { + $proposed = substr( $proposed, 0, $semiPos ); + } + + $proposedFormat = $this->getDataFormat( $version, $proposed ); + if ( $proposedFormat == 'unknown' + && $proposed != 'multipart/mixed' + && $proposed != 'multipart/x-mixed-replace' ) + { + return $proposed; + } + if ( strval( $chunk ) === '' ) { + return $proposed; + } + + // Truncate chunk at 255 bytes + $chunk = substr( $chunk, 0, 255 ); + + // IE does the Check*Headers() calls last, and instead does the following image + // type checks by directly looking for the magic numbers. What I do here should + // have the same effect since the magic number checks are identical in both cases. + $result = $this->sampleData( $version, $chunk ); + $sampleFound = $result['found']; + $counters = $result['counters']; + $binaryType = $this->checkBinaryHeaders( $version, $chunk ); + $textType = $this->checkTextHeaders( $version, $chunk ); + + if ( $proposed == 'text/html' && isset( $sampleFound['html'] ) ) { + return 'text/html'; + } + if ( $proposed == 'image/gif' && $binaryType == 'image/gif' ) { + return 'image/gif'; + } + if ( ( $proposed == 'image/pjpeg' || $proposed == 'image/jpeg' ) + && $binaryType == 'image/pjpeg' ) + { + return $proposed; + } + // PNG check added in IE 7 + if ( $version >= 'ie07' + && ( $proposed == 'image/x-png' || $proposed == 'image/png' ) + && $binaryType == 'image/x-png' ) + { + return $proposed; + } + + // CDF was removed in IE 7 so it won't be in $sampleFound for later versions + if ( isset( $sampleFound['cdf'] ) ) { + return 'application/x-cdf'; + } + + // RSS and Atom were added in IE 7 so they won't be in $sampleFound for + // previous versions + if ( isset( $sampleFound['rss'] ) ) { + return 'application/rss+xml'; + } + if ( isset( $sampleFound['rdf-tag'] ) + && isset( $sampleFound['rdf-url'] ) + && isset( $sampleFound['rdf-purl'] ) ) + { + return 'application/rss+xml'; + } + if ( isset( $sampleFound['atom'] ) ) { + return 'application/atom+xml'; + } + + if ( isset( $sampleFound['xml'] ) ) { + // TODO: I'm not sure under what circumstances this flag is enabled + if ( strpos( $version, 'strict' ) !== false ) { + if ( $proposed == 'text/html' || $proposed == 'text/xml' ) { + return 'text/xml'; + } + } else { + return 'text/xml'; + } + } + if ( isset( $sampleFound['html'] ) ) { + // TODO: I'm not sure under what circumstances this flag is enabled + if ( strpos( $version, 'nohtml' ) !== false ) { + if ( $proposed == 'text/plain' ) { + return 'text/html'; + } + } else { + return 'text/html'; + } + } + if ( isset( $sampleFound['xbm'] ) ) { + return 'image/x-bitmap'; + } + if ( isset( $sampleFound['binhex'] ) ) { + return 'application/macbinhex40'; + } + if ( isset( $sampleFound['scriptlet'] ) ) { + if ( strpos( $version, 'strict' ) !== false ) { + if ( $proposed == 'text/plain' || $proposed == 'text/scriptlet' ) { + return 'text/scriptlet'; + } + } else { + return 'text/scriptlet'; + } + } + + // Freaky heuristics to determine if the data is text or binary + // The heuristic is of course broken for non-ASCII text + if ( $counters['ctrl'] != 0 && ( $counters['ff'] + $counters['low'] ) + < ( $counters['ctrl'] + $counters['high'] ) * 16 ) + { + $kindOfBinary = true; + $type = $binaryType ? $binaryType : $textType; + if ( $type === false ) { + $type = 'application/octet-stream'; + } + } else { + $kindOfBinary = false; + $type = $textType ? $textType : $binaryType; + if ( $type === false ) { + $type = 'text/plain'; + } + } + + // Check if the output format is ambiguous + // This generally means that detection failed, real types aren't ambiguous + $detectedFormat = $this->getDataFormat( $version, $type ); + if ( $detectedFormat != 'ambiguous' ) { + return $type; + } + + if ( $proposedFormat != 'ambiguous' ) { + // FormatAgreesWithData() + if ( $proposedFormat == 'text' && !$kindOfBinary ) { + return $proposed; + } + if ( $proposedFormat == 'binary' && $kindOfBinary ) { + return $proposed; + } + if ( $proposedFormat == 'html' ) { + return $proposed; + } + } + + // Find a MIME type by searching the registry for the file extension. + $dotPos = strrpos( $fileName, '.' ); + if ( $dotPos === false ) { + return $type; + } + $ext = substr( $fileName, $dotPos ); + if ( isset( $this->registry[$ext] ) ) { + return $this->registry[$ext]; + } + + // TODO: If the extension has an application registered to it, IE will return + // application/octet-stream. We'll skip that, so we could erroneously + // return text/plain or application/x-netcdf where application/octet-stream + // would be correct. + + return $type; + } + + /** + * Check for text headers at the start of the chunk + * Confirmed same in 5 and 7. + * @param $version + * @param $chunk + * @return bool|string + */ + private function checkTextHeaders( $version, $chunk ) { + $chunk2 = substr( $chunk, 0, 2 ); + $chunk4 = substr( $chunk, 0, 4 ); + $chunk5 = substr( $chunk, 0, 5 ); + if ( $chunk4 == '%PDF' ) { + return 'application/pdf'; + } + if ( $chunk2 == '%!' ) { + return 'application/postscript'; + } + if ( $chunk5 == '{\\rtf' ) { + return 'text/richtext'; + } + if ( $chunk5 == 'begin' ) { + return 'application/base64'; + } + return false; + } + + /** + * Check for binary headers at the start of the chunk + * Confirmed same in 5 and 7. + * @param $version + * @param $chunk + * @return bool|string + */ + private function checkBinaryHeaders( $version, $chunk ) { + $chunk2 = substr( $chunk, 0, 2 ); + $chunk3 = substr( $chunk, 0, 3 ); + $chunk4 = substr( $chunk, 0, 4 ); + $chunk5 = substr( $chunk, 0, 5 ); + $chunk5uc = strtoupper( $chunk5 ); + $chunk8 = substr( $chunk, 0, 8 ); + if ( $chunk5uc == 'GIF87' || $chunk5uc == 'GIF89' ) { + return 'image/gif'; + } + if ( $chunk2 == "\xff\xd8" ) { + return 'image/pjpeg'; // actually plain JPEG but this is what IE returns + } + + if ( $chunk2 == 'BM' + && substr( $chunk, 6, 2 ) == "\000\000" + && substr( $chunk, 8, 2 ) == "\000\000" ) + { + return 'image/bmp'; // another non-standard MIME + } + if ( $chunk4 == 'RIFF' + && substr( $chunk, 8, 4 ) == 'WAVE' ) + { + return 'audio/wav'; + } + // These were integer literals in IE + // Perhaps the author was not sure what the target endianness was + if ( $chunk4 == ".sd\000" + || $chunk4 == ".snd" + || $chunk4 == "\000ds." + || $chunk4 == "dns." ) + { + return 'audio/basic'; + } + if ( $chunk3 == "MM\000" ) { + return 'image/tiff'; + } + if ( $chunk2 == 'MZ' ) { + return 'application/x-msdownload'; + } + if ( $chunk8 == "\x89PNG\x0d\x0a\x1a\x0a" ) { + return 'image/x-png'; // [sic] + } + if ( strlen( $chunk ) >= 5 ) { + $byte2 = ord( $chunk[2] ); + $byte4 = ord( $chunk[4] ); + if ( $byte2 >= 3 && $byte2 <= 31 && $byte4 == 0 && $chunk2 == 'JG' ) { + return 'image/x-jg'; + } + } + // More endian confusion? + if ( $chunk4 == 'MROF' ) { + return 'audio/x-aiff'; + } + $chunk4_8 = substr( $chunk, 8, 4 ); + if ( $chunk4 == 'FORM' && ( $chunk4_8 == 'AIFF' || $chunk4_8 == 'AIFC' ) ) { + return 'audio/x-aiff'; + } + if ( $chunk4 == 'RIFF' && $chunk4_8 == 'AVI ' ) { + return 'video/avi'; + } + if ( $chunk4 == "\x00\x00\x01\xb3" || $chunk4 == "\x00\x00\x01\xba" ) { + return 'video/mpeg'; + } + if ( $chunk4 == "\001\000\000\000" + && substr( $chunk, 40, 4 ) == ' EMF' ) + { + return 'image/x-emf'; + } + if ( $chunk4 == "\xd7\xcd\xc6\x9a" ) { + return 'image/x-wmf'; + } + if ( $chunk4 == "\xca\xfe\xba\xbe" ) { + return 'application/java'; + } + if ( $chunk2 == 'PK' ) { + return 'application/x-zip-compressed'; + } + if ( $chunk2 == "\x1f\x9d" ) { + return 'application/x-compressed'; + } + if ( $chunk2 == "\x1f\x8b" ) { + return 'application/x-gzip-compressed'; + } + // Skip redundant check for ZIP + if ( $chunk5 == "MThd\000" ) { + return 'audio/mid'; + } + if ( $chunk4 == '%PDF' ) { + return 'application/pdf'; + } + return false; + } + + /** + * Do heuristic checks on the bulk of the data sample. + * Search for HTML tags. + * @param $version + * @param $chunk + * @return array + */ + protected function sampleData( $version, $chunk ) { + $found = []; + $counters = [ + 'ctrl' => 0, + 'high' => 0, + 'low' => 0, + 'lf' => 0, + 'cr' => 0, + 'ff' => 0 + ]; + $htmlTags = [ + 'html', + 'head', + 'title', + 'body', + 'script', + 'a href', + 'pre', + 'img', + 'plaintext', + 'table' + ]; + $rdfUrl = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; + $rdfPurl = 'http://purl.org/rss/1.0/'; + $xbmMagic1 = '#define'; + $xbmMagic2 = '_width'; + $xbmMagic3 = '_bits'; + $binhexMagic = 'converted with BinHex'; + $chunkLength = strlen( $chunk ); + + for ( $offset = 0; $offset < $chunkLength; $offset++ ) { + $curChar = $chunk[$offset]; + if ( $curChar == "\x0a" ) { + $counters['lf']++; + continue; + } elseif ( $curChar == "\x0d" ) { + $counters['cr']++; + continue; + } elseif ( $curChar == "\x0c" ) { + $counters['ff']++; + continue; + } elseif ( $curChar == "\t" ) { + $counters['low']++; + continue; + } elseif ( ord( $curChar ) < 32 ) { + $counters['ctrl']++; + continue; + } elseif ( ord( $curChar ) >= 128 ) { + $counters['high']++; + continue; + } + + $counters['low']++; + if ( $curChar == '<' ) { + // XML + $remainder = substr( $chunk, $offset + 1 ); + if ( !strncasecmp( $remainder, '?XML', 4 ) ) { + $nextChar = substr( $chunk, $offset + 5, 1 ); + if ( $nextChar == ':' || $nextChar == ' ' || $nextChar == "\t" ) { + $found['xml'] = true; + } + } + // Scriptlet (JSP) + if ( !strncasecmp( $remainder, 'SCRIPTLET', 9 ) ) { + $found['scriptlet'] = true; + break; + } + // HTML + foreach ( $htmlTags as $tag ) { + if ( !strncasecmp( $remainder, $tag, strlen( $tag ) ) ) { + $found['html'] = true; + } + } + // Skip broken check for additional tags (HR etc.) + + // CHANNEL replaced by RSS, RDF and FEED in IE 7 + if ( $version < 'ie07' ) { + if ( !strncasecmp( $remainder, 'CHANNEL', 7 ) ) { + $found['cdf'] = true; + } + } else { + // RSS + if ( !strncasecmp( $remainder, 'RSS', 3 ) ) { + $found['rss'] = true; + break; // return from SampleData + } + if ( !strncasecmp( $remainder, 'rdf:RDF', 7 ) ) { + $found['rdf-tag'] = true; + // no break + } + if ( !strncasecmp( $remainder, 'FEED', 4 ) ) { + $found['atom'] = true; + break; + } + } + continue; + } + // Skip broken check for --> + + // RSS URL checks + // For some reason both URLs must appear before it is recognised + $remainder = substr( $chunk, $offset ); + if ( !strncasecmp( $remainder, $rdfUrl, strlen( $rdfUrl ) ) ) { + $found['rdf-url'] = true; + if ( isset( $found['rdf-tag'] ) + && isset( $found['rdf-purl'] ) ) // [sic] + { + break; + } + continue; + } + + if ( !strncasecmp( $remainder, $rdfPurl, strlen( $rdfPurl ) ) ) { + if ( isset( $found['rdf-tag'] ) + && isset( $found['rdf-url'] ) ) // [sic] + { + break; + } + continue; + } + + // XBM checks + if ( !strncasecmp( $remainder, $xbmMagic1, strlen( $xbmMagic1 ) ) ) { + $found['xbm1'] = true; + continue; + } + if ( $curChar == '_' ) { + if ( isset( $found['xbm2'] ) ) { + if ( !strncasecmp( $remainder, $xbmMagic3, strlen( $xbmMagic3 ) ) ) { + $found['xbm'] = true; + break; + } + } elseif ( isset( $found['xbm1'] ) ) { + if ( !strncasecmp( $remainder, $xbmMagic2, strlen( $xbmMagic2 ) ) ) { + $found['xbm2'] = true; + } + } + } + + // BinHex + if ( !strncmp( $remainder, $binhexMagic, strlen( $binhexMagic ) ) ) { + $found['binhex'] = true; + } + } + return [ 'found' => $found, 'counters' => $counters ]; + } + + /** + * @param $version + * @param $type + * @return int|string + */ + protected function getDataFormat( $version, $type ) { + $types = $this->typeTable[$version]; + if ( $type == '(null)' || strval( $type ) === '' ) { + return 'ambiguous'; + } + foreach ( $types as $format => $list ) { + if ( in_array( $type, $list ) ) { + return $format; + } + } + return 'unknown'; + } +} diff --git a/includes/libs/mime/MimeAnalyzer.php b/includes/libs/mime/MimeAnalyzer.php new file mode 100644 index 0000000000..5f4d7c9ef4 --- /dev/null +++ b/includes/libs/mime/MimeAnalyzer.php @@ -0,0 +1,1166 @@ + ext + * map. Each line contains a MIME type followed by a space separated list of + * extensions. If multiple extensions for a single MIME type exist or if + * multiple MIME types exist for a single extension then in most cases + * MediaWiki assumes that the first extension following the MIME type is the + * canonical extension, and the first time a MIME type appears for a certain + * extension is considered the canonical MIME type. + * + * (Note that appending the type file list to the end of self::$wellKnownTypes + * sucks because you can't redefine canonical types. This could be fixed by + * appending self::$wellKnownTypes behind type file list, but who knows + * what will break? In practice this probably isn't a problem anyway -- Bryan) + */ + protected static $wellKnownTypes = <<typeFile = $params['typeFile']; + $this->infoFile = $params['infoFile']; + $this->xmlTypes = $params['xmlTypes']; + $this->initCallback = isset( $params['initCallback'] ) + ? $params['initCallback'] + : null; + $this->detectCallback = isset( $params['detectCallback'] ) + ? $params['detectCallback'] + : null; + $this->guessCallback = isset( $params['guessCallback'] ) + ? $params['guessCallback'] + : null; + $this->extCallback = isset( $params['extCallback'] ) + ? $params['extCallback'] + : null; + $this->logger = isset( $params['logger'] ) + ? $params['logger'] + : new \Psr\Log\NullLogger(); + + $this->loadFiles(); + } + + protected function loadFiles() { + /** + * --- load mime.types --- + */ + + # Allow media handling extensions adding MIME-types and MIME-info + if ( $this->initCallback ) { + call_user_func( $this->initCallback, $this ); + } + + $types = self::$wellKnownTypes; + + $mimeTypeFile = $this->typeFile; + if ( $mimeTypeFile ) { + if ( is_file( $mimeTypeFile ) && is_readable( $mimeTypeFile ) ) { + $this->logger->info( __METHOD__ . ": loading mime types from $mimeTypeFile\n" ); + $types .= "\n"; + $types .= file_get_contents( $mimeTypeFile ); + } else { + $this->logger->info( __METHOD__ . ": can't load mime types from $mimeTypeFile\n" ); + } + } else { + $this->logger->info( __METHOD__ . + ": no mime types file defined, using built-ins only.\n" ); + } + + $types .= "\n" . $this->extraTypes; + + $types = str_replace( [ "\r\n", "\n\r", "\n\n", "\r\r", "\r" ], "\n", $types ); + $types = str_replace( "\t", " ", $types ); + + $this->mimetoExt = []; + $this->mExtToMime = []; + + $lines = explode( "\n", $types ); + foreach ( $lines as $s ) { + $s = trim( $s ); + if ( empty( $s ) ) { + continue; + } + if ( strpos( $s, '#' ) === 0 ) { + continue; + } + + $s = strtolower( $s ); + $i = strpos( $s, ' ' ); + + if ( $i === false ) { + continue; + } + + $mime = substr( $s, 0, $i ); + $ext = trim( substr( $s, $i + 1 ) ); + + if ( empty( $ext ) ) { + continue; + } + + if ( !empty( $this->mimetoExt[$mime] ) ) { + $this->mimetoExt[$mime] .= ' ' . $ext; + } else { + $this->mimetoExt[$mime] = $ext; + } + + $extensions = explode( ' ', $ext ); + + foreach ( $extensions as $e ) { + $e = trim( $e ); + if ( empty( $e ) ) { + continue; + } + + if ( !empty( $this->mExtToMime[$e] ) ) { + $this->mExtToMime[$e] .= ' ' . $mime; + } else { + $this->mExtToMime[$e] = $mime; + } + } + } + + /** + * --- load mime.info --- + */ + + $mimeInfoFile = $this->infoFile; + + $info = self::$wellKnownInfo; + + if ( $mimeInfoFile ) { + if ( is_file( $mimeInfoFile ) && is_readable( $mimeInfoFile ) ) { + $this->logger->info( __METHOD__ . ": loading mime info from $mimeInfoFile\n" ); + $info .= "\n"; + $info .= file_get_contents( $mimeInfoFile ); + } else { + $this->logger->info( __METHOD__ . ": can't load mime info from $mimeInfoFile\n" ); + } + } else { + $this->logger->info( __METHOD__ . + ": no mime info file defined, using built-ins only.\n" ); + } + + $info .= "\n" . $this->extraInfo; + + $info = str_replace( [ "\r\n", "\n\r", "\n\n", "\r\r", "\r" ], "\n", $info ); + $info = str_replace( "\t", " ", $info ); + + $this->mimeTypeAliases = []; + $this->mediaTypes = []; + + $lines = explode( "\n", $info ); + foreach ( $lines as $s ) { + $s = trim( $s ); + if ( empty( $s ) ) { + continue; + } + if ( strpos( $s, '#' ) === 0 ) { + continue; + } + + $s = strtolower( $s ); + $i = strpos( $s, ' ' ); + + if ( $i === false ) { + continue; + } + + # print "processing MIME INFO line $s
"; + + $match = []; + if ( preg_match( '!\[\s*(\w+)\s*\]!', $s, $match ) ) { + $s = preg_replace( '!\[\s*(\w+)\s*\]!', '', $s ); + $mtype = trim( strtoupper( $match[1] ) ); + } else { + $mtype = MEDIATYPE_UNKNOWN; + } + + $m = explode( ' ', $s ); + + if ( !isset( $this->mediaTypes[$mtype] ) ) { + $this->mediaTypes[$mtype] = []; + } + + foreach ( $m as $mime ) { + $mime = trim( $mime ); + if ( empty( $mime ) ) { + continue; + } + + $this->mediaTypes[$mtype][] = $mime; + } + + if ( count( $m ) > 1 ) { + $main = $m[0]; + $mCount = count( $m ); + for ( $i = 1; $i < $mCount; $i += 1 ) { + $mime = $m[$i]; + $this->mimeTypeAliases[$mime] = $main; + } + } + } + } + + public function setLogger( LoggerInterface $logger ) { + $this->logger = $logger; + } + + /** + * Adds to the list mapping MIME to file extensions. + * As an extension author, you are encouraged to submit patches to + * MediaWiki's core to add new MIME types to mime.types. + * @param string $types + */ + public function addExtraTypes( $types ) { + $this->extraTypes .= "\n" . $types; + } + + /** + * Adds to the list mapping MIME to media type. + * As an extension author, you are encouraged to submit patches to + * MediaWiki's core to add new MIME info to mime.info. + * @param string $info + */ + public function addExtraInfo( $info ) { + $this->extraInfo .= "\n" . $info; + } + + /** + * Returns a list of file extensions for a given MIME type as a space + * separated string or null if the MIME type was unrecognized. Resolves + * MIME type aliases. + * + * @param string $mime + * @return string|null + */ + public function getExtensionsForType( $mime ) { + $mime = strtolower( $mime ); + + // Check the mime-to-ext map + if ( isset( $this->mimetoExt[$mime] ) ) { + return $this->mimetoExt[$mime]; + } + + // Resolve the MIME type to the canonical type + if ( isset( $this->mimeTypeAliases[$mime] ) ) { + $mime = $this->mimeTypeAliases[$mime]; + if ( isset( $this->mimetoExt[$mime] ) ) { + return $this->mimetoExt[$mime]; + } + } + + return null; + } + + /** + * Returns a list of MIME types for a given file extension as a space + * separated string or null if the extension was unrecognized. + * + * @param string $ext + * @return string|null + */ + public function getTypesForExtension( $ext ) { + $ext = strtolower( $ext ); + + $r = isset( $this->mExtToMime[$ext] ) ? $this->mExtToMime[$ext] : null; + return $r; + } + + /** + * Returns a single MIME type for a given file extension or null if unknown. + * This is always the first type from the list returned by getTypesForExtension($ext). + * + * @param string $ext + * @return string|null + */ + public function guessTypesForExtension( $ext ) { + $m = $this->getTypesForExtension( $ext ); + if ( is_null( $m ) ) { + return null; + } + + // TODO: Check if this is needed; strtok( $m, ' ' ) should be sufficient + $m = trim( $m ); + $m = preg_replace( '/\s.*$/', '', $m ); + + return $m; + } + + /** + * Tests if the extension matches the given MIME type. Returns true if a + * match was found, null if the MIME type is unknown, and false if the + * MIME type is known but no matches where found. + * + * @param string $extension + * @param string $mime + * @return bool|null + */ + public function isMatchingExtension( $extension, $mime ) { + $ext = $this->getExtensionsForType( $mime ); + + if ( !$ext ) { + return null; // Unknown MIME type + } + + $ext = explode( ' ', $ext ); + + $extension = strtolower( $extension ); + return in_array( $extension, $ext ); + } + + /** + * Returns true if the MIME type is known to represent an image format + * supported by the PHP GD library. + * + * @param string $mime + * + * @return bool + */ + public function isPHPImageType( $mime ) { + // As defined by imagegetsize and image_type_to_mime + static $types = [ + 'image/gif', 'image/jpeg', 'image/png', + 'image/x-bmp', 'image/xbm', 'image/tiff', + 'image/jp2', 'image/jpeg2000', 'image/iff', + 'image/xbm', 'image/x-xbitmap', + 'image/vnd.wap.wbmp', 'image/vnd.xiff', + 'image/x-photoshop', + 'application/x-shockwave-flash', + ]; + + return in_array( $mime, $types ); + } + + /** + * Returns true if the extension represents a type which can + * be reliably detected from its content. Use this to determine + * whether strict content checks should be applied to reject + * invalid uploads; if we can't identify the type we won't + * be able to say if it's invalid. + * + * @todo Be more accurate when using fancy MIME detector plugins; + * right now this is the bare minimum getimagesize() list. + * @param string $extension + * @return bool + */ + function isRecognizableExtension( $extension ) { + static $types = [ + // Types recognized by getimagesize() + 'gif', 'jpeg', 'jpg', 'png', 'swf', 'psd', + 'bmp', 'tiff', 'tif', 'jpc', 'jp2', + 'jpx', 'jb2', 'swc', 'iff', 'wbmp', + 'xbm', + + // Formats we recognize magic numbers for + 'djvu', 'ogx', 'ogg', 'ogv', 'oga', 'spx', + 'mid', 'pdf', 'wmf', 'xcf', 'webm', 'mkv', 'mka', + 'webp', + + // XML formats we sure hope we recognize reliably + 'svg', + ]; + return in_array( strtolower( $extension ), $types ); + } + + /** + * Improves a MIME type using the file extension. Some file formats are very generic, + * so their MIME type is not very meaningful. A more useful MIME type can be derived + * by looking at the file extension. Typically, this method would be called on the + * result of guessMimeType(). + * + * @param string $mime The MIME type, typically guessed from a file's content. + * @param string $ext The file extension, as taken from the file name + * + * @return string The MIME type + */ + public function improveTypeFromExtension( $mime, $ext ) { + if ( $mime === 'unknown/unknown' ) { + if ( $this->isRecognizableExtension( $ext ) ) { + $this->logger->info( __METHOD__ . ': refusing to guess mime type for .' . + "$ext file, we should have recognized it\n" ); + } else { + // Not something we can detect, so simply + // trust the file extension + $mime = $this->guessTypesForExtension( $ext ); + } + } elseif ( $mime === 'application/x-opc+zip' ) { + if ( $this->isMatchingExtension( $ext, $mime ) ) { + // A known file extension for an OPC file, + // find the proper MIME type for that file extension + $mime = $this->guessTypesForExtension( $ext ); + } else { + $this->logger->info( __METHOD__ . + ": refusing to guess better type for $mime file, " . + ".$ext is not a known OPC extension.\n" ); + $mime = 'application/zip'; + } + } elseif ( $mime === 'text/plain' && $this->findMediaType( ".$ext" ) === MEDIATYPE_TEXT ) { + // Textual types are sometimes not recognized properly. + // If detected as text/plain, and has an extension which is textual + // improve to the extension's type. For example, csv and json are often + // misdetected as text/plain. + $mime = $this->guessTypesForExtension( $ext ); + } + + # Media handling extensions can improve the MIME detected + $callback = $this->extCallback; + if ( $callback ) { + $callback( $this, $ext, $mime /* by reference */ ); + } + + if ( isset( $this->mimeTypeAliases[$mime] ) ) { + $mime = $this->mimeTypeAliases[$mime]; + } + + $this->logger->info( __METHOD__ . ": improved mime type for .$ext: $mime\n" ); + return $mime; + } + + /** + * MIME type detection. This uses detectMimeType to detect the MIME type + * of the file, but applies additional checks to determine some well known + * file formats that may be missed or misinterpreted by the default MIME + * detection (namely XML based formats like XHTML or SVG, as well as ZIP + * based formats like OPC/ODF files). + * + * @param string $file The file to check + * @param string|bool $ext The file extension, or true (default) to extract + * it from the filename. Set it to false to ignore the extension. DEPRECATED! + * Set to false, use improveTypeFromExtension($mime, $ext) later to improve MIME type. + * + * @return string The MIME type of $file + */ + public function guessMimeType( $file, $ext = true ) { + if ( $ext ) { // TODO: make $ext default to false. Or better, remove it. + $this->logger->info( __METHOD__ . + ": WARNING: use of the \$ext parameter is deprecated. " . + "Use improveTypeFromExtension(\$mime, \$ext) instead.\n" ); + } + + $mime = $this->doGuessMimeType( $file, $ext ); + + if ( !$mime ) { + $this->logger->info( __METHOD__ . + ": internal type detection failed for $file (.$ext)...\n" ); + $mime = $this->detectMimeType( $file, $ext ); + } + + if ( isset( $this->mimeTypeAliases[$mime] ) ) { + $mime = $this->mimeTypeAliases[$mime]; + } + + $this->logger->info( __METHOD__ . ": guessed mime type of $file: $mime\n" ); + return $mime; + } + + /** + * Guess the MIME type from the file contents. + * + * @todo Remove $ext param + * + * @param string $file + * @param mixed $ext + * @return bool|string + * @throws UnexpectedValueException + */ + private function doGuessMimeType( $file, $ext ) { + // Read a chunk of the file + MediaWiki\suppressWarnings(); + $f = fopen( $file, 'rb' ); + MediaWiki\restoreWarnings(); + + if ( !$f ) { + return 'unknown/unknown'; + } + + $fsize = filesize( $file ); + if ( $fsize === false ) { + return 'unknown/unknown'; + } + + $head = fread( $f, 1024 ); + $tailLength = min( 65558, $fsize ); // 65558 = maximum size of a zip EOCDR + if ( fseek( $f, -1 * $tailLength, SEEK_END ) === -1 ) { + throw new UnexpectedValueException( + "Seeking $tailLength bytes from EOF failed in " . __METHOD__ ); + } + $tail = $tailLength ? fread( $f, $tailLength ) : ''; + fclose( $f ); + + $this->logger->info( __METHOD__ . + ": analyzing head and tail of $file for magic numbers.\n" ); + + // Hardcode a few magic number checks... + $headers = [ + // Multimedia... + 'MThd' => 'audio/midi', + 'OggS' => 'application/ogg', + + // Image formats... + // Note that WMF may have a bare header, no magic number. + "\x01\x00\x09\x00" => 'application/x-msmetafile', // Possibly prone to false positives? + "\xd7\xcd\xc6\x9a" => 'application/x-msmetafile', + '%PDF' => 'application/pdf', + 'gimp xcf' => 'image/x-xcf', + + // Some forbidden fruit... + 'MZ' => 'application/octet-stream', // DOS/Windows executable + "\xca\xfe\xba\xbe" => 'application/octet-stream', // Mach-O binary + "\x7fELF" => 'application/octet-stream', // ELF binary + ]; + + foreach ( $headers as $magic => $candidate ) { + if ( strncmp( $head, $magic, strlen( $magic ) ) == 0 ) { + $this->logger->info( __METHOD__ . + ": magic header in $file recognized as $candidate\n" ); + return $candidate; + } + } + + /* Look for WebM and Matroska files */ + if ( strncmp( $head, pack( "C4", 0x1a, 0x45, 0xdf, 0xa3 ), 4 ) == 0 ) { + $doctype = strpos( $head, "\x42\x82" ); + if ( $doctype ) { + // Next byte is datasize, then data (sizes larger than 1 byte are stupid muxers) + $data = substr( $head, $doctype + 3, 8 ); + if ( strncmp( $data, "matroska", 8 ) == 0 ) { + $this->logger->info( __METHOD__ . ": recognized file as video/x-matroska\n" ); + return "video/x-matroska"; + } elseif ( strncmp( $data, "webm", 4 ) == 0 ) { + $this->logger->info( __METHOD__ . ": recognized file as video/webm\n" ); + return "video/webm"; + } + } + $this->logger->info( __METHOD__ . ": unknown EBML file\n" ); + return "unknown/unknown"; + } + + /* Look for WebP */ + if ( strncmp( $head, "RIFF", 4 ) == 0 && + strncmp( substr( $head, 8, 7 ), "WEBPVP8", 7 ) == 0 + ) { + $this->logger->info( __METHOD__ . ": recognized file as image/webp\n" ); + return "image/webp"; + } + + /** + * Look for PHP. Check for this before HTML/XML... Warning: this is a + * heuristic, and won't match a file with a lot of non-PHP before. It + * will also match text files which could be PHP. :) + * + * @todo FIXME: For this reason, the check is probably useless -- an attacker + * could almost certainly just pad the file with a lot of nonsense to + * circumvent the check in any case where it would be a security + * problem. On the other hand, it causes harmful false positives (bug + * 16583). The heuristic has been cut down to exclude three-character + * strings like "logger->info( __METHOD__ . ": recognized $file as application/x-php\n" ); + return 'application/x-php'; + } + + /** + * look for XML formats (XHTML and SVG) + */ + $xml = new XmlTypeCheck( $file ); + if ( $xml->wellFormed ) { + $xmlTypes = $this->xmlTypes; + if ( isset( $xmlTypes[$xml->getRootElement()] ) ) { + return $xmlTypes[$xml->getRootElement()]; + } else { + return 'application/xml'; + } + } + + /** + * look for shell scripts + */ + $script_type = null; + + # detect by shebang + if ( substr( $head, 0, 2 ) == "#!" ) { + $script_type = "ASCII"; + } elseif ( substr( $head, 0, 5 ) == "\xef\xbb\xbf#!" ) { + $script_type = "UTF-8"; + } elseif ( substr( $head, 0, 7 ) == "\xfe\xff\x00#\x00!" ) { + $script_type = "UTF-16BE"; + } elseif ( substr( $head, 0, 7 ) == "\xff\xfe#\x00!" ) { + $script_type = "UTF-16LE"; + } + + if ( $script_type ) { + if ( $script_type !== "UTF-8" && $script_type !== "ASCII" ) { + // Quick and dirty fold down to ASCII! + $pack = [ 'UTF-16BE' => 'n*', 'UTF-16LE' => 'v*' ]; + $chars = unpack( $pack[$script_type], substr( $head, 2 ) ); + $head = ''; + foreach ( $chars as $codepoint ) { + if ( $codepoint < 128 ) { + $head .= chr( $codepoint ); + } else { + $head .= '?'; + } + } + } + + $match = []; + + if ( preg_match( '%/?([^\s]+/)(\w+)%', $head, $match ) ) { + $mime = "application/x-{$match[2]}"; + $this->logger->info( __METHOD__ . ": shell script recognized as $mime\n" ); + return $mime; + } + } + + // Check for ZIP variants (before getimagesize) + if ( strpos( $tail, "PK\x05\x06" ) !== false ) { + $this->logger->info( __METHOD__ . ": ZIP header present in $file\n" ); + return $this->detectZipType( $head, $tail, $ext ); + } + + MediaWiki\suppressWarnings(); + $gis = getimagesize( $file ); + MediaWiki\restoreWarnings(); + + if ( $gis && isset( $gis['mime'] ) ) { + $mime = $gis['mime']; + $this->logger->info( __METHOD__ . ": getimagesize detected $file as $mime\n" ); + return $mime; + } + + # Media handling extensions can guess the MIME by content + # It's intentionally here so that if core is wrong about a type (false positive), + # people will hopefully nag and submit patches :) + $mime = false; + # Some strings by reference for performance - assuming well-behaved hooks + $callback = $this->guessCallback; + if ( $callback ) { + $callback( $this, $head, $tail, $file, $mime /* by reference */ ); + }; + + return $mime; + } + + /** + * Detect application-specific file type of a given ZIP file from its + * header data. Currently works for OpenDocument and OpenXML types... + * If can't tell, returns 'application/zip'. + * + * @param string $header Some reasonably-sized chunk of file header + * @param string|null $tail The tail of the file + * @param string|bool $ext The file extension, or true to extract it from the filename. + * Set it to false (default) to ignore the extension. DEPRECATED! Set to false, + * use improveTypeFromExtension($mime, $ext) later to improve MIME type. + * + * @return string + */ + function detectZipType( $header, $tail = null, $ext = false ) { + if ( $ext ) { # TODO: remove $ext param + $this->logger->info( __METHOD__ . + ": WARNING: use of the \$ext parameter is deprecated. " . + "Use improveTypeFromExtension(\$mime, \$ext) instead.\n" ); + } + + $mime = 'application/zip'; + $opendocTypes = [ + 'chart-template', + 'chart', + 'formula-template', + 'formula', + 'graphics-template', + 'graphics', + 'image-template', + 'image', + 'presentation-template', + 'presentation', + 'spreadsheet-template', + 'spreadsheet', + 'text-template', + 'text-master', + 'text-web', + 'text' ]; + + // http://lists.oasis-open.org/archives/office/200505/msg00006.html + $types = '(?:' . implode( '|', $opendocTypes ) . ')'; + $opendocRegex = "/^mimetype(application\/vnd\.oasis\.opendocument\.$types)/"; + + $openxmlRegex = "/^\[Content_Types\].xml/"; + + if ( preg_match( $opendocRegex, substr( $header, 30 ), $matches ) ) { + $mime = $matches[1]; + $this->logger->info( __METHOD__ . ": detected $mime from ZIP archive\n" ); + } elseif ( preg_match( $openxmlRegex, substr( $header, 30 ) ) ) { + $mime = "application/x-opc+zip"; + # TODO: remove the block below, as soon as improveTypeFromExtension is used everywhere + if ( $ext !== true && $ext !== false ) { + /** This is the mode used by getPropsFromPath + * These MIME's are stored in the database, where we don't really want + * x-opc+zip, because we use it only for internal purposes + */ + if ( $this->isMatchingExtension( $ext, $mime ) ) { + /* A known file extension for an OPC file, + * find the proper mime type for that file extension + */ + $mime = $this->guessTypesForExtension( $ext ); + } else { + $mime = "application/zip"; + } + } + $this->logger->info( __METHOD__ . + ": detected an Open Packaging Conventions archive: $mime\n" ); + } elseif ( substr( $header, 0, 8 ) == "\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" && + ( $headerpos = strpos( $tail, "PK\x03\x04" ) ) !== false && + preg_match( $openxmlRegex, substr( $tail, $headerpos + 30 ) ) ) { + if ( substr( $header, 512, 4 ) == "\xEC\xA5\xC1\x00" ) { + $mime = "application/msword"; + } + switch ( substr( $header, 512, 6 ) ) { + case "\xEC\xA5\xC1\x00\x0E\x00": + case "\xEC\xA5\xC1\x00\x1C\x00": + case "\xEC\xA5\xC1\x00\x43\x00": + $mime = "application/vnd.ms-powerpoint"; + break; + case "\xFD\xFF\xFF\xFF\x10\x00": + case "\xFD\xFF\xFF\xFF\x1F\x00": + case "\xFD\xFF\xFF\xFF\x22\x00": + case "\xFD\xFF\xFF\xFF\x23\x00": + case "\xFD\xFF\xFF\xFF\x28\x00": + case "\xFD\xFF\xFF\xFF\x29\x00": + case "\xFD\xFF\xFF\xFF\x10\x02": + case "\xFD\xFF\xFF\xFF\x1F\x02": + case "\xFD\xFF\xFF\xFF\x22\x02": + case "\xFD\xFF\xFF\xFF\x23\x02": + case "\xFD\xFF\xFF\xFF\x28\x02": + case "\xFD\xFF\xFF\xFF\x29\x02": + $mime = "application/vnd.msexcel"; + break; + } + + $this->logger->info( __METHOD__ . + ": detected a MS Office document with OPC trailer\n" ); + } else { + $this->logger->info( __METHOD__ . ": unable to identify type of ZIP archive\n" ); + } + return $mime; + } + + /** + * Internal MIME type detection. Detection is done using the fileinfo + * extension if it is available. It can be overriden by callback, which could + * use an external program, for example. If detection fails and $ext is not false, + * the MIME type is guessed from the file extension, using guessTypesForExtension. + * + * If the MIME type is still unknown, getimagesize is used to detect the + * MIME type if the file is an image. If no MIME type can be determined, + * this function returns 'unknown/unknown'. + * + * @param string $file The file to check + * @param string|bool $ext The file extension, or true (default) to extract it from the filename. + * Set it to false to ignore the extension. DEPRECATED! Set to false, use + * improveTypeFromExtension($mime, $ext) later to improve MIME type. + * + * @return string The MIME type of $file + */ + private function detectMimeType( $file, $ext = true ) { + /** @todo Make $ext default to false. Or better, remove it. */ + if ( $ext ) { + $this->logger->info( __METHOD__ . + ": WARNING: use of the \$ext parameter is deprecated. " + . "Use improveTypeFromExtension(\$mime, \$ext) instead.\n" ); + } + + $callback = $this->detectCallback; + $m = null; + if ( $callback ) { + $m = $callback( $file ); + } elseif ( function_exists( "finfo_open" ) && function_exists( "finfo_file" ) ) { + $mime_magic_resource = finfo_open( FILEINFO_MIME ); + + if ( $mime_magic_resource ) { + $m = finfo_file( $mime_magic_resource, $file ); + finfo_close( $mime_magic_resource ); + } else { + $this->logger->info( __METHOD__ . + ": finfo_open failed on " . FILEINFO_MIME . "!\n" ); + } + } else { + $this->logger->info( __METHOD__ . ": no magic mime detector found!\n" ); + } + + if ( $m ) { + # normalize + $m = preg_replace( '![;, ].*$!', '', $m ); # strip charset, etc + $m = trim( $m ); + $m = strtolower( $m ); + + if ( strpos( $m, 'unknown' ) !== false ) { + $m = null; + } else { + $this->logger->info( __METHOD__ . ": magic mime type of $file: $m\n" ); + return $m; + } + } + + // If desired, look at extension as a fallback. + if ( $ext === true ) { + $i = strrpos( $file, '.' ); + $ext = strtolower( $i ? substr( $file, $i + 1 ) : '' ); + } + if ( $ext ) { + if ( $this->isRecognizableExtension( $ext ) ) { + $this->logger->info( __METHOD__ . ": refusing to guess mime type for .$ext file, " + . "we should have recognized it\n" ); + } else { + $m = $this->guessTypesForExtension( $ext ); + if ( $m ) { + $this->logger->info( __METHOD__ . ": extension mime type of $file: $m\n" ); + return $m; + } + } + } + + // Unknown type + $this->logger->info( __METHOD__ . ": failed to guess mime type for $file!\n" ); + return 'unknown/unknown'; + } + + /** + * Determine the media type code for a file, using its MIME type, name and + * possibly its contents. + * + * This function relies on the findMediaType(), mapping extensions and MIME + * types to media types. + * + * @todo analyse file if need be + * @todo look at multiple extension, separately and together. + * + * @param string $path Full path to the image file, in case we have to look at the contents + * (if null, only the MIME type is used to determine the media type code). + * @param string $mime MIME type. If null it will be guessed using guessMimeType. + * + * @return string A value to be used with the MEDIATYPE_xxx constants. + */ + function getMediaType( $path = null, $mime = null ) { + if ( !$mime && !$path ) { + return MEDIATYPE_UNKNOWN; + } + + // If MIME type is unknown, guess it + if ( !$mime ) { + $mime = $this->guessMimeType( $path, false ); + } + + // Special code for ogg - detect if it's video (theora), + // else label it as sound. + if ( $mime == 'application/ogg' && file_exists( $path ) ) { + + // Read a chunk of the file + $f = fopen( $path, "rt" ); + if ( !$f ) { + return MEDIATYPE_UNKNOWN; + } + $head = fread( $f, 256 ); + fclose( $f ); + + $head = str_replace( 'ffmpeg2theora', '', strtolower( $head ) ); + + // This is an UGLY HACK, file should be parsed correctly + if ( strpos( $head, 'theora' ) !== false ) { + return MEDIATYPE_VIDEO; + } elseif ( strpos( $head, 'vorbis' ) !== false ) { + return MEDIATYPE_AUDIO; + } elseif ( strpos( $head, 'flac' ) !== false ) { + return MEDIATYPE_AUDIO; + } elseif ( strpos( $head, 'speex' ) !== false ) { + return MEDIATYPE_AUDIO; + } else { + return MEDIATYPE_MULTIMEDIA; + } + } + + $type = null; + // Check for entry for full MIME type + if ( $mime ) { + $type = $this->findMediaType( $mime ); + if ( $type !== MEDIATYPE_UNKNOWN ) { + return $type; + } + } + + // Check for entry for file extension + if ( $path ) { + $i = strrpos( $path, '.' ); + $e = strtolower( $i ? substr( $path, $i + 1 ) : '' ); + + // TODO: look at multi-extension if this fails, parse from full path + $type = $this->findMediaType( '.' . $e ); + if ( $type !== MEDIATYPE_UNKNOWN ) { + return $type; + } + } + + // Check major MIME type + if ( $mime ) { + $i = strpos( $mime, '/' ); + if ( $i !== false ) { + $major = substr( $mime, 0, $i ); + $type = $this->findMediaType( $major ); + if ( $type !== MEDIATYPE_UNKNOWN ) { + return $type; + } + } + } + + if ( !$type ) { + $type = MEDIATYPE_UNKNOWN; + } + + return $type; + } + + /** + * Returns a media code matching the given MIME type or file extension. + * File extensions are represented by a string starting with a dot (.) to + * distinguish them from MIME types. + * + * This function relies on the mapping defined by $this->mMediaTypes + * @access private + * @param string $extMime + * @return int|string + */ + function findMediaType( $extMime ) { + if ( strpos( $extMime, '.' ) === 0 ) { + // If it's an extension, look up the MIME types + $m = $this->getTypesForExtension( substr( $extMime, 1 ) ); + if ( !$m ) { + return MEDIATYPE_UNKNOWN; + } + + $m = explode( ' ', $m ); + } else { + // Normalize MIME type + if ( isset( $this->mimeTypeAliases[$extMime] ) ) { + $extMime = $this->mimeTypeAliases[$extMime]; + } + + $m = [ $extMime ]; + } + + foreach ( $m as $mime ) { + foreach ( $this->mediaTypes as $type => $codes ) { + if ( in_array( $mime, $codes, true ) ) { + return $type; + } + } + } + + return MEDIATYPE_UNKNOWN; + } + + /** + * Get the MIME types that various versions of Internet Explorer would + * detect from a chunk of the content. + * + * @param string $fileName The file name (unused at present) + * @param string $chunk The first 256 bytes of the file + * @param string $proposed The MIME type proposed by the server + * @return array + */ + public function getIEMimeTypes( $fileName, $chunk, $proposed ) { + $ca = $this->getIEContentAnalyzer(); + return $ca->getRealMimesFromData( $fileName, $chunk, $proposed ); + } + + /** + * Get a cached instance of IEContentAnalyzer + * + * @return IEContentAnalyzer + */ + protected function getIEContentAnalyzer() { + if ( is_null( $this->IEAnalyzer ) ) { + $this->IEAnalyzer = new IEContentAnalyzer; + } + return $this->IEAnalyzer; + } +} diff --git a/includes/libs/mime/XmlTypeCheck.php b/includes/libs/mime/XmlTypeCheck.php new file mode 100644 index 0000000000..f057140a6b --- /dev/null +++ b/includes/libs/mime/XmlTypeCheck.php @@ -0,0 +1,347 @@ + '', + ]; + + /** + * @param string $input a filename or string containing the XML element + * @param callable $filterCallback (optional) + * Function to call to do additional custom validity checks from the + * SAX element handler event. This gives you access to the element + * namespace, name, attributes, and text contents. + * Filter should return 'true' to toggle on $this->filterMatch + * @param bool $isFile (optional) indicates if the first parameter is a + * filename (default, true) or if it is a string (false) + * @param array $options list of additional parsing options: + * processing_instruction_handler: Callback for xml_set_processing_instruction_handler + */ + function __construct( $input, $filterCallback = null, $isFile = true, $options = [] ) { + $this->filterCallback = $filterCallback; + $this->parserOptions = array_merge( $this->parserOptions, $options ); + $this->validateFromInput( $input, $isFile ); + } + + /** + * Alternative constructor: from filename + * + * @param string $fname the filename of an XML document + * @param callable $filterCallback (optional) + * Function to call to do additional custom validity checks from the + * SAX element handler event. This gives you access to the element + * namespace, name, and attributes, but not to text contents. + * Filter should return 'true' to toggle on $this->filterMatch + * @return XmlTypeCheck + */ + public static function newFromFilename( $fname, $filterCallback = null ) { + return new self( $fname, $filterCallback, true ); + } + + /** + * Alternative constructor: from string + * + * @param string $string a string containing an XML element + * @param callable $filterCallback (optional) + * Function to call to do additional custom validity checks from the + * SAX element handler event. This gives you access to the element + * namespace, name, and attributes, but not to text contents. + * Filter should return 'true' to toggle on $this->filterMatch + * @return XmlTypeCheck + */ + public static function newFromString( $string, $filterCallback = null ) { + return new self( $string, $filterCallback, false ); + } + + /** + * Get the root element. Simple accessor to $rootElement + * + * @return string + */ + public function getRootElement() { + return $this->rootElement; + } + + /** + * @param string $fname the filename + */ + private function validateFromInput( $xml, $isFile ) { + $reader = new XMLReader(); + if ( $isFile ) { + $s = $reader->open( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING ); + } else { + $s = $reader->XML( $xml, null, LIBXML_NOERROR | LIBXML_NOWARNING ); + } + if ( $s !== true ) { + // Couldn't open the XML + $this->wellFormed = false; + } else { + $oldDisable = libxml_disable_entity_loader( true ); + $reader->setParserProperty( XMLReader::SUBST_ENTITIES, true ); + try { + $this->validate( $reader ); + } catch ( Exception $e ) { + // Calling this malformed, because we didn't parse the whole + // thing. Maybe just an external entity refernce. + $this->wellFormed = false; + $reader->close(); + libxml_disable_entity_loader( $oldDisable ); + throw $e; + } + $reader->close(); + libxml_disable_entity_loader( $oldDisable ); + } + } + + private function readNext( XMLReader $reader ) { + set_error_handler( [ $this, 'XmlErrorHandler' ] ); + $ret = $reader->read(); + restore_error_handler(); + return $ret; + } + + public function XmlErrorHandler( $errno, $errstr ) { + $this->wellFormed = false; + } + + private function validate( $reader ) { + + // First, move through anything that isn't an element, and + // handle any processing instructions with the callback + do { + if ( !$this->readNext( $reader ) ) { + // Hit the end of the document before any elements + $this->wellFormed = false; + return; + } + if ( $reader->nodeType === XMLReader::PI ) { + $this->processingInstructionHandler( $reader->name, $reader->value ); + } + } while ( $reader->nodeType != XMLReader::ELEMENT ); + + // Process the rest of the document + do { + switch ( $reader->nodeType ) { + case XMLReader::ELEMENT: + $name = $this->expandNS( + $reader->name, + $reader->namespaceURI + ); + if ( $this->rootElement === '' ) { + $this->rootElement = $name; + } + $empty = $reader->isEmptyElement; + $attrs = $this->getAttributesArray( $reader ); + $this->elementOpen( $name, $attrs ); + if ( $empty ) { + $this->elementClose(); + } + break; + + case XMLReader::END_ELEMENT: + $this->elementClose(); + break; + + case XMLReader::WHITESPACE: + case XMLReader::SIGNIFICANT_WHITESPACE: + case XMLReader::CDATA: + case XMLReader::TEXT: + $this->elementData( $reader->value ); + break; + + case XMLReader::ENTITY_REF: + // Unexpanded entity (maybe external?), + // don't send to the filter (xml_parse didn't) + break; + + case XMLReader::COMMENT: + // Don't send to the filter (xml_parse didn't) + break; + + case XMLReader::PI: + // Processing instructions can happen after the header too + $this->processingInstructionHandler( + $reader->name, + $reader->value + ); + break; + default: + // One of DOC, DOC_TYPE, ENTITY, END_ENTITY, + // NOTATION, or XML_DECLARATION + // xml_parse didn't send these to the filter, so we won't. + } + + } while ( $this->readNext( $reader ) ); + + if ( $this->stackDepth !== 0 ) { + $this->wellFormed = false; + } elseif ( $this->wellFormed === null ) { + $this->wellFormed = true; + } + + } + + /** + * Get all of the attributes for an XMLReader's current node + * @param $r XMLReader + * @return array of attributes + */ + private function getAttributesArray( XMLReader $r ) { + $attrs = []; + while ( $r->moveToNextAttribute() ) { + if ( $r->namespaceURI === 'http://www.w3.org/2000/xmlns/' ) { + // XMLReader treats xmlns attributes as normal + // attributes, while xml_parse doesn't + continue; + } + $name = $this->expandNS( $r->name, $r->namespaceURI ); + $attrs[$name] = $r->value; + } + return $attrs; + } + + /** + * @param $name element or attribute name, maybe with a full or short prefix + * @param $namespaceURI the namespaceURI + * @return string the name prefixed with namespaceURI + */ + private function expandNS( $name, $namespaceURI ) { + if ( $namespaceURI ) { + $parts = explode( ':', $name ); + $localname = array_pop( $parts ); + return "$namespaceURI:$localname"; + } + return $name; + } + + /** + * @param $name + * @param $attribs + */ + private function elementOpen( $name, $attribs ) { + $this->elementDataContext[] = [ $name, $attribs ]; + $this->elementData[] = ''; + $this->stackDepth++; + } + + /** + */ + private function elementClose() { + list( $name, $attribs ) = array_pop( $this->elementDataContext ); + $data = array_pop( $this->elementData ); + $this->stackDepth--; + $callbackReturn = false; + + if ( is_callable( $this->filterCallback ) ) { + $callbackReturn = call_user_func( + $this->filterCallback, + $name, + $attribs, + $data + ); + } + if ( $callbackReturn ) { + // Filter hit! + $this->filterMatch = true; + $this->filterMatchType = $callbackReturn; + } + } + + /** + * @param $data + */ + private function elementData( $data ) { + // Collect any data here, and we'll run the callback in elementClose + $this->elementData[ $this->stackDepth - 1 ] .= trim( $data ); + } + + /** + * @param $target + * @param $data + */ + private function processingInstructionHandler( $target, $data ) { + $callbackReturn = false; + if ( $this->parserOptions['processing_instruction_handler'] ) { + $callbackReturn = call_user_func( + $this->parserOptions['processing_instruction_handler'], + $target, + $data + ); + } + if ( $callbackReturn ) { + // Filter hit! + $this->filterMatch = true; + $this->filterMatchType = $callbackReturn; + } + } +} diff --git a/includes/libs/mime/defines.php b/includes/libs/mime/defines.php new file mode 100644 index 0000000000..ae0b5f8b61 --- /dev/null +++ b/includes/libs/mime/defines.php @@ -0,0 +1,46 @@ +get( $key, $curTTL, $checkKeys, $asOf ); // current value $value = $cValue; // return value - // Determine if a regeneration is desired + $preCallbackTime = microtime( true ); + // Determine if a cached value regeneration is needed or desired if ( $value !== false && $curTTL > 0 && $this->isValid( $value, $versioned, $asOf, $minTime ) && !$this->worthRefreshExpiring( $curTTL, $lowTTL ) - && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow ) + && !$this->worthRefreshPopular( $asOf, $ageNew, $popWindow, $preCallbackTime ) ) { return $value; } @@ -1013,8 +1014,10 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { } if ( $value !== false && $ttl >= 0 ) { - // Update the cache; this will fail if the key is tombstoned $setOpts['lockTSE'] = $lockTSE; + // Use best known "since" timestamp if not provided + $setOpts += [ 'since' => $preCallbackTime ]; + // Update the cache; this will fail if the key is tombstoned $this->set( $key, $value, $ttl, $setOpts ); } @@ -1046,7 +1049,7 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * Example usage: * @code * $rows = $cache->getMultiWithSetCallback( - * // Map of cache keys to entitiy IDs + * // Map of cache keys to entity IDs * $cache->makeMultiKeys( * $this->fileVersionIds(), * function ( $id, WANObjectCache $cache ) { @@ -1336,10 +1339,11 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface { * @param float $asOf UNIX timestamp of the value * @param integer $ageNew Age of key when this might recommend refreshing (seconds) * @param integer $timeTillRefresh Age of key when it should be refreshed if popular (seconds) + * @param float $now The current UNIX timestamp * @return bool */ - protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh ) { - $age = microtime( true ) - $asOf; + protected function worthRefreshPopular( $asOf, $ageNew, $timeTillRefresh, $now ) { + $age = $now - $asOf; $timeOld = $age - $ageNew; if ( $timeOld <= 0 ) { return false; diff --git a/includes/libs/rdbms/TransactionProfiler.php b/includes/libs/rdbms/TransactionProfiler.php index 12f6df5d4a..bf5e299865 100644 --- a/includes/libs/rdbms/TransactionProfiler.php +++ b/includes/libs/rdbms/TransactionProfiler.php @@ -80,11 +80,15 @@ class TransactionProfiler implements LoggerAwareInterface { } /** - * @param bool $value + * @param bool $value New value + * @return bool Old value * @since 1.28 */ public function setSilenced( $value ) { + $old = $this->silenced; $this->silenced = $value; + + return $old; } /** diff --git a/includes/libs/rdbms/database/Database.php b/includes/libs/rdbms/database/Database.php index fc55671d30..a5a170ba06 100644 --- a/includes/libs/rdbms/database/Database.php +++ b/includes/libs/rdbms/database/Database.php @@ -50,8 +50,8 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware /** @var string SQL query */ protected $mLastQuery = ''; - /** @var bool */ - protected $mDoneWrites = false; + /** @var float|bool UNIX timestamp of last write query */ + protected $mLastWriteTime = false; /** @var string|bool */ protected $mPHPError = false; /** @var string */ @@ -511,11 +511,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware } public function doneWrites() { - return (bool)$this->mDoneWrites; + return (bool)$this->mLastWriteTime; } public function lastDoneWrites() { - return $this->mDoneWrites ?: false; + return $this->mLastWriteTime ?: false; } public function writesPending() { @@ -820,7 +820,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware throw new DBReadOnlyError( $this, "Database is read-only: $reason" ); } # Set a flag indicating that writes have been done - $this->mDoneWrites = microtime( true ); + $this->mLastWriteTime = microtime( true ); } // Add trace comment to the begin of the sql string, right after the operator. @@ -2751,7 +2751,7 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware $writeTime = $this->pendingWriteQueryDuration( self::ESTIMATE_DB_APPLY ); $this->doCommit( $fname ); if ( $this->mTrxDoneWrites ) { - $this->mDoneWrites = microtime( true ); + $this->mLastWriteTime = microtime( true ); $this->trxProfiler->transactionWritingOut( $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime ); } diff --git a/includes/libs/rdbms/loadbalancer/LoadBalancer.php b/includes/libs/rdbms/loadbalancer/LoadBalancer.php index 083dcd68cd..32df19dcd7 100644 --- a/includes/libs/rdbms/loadbalancer/LoadBalancer.php +++ b/includes/libs/rdbms/loadbalancer/LoadBalancer.php @@ -1324,7 +1324,7 @@ class LoadBalancer implements ILoadBalancer { $cache->makeGlobalKey( __CLASS__, 'server-read-only', $masterServer ), self::TTL_CACHE_READONLY, function () use ( $domain, $conn ) { - $this->trxProfiler->setSilenced( true ); + $old = $this->trxProfiler->setSilenced( true ); try { $dbw = $conn ?: $this->getConnection( self::DB_MASTER, [], $domain ); $readOnly = (int)$dbw->serverIsReadOnly(); @@ -1334,7 +1334,7 @@ class LoadBalancer implements ILoadBalancer { } catch ( DBError $e ) { $readOnly = 0; } - $this->trxProfiler->setSilenced( false ); + $this->trxProfiler->setSilenced( $old ); return $readOnly; }, [ 'pcTTL' => $cache::TTL_PROC_LONG, 'busyValue' => 0 ] diff --git a/includes/mime.info b/includes/mime.info deleted file mode 100644 index b04d3c68a2..0000000000 --- a/includes/mime.info +++ /dev/null @@ -1,119 +0,0 @@ -# MIME type info file. -# the first MIME type in each line is the "main" MIME type, -# the others are aliases for this type -# the media type is given in upper case and square brackets, -# like [BITMAP], and must indicate a media type as defined by -# the MEDIATYPE_xxx constants in Defines.php - - -image/gif [BITMAP] -image/png image/x-png [BITMAP] -image/ief [BITMAP] -image/jpeg image/pjpeg [BITMAP] -image/jp2 [BITMAP] -image/xbm [BITMAP] -image/tiff [BITMAP] -image/x-icon image/x-ico image/vnd.microsoft.icon [BITMAP] -image/x-rgb [BITMAP] -image/x-portable-pixmap [BITMAP] -image/x-portable-graymap image/x-portable-greymap [BITMAP] -image/x-bmp image/x-ms-bmp image/bmp application/x-bmp application/bmp [BITMAP] -image/x-photoshop image/psd image/x-psd image/photoshop image/vnd.adobe.photoshop [BITMAP] -image/vnd.djvu image/x.djvu image/x-djvu [BITMAP] -image/webp [BITMAP] - -image/svg+xml application/svg+xml application/svg image/svg [DRAWING] -application/postscript [DRAWING] -application/x-latex [DRAWING] -application/x-tex [DRAWING] -application/x-dia-diagram [DRAWING] - - -audio/mpeg audio/mp3 audio/mpeg3 [AUDIO] -audio/mp4 [AUDIO] -audio/wav audio/x-wav audio/wave [AUDIO] -audio/midi audio/mid [AUDIO] -audio/basic [AUDIO] -audio/ogg [AUDIO] -audio/x-aiff [AUDIO] -audio/x-pn-realaudio [AUDIO] -audio/x-realaudio [AUDIO] -audio/webm [AUDIO] -audio/x-matroska [AUDIO] -audio/x-flac [AUDIO] -audio/flac [AUDIO] - -video/mpeg application/mpeg [VIDEO] -video/ogg [VIDEO] -video/x-sgi-video [VIDEO] -video/x-flv [VIDEO] -video/webm [VIDEO] -video/x-matroska [VIDEO] -video/mp4 [VIDEO] - -application/ogg application/x-ogg audio/ogg audio/x-ogg video/ogg video/x-ogg [MULTIMEDIA] - -application/x-shockwave-flash [MULTIMEDIA] -audio/x-pn-realaudio-plugin [MULTIMEDIA] -model/iges [MULTIMEDIA] -model/mesh [MULTIMEDIA] -model/vrml [MULTIMEDIA] -video/quicktime [MULTIMEDIA] -video/x-msvideo [MULTIMEDIA] - -text/plain [TEXT] -text/html application/xhtml+xml [TEXT] -application/xml text/xml [TEXT] -text [TEXT] -application/json [TEXT] -text/csv [TEXT] -text/tab-separated-values [TEXT] - -application/zip application/x-zip [ARCHIVE] -application/x-gzip [ARCHIVE] -application/x-bzip [ARCHIVE] -application/x-bzip2 [ARCHIVE] -application/x-tar [ARCHIVE] -application/x-stuffit [ARCHIVE] -application/x-opc+zip [ARCHIVE] -application/x-7z-compressed [ARCHIVE] - -application/javascript text/javascript application/x-javascript application/x-ecmascript text/ecmascript [EXECUTABLE] -application/x-bash [EXECUTABLE] -application/x-sh [EXECUTABLE] -application/x-csh [EXECUTABLE] -application/x-tcsh [EXECUTABLE] -application/x-tcl [EXECUTABLE] -application/x-perl [EXECUTABLE] -application/x-python [EXECUTABLE] - -application/pdf application/acrobat [OFFICE] -application/msword [OFFICE] -application/vnd.ms-excel [OFFICE] -application/vnd.ms-powerpoint [OFFICE] -application/x-director [OFFICE] -text/rtf [OFFICE] - -application/vnd.openxmlformats-officedocument.wordprocessingml.document [OFFICE] -application/vnd.openxmlformats-officedocument.wordprocessingml.template [OFFICE] -application/vnd.ms-word.document.macroEnabled.12 [OFFICE] -application/vnd.ms-word.template.macroEnabled.12 [OFFICE] -application/vnd.openxmlformats-officedocument.presentationml.template [OFFICE] -application/vnd.openxmlformats-officedocument.presentationml.slideshow [OFFICE] -application/vnd.openxmlformats-officedocument.presentationml.presentation [OFFICE] -application/vnd.ms-powerpoint.addin.macroEnabled.12 [OFFICE] -application/vnd.ms-powerpoint.presentation.macroEnabled.12 [OFFICE] -application/vnd.ms-powerpoint.presentation.macroEnabled.12 [OFFICE] -application/vnd.ms-powerpoint.slideshow.macroEnabled.12 [OFFICE] -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet [OFFICE] -application/vnd.openxmlformats-officedocument.spreadsheetml.template [OFFICE] -application/vnd.ms-excel.sheet.macroEnabled.12 [OFFICE] -application/vnd.ms-excel.template.macroEnabled.12 [OFFICE] -application/vnd.ms-excel.addin.macroEnabled.12 [OFFICE] -application/vnd.ms-excel.sheet.binary.macroEnabled.12 [OFFICE] -application/acad application/x-acad application/autocad_dwg image/x-dwg application/dwg application/x-dwg application/x-autocad image/vnd.dwg drawing/dwg [DRAWING] -chemical/x-mdl-molfile [DRAWING] -chemical/x-mdl-sdfile [DRAWING] -chemical/x-mdl-rxnfile [DRAWING] -chemical/x-mdl-rdfile [DRAWING] -chemical/x-mdl-rgfile [DRAWING] diff --git a/includes/mime.types b/includes/mime.types deleted file mode 100644 index b4f515af58..0000000000 --- a/includes/mime.types +++ /dev/null @@ -1,188 +0,0 @@ -application/acad dwg -application/andrew-inset ez -application/mac-binhex40 hqx -application/mac-compactpro cpt -application/mathml+xml mathml -application/msword doc dot -application/octet-stream bin dms lha lzh exe class so dll -application/oda oda -application/ogg ogx ogg ogm ogv oga spx opus -application/pdf pdf -application/postscript ai eps ps -application/rdf+xml rdf -application/smil smi smil -application/srgs gram -application/srgs+xml grxml -application/vnd.mif mif -application/vnd.ms-excel xls xlt xla -application/vnd.ms-powerpoint ppt pot pps ppa -application/vnd.wap.wbxml wbxml -application/vnd.wap.wmlc wmlc -application/vnd.wap.wmlscriptc wmlsc -application/voicexml+xml vxml -application/x-7z-compressed 7z -application/x-bcpio bcpio -application/x-bzip bz -application/x-bzip2 bz2 -application/x-cdlink vcd -application/x-chess-pgn pgn -application/x-cpio cpio -application/x-csh csh -application/x-dia-diagram dia -application/x-director dcr dir dxr -application/x-dvi dvi -application/x-futuresplash spl -application/x-gtar gtar tar -application/x-gzip gz -application/x-hdf hdf -application/x-jar jar -application/javascript js -application/json json -application/x-koan skp skd skt skm -application/x-latex latex -application/x-netcdf nc cdf -application/x-sh sh -application/x-shar shar -application/x-shockwave-flash swf -application/x-stuffit sit -application/x-sv4cpio sv4cpio -application/x-sv4crc sv4crc -application/x-tar tar -application/x-tcl tcl -application/x-tex tex -application/x-texinfo texinfo texi -application/x-troff t tr roff -application/x-troff-man man -application/x-troff-me me -application/x-troff-ms ms -application/x-ustar ustar -application/x-wais-source src -application/x-xpinstall xpi -application/xhtml+xml xhtml xht -application/xslt+xml xslt -application/xml xml xsl xsd kml -application/xml-dtd dtd -application/zip zip jar xpi sxc stc sxd std sxi sti sxm stm sxw stw -application/x-rar rar -application/font-woff woff -application/font-woff2 woff2 -application/vnd.ms-fontobject eot -application/x-font-ttf ttf -audio/basic au snd -audio/midi mid midi kar -audio/mpeg mpga mp2 mp3 -audio/ogg oga ogg spx opus -video/webm webm -audio/webm webm -audio/x-aiff aif aiff aifc -audio/x-matroska mka mkv -audio/x-mpegurl m3u -audio/x-ogg oga ogg spx opus -audio/x-pn-realaudio ram rm -audio/x-pn-realaudio-plugin rpm -audio/x-realaudio ra -audio/x-wav wav -audio/wav wav -audio/x-flac flac -audio/flac flac -chemical/x-pdb pdb -chemical/x-xyz xyz -image/bmp bmp -image/cgm cgm -image/gif gif -image/ief ief -image/jp2 j2k jp2 jpg2 -image/jpeg jpeg jpg jpe -image/png png apng -image/svg+xml svg -image/tiff tiff tif -image/vnd.djvu djvu djv -image/vnd.microsoft.icon ico -image/vnd.wap.wbmp wbmp -image/webp webp -image/x-cmu-raster ras -image/x-icon ico -image/x-ms-bmp bmp -image/x-portable-anymap pnm -image/x-portable-bitmap pbm -image/x-portable-graymap pgm -image/x-portable-pixmap ppm -image/x-rgb rgb -image/x-photoshop psd -image/x-xbitmap xbm -image/x-xpixmap xpm -image/x-xwindowdump xwd -model/iges igs iges -model/mesh msh mesh silo -model/vrml wrl vrml -text/calendar ics ifb -text/css css -text/csv csv -text/html html htm -text/plain txt -text/richtext rtx -text/rtf rtf -text/sgml sgml sgm -text/tab-separated-values tsv -text/vnd.wap.wml wml -text/vnd.wap.wmlscript wmls -text/xml xml xsl xslt rss rdf -text/x-component htc -text/x-setext etx -text/x-sawfish jl -video/mpeg mpeg mpg mpe -video/mp4 mp4 m4a m4p m4b m4r m4v -audio/mp4 m4a -video/ogg ogv ogm ogg -video/quicktime qt mov -video/vnd.mpegurl mxu -video/x-flv flv -video/x-matroska mkv mka -video/x-msvideo avi -video/x-ogg ogv ogm ogg -video/x-sgi-movie movie -x-conference/x-cooltalk ice -application/vnd.oasis.opendocument.chart odc -application/vnd.oasis.opendocument.chart-template otc -application/vnd.oasis.opendocument.database odb -application/vnd.oasis.opendocument.formula odf -application/vnd.oasis.opendocument.formula-template otf -application/vnd.oasis.opendocument.graphics odg -application/vnd.oasis.opendocument.graphics-template otg -application/vnd.oasis.opendocument.image odi -application/vnd.oasis.opendocument.image-template oti -application/vnd.oasis.opendocument.presentation odp -application/vnd.oasis.opendocument.presentation-template otp -application/vnd.oasis.opendocument.spreadsheet ods -application/vnd.oasis.opendocument.spreadsheet-template ots -application/vnd.oasis.opendocument.text odt -application/vnd.oasis.opendocument.text-master odm -application/vnd.oasis.opendocument.text-template ott -application/vnd.oasis.opendocument.text-web oth -application/vnd.openxmlformats-officedocument.wordprocessingml.document docx -application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx -application/vnd.ms-word.document.macroEnabled.12 docm -application/vnd.ms-word.template.macroEnabled.12 dotm -application/vnd.openxmlformats-officedocument.presentationml.template potx -application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx -application/vnd.openxmlformats-officedocument.presentationml.presentation pptx -application/vnd.ms-powerpoint.addin.macroEnabled.12 ppam -application/vnd.ms-powerpoint.presentation.macroEnabled.12 pptm -application/vnd.ms-powerpoint.presentation.macroEnabled.12 potm -application/vnd.ms-powerpoint.slideshow.macroEnabled.12 ppsm -application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx -application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx -application/vnd.ms-excel.sheet.macroEnabled.12 xlsm -application/vnd.ms-excel.template.macroEnabled.12 xltm -application/vnd.ms-excel.addin.macroEnabled.12 xlam -application/vnd.ms-excel.sheet.binary.macroEnabled.12 xlsb -model/vnd.dwfx+xps dwfx -application/vnd.ms-xpsdocument xps -application/x-opc+zip docx dotx docm dotm potx ppsx pptx ppam pptm potm ppsm xlsx xltx xlsm xltm xlam xlsb dwfx xps -chemical/x-mdl-molfile mol -chemical/x-mdl-sdfile sdf -chemical/x-mdl-rxnfile rxn -chemical/x-mdl-rdfile rd -chemical/x-mdl-rgfile rg -application/x-amf amf -application/sla stl diff --git a/includes/objectcache/ObjectCache.php b/includes/objectcache/ObjectCache.php index 8160a75ba6..53a474b6e3 100644 --- a/includes/objectcache/ObjectCache.php +++ b/includes/objectcache/ObjectCache.php @@ -291,24 +291,6 @@ class ObjectCache { return $cache; } - /** - * @param array $params [optional] Array key 'fallback' for $fallback. - * @param int|string $fallback Fallback cache, e.g. (CACHE_NONE, "hash") (since 1.24) - * @return BagOStuff - * @deprecated since 1.27 - */ - public static function newAccelerator( $params = [], $fallback = null ) { - if ( $fallback === null ) { - if ( is_array( $params ) && isset( $params['fallback'] ) ) { - $fallback = $params['fallback']; - } elseif ( !is_array( $params ) ) { - $fallback = $params; - } - } - - return self::getLocalServerInstance( $fallback ); - } - /** * Create a new cache object of the specified type. * diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index c791587781..3dc41fba58 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -121,6 +121,11 @@ class WikiPage implements Page, IDBAccessObject { throw new MWException( "Invalid or virtual namespace $ns given." ); } + $page = null; + if ( !Hooks::run( 'WikiPageFactory', [ $title, &$page ] ) ) { + return $page; + } + switch ( $ns ) { case NS_FILE: $page = new WikiFilePage( $title ); diff --git a/includes/parser/Parser.php b/includes/parser/Parser.php index a32acc2d36..f0898bad51 100644 --- a/includes/parser/Parser.php +++ b/includes/parser/Parser.php @@ -3784,9 +3784,28 @@ class Parser { * @return string */ public function extensionSubstitution( $params, $frame ) { + static $errorStr = ''; + static $errorLen = 20; + $name = $frame->expand( $params['name'] ); + if ( substr( $name, 0, $errorLen ) === $errorStr ) { + // Probably expansion depth or node count exceeded. Just punt the + // error up. + return $name; + } + $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] ); + if ( substr( $attrText, 0, $errorLen ) === $errorStr ) { + // See above + return $attrText; + } + $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] ); + if ( substr( $content, 0, $errorLen ) === $errorStr ) { + // See above + return $content; + } + $marker = self::MARKER_PREFIX . "-$name-" . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX; @@ -3844,6 +3863,10 @@ class Parser { $output = "<$name$attrText/>"; } else { $close = is_null( $params['close'] ) ? '' : $frame->expand( $params['close'] ); + if ( substr( $close, 0, $errorLen ) === $errorStr ) { + // See above + return $close; + } $output = "<$name$attrText>$content$close"; } } diff --git a/includes/skins/BaseTemplate.php b/includes/skins/BaseTemplate.php index 87865dfa0b..6ea8b89bbc 100644 --- a/includes/skins/BaseTemplate.php +++ b/includes/skins/BaseTemplate.php @@ -29,10 +29,11 @@ abstract class BaseTemplate extends QuickTemplate { * Get a Message object with its context set * * @param string $name Message name + * @param ... $params Message params * @return Message */ - public function getMsg( $name ) { - return $this->getSkin()->msg( $name ); + public function getMsg( $name /* ... */ ) { + return call_user_func_array( [ $this->getSkin(), 'msg' ], func_get_args() ); } function msg( $str ) { diff --git a/includes/specials/SpecialConfirmemail.php b/includes/specials/SpecialConfirmemail.php index 7b4e9db01e..f494b9d666 100644 --- a/includes/specials/SpecialConfirmemail.php +++ b/includes/specials/SpecialConfirmemail.php @@ -69,9 +69,9 @@ class EmailConfirmation extends UnlistedSpecialPage { $this->getOutput()->addWikiMsg( 'confirmemail_noemail' ); } } else { - $trxProfiler->setSilenced( true ); + $old = $trxProfiler->setSilenced( true ); $this->attemptConfirm( $code ); - $trxProfiler->setSilenced( false ); + $trxProfiler->setSilenced( $old ); } } diff --git a/includes/specials/SpecialEmailInvalidate.php b/includes/specials/SpecialEmailInvalidate.php index d2e3e7f132..c54abadd54 100644 --- a/includes/specials/SpecialEmailInvalidate.php +++ b/includes/specials/SpecialEmailInvalidate.php @@ -45,9 +45,9 @@ class EmailInvalidation extends UnlistedSpecialPage { $this->checkReadOnly(); $this->checkPermissions(); - $trxProfiler->setSilenced( true ); + $old = $trxProfiler->setSilenced( true ); $this->attemptInvalidate( $code ); - $trxProfiler->setSilenced( false ); + $trxProfiler->setSilenced( $old ); } /** diff --git a/includes/specials/SpecialRevisiondelete.php b/includes/specials/SpecialRevisiondelete.php index fcd4ab5133..dcaff4d9f1 100644 --- a/includes/specials/SpecialRevisiondelete.php +++ b/includes/specials/SpecialRevisiondelete.php @@ -106,7 +106,7 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { ]; public function __construct() { - parent::__construct( 'Revisiondelete', 'deletedhistory' ); + parent::__construct( 'Revisiondelete', 'deleterevision' ); } public function doesWrites() { @@ -210,17 +210,19 @@ class SpecialRevisionDelete extends UnlistedSpecialPage { $this->showForm(); } - $qc = $this->getLogQueryCond(); - # Show relevant lines from the deletion log - $deleteLogPage = new LogPage( 'delete' ); - $output->addHTML( "

" . $deleteLogPage->getName()->escaped() . "

\n" ); - LogEventsList::showLogExtract( - $output, - 'delete', - $this->targetObj, - '', /* user */ - [ 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ] - ); + if ( $user->isAllowed( 'deletedhistory' ) ) { + $qc = $this->getLogQueryCond(); + # Show relevant lines from the deletion log + $deleteLogPage = new LogPage( 'delete' ); + $output->addHTML( "

" . $deleteLogPage->getName()->escaped() . "

\n" ); + LogEventsList::showLogExtract( + $output, + 'delete', + $this->targetObj, + '', /* user */ + [ 'lim' => 25, 'conds' => $qc, 'useMaster' => $this->wasSaved ] + ); + } # Show relevant lines from the suppression log if ( $user->isAllowed( 'suppressionlog' ) ) { $suppressLogPage = new LogPage( 'suppress' ); diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 99f9c7c029..4824961c70 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -618,9 +618,10 @@ class SpecialWatchlist extends ChangesListSpecialPage { $form .= Xml::openElement( 'form', [ 'method' => 'get', - 'action' => $this->getPageTitle()->getLocalURL(), + 'action' => wfScript(), 'id' => 'mw-watchlist-form' ] ); + $form .= Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ); $form .= Xml::fieldset( $this->msg( 'watchlist-options' )->text(), false, diff --git a/includes/utils/MWGrants.php b/includes/utils/MWGrants.php deleted file mode 100644 index 58efdc7278..0000000000 --- a/includes/utils/MWGrants.php +++ /dev/null @@ -1,214 +0,0 @@ - array of rights - */ - public static function getRightsByGrant() { - global $wgGrantPermissions; - - $res = []; - foreach ( $wgGrantPermissions as $grant => $rights ) { - $res[$grant] = array_keys( array_filter( $rights ) ); - } - return $res; - } - - /** - * Fetch the display name of the grant - * @param string $grant - * @param Language|string|null $lang - * @return string Grant description - */ - public static function grantName( $grant, $lang = null ) { - // Give grep a chance to find the usages: - // grant-blockusers, grant-createeditmovepage, grant-delete, - // grant-editinterface, grant-editmycssjs, grant-editmywatchlist, - // grant-editpage, grant-editprotected, grant-highvolume, - // grant-oversight, grant-patrol, grant-protect, grant-rollback, - // grant-sendemail, grant-uploadeditmovefile, grant-uploadfile, - // grant-basic, grant-viewdeleted, grant-viewmywatchlist, - // grant-createaccount - $msg = wfMessage( "grant-$grant" ); - if ( $lang !== null ) { - if ( is_string( $lang ) ) { - $lang = Language::factory( $lang ); - } - $msg->inLanguage( $lang ); - } - if ( !$msg->exists() ) { - $msg = wfMessage( 'grant-generic', $grant ); - if ( $lang ) { - $msg->inLanguage( $lang ); - } - } - return $msg->text(); - } - - /** - * Fetch the display names for the grants. - * @param string[] $grants - * @param Language|string|null $lang - * @return string[] Corresponding grant descriptions - */ - public static function grantNames( array $grants, $lang = null ) { - if ( $lang !== null ) { - if ( is_string( $lang ) ) { - $lang = Language::factory( $lang ); - } - } - - $ret = []; - foreach ( $grants as $grant ) { - $ret[] = self::grantName( $grant, $lang ); - } - return $ret; - } - - /** - * Fetch the rights allowed by a set of grants. - * @param string[]|string $grants - * @return string[] - */ - public static function getGrantRights( $grants ) { - global $wgGrantPermissions; - - $rights = []; - foreach ( (array)$grants as $grant ) { - if ( isset( $wgGrantPermissions[$grant] ) ) { - $rights = array_merge( $rights, array_keys( array_filter( $wgGrantPermissions[$grant] ) ) ); - } - } - return array_unique( $rights ); - } - - /** - * Test that all grants in the list are known. - * @param string[] $grants - * @return bool - */ - public static function grantsAreValid( array $grants ) { - return array_diff( $grants, self::getValidGrants() ) === []; - } - - /** - * Divide the grants into groups. - * @param string[]|null $grantsFilter - * @return array Map of (group => (grant list)) - */ - public static function getGrantGroups( $grantsFilter = null ) { - global $wgGrantPermissions, $wgGrantPermissionGroups; - - if ( is_array( $grantsFilter ) ) { - $grantsFilter = array_flip( $grantsFilter ); - } - - $groups = []; - foreach ( $wgGrantPermissions as $grant => $rights ) { - if ( $grantsFilter !== null && !isset( $grantsFilter[$grant] ) ) { - continue; - } - if ( isset( $wgGrantPermissionGroups[$grant] ) ) { - $groups[$wgGrantPermissionGroups[$grant]][] = $grant; - } else { - $groups['other'][] = $grant; - } - } - - return $groups; - } - - /** - * Get the list of grants that are hidden and should always be granted - * @return string[] - */ - public static function getHiddenGrants() { - global $wgGrantPermissionGroups; - - $grants = []; - foreach ( $wgGrantPermissionGroups as $grant => $group ) { - if ( $group === 'hidden' ) { - $grants[] = $grant; - } - } - return $grants; - } - - /** - * Generate a link to Special:ListGrants for a particular grant name. - * - * This should be used to link end users to a full description of what - * rights they are giving when they authorize a grant. - * - * @param string $grant the grant name - * @param Language|string|null $lang - * @return string (proto-relative) HTML link - */ - public static function getGrantsLink( $grant, $lang = null ) { - return \Linker::linkKnown( - \SpecialPage::getTitleFor( 'Listgrants', false, $grant ), - htmlspecialchars( self::grantName( $grant, $lang ) ) - ); - } - - /** - * Generate wikitext to display a list of grants - * @param string[]|null $grantsFilter If non-null, only display these grants. - * @param Language|string|null $lang - * @return string Wikitext - */ - public static function getGrantsWikiText( $grantsFilter, $lang = null ) { - global $wgContLang; - - if ( is_string( $lang ) ) { - $lang = Language::factory( $lang ); - } elseif ( $lang === null ) { - $lang = $wgContLang; - } - - $s = ''; - foreach ( self::getGrantGroups( $grantsFilter ) as $group => $grants ) { - if ( $group === 'hidden' ) { - continue; // implicitly granted - } - $s .= "*" . - wfMessage( "grant-group-$group" )->inLanguage( $lang )->text() . "\n"; - $s .= ":" . $lang->semicolonList( self::grantNames( $grants, $lang ) ) . "\n"; - } - return "$s\n"; - } - -} diff --git a/languages/Language.php b/languages/Language.php index 7354155e51..7ef2effb98 100644 --- a/languages/Language.php +++ b/languages/Language.php @@ -4461,14 +4461,15 @@ class Language { } /** - * @todo Document + * Formats a time given in seconds into a string representation of that time. + * * @param int|float $seconds - * @param array $format Optional - * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour. - * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours. + * @param array $format An optional argument that formats the returned string in different ways: + * If $format['avoid'] === 'avoidseconds': don't show seconds if $seconds >= 1 hour, + * If $format['avoid'] === 'avoidminutes': don't show seconds/minutes if $seconds > 48 hours, * If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev' * and friends. - * For backwards compatibility, $format may also be one of the strings 'avoidseconds' + * @note For backwards compatibility, $format may also be one of the strings 'avoidseconds' * or 'avoidminutes'. * @return string */ diff --git a/languages/i18n/an.json b/languages/i18n/an.json index 092ecc416c..75e8de806a 100644 --- a/languages/i18n/an.json +++ b/languages/i18n/an.json @@ -466,6 +466,7 @@ "minoredit": "He feito una edición menor", "watchthis": "Cosirar ista pachina", "savearticle": "Alzar pachina", + "publishpage": "Publicar a pachina", "publishchanges": "Publicar os cambeos", "preview": "Previsualización", "showpreview": "Amostrar previsualización", diff --git a/languages/i18n/ar.json b/languages/i18n/ar.json index 04b736920d..5b16b7e8b8 100644 --- a/languages/i18n/ar.json +++ b/languages/i18n/ar.json @@ -3552,7 +3552,7 @@ "htmlform-user-not-exists": "$1 غير موجود", "htmlform-user-not-valid": "اسم المستخدم $1 غير صالح.", "logentry-delete-delete": "{{GENDER:$2|حذف|حذفت}} $1 صفحة $3", - "logentry-delete-restore": "{{GENDER:$2|استعاد|استعادت}} $1 صفحة $3", + "logentry-delete-restore": "{{GENDER:$2|استرجع|استرجعت}} $1 صفحة $3", "logentry-delete-event": "{{GENDER:$2|غيّر|غيّرت}} $1 إمكانية مشاهدة {{PLURAL:$5||حدث|حدثين|$5 أحداث|$5 حدثًا|$5 حدث}} في سجل $3: $4", "logentry-delete-revision": "غيّر{{GENDER:$2||ت}} $1 إمكانية مشاهدة {{PLURAL:$5||مراجعة واحدة|مراجعتين|$5 مراجعات|$5 مراجعة}} في صفحة $3: $4", "logentry-delete-event-legacy": "{{GENDER:$2|غيّر|غيّرت}} $1 إمكانية رؤية أحداث في سجل $3", diff --git a/languages/i18n/ba.json b/languages/i18n/ba.json index a88feea508..733cb953c6 100644 --- a/languages/i18n/ba.json +++ b/languages/i18n/ba.json @@ -1533,7 +1533,7 @@ "upload-curl-error28": "Көтөү ваҡыты үтте", "upload-curl-error28-text": "\nСайт бигерәк оҙаҡ яуап бирмәй.\nЗинһар, сайттың эшләүен тикшерегеҙ һәм, бер аҙ көткәндән һуң, яңынан ҡабатлап ҡарағыҙ.\nБәлки, һеҙгә сайт бушыраҡ саҡта ҡабатлап ҡарарға кәрәктер.", "license": "Рөхсәтнамә:", - "license-header": "Рөхсәтнәмә", + "license-header": "Рөхсәтнамә", "nolicense": "Бер нимә лә һайланмаған", "licenses-edit": "Лицензия параметрҙарын үҙгәртергә", "license-nopreview": "(Ҡарап сығыу мөмкин түгел)", diff --git a/languages/i18n/be-tarask.json b/languages/i18n/be-tarask.json index c3352562a5..95071ab4d6 100644 --- a/languages/i18n/be-tarask.json +++ b/languages/i18n/be-tarask.json @@ -669,8 +669,8 @@ "anontalkpagetext": "----\nГэта старонка гутарак ананімнага ўдзельніка, які яшчэ не стварыў сабе рахунак альбо не ўжывае яго.\nТаму мы вымушаныя ўжываць лічбавы IP-адрас дзеля ягонай ідэнтыфікацыі. Адзін IP-адрас можа выкарыстоўвацца некалькімі ўдзельнікамі. Калі Вы — ананімны ўдзельнік і лічыце, што атрымалі не прызначаныя Вам камэнтары, калі ласка, [[Special:CreateAccount|стварыце рахунак]] альбо [[Special:UserLogin|ўвайдзіце ў сыстэму]], каб у будучыні пазьбегнуць магчымай блытаніны зь іншымі ананімнымі ўдзельнікамі.", "noarticletext": "Цяпер тэкст на гэтай старонцы адсутнічае.\nВы можаце [[Special:Search/{{PAGENAME}}|пашукаць гэтую назву]] сярод іншых старонак, [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пашукаць у адпаведных журналах падзеяў]\nальбо [{{fullurl:{{FULLPAGENAME}}|action=edit}} стварыць гэтую старонку].", "noarticletext-nopermission": "Цяпер на гэтай старонцы тэкст адсутнічае.\nВы можаце [[Special:Search/{{PAGENAME}}|пашукаць назву гэтай старонкі]] на іншых старонках, альбо [{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} пашукаць зьвязаныя запісы ў журналах], але ў вас няма дазволу ствараць гэтую старонку.", - "missing-revision": "Вэрсія старонкі №$1 з назвай «{{FULLPAGENAME}}» не існуе.\n\nЗвычайна гэта здараецца з-за перахода па састарэлай спасылцы на старонку, якая была выдаленая.\nПадрабязнасьці можна знайсьці ў [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале выдаленьняў].", - "userpage-userdoesnotexist": "Рахунак удзельніка «$1» не зарэгістраваны. Калі ласка, удакладніце, ці жадаеце Вы стварыць/рэдагаваць гэтую старонку.", + "missing-revision": "Вэрсія старонкі №$1 з назвай «{{FULLPAGENAME}}» не існуе.\n\nЗвычайна гэта здараецца з-за пераходу па састарэлай спасылцы на старонку, якая была выдаленая.\nПадрабязнасьці можна знайсьці ў [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} журнале выдаленьняў].", + "userpage-userdoesnotexist": "Рахунак удзельніка «$1» не зарэгістраваны. Калі ласка, удакладніце, ці жадаеце Вы стварыць/рэдагаваць гэтую старонку.", "userpage-userdoesnotexist-view": "Рахунак «$1» ня створаны.", "blocked-notice-logextract": "Гэты ўдзельнік у дадзены момант заблякаваны.\nАпошні запіс з журналу блякаваньняў пададзены ніжэй для даведкі:", "clearyourcache": "Заўвага: каб пабачыць зьмены пасьля захаваньня, Вам можа спатрэбіцца ачысьціць кэш Вашага браўзэра. \n* Firefox / Safari: трымайце Shift і націсьніце Reload, ці націсьніце Ctrl-F5 ці Ctrl-R (⌘-R на Mac)\n* Google Chrome: націсьніце Ctrl-Shift-R (⌘-Shift-R на Mac)\n* Internet Explorer: трымайце Ctrl і націсьніце Refresh, ці націсьніце Ctrl-F5\n* Opera: перайдзіце ў Menu → Settings (Opera → Preferences на Mac), а потым у Privacy & security → Clear browsing data → Cached images and files.", @@ -3678,7 +3678,11 @@ "log-action-filter-managetags-deactivate": "Дэактывацыя метак", "log-action-filter-move-move": "Перанос безь перазапісу перанакіраваньняў", "log-action-filter-move-move_redir": "Перанос зь перазапісам перанакіраваньняў", + "log-action-filter-newusers-create": "Створаны ананімным удзельнікам", + "log-action-filter-newusers-create2": "Створаны зарэгістраваным удзельнікам", "log-action-filter-newusers-autocreate": "Аўтаматычнае стварэньне", + "log-action-filter-newusers-byemail": "Створаны паролем, дасланым электроннай поштай", + "log-action-filter-patrol-patrol": "Ручное патруляваньне", "log-action-filter-patrol-autopatrol": "Аўтаматычнае патруляваньне", "log-action-filter-protect-protect": "Абарона", "log-action-filter-protect-unprotect": "Зьняцьце абароны", diff --git a/languages/i18n/bg.json b/languages/i18n/bg.json index 67965f505c..a5eecb0bdf 100644 --- a/languages/i18n/bg.json +++ b/languages/i18n/bg.json @@ -36,7 +36,8 @@ "Ket", "Ricordo.tenerissimo", "Plamen", - "Iliev" + "Iliev", + "Spas.Z.Spasov" ] }, "tog-underline": "Подчертаване на препратките:", @@ -368,7 +369,7 @@ "title-invalid-interwiki": "Желаното заглавие на страница съдържа препратка към друго уики, което не може да бъде ползвано в заглавия.", "title-invalid-talk-namespace": "Желаното заглавие на страница се отнася към беседа, която не съществува", "title-invalid-characters": "Желаното заглавие на статия съдържа невалидни знаци: „$1“", - "title-invalid-relative": "Заглавието съдържа относителен път. Относителни заглавия на статии (./,../) са невалидни, защото често ще са недостижимо, когато биват извиквани от браузъра на потребителя.", + "title-invalid-relative": "Заглавието съдържа относителен път. Относителните заглавия на статии (./,../) са невалидни, защото често са недостижими, когато биват обработвани от браузъра на потребителя.", "title-invalid-magic-tilde": "Желаното заглавие на статия съдържа невалидна поредица от тилди (~~~).", "title-invalid-too-long": "Желаното заглавие на статия е твърде дълго. Трябва да е не по-дълго от $1 {{PLURAL:$1|байт|байта}} в кодиране UTF-8.", "title-invalid-leading-colon": "Желаното заглавие на статия съдържа невалидно двоеточие в началото.", @@ -405,6 +406,7 @@ "virus-scanfailed": "сканирането не сполучи (код $1)", "virus-unknownscanner": "непознат антивирус:", "logouttext": "'''Излязохте от системата.'''\n\nОбърнете внимание, че някои страници все още ще се показват така, сякаш сте влезли, докато не изтриете кеша на браузъра.", + "cannotlogoutnow-title": "Не може да излезете сега.", "welcomeuser": "Здравейте, $1!", "welcomecreation-msg": "Вашата сметка беше създадена.\nМожете да промените [[Special:Preferences|настройките на {{SITENAME}}]] според предпочитанията си.", "yourname": "Потребителско име:", @@ -420,6 +422,10 @@ "createacct-yourpasswordagain-ph": "Въвежда се паролата (повторно)", "userlogin-remembermypassword": "Запомняне", "userlogin-signwithsecure": "Използване на защитена връзка", + "cannotlogin-title": "Не може да влезете в", + "cannotlogin-text": "Влизането в системата не е възможно.", + "cannotloginnow-title": "Не може да влезете сега", + "cannotcreateaccount-title": "Невъзможно е да бъде създадена потребителска сметка", "yourdomainname": "Домейн:", "password-change-forbidden": "Не можете да променяте пароли в това уики.", "externaldberror": "Или е станала грешка в базата от данни при външното удостоверяване, или не ви е позволено да обновявате външната си сметка.", @@ -492,7 +498,7 @@ "eauthentsent": "Писмото за потвърждение е изпратено на посочения адрес. В него са описани действията, които трябва да се извършат, за да потвърдите, че този адрес за електронна поща действително е ваш.", "throttled-mailpassword": "Функцията за напомняне на паролата е използвана през {{PLURAL:$1|последния един час|последните $1 часа}}.\nЗа предотвратяване на злоупотреби е разрешено да се изпраща не повече от едно напомняне в рамките на {{PLURAL:$1|един час|$1 часа}}.", "mailerror": "Грешка при изпращане на писмо: $1", - "acct_creation_throttle_hit": "През последното денонощие, през този IP-адрес посетители на това уики са създали {{PLURAL:$1|1 сметка |$1 сметки}}, което е максималният допустим брой за този период.\nВ резултат, към момента не могат да създават повече потребителски сметки през този IP-адрес.", + "acct_creation_throttle_hit": "През последните $2, през този IP-адрес посетители на това уики са създали {{PLURAL:$1|1 сметка |$1 сметки}}, което е максималният допустим брой за този период.\nВ резултат, към момента не могат да създават повече потребителски сметки през този IP-адрес.", "emailauthenticated": "Адресът на електронната ви поща беше потвърден на $2 в $3.", "emailnotauthenticated": "Адресът на електронната ви поща все още не е потвърден.\nНяма да получавате писма за никоя от следните възможности.", "noemailprefs": "За да работят тези функционалности, трябва да посочите адрес на електронна поща в своите настройки.", @@ -743,6 +749,7 @@ "undo-success": "Редакцията може да бъде върната. Прегледайте долното сравнение и се уверете, че наистина искате да го направите. След това съхранете страницата, за да извършите връщането.", "undo-failure": "Редакцията не може да бъде върната поради конфликтни междинни редакции.", "undo-norev": "Редакцията не може да бъде върната, тъй като не съществува или е била изтрита.", + "undo-nochange": "Тази редакция изглежда вече е отменена.", "undo-summary": "Премахната редакция $1 на [[Special:Contributions/$2|$2]] ([[User talk:$2|беседа]])", "undo-summary-username-hidden": "Отмяна на редакция $1 от скрит потребител", "cantcreateaccount-text": "[[User:$3|Потребител:$3]] е блокирал(а) създаването на сметки от този IP-адрес ('''$1''').\n\nПричината, изложена от $3, е ''$2''", @@ -791,13 +798,15 @@ "rev-showdeleted": "показване", "revisiondelete": "Изтриване/възстановяване на версии", "revdelete-nooldid-title": "Не е зададена версия", - "revdelete-nooldid-text": "Не сте задали версия или версии за изпълнението на тази функция.", + "revdelete-nooldid-text": "Не сте задали целева версия за изпълнението на тази функция или определената версия не съществува, или се опитвате да скриете настоящата версия.", "revdelete-no-file": "Посоченият файл не съществува.", "revdelete-show-file-confirm": "Необходимо е потвърждение, че желаете да прегледате изтритата версия на файла „$1“ от $2 $3.", "revdelete-show-file-submit": "Да", "revdelete-selected-text": "{{PLURAL:$1|Избрана версия|Избрани версии}} от [[:$2]]:", "logdelete-selected": "{{PLURAL:$1|Избрано събитие|Избрани събития}}:", "revdelete-text-text": "Изтритите редакции ще продължат да се виждат в историята на страницата, но части от съдържанието ще бъдат публично недостъпни.", + "revdelete-text-file": "Изтритите файлови редакции ще продължат да се виждат в историята на страницата, но части от съдържанието им ще бъдат публично недостъпни.", + "logdelete-text": "Изтриват записи в дневника ще продължат да се виждат в дневниците, но част от тяхното съдържание ще бъде недостъпно за обществеността.", "revdelete-text-others": "Другите администратори ще продължат да имат достъп до скритото съдържание и могат да го възстановят, освен ако не бъдат наложени допълнителни ограничения.", "revdelete-confirm": "Необходимо е да потвърдите, че желаете да извършите действието, разбирате последствията и го правите според [[{{MediaWiki:Policy-url}}|политиката]].", "revdelete-suppress-text": "Премахването трябва да се използва '''само''' при следните случаи:\n* Потенциално уязвима в правно отношение информация\n* Неподходяща лична информация\n*: ''домашни адреси и телефонни номера, номера за социално осигуряване и др.''", @@ -1003,7 +1012,7 @@ "badsig": "Избраният подпис не е валиден. Проверете HTML-етикетите!", "badsiglength": "Вашият подпис е твърде дълъг.\nПодписите не могат да надвишават $1 {{PLURAL:$1|знак|знака}}.", "yourgender": "Какво описание Ви подхожда най-много?", - "gender-unknown": "Предпочитам да не посоча", + "gender-unknown": "Когато ви споменава, софтуерът ще използва неутрални думи за пол, когато е възможно", "gender-male": "Той редактира уики страниците", "gender-female": "Тя редактира уики страниците", "prefs-help-gender": "По желание: използва се за коректно обръщение по род в системните съобщения на софтуера. Тази информация е публично достъпна.", @@ -1135,6 +1144,7 @@ "right-sendemail": "Изпращане на е-писма до другите потребители", "right-passwordreset": "Преглеждане на е-писма за възстановяване на парола", "grant-group-email": "Изпращане на е-писмо", + "grant-blockusers": "Блокиране и отблокиране на потребители", "grant-createaccount": "Създаване на сметки", "grant-createeditmovepage": "Създаване, редактиране и преместване на страници", "grant-delete": "Изтриване на страници, редакции и записи в дневника", @@ -1143,6 +1153,7 @@ "grant-editmywatchlist": "редактиране на списъка ви за наблюдение", "grant-editpage": "Редактиране на съществуващи страници", "grant-editprotected": "Редактиране на защитени страници", + "grant-sendemail": "Изпращане на имейл до други потребители", "grant-uploadeditmovefile": "Качване, заменяне и прехвърляне на файлове", "grant-uploadfile": "Качване на нови файлове", "grant-basic": "Основни права", @@ -1351,6 +1362,7 @@ "upload-http-error": "Възникна HTTP грешка: $1", "upload-dialog-title": "Качване на файл", "upload-dialog-button-cancel": "Отказване", + "upload-dialog-button-back": "Обратно", "upload-dialog-button-done": "Готово", "upload-dialog-button-save": "Съхраняване", "upload-dialog-button-upload": "Качване", @@ -1620,6 +1632,7 @@ "apihelp-no-such-module": "Модул \"$1\" не беше намерен.", "apisandbox": "Пясъчник за API", "apisandbox-fullscreen": "Разшири полето", + "apisandbox-unfullscreen": "Показване на страница", "apisandbox-submit": "Направи запитване", "apisandbox-reset": "Изчистване", "apisandbox-retry": "Повторен опит", @@ -1634,6 +1647,8 @@ "apisandbox-dynamic-error-exists": "Параметър с име \"$1\" вече съществува.", "apisandbox-results": "Резултати", "apisandbox-request-url-label": "URL-адрес на заявката:", + "apisandbox-continue": "Продължаване", + "apisandbox-continue-clear": "Изчистване", "booksources": "Източници на книги", "booksources-search-legend": "Търсене на информация за книга", "booksources-search": "Търсене", @@ -3183,7 +3198,7 @@ "mediastatistics-table-totalbytes": "Общ размер", "mediastatistics-header-unknown": "Неизвестно", "mediastatistics-header-bitmap": "Растерни изображения", - "mediastatistics-header-drawing": "Рисунки (векторни изображения)", + "mediastatistics-header-drawing": "Чертежи (векторни изображения)", "mediastatistics-header-audio": "Аудио", "mediastatistics-header-video": "Видео", "mediastatistics-header-multimedia": "Мултимедия", diff --git a/languages/i18n/de.json b/languages/i18n/de.json index fa332e7e78..445a13df2e 100644 --- a/languages/i18n/de.json +++ b/languages/i18n/de.json @@ -2774,6 +2774,7 @@ "newimages-showbots": "Von Bots hochgeladene Dateien anzeigen", "newimages-hidepatrolled": "Kontrollierte Dateien ausblenden", "noimages": "Keine Dateien gefunden.", + "gallery-slideshow-toggle": "Vorschaubilder umschalten", "ilsubmit": "Suchen", "bydate": "nach Datum", "sp-newimages-showfrom": "Zeige neue Dateien ab $1, $2 Uhr", @@ -3580,7 +3581,7 @@ "feedback-thanks": "Vielen Dank. Deine Rückmeldung wurde auf der Seite „[$2 $1]“ gespeichert.", "feedback-thanks-title": "Danke!", "feedback-useragent": "User Agent:", - "searchsuggest-search": "Suchen", + "searchsuggest-search": "{{SITENAME}} durchsuchen", "searchsuggest-containing": "enthält …", "api-error-autoblocked": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem gesperrten Benutzer verwendet wurde.", "api-error-badaccess-groups": "Du hast nicht die Berechtigung Dateien in dieses Wiki hochzuladen.", diff --git a/languages/i18n/dty.json b/languages/i18n/dty.json index 75cf4eaba3..2a4bf50b7f 100644 --- a/languages/i18n/dty.json +++ b/languages/i18n/dty.json @@ -458,7 +458,7 @@ "wrongpassword": "पासवर्ड गलत हालियो।\nकृपया आजी प्रयास गरया।", "wrongpasswordempty": "हालिएया पासवर्ड खालि थ्यो।\nकृपया आजी प्रयास गरया।", "password-name-match": "तमरो प्रवेशशव्द प्रयोगकर्ता नाम है फरक हुनपडन्छ ।", - "password-login-forbidden": "ये प्रयोगकर्ता नाम र प्रवेश शव्द वर्जित गरिया छ।", + "password-login-forbidden": "ये प्रयोगकर्ता नाम र प्रवेश शब्द वर्जित गरिया छ।", "mailmypassword": "पासवर्ड पूर्वनिर्धारित गर", "passwordremindertitle": "{{SITENAME}}का लागि नयाँ अस्थायी पासवर्ड", "passwordremindertext": "कसैले (सायद तमी, IP ठेगाना $1 बाट), {{SITENAME}}($4) को लागि नौलो पासवर्ड अनुरोध गर्या छ । प्रयोगकर्ता \"$2\" को लागि नौलो अस्थायी पासवर्ड \"$3\"तयार पारिया छ । यदि यो तमरो इच्छामी भयाको भया अहिले तमीले लगइन गरीबर नौलो पासवर्ड छान्नु पड्ड्या हुन्छ ।\nतमरो अस्थायी पासवर्ड {{PLURAL:$5|एक दिन|$5 दिनहरू पछि}} अमान्य हुन्याछ ।\n\nयदि कोही अरुले नै अनुरोध गर्याको हो भण्या , या तमीले आफ्नो पासवर्ड सम्झ्यौ भण्या, अथवा\nत्यैलाई परिवर्तन गर्न चाहन्नौ भण्या, तमीले यो सन्देसको वेवास्ता गद्दसक्द्याहौ र पुरानै पासवर्ड प्रयोग गरिरहन सक्द्याहौ ।", diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 30adf58f60..4107b9e67f 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -4227,5 +4227,7 @@ "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users.", "restrictionsfield-badip": "Invalid IP address or range: $1", "restrictionsfield-label": "Allowed IP ranges:", - "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use
0.0.0.0/0
::/0" + "restrictionsfield-help": "One IP address or CIDR range per line. To enable everything, use
0.0.0.0/0
::/0", + "edit-error-short": "Error: $1", + "edit-error-long": "Errors:\n\n$1" } diff --git a/languages/i18n/es.json b/languages/i18n/es.json index 50ba85971f..efd19f39d6 100644 --- a/languages/i18n/es.json +++ b/languages/i18n/es.json @@ -545,6 +545,7 @@ "userlogin-remembermypassword": "Mantener mi sesión iniciada", "userlogin-signwithsecure": "Usar conexión segura", "cannotlogin-title": "No se puede iniciar sesión", + "cannotlogin-text": "No ha sido posible iniciar sesión.", "cannotloginnow-title": "No se puede iniciar sesión ahora", "cannotloginnow-text": "No se puede iniciar sesión cuando se usa $1.", "cannotcreateaccount-title": "No se pueden crear cuentas", @@ -626,7 +627,7 @@ "eauthentsent": "Se ha enviado un correo electrónico de confirmación a la dirección especificada.\nAntes de que se envíe cualquier otro correo a la cuenta tienes que seguir las instrucciones enviadas en el mensaje para así confirmar que la dirección te pertenece.", "throttled-mailpassword": "Ya se ha enviado un recordatorio de contraseña en {{PLURAL:$1|la última hora|las últimas $1 horas}}.\nPara evitar los abusos, solo se enviará un recordatorio de contraseña cada {{PLURAL:$1|hora|$1 horas}}.", "mailerror": "Error al enviar el mensaje: $1", - "acct_creation_throttle_hit": "Los visitantes a este wiki usando tu dirección IP han creado {{PLURAL:$1|una cuenta|$1 cuentas}} en el último día, lo cual es lo máximo permitido en este periodo de tiempo.\nComo resultado, los visitantes usando esta dirección IP no pueden crear más cuentas en este momento.", + "acct_creation_throttle_hit": "Los visitantes a este wiki usando tu dirección IP han creado {{PLURAL:$1|una cuenta|$1 cuentas}} en el último $2, lo cual es lo máximo permitido en este periodo de tiempo.\nComo resultado, los visitantes usando esta dirección IP no pueden crear más cuentas en este momento.", "emailauthenticated": "Tu dirección de correo electrónico fue confirmada el $2 a las $3.", "emailnotauthenticated": "Aún no has confirmado tu dirección de correo electrónico.\nHasta que lo hagas, las siguientes funciones no estarán disponibles.", "noemailprefs": "Especifica una dirección electrónica para habilitar estas características.", @@ -877,6 +878,8 @@ "invalid-content-data": "Datos de contenido incorrectos", "content-not-allowed-here": "El contenido «$1» no está permitido en la página [[$2]]", "editwarning-warning": "Se perderán los cambios si se cierra esta página.\nSi has iniciado sesión, puedes desactivar este aviso en la sección «{{int:prefs-editing}}» de las preferencias.", + "editpage-invalidcontentmodel-title": "Modelo de contenido no soportado", + "editpage-invalidcontentmodel-text": "El modelo de contenido \"$1\" no se admite.", "editpage-notsupportedcontentformat-title": "Formato de contenido no compatible", "editpage-notsupportedcontentformat-text": "El formato de contenido $1 no es compatible con el modelo de contenido $2.", "content-model-wikitext": "texto wiki", @@ -2265,7 +2268,7 @@ "undeletedrevisions": "{{PLURAL:$1|Una revisión restaurada|$1 revisiones restauradas}}", "undeletedrevisions-files": "{{PLURAL:$1|1 revisión|$1 revisiones}} y {{PLURAL:$2|1 archivo|$2 archivos}} restaurados", "undeletedfiles": "$1 {{PLURAL:$1|archivo restaurado|archivos restaurados}}", - "cannotundelete": "Hubo un error durante la restauración:\n$1", + "cannotundelete": "Hubo un error en la totalidad o en parte del proceso de la restauración:\n$1", "undeletedpage": "Se ha restaurado $1\n\nConsulta el [[Special:Log/delete|registro de borrados]] para ver una lista de los últimos borrados y restauraciones.", "undelete-header": "En el [[Special:Log/delete|registro de borrados]] se listan las páginas eliminadas.", "undelete-search-title": "Buscar páginas borradas", @@ -3387,6 +3390,8 @@ "tag-filter": "Filtro de [[Special:Tags|etiquetas]]:", "tag-filter-submit": "Filtro", "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Etiqueta|Etiquetas}}]]: $2)", + "tag-mw-contentmodelchange": "cambio de modelo de contenido", + "tag-mw-contentmodelchange-description": "Ediciones que [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel cambian el modelo de contenido] de una página", "tags-title": "Etiquetas", "tags-intro": "Esta página lista las etiquetas con las que el software puede marcar una edición y su significado.", "tags-tag": "Nombre de etiqueta", @@ -3906,5 +3911,7 @@ "unlinkaccounts-success": "Se ha desvinculado la cuenta.", "authenticationdatachange-ignored": "El cambio den los datos de autentificacion no fue realizado. ¿Tal vez, no se configuró un proveedor?", "userjsispublic": "Recuerda: las subpáginas JavaScript no deberían contener datos confidenciales, pues otros usuarios los pueden ver.", - "usercssispublic": "Recuerda: las subpáginas CSS no deberían contener datos confidenciales, pues otros usuarios los pueden ver." + "usercssispublic": "Recuerda: las subpáginas CSS no deberían contener datos confidenciales, pues otros usuarios los pueden ver.", + "restrictionsfield-badip": "Dirección IP o rango inválido: $1", + "restrictionsfield-label": "Rangos de IP permitidos:" } diff --git a/languages/i18n/fr.json b/languages/i18n/fr.json index 67238405ba..88b0b5f0d2 100644 --- a/languages/i18n/fr.json +++ b/languages/i18n/fr.json @@ -987,7 +987,7 @@ "revdelete-radio-same": "(ne pas changer)", "revdelete-radio-set": "Masqué", "revdelete-radio-unset": "Visible", - "revdelete-suppress": "Supprimer également les données des administrateurs", + "revdelete-suppress": "Masquer également les données pour les administrateurs", "revdelete-unsuppress": "Enlever les restrictions sur les versions restaurées", "revdelete-log": "Motif :", "revdelete-submit": "Appliquer {{PLURAL:$1|à la révision sélectionnée|aux révisions sélectionnées}}", @@ -2833,6 +2833,7 @@ "newimages-showbots": "Afficher les imports faits par des robots", "newimages-hidepatrolled": "Masquer les téléchargements patrouillés", "noimages": "Aucune image à afficher.", + "gallery-slideshow-toggle": "Basculer les vignettes", "ilsubmit": "Rechercher", "bydate": "par date", "sp-newimages-showfrom": "Afficher les nouveaux fichiers à partir du $1 à $2", @@ -3517,7 +3518,7 @@ "tags-delete-title": "Supprimer la balise", "tags-delete-explanation-initial": "Vous êtes sur le point de supprimer la balise « $1 » de la base de données.", "tags-delete-explanation-in-use": "Elle sera supprimée de {{PLURAL:$2|$2 révision ou entrée de journal à laquelle|toutes les $2 révisions et/ou entrées de journal auxquelles}} elle est actuellement appliquée.", - "tags-delete-explanation-warning": "Cette action est irréversible et ne peut pas être annulée, même pas par les administrateurs de base de données. Soyez certain que c'est la balise que vous voulez supprimer.", + "tags-delete-explanation-warning": "Cette action est irréversible et ne peut pas être annulée, même pas par les administrateurs de base de données. Soyez certain que c'est cette balise que vous voulez supprimer.", "tags-delete-explanation-active": "La balise « $1 » est toujours active, et continuera à être appliquée dans le futur. Pour arrêter cela, allez à l'endroit (ou aux endroits) où la balise est appliquée, et désactivez la.", "tags-delete-reason": "Motif :", "tags-delete-submit": "Supprimer cette balise de manière irréversible", @@ -3674,7 +3675,7 @@ "log-description-managetags": "Cette page recense les tâches de maintenance liées aux [[Special:Tags|balises]]. Le journal contient uniquement les actions faites manuellement par un administrateur ; les balises peuvent être créées ou supprimées par le logiciel wiki sans que cette action ne soit inscrite dans ce journal.", "logentry-managetags-create": "$1 {{GENDER:$2|a créé}} la balise « $4 ».", "logentry-managetags-delete": "$1 {{GENDER:$2|a supprimé}} la balise « $4 » (retirée {{PLURAL:$5|d'une révision ou entrée de journal|de $5 révisions ou entrées de journal}})", - "logentry-managetags-activate": "$1 {{GENDER:$2|a activé}} la balise \"$4\" pour l’usage des utilisateurs et des robots", + "logentry-managetags-activate": "$1 {{GENDER:$2|a activé}} la balise « $4 » pour l’usage des utilisateurs et des robots", "logentry-managetags-deactivate": "$1 {{GENDER:$2|a désactivé}} la balise « $4 » pour l’usage des utilisateurs et des robots", "log-name-tag": "Journal des balises", "log-description-tag": "Cette page montre quand des utilisateurs ont ajouté ou supprimé des [[Special:Tags|balises]] de révisions individuelles ou d’entrées de journal. Le journal ne liste pas les actions de marquage quand elles ont lieu au cours d’une modification, d’une suppression, ou d’une action semblable.", diff --git a/languages/i18n/gl.json b/languages/i18n/gl.json index 051e76bbd7..4724ab183d 100644 --- a/languages/i18n/gl.json +++ b/languages/i18n/gl.json @@ -954,7 +954,7 @@ "searchprofile-advanced-tooltip": "Procurar nos espazos de nomes elixidos", "search-result-size": "$1 ({{PLURAL:$2|1 palabra|$2 palabras}})", "search-result-category-size": "{{PLURAL:$1|1 membro|$1 membros}} ({{PLURAL:$2|1 subcategoría|$2 subcategorías}}, {{PLURAL:$3|1 ficheiro|$3 ficheiros}})", - "search-redirect": "(redirixido desde $1)", + "search-redirect": "(redirección desde \"$1\")", "search-section": "(sección \"$1\")", "search-category": "(categoría $1)", "search-file-match": "(coincide co contido do ficheiro)", @@ -1293,8 +1293,8 @@ "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|desde a última visita}}", "enhancedrc-history": "historial", "recentchanges": "Cambios recentes", - "recentchanges-legend": "Opcións dos cambios", - "recentchanges-summary": "Nesta páxina podes seguir as modificacións máis recentes feitas no wiki.", + "recentchanges-legend": "Opcións dos cambios recentes", + "recentchanges-summary": "Nesta páxina pode seguir as modificacións máis recentes feitas no wiki.", "recentchanges-noresult": "Non se produciron cambios que coincidisen con eses criterios durante o período especificado.", "recentchanges-feed-description": "Nesta fonte de novas pode seguir as modificacións máis recentes feitas no wiki.", "recentchanges-label-newpage": "Esta edición creou unha nova páxina", @@ -2699,6 +2699,7 @@ "newimages-showbots": "Mostrar as cargas feitas por bots", "newimages-hidepatrolled": "Ocultar as subas verificadas", "noimages": "Non hai imaxes para ver.", + "gallery-slideshow-toggle": "Intercambiar miniaturas", "ilsubmit": "Procurar", "bydate": "por data", "sp-newimages-showfrom": "Mostrar os novos ficheiros comezando polo $1 ás $2", diff --git a/languages/i18n/he.json b/languages/i18n/he.json index 458f34f5e9..e1b5118714 100644 --- a/languages/i18n/he.json +++ b/languages/i18n/he.json @@ -2715,6 +2715,7 @@ "newimages-showbots": "הצגת העלאות שבוצעו על־ידי בוטים", "newimages-hidepatrolled": "הסתרת העלאות בדוקות", "noimages": "אין קבצים.", + "gallery-slideshow-toggle": "הצגת/הסתרת תמונות ממוזערות", "ilsubmit": "חיפוש", "bydate": "לפי תאריך", "sp-newimages-showfrom": "הצגת קבצים חדשים החל מ־$2, $1", @@ -3584,7 +3585,7 @@ "feedback-thanks": "תודה! המשוב שלך פורסם בדף \"[$2 $1]\".", "feedback-thanks-title": "תודה!", "feedback-useragent": "User agent:", - "searchsuggest-search": "חיפוש", + "searchsuggest-search": "חיפוש ב{{grammar:תחילית|{{SITENAME}}}}", "searchsuggest-containing": "כולל...", "api-error-autoblocked": "כתובת ה־IP שלך נחסמה אוטומטית, כי היא הייתה בשימוש על־ידי משתמש חסום.", "api-error-badaccess-groups": "אינך מורשה להעלות קבצים לאתר הוויקי הזה.", diff --git a/languages/i18n/hi.json b/languages/i18n/hi.json index 097d801a85..f6988a0cde 100644 --- a/languages/i18n/hi.json +++ b/languages/i18n/hi.json @@ -73,7 +73,8 @@ "YmKavishwar", "Upendradutt93", "Nemo bis", - "Wassan.anmol" + "Wassan.anmol", + "Ziyaurr" ] }, "tog-underline": "कड़ियाँ अधोरेखन:", @@ -206,7 +207,7 @@ "category-file-count-limited": "इस श्रेणी में निम्नलिखित {{PLURAL:$1|फ़ाइल है।|फ़ाइलें हैं।}}", "listingcontinuesabbrev": "जारी", "index-category": "सूचीबद्ध पृष्ठ", - "noindex-category": "असूचीबद्ध पृष्ठ", + "noindex-category": "असूचीबद्ध पृष्ठों", "broken-file-category": "टूटी हुई फ़ाइल कड़ियों वाले पृष्ठ", "about": "के बारे में", "article": "सामग्री लेख", diff --git a/languages/i18n/kk-cyrl.json b/languages/i18n/kk-cyrl.json index 279b6a0d72..29bd82dc39 100644 --- a/languages/i18n/kk-cyrl.json +++ b/languages/i18n/kk-cyrl.json @@ -1130,7 +1130,7 @@ "right-noratelimit": "Еселік шектелімдері ықпал етпейді", "right-import": "Басқа уикилерден беттерді сырттан алу", "right-importupload": "Файлдарды жүктеу арқылы беттерді сырттан алу", - "right-patrol": "Басқарардың өңдемелерін тексерілді деп белгілеу", + "right-patrol": "Басқалардың өңдемелерін тексерілді деп белгілеу", "right-autopatrol": "Өз өңдемелерін тексерілді деп өздіктік белгілеу", "right-patrolmarks": "Жуықтағы өзгерістердегі зерттеу белгілерін көру", "right-unwatchedpages": "Бақыланылмаған бет тізімін көру", diff --git a/languages/i18n/lez.json b/languages/i18n/lez.json index f1d9a18162..a497488845 100644 --- a/languages/i18n/lez.json +++ b/languages/i18n/lez.json @@ -261,7 +261,6 @@ "yourname": "Уртахдин тӀвар", "yourpassword": "Парол", "yourpasswordagain": "Парол кхьин хъувун:", - "remembermypassword": "И браузерда зи логин рикӀел хуьхь (лап гзаф $1 {{PLURAL:$1|1=югъ|йикъар}})", "yourdomainname": "Куь домен", "login": "Гьахьун", "nav-login-createaccount": "Гьахьун/аккаунт туькӀуьрун", @@ -496,7 +495,7 @@ "youremail": "Электрон почта:", "username": "Уртахдин тӀвар", "yourrealname": "Xалис тIвар:", - "yourlanguage": "ЧIалар", + "yourlanguage": "ЧӀалар", "yournick": "ЦӀийи къул:", "yourgender": "Жинс:", "gender-male": "итимдин", @@ -633,8 +632,8 @@ "file-anchor-link": "Файл", "filehist": "Файлдин тарих", "filehist-help": "Файлдин виликан жуьре килигун патал, гьа а жуьредин тарих/вахт илиса,", - "filehist-deleteall": "вири къакъудун", - "filehist-deleteone": "къакъудун", + "filehist-deleteall": "вири алудун", + "filehist-deleteone": "алудун", "filehist-revert": "элкъуьрна хкун", "filehist-current": "алай", "filehist-datetime": "Тарих/вахт", @@ -657,7 +656,7 @@ "filedelete": "$1 алудун", "filedelete-legend": "Файл алудун", "filedelete-comment": "Кар", - "filedelete-submit": "Къакъудун", + "filedelete-submit": "Алудун", "filedelete-reason-otherlist": "Муькуь себеб", "mimesearch": "MIME ахтармишун", "download": "АцIун", @@ -733,10 +732,10 @@ "unwatching": "Амма клигнай", "created": "туькIуьрнава", "changed": "дегишнава", - "deletepage": "Къакъудун хъувун", + "deletepage": "Алудун хъувун", "confirm": "Тестикьун", "delete-confirm": "«$1» алудун", - "delete-legend": "Къакъудун", + "delete-legend": "Алудун", "confirmdeletetext": "Квез чlуриз кlанзани чарар гьадан вири тарихар галаз. Буюр, сидикъара,куьне чlурзатlа, куьн агъавурда автlа вуч ийизатlа ва куьне ийизатlа жуьреда [[{{MediaWiki:Policy-url}}| политика]].", "actioncomplete": "Кар авунва", "actionfailed": "Кар йиз алакьнавач", diff --git a/languages/i18n/lij.json b/languages/i18n/lij.json index 8c5b16c193..c9f60919f7 100644 --- a/languages/i18n/lij.json +++ b/languages/i18n/lij.json @@ -40,7 +40,7 @@ "tog-enotifminoredits": "Mandime una email ascì pe e modifiche menoî de pagine e di file", "tog-enotifrevealaddr": "Mostra o mæ addresso inte e-mail de notiffica", "tog-shownumberswatching": "Mostra o numero di utenti che tegnan d'oeuggio sta pagina", - "tog-oldsig": "Firma attuale:", + "tog-oldsig": "Firma attoale:", "tog-fancysig": "Tratta a firma comme wikitesto (sensa un collegamento aotomatico)", "tog-uselivepreview": "Abillita a fonsion de l'anteprimma in diretta", "tog-forceeditsummary": "Domanda conferma se o campo ogetto o l'è veuo", @@ -57,7 +57,7 @@ "tog-showhiddencats": "Fa vedde e categorîe ascose", "tog-norollbackdiff": "Ometti o confronto tra verscioin doppo ch'ho fæto o ripristino", "tog-useeditwarning": "Avertime se lascio 'na paggina de modiffica sens'avei sarvou i cangi", - "tog-prefershttps": "Deuvia sempre una connescion segua quande se intra", + "tog-prefershttps": "Adœuvia delongo una connescion segua quande se intra", "underline-always": "Sempre", "underline-never": "Mâi", "underline-default": "Impostassioin predefinie do navegatô o da skin", @@ -145,14 +145,14 @@ "category-file-count-limited": "Questa categoria a contegne {{PLURAL:$1|o file indicao|i $1 file indicæ}} chi de sotta.", "listingcontinuesabbrev": "cont.", "index-category": "Paggine indiçizzæ", - "noindex-category": "Pàgine sénsa indiçe", + "noindex-category": "Paggine sença endexo", "broken-file-category": "Paggine con di colegamenti a di file che no ghe son", "about": "Informaçioìn", "article": "Pagina di contegnùi", "newwindow": "(O s'arve inte 'n âtro barcon)", "cancel": "Scancella", "moredotdotdot": "De ciû...", - "morenotlisted": "Questa lista a no l'è completa.", + "morenotlisted": "Questa lista a poriæ ese incompleta.", "mypage": "Paggina", "mytalk": "Discuscioin", "anontalk": "Discuscion pe questo addresso IP", @@ -211,6 +211,8 @@ "talk": "Discuscion", "views": "Vìxite", "toolbox": "Arneixi", + "tool-link-userrights": "Modiffica groppi {{GENDER:$1|utente}}", + "tool-link-emailuser": "Manda un'e-mail a questo {{GENDER:$1|utente}}", "userpage": "Veddi a paggina utente", "projectpage": "Veddi a paggina de servissio", "imagepage": "Vizualizza a paggina do file", @@ -404,9 +406,11 @@ "userlogin-remembermypassword": "Mantegnime collegou", "userlogin-signwithsecure": "Adoeuvia una conescion segua", "cannotlogin-title": "Imposcibbile intrâ", + "cannotlogin-text": "L'accesso o no l'è poscibbile.", "cannotloginnow-title": "Aoa no se poeu intrâ", "cannotloginnow-text": "Quande s'adoeuvia $1 no se poeu intrâ.", "cannotcreateaccount-title": "Imposcibbile creâ di utençe", + "cannotcreateaccount-text": "A creaçion diretta de l'utença a no l'è attivâ insce questo wiki.", "yourdomainname": "Indirisso do scito:", "password-change-forbidden": "No ti peu cangiâ poula segretta in questa wiki.", "externaldberror": "Gh'è stæto un aro co-o server de aotenticaçion esterno, oppû no ti g'hæ i aotorizzaçioin pe aggiornâ o to accesso esterno.", @@ -534,7 +538,6 @@ "botpasswords-label-resetpassword": "Reimposta a poula segretta", "botpasswords-label-grants": "Assegnaçioin applicabile:", "botpasswords-help-grants": "Ogni assegnaçion a dà accesso a-i driti utente elencæ che un'utença a g'ha zà. Amia a [[Special:ListGrants|tabella d'e assegnaçioin]] pe de ulteioî informaçioin.", - "botpasswords-label-restrictions": "Restriçioin d'utilizzo:", "botpasswords-label-grants-column": "Assegnaçioin", "botpasswords-bad-appid": "O nomme bot \"$1\" o no l'è vallido.", "botpasswords-insert-failed": "Imposcibile azonze o nomme bot \"$1\". O l'è za stæto azonto?", @@ -579,7 +582,7 @@ "passwordreset-emailelement": "Nomme utente: \n$1\n\nPoula segretta temporannia: \n$2", "passwordreset-emailsentemail": "Se questo addresso de posta elettronnica o l'è associou a-a teu utença, alloa saiâ inviou un'e-mail pe rempostâ a poula segretta.", "passwordreset-emailsentusername": "Se gh'è un adreçço de posta elettronica associou con questo nomme utente, alloa saiâ inviou una email pe rempostâ a password.", - "passwordreset-emailsent-capture2": "L'email de rempostaçion da password {{PLURAL:$1|a l'è stæta inviâ|son stæte inviæ}}. {{PLURAL:$1|O nomme|L'elenco di nommi}} utente e password o l'è mostrou chì de sotta.", + "passwordreset-emailsent-capture2": "{{PLURAL:$1|L'|E }}e-mail de rempostaçion da password {{PLURAL:$1|a l'è stæta inviâ|son stæte inviæ}}. {{PLURAL:$1|O nomme|L'elenco di nommi}} utente e password o l'è mostrou chì.", "passwordreset-emailerror-capture2": "Invio de email {{GENDER:$2|a l'utente}} non ariescio: $1. {{PLURAL:$3|O nomme|L'elenco di nommi}} utente e password o l'è mostrou chì de sotta.", "passwordreset-nocaller": "Un chi ciamma ti g'hæ da dâlo", "passwordreset-nosuchcaller": "O ciamante o no l'existe: $1", @@ -937,7 +940,7 @@ "searchprofile-advanced-tooltip": "Çerca inti namespace personalizæ", "search-result-size": "$1 ({{PLURAL:$2|1 paròlla|$2 paròlle}})", "search-result-category-size": "{{PLURAL:$1|1 utente|$1 utenti}} ({{PLURAL:$2|1 sottocategoria|$2 sottocategorie}}, {{PLURAL:$3|1 file|$3 file}})", - "search-redirect": "(redirect $1)", + "search-redirect": "(Rendriçço da $1)", "search-section": "(seçión $1)", "search-category": "(categoria $1)", "search-file-match": "(corrispondença into contegnuo do file)", @@ -1450,6 +1453,7 @@ "upload-dialog-disabled": "O caregamento di file tramite questo barcon de dialogo o l'è disabilitou inte questo wiki.", "upload-dialog-title": "Carrega file", "upload-dialog-button-cancel": "Anulla", + "upload-dialog-button-back": "Inderê", "upload-dialog-button-done": "Fæto", "upload-dialog-button-save": "Sarva", "upload-dialog-button-upload": "Carrega", @@ -1518,6 +1522,7 @@ "uploadstash-errclear": "O nettezzo di file o no l'è ariescio.", "uploadstash-refresh": "Aggiorna l'elenco di file", "uploadstash-thumbnail": "veddi miniatua", + "uploadstash-exception": "Imposcibile memoizâ o caregamento in stash ($1): \"$2\".", "invalid-chunk-offset": "Offset d'a parte non vallido.", "img-auth-accessdenied": "Accesso negou", "img-auth-nopathinfo": "PATH_INFO mancante.\nO server o no l'è impostou pe passâ quest'informaçion.\nO poriæ ese basou insce CGI e o no poeu supportâ img_auth.\nAmia https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.", @@ -1799,6 +1804,8 @@ "apisandbox-results-fixtoken-fail": "Imposcibile recuperâ o token \"$1\".", "apisandbox-alert-page": "I campi insce questa pagina no son vallidi.", "apisandbox-alert-field": "O valô de questo campo o no l'è vallido.", + "apisandbox-continue": "Continnoa", + "apisandbox-continue-clear": "Nettezza", "booksources": "Fonte libraie", "booksources-search-legend": "Çerca e fonti", "booksources-isbn": "Codice ISBN:", @@ -3214,6 +3221,7 @@ "tag-filter-submit": "Filtro", "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Etichetta|Etichette}}]]: $2)", "tag-mw-contentmodelchange": "cangio a-o modello di contegnui", + "tag-mw-contentmodelchange-description": "Modiffiche che [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel cangian o modello di contegnui] de 'na paggina", "tags-title": "Etichette", "tags-intro": "Questa pagina a l'elenca i etichette che o software o poriæ associâ a 'na modiffica e o so scignificou.", "tags-tag": "Nomme de l'etichetta", @@ -3333,6 +3341,18 @@ "htmlform-cloner-create": "Azonzi de l'atro", "htmlform-cloner-delete": "Leva", "htmlform-cloner-required": "Ghe voeu a-o manco un valô.", + "htmlform-date-placeholder": "AAAA-MM-GG", + "htmlform-time-placeholder": "HH:MM:SS", + "htmlform-datetime-placeholder": "AAAA-MM-GG HH:MM:SS", + "htmlform-date-invalid": "O valô specificou o no l'è riconoscito comme dæta. Prœuva a dœuviâ o formato AAAA-MM-GG.", + "htmlform-time-invalid": "O valô specificou o no l'è riconosciuo comme oraio. Prœuva a dœuviâ o formato HH:MM:SS.", + "htmlform-datetime-invalid": "O valô specificou o no l'è riconosciuo comme dæta e oa. Prœuva a dœuviâ o formato AAAA-MM-GG HH:MM:SS.", + "htmlform-date-toolow": "O valô specificou o l'è precedente a-a primma dæta consentia do $1.", + "htmlform-date-toohigh": "O valô specificou o l'è succescivo a l'urtima dæta consentia do $1.", + "htmlform-time-toolow": "O valô specificou o l'è precedente a-o primmo oraio consentio do $1.", + "htmlform-time-toohigh": "O valô specificou o l'è succescivo a l'urtimo oraio consentio do $1.", + "htmlform-datetime-toolow": "O valô specificou o l'è precedente a-a primma dæta e oa consentia do $1.", + "htmlform-datetime-toohigh": "O valô specificou o l'è succescivo a l'urtima dæta e oa consentia do $1.", "htmlform-title-badnamespace": "[[:$1]] a no se troeuva into namespace \"{{ns:$2}}\".", "htmlform-title-not-creatable": "\"$1\" o l'è o tittolo de una paggina non creabile", "htmlform-title-not-exists": "$1 a no l'existe.", @@ -3416,7 +3436,6 @@ "feedback-external-bug-report-button": "Documenta un problema tecnico", "feedback-dialog-title": "Invia un feedback", "feedback-dialog-intro": "Doeuvia o moddulo sottostante pe inviâ o to feedback. O to commento o l'appariâ inta paggina \"$1\", assemme a-o to nomme utente.", - "feedback-error-title": "Errô", "feedback-error1": "Errô: Da-a API l'è arrivou un risultou non riconosciuo", "feedback-error2": "Errô: No l'è stæto poscibbile eseguî a modiffica", "feedback-error3": "Errô: Nisciun-a risposta da-a API", @@ -3717,5 +3736,10 @@ "linkaccounts-submit": "Collega utençe", "unlinkaccounts": "Scollega utençe", "unlinkaccounts-success": "L'utença a l'è stæta scollegâ.", - "authenticationdatachange-ignored": "O cangiamento da dæta d'aotenticaçion o no l'è passou. Foscia che no gh'ea un provider configuou?" + "authenticationdatachange-ignored": "O cangiamento da dæta d'aotenticaçion o no l'è passou. Foscia che no gh'ea un provider configuou?", + "userjsispublic": "Regorda: e sottopaggine JavaScript no devan contegnî dæti riservæ percose son vixoalizabbile da-i atri utenti.", + "usercssispublic": "Regorda: e sottopagine CSS no devan contegnî dæti riservæ percose son vixoalizabbile da i atri utenti.", + "restrictionsfield-badip": "Intervallo d'adreççi IP non vallido:$1", + "restrictionsfield-label": "Intervalli IP consentii:", + "restrictionsfield-help": "Un adresso IP ò intervallo CIDR pe linnia. Pe consentî tutto, adœuvia
0.0.0.0/0
::/0" } diff --git a/languages/i18n/lt.json b/languages/i18n/lt.json index 18a05e5dc5..e152c768a0 100644 --- a/languages/i18n/lt.json +++ b/languages/i18n/lt.json @@ -2153,7 +2153,7 @@ "contributions-title": "{{GENDER:$1|Naudotojo|Naudotojos}} $1 indėlis", "mycontris": "Indėlis", "anoncontribs": "Indėlis", - "contribsub2": "Dėl {{GENDER:$3|$1}} ($2)", + "contribsub2": "Naudotojas: {{GENDER:$3|$1}} ($2)", "contributions-userdoesnotexist": "Naudotojo paskyra „$1“ neužregistruota.", "nocontribs": "Jokie keitimai neatitiko šių kriterijų.", "uctop": "(dabartinis)", diff --git a/languages/i18n/ne.json b/languages/i18n/ne.json index c9b382bfb1..f475dbcc06 100644 --- a/languages/i18n/ne.json +++ b/languages/i18n/ne.json @@ -274,7 +274,7 @@ "retrievedfrom": " \"$1\" बाट निकालिएको", "youhavenewmessages": "तपाईंको लागि ($2) मा $1 छ ।", "youhavenewmessagesfromusers": "तपाईंको लागि {{PLURAL:$3|प्रयोगकर्ता|$3 प्रयोगकर्ताहरू}} का $1 छन् । ($2)", - "youhavenewmessagesmanyusers": "तपाईँलाई धेरै प्रयोगकर्ताहरू($2) बाट $1 छ ।", + "youhavenewmessagesmanyusers": "तपाईंलाई धेरै प्रयोगकर्ताहरू($2) बाट $1 छ ।", "newmessageslinkplural": "{{PLURAL:$1|एउटा नयाँ सन्देश|999=नयाँ सन्देशहरू}}", "newmessagesdifflinkplural": "अन्तिम {{PLURAL:$1|परिवर्तन|999=परिवर्तनहरू}}", "youhavenewmessagesmulti": "तपाईंको लागि $1 मा नयाँ सन्देशहरू छन्", @@ -384,8 +384,8 @@ "namespaceprotected": " '''$1''' नेमस्पेसमा रहेका पृष्ठहरू सम्पादन गर्ने अनुमति यहाँलाई छैन ।", "customcssprotected": "तपाईंलाई यो पृष्ठ सम्पादन गर्ने अनुमति छैन, किनकी यसमा कुनै अर्को प्रयोगकर्ताको व्यक्तिगत अभिरुचीहरू संग्रहित छन् ।", "customjsprotected": "तपाईंलाई यो जाभास्कृप्ट पृष्ठ सम्पादन गर्ने अनुमति छैन, किनकी यसमा कुनै अर्को प्रयोगकर्ताको व्यक्तिगत अभिरुचीहरू संग्रहित छन् ।", - "mycustomcssprotected": "यस CSSपृष्ठ सम्पादन गर्नको लागि लागि तपाईँलाई अनुमति छैन ।", - "mycustomjsprotected": "यस JavaScript पृष्ठ सम्पादन गर्नको लागि लागि तपाईँलाई अनुमति छैन ।", + "mycustomcssprotected": "यो CSSपृष्ठ सम्पादन गर्नको लागि लागि तपाईंलाई अनुमति छैन ।", + "mycustomjsprotected": "यो जावास्क्रिप्ट पृष्ठ सम्पादन गर्नको लागि तपाईंलाई अनुमति छैन ।", "myprivateinfoprotected": "तपाईँसँग तपाईँको निजी जानकारीहरू सम्पादन गर्ने अनुमती छैन", "mypreferencesprotected": "तपाईँसँग तपाईँको अभिरुचीहरू सम्पादन गर्ने अनुमती छैन", "ns-specialprotected": "विशेष पृष्ठहरू सम्पादन गर्न सकिदैन।", @@ -448,7 +448,7 @@ "createacct-reason-ph": "किन तपाईं नयाँ खाता खोलिरहनु भएको हो ?", "createacct-submit": "तपाईँको खाता सिर्जना गर्नुहोस", "createacct-another-submit": "खाता खोल्नुहोस्", - "createacct-benefit-heading": "{{SITENAME}} तपाईँ जस्तै मानिसहरूद्वारा सिर्जना गरिएको हो ।", + "createacct-benefit-heading": "{{SITENAME}} तपाईं जस्तै मानिसहरूद्वारा सिर्जना गरिएको हो ।", "createacct-benefit-body1": "{{PLURAL:$1|सम्पादन|सम्पादनहरू}}", "createacct-benefit-body2": "{{PLURAL:$1|पृष्ठ|पृष्ठहरू}}", "createacct-benefit-body3": "हालैका {{PLURAL:$1|योगदानकर्ता|योगदानकर्ताहरू}}", @@ -473,10 +473,10 @@ "passwordtooshort": "पासवर्ड कम्तिमा {{PLURAL:$1|१ अक्षर|$1 अक्षरहरू}}को हुनुपर्छ।", "passwordtoolong": "पासवर्ड {{PLURAL:$1|१ अक्षर|$1 अक्षरहरू}} भन्दा लामो हुनु हुदैन ।", "password-name-match": "तपाईँको प्रवेशशव्द प्रयोगकर्ता नाम भन्दा फरक हुनुपर्छ ।", - "password-login-forbidden": "यो प्रयोगकर्ता नाम र प्रवेश शव्द वर्जित गरिएकोछ ।", + "password-login-forbidden": "यो प्रयोगकर्ता नाम र प्रवेश शब्द वर्जित गरिएकोछ ।", "mailmypassword": "पासवर्ड पूर्वनिर्धारित गर्नुहोस्", "passwordremindertitle": "{{SITENAME}}को लागि नयाँ अस्थायी पासवर्ड", - "passwordremindertext": "कसैले (सायद तपाईँ, IP ठेगाना $1 बाट), {{SITENAME}}($4) को लागि नयाँ प्रवेसशब्द अनुरोध गर्नुभएको छ । प्रयोगकर्ता \"$2\" को लागि नयाँ अस्थायी प्रवेसशब्द \"$3\"तयार पारिएको छ । यदि यो तपाईंको इच्छामा भएको भए अहिले तपाईँले प्रवेशगरी नयाँ प्रवेसशब्द छान्नु पर्ने हुन्छ।\nतपाईंको अस्थायी प्रवेसशब्द {{PLURAL:$5|एक दिन|$5 दिनहरू पछि}} अमान्य हुनेछ ।\n\nयदि कोही अरुले नै अनुरोध गरेको हो भने , या तपाईंले आफ्नो प्रवेसशब्द सम्झिनु भयो भने, अथवा\nत्यसलाई परिवर्तन गर्न चाहनुहुन्न भने, तपाईँले यो सन्देसको वेवास्ता गर्नसक्नुहुन्छ र पुरानै प्रवेसशब्द प्रयोग गरिरहन सक्नुहुन्छ ।", + "passwordremindertext": "कसैले (सायद तपाईं, IP ठेगाना $1 बाट), {{SITENAME}}($4) को लागि नयाँ प्रवेशशब्द अनुरोध गर्नुभएको छ । प्रयोगकर्ता \"$2\" को लागि नयाँ अस्थायी प्रवेशशब्द \"$3\"तयार पारिएको छ । यदि यो तपाईंको इच्छामा भएको भए अहिले तपाईँले प्रवेशगरी नयाँ प्रवेशशब्द छान्नु पर्ने हुन्छ ।\nतपाईंको अस्थायी प्रवेशशब्द {{PLURAL:$5|एक दिन|$5 दिनहरू पछि}} अमान्य हुनेछ ।\n\nयदि कोही अरुले नै अनुरोध गरेको हो भने , या तपाईंले आफ्नो प्रवेशशब्द सम्झिनु भयो भने, अथवा\nत्यसलाई परिवर्तन गर्न चाहनुहुन्न भने, तपाईंले यो सन्देशको वेवास्ता गर्नसक्नुहुन्छ र पुरानै प्रवेशशब्द प्रयोग गरिरहन सक्नुहुन्छ ।", "noemail": "प्रयोगकर्ता \"$1\"को लागि कुनै पनि इ-मेल दर्ता गरिएको छैन ।", "noemailcreate": "तपाईंले सही ई-मेल ठेगाना दिनुपर्छ", "passwordsent": "\"$1\" को लागि दर्ता गरिएको ई-मेल ठेगानामा एक प्रवेशशव्द पठाइएको छ।\nकृपया त्यसलाई प्राप्त गरेपछि प्रवेश गर्नुहोला ।", @@ -515,7 +515,7 @@ "resetpass_header": "खाताको पासवर्ड परिवर्तन गर्ने", "oldpassword": "पुरानो पासवर्ड:", "newpassword": "नयाँ पासवर्ड:", - "retypenew": "प्रवेश शव्द पुन: दिनुहोस् :", + "retypenew": "प्रवेश शब्द पुन: दिनुहोस् :", "resetpass_submit": "पासवर्ड व्यवस्थित गरी र प्रवेशगर्ने", "changepassword-success": "तपाईँको पासवर्ड सफलतापूर्वक परिवर्तन भयो!", "changepassword-throttled": "तपाईंले भर्खरै धेरै पल्ट प्रवेश (लग इन)को निम्ति प्रयास गर्नुभएको छ। \nकृपया $1 पर्खेर मात्र प्रयास गर्नुहोस्।", @@ -523,7 +523,7 @@ "resetpass-no-info": "यो पृष्ठ सिधै हेर्नको लागि तपाईँले प्रवेश गर्नुपर्छ ।", "resetpass-submit-loggedin": "प्रवेसशब्द परिवर्तन गर्ने", "resetpass-submit-cancel": "रद्द गर्ने", - "resetpass-wrong-oldpass": "अस्थायी अथवा हालिएको प्रवेसशब्द अमान्य\nतपाईंले अघिबाट नैं प्रवेसशब्द सफलता पूर्वक परिवर्तन गरिसक्नु भएको हो वा नयाँ प्रवेसशब्दको निम्ति निवेदन गर्नुभएकोछ।", + "resetpass-wrong-oldpass": "अस्थायी अथवा हालिएको प्रवेश शब्द अमान्य\nतपाईंले अघिबाट नैं प्रवेश शब्द सफलता पूर्वक परिवर्तन गरिसक्नु भएको हो वा नयाँ प्रवेश शब्दको निम्ति निवेदन गर्नुभएकोछ।", "resetpass-recycled": "कृपया वर्तमान पासर्वड भन्दा फरक पासर्वडलाई पुनः मिलाउनुहोस् ।", "resetpass-temp-emailed": "तपाईं अस्थाई इमेल कोडले प्रवेश गर्नुभएको छ।\nप्रवेश सफल पार्नका लागि, तपाईंले यहाँ एउटा नयाँ पासवर्ड राख्नु पर्नेछ:", "resetpass-temp-password": "अस्थाइ पासवर्ड", @@ -534,7 +534,7 @@ "passwordreset": "प्रवेशशव्द पुनः तय गर्ने", "passwordreset-text-one": "इमेल मार्फल अस्थायी पासवर्ड प्राप्त गर्नको लागी यस फारमलाई पूर्ण रूपमा भर्नुहोस् ।", "passwordreset-text-many": "{{PLURAL:$1|कृपया यहाँ मध्ये एउटा क्षेत्र भरि अस्थाई पासवर्ड इमेल मार्फत प्राप्त गर्नुहोस।}}", - "passwordreset-disabled": "प्रवेश शव्द पुनः निर्धारण गर्ने व्यवस्था यस विकिमा निस्क्रिय पारिएको छ।", + "passwordreset-disabled": "प्रवेश शब्द पुनः निर्धारण गर्ने व्यवस्था यस विकिमा निस्क्रिय पारिएको छ ।", "passwordreset-emaildisabled": "इमेल सुविधा यस विकिमा निस्क्रिय बनाइएको छ ।", "passwordreset-username": "प्रयोगकर्ता नाम:", "passwordreset-domain": "डोमेन", @@ -666,8 +666,8 @@ "hiddencategories": "यो पृष्ठ निम्न {{PLURAL:$1|1 लुकाइएको श्रेणी|$1 लुकाइएका श्रेणीहरू}}को सदस्य हो :", "edittools": "", "edittools-upload": "-", - "nocreatetext": "{{SITENAME}} ले नयाँ पृष्ठ सृजना गर्न सक्ने क्षमतामा रोक लगाएको छ।\nतपाईँ पछाडि जानु भइ रहिआएको पृष्ठ सम्पादन गर्नसक्नुहुन्छ , अथवा [[Special:UserLogin|प्रवेश गर्नुहोस या नयाँ खाता सृजना गर्नुहोस् ]]।", - "nocreate-loggedin": "नयाँ पृष्ठ सृजनागर्नको लागि तपाईँलाई अनुमति छैन ।", + "nocreatetext": "{{SITENAME}} ले नयाँ पृष्ठ सृजना गर्न सक्ने क्षमतामा रोक लगाएको छ।\nतपाईं पछाडि गएर यहाँ रहेका पृष्ठ सम्पादन गर्नसक्नुहुन्छ , अथवा [[Special:UserLogin|प्रवेश गर्नुहोस या नयाँ खाता खोल्नुहोस्]]।", + "nocreate-loggedin": "नयाँ पृष्ठ सृजनागर्नको लागि तपाईंलाई अनुमति छैन ।", "sectioneditnotsupported-title": "खण्ड सम्पादन असमर्थित", "sectioneditnotsupported-text": "यस पृष्ठमा खण्ड सम्पादन असमर्थित", "permissionserrors": "अनुमति नभएको", @@ -772,7 +772,7 @@ "revdelete-nooldid-title": "अमान्य पुनरावलोकन लक्ष", "revdelete-nooldid-text": "यस क्रियालाई गर्नको लागि तपाईंले लक्ष्य अवतरण दिनु भएको छैन, वा तपाईंले दिएको अवतरण अस्तित्वमा छैन वा तपाईं सद्य अवतरणलाई लुकाउने प्रयत्न गर्दै हुनुहुन्छ।", "revdelete-no-file": "खुलाइएको पृष्ठ अस्तित्वमा छैन", - "revdelete-show-file-confirm": "तपाईँ $2 बाट $3 मा मेटिएको फाइल \"$1\" को पुनरावलोकन हेर्न चाहनुहुन्छ भन्ने कुरामा निश्चित हुनुहुन्छ ?", + "revdelete-show-file-confirm": "तपाईं $2 बाट $3 मा मेटिएको फाइल \"$1\" को पुनरावलोकन हेर्न चाहनुहुन्छ भन्ने कुरामा निश्चित हुनुहुन्छ ?", "revdelete-show-file-submit": "हो", "revdelete-selected-text": "[[:$2]] को {{PLURAL:$1|छानिएको संशोधन|छानिएका संशोधनहरू}}:", "revdelete-selected-file": "[[:$2]] को {{PLURAL:$1|छानिएको फाइल संस्करण|छानिएका फाइल संस्करणहरू}}:", @@ -963,7 +963,7 @@ "timezoneregion-europe": "युरोप", "timezoneregion-indian": "हिन्द महासागर", "timezoneregion-pacific": "प्राशान्त महासागर", - "allowemail": "अरु प्रयोगकर्ताहरुबाट प्राप्त हुने ईमेल enable गर्नुहोस् ।", + "allowemail": "अरु प्रयोगकर्ताहरूबाट प्राप्त हुने ईमेल सक्षम गर्नुहोस् ।", "prefs-searchoptions": "खोज्ने", "prefs-namespaces": "नेमस्पेसेज", "default": "पूर्वनिर्धारित", @@ -971,7 +971,7 @@ "prefs-custom-css": "अनुकुलित CSS", "prefs-custom-js": "अनुकुलित JS", "prefs-common-css-js": "साझा CSS/जाभा स्क्रिप्ट सबै त्वचा(स्किन)को लागि:", - "prefs-reset-intro": "तपाईँ यो पृष्ठलाई आफ्नो अभिरुचीहरू साइट पूर्वावस्थामा फर्काउन प्रयोग गर्न सक्नुहुन्छ । त्यस पछि यसलाई रद्द गर्न सक्नुहुन्न ।", + "prefs-reset-intro": "तपाईं यो पृष्ठलाई आफ्नो अभिरुचीहरू साइट पूर्वावस्थामा फर्काउन प्रयोग गर्न सक्नुहुन्छ । त्यस पछि यसलाई रद्द गर्न सक्नुहुन्न ।", "prefs-emailconfirm-label": "इ-मेल एकिन प्रक्रिया :", "youremail": "ईमेल", "username": "{{GENDER:$1|प्रयोगकर्ता नाम}}:", @@ -994,8 +994,8 @@ "prefs-help-gender": "यो जानकारी दिनु वैकल्पिक छ।\nयो सफ्टवेयरमा लिङ्गको आधारमा तपाईंको लागि सहि सम्बोधन गर्नको निमित्त हुन्छ।\nयो जानकारी सार्वजनिक गरिनेछ।", "email": "ईमेल", "prefs-help-realname": "वास्तविक नाम ऐच्छिक हो ।\nतपाईंले खुलाउनु भएको खण्डमा तपाईंको कामको श्रेय दिनको लागि यसको प्रयोग गरिने छ ।", - "prefs-help-email": "इमेल ठेगाना ऐच्छिक हो, तर प्रवेश शव्दको पुनर्स्थापनाको लागि आवश्यकता छ, के तपाईंले प्रवेश शव्द भुल्नु हुन्थ्यो।", - "prefs-help-email-others": "तपाईंले यो पनि चयन गर्न सक्नुहुन्छ कि अरुहरुले तपाईंको परिचय नपाई तपाईंसित तपाईंको प्रयोगकर्ता अथवा वार्तालाप पृष्ठको माध्यमले सम्पर्क राखुन् ।", + "prefs-help-email": "इमेल ठेगाना ऐच्छिक हो, तर प्रवेश शब्दको पुनर्स्थापनाका लागि आवश्यकता छ, तपाईंले प्रवेश शब्द त के भुल्नु हुन्थ्यो ।", + "prefs-help-email-others": "तपाईंले यो पनि चयन गर्न सक्नुहुन्छ कि अरुले तपाईंको परिचय नपाई तपाईंसित तपाईंको प्रयोगकर्ता अथवा वार्तालाप पृष्ठको माध्यमले सम्पर्क राखुन् ।", "prefs-help-email-required": "इमेल ठेगामा चाहिन्छ ।", "prefs-info": "साधारण जानकारी", "prefs-i18n": "अन्तर्राष्ट्रियकरण", @@ -1752,7 +1752,7 @@ "usermaildisabledtext": "यस विकिमा तपाईं अरु प्रयोगकर्तालाई ई-मेल पठाउन सक्नुहुन्न", "noemailtitle": "ईमेल ठेगाना नभएको", "noemailtext": "प्रयोगकर्ताले सही ई-मेल ठेगाना दर्शाएको छैन।", - "nowikiemailtext": "यी प्रयोगकर्ताले अरु प्रयोगकर्ताहरुबाट ई-मेल स्वीकार नगर्ने छनोट गरेकाछन्।", + "nowikiemailtext": "यी प्रयोगकर्ताले अरु प्रयोगकर्ताहरूबाट ई-मेल स्वीकार नगर्ने छनोट गरेकाछन्।", "emailnotarget": "प्राप्तकर्ताको रुपमा नभएको अथवा अमान्य प्रयोगकर्ता।", "emailtarget": "प्राप्तकर्ताको प्रयोगकर्ता नाम हाल्नुहोस्", "emailusername": "प्रयोगकर्ता-नाम:", @@ -1901,7 +1901,7 @@ "protect-expiring-local": "समाप्ति समय $1", "protect-expiry-indefinite": "अनिश्चित काल", "protect-cascade": "यो पृष्ठमा संलग्न सुरक्षित पृष्ठहरू(लामबद्द सुरक्षा)", - "protect-cantedit": "तपाईँ यस पृष्ठको सुरक्षा स्तर परिवर्तन गर्न सक्नुहुन्न , किन कि तपाईँलाई यसको सम्पादनको अनुमति छैन ।", + "protect-cantedit": "तपाईं यस पृष्ठको सुरक्षा स्तर परिवर्तन गर्न सक्नुहुन्न , किन कि तपाईंलाई यो काम गर्न अनुमति छैन ।", "protect-othertime": "अरु समय :", "protect-othertime-op": "अरु समय", "protect-existing-expiry": "वर्तमान समय सीमा :$3, $2", @@ -2144,8 +2144,8 @@ "moveuserpage-warning": "'''चेतावनी:''' तपाईंले प्रयोगकर्ता पृष्ठ सार्न आँट्नु भएकोछ। कृपया याद राख्नुहोस् पृष्ठ मात्र सारिने छ र प्रयोगकर्ताको अर्को नाम राख्न '''सकिंदैन'''।", "movecategorypage-warning": "चेतावनी: तपाईं एउटा श्रेणी पृष्ठलाई स्थानान्तरित गर्न जादै हुनुहुन्छ। याद राख्नुहोस् कि मात्रै यो पृष्ठ स्थानान्तरित हुनेछ र पुरानो श्रेणीमा सामेल पृष्ठ नयाँ श्रेणी अन्तर्गत जाने छैन।", "movenologintext": "पृष्ठ सार्नको लागि तपाईं दर्ता गरिएको र [[Special:UserLogin|प्रवेश गरेको]] प्रयोगकर्ता हुनुपर्छ ।", - "movenotallowed": "तपाईँलाई पृष्ठ सार्ने अनुमति छैन", - "movenotallowedfile": "फाइल हटाउने अनुमति तपाईँलाई छैन।", + "movenotallowed": "तपाईंलाई पृष्ठ सार्ने अनुमति छैन", + "movenotallowedfile": "फाइल हटाउने अनुमति तपाईंलाई छैन।", "cant-move-user-page": "तपाईसँग प्रयोगकर्ता पृष्ठहरू सार्न अनुमती छैन (सहपृष्ठहरू बाहेक)", "cant-move-to-user-page": "तपाईंलाई पृष्ठहरू प्रयोगकर्ता पृष्ठमा सार्न अनुमती छैन (प्रयोगकर्ता सहपृष्ठहरूमा बाहेक)", "cant-move-category-page": "तपाईंलाई श्रेणीको पृष्ठहरू सार्ने अनुमति छैन ।", @@ -2290,7 +2290,7 @@ "javascripttest-pagetext-unknownaction": "अज्ञात कारवाही \"$1\" ।", "javascripttest-qunit-intro": "mediawiki.org मा [$1 जाँचको कागजात] हेर्नुहोस् ।", "tooltip-pt-userpage": "{{GENDER:| तपाईंको प्रयोगकर्ता}} पृष्ठ", - "tooltip-pt-anonuserpage": "तपाईँ जुन IP ठेगानाको रुपमा सम्पादन गर्दै हुनुहुन्छ , त्यसको प्रयोगकर्ता पृष्ठ निम्न छ :", + "tooltip-pt-anonuserpage": "तपाईं जुन IP ठेगानाको रुपमा सम्पादन गर्दै हुनुहुन्छ , त्यसको प्रयोगकर्ता पृष्ठ निम्न छ :", "tooltip-pt-mytalk": "{{GENDER:|तपाईंको}} वार्ता पृष्ठ", "tooltip-pt-anontalk": "यो IP ठेगानाबाट गरिएका सम्पादनका बारेमा बार्तालाप", "tooltip-pt-preferences": "{{GENDER:|तपाईंका}} अभिरुचिहरू", @@ -3092,7 +3092,7 @@ "specialpages-group-highuse": "उच्च प्रयोग भएका पृष्ठहरू", "specialpages-group-pages": "पृष्ठहरूको सूची:", "specialpages-group-pagetools": "पृष्ठ उपकरणहरू", - "specialpages-group-wiki": "डेटा र औजारहरु", + "specialpages-group-wiki": "डेटा र औजारहरू", "specialpages-group-redirects": "विशेष पृष्ठमा पठाउने", "specialpages-group-spam": "स्पाम उपकरणहरू", "specialpages-group-developer": "विकासकर्ता उपकरणहरू", @@ -3122,7 +3122,7 @@ "tags-activate": "सक्रिय गर्ने", "tags-deactivate": "निष्क्रिय गर्ने", "tags-hitcount": "$1 {{PLURAL:$1|परिवर्तन|परिवर्तनहरू}}", - "tags-manage-no-permission": "ट्याग मिलान गर्नको लागि तपाईँलाई अनुमति छैन।", + "tags-manage-no-permission": "ट्याग मिलान गर्नको लागि तपाईंलाई अनुमति छैन ।", "tags-create-heading": "नयाँ ट्याग बनाउने", "tags-create-explanation": "पुनः निर्धारित रूपले, नवनिर्मित ट्याग प्रयोगकर्ताहरू र बोटहरूको लागी रहनेछ।", "tags-create-tag-name": "ट्याग नाम:", diff --git a/languages/i18n/nl.json b/languages/i18n/nl.json index ed1da625d5..8f06a69013 100644 --- a/languages/i18n/nl.json +++ b/languages/i18n/nl.json @@ -1858,6 +1858,7 @@ "apisandbox-results-fixtoken-fail": "Het ophalen van het token van type \"$1\" is mislukt.", "apisandbox-alert-page": "Velden op deze pagina zijn niet geldig.", "apisandbox-alert-field": "De waarde van dit veld is niet geldig.", + "apisandbox-continue-clear": "Wissen", "booksources": "Boekinformatie", "booksources-search-legend": "Bronnen en gegevens over een boek zoeken", "booksources-search": "Zoeken", @@ -3407,6 +3408,9 @@ "htmlform-cloner-create": "Meer toevoegen", "htmlform-cloner-delete": "Verwijderen", "htmlform-cloner-required": "Ten minste één waarde is vereist.", + "htmlform-date-placeholder": "JJJJ-MM-DD", + "htmlform-time-placeholder": "HH:MM:SS", + "htmlform-datetime-placeholder": "JJJJ-MM-DD HH:MM:SS", "htmlform-title-badnamespace": "[[:$1]] bevindt zich niet in de naamruimte \"{{ns:$2}}\".", "htmlform-title-not-creatable": "\"$1\" is geen paginanaam die aangemaakt kan worden", "htmlform-title-not-exists": "$1 bestaat niet.", diff --git a/languages/i18n/pl.json b/languages/i18n/pl.json index 890bc36f1f..2886de11b9 100644 --- a/languages/i18n/pl.json +++ b/languages/i18n/pl.json @@ -3328,6 +3328,7 @@ "tag-filter-submit": "Filtr", "tag-list-wrapper": "([[Special:Tags|{{PLURAL:$1|Znacznik|Znaczniki}}]]: $2)", "tag-mw-contentmodelchange": "zmiana modelu zawartości", + "tag-mw-contentmodelchange-description": "Edycje, które [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:ChangeContentModel zmieniają model zawartości] strony", "tags-title": "Znaczniki", "tags-intro": "Na tej stronie znajduje się lista znaczników, którymi oprogramowanie może oznaczyć edycje, oraz ich opisy.", "tags-tag": "Nazwa znacznika", diff --git a/languages/i18n/pt.json b/languages/i18n/pt.json index edbcaa025f..ddd47e8f99 100644 --- a/languages/i18n/pt.json +++ b/languages/i18n/pt.json @@ -605,7 +605,7 @@ "botpasswords-update-failed": "Falha ao atualizar o nome do robô \"$1\". Será que foi eliminado?", "botpasswords-created-title": "Criada palavra-passe para o robô", "botpasswords-created-body": "A palavra-passe de robô para o robô \"$1\" do utilizador \"$2\" foi criada.", - "botpasswords-updated-title": "A palavra-passe de robô foi actualizada.", + "botpasswords-updated-title": "A palavra-passe de robô foi atualizada.", "botpasswords-updated-body": "O robô palavra-passe para o nome do robô \"$1\" do utilizador \"$2\" foi atualizado.", "botpasswords-deleted-title": "Palavra-passe de robô eliminada", "botpasswords-deleted-body": "O robô palavra-passe para o nome do robô \"$1\"do utilizador \"$2\" foi eliminado.", @@ -755,7 +755,7 @@ "explainconflict": "A página foi alterada por alguém desde que começou a editá-la.\nA caixa de texto abaixo mostra o texto existente neste momento.\nAs suas mudanças são mostradas na área ao fundo da página.\nTerá de reintegrar as suas mudanças no texto da caixa abaixo.\n'''Só''' o texto desta caixa será gravado quando clicar \"{{int:savearticle}}\".", "yourtext": "O seu texto", "storedversion": "Versão gravada", - "nonunicodebrowser": "'''Aviso: O seu navegador não é compatível com as especificações Unicode.\nFoi activado um sistema de edição alternativo que lhe permite editar as páginas com segurança: os caracteres não-ASCII aparecerão na caixa de edição no formato de códigos hexadecimais.'''", + "nonunicodebrowser": "Aviso: O seu navegador não é compatível com as especificações Unicode.\nFoi ativado um sistema de edição alternativo que lhe permite editar as páginas com segurança: os caracteres não-ASCII aparecerão na caixa de edição no formato de códigos hexadecimais.", "editingold": "'''Aviso: Está a editar uma revisão desatualizada desta página.'''\nSe gravar, todas as mudanças feitas a partir desta revisão serão perdidas.", "yourdiff": "Diferenças", "copyrightwarning": "Note, por favor, que todas as suas contribuições na {{SITENAME}} são consideradas publicadas nos termos da licença $2 (consulte $1 para mais detalhes).\nSe não deseja que o seu texto possa ser inexoravelmente editado e redistribuído, não o envie.\nGarante-nos também que isto é algo escrito por si, ou copiado do domínio público ou de outra fonte de teor livre.
\n'''Não envie conteúdos cujos direitos de autor estão protegidos, sem ter a devida permissão!'''", @@ -1259,7 +1259,7 @@ "grant-group-file-interaction": "Interagir com conteúdo multimédia", "grant-group-watchlist-interaction": "Interagir com a sua lista de vigiados", "grant-group-email": "Enviar correio electrónico", - "grant-group-high-volume": "Realizar actividades em grande quantidade", + "grant-group-high-volume": "Realizar atividades em grande quantidade", "grant-group-customization": "Personalização e preferências", "grant-group-administration": "Executar acções administrativas", "grant-group-private-information": "Aceder aos seus dados privados", @@ -2630,7 +2630,7 @@ "nocredits": "Não há informação disponível sobre os créditos desta página.", "spamprotectiontitle": "Filtro de proteção contra spam", "spamprotectiontext": "A página que deseja gravar foi bloqueada pelo filtro de ''spam''.\nEste bloqueio foi provavelmente causado por uma ligação para um sítio externo que consta da lista negra.", - "spamprotectionmatch": "O seguinte texto activou o filtro de spam: $1", + "spamprotectionmatch": "O seguinte texto ativou o filtro de spam: $1", "spambot_username": "MediaWiki limpeza de spam", "spam_reverting": "A reverter para a última revisão que não contém ligação para $1", "spam_blanking": "Todas as revisões continham ligações para $1; a esvaziar", @@ -2740,6 +2740,7 @@ "newimages-showbots": "Mostrar carregamentos feitos por robôs", "newimages-hidepatrolled": "Ocultar carregamentos patrulhados", "noimages": "Nada para ver.", + "gallery-slideshow-toggle": "Alternar miniaturas", "ilsubmit": "Pesquisar", "bydate": "por data", "sp-newimages-showfrom": "Mostrar novos ficheiros a partir das $2 de $1", @@ -3123,7 +3124,7 @@ "confirmemail_success": "O seu endereço de correio eletrónico foi confirmado.\nPode agora [[Special:UserLogin|autenticar-se]] e desfrutar da wiki.", "confirmemail_loggedin": "O seu endereço de correio eletrónico foi confirmado.", "confirmemail_subject": "Confirmação de endereço de correio eletrónico da {{SITENAME}}", - "confirmemail_body": "Alguém, provavelmente você a partir do endereço IP $1,\nregistou uma conta \"$2\" com este endereço de correio electrónico em {{SITENAME}}.\n\nPara confirmar que esta conta é realmente sua e activar\nas funcionalidades de correio electrónico em {{SITENAME}}, abra a seguinte ligação no seu navegador:\n\n$3\n\nSe a conta *não* é sua, abra a seguinte ligação para cancelar a confirmação do endereço de correio electrónico:\n\n$5\n\nEste código de confirmação expira a $4.", + "confirmemail_body": "Alguém, provavelmente você a partir do endereço IP $1,\nregistou uma conta \"$2\" com este endereço de correio eletrónico em {{SITENAME}}.\n\nPara confirmar que esta conta é realmente sua e ativar\nas funcionalidades de correio eletrónico em {{SITENAME}}, abra a seguinte ligação no seu navegador:\n\n$3\n\nSe a conta *não* é sua, abra a seguinte ligação para cancelar a confirmação do endereço de correio eletrónico:\n\n$5\n\nEste código de confirmação expira a $4.", "confirmemail_body_changed": "Alguém, provavelmente você a partir do endereço IP $1,\nalterou o endereço de correio eletrónico da conta \"$2\" para este em {{SITENAME}}.\n\nPara confirmar que esta conta é realmente sua e reativar\nas funcionalidades de correio eletrónico em {{SITENAME}},\nabra o seguinte ligação no seu navegador:\n\n$3\n\nCaso a conta *não* lhe pertença, abra a seguinte ligação\npara cancelar a confirmação do endereço de correio eletrónico:\n\n$5\n\nEste código de confirmação expira a $4.", "confirmemail_body_set": "Alguém, provavelmente você a partir do endereço IP $1,\ndefiniu o seu endereço de correio eletrónico como correio da conta \"$2\" em {{SITENAME}}.\n\nPara confirmar que esta conta é realmente sua e reativar\nas funcionalidades de correio eletrónico em {{SITENAME}},\nabra a seguinte ligação no seu navegador:\n\n$3\n\nCaso a conta *não* lhe pertença, abra a seguinte ligação\npara cancelar a confirmação do endereço de correio eletrónico:\n\n$5\n\nEste código de confirmação expira a $4.", "confirmemail_invalidated": "Confirmação de endereço de correio eletrónico cancelada", @@ -3520,7 +3521,7 @@ "feedback-thanks": "Obrigado! O seu comentário foi adicionado à página \"[$2 $1]\".", "feedback-thanks-title": "Obrigado!", "feedback-useragent": "Agente de utilizador:", - "searchsuggest-search": "Pesquisa", + "searchsuggest-search": "Pesquisar em {{SITENAME}}", "searchsuggest-containing": "contendo...", "api-error-autoblocked": "O seu endereço de IP foi bloqueado automaticamente, pois foi utilizado por um utilizador bloqueado.", "api-error-badaccess-groups": "Não tem permissão para enviar ficheiros para esta wiki.", @@ -3815,5 +3816,5 @@ "usercssispublic": "Nota: As subpáginas de CSS não devem conter dados confidenciais porque podem ser vistas por outros utilizadores.", "restrictionsfield-badip": "Endereço IP (ou gama de endereços IP) inválido: $1", "restrictionsfield-label": "Gamas de endereços IP permitidas:", - "restrictionsfield-help": "Um endereço IP ou uma gama CIDR por linha. Para activar todos,\nuse
0.0.0.0/0
::/0" + "restrictionsfield-help": "Um endereço IP ou uma gama CIDR por linha. Para ativar todos,\nuse
0.0.0.0/0
::/0" } diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 235bf1ef20..27fa6e85f1 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -4411,5 +4411,7 @@ "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}", "restrictionsfield-badip": "An error message shown when one entered an invalid IP address or range in a restrictions field (such as Special:BotPassword). $1 is the IP address.", "restrictionsfield-label": "Field label shown for restriction fields (e.g. on Special:BotPassword).", - "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword)." + "restrictionsfield-help": "Placeholder text displayed in restriction fields (e.g. on Special:BotPassword).", + "edit-error-short": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-long}}", + "edit-error-long": "Error message. Parameters:\n* $1 - the error details\nSee also:\n* {{msg-mw|edit-error-short}}" } diff --git a/languages/i18n/sl.json b/languages/i18n/sl.json index 36470f5056..4dec6106ba 100644 --- a/languages/i18n/sl.json +++ b/languages/i18n/sl.json @@ -2678,6 +2678,7 @@ "newimages-showbots": "Prikaži nalaganja botov", "newimages-hidepatrolled": "Skrij nadzorovana nalaganja", "noimages": "Nič ni videti.", + "gallery-slideshow-toggle": "Preklopi sličice", "ilsubmit": "Išči", "bydate": "po datumu", "sp-newimages-showfrom": "Prikaži datoteke, naložene od $2, $1 naprej", @@ -3469,7 +3470,7 @@ "feedback-thanks": "Havala! Vaše povratne informacije smo objavili na strani »[$2 $1]«.", "feedback-thanks-title": "Hvala!", "feedback-useragent": "Uporabniški agent:", - "searchsuggest-search": "Iskanje", + "searchsuggest-search": "Iskanje po {{GRAMMAR:dajalnik|{{SITENAME}}}}", "searchsuggest-containing": "vsebujoč ...", "api-error-autoblocked": "Vaš IP-naslov smo samodejno blokirali, saj ga je uporabljal blokiran uporabnik.", "api-error-badaccess-groups": "Nalaganje datotek na ta wiki vam ni dovoljeno.", diff --git a/languages/i18n/sq.json b/languages/i18n/sq.json index c82f7ffd1c..2697621c85 100644 --- a/languages/i18n/sq.json +++ b/languages/i18n/sq.json @@ -49,6 +49,7 @@ "tog-watchdefault": "Shto faqet dhe skedat e redaktuara prej meje tek lista e faqeve nën mbikqyrje", "tog-watchmoves": "Shto faqet dhe skedat e zhvendosura prej meje tek lista e faqeve nën mbikqyrje", "tog-watchdeletion": "Shto faqet dhe skedat e grisura prej meje tek lista e faqeve nën mbikqyrje", + "tog-watchuploads": "Shtoni fotografitë e rreja që ngarkoj në listën mbikëqyrëse", "tog-watchrollback": "Shto faqet ku unë kam kryer një rikthim tek lista ime mbikqyrëse", "tog-minordefault": "Shëno të gjitha redaktimet si të vogla automatikisht", "tog-previewontop": "Trego se si do të duket faqja mbi kutinë redaktimit", @@ -58,7 +59,7 @@ "tog-enotifminoredits": "Më njofto me email edhe kur ka redaktime të vogla të faqeve dhe skedave", "tog-enotifrevealaddr": "Trego adresën time të emailit në emailet njoftuese", "tog-shownumberswatching": "Trego numrin e përdoruesve që vëzhgojnë këtë faqe", - "tog-oldsig": "Nënshkrimi ekzistues:", + "tog-oldsig": "Nënshkrimi juaj ekzistues:", "tog-fancysig": "Mbaje nënshkrimin si wikitekst (pa lidhje automatike)", "tog-uselivepreview": "Trego parapamjen drejtpërdrejt", "tog-forceeditsummary": "Më njofto kur përmbledhjen e redaktimit e lë bosh", @@ -66,6 +67,7 @@ "tog-watchlisthidebots": "Fshih redaktimet e robotëve nga lista e faqeve të vëzhguara", "tog-watchlisthideminor": "Fshih redaktimet e vogla nga lista e faqeve të vëzhguara", "tog-watchlisthideliu": "Fshih redaktimet e përdoruesve nga lista e faqeve të vëzhguara", + "tog-watchlistreloadautomatically": "Rifreskoni listën mbikëqyrëse automatikisht sa herë një filtër ndryshohet (JavaScript e domosdoshme)", "tog-watchlisthideanons": "Fshih redaktimet përdoruesve anonim nga lista e faqeve të vëzhguara", "tog-watchlisthidepatrolled": "Fshih redaktimet e vrojtuara nga lista e faqeve të vëzhguara", "tog-watchlisthidecategorization": "Fshih kategorizimin e faqeve", @@ -169,7 +171,7 @@ "newwindow": "(hapet në një dritare të re)", "cancel": "Anulo", "moredotdotdot": "Më shumë...", - "morenotlisted": "Kjo listë nuk është e plotë.", + "morenotlisted": "Kjo listë mund të mos jetë e plotë.", "mypage": "Faqja", "mytalk": "Diskuto", "anontalk": "Diskutimi", @@ -413,7 +415,6 @@ "yourpasswordagain": "Fusni fjalëkalimin përsëri", "createacct-yourpasswordagain": "Konfirmoni fjalëkalimin", "createacct-yourpasswordagain-ph": "Shtypni fjalëkalimin përsëri", - "remembermypassword": "Mbaj mënd fjalëkalimin tim për tërë vizitat e ardhshme (për një kohë maksimale prej $1 {{PLURAL:$1|dite|ditësh}})", "userlogin-remembermypassword": "Më mbaj të kyçur", "userlogin-signwithsecure": "Përdor lidhje të sigurtë", "yourdomainname": "Faqja juaj", @@ -590,6 +591,7 @@ "minoredit": "Ky është një redaktim i vogël", "watchthis": "Vëzhgoje këtë faqe", "savearticle": "Kryej ndryshimet", + "savechanges": "Ruaj ndryshimet", "publishpage": "Publiko faqen", "publishchanges": "Publiko ndryshimet", "preview": "Shqyrto", @@ -689,10 +691,12 @@ "defaultmessagetext": "Teksti i porosisë së parazgjedhur", "invalid-content-data": "Të pavlefshme të dhënave e përmbajtjes", "editwarning-warning": "Duke e lënë këtë faqe mund të shkaktojë ju për të humbur të gjitha ndryshimet që keni bërë ju.\nNëse ju jeni regjistruar, ju mund të çaktivizoni këtë paralajmërim në \"{{int:prefs-editing}}\" seksionin e preferencave tuaja.", + "content-model-wikitext": "wikitekst", "content-model-text": "tekst i thejshtë", "content-model-javascript": "JavaScript", "content-json-empty-object": "Objekt bosh", "content-json-empty-array": "Fushë boshe", + "deprecated-self-close-category": "Faqet përdorin tag HTML mbyllës jo të sakt", "expensive-parserfunction-warning": "Kujdes: Kjo faqe ka shumë kërkesa që kërkojnë analizë gramatikore të kushtueshme për sistemin.\n\nDuhet të ketë më pakë se $2, {{PLURAL:$2|kërkesë|kërkesa}}, kurse tani {{PLURAL:$1|është $1 kërkesë|janë $1 kërkesa}}.", "expensive-parserfunction-category": "Faqe me shumë shprehje të kushtueshmë për analizë gramatikore", "post-expand-template-inclusion-warning": "'''Kujdes''': Numri i shablloneve që perfshihen është shumë i madh.\nDisa shabllone nuk do të përfshihen.", @@ -903,6 +907,7 @@ "columns": "Kollona:", "searchresultshead": "Kërkimi", "stub-threshold": "Kufiri për formatin e lidhjeve cung (B):", + "stub-threshold-sample-link": "shembull", "stub-threshold-disabled": "Çaktivizuar", "recentchangesdays": "Numri i ditëve të treguara në ndryshime së fundmi:", "recentchangesdays-max": "(maksimum $1 {{PLURAL:$1|dit|ditë}})", @@ -1078,7 +1083,16 @@ "right-managechangetags": "Krijoni dhe fshini [[Special:Tags|tags]] nga baza e të dhënave", "right-applychangetags": "Aplikoni [[Special:Tags|tags]] së bashku me ndryshimet", "right-changetags": "Shtoni dhe të largoni në mënyrë arbitrare [[Special:Tags|tags]] në rishikimet individuale dhe regjistrimet e historikut", + "grant-group-email": "Dërgoni email", + "grant-group-administration": "Kryej veprime administrative", + "grant-group-private-information": "Qasu të dhënave private në lidhje me ju", + "grant-group-other": "Veprimtari të ndryshme", "grant-blockusers": "Blloko dhe zhblloko përdoruesit", + "grant-createaccount": "Krijo llogari", + "grant-createeditmovepage": "Krijoni, redaktoni, dhe lëvizni faqet", + "grant-editmywatchlist": "Redaktoni listën tuaj mbikqyrëse", + "grant-editpage": "Redaktoni faqet ekzistuese", + "grant-editprotected": "Redakto faqet e mbrojtura", "newuserlogpage": "Regjistri i llogarive", "newuserlogpagetext": "Ky është një regjistër i llogarive të fundit që janë hapur", "rightslog": "Regjistri i privilegjeve të përdoruesit", @@ -1137,6 +1151,7 @@ "recentchanges-label-plusminus": "Madhësia e faqes ndryshoi me këtë numër bajtësh", "recentchanges-legend-heading": "Legjenda:", "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} (shiko gjithashtu [[Special:NewPages|listën e faqeve të reja]])", + "recentchanges-submit": "Shfaq", "rcnotefrom": "Më poshtë {{PLURAL:$5|është shfaqur ndryshimi|janë shfaqur ndryshimet}} që nga $3, $4 (deri në $1).", "rclistfrom": "Tregon ndryshime së fundmi duke filluar nga $3 $2", "rcshowhideminor": "$1 redaktimet e vogla", @@ -1157,6 +1172,8 @@ "rcshowhidemine": "$1 redaktimet e mia", "rcshowhidemine-show": "Shfaq", "rcshowhidemine-hide": "Fshih", + "rcshowhidecategorization-show": "Shfaq", + "rcshowhidecategorization-hide": "Fshih", "rclinks": "Trego $1 ndryshime gjatë $2 ditëve
$3", "diff": "ndrysh", "hist": "hist", @@ -1269,9 +1286,17 @@ "upload-copy-upload-invalid-domain": "Ngarkesat e kopjimit nuk janë në dispozicion nga ky domein.", "upload-dialog-title": "Ngarko skedën", "upload-dialog-button-cancel": "Anulo", + "upload-dialog-button-back": "Prapa", "upload-dialog-button-done": "Mbyll", "upload-dialog-button-save": "Ruaj", "upload-dialog-button-upload": "Ngarko", + "upload-form-label-infoform-title": "Detaje", + "upload-form-label-infoform-name": "Emri", + "upload-form-label-usage-title": "Përdorimi", + "upload-form-label-usage-filename": "Emri i skedarit", + "upload-form-label-own-work": "Kjo është e puna ime", + "upload-form-label-infoform-categories": "Kategoritë", + "upload-form-label-infoform-date": "Data", "backend-fail-stream": "Nuk mund të kalojë skedën $1.", "backend-fail-backup": "Nuk mund të rezervojë skedën $1.", "backend-fail-notexists": "Skeda $1 nuk ekziston.", @@ -1339,6 +1364,7 @@ "license": "Licencimi:", "license-header": "Licencimi:", "nolicense": "Asnjë nuk është zgjedhur", + "licenses-edit": "Redakto opsionet e licencës", "license-nopreview": "(Nuk ka parapamje)", "upload_source_url": " (URL e vlefshme, publikisht e përdorshme)", "upload_source_file": " (skeda në kompjuterin tuaj)", @@ -1493,6 +1519,7 @@ "mostrevisions": "Artikuj më të redaktuar", "prefixindex": "Të gjitha faqet me parashtesa", "prefixindex-namespace": "Të gjitha faqet me parashtesë (hapësira $1)", + "prefixindex-submit": "Shfaq", "shortpages": "Artikuj të shkurtër", "longpages": "Artikuj të gjatë", "deadendpages": "Artikuj pa rrugëdalje", @@ -1500,18 +1527,25 @@ "protectedpages": "Faqe të mbrojtura", "protectedpages-indef": "Vetëm mbrojtjet pa afat", "protectedpages-cascade": "Vetëm mbrojtjet", + "protectedpages-noredirect": "Fshih përcjellimet", "protectedpagesempty": "Nuk ka faqe të mbrojtura me të dhënat e kërkuara.", "protectedpages-page": "Faqja", "protectedpages-expiry": "Skadon", "protectedpages-reason": "Arsyeja", + "protectedpages-submit": "Shfaq faqet", + "protectedpages-unknown-timestamp": "E panjohur", + "protectedpages-unknown-performer": "Përdorues i panjohur", "protectedtitles": "Titujt e mbrojtur", + "protectedtitles-summary": "Kjo faqe liston titujt që aktualisht janë të mbrojtura nga krijimi. Për një listë të faqeve ekzistuese që janë të mbrojtura, shih [[{{#special:ProtectedPages}}|{{int:protectedpages}}]].", "protectedtitlesempty": "Asnjë titull i mbrojtur nuk u gjet në këtë hapësirë.", + "protectedtitles-submit": "Shfaq titujt", "listusers": "Lista e përdoruesve", "listusers-editsonly": "Trego vetëm përdoruesit me redaktime", "listusers-creationsort": "Radhiti sipas datës së krijimit", "usereditcount": "$1 {{PLURAL:$1|redaktim|redaktime}}", "usercreated": "{{GENDER:$3|Krijuar}} më $1 në $2", "newpages": "Artikuj të rinj", + "newpages-submit": "Shfaq", "newpages-username": "Përdoruesi:", "ancientpages": "Artikuj më të vjetër", "move": "Zhvendose", @@ -1526,7 +1560,20 @@ "pager-older-n": "{{PLURAL:$1|1 më i vjetër|$1 më të vjetra}}", "suppress": "Shtypur", "querypage-disabled": "Kjo faqe speciale është çaktivizuar për arsye të performancës.", + "apihelp": "API ndihmë", "apihelp-no-such-module": "Moduli \"$1\" nuk u gjet.", + "apisandbox": "API livadhi", + "apisandbox-jsonly": "JavaScript është e domosdoshme që të përdorni API livadhi.", + "apisandbox-api-disabled": "API nuk është në dispozicion për këtë faqe.", + "apisandbox-unfullscreen": "Shfaq faqen", + "apisandbox-submit": "Bëj kërkesë", + "apisandbox-reset": "Pastro", + "apisandbox-retry": "Riprovo", + "apisandbox-examples": "Shembuj", + "apisandbox-dynamic-parameters": "Parametra shtesë", + "apisandbox-dynamic-parameters-add-label": "Shto parametër:", + "apisandbox-dynamic-parameters-add-placeholder": "Emri i parametrit", + "apisandbox-results": "Rezultatet", "booksources": "Burime librash", "booksources-search-legend": "Kërkim burimor librash", "booksources-search": "Kërko", @@ -2777,8 +2824,6 @@ "htmlform-submit": "Dërgo", "htmlform-reset": "Zhbëj ndryshimin", "htmlform-selectorother-other": "Gjitha", - "sqlite-has-fts": "$1 me mbështetje të kërkimit me teskt të plotë", - "sqlite-no-fts": "$1 pa mbështetje të kërkimit me teskt të plotë", "logentry-delete-delete": "$1 {{GENDER:$2|grisi}} faqen $3", "logentry-delete-restore": "$1 {{GENDER:$2|riktheu}} faqen $3", "logentry-delete-event": "$1 {{GENDER:$2|ndryshoi}} dukshmërinë e {{PLURAL:$5|e një ngjarjeje regjistri|$5 ngjarjeve regjistri}} në $3: $4", diff --git a/languages/i18n/uk.json b/languages/i18n/uk.json index 66f355dade..0fe4217c88 100644 --- a/languages/i18n/uk.json +++ b/languages/i18n/uk.json @@ -66,7 +66,8 @@ "Mix Gerder", "E.belykh", "Visem", - "MMH" + "MMH", + "Олександр" ] }, "tog-underline": "Підкреслювання посилань:", @@ -2757,6 +2758,7 @@ "newimages-showbots": "Показати завантаження ботами", "newimages-hidepatrolled": "Приховати відпатрульовані завантаження", "noimages": "Файли відсутні.", + "gallery-slideshow-toggle": "Перемикання мініатюр", "ilsubmit": "Шукати", "bydate": "за датою", "sp-newimages-showfrom": "Показати нові зображення, починаючи з $2, $1", diff --git a/languages/i18n/vro.json b/languages/i18n/vro.json index d11a63988d..84edb2975e 100644 --- a/languages/i18n/vro.json +++ b/languages/i18n/vro.json @@ -292,6 +292,7 @@ "nstab-template": "Näüdüs", "nstab-help": "Oppus", "nstab-category": "Katõgooria", + "mainpage-nstab": "Pääleht", "nosuchaction": "Säänest tallitust olõ-i.", "nosuchactiontext": "Seo aadrõsi manoq käüvä tallitus om viganõ.\nVõimalik, et sa kirotit aadrõsi võlssi vai pruugõt vigast linki.\nNiisama või taa ollaq {{SITENAME}} tarkvara viga.", "nosuchspecialpage": "Säänest tallituslehekülge olõ-i.", @@ -366,7 +367,6 @@ "yourpasswordagain": "Kirodaq viilkõrd salasõna", "createacct-yourpasswordagain": "Kinnüdäq uma salasõna", "createacct-yourpasswordagain-ph": "Kirodaq salasõna vahtsõst", - "remembermypassword": "Jätäq salasõna miilde (kooniq $1 {{PLURAL:$1|pääväs|pääväs}})", "userlogin-remembermypassword": "Jääq uma nimega sisse", "userlogin-signwithsecure": "Pruugiq kaidsõtut võrgoütistüst", "yourdomainname": "Võrgonimi", @@ -577,7 +577,6 @@ "undo-success": "Tagasivõtminõ läts' kõrda. Kaeq üle, kas taa om tuu, midä sa tetäq tahtsõt ja pästäq muutusõq.", "undo-failure": "Tagasivõtminõ lää-s kõrda samal aol tettüide muutmiisi vastaolo peräst. Võit muutusõq käsilde tagasi võttaq.", "undo-summary": "Tagasi võet muutminõ #$1, mink tekk' [[Special:Contributions/$2|$2]] ([[User talk:$2|Arotus]])", - "cantcreateaccounttitle": "Pruukjanime luuminõ lää-s kõrda", "cantcreateaccount-text": "Pruukjanime luuminõ taa puutri võrgoaadrõsi päält ('''$1''') om ärq keelet. Kiildjä: [[User:$3|$3]].\n\n$3 kirjäpant põhjus: ''$2''", "viewpagelogs": "Kaeq seo lehe muutmisnimekirjä.", "nohistory": "Seo leheküle pääl ei olõq vanõmbit kujjõ.", @@ -670,7 +669,7 @@ "searchprofile-advanced-tooltip": "Otsiq etteannõtuist nimeruumõst", "search-result-size": "$1 ({{PLURAL:$2|1 sõna|$2 sõnna}})", "search-result-category-size": "{{PLURAL:$1|1 lehekülg|$1 lehekülge}} ({{PLURAL:$2|1 alambkatõgooria|$2 alambkatõgooriat}}, {{PLURAL:$3|1 fail|$3 faili}})", - "search-redirect": "(ümbresaatminõ $1)", + "search-redirect": "(ümbresaatminõ lehelt $1)", "search-section": "(alljago $1)", "search-suggest": "Kas mõtlit: $1", "search-interwiki-caption": "Sõsarprojektiq", @@ -1195,6 +1194,7 @@ "contributions": "{{GENDER:$1|Pruukja}} toimõndusõq", "contributions-title": "Pruukja $1 toimõndusõq", "mycontris": "Hindä kirotusõq", + "anoncontribs": "Hindä kirotusõq", "contribsub2": "Pruukja {{GENDER:$3|$1}} ($2) toimõndusõq", "nocontribs": "Sääntsit muutmiisi es lövväq.", "uctop": "(parhillanõ)", @@ -1368,16 +1368,16 @@ "import-logentry-interwiki-detail": "$1 {{PLURAL:$1|kujo|kujjo}} lehest $2", "tooltip-pt-userpage": "Suq pruukjaleht", "tooltip-pt-anonuserpage": "Su puutri võrgoaadrõsi pruukjaleht", - "tooltip-pt-mytalk": "Mu arotusleht", + "tooltip-pt-mytalk": "{{GENDER:|Muq arotusleht}}", "tooltip-pt-anontalk": "Arotus taa puutri võrgoaadrõsi päält tettüisi toimõnduisi üle", - "tooltip-pt-preferences": "Mu säädmiseq", + "tooltip-pt-preferences": "{{GENDER:|Mu säädmiseq}}", "tooltip-pt-watchlist": "Nimekiri lehist, mil tahtnuq silmä pääl hoitaq", "tooltip-pt-mycontris": "Suq toimõnduisi nimekiri", "tooltip-pt-login": "Mineq nimega sisse vai tiiq hindäle pruukjanimi (soovitav).", "tooltip-pt-logout": "Mineq nime alt vällä", "tooltip-pt-createaccount": "Tuu olõ-õi joht kohustuslik, a sul tasos luvvaq konto ja nimega sisse minnäq.", "tooltip-ca-talk": "Arotus lehe sisu üle", - "tooltip-ca-edit": "Saa võit taad lehte toimõndaq.", + "tooltip-ca-edit": "Toimõndaq seod lehte", "tooltip-ca-addsection": "Tiiq vahtsõnõ alljago", "tooltip-ca-viewsource": "Taa om kaidsõt leht. Saat kaiaq õnnõ taa lättekuudi.", "tooltip-ca-history": "Taa lehe vanõmbaq kujoq.", @@ -1402,7 +1402,7 @@ "tooltip-t-recentchangeslinked": "Viimädseq muutmisõq lehile, mink pääle näüdätäs linkega seo lehe päält", "tooltip-feed-rss": "Taa lehe RSS-kujo", "tooltip-feed-atom": "Taa lehe Atom-kujo", - "tooltip-t-contributions": "Näütäq taa pruukja toimõnduisi nimekirjä", + "tooltip-t-contributions": "Näütäq {{GENDER:$1|seo pruukja}} toimõnduisi nimekirjä", "tooltip-t-emailuser": "Saadaq taalõ pruukjalõ e-kiri", "tooltip-t-upload": "Laadiq üles teedüstüid", "tooltip-t-specialpages": "Näütäq tallituslehekülgi", @@ -1411,7 +1411,7 @@ "tooltip-ca-nstab-main": "Näütäq sisulehekülge", "tooltip-ca-nstab-user": "Näütäq pruukjalehekülge", "tooltip-ca-nstab-media": "Näütäq meediälehekülge", - "tooltip-ca-nstab-special": "Taa om tallituslehekülg", + "tooltip-ca-nstab-special": "Seo om tallituslehekülg, seod saa-ai toimõndaq", "tooltip-ca-nstab-project": "Näütäq projektilehekülge", "tooltip-ca-nstab-image": "Näütäq teedüstü lehekülge", "tooltip-ca-nstab-mediawiki": "Näütäq tallitusteedüst", @@ -1445,7 +1445,7 @@ "spambot_username": "MediaWiki prahihäötäjä", "spam_reverting": "Tagasi pööret viimädse kujo pääle, koh olõ-i linke lehele $1", "spam_blanking": "Kõigin kujõn oll' linke lehele $1. Leht tühäs tett.", - "simpleantispam-label": "Rämpspostikontroll.\n'''ÄRQ''' täütkuq seod väljä!", + "simpleantispam-label": "Rämpspostikontroll.\nÄrq täütkuq seod väljä!", "pageinfo-toolboxlink": "Leheküle andmõq", "markaspatrolleddiff": "Märgiq ülekaetus", "markaspatrolledtext": "Märgiq toimõndus ülekaetus", diff --git a/maintenance/dev/includes/router.php b/maintenance/dev/includes/router.php index 97c8954a77..cdef7e0637 100644 --- a/maintenance/dev/includes/router.php +++ b/maintenance/dev/includes/router.php @@ -63,7 +63,8 @@ if ( $ext == 'php' || $ext == 'php5' ) { return true; } $mime = false; -$lines = explode( "\n", file_get_contents( "includes/mime.types" ) ); +// Borrow mime type file from MimeAnalyzer +$lines = explode( "\n", file_get_contents( "includes/libs/mime/mime.types" ) ); foreach ( $lines as $line ) { $exts = explode( " ", $line ); $mime = array_shift( $exts ); diff --git a/resources/Resources.php b/resources/Resources.php index 2b7a644bed..2e4a15dc60 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1812,11 +1812,13 @@ return [ 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.apisandbox.styles' => [ + 'targets' => [ 'desktop', 'mobile' ], 'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.top.css', ], 'mediawiki.special.apisandbox' => [ 'styles' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.css', 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.apisandbox.js', + 'targets' => [ 'desktop', 'mobile' ], 'dependencies' => [ 'mediawiki.api', 'mediawiki.jqueryMsg', diff --git a/resources/src/mediawiki.legacy/commonPrint.css b/resources/src/mediawiki.legacy/commonPrint.css index d387a2d7bb..ec94df34e4 100644 --- a/resources/src/mediawiki.legacy/commonPrint.css +++ b/resources/src/mediawiki.legacy/commonPrint.css @@ -10,10 +10,9 @@ * Hide all the elements irrelevant for printing */ .noprint, -div#jump-to-nav, +#jump-to-nav, .mw-jump, -div.top, -div#column-one, +#column-one, .mw-editsection, .mw-editsection-like, #footer-places, @@ -21,12 +20,12 @@ div#column-one, .usermessage, .patrollink, .ns-0 .mw-redirectedfrom, -div.magnify, +.magnify, #mw-navigation, #siteNotice, /* Deprecated, changed in core */ -div#f-poweredbyico, -div#f-copyrightico, +#f-poweredbyico, +#f-copyrightico, li#about, li#disclaimer, li#mobileview, @@ -228,7 +227,7 @@ div.floatleft p { font-style: italic; } -div.center { +.center { text-align: center; } @@ -246,7 +245,7 @@ div.thumb { div.thumbinner { background-color: #fff; border: 1pt solid #ccc; - padding: 3px !important; + padding: 3px; font-size: 94%; text-align: center; /* new block formatting context, @@ -262,7 +261,7 @@ html .thumbcaption { border: none; text-align: left; line-height: 1.4em; - padding: 3px !important; + padding: 3px; font-size: 94%; } @@ -325,10 +324,6 @@ table.listing td { border-collapse: collapse; } -a.sortheader { - margin: 0 0.3em; -} - /** * Categories */ diff --git a/tests/phpunit/includes/MediaWikiServicesTest.php b/tests/phpunit/includes/MediaWikiServicesTest.php index f054c0e4f5..0ff903f0ec 100644 --- a/tests/phpunit/includes/MediaWikiServicesTest.php +++ b/tests/phpunit/includes/MediaWikiServicesTest.php @@ -319,6 +319,7 @@ class MediaWikiServicesTest extends MediaWikiTestCase { 'LinkRenderer' => [ 'LinkRenderer', LinkRenderer::class ], 'LinkRendererFactory' => [ 'LinkRendererFactory', LinkRendererFactory::class ], '_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ], + 'MimeAnalyzer' => [ 'MimeAnalyzer', MimeAnalyzer::class ], 'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ], 'TitleParser' => [ 'TitleParser', TitleParser::class ], 'ProxyLookup' => [ 'ProxyLookup', ProxyLookup::class ], diff --git a/tests/phpunit/includes/MimeMagicTest.php b/tests/phpunit/includes/MimeMagicTest.php deleted file mode 100644 index e00cf0ce30..0000000000 --- a/tests/phpunit/includes/MimeMagicTest.php +++ /dev/null @@ -1,51 +0,0 @@ -mimeMagic = MimeMagic::singleton(); - parent::setUp(); - } - - /** - * @dataProvider providerImproveTypeFromExtension - * @param string $ext File extension (no leading dot) - * @param string $oldMime Initially detected MIME - * @param string $expectedMime MIME type after taking extension into account - */ - function testImproveTypeFromExtension( $ext, $oldMime, $expectedMime ) { - $actualMime = $this->mimeMagic->improveTypeFromExtension( $oldMime, $ext ); - $this->assertEquals( $expectedMime, $actualMime ); - } - - function providerImproveTypeFromExtension() { - return [ - [ 'gif', 'image/gif', 'image/gif' ], - [ 'gif', 'unknown/unknown', 'unknown/unknown' ], - [ 'wrl', 'unknown/unknown', 'model/vrml' ], - [ 'txt', 'text/plain', 'text/plain' ], - [ 'csv', 'text/plain', 'text/csv' ], - [ 'tsv', 'text/plain', 'text/tab-separated-values' ], - [ 'js', 'text/javascript', 'application/javascript' ], - [ 'js', 'application/x-javascript', 'application/javascript' ], - [ 'json', 'text/plain', 'application/json' ], - [ 'foo', 'application/x-opc+zip', 'application/zip' ], - [ 'docx', 'application/x-opc+zip', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ], - [ 'djvu', 'image/x-djvu', 'image/vnd.djvu' ], - [ 'wav', 'audio/wav', 'audio/wav' ], - ]; - } - - /** - * Test to make sure that encoder=ffmpeg2theora doesn't trigger - * MEDIATYPE_VIDEO (bug 63584) - */ - function testOggRecognize() { - $oggFile = __DIR__ . '/../data/media/say-test.ogg'; - $actualType = $this->mimeMagic->getMediaType( $oggFile, 'application/ogg' ); - $this->assertEquals( $actualType, MEDIATYPE_AUDIO ); - } -} diff --git a/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php b/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php new file mode 100644 index 0000000000..85927a393d --- /dev/null +++ b/tests/phpunit/includes/libs/mime/MimeAnalyzerTest.php @@ -0,0 +1,62 @@ +mimeAnalyzer = new MimeAnalyzer( [ + 'infoFile' => $IP . "/includes/libs/mime/mime.info", + 'typeFile' => $IP . "/includes/libs/mime/mime.types", + 'xmlTypes' => [ + 'http://www.w3.org/2000/svg:svg' => 'image/svg+xml', + 'svg' => 'image/svg+xml', + 'http://www.lysator.liu.se/~alla/dia/:diagram' => 'application/x-dia-diagram', + 'http://www.w3.org/1999/xhtml:html' => 'text/html', // application/xhtml+xml? + 'html' => 'text/html', // application/xhtml+xml? + ] + ] ); + parent::setUp(); + } + + /** + * @dataProvider providerImproveTypeFromExtension + * @param string $ext File extension (no leading dot) + * @param string $oldMime Initially detected MIME + * @param string $expectedMime MIME type after taking extension into account + */ + function testImproveTypeFromExtension( $ext, $oldMime, $expectedMime ) { + $actualMime = $this->mimeAnalyzer->improveTypeFromExtension( $oldMime, $ext ); + $this->assertEquals( $expectedMime, $actualMime ); + } + + function providerImproveTypeFromExtension() { + return [ + [ 'gif', 'image/gif', 'image/gif' ], + [ 'gif', 'unknown/unknown', 'unknown/unknown' ], + [ 'wrl', 'unknown/unknown', 'model/vrml' ], + [ 'txt', 'text/plain', 'text/plain' ], + [ 'csv', 'text/plain', 'text/csv' ], + [ 'tsv', 'text/plain', 'text/tab-separated-values' ], + [ 'js', 'text/javascript', 'application/javascript' ], + [ 'js', 'application/x-javascript', 'application/javascript' ], + [ 'json', 'text/plain', 'application/json' ], + [ 'foo', 'application/x-opc+zip', 'application/zip' ], + [ 'docx', 'application/x-opc+zip', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ], + [ 'djvu', 'image/x-djvu', 'image/vnd.djvu' ], + [ 'wav', 'audio/wav', 'audio/wav' ], + ]; + } + + /** + * Test to make sure that encoder=ffmpeg2theora doesn't trigger + * MEDIATYPE_VIDEO (bug 63584) + */ + function testOggRecognize() { + $oggFile = __DIR__ . '/../../../data/media/say-test.ogg'; + $actualType = $this->mimeAnalyzer->getMediaType( $oggFile, 'application/ogg' ); + $this->assertEquals( $actualType, MEDIATYPE_AUDIO ); + } +}