* (bug 69789) Title::getContentModel() now loads from the database when
necessary instead of incorrectly returning the default content model.
* (bug 69249) wfBaseConvert() now works around PHP Bug #50175 when using GMP.
+* (bug 57909) URLs in the externallinks table will no longer have certain
+ characters decoded in the query string.
=== Action API changes in 1.24 ===
* action=parse API now supports prop=modules, which provides the list of
Currently unused, but planned to provide support for marking individual
language links in the UI, e.g. for featured articles.
+'LanguageSelector': Hook to change the language selector available on a page.
+$out: The output page.
+$cssClassName: CSS class name of the language selector.
+
'LinkBegin': Used when generating internal and interwiki links in
Linker::link(), before processing starts. Return false to skip default
processing and return $ret. See documentation for Linker::link() for details on
'Message' => 'includes/Message.php',
'MessageBlobStore' => 'includes/MessageBlobStore.php',
'MimeMagic' => 'includes/MimeMagic.php',
+ 'MovePage' => 'includes/MovePage.php',
'MWHookException' => 'includes/Hooks.php',
'MWHttpRequest' => 'includes/HttpFunctions.php',
'MWNamespace' => 'includes/MWNamespace.php',
'SVGReader' => 'includes/media/SVGMetadataExtractor.php',
'ThumbnailImage' => 'includes/media/MediaTransformOutput.php',
'TiffHandler' => 'includes/media/Tiff.php',
+ 'TransformationalImageHandler' => 'includes/media/TransformationalImageHandler.php',
'TransformParameterError' => 'includes/media/MediaTransformOutput.php',
'XCFHandler' => 'includes/media/XCF.php',
'XMPInfo' => 'includes/media/XMPInfo.php',
'includes/resourceloader/DerivativeResourceLoaderContext.php',
'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php',
'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php',
+ 'ResourceLoaderEditToolbarModule' => 'includes/resourceloader/ResourceLoaderEditToolbarModule.php',
'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderFileModule.php',
'ResourceLoaderFilePageModule' => 'includes/resourceloader/ResourceLoaderFilePageModule.php',
'ResourceLoaderFilePath' => 'includes/resourceloader/ResourceLoaderFilePath.php',
( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
( $bot ? EDIT_FORCE_BOT : 0 );
- $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
- false, null, $this->contentFormat );
+ $doEditStatus = $this->mArticle->doEditContent(
+ $content,
+ $this->summary,
+ $flags,
+ false,
+ null,
+ $content->getDefaultFormat()
+ );
if ( !$doEditStatus->isOK() ) {
// Failure from doEdit()
// Do this in its own transaction to reduce contention...
$dbw = wfGetDB( DB_MASTER );
$dbw->onTransactionIdle( function () use ( $dbw, $title, $watch, $wgUser, $fname ) {
- $dbw->begin( $fname );
WatchAction::doWatchOrUnwatch( $watch, $title, $wgUser );
- $dbw->commit( $fname );
} );
}
}
* @return string
*/
static function getEditToolbar() {
- global $wgStylePath, $wgContLang, $wgLang, $wgOut;
+ global $wgContLang, $wgOut;
global $wgEnableUploads, $wgForeignFileRepos;
$imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
/**
* $toolarray is an array of arrays each of which includes the
- * filename of the button image (without path), the opening
- * tag, the closing tag, optionally a sample text that is
+ * opening tag, the closing tag, optionally a sample text that is
* inserted between the two when no selection is highlighted
* and. The tip text is shown when the user moves the mouse
* over the button.
+ *
+ * Images are defined in ResourceLoaderEditToolbarModule.
*/
$toolarray = array(
array(
- 'image' => $wgLang->getImageFile( 'button-bold' ),
'id' => 'mw-editbutton-bold',
'open' => '\'\'\'',
'close' => '\'\'\'',
'tip' => wfMessage( 'bold_tip' )->text(),
),
array(
- 'image' => $wgLang->getImageFile( 'button-italic' ),
'id' => 'mw-editbutton-italic',
'open' => '\'\'',
'close' => '\'\'',
'tip' => wfMessage( 'italic_tip' )->text(),
),
array(
- 'image' => $wgLang->getImageFile( 'button-link' ),
'id' => 'mw-editbutton-link',
'open' => '[[',
'close' => ']]',
'tip' => wfMessage( 'link_tip' )->text(),
),
array(
- 'image' => $wgLang->getImageFile( 'button-extlink' ),
'id' => 'mw-editbutton-extlink',
'open' => '[',
'close' => ']',
'tip' => wfMessage( 'extlink_tip' )->text(),
),
array(
- 'image' => $wgLang->getImageFile( 'button-headline' ),
'id' => 'mw-editbutton-headline',
'open' => "\n== ",
'close' => " ==\n",
'tip' => wfMessage( 'headline_tip' )->text(),
),
$imagesAvailable ? array(
- 'image' => $wgLang->getImageFile( 'button-image' ),
'id' => 'mw-editbutton-image',
'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
'close' => ']]',
'tip' => wfMessage( 'image_tip' )->text(),
) : false,
$imagesAvailable ? array(
- 'image' => $wgLang->getImageFile( 'button-media' ),
'id' => 'mw-editbutton-media',
'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
'close' => ']]',
'tip' => wfMessage( 'media_tip' )->text(),
) : false,
array(
- 'image' => $wgLang->getImageFile( 'button-nowiki' ),
'id' => 'mw-editbutton-nowiki',
'open' => "<nowiki>",
'close' => "</nowiki>",
'tip' => wfMessage( 'nowiki_tip' )->text(),
),
array(
- 'image' => $wgLang->getImageFile( 'button-sig' ),
'id' => 'mw-editbutton-signature',
'open' => '--~~~~',
'close' => '',
'tip' => wfMessage( 'sig_tip' )->text(),
),
array(
- 'image' => $wgLang->getImageFile( 'button-hr' ),
'id' => 'mw-editbutton-hr',
'open' => "\n----\n",
'close' => '',
}
$params = array(
- $wgStylePath . '/common/images/' . $tool['image'],
+ // Images are defined in ResourceLoaderEditToolbarModule
+ false,
// Note that we use the tip both for the ALT tag and the TITLE tag of the image.
// Older browsers show a "speedtip" type message only for ALT.
// Ideally these should be different, realistically they
*
* This function replaces all old wfMsg* functions.
*
- * @param string $key Message key
+ * @param string|string[] $key Message key, or array of keys
* @param mixed $params,... Normal message parameters
* @return Message
*
--- /dev/null
+<?php
+
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Handles the backend logic of moving a page from one title
+ * to another.
+ *
+ * @since 1.24
+ */
+class MovePage {
+
+ /**
+ * @var Title
+ */
+ protected $oldTitle;
+
+ /**
+ * @var Title
+ */
+ protected $newTitle;
+
+ public function __construct( Title $oldTitle, Title $newTitle ) {
+ $this->oldTitle = $oldTitle;
+ $this->newTitle = $newTitle;
+ }
+
+ /**
+ * @param User $user
+ * @param string $reason
+ * @param bool $createRedirect
+ * @return Status
+ */
+ public function move( User $user, $reason, $createRedirect ) {
+ global $wgCategoryCollation;
+
+ // If it is a file, move it first.
+ // It is done before all other moving stuff is done because it's hard to revert.
+ $dbw = wfGetDB( DB_MASTER );
+ if ( $this->oldTitle->getNamespace() == NS_FILE ) {
+ $file = wfLocalFile( $this->oldTitle );
+ if ( $file->exists() ) {
+ $status = $file->move( $this->newTitle );
+ if ( !$status->isOk() ) {
+ return $status;
+ }
+ }
+ // Clear RepoGroup process cache
+ RepoGroup::singleton()->clearCache( $this->oldTitle );
+ RepoGroup::singleton()->clearCache( $this->newTitle ); # clear false negative cache
+ }
+
+ $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own.
+ $pageid = $this->oldTitle->getArticleID( Title::GAID_FOR_UPDATE );
+ $protected = $this->oldTitle->isProtected();
+
+ // Do the actual move
+ $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect );
+
+ // Refresh the sortkey for this row. Be careful to avoid resetting
+ // cl_timestamp, which may disturb time-based lists on some sites.
+ // @todo This block should be killed, it's duplicating code
+ // from LinksUpdate::getCategoryInsertions() and friends.
+ $prefixes = $dbw->select(
+ 'categorylinks',
+ array( 'cl_sortkey_prefix', 'cl_to' ),
+ array( 'cl_from' => $pageid ),
+ __METHOD__
+ );
+ if ( $this->newTitle->getNamespace() == NS_CATEGORY ) {
+ $type = 'subcat';
+ } elseif ( $this->newTitle->getNamespace() == NS_FILE ) {
+ $type = 'file';
+ } else {
+ $type = 'page';
+ }
+ foreach ( $prefixes as $prefixRow ) {
+ $prefix = $prefixRow->cl_sortkey_prefix;
+ $catTo = $prefixRow->cl_to;
+ $dbw->update( 'categorylinks',
+ array(
+ 'cl_sortkey' => Collation::singleton()->getSortKey(
+ $this->newTitle->getCategorySortkey( $prefix ) ),
+ 'cl_collation' => $wgCategoryCollation,
+ 'cl_type' => $type,
+ 'cl_timestamp=cl_timestamp' ),
+ array(
+ 'cl_from' => $pageid,
+ 'cl_to' => $catTo ),
+ __METHOD__
+ );
+ }
+
+ $redirid = $this->oldTitle->getArticleID();
+
+ if ( $protected ) {
+ # Protect the redirect title as the title used to be...
+ $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
+ array(
+ 'pr_page' => $redirid,
+ 'pr_type' => 'pr_type',
+ 'pr_level' => 'pr_level',
+ 'pr_cascade' => 'pr_cascade',
+ 'pr_user' => 'pr_user',
+ 'pr_expiry' => 'pr_expiry'
+ ),
+ array( 'pr_page' => $pageid ),
+ __METHOD__,
+ array( 'IGNORE' )
+ );
+ # Update the protection log
+ $log = new LogPage( 'protect' );
+ $comment = wfMessage(
+ 'prot_1movedto2',
+ $this->oldTitle->getPrefixedText(),
+ $this->newTitle->getPrefixedText()
+ )->inContentLanguage()->text();
+ if ( $reason ) {
+ $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
+ }
+ // @todo FIXME: $params?
+ $logId = $log->addEntry(
+ 'move_prot',
+ $this->newTitle,
+ $comment,
+ array( $this->oldTitle->getPrefixedText() ),
+ $user
+ );
+
+ // reread inserted pr_ids for log relation
+ $insertedPrIds = $dbw->select(
+ 'page_restrictions',
+ 'pr_id',
+ array( 'pr_page' => $redirid ),
+ __METHOD__
+ );
+ $logRelationsValues = array();
+ foreach ( $insertedPrIds as $prid ) {
+ $logRelationsValues[] = $prid->pr_id;
+ }
+ $log->addRelations( 'pr_id', $logRelationsValues, $logId );
+ }
+
+ // Update *_from_namespace fields as needed
+ if ( $this->oldTitle->getNamespace() != $this->newTitle->getNamespace() ) {
+ $dbw->update( 'pagelinks',
+ array( 'pl_from_namespace' => $this->newTitle->getNamespace() ),
+ array( 'pl_from' => $pageid ),
+ __METHOD__
+ );
+ $dbw->update( 'templatelinks',
+ array( 'tl_from_namespace' => $this->newTitle->getNamespace() ),
+ array( 'tl_from' => $pageid ),
+ __METHOD__
+ );
+ $dbw->update( 'imagelinks',
+ array( 'il_from_namespace' => $this->newTitle->getNamespace() ),
+ array( 'il_from' => $pageid ),
+ __METHOD__
+ );
+ }
+
+ # Update watchlists
+ $oldtitle = $this->oldTitle->getDBkey();
+ $newtitle = $this->newTitle->getDBkey();
+ $oldsnamespace = MWNamespace::getSubject( $this->oldTitle->getNamespace() );
+ $newsnamespace = MWNamespace::getSubject( $this->newTitle->getNamespace() );
+ if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
+ WatchedItem::duplicateEntries( $this->oldTitle, $this->newTitle );
+ }
+
+ $dbw->commit( __METHOD__ );
+
+ wfRunHooks( 'TitleMoveComplete', array( &$this->oldTitle, &$this->newTitle, &$user, $pageid, $redirid, $reason ) );
+ return Status::newGood();
+
+ }
+
+ /**
+ * Move page to a title which is either a redirect to the
+ * source page or nonexistent
+ *
+ * @fixme This was basically directly moved from Title, it should be split into smaller functions
+ * @param User $user the User doing the move
+ * @param Title $nt The page to move to, which should be a redirect or nonexistent
+ * @param string $reason The reason for the move
+ * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
+ * if the user has the suppressredirect right
+ * @throws MWException
+ */
+ private function moveToInternal( User $user, &$nt, $reason = '', $createRedirect = true ) {
+ global $wgContLang;
+
+ if ( $nt->exists() ) {
+ $moveOverRedirect = true;
+ $logType = 'move_redir';
+ } else {
+ $moveOverRedirect = false;
+ $logType = 'move';
+ }
+
+ if ( $createRedirect ) {
+ if ( $this->oldTitle->getNamespace() == NS_CATEGORY
+ && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
+ ) {
+ $redirectContent = new WikitextContent(
+ wfMessage( 'category-move-redirect-override' )
+ ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
+ } else {
+ $contentHandler = ContentHandler::getForTitle( $this->oldTitle );
+ $redirectContent = $contentHandler->makeRedirectContent( $nt,
+ wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
+ }
+
+ // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
+ } else {
+ $redirectContent = null;
+ }
+
+ // bug 57084: log_page should be the ID of the *moved* page
+ $oldid = $this->oldTitle->getArticleID();
+ $logTitle = clone $this->oldTitle;
+
+ $logEntry = new ManualLogEntry( 'move', $logType );
+ $logEntry->setPerformer( $user );
+ $logEntry->setTarget( $logTitle );
+ $logEntry->setComment( $reason );
+ $logEntry->setParameters( array(
+ '4::target' => $nt->getPrefixedText(),
+ '5::noredir' => $redirectContent ? '0': '1',
+ ) );
+
+ $formatter = LogFormatter::newFromEntry( $logEntry );
+ $formatter->setContext( RequestContext::newExtraneousContext( $this->oldTitle ) );
+ $comment = $formatter->getPlainActionText();
+ if ( $reason ) {
+ $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
+ }
+ # Truncate for whole multibyte characters.
+ $comment = $wgContLang->truncate( $comment, 255 );
+
+ $dbw = wfGetDB( DB_MASTER );
+
+ $newpage = WikiPage::factory( $nt );
+
+ if ( $moveOverRedirect ) {
+ $newid = $nt->getArticleID();
+ $newcontent = $newpage->getContent();
+
+ # Delete the old redirect. We don't save it to history since
+ # by definition if we've got here it's rather uninteresting.
+ # We have to remove it so that the next step doesn't trigger
+ # a conflict on the unique namespace+title index...
+ $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
+
+ $newpage->doDeleteUpdates( $newid, $newcontent );
+ }
+
+ # Save a null revision in the page's history notifying of the move
+ $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
+ if ( !is_object( $nullRevision ) ) {
+ throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
+ }
+
+ $nullRevision->insertOn( $dbw );
+
+ # Change the name of the target page:
+ $dbw->update( 'page',
+ /* SET */ array(
+ 'page_namespace' => $nt->getNamespace(),
+ 'page_title' => $nt->getDBkey(),
+ ),
+ /* WHERE */ array( 'page_id' => $oldid ),
+ __METHOD__
+ );
+
+ // clean up the old title before reset article id - bug 45348
+ if ( !$redirectContent ) {
+ WikiPage::onArticleDelete( $this->oldTitle );
+ }
+
+ $this->oldTitle->resetArticleID( 0 ); // 0 == non existing
+ $nt->resetArticleID( $oldid );
+ $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397
+
+ $newpage->updateRevisionOn( $dbw, $nullRevision );
+
+ wfRunHooks( 'NewRevisionFromEditComplete',
+ array( $newpage, $nullRevision, $nullRevision->getParentId(), $user ) );
+
+ $newpage->doEditUpdates( $nullRevision, $user, array( 'changed' => false ) );
+
+ if ( !$moveOverRedirect ) {
+ WikiPage::onArticleCreate( $nt );
+ }
+
+ # Recreate the redirect, this time in the other direction.
+ if ( $redirectContent ) {
+ $redirectArticle = WikiPage::factory( $this->oldTitle );
+ $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // bug 46397
+ $newid = $redirectArticle->insertOn( $dbw );
+ if ( $newid ) { // sanity
+ $this->oldTitle->resetArticleID( $newid );
+ $redirectRevision = new Revision( array(
+ 'title' => $this->oldTitle, // for determining the default content model
+ 'page' => $newid,
+ 'user_text' => $user->getName(),
+ 'user' => $user->getId(),
+ 'comment' => $comment,
+ 'content' => $redirectContent ) );
+ $redirectRevision->insertOn( $dbw );
+ $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
+
+ wfRunHooks( 'NewRevisionFromEditComplete',
+ array( $redirectArticle, $redirectRevision, false, $user ) );
+
+ $redirectArticle->doEditUpdates( $redirectRevision, $user, array( 'created' => true ) );
+ }
+ }
+
+ # Log the move
+ $logid = $logEntry->insert();
+ $logEntry->publish( $logid );
+ }
+
+}
\ No newline at end of file
* @return string
*/
function getButtons() {
+ global $wgUseMediaWikiUIEverywhere;
+ $attrs = $wgUseMediaWikiUIEverywhere ? array( 'class' => 'mw-ui-button mw-ui-quiet' ) : array();
+
if ( !$this->getModifiedUser()->isAllowedAny( 'editmyprivateinfo', 'editmyoptions' ) ) {
return '';
}
if ( $this->getModifiedUser()->isAllowed( 'editmyoptions' ) ) {
$t = SpecialPage::getTitleFor( 'Preferences', 'reset' );
- $html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped() );
+ $html .= "\n" . Linker::link( $t, $this->msg( 'restoreprefs' )->escaped(),
+ $attrs );
$html = Xml::tags( 'div', array( 'class' => 'mw-prefs-buttons' ), $html );
}
* Check whether a given move operation would be valid.
* Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
*
+ * @todo move this into MovePage
* @param Title $nt The new title
* @param bool $auth Indicates whether $wgUser's permissions
* should be checked
/**
* Move a title to a new location
*
+ * @todo Deprecate this in favor of MovePage
* @param Title $nt The new title
* @param bool $auth Indicates whether $wgUser's permissions
* should be checked
* @return array|bool True on success, getUserPermissionsErrors()-like array on failure
*/
public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
- global $wgUser, $wgCategoryCollation;
+ global $wgUser;
$err = $this->isValidMoveOperation( $nt, $auth, $reason );
if ( is_array( $err ) ) {
// Auto-block user's IP if the account was "hard" blocked
wfRunHooks( 'TitleMove', array( $this, $nt, $wgUser ) );
- // If it is a file, move it first.
- // It is done before all other moving stuff is done because it's hard to revert.
- $dbw = wfGetDB( DB_MASTER );
- if ( $this->getNamespace() == NS_FILE ) {
- $file = wfLocalFile( $this );
- if ( $file->exists() ) {
- $status = $file->move( $nt );
- if ( !$status->isOk() ) {
- return $status->getErrorsArray();
- }
- }
- // Clear RepoGroup process cache
- RepoGroup::singleton()->clearCache( $this );
- RepoGroup::singleton()->clearCache( $nt ); # clear false negative cache
- }
-
- $dbw->begin( __METHOD__ ); # If $file was a LocalFile, its transaction would have closed our own.
- $pageid = $this->getArticleID( self::GAID_FOR_UPDATE );
- $protected = $this->isProtected();
-
- // Do the actual move
- $this->moveToInternal( $nt, $reason, $createRedirect );
-
- // Refresh the sortkey for this row. Be careful to avoid resetting
- // cl_timestamp, which may disturb time-based lists on some sites.
- // @todo This block should be killed, it's duplicating code
- // from LinksUpdate::getCategoryInsertions() and friends.
- $prefixes = $dbw->select(
- 'categorylinks',
- array( 'cl_sortkey_prefix', 'cl_to' ),
- array( 'cl_from' => $pageid ),
- __METHOD__
- );
- if ( $nt->getNamespace() == NS_CATEGORY ) {
- $type = 'subcat';
- } elseif ( $nt->getNamespace() == NS_FILE ) {
- $type = 'file';
- } else {
- $type = 'page';
- }
- foreach ( $prefixes as $prefixRow ) {
- $prefix = $prefixRow->cl_sortkey_prefix;
- $catTo = $prefixRow->cl_to;
- $dbw->update( 'categorylinks',
- array(
- 'cl_sortkey' => Collation::singleton()->getSortKey(
- $nt->getCategorySortkey( $prefix ) ),
- 'cl_collation' => $wgCategoryCollation,
- 'cl_type' => $type,
- 'cl_timestamp=cl_timestamp' ),
- array(
- 'cl_from' => $pageid,
- 'cl_to' => $catTo ),
- __METHOD__
- );
- }
-
- $redirid = $this->getArticleID();
-
- if ( $protected ) {
- # Protect the redirect title as the title used to be...
- $dbw->insertSelect( 'page_restrictions', 'page_restrictions',
- array(
- 'pr_page' => $redirid,
- 'pr_type' => 'pr_type',
- 'pr_level' => 'pr_level',
- 'pr_cascade' => 'pr_cascade',
- 'pr_user' => 'pr_user',
- 'pr_expiry' => 'pr_expiry'
- ),
- array( 'pr_page' => $pageid ),
- __METHOD__,
- array( 'IGNORE' )
- );
- # Update the protection log
- $log = new LogPage( 'protect' );
- $comment = wfMessage(
- 'prot_1movedto2',
- $this->getPrefixedText(),
- $nt->getPrefixedText()
- )->inContentLanguage()->text();
- if ( $reason ) {
- $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
- }
- // @todo FIXME: $params?
- $logId = $log->addEntry(
- 'move_prot',
- $nt,
- $comment,
- array( $this->getPrefixedText() ),
- $wgUser
- );
-
- // reread inserted pr_ids for log relation
- $insertedPrIds = $dbw->select(
- 'page_restrictions',
- 'pr_id',
- array( 'pr_page' => $redirid ),
- __METHOD__
- );
- $logRelationsValues = array();
- foreach ( $insertedPrIds as $prid ) {
- $logRelationsValues[] = $prid->pr_id;
- }
- $log->addRelations( 'pr_id', $logRelationsValues, $logId );
- }
-
- // Update *_from_namespace fields as needed
- if ( $this->getNamespace() != $nt->getNamespace() ) {
- $dbw->update( 'pagelinks',
- array( 'pl_from_namespace' => $nt->getNamespace() ),
- array( 'pl_from' => $pageid ),
- __METHOD__
- );
- $dbw->update( 'templatelinks',
- array( 'tl_from_namespace' => $nt->getNamespace() ),
- array( 'tl_from' => $pageid ),
- __METHOD__
- );
- $dbw->update( 'imagelinks',
- array( 'il_from_namespace' => $nt->getNamespace() ),
- array( 'il_from' => $pageid ),
- __METHOD__
- );
- }
-
- # Update watchlists
- $oldtitle = $this->getDBkey();
- $newtitle = $nt->getDBkey();
- $oldsnamespace = MWNamespace::getSubject( $this->getNamespace() );
- $newsnamespace = MWNamespace::getSubject( $nt->getNamespace() );
- if ( $oldsnamespace != $newsnamespace || $oldtitle != $newtitle ) {
- WatchedItem::duplicateEntries( $this, $nt );
- }
-
- $dbw->commit( __METHOD__ );
-
- wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid, $reason ) );
- return true;
- }
-
- /**
- * Move page to a title which is either a redirect to the
- * source page or nonexistent
- *
- * @param Title $nt The page to move to, which should be a redirect or nonexistent
- * @param string $reason The reason for the move
- * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
- * if the user has the suppressredirect right
- * @throws MWException
- */
- private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
- global $wgUser, $wgContLang;
-
- if ( $nt->exists() ) {
- $moveOverRedirect = true;
- $logType = 'move_redir';
- } else {
- $moveOverRedirect = false;
- $logType = 'move';
- }
-
- if ( $createRedirect ) {
- if ( $this->getNamespace() == NS_CATEGORY
- && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
- ) {
- $redirectContent = new WikitextContent(
- wfMessage( 'category-move-redirect-override' )
- ->params( $nt->getPrefixedText() )->inContentLanguage()->plain() );
- } else {
- $contentHandler = ContentHandler::getForTitle( $this );
- $redirectContent = $contentHandler->makeRedirectContent( $nt,
- wfMessage( 'move-redirect-text' )->inContentLanguage()->plain() );
- }
-
- // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
+ $mp = new MovePage( $this, $nt );
+ $status = $mp->move( $wgUser, $reason, $createRedirect );
+ if ( $status->isOK() ) {
+ return true;
} else {
- $redirectContent = null;
- }
-
- // bug 57084: log_page should be the ID of the *moved* page
- $oldid = $this->getArticleID();
- $logTitle = clone $this;
-
- $logEntry = new ManualLogEntry( 'move', $logType );
- $logEntry->setPerformer( $wgUser );
- $logEntry->setTarget( $logTitle );
- $logEntry->setComment( $reason );
- $logEntry->setParameters( array(
- '4::target' => $nt->getPrefixedText(),
- '5::noredir' => $redirectContent ? '0': '1',
- ) );
-
- $formatter = LogFormatter::newFromEntry( $logEntry );
- $formatter->setContext( RequestContext::newExtraneousContext( $this ) );
- $comment = $formatter->getPlainActionText();
- if ( $reason ) {
- $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
- }
- # Truncate for whole multibyte characters.
- $comment = $wgContLang->truncate( $comment, 255 );
-
- $dbw = wfGetDB( DB_MASTER );
-
- $newpage = WikiPage::factory( $nt );
-
- if ( $moveOverRedirect ) {
- $newid = $nt->getArticleID();
- $newcontent = $newpage->getContent();
-
- # Delete the old redirect. We don't save it to history since
- # by definition if we've got here it's rather uninteresting.
- # We have to remove it so that the next step doesn't trigger
- # a conflict on the unique namespace+title index...
- $dbw->delete( 'page', array( 'page_id' => $newid ), __METHOD__ );
-
- $newpage->doDeleteUpdates( $newid, $newcontent );
+ return $status->getErrorsArray();
}
-
- # Save a null revision in the page's history notifying of the move
- $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $wgUser );
- if ( !is_object( $nullRevision ) ) {
- throw new MWException( 'No valid null revision produced in ' . __METHOD__ );
- }
-
- $nullRevision->insertOn( $dbw );
-
- # Change the name of the target page:
- $dbw->update( 'page',
- /* SET */ array(
- 'page_namespace' => $nt->getNamespace(),
- 'page_title' => $nt->getDBkey(),
- ),
- /* WHERE */ array( 'page_id' => $oldid ),
- __METHOD__
- );
-
- // clean up the old title before reset article id - bug 45348
- if ( !$redirectContent ) {
- WikiPage::onArticleDelete( $this );
- }
-
- $this->resetArticleID( 0 ); // 0 == non existing
- $nt->resetArticleID( $oldid );
- $newpage->loadPageData( WikiPage::READ_LOCKING ); // bug 46397
-
- $newpage->updateRevisionOn( $dbw, $nullRevision );
-
- wfRunHooks( 'NewRevisionFromEditComplete',
- array( $newpage, $nullRevision, $nullRevision->getParentId(), $wgUser ) );
-
- $newpage->doEditUpdates( $nullRevision, $wgUser, array( 'changed' => false ) );
-
- if ( !$moveOverRedirect ) {
- WikiPage::onArticleCreate( $nt );
- }
-
- # Recreate the redirect, this time in the other direction.
- if ( $redirectContent ) {
- $redirectArticle = WikiPage::factory( $this );
- $redirectArticle->loadFromRow( false, WikiPage::READ_LOCKING ); // bug 46397
- $newid = $redirectArticle->insertOn( $dbw );
- if ( $newid ) { // sanity
- $this->resetArticleID( $newid );
- $redirectRevision = new Revision( array(
- 'title' => $this, // for determining the default content model
- 'page' => $newid,
- 'user_text' => $wgUser->getName(),
- 'user' => $wgUser->getId(),
- 'comment' => $comment,
- 'content' => $redirectContent ) );
- $redirectRevision->insertOn( $dbw );
- $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
-
- wfRunHooks( 'NewRevisionFromEditComplete',
- array( $redirectArticle, $redirectRevision, false, $wgUser ) );
-
- $redirectArticle->doEditUpdates( $redirectRevision, $wgUser, array( 'created' => true ) );
- }
- }
-
- # Log the move
- $logid = $logEntry->insert();
- $logEntry->publish( $logid );
}
/**
$text = $this->getNativeData();
$pst = rtrim( $text );
- return ( $text === $pst ) ? $this : new static( $pst );
+ return ( $text === $pst ) ? $this : new static( $pst, $this->getModel() );
}
/**
}
}
+ // Thumbnailing a very large file could result in network saturation if
+ // everyone does it at once.
+ if ( $this->getSize() >= 1e7 ) { // 10MB
+ $that = $this;
+ $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $this->getName() ),
+ array(
+ 'doWork' => function() use ( $that ) {
+ return $that->getLocalRefPath();
+ }
+ )
+ );
+ $srcPath = $work->execute();
+ } else {
+ $srcPath = $this->getLocalRefPath();
+ }
+
// Original file
return array(
- 'path' => $this->getLocalRefPath(),
+ 'path' => $srcPath,
'width' => $this->getWidth(),
'height' => $this->getHeight()
);
protected $mFieldTree;
protected $mShowReset = false;
protected $mShowSubmit = true;
+ protected $mSubmitModifierClass = 'mw-ui-constructive';
protected $mSubmitCallback;
protected $mValidationErrorMessage;
if ( !empty( $field->mParams['nodata'] ) ) {
continue;
}
+ if ( $field->isHidden( $this->mFieldData ) ) {
+ continue;
+ }
if ( $field->validate(
$this->mFieldData[$fieldname],
$this->mFieldData )
$attribs['class'] = array( 'mw-htmlform-submit' );
if ( $this->isVForm() || $useMediaWikiUIEverywhere ) {
- array_push( $attribs['class'], 'mw-ui-button', 'mw-ui-constructive' );
+ array_push( $attribs['class'], 'mw-ui-button', $this->mSubmitModifierClass );
}
if ( $this->isVForm() ) {
return $this;
}
+ /**
+ * Identify that the submit button in the form has a destructive action
+ *
+ */
+ public function setSubmitDestructive() {
+ $this->mSubmitModifierClass = 'mw-ui-destructive';
+ }
+
/**
* Set the text for the submit button to a message
* @since 1.19
}
$data = $data[$key];
}
- $testValue = $data;
+ $testValue = (string)$data;
break;
}
*
* @ingroup Media
*/
-class BitmapHandler extends ImageHandler {
- /**
- * @param File $image
- * @param array $params Transform parameters. Entries with the keys 'width'
- * and 'height' are the respective screen width and height, while the keys
- * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions.
- * @return bool
- */
- function normaliseParams( $image, &$params ) {
- if ( !parent::normaliseParams( $image, $params ) ) {
- return false;
- }
-
- # Obtain the source, pre-rotation dimensions
- $srcWidth = $image->getWidth( $params['page'] );
- $srcHeight = $image->getHeight( $params['page'] );
-
- # Don't make an image bigger than the source
- if ( $params['physicalWidth'] >= $srcWidth ) {
- $params['physicalWidth'] = $srcWidth;
- $params['physicalHeight'] = $srcHeight;
-
- # Skip scaling limit checks if no scaling is required
- # due to requested size being bigger than source.
- if ( !$image->mustRender() ) {
- return true;
- }
- }
-
- # Check if the file is smaller than the maximum image area for thumbnailing
- $checkImageAreaHookResult = null;
- wfRunHooks(
- 'BitmapHandlerCheckImageArea',
- array( $image, &$params, &$checkImageAreaHookResult )
- );
-
- if ( is_null( $checkImageAreaHookResult ) ) {
- global $wgMaxImageArea;
-
- if ( $srcWidth * $srcHeight > $wgMaxImageArea
- && !( $image->getMimeType() == 'image/jpeg'
- && self::getScalerType( false, false ) == 'im' )
- ) {
- # Only ImageMagick can efficiently downsize jpg images without loading
- # the entire file in memory
- return false;
- }
- } else {
- return $checkImageAreaHookResult;
- }
-
- return true;
- }
-
- /**
- * Extracts the width/height if the image will be scaled before rotating
- *
- * This will match the physical size/aspect ratio of the original image
- * prior to application of the rotation -- so for a portrait image that's
- * stored as raw landscape with 90-degress rotation, the resulting size
- * will be wider than it is tall.
- *
- * @param array $params Parameters as returned by normaliseParams
- * @param int $rotation The rotation angle that will be applied
- * @return array ($width, $height) array
- */
- public function extractPreRotationDimensions( $params, $rotation ) {
- if ( $rotation == 90 || $rotation == 270 ) {
- # We'll resize before rotation, so swap the dimensions again
- $width = $params['physicalHeight'];
- $height = $params['physicalWidth'];
- } else {
- $width = $params['physicalWidth'];
- $height = $params['physicalHeight'];
- }
-
- return array( $width, $height );
- }
-
- /**
- * @param File $image
- * @param string $dstPath
- * @param string $dstUrl
- * @param array $params
- * @param int $flags
- * @return MediaTransformError|ThumbnailImage|TransformParameterError
- */
- function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
- if ( !$this->normaliseParams( $image, $params ) ) {
- return new TransformParameterError( $params );
- }
-
- # Create a parameter array to pass to the scaler
- $scalerParams = array(
- # The size to which the image will be resized
- 'physicalWidth' => $params['physicalWidth'],
- 'physicalHeight' => $params['physicalHeight'],
- 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
- # The size of the image on the page
- 'clientWidth' => $params['width'],
- 'clientHeight' => $params['height'],
- # Comment as will be added to the Exif of the thumbnail
- 'comment' => isset( $params['descriptionUrl'] )
- ? "File source: {$params['descriptionUrl']}"
- : '',
- # Properties of the original image
- 'srcWidth' => $image->getWidth(),
- 'srcHeight' => $image->getHeight(),
- 'mimeType' => $image->getMimeType(),
- 'dstPath' => $dstPath,
- 'dstUrl' => $dstUrl,
- );
-
- if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
- $scalerParams['quality'] = 30;
- }
-
- # Determine scaler type
- $scaler = self::getScalerType( $dstPath );
-
- wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
- "thumbnail at $dstPath using scaler $scaler\n" );
-
- if ( !$image->mustRender() &&
- $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
- && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
- && !isset( $scalerParams['quality'] )
- ) {
-
- # normaliseParams (or the user) wants us to return the unscaled image
- wfDebug( __METHOD__ . ": returning unscaled image\n" );
-
- return $this->getClientScalingThumbnailImage( $image, $scalerParams );
- }
-
- if ( $scaler == 'client' ) {
- # Client-side image scaling, use the source URL
- # Using the destination URL in a TRANSFORM_LATER request would be incorrect
- return $this->getClientScalingThumbnailImage( $image, $scalerParams );
- }
-
- if ( $flags & self::TRANSFORM_LATER ) {
- wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
- $newParams = array(
- 'width' => $scalerParams['clientWidth'],
- 'height' => $scalerParams['clientHeight']
- );
- if ( isset( $params['quality'] ) ) {
- $newParams['quality'] = $params['quality'];
- }
- return new ThumbnailImage( $image, $dstUrl, false, $newParams );
- }
-
- # Try to make a target path for the thumbnail
- if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
- wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
- "directory, falling back to client scaling\n" );
-
- return $this->getClientScalingThumbnailImage( $image, $scalerParams );
- }
-
- # Transform functions and binaries need a FS source file
- $thumbnailSource = $image->getThumbnailSource( $params );
-
- $scalerParams['srcPath'] = $thumbnailSource['path'];
- $scalerParams['srcWidth'] = $thumbnailSource['width'];
- $scalerParams['srcHeight'] = $thumbnailSource['height'];
-
- if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
- wfDebugLog( 'thumbnail',
- sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
- wfHostname(), $image->getName() ) );
-
- return new MediaTransformError( 'thumbnail_error',
- $scalerParams['clientWidth'], $scalerParams['clientHeight'],
- wfMessage( 'filemissing' )->text()
- );
- }
-
- # Try a hook
- $mto = null;
- wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
- if ( !is_null( $mto ) ) {
- wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
- $scaler = 'hookaborted';
- }
-
- switch ( $scaler ) {
- case 'hookaborted':
- # Handled by the hook above
- /** @var MediaTransformOutput $mto */
- $err = $mto->isError() ? $mto : false;
- break;
- case 'im':
- $err = $this->transformImageMagick( $image, $scalerParams );
- break;
- case 'custom':
- $err = $this->transformCustom( $image, $scalerParams );
- break;
- case 'imext':
- $err = $this->transformImageMagickExt( $image, $scalerParams );
- break;
- case 'gd':
- default:
- $err = $this->transformGd( $image, $scalerParams );
- break;
- }
-
- # Remove the file if a zero-byte thumbnail was created, or if there was an error
- $removed = $this->removeBadFile( $dstPath, (bool)$err );
- if ( $err ) {
- # transform returned MediaTransforError
- return $err;
- } elseif ( $removed ) {
- # Thumbnail was zero-byte and had to be removed
- return new MediaTransformError( 'thumbnail_error',
- $scalerParams['clientWidth'], $scalerParams['clientHeight'],
- wfMessage( 'unknown-error' )->text()
- );
- } elseif ( $mto ) {
- return $mto;
- } else {
- $newParams = array(
- 'width' => $scalerParams['clientWidth'],
- 'height' => $scalerParams['clientHeight']
- );
- if ( isset( $params['quality'] ) ) {
- $newParams['quality'] = $params['quality'];
- }
- return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
- }
- }
+class BitmapHandler extends TransformationalImageHandler {
/**
* Returns which scaler type should be used. Creates parent directories
*
* @param string $dstPath
* @param bool $checkDstPath
- * @return string One of client, im, custom, gd, imext
+ * @return string|Callable One of client, im, custom, gd, imext or an array( object, method )
*/
- protected static function getScalerType( $dstPath, $checkDstPath = true ) {
+ protected function getScalerType( $dstPath, $checkDstPath = true ) {
global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
if ( !$dstPath && $checkDstPath ) {
return $scaler;
}
- /**
- * Get a ThumbnailImage that respresents an image that will be scaled
- * client side
- *
- * @param File $image File associated with this thumbnail
- * @param array $scalerParams Array with scaler params
- * @return ThumbnailImage
- *
- * @todo FIXME: No rotation support
- */
- protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
- $params = array(
- 'width' => $scalerParams['clientWidth'],
- 'height' => $scalerParams['clientHeight']
- );
-
- return new ThumbnailImage( $image, $image->getURL(), null, $params );
- }
-
/**
* Transform an image using ImageMagick
*
return false; # No error
}
- /**
- * Get a MediaTransformError with error 'thumbnail_error'
- *
- * @param array $params Parameter array as passed to the transform* functions
- * @param string $errMsg Error message
- * @return MediaTransformError
- */
- public function getMediaTransformError( $params, $errMsg ) {
- return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
- $params['clientHeight'], $errMsg );
- }
-
/**
* Transform an image using the built in GD library
*
}
/**
- * Escape a string for ImageMagick's property input (e.g. -set -comment)
- * See InterpretImageProperties() in magick/property.c
- * @param string $s
- * @return string
- */
- function escapeMagickProperty( $s ) {
- // Double the backslashes
- $s = str_replace( '\\', '\\\\', $s );
- // Double the percents
- $s = str_replace( '%', '%%', $s );
- // Escape initial - or @
- if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
- $s = '\\' . $s;
- }
-
- return $s;
- }
-
- /**
- * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
- * and GetPathComponent() in magick/utility.c.
- *
- * This won't work with an initial ~ or @, so input files should be prefixed
- * with the directory name.
- *
- * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
- * it's broken in a way that doesn't involve trying to convert every file
- * in a directory, so we're better off escaping and waiting for the bugfix
- * to filter down to users.
- *
- * @param string $path The file path
- * @param bool|string $scene The scene specification, or false if there is none
- * @throws MWException
- * @return string
- */
- function escapeMagickInput( $path, $scene = false ) {
- # Die on initial metacharacters (caller should prepend path)
- $firstChar = substr( $path, 0, 1 );
- if ( $firstChar === '~' || $firstChar === '@' ) {
- throw new MWException( __METHOD__ . ': cannot escape this path name' );
- }
-
- # Escape glob chars
- $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
-
- return $this->escapeMagickPath( $path, $scene );
- }
-
- /**
- * Escape a string for ImageMagick's output filename. See
- * InterpretImageFilename() in magick/image.c.
- * @param string $path The file path
- * @param bool|string $scene The scene specification, or false if there is none
- * @return string
- */
- function escapeMagickOutput( $path, $scene = false ) {
- $path = str_replace( '%', '%%', $path );
-
- return $this->escapeMagickPath( $path, $scene );
- }
-
- /**
- * Armour a string against ImageMagick's GetPathComponent(). This is a
- * helper function for escapeMagickInput() and escapeMagickOutput().
- *
- * @param string $path The file path
- * @param bool|string $scene The scene specification, or false if there is none
- * @throws MWException
- * @return string
- */
- protected function escapeMagickPath( $path, $scene = false ) {
- # Die on format specifiers (other than drive letters). The regex is
- # meant to match all the formats you get from "convert -list format"
- if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
- if ( wfIsWindows() && is_dir( $m[0] ) ) {
- // OK, it's a drive letter
- // ImageMagick has a similar exception, see IsMagickConflict()
- } else {
- throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
- }
- }
-
- # If there are square brackets, add a do-nothing scene specification
- # to force a literal interpretation
- if ( $scene === false ) {
- if ( strpos( $path, '[' ) !== false ) {
- $path .= '[0--1]';
- }
- } else {
- $path .= "[$scene]";
- }
-
- return $path;
- }
-
- /**
- * Retrieve the version of the installed ImageMagick
- * You can use PHPs version_compare() to use this value
- * Value is cached for one hour.
- * @return string Representing the IM version.
+ * Callback for transformGd when transforming jpeg images.
*/
- protected function getMagickVersion() {
- global $wgMemc;
-
- $cache = $wgMemc->get( "imagemagick-version" );
- if ( !$cache ) {
- global $wgImageMagickConvertCommand;
- $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
- wfDebug( __METHOD__ . ": Running convert -version\n" );
- $retval = '';
- $return = wfShellExec( $cmd, $retval );
- $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
- if ( $x != 1 ) {
- wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
-
- return null;
- }
- $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
-
- return $matches[1];
- }
-
- return $cache;
- }
-
// FIXME: transformImageMagick() & transformImageMagickExt() uses JPEG quality 80, here it's 95?
static function imageJpegWrapper( $dst_image, $thumbPath, $quality = 95 ) {
imageinterlace( $dst_image );
*
* @return bool
*/
- public static function canRotate() {
- $scaler = self::getScalerType( null, false );
+ public function canRotate() {
+ $scaler = $this->getScalerType( null, false );
switch ( $scaler ) {
case 'im':
# ImageMagick supports autorotation
* @see $wgEnableAutoRotation
* @return bool Whether auto rotation is enabled
*/
- public static function autoRotateEnabled() {
+ public function autoRotateEnabled() {
global $wgEnableAutoRotation;
if ( $wgEnableAutoRotation === null ) {
- // Only enable auto-rotation when the bitmap handler can rotate
- $wgEnableAutoRotation = BitmapHandler::canRotate();
+ // Only enable auto-rotation when we actually can
+ return $this->canRotate();
}
return $wgEnableAutoRotation;
$rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
$scene = false;
- $scaler = self::getScalerType( null, false );
+ $scaler = $this->getScalerType( null, false );
switch ( $scaler ) {
case 'im':
$cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
"$scaler rotation not implemented" );
}
}
-
- /**
- * Returns whether the file needs to be rendered. Returns true if the
- * file requires rotation and we are able to rotate it.
- *
- * @param File $file
- * @return bool
- */
- public function mustRender( $file ) {
- return self::canRotate() && $this->getRotation( $file ) != 0;
- }
}
// Don't just call $image->getMetadata(); FSFile::getPropsFromPath() calls us with a bogus object.
// This may mean we read EXIF data twice on initial upload.
- if ( BitmapHandler::autoRotateEnabled() ) {
+ if ( $this->autoRotateEnabled() ) {
$meta = $this->getMetadata( $image, $path );
$rotation = $this->getRotationForExif( $meta );
} else {
* @return int 0, 90, 180 or 270
*/
public function getRotation( $file ) {
- if ( !BitmapHandler::autoRotateEnabled() ) {
+ if ( !$this->autoRotateEnabled() ) {
return 0;
}
/**
* True if the handler can rotate the media
- * @since 1.21
+ * @since 1.24 non-static. From 1.21-1.23 was static
* @return bool
*/
- public static function canRotate() {
+ public function canRotate() {
return false;
}
--- /dev/null
+<?php
+/**
+ * Base class for handlers which require transforming images in a
+ * similar way as BitmapHandler does.
+ *
+ * This was split from BitmapHandler on the basis that some extensions
+ * might want to work in a similar way to BitmapHandler, but for
+ * different formats.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup Media
+ */
+
+/**
+ * Handler for images that need to be transformed
+ *
+ * @since 1.24
+ * @ingroup Media
+ */
+abstract class TransformationalImageHandler extends ImageHandler {
+ /**
+ * @param File $image
+ * @param array $params Transform parameters. Entries with the keys 'width'
+ * and 'height' are the respective screen width and height, while the keys
+ * 'physicalWidth' and 'physicalHeight' indicate the thumbnail dimensions.
+ * @return bool
+ */
+ function normaliseParams( $image, &$params ) {
+ if ( !parent::normaliseParams( $image, $params ) ) {
+ return false;
+ }
+
+ # Obtain the source, pre-rotation dimensions
+ $srcWidth = $image->getWidth( $params['page'] );
+ $srcHeight = $image->getHeight( $params['page'] );
+
+ # Don't make an image bigger than the source
+ if ( $params['physicalWidth'] >= $srcWidth ) {
+ $params['physicalWidth'] = $srcWidth;
+ $params['physicalHeight'] = $srcHeight;
+
+ # Skip scaling limit checks if no scaling is required
+ # due to requested size being bigger than source.
+ if ( !$image->mustRender() ) {
+ return true;
+ }
+ }
+
+ # Check if the file is smaller than the maximum image area for thumbnailing
+ # For historical reasons, hook starts with BitmapHandler
+ $checkImageAreaHookResult = null;
+ wfRunHooks(
+ 'BitmapHandlerCheckImageArea',
+ array( $image, &$params, &$checkImageAreaHookResult )
+ );
+
+ if ( is_null( $checkImageAreaHookResult ) ) {
+ global $wgMaxImageArea;
+
+ if ( $srcWidth * $srcHeight > $wgMaxImageArea
+ && !( $image->getMimeType() == 'image/jpeg'
+ && $this->getScalerType( false, false ) == 'im' )
+ ) {
+ # Only ImageMagick can efficiently downsize jpg images without loading
+ # the entire file in memory
+ return false;
+ }
+ } else {
+ return $checkImageAreaHookResult;
+ }
+
+ return true;
+ }
+
+ /**
+ * Extracts the width/height if the image will be scaled before rotating
+ *
+ * This will match the physical size/aspect ratio of the original image
+ * prior to application of the rotation -- so for a portrait image that's
+ * stored as raw landscape with 90-degress rotation, the resulting size
+ * will be wider than it is tall.
+ *
+ * @param array $params Parameters as returned by normaliseParams
+ * @param int $rotation The rotation angle that will be applied
+ * @return array ($width, $height) array
+ */
+ public function extractPreRotationDimensions( $params, $rotation ) {
+ if ( $rotation == 90 || $rotation == 270 ) {
+ # We'll resize before rotation, so swap the dimensions again
+ $width = $params['physicalHeight'];
+ $height = $params['physicalWidth'];
+ } else {
+ $width = $params['physicalWidth'];
+ $height = $params['physicalHeight'];
+ }
+
+ return array( $width, $height );
+ }
+
+ /**
+ * Create a thumbnail.
+ *
+ * This sets up various parameters, and then calls a helper method
+ * based on $this->getScalerType in order to scale the image.
+ *
+ * @param File $image
+ * @param string $dstPath
+ * @param string $dstUrl
+ * @param array $params
+ * @param int $flags
+ * @return MediaTransformError|ThumbnailImage|TransformParameterError
+ */
+ function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
+ if ( !$this->normaliseParams( $image, $params ) ) {
+ return new TransformParameterError( $params );
+ }
+
+ # Create a parameter array to pass to the scaler
+ $scalerParams = array(
+ # The size to which the image will be resized
+ 'physicalWidth' => $params['physicalWidth'],
+ 'physicalHeight' => $params['physicalHeight'],
+ 'physicalDimensions' => "{$params['physicalWidth']}x{$params['physicalHeight']}",
+ # The size of the image on the page
+ 'clientWidth' => $params['width'],
+ 'clientHeight' => $params['height'],
+ # Comment as will be added to the Exif of the thumbnail
+ 'comment' => isset( $params['descriptionUrl'] )
+ ? "File source: {$params['descriptionUrl']}"
+ : '',
+ # Properties of the original image
+ 'srcWidth' => $image->getWidth(),
+ 'srcHeight' => $image->getHeight(),
+ 'mimeType' => $image->getMimeType(),
+ 'dstPath' => $dstPath,
+ 'dstUrl' => $dstUrl,
+ );
+
+ if ( isset( $params['quality'] ) && $params['quality'] === 'low' ) {
+ $scalerParams['quality'] = 30;
+ }
+
+ // For subclasses that might be paged.
+ if ( $image->isMultipage() && isset( $params['page'] ) ) {
+ $scalerParams['page'] = intval( $params['page'] );
+ }
+
+ # Determine scaler type
+ $scaler = $this->getScalerType( $dstPath );
+
+ wfDebug( __METHOD__ . ": creating {$scalerParams['physicalDimensions']} " .
+ "thumbnail at $dstPath using scaler $scaler\n" );
+
+ if ( !$image->mustRender() &&
+ $scalerParams['physicalWidth'] == $scalerParams['srcWidth']
+ && $scalerParams['physicalHeight'] == $scalerParams['srcHeight']
+ && !isset( $scalerParams['quality'] )
+ ) {
+
+ # normaliseParams (or the user) wants us to return the unscaled image
+ wfDebug( __METHOD__ . ": returning unscaled image\n" );
+
+ return $this->getClientScalingThumbnailImage( $image, $scalerParams );
+ }
+
+ if ( $scaler == 'client' ) {
+ # Client-side image scaling, use the source URL
+ # Using the destination URL in a TRANSFORM_LATER request would be incorrect
+ return $this->getClientScalingThumbnailImage( $image, $scalerParams );
+ }
+
+ if ( $flags & self::TRANSFORM_LATER ) {
+ wfDebug( __METHOD__ . ": Transforming later per flags.\n" );
+ $newParams = array(
+ 'width' => $scalerParams['clientWidth'],
+ 'height' => $scalerParams['clientHeight']
+ );
+ if ( isset( $params['quality'] ) ) {
+ $newParams['quality'] = $params['quality'];
+ }
+ if ( isset( $params['page'] ) && $params['page'] ) {
+ $newParams['page'] = $params['page'];
+ }
+ return new ThumbnailImage( $image, $dstUrl, false, $newParams );
+ }
+
+ # Try to make a target path for the thumbnail
+ if ( !wfMkdirParents( dirname( $dstPath ), null, __METHOD__ ) ) {
+ wfDebug( __METHOD__ . ": Unable to create thumbnail destination " .
+ "directory, falling back to client scaling\n" );
+
+ return $this->getClientScalingThumbnailImage( $image, $scalerParams );
+ }
+
+ # Transform functions and binaries need a FS source file
+ $thumbnailSource = $this->getThumbnailSource( $image, $params );
+
+ $scalerParams['srcPath'] = $thumbnailSource['path'];
+ $scalerParams['srcWidth'] = $thumbnailSource['width'];
+ $scalerParams['srcHeight'] = $thumbnailSource['height'];
+
+ if ( $scalerParams['srcPath'] === false ) { // Failed to get local copy
+ wfDebugLog( 'thumbnail',
+ sprintf( 'Thumbnail failed on %s: could not get local copy of "%s"',
+ wfHostname(), $image->getName() ) );
+
+ return new MediaTransformError( 'thumbnail_error',
+ $scalerParams['clientWidth'], $scalerParams['clientHeight'],
+ wfMessage( 'filemissing' )->text()
+ );
+ }
+
+ # Try a hook. Called "Bitmap" for historical reasons.
+ /** @var $mto MediaTransformOutput */
+ $mto = null;
+ wfRunHooks( 'BitmapHandlerTransform', array( $this, $image, &$scalerParams, &$mto ) );
+ if ( !is_null( $mto ) ) {
+ wfDebug( __METHOD__ . ": Hook to BitmapHandlerTransform created an mto\n" );
+ $scaler = 'hookaborted';
+ }
+
+ // $scaler will return a MediaTransformError on failure, or false on success.
+ // If the scaler is succesful, it will have created a thumbnail at the destination
+ // path.
+ if ( is_array( $scaler ) && is_callable( $scaler ) ) {
+ // Allow subclasses to specify their own rendering methods.
+ $err = call_user_func( $scaler, $image, $scalerParams );
+ } else {
+ switch ( $scaler ) {
+ case 'hookaborted':
+ # Handled by the hook above
+ $err = $mto->isError() ? $mto : false;
+ break;
+ case 'im':
+ $err = $this->transformImageMagick( $image, $scalerParams );
+ break;
+ case 'custom':
+ $err = $this->transformCustom( $image, $scalerParams );
+ break;
+ case 'imext':
+ $err = $this->transformImageMagickExt( $image, $scalerParams );
+ break;
+ case 'gd':
+ default:
+ $err = $this->transformGd( $image, $scalerParams );
+ break;
+ }
+ }
+
+ # Remove the file if a zero-byte thumbnail was created, or if there was an error
+ $removed = $this->removeBadFile( $dstPath, (bool)$err );
+ if ( $err ) {
+ # transform returned MediaTransforError
+ return $err;
+ } elseif ( $removed ) {
+ # Thumbnail was zero-byte and had to be removed
+ return new MediaTransformError( 'thumbnail_error',
+ $scalerParams['clientWidth'], $scalerParams['clientHeight'],
+ wfMessage( 'unknown-error' )->text()
+ );
+ } elseif ( $mto ) {
+ return $mto;
+ } else {
+ $newParams = array(
+ 'width' => $scalerParams['clientWidth'],
+ 'height' => $scalerParams['clientHeight']
+ );
+ if ( isset( $params['quality'] ) ) {
+ $newParams['quality'] = $params['quality'];
+ }
+ if ( isset( $params['page'] ) && $params['page'] ) {
+ $newParams['page'] = $params['page'];
+ }
+ return new ThumbnailImage( $image, $dstUrl, $dstPath, $newParams );
+ }
+ }
+
+ /**
+ * Get the source file for the transform
+ *
+ * @param $file File
+ * @param $params Array
+ * @return Array Array with keys width, height and path.
+ */
+ protected function getThumbnailSource( $file, $params ) {
+ return $file->getThumbnailSource( $params );
+ }
+
+ /**
+ * Returns what sort of scaler type should be used.
+ *
+ * Values can be one of client, im, custom, gd, imext, or an array
+ * of object, method-name to call that specific method.
+ *
+ * If specifying a custom scaler command with array( Obj, method ),
+ * the method in question should take 2 parameters, a File object,
+ * and a $scalerParams array with various options (See doTransform
+ * for what is in $scalerParams). On error it should return a
+ * MediaTransformError object. On success it should return false,
+ * and simply make sure the thumbnail file is located at
+ * $scalerParams['dstPath'].
+ *
+ * If there is a problem with the output path, it returns "client"
+ * to do client side scaling.
+ *
+ * @param string $dstPath
+ * @param bool $checkDstPath Check that $dstPath is valid
+ * @return string|Callable One of client, im, custom, gd, imext, or a Callable array.
+ */
+ abstract protected function getScalerType( $dstPath, $checkDstPath = true );
+
+ /**
+ * Get a ThumbnailImage that respresents an image that will be scaled
+ * client side
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $scalerParams Array with scaler params
+ * @return ThumbnailImage
+ *
+ * @todo FIXME: No rotation support
+ */
+ protected function getClientScalingThumbnailImage( $image, $scalerParams ) {
+ $params = array(
+ 'width' => $scalerParams['clientWidth'],
+ 'height' => $scalerParams['clientHeight']
+ );
+
+ return new ThumbnailImage( $image, $image->getURL(), null, $params );
+ }
+
+ /**
+ * Transform an image using ImageMagick
+ *
+ * This is a stub method. The real method is in BitmapHander.
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
+ */
+ protected function transformImageMagick( $image, $params ) {
+ return $this->getMediaTransformError( $params, "Unimplemented" );
+ }
+
+ /**
+ * Transform an image using the Imagick PHP extension
+ *
+ * This is a stub method. The real method is in BitmapHander.
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
+ */
+ protected function transformImageMagickExt( $image, $params ) {
+ return $this->getMediaTransformError( $params, "Unimplemented" );
+ }
+
+ /**
+ * Transform an image using a custom command
+ *
+ * This is a stub method. The real method is in BitmapHander.
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
+ */
+ protected function transformCustom( $image, $params ) {
+ return $this->getMediaTransformError( $params, "Unimplemented" );
+ }
+
+ /**
+ * Get a MediaTransformError with error 'thumbnail_error'
+ *
+ * @param array $params Parameter array as passed to the transform* functions
+ * @param string $errMsg Error message
+ * @return MediaTransformError
+ */
+ public function getMediaTransformError( $params, $errMsg ) {
+ return new MediaTransformError( 'thumbnail_error', $params['clientWidth'],
+ $params['clientHeight'], $errMsg );
+ }
+
+ /**
+ * Transform an image using the built in GD library
+ *
+ * This is a stub method. The real method is in BitmapHander.
+ *
+ * @param File $image File associated with this thumbnail
+ * @param array $params Array with scaler params
+ *
+ * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
+ */
+ protected function transformGd( $image, $params ) {
+ return $this->getMediaTransformError( $params, "Unimplemented" );
+ }
+
+ /**
+ * Escape a string for ImageMagick's property input (e.g. -set -comment)
+ * See InterpretImageProperties() in magick/property.c
+ * @param string $s
+ * @return string
+ */
+ function escapeMagickProperty( $s ) {
+ // Double the backslashes
+ $s = str_replace( '\\', '\\\\', $s );
+ // Double the percents
+ $s = str_replace( '%', '%%', $s );
+ // Escape initial - or @
+ if ( strlen( $s ) > 0 && ( $s[0] === '-' || $s[0] === '@' ) ) {
+ $s = '\\' . $s;
+ }
+
+ return $s;
+ }
+
+ /**
+ * Escape a string for ImageMagick's input filenames. See ExpandFilenames()
+ * and GetPathComponent() in magick/utility.c.
+ *
+ * This won't work with an initial ~ or @, so input files should be prefixed
+ * with the directory name.
+ *
+ * Glob character unescaping is broken in ImageMagick before 6.6.1-5, but
+ * it's broken in a way that doesn't involve trying to convert every file
+ * in a directory, so we're better off escaping and waiting for the bugfix
+ * to filter down to users.
+ *
+ * @param string $path The file path
+ * @param bool|string $scene The scene specification, or false if there is none
+ * @throws MWException
+ * @return string
+ */
+ function escapeMagickInput( $path, $scene = false ) {
+ # Die on initial metacharacters (caller should prepend path)
+ $firstChar = substr( $path, 0, 1 );
+ if ( $firstChar === '~' || $firstChar === '@' ) {
+ throw new MWException( __METHOD__ . ': cannot escape this path name' );
+ }
+
+ # Escape glob chars
+ $path = preg_replace( '/[*?\[\]{}]/', '\\\\\0', $path );
+
+ return $this->escapeMagickPath( $path, $scene );
+ }
+
+ /**
+ * Escape a string for ImageMagick's output filename. See
+ * InterpretImageFilename() in magick/image.c.
+ * @param string $path The file path
+ * @param bool|string $scene The scene specification, or false if there is none
+ * @return string
+ */
+ function escapeMagickOutput( $path, $scene = false ) {
+ $path = str_replace( '%', '%%', $path );
+
+ return $this->escapeMagickPath( $path, $scene );
+ }
+
+ /**
+ * Armour a string against ImageMagick's GetPathComponent(). This is a
+ * helper function for escapeMagickInput() and escapeMagickOutput().
+ *
+ * @param string $path The file path
+ * @param bool|string $scene The scene specification, or false if there is none
+ * @throws MWException
+ * @return string
+ */
+ protected function escapeMagickPath( $path, $scene = false ) {
+ # Die on format specifiers (other than drive letters). The regex is
+ # meant to match all the formats you get from "convert -list format"
+ if ( preg_match( '/^([a-zA-Z0-9-]+):/', $path, $m ) ) {
+ if ( wfIsWindows() && is_dir( $m[0] ) ) {
+ // OK, it's a drive letter
+ // ImageMagick has a similar exception, see IsMagickConflict()
+ } else {
+ throw new MWException( __METHOD__ . ': unexpected colon character in path name' );
+ }
+ }
+
+ # If there are square brackets, add a do-nothing scene specification
+ # to force a literal interpretation
+ if ( $scene === false ) {
+ if ( strpos( $path, '[' ) !== false ) {
+ $path .= '[0--1]';
+ }
+ } else {
+ $path .= "[$scene]";
+ }
+
+ return $path;
+ }
+
+ /**
+ * Retrieve the version of the installed ImageMagick
+ * You can use PHPs version_compare() to use this value
+ * Value is cached for one hour.
+ * @return string Representing the IM version.
+ */
+ protected function getMagickVersion() {
+ global $wgMemc;
+
+ $cache = $wgMemc->get( "imagemagick-version" );
+ if ( !$cache ) {
+ global $wgImageMagickConvertCommand;
+ $cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . ' -version';
+ wfDebug( __METHOD__ . ": Running convert -version\n" );
+ $retval = '';
+ $return = wfShellExec( $cmd, $retval );
+ $x = preg_match( '/Version: ImageMagick ([0-9]*\.[0-9]*\.[0-9]*)/', $return, $matches );
+ if ( $x != 1 ) {
+ wfDebug( __METHOD__ . ": ImageMagick version check failed\n" );
+
+ return null;
+ }
+ $wgMemc->set( "imagemagick-version", $matches[1], 3600 );
+
+ return $matches[1];
+ }
+
+ return $cache;
+ }
+
+ /**
+ * Returns whether the current scaler supports rotation.
+ *
+ * @since 1.24 No longer static
+ * @return bool
+ */
+ public function canRotate() {
+ return false;
+ }
+
+ /**
+ * Should we automatically rotate an image based on exif
+ *
+ * @since 1.24 No longer static
+ * @see $wgEnableAutoRotation
+ * @return bool Whether auto rotation is enabled
+ */
+ public function autoRotateEnabled() {
+ return false;
+ }
+
+ /**
+ * Rotate a thumbnail.
+ *
+ * This is a stub. See BitmapHandler::rotate.
+ *
+ * @param File $file
+ * @param array $params Rotate parameters.
+ * 'rotation' clockwise rotation in degrees, allowed are multiples of 90
+ * @since 1.24 Is non-static. From 1.21 it was static
+ * @return bool
+ */
+ public function rotate( $file, $params ) {
+ return new MediaTransformError( 'thumbnail_error', 0, 0,
+ "$scaler rotation not implemented" );
+ }
+
+ /**
+ * Returns whether the file needs to be rendered. Returns true if the
+ * file requires rotation and we are able to rotate it.
+ *
+ * @param File $file
+ * @return bool
+ */
+ public function mustRender( $file ) {
+ return $this->canRotate() && $this->getRotation( $file ) != 0;
+ }
+}
* @param bool $checkDstPath
* @return string
*/
- protected static function getScalerType( $dstPath, $checkDstPath = true ) {
+ protected function getScalerType( $dstPath, $checkDstPath = true ) {
return "im";
}
$this->getExternalLinkAttribs( $url ) );
# Register it in the output object...
# Replace unnecessary URL escape codes with their equivalent characters
- $pasteurized = self::replaceUnusualEscapes( $url );
+ $pasteurized = self::normalizeLinkUrl( $url );
$this->mOutput->addExternalLink( $pasteurized );
}
wfProfileOut( __METHOD__ );
# Register link in the output object.
# Replace unnecessary URL escape codes with the referenced character
# This prevents spammers from hiding links from the filters
- $pasteurized = self::replaceUnusualEscapes( $url );
+ $pasteurized = self::normalizeLinkUrl( $url );
$this->mOutput->addExternalLink( $pasteurized );
}
}
/**
- * Replace unusual URL escape codes with their equivalent characters
+ * Replace unusual escape codes in a URL with their equivalent characters
*
+ * @deprecated since 1.24, use normalizeLinkUrl
* @param string $url
* @return string
- *
- * @todo This can merge genuinely required bits in the path or query string,
- * breaking legit URLs. A proper fix would treat the various parts of
- * the URL differently; as a workaround, just use the output for
- * statistical records, not for actual linking/output.
*/
public static function replaceUnusualEscapes( $url ) {
- return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
- array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
+ wfDeprecated( __METHOD__, '1.24' );
+ return self::normalizeLinkUrl( $url );
}
/**
- * Callback function used in replaceUnusualEscapes().
- * Replaces unusual URL escape codes with their equivalent character
+ * Replace unusual escape codes in a URL with their equivalent characters
*
- * @param array $matches
+ * This generally follows the syntax defined in RFC 3986, with special
+ * consideration for HTTP query strings.
*
+ * @param string $url
* @return string
*/
- private static function replaceUnusualEscapesCallback( $matches ) {
- $char = urldecode( $matches[0] );
- $ord = ord( $char );
- # Is it an unsafe or HTTP reserved character according to RFC 1738?
- if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
- # No, shouldn't be escaped
- return $char;
- } else {
- # Yes, leave it escaped
- return $matches[0];
+ public static function normalizeLinkUrl( $url ) {
+ # First, make sure unsafe characters are encoded
+ $url = preg_replace_callback( '/[\x00-\x20"<>\[\\\\\]^`{|}\x7F-\xFF]/',
+ function ( $m ) {
+ return rawurlencode( $m[0] );
+ },
+ $url
+ );
+
+ $ret = '';
+ $end = strlen( $url );
+
+ # Fragment part - 'fragment'
+ $start = strpos( $url, '#' );
+ if ( $start !== false && $start < $end ) {
+ $ret = self::normalizeUrlComponent(
+ substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}' ) . $ret;
+ $end = $start;
+ }
+
+ # Query part - 'query' minus &=+;
+ $start = strpos( $url, '?' );
+ if ( $start !== false && $start < $end ) {
+ $ret = self::normalizeUrlComponent(
+ substr( $url, $start, $end - $start ), '"#%<>[\]^`{|}&=+;' ) . $ret;
+ $end = $start;
}
+
+ # Scheme and path part - 'pchar'
+ # (we assume no userinfo or encoded colons in the host)
+ $ret = self::normalizeUrlComponent(
+ substr( $url, 0, $end ), '"#%<>[\]^`{|}/?' ) . $ret;
+
+ return $ret;
+ }
+
+ private static function normalizeUrlComponent( $component, $unsafe ) {
+ $callback = function ( $matches ) use ( $unsafe ) {
+ $char = urldecode( $matches[0] );
+ $ord = ord( $char );
+ if ( $ord > 32 && $ord < 127 && strpos( $unsafe, $char ) === false ) {
+ # Unescape it
+ return $char;
+ } else {
+ # Leave it escaped, but use uppercase for a-f
+ return strtoupper( $matches[0] );
+ }
+ };
+ return preg_replace_callback( '/%[0-9A-Fa-f]{2}/', $callback, $component );
}
/**
$dbw = wfGetDB( DB_MASTER );
$useTrx = ( $dbw->getType() === 'sqlite' ); // much faster
if ( $useTrx ) {
- $dbw->begin();
+ $dbw->startAtomic( __METHOD__ );
}
foreach ( $this->mCollated as $name => $data ) {
$eventCount = $data['count'];
// "pf_time=pf_time + VALUES(pf_time)";
}
if ( $useTrx ) {
- $dbw->commit();
+ $dbw->endAtomic( __METHOD__ );
}
} catch ( DBError $e ) {
}
--- /dev/null
+<?php
+/**
+ * Resource loader module for the edit toolbar.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for the edit toolbar.
+ *
+ * @since 1.24
+ */
+class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule {
+ /**
+ * Serialize a string (escape and quote) for use as a CSS string value.
+ * http://www.w3.org/TR/2013/WD-cssom-20131205/#serialize-a-string
+ *
+ * @param string $value
+ * @return string
+ */
+ private static function cssSerializeString( $value ) {
+ if ( strstr( $value, "\0" ) ) {
+ throw new Exception( "Invalid character in CSS string" );
+ }
+ $value = strtr( $value, array( '\\' => '\\\\', '"' => '\\"' ) );
+ $value = preg_replace_callback( '/[\x01-\x1f\x7f-\x9f]/', function ( $match ) {
+ return '\\' . base_convert( ord( $match[0] ), 10, 16 ) . ' ';
+ }, $value );
+ return '"' . $value . '"';
+ }
+
+ /**
+ * Get language-specific LESS variables for this module.
+ *
+ * @return array
+ */
+ private function getLessVars( ResourceLoaderContext $context ) {
+ $language = Language::factory( $context->getLanguage() );
+
+ // This is very conveniently formatted and we can pass it right through
+ $vars = $language->getImageFiles();
+
+ // lessc tries to be helpful and parse our variables as LESS source code
+ foreach ( $vars as $key => &$value ) {
+ $value = self::cssSerializeString( $value );
+ }
+
+ return $vars;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return int UNIX timestamp
+ */
+ public function getModifiedTime( ResourceLoaderContext $context ) {
+ return max(
+ parent::getModifiedTime( $context ),
+ $this->getHashMtime( $context )
+ );
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string Hash
+ */
+ public function getModifiedHash( ResourceLoaderContext $context ) {
+ return md5(
+ parent::getModifiedHash( $context ) .
+ serialize( $this->getLessVars( $context ) )
+ );
+ }
+
+ /**
+ * Get a LESS compiler instance for this module.
+ *
+ * Set our variables in it.
+ *
+ * @throws MWException
+ * @param ResourceLoaderContext $context
+ * @return lessc
+ */
+ protected function getLessCompiler( ResourceLoaderContext $context = null ) {
+ $compiler = parent::getLessCompiler();
+ $compiler->setVariables( $this->getLessVars( $context ) );
+ return $compiler;
+ }
+}
public function getStyles( ResourceLoaderContext $context ) {
$styles = $this->readStyleFiles(
$this->getStyleFiles( $context ),
- $this->getFlip( $context )
+ $this->getFlip( $context ),
+ $context
);
// Collect referenced files
$this->localFileRefs = array_unique( $this->localFileRefs );
*
* @param array $styles List of media type/list of file paths pairs, to read, remap and
* concetenate
- *
* @param bool $flip
+ * @param ResourceLoaderContext $context (optional)
*
* @throws MWException
* @return array List of concatenated and remapped CSS data from $styles,
* keyed by media type
*/
- public function readStyleFiles( array $styles, $flip ) {
+ public function readStyleFiles( array $styles, $flip, $context = null ) {
if ( empty( $styles ) ) {
return array();
}
$uniqueFiles = array_unique( $files, SORT_REGULAR );
$styleFiles = array();
foreach ( $uniqueFiles as $file ) {
- $styleFiles[] = $this->readStyleFile( $file, $flip );
+ $styleFiles[] = $this->readStyleFile( $file, $flip, $context );
}
$styles[$media] = implode( "\n", $styleFiles );
}
*
* @param string $path File path of style file to read
* @param bool $flip
+ * @param ResourceLoaderContext $context (optional)
*
* @return string CSS data in script file
* @throws MWException If the file doesn't exist
*/
- protected function readStyleFile( $path, $flip ) {
+ protected function readStyleFile( $path, $flip, $context = null ) {
$localPath = $this->getLocalPath( $path );
$remotePath = $this->getRemotePath( $path );
if ( !file_exists( $localPath ) ) {
}
if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
- $style = $this->compileLessFile( $localPath );
+ $compiler = $this->getLessCompiler( $context );
+ $style = $this->compileLessFile( $localPath, $compiler );
$this->hasGeneratedStyles = true;
} else {
$style = file_get_contents( $localPath );
* @since 1.22
* @throws Exception If lessc encounters a parse error
* @param string $fileName File path of LESS source
+ * @param lessc $compiler Compiler to use, if not default
* @return string CSS source
*/
- protected function compileLessFile( $fileName ) {
- $compiler = ResourceLoader::getLessCompiler( $this->getConfig() );
+ protected function compileLessFile( $fileName, $compiler = null ) {
+ if ( !$compiler ) {
+ $compiler = $this->getLessCompiler();
+ }
$result = $compiler->compileFile( $fileName );
$this->localFileRefs += array_keys( $compiler->allParsedFiles() );
return $result;
}
+
+ /**
+ * Get a LESS compiler instance for this module in given context.
+ *
+ * Just calls ResourceLoader::getLessCompiler() by default to get a global compiler.
+ *
+ * @param ResourceLoaderContext $context
+ * @throws MWException
+ * @since 1.24
+ * @return lessc
+ */
+ protected function getLessCompiler( ResourceLoaderContext $context = null ) {
+ return ResourceLoader::getLessCompiler( $this->getConfig() );
+ }
}
'wgDBname' => $conf->get( 'DBname' ),
// This sucks, it is only needed on Special:Upload, but I could
// not find a way to add vars only for a certain module
- 'wgFileCanRotate' => BitmapHandler::canRotate(),
+ 'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
'wgAvailableSkins' => Skin::getSkinNames(),
'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
// MediaWiki sets cookies to have this prefix by default
$dbw = $this->sitesTable->getWriteDbConnection();
- $trx = $dbw->trxLevel();
-
- if ( $trx == 0 ) {
- $dbw->begin( __METHOD__ );
- }
+ $dbw->startAtomic( __METHOD__ );
$success = true;
);
}
- if ( $trx == 0 ) {
- $dbw->commit( __METHOD__ );
- }
+ $dbw->endAtomic( __METHOD__ );
// purge cache
$this->reset();
wfProfileIn( __METHOD__ );
$dbw = $this->sitesTable->getWriteDbConnection();
- $trx = $dbw->trxLevel();
-
- if ( $trx == 0 ) {
- $dbw->begin( __METHOD__ );
- }
-
+ $dbw->startAtomic( __METHOD__ );
$ok = $dbw->delete( 'sites', '*', __METHOD__ );
$ok = $dbw->delete( 'site_identifiers', '*', __METHOD__ ) && $ok;
-
- if ( $trx == 0 ) {
- $dbw->commit( __METHOD__ );
- }
+ $dbw->endAtomic( __METHOD__);
$this->reset();
$page['language'] = array(
'id' => 'mw-pl-languageselector',
+ 'cssclass' => 'mw-languageselector',
'type' => 'select',
'options' => $options,
'label-message' => 'pagelang-language',
public function alterForm( HTMLForm $form ) {
$form->setDisplayFormat( 'vform' );
$form->setWrapperLegend( false );
+ wfRunHooks( 'LanguageSelector', array( $this->getOutput(), 'mw-languageselector' ) );
}
/**
$htmlForm = new HTMLForm( array(), $context, 'prefs-restore' );
$htmlForm->setSubmitTextMsg( 'restoreprefs' );
+ $htmlForm->setSubmitDestructive();
$htmlForm->setSubmitCallback( array( $this, 'submitReset' ) );
$htmlForm->suppressReset();
protected function getGroupName() {
return 'media';
}
+
+ /**
+ * Should we rotate images in the preview on Special:Upload.
+ *
+ * This controls js: mw.config.get( 'wgFileCanRotate' )
+ *
+ * @todo What about non-BitmapHandler handled files?
+ */
+ static public function rotationEnabled() {
+ $bitmapHandler = new BitmapHandler();
+ return $bitmapHandler->autoRotateEnabled();
+ }
}
/**
return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
}
+ /**
+ * @return array
+ * @since 1.24
+ */
+ function getImageFiles() {
+ return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
+ }
+
/**
* @return array
*/
},
"sidebar": "\n* navigation\n** mainpage|mainpage-description\n** recentchanges-url|recentchanges\n** randompage-url|randompage\n** helppage|help\n* SEARCH\n* TOOLBOX\n* LANGUAGES",
"tog-underline": "Link underlining:",
- "tog-hideminor": "Hide minor edits in recent changes",
- "tog-hidepatrolled": "Hide patrolled edits in recent changes",
+ "tog-hideminor": "Hide minor edits from recent changes",
+ "tog-hidepatrolled": "Hide patrolled edits from recent changes",
"tog-newpageshidepatrolled": "Hide patrolled pages from new page list",
"tog-extendwatchlist": "Expand watchlist to show all changes, not just the most recent",
"tog-usenewrc": "Group changes by page in recent changes and watchlist",
"preferences-summary": "",
"mypreferences": "Preferences",
"prefs-edits": "Number of edits:",
- "prefsnologintext2": "Please login to change your preferences.",
+ "prefsnologintext2": "Please log in to change your preferences.",
"prefs-skin": "Skin",
"skin-preview": "Preview",
"datedefault": "No preference",
"mywatchlist": "Watchlist",
"watchlistfor2": "For $1 $2",
"nowatchlist": "You have no items on your watchlist.",
- "watchlistanontext": "Please login to view or edit items on your watchlist.",
+ "watchlistanontext": "Please log in to view or edit items on your watchlist.",
"watchnologin": "Not logged in",
"addwatch": "Add to watchlist",
"addedwatchtext": "The page \"[[:$1]]\" has been added to your [[Special:Watchlist|watchlist]].\nFuture changes to this page and its associated talk page will be listed there.",
"contribsub2": "برای {{GENDER:$3|$1}} ($2)",
"contributions-userdoesnotexist": "حساب کاربری «$1» ثبت نشدهاست.",
"nocontribs": "هیچ تغییری با این مشخصات یافت نشد.",
- "uctop": "(نسخه کنونی)",
+ "uctop": "(نسخهٔ کنونی)",
"month": "در این ماه (و پیش از آن):",
"year": "در این سال (و پیش از آن):",
"sp-contributions-newbies": "فقط مشارکتهای تازهکاران نمایش داده شود",
"gotaccountlink": "登入",
"userlogin-resetlink": "毋記得汝嘅登入信息?",
"userlogin-resetpassword-link": "添忘訖汝嘅密碼?",
- "userlogin-loggedin": "汝已作為{{GENDER:$1|$1}}登錄。\n利用以下表單以作為另一賬戶登錄。",
"createacct-emailrequired": "電郵地址:",
"createacct-emailoptional": "電郵地址(可選)",
"createacct-email-ph": "設置電郵地址",
"template-protected": "(保護)",
"template-semiprotected": "(半保護)",
"hiddencategories": "邇頁屬於$1隻隱藏分類嘅成員:",
- "edittools": "<!-- 邇肚嘅文字將分展示在編寫撈上傳表單以下。 -->",
+ "edittools": "<!-- 邇肚嘅文字將分展示在編寫撈上傳表單以下。 -->\n<div id=\"editpage-specialchars\" class=\"plainlinks edittools-version-test003\" style=\"margin-top: 15px; border-width: 1px; border-style: solid; border-color: #aaaaaa; padding: 2px;\"> <span id=\"edittools_main\">'''Insert:''' <charinsert>– — … ‘ “ ’ ” ° ″ ′ ≈ ≠ ≤ ≥ ± − × ÷ ← → · § </charinsert></span><span id=\"edittools_name\"> '''Sign your username:''' <charinsert>--~~~~</charinsert> <small>(on [[Help:Talk pages|talk pages]])</small></span> ---- <small id=\"edittools_newsectionshere\"><span id=\"edittools_hide_for_script_test\"><span id=\"edittools_wikimarkup\">'''Wiki markup:''' <charinsert><nowiki>{{</nowiki>+<nowiki>}}</nowiki> </charinsert> <charinsert><nowiki>{{{</nowiki>+<nowiki>}}}</nowiki> </charinsert> <charinsert><nowiki>|</nowiki></charinsert> <charinsert>[+]</charinsert> <charinsert>[[+]]</charinsert> <charinsert>[[Category:+]]</charinsert> <charinsert>#REDIRECT [[+]]</charinsert> <charinsert>{{Subst:Fôn-ngiàng}}</charinsert> <charinsert>{{Subst:PAGENAME}}</charinsert> <charinsert> </charinsert> <charinsert><s>+</s></charinsert> <charinsert><sup>+</sup></charinsert> <charinsert><sub>+</sub></charinsert> <charinsert><code>+</code></charinsert> <charinsert><blockquote>+</blockquote></charinsert> <charinsert><ref>+</ref></charinsert> <charinsert><nowiki>{{</nowiki>Reflist<nowiki>}}</nowiki></charinsert> <charinsert><references/></charinsert> <charinsert><includeonly>+</includeonly></charinsert> <charinsert><noinclude>+</noinclude></charinsert> <charinsert><nowiki>{{</nowiki>DEFAULTSORT:+<nowiki>}}</nowiki></charinsert> <charinsert><nowiki>+</nowiki></charinsert> <charinsert><nowiki><!-- </nowiki>+<nowiki> --></nowiki></charinsert> <charinsert><nowiki><span class=\"plainlinks\"></nowiki>+<nowiki></span></nowiki></charinsert> • ([[Wikipedia:Template messages|templates]])<br/></span> <span id=\"edittools_symbols\">'''Symbols:''' <charinsert> ~ | ¡ ¿ † ‡ ↔ ↑ ↓ • ¶</charinsert> <charinsert> # ½ ⅓ ⅔ ¼ ¾ ⅛ ⅜ ⅝ ⅞ ∞ </charinsert> <charinsert> ‘ “ ’ ” «+»</charinsert> <charinsert> ¤ ₳ ฿ ₵ ¢ ₡ ₢ $ ₫ ₯ € ₠ ₣ ƒ ₴ ₭ ₤ ℳ ₥ ₦ № ₧ ₰ £ ៛ ₨ ₪ ৳ ₮ ₩ ¥ </charinsert> <charinsert> ♠ ♣ ♥ ♦ </charinsert> <charinsert>m²</charinsert> <charinsert>m³</charinsert><br/></span> <span id=\"edittools_characters\">'''Characters:''' <span class=\"latinx\"> <charinsert> Á á Ć ć É é Í í Ĺ ĺ Ḿ ḿ Ń ń Ó ó Ŕ ŕ Ś ś Ú ú Ý ý Ź ź </charinsert> <charinsert> À à È è Ì ì M̀ m̀ Ǹ ǹ Ò ò Ù ù </charinsert> <charinsert>  â Ĉ ĉ Ê ê Ĝ ĝ Ĥ ĥ Î î Ĵ ĵ Ô ô Ŝ ŝ Û û Ŵ ŵ Ŷ ŷ </charinsert> <charinsert> A̤ a̤ E̤ e̤ I̤ i̤ O̤ o̤ Ṳ ṳ </charinsert> \n<charinsert> A̍ a̍ E̍ e̍ I̍ i̍ O̍ o̍ U̍ u̍ </charinsert> <charinsert> À̤ à̤ È̤ è̤ Ì̤ ì̤ Ò̤ ò̤ Ṳ̀ ṳ̀ </charinsert> \n<charinsert> Á̤ á̤ É̤ é̤ Í̤ í̤ Ó̤ ó̤ Ṳ́ ṳ́ </charinsert> <charinsert> A̤̍ a̤̍ E̤̍ e̤̍ I̤̍ i̤̍ O̤̍ o̤̍ Ṳ̍ ṳ̍ </charinsert> \n<charinsert> Â̤ â̤ Ê̤ ê̤ Î̤ î̤ Ô̤ ô̤ Ṳ̂ ṳ̂ </charinsert> <charinsert>  â Ĉ ĉ Ê ê Ĝ ĝ Ĥ ĥ Î î Ĵ ĵ Ô ô Ŝ ŝ Û û Ŵ ŵ Ŷ ŷ </charinsert> <charinsert> Ä ä Ë ë Ï ï Ö ö Ü ü Ÿ ÿ </charinsert> <charinsert> ß </charinsert> <charinsert> à ã Ẽ ẽ Ĩ ĩ Ñ ñ Õ õ Ũ ũ Ỹ ỹ</charinsert> <charinsert> Ç ç Ģ ģ Ķ ķ Ļ ļ Ņ ņ Ŗ ŗ Ş ş Ţ ţ </charinsert> <charinsert> Đ đ </charinsert> <charinsert> Ů ů </charinsert> <charinsert> Ǎ ǎ Č č Ď ď Ě ě Ǐ ǐ Ľ ľ Ň ň Ǒ ǒ Ř ř Š š Ť ť Ǔ ǔ Ž ž </charinsert> <charinsert> Ā ā Ē ē Ī ī Ō ō Ū ū Ȳ ȳ Ǣ ǣ </charinsert> <charinsert> ǖ ǘ ǚ ǜ </charinsert> <charinsert> Ă ă Ĕ ĕ Ğ ğ Ĭ ĭ Ŏ ŏ Ŭ ŭ </charinsert> <charinsert> Ċ ċ Ė ė Ġ ġ İ ı Ż ż </charinsert> <charinsert> Ą ą Ę ę Į į Ǫ ǫ Ų ų </charinsert> <charinsert> Ḍ ḍ Ḥ ḥ Ḷ ḷ Ḹ ḹ Ṃ ṃ Ṇ ṇ Ṛ ṛ Ṝ ṝ Ṣ ṣ Ṭ ṭ </charinsert> <charinsert> Ł ł </charinsert> <charinsert> Ő ő Ű ű </charinsert> <charinsert> Ŀ ŀ </charinsert> <charinsert> Ħ ħ </charinsert> <charinsert> Ð ð Þ þ </charinsert> <charinsert> Œ œ </charinsert> <charinsert> Æ æ Ø ø Å å </charinsert> <charinsert> Ə ə </charinsert></span> <span id=\"edittools_latinx_template\"> • <charinsert><nowiki>{{</nowiki><nowiki>Unicode|</nowiki>+<nowiki>}}</nowiki></charinsert></span><br/></span> <span id=\"edittools_greek\">'''Hî-lia̍p-vùn:''' <charinsert> Ά ά Έ έ Ή ή Ί ί Ό ό Ύ ύ Ώ ώ </charinsert> <charinsert> Α α Β β Γ γ Δ δ </charinsert> <charinsert> Ε ε Ζ ζ Η η Θ θ </charinsert> <charinsert> Ι ι Κ κ Λ λ Μ μ </charinsert> <charinsert> Ν ν Ξ ξ Ο ο Π π </charinsert> <charinsert> Ρ ρ Σ σ ς Τ τ Υ υ </charinsert> <charinsert> Φ φ Χ χ Ψ ψ Ω ω </charinsert> <span id=\"edittools_greek_template\">• <charinsert><nowiki>{{</nowiki><nowiki>Polytonic|</nowiki>+<nowiki>}}</nowiki></charinsert></span> <span id=\"edittools_greek_example\">• ([[Greek diacritics#Computer encoding|polytonic list]])</span><br/></span> <span id=\"edittools_cyrillic\">'''Cyrillic:''' <charinsert> А а Б б В в Г г </charinsert> <charinsert> Ґ ґ Ѓ ѓ Д д Ђ ђ </charinsert> <charinsert> Е е Ё ё Є є Ж ж </charinsert> <charinsert> З з Ѕ ѕ И и І і </charinsert> <charinsert> Ї ї Й й Ј ј К к </charinsert> <charinsert> Ќ ќ Л л Љ љ М м </charinsert> <charinsert> Н н Њ њ О о П п </charinsert> <charinsert> Р р С с Т т Ћ ћ </charinsert> <charinsert> У у Ў ў Ф ф Х х </charinsert> <charinsert> Ц ц Ч ч Џ џ Ш ш </charinsert> <charinsert> Щ щ Ъ ъ Ы ы Ь ь </charinsert> <charinsert> Э э Ю ю Я я </charinsert> <br/></span> <span id=\"edittools_ipa\">'''IPA:''' <span title=\"Pronunciation in IPA\" class=\"IPA\"><charinsert>t̪ d̪ ʈ ɖ ɟ ɡ ɢ ʡ ʔ </charinsert> <charinsert> ɸ ʃ ʒ ɕ ʑ ʂ ʐ ʝ ɣ ʁ ʕ ʜ ʢ ɦ </charinsert> <charinsert> ɱ ɳ ɲ ŋ ɴ </charinsert> <charinsert> ʋ ɹ ɻ ɰ </charinsert> <charinsert> ʙ ʀ ɾ ɽ </charinsert> <charinsert> ɫ ɬ ɮ ɺ ɭ ʎ ʟ </charinsert> <charinsert> ɥ ʍ ɧ </charinsert> <charinsert> ɓ ɗ ʄ ɠ ʛ </charinsert> <charinsert> ʘ ǀ ǃ ǂ ǁ </charinsert> <charinsert> ɨ ʉ ɯ </charinsert> <charinsert> ɪ ʏ ʊ </charinsert> <charinsert> ɘ ɵ ɤ </charinsert> <charinsert> ə ɚ </charinsert> <charinsert> ɛ ɜ ɝ ɞ ʌ ɔ </charinsert> <charinsert> ɐ ɶ ɑ ɒ </charinsert> <charinsert> ʰ ʷ ʲ ˠ ˤ ⁿ ˡ </charinsert> <charinsert> ˈ ˌ ː ˑ ̪ </charinsert> </span> • <charinsert><nowiki>{{</nowiki><nowiki>IPA|</nowiki>+<nowiki>}}</charinsert></small>\n</div>",
"nocreatetext": "Chhṳ́ mióng-chham han-chṳ chhóng-chho sîn hong-mien ke kûng-yung. ngì khó-yî fán-fì pin phiên-cho yí-kîn yû ke hong-mien, fe̍t-chá [[Special:UserLogin|tên-liu̍k fe̍t-he chhóng-kien sîn chong-fu]].",
"nocreate-loggedin": "汝並無權限去創建新頁面。",
"permissionserrors": "權限差錯",
"searchrelated": "相關",
"searchall": "全部",
"showingresults": "Ha-mien hién-sṳ chhiùng thi-'''$2'''-thiàu khôi-sṳ́ ke '''$1'''-thiàu kiet-kó:",
- "showingresultsheader": "對'''$4'''嘅{{PLURAL:$5|第'''$1'''到第'''$3'''隻結果|第'''$1 - $2'''隻,共'''$3'''隻結果}}",
"search-nonefound": "在查詢肚無結果相符。",
"powersearch-legend": "高級搜尋",
"powersearch-ns": "在下背嘅名字空間肚搜尋:",
"hidetoc": "elrejtés",
"collapsible-collapse": "becsuk",
"collapsible-expand": "kinyit",
+ "confirmable-confirm": "Biztos {{GENDER:$1|vagy}} benne?",
+ "confirmable-yes": "Igen",
+ "confirmable-no": "Nem",
"thisisdeleted": "$1 megtekintése vagy helyreállítása?",
"viewdeleted": "$1 megtekintése?",
"restorelink": "{{PLURAL:$1|Egy|$1}} törölt szerkesztés",
"searchrelated": "მიბმული",
"searchall": "ყველა",
"showingresults": "ქვემოთ იხილეთ <b>$1</b>-მდე შედეგი დაწყებული #<b>$2</b>-იდან.",
- "showingresultsheader": "{{PLURAL:$5|რეზულტატი '''$1''' '''$3'''-დან|რეზულტატები '''$1 — $2''' -დან '''$3'''}} '''$4'''-თვის",
"search-nonefound": "მოთხოვნის შესაბამისობა არ არის ნაპოვნი.",
"powersearch-legend": "გაფართოებული ძიება",
"powersearch-ns": "ძიება სახელთა სივრცეებში:",
"listusers": "მომხმარებლების სია",
"listusers-editsonly": "აჩვენთ მხოლოდ ის მომხმარებლები, რომლებსაც ერთი შესწორება აქვს გაკეთებული.",
"listusers-creationsort": "დაალაგეთ შექმნის თარიღის მიხედვით.",
+ "listusers-desc": "კლების მიხედვით დალაგება",
"usereditcount": "$1 რედაქტირება",
"usercreated": "{{GENDER:$3|შექმნილია}} $2-ზე $1-ში",
"newpages": "ახალი გვერდები",
"tooltip-recreate": "Бет жойылғанына қарамастан қайта бастау",
"tooltip-upload": "Жүктеуді бастау",
"tooltip-rollback": "\"Шегіндіру\" сілтемесін бір рет басу арқылы соңға редактордың барлық қатар өңдемелерін өшіру",
+ "tooltip-undo": "«Жоққа шығару» сілтемесін бассаңыз бұл өңдеме болдырылмайды және өңдеу пішіні қарап шығу режимінде ашылады. Ол өңдеу түйіндемесіне себебін қосуға мүмкіндік береді.",
"tooltip-preferences-save": "Бапталымдарыңызды сақтау",
"tooltip-summary": "Қысқаша түйіндемесін енгізіңіз",
"interlanguage-link-title-nonlang": "$1 – $2",
"wantedpages-badtitle": "سرون نامعتور د کومله نتیجه یا:$1",
"wantedfiles": "فایلیا حاستنی",
"wantedfiletext-cat": "جانیایا هاری وه کار گرته بوئن ولی وجود نارن. همچنو شایت جانیایا وه دری وا یه که ایچه هیئشون نومگه کاری بینه.هر گرینج مثبت دورویی <del>خط مئوره.</del> به اضافه یه، بلگه یایی که که جانیایا بی وجودن نه د خوشو دارن د [[:$1]] نومگه کاری بینه.",
+ "wantedfiletext-cat-noforeign": "جانیایا هاری وه کار گرته بوئن ولی نیئشو. اضافه وه یه بلگه یایی که جانیایا نادیار د خوشو دارن هان د [[:$1]].",
"wantedfiletext-nocat-noforeign": "جانیایا هاری وه کار گرته بوئن ولی نیئشو.",
"wantedtemplates": "قالویا حاستنی",
"mostlinked": "بلگه یا که بیشتر هوم پیوند بینه",
"protectedpages-indef": "فقط پر و پیم بیین یا بی زمون",
"protectedpages-cascade": "فقط پر و پیم بیین تافنمایی",
"protectedpages-noredirect": "واگردونیا قام بیه",
+ "protectedpagesempty": "د ایسنی هیچ بلگه ای پر و پیم نبیه.",
"protectedpages-timestamp": "سردیس گات",
"protectedpages-page": "بلگه",
"protectedpages-expiry": "تموم بیه آ",
"protectedpages-unknown-timestamp": "ناشناس",
"protectedpages-unknown-performer": "کارور ناشناس",
"protectedtitles": "سرونیا پر و پیم بیه",
+ "protectedtitlesempty": "د ایسنی هیچ سرونی وا ای پارامتریا پر و پیم نبیه",
"listusers": "نوم گه کارور",
"listusers-editsonly": "فقط کاروریایی که ویرایشت می کن نشو بیه",
"listusers-creationsort": "سرجاخودگری د اساس گات دروس بیین",
"ancientpages": "بلگه یا نهاتر",
"move": "جاوه جا بوئيت",
"movethispage": "ای بگله نه جا وه جا كو",
+ "unusedcategoriestext": "ای دسه یا هیئشو ولی د ایسنی هیچ گوتار یا دسه ای ونونه وه کار نمی بنه.",
"notargettitle": "رسینه جایی نئ",
+ "notargettext": "شما بلگه یا کاریاری مقصدی سی انجوم دئن ای کنشت ریش انتخاو نکردیته.",
"nopagetitle": "چنی بلگه ای نیئش",
+ "nopagetext": "بلگه حاستنی که شما دیاری کردیته وجود ناره.",
"pager-newer-n": "{{جمی:$1|وانها تر 1وانها تر $1}}",
"pager-older-n": "{{جمی:$1|گپسالتر 1|گپسالتر $1}}",
"suppress": "پائیئن",
+ "querypage-disabled": "ای بلگه ویجه سی دلیلیا انجومکاری ناکشتگر بیه.",
"booksources": "سرچشمه يل كتاو",
"booksources-search-legend": "پی جوری سی سرچشمه یا کتاو",
"booksources-isbn": "آی اس بی ان:",
"log": "نیسنن رخ ونیا",
"all-logs-page": "همه پهرستنومه یا عمومی",
"logempty": "او چی ای که شما میهایت د پهرستنومه نیئش.",
+ "log-title-wildcard": "بلگه یایی نه پی جوری کو که وا ای سرون شرو موئن",
"showhideselectedlogentries": "آلشت دئن ورتیه گر پهرستنومه یا انتخاو بیه",
"allpages": "همه بلگيا",
"nextpage": "بلگه نهایی($1)",
"allpagesbadtitle": "عنوان بلگه حاسته بیه معتور نی،یا یه گل مئن زونی یا مئن ویکی عنوان غلطه.\nیه شایت شومل یکی با یا بیشتر کاراکتریا نبوئه که سی ای موضوعیا استفاده بوئن",
"allpages-bad-ns": "{{نوم دیارگه}} د ای نوم جا نئ \"$1\".",
"allpages-hide-redirects": "واگردونیا قام بیه",
+ "cachedspecial-viewing-cached-ttl": "شما د حال و بار دیئن یه گل نسقه ای د ای بلگه که ها د مینجاگیر هیئت که شایت سی $1 دماتر با.",
"cachedspecial-refresh-now": "دیئن آخری.",
"categories": "دسه يا",
"categoriesfrom": "دسه یایی که د شرو بینه نشو بیه:",
"listusers-noresult": "هیچ کاروری پیدا نبی",
"listusers-blocked": "(قلف بيه)",
"activeusers": "نوم گه کاروریا کارکو",
+ "activeusers-count": "$1 {{PLURAL:$1|کنشت|کنشت}} در {{PLURAL:$3|رو|$3 رو}} دماتر",
"activeusers-from": "کاریاریایی که د شرو بینه نشو بیه:",
"activeusers-hidebots": "بوتیا قام کو",
"activeusers-hidesysops": "دیوون داریا نه قام کو",
"actioncomplete": "عملكرد كامل بيه",
"actionfailed": "عملكرد شكست حرده",
"dellogpage": "لاگ پاك كردن",
+ "dellogpagetext": "نومگه هاری یه گل نومگه د آخری چیا پاکسا بیه هئ.",
"deletionlog": "پهرستنومه پاک بیئن",
"reverted": "لرسه د نزیکترین وانئری",
"deletecomment": "دليل:",
"deletereasonotherlist": "دلیل هنی",
"deletereason-dropdown": "* دلیلیا پاکسا کردن رسم بیه\n** اسپم\n** خراوکاری\n** رعایت نبین کپی رایت\n** درحاست نیسنه\n** نهاورگشت شکست حرده",
"delete-edit-reasonlist": "دلیلیا پاکسا کردنه نه ویرایشت بکید",
+ "deleteprotected": "شما نمی تونیت ای بلگه نه پاکسا بکیت سی یه که وه پر و پیم بیه.",
"rollback": "چواشه کردن ویرایشتیا",
"rollback_short": "چواشه کردن",
"rollbacklink": "ورگشتن",
+ "rollbacklinkcount": "چواشه کردن $1 {{PLURAL:$1|ویرایشت|ویرایشتیا}}",
+ "rollbacklinkcount-morethan": "چواشه کردن بیشتر د$1 {{PLURAL:$1|ویرایشت|ویرایشتیا}}",
"rollbackfailed": "چواشه کردن د خوئی انجوم نبی",
"editcomment": "ویرایشت چکشه وه: \"''$1''\" بی.",
"sessionfailure-title": "شکست حردن نشینگه",
"protect-default": "همه کاروریا اجازه دارن",
"protect-summary-desc": "[$1=$2] ($3)",
"protect-summary-cascade": "د حال و بال تافنمایی",
+ "protect-expiring": "گات تموم بیین $1 (یو تی سی)",
+ "protect-expiring-local": "گات تموم بیین $1",
"protect-expiry-indefinite": "بی زمون",
"protect-othertime": "وخت هنی:",
"protect-othertime-op": "گات هنی",
"protect-edit-reasonlist": "دلیلا پر و پیم بیین ویرایشت",
"protect-expiry-options": "1 ساعت:1 ساعت,1 روز:1 روز,1 هفته:1 هفته,2 هفته:2 هفته,1 ما:1 ما,3 ما:3 ما,6 ما:6 ما,1 سال:1 سال,بی حساو:بی حساو",
"restriction-type": "دسرسی:",
+ "restriction-level": "ریتراز محدودیت:",
"minimum-size": "انازه کمترونه",
"maximum-size": "انازه بیشترونه",
"pagesize": "(بایتیا)",
"revertmove": "لرستن",
"delete_and_move": "پاکسا و جا وه جا بوئه",
"delete_and_move_confirm": "هری بلگه نه پاکسا کو",
+ "immobile-source-page": "ای بلگه جا وه جا کردنی نئ.",
"export": "وه صحرا ديئن بلگيا",
"exportall": "وه صحرا ديئن همه بلگيا",
"export-submit": "وه در ديئن",
"thumbnail-more": "گپ كردن",
"filemissing": "گم بیئن جانیا",
"thumbnail_error": "خطا د راس بیئن بن کلئکی:$1",
+ "import": "وامین اوردن بلگه یا",
"import-interwiki-sourcewiki": "سرچشمه ویکی:",
"import-interwiki-sourcepage": "بلگه سرچشمه:",
"import-interwiki-submit": "وامین اوردن",
"unblocked": "[[User:$1|$1]] foi desbloqueado",
"unblocked-range": "A gama $1 foi desbloqueada",
"unblocked-id": "O bloqueio de $1 foi removido com sucesso",
+ "unblocked-ip": "[[Special:Contributions/$1|$1]] foi desbloqueado.",
"blocklist": "Utilizadores bloqueados",
"ipblocklist": "Utilizadores bloqueados",
"ipblocklist-legend": "Procurar um utilizador bloqueado",
"otherlanguages": "This message is shown under the toolbox. It is used if there are interwiki links added to the page, like <code><nowiki>[[</nowiki>en:Interwiki article]]</code>.\n{{Identical|Otherlanguages}}",
"redirectedfrom": "The text displayed when a certain page is redirected to another page. Parameters:\n* $1 - the name of the page user came from",
"redirectpagesub": "Displayed under the page title of a page which is a redirect to another page, see [{{fullurl:Project:Translators|redirect=no}} Project:Translators] for example.\n\n{{Identical|Redirect page}}",
- "redirectto": "Alt text of the arrow icon shown on redirect pages ([[commons:File:Sample redirect page.jpg]]).\n\n{{Identical|Redirect page}}",
+ "redirectto": "Alt text of the arrow icon shown on redirect pages ([[commons:File:Sample redirect page.jpg]]).\n\n{{Identical|Redirect to}}",
"talkpageheader": "{{notranslate}}",
"lastmodifiedat": "This message is shown below each page, in the footer with the logos and links.\n\nParameters:\n* $1 - date\n* $2 - time\nSee also:\n* {{msg-mw|Lastmodifiedatby}}",
"viewcount": "Used as page-view counter. Parameters:\n* $1 - number of pageviews",
"right-deletedtext": "просмотр удалённого текста и изменений между удалёнными версиями страниц",
"right-browsearchive": "поиск удалённых страниц",
"right-undelete": "восстановление страниц",
- "right-suppressrevision": "Ð\9fÑ\80оÑ\81моÑ\82Ñ\80, Ñ\81крытие и восстановление скрытых версий страниц",
+ "right-suppressrevision": "пÑ\80оÑ\81моÑ\82Ñ\80, Ñ\81окрытие и восстановление скрытых версий страниц",
"right-viewsuppressed": "Просмотр версий, скрытых от всех участников",
"right-suppressionlog": "просмотр частных журналов",
"right-block": "установка ограничений на редактирование для других участников",
"creditspage": "Autores de sa pàgina",
"pageinfo-header-edits": "Istòria de is mudàntzias",
"pageinfo-article-id": "ID pàgina",
+ "pageinfo-firstuser": "Creadore de sa pàgina",
+ "pageinfo-firsttime": "Data de creatzione de sa pàgina",
+ "pageinfo-lastuser": "Ùrtimu contribudore",
+ "pageinfo-lasttime": "Data de s'ùrtimu càmbiu",
+ "pageinfo-edits": "Nùmeru totale de càmbios",
+ "pageinfo-authors": "Nùmeru totale de autores dislindados",
+ "pageinfo-redirectsto-info": "info",
"pageinfo-contentpage-yes": "Eja",
"pageinfo-protect-cascading-yes": "Eja",
+ "pageinfo-category-pages": "Nùmeru de pàginas",
+ "pageinfo-category-subcats": "Nùmeru de sutacategorias",
+ "pageinfo-category-files": "Nùmeru de documentos",
+ "markaspatrolleddiff": "Marca comente cumprobadu",
+ "markaspatrolledtext": "Marca custa pàgina comente cumprobada",
+ "markedaspatrolled": "Marcada comente cumprobada",
"previousdiff": "← Càmbiu in segus",
"nextdiff": "Càmbiu in antis →",
+ "thumbsize": "Mannesa de is miniaturas:",
"widthheightpage": "$1 × $2, $3 {{PLURAL:$3|pàgina|pàginas}}",
+ "file-info": "mannesa de su documentu: $1, casta de MIME: $2",
"file-info-size": "$1 × $2 pixels, mannesa de su file: $3, tipu de MIME: $4",
"file-nohires": "Non si tenent risolutziones prus artas.",
"svg-long-desc": "file in formadu SVG, mannesa nominale $1 × $2 pixel, mannesa de su file: $3",
"show-big-image": "Versione a risolutzione arta",
"imagelisttext": "Innoe sighendi du est una lista de '''$1''' {{PLURAL:$1|file|files}} ordinada $2.",
+ "newimages-legend": "Filtru",
"ilsubmit": "Chirca",
"bydate": "data",
"video-dims": "$1, $2×$3",
"seconds-abbrev": "$1s",
"minutes-abbrev": "$1m",
"hours-abbrev": "$1h",
+ "seconds": "{{PLURAL:$1|$1 segundu|$1 segundos}}",
+ "minutes": "{{PLURAL:$1|$1 minutu|$1 minutos}}",
+ "hours": "{{PLURAL:$1|$1 ora|$1 oras}}",
+ "days": "{{PLURAL:$1|$1 die|$1 dies}}",
+ "weeks": "{{PLURAL:$1|$1 chida|$1 chidas}}",
+ "months": "{{PLURAL:$1|$1 mese|$1 meses}}",
+ "years": "{{PLURAL:$1|$1 annu|$1 annos}}",
+ "ago": "como $1",
+ "just-now": "immoe-immoe",
+ "hours-ago": "como $1 {{PLURAL:$1|ora|oras}}",
+ "minutes-ago": "como $1 {{PLURAL:$1|minutu|minutos}}",
+ "seconds-ago": "como $1 {{PLURAL:$1|segundu|segundos}}",
+ "monday-at": "Lunis a is $1",
+ "tuesday-at": "Martis a is $1",
+ "wednesday-at": "Mèrcuris a is $1",
+ "thursday-at": "Gioja a is $1",
+ "friday-at": "Chenàbura a is $1",
+ "saturday-at": "Sàbadu a is $1",
+ "sunday-at": "Domìnigu a is $1",
+ "yesterday-at": "Eris a is $1",
"bad_image_list": "Su formadu est su chi sighit:\n\nBenint consideradas isceti is listas punnadas (lìnias chi incumentzant cun *).\nSu primu ligòngiu in cada una lìnia depet èssere unu ligòngiu a unu documentu malu (o indesideradu).\nIs ligòngios chi sighint in sa matessi lìnia sunt cunsiderados comente etzetziones (est a nàrrere, pàginas in ue si podet usare su documentu).",
"metadata": "Metadatos",
"metadata-help": "Custu file cuntènnit informatziones annuntiles, probabilmente annúnghedas dae sa fotocamera o dae su scannerizadore impreadu pro ddu creare o ddu digitalizare. Si su file est istadu mudadu, unos cantos particulares podent non currispòndere a sa realidade.",
"exif-fnumber-format": "f/$1",
"exif-flash": "Flash",
"exif-focallength-format": "$1 mm",
+ "exif-contrast": "Cuntrastu",
+ "exif-saturation": "Saturassione",
+ "exif-gpslatituderef": "Latitùdine nord o sud",
+ "exif-gpslatitude": "Latitùdine",
+ "exif-gpslongituderef": "Longitùdine est o ovest",
+ "exif-gpslongitude": "Longitùdine",
+ "exif-source": "Orìgine",
+ "exif-languagecode": "Limba",
+ "exif-iimcategory": "Categoria",
"exif-compression-6": "JPEG",
"exif-photometricinterpretation-2": "RGB",
"exif-photometricinterpretation-6": "YCbCr",
"exif-componentsconfiguration-1": "Y",
"exif-componentsconfiguration-2": "Cb",
"exif-componentsconfiguration-3": "Cr",
+ "exif-exposureprogram-1": "Ghia",
"exif-subjectdistance-value": "$1 metros",
+ "exif-meteringmode-255": "Àteru",
+ "exif-lightsource-0": "Isconnottu",
"exif-lightsource-4": "Lampu",
"exif-gaincontrol-0": "Nudda",
"exif-contrast-0": "Normale",
+ "exif-saturation-0": "Normale",
"exif-sharpness-0": "Normale",
"watchlistall2": "totu",
"namespacesall": "totu",
"monthsall": "totu",
"confirmemail": "Cunfirma s'indiritzu e-mail",
"confirm_purge_button": "OK",
+ "confirm-watch-button": "OK",
+ "confirm-unwatch-button": "OK",
"semicolon-separator": "; ",
"comma-separator": ", ",
"colon-separator": ": ",
"version-version": "(Versione $1)",
"version-license": "Licèntzia MediaWiki",
"version-ext-license": "Licèntzia",
+ "version-ext-colheader-version": "Versione",
"version-ext-colheader-license": "Licèntzia",
+ "version-ext-colheader-description": "Descritzione",
+ "version-ext-colheader-credits": "Autores",
+ "version-license-title": "Litzèntzia pro $1",
"version-poweredby-others": "àteros",
"version-software-version": "Versione",
"version-entrypoints-header-url": "URL",
"specialpages-group-login": "Intra / crea contu",
"specialpages-group-pages": "Listas de is pàginas",
"tag-filter": "Filtra pro [[Special:Tags|etichetta]]:",
+ "tag-filter-submit": "Filtru",
"tags-active-yes": "Eja",
"tags-active-no": "No",
"tags-edit": "càmbia",
+ "tags-hitcount": "$1 {{PLURAL:$1|càmbiu|càmbios}}",
"compare-page1": "Pàgina 1",
"compare-page2": "Pàgina 2",
"compare-rev1": "Revisione 1",
"logentry-upload-overwrite": "$1 {{GENDER:$2|carrigadu}} una versione noa de $3",
"logentry-upload-revert": "$1 {{GENDER:$2|carrigadu}} $3",
"rightsnone": "(nisciunu)",
+ "feedback-subject": "Ogetu:",
+ "feedback-message": "Messàgiu:",
+ "feedback-cancel": "Annudda",
"feedback-close": "Fatu",
"searchsuggest-search": "Chirca",
"expand_templates_ok": "OK",
"expand_templates_preview": "Antiprima",
"pagelang-name": "Pàgina",
+ "pagelang-language": "Limba",
"pagelang-select-lang": "Sèbera limba"
}
"searchrelated": "currilati",
"searchall": "tutti",
"showingresults": "Ammustra nzinu a {{PLURAL:$1|'''1''' risurtatu|'''$1''' risurtati}} a pàrtiri dô nùmmuru '''$2'''.",
- "showingresultsheader": "{{PLURAL:$5|Risultatu '''$1''' di '''$3'''|Risultati '''$1 - $2''' di '''$3'''}} pi '''$4'''",
"search-nonefound": "La circata nun desi nuddu risurtatu.",
"powersearch-legend": "Ricerca avanzata",
"powersearch-ns": "Cerca ntê namespace:",
"logentry-delete-delete": "$1 cancillau la pàggina $3",
"revdelete-restricted": "ristrizzioni ai suli amministratura attivate",
"revdelete-unrestricted": "ristrizzioni pi suli amministraturi rimossi",
- "logentry-move-move": "$1 spustau la pàggina $3 a $4",
+ "logentry-move-move": "$1 {{GENDER:$2|spustau}} la pàggina $3 nti $4",
+ "logentry-move-move_redir": "$1 {{GENDER:$2|spustau}} la pàggina $3 nti $4 cu nu rinnirizzamentu",
"logentry-newusers-create": "$1 criau na utenza",
"rightsnone": "(nuddu)",
"searchsuggest-search": "Risciduta",
"watchlistedit-clear-submit": "Испразни списак надгледања (Ово је трајно!)",
"watchlistedit-clear-done": "Ваш списак надгледања је испражњен.",
"watchlistedit-clear-removed": "{{PLURAL:$1|1 наслов је уклоњен|$1 наслова је уклоњено}}:",
+ "watchlistedit-too-many": "Има превише страница за приказ овде.",
"watchlisttools-clear": "испразни списак надгледања",
"watchlisttools-view": "прикажи сродне измене",
"watchlisttools-edit": "прикажи и уреди списак надгледања",
"editfont-style": "Typsnitt i redigeringsrutan:",
"editfont-default": "Webbläsarens standard",
"editfont-monospace": "Fast bredd",
- "editfont-sansserif": "Sans-serif",
- "editfont-serif": "Serif",
+ "editfont-sansserif": "Sans-serif-teckensnitt",
+ "editfont-serif": "Serif-teckensnitt",
"sunday": "söndag",
"monday": "måndag",
"tuesday": "tisdag",
"searchall": "אלץ",
"showingresults": "ווייזן ביז {{PLURAL:$1|רעזולטאט '''איינס'''|'''$1''' רעזולטאטן}} אנגעפאנגן פון נומער #'''$2''':",
"showingresultsinrange": "ווײַזן אונטן ביז {{PLURAL:$1|<strong>1</strong> רעזולטאט|<strong>$1</strong> רעזולטאטן}} אין גרייך #<strong>$2</strong> ביז #<strong>$3</strong>.",
- "showingresultsheader": "{{PLURAL:$5|רעזולטאַט '''$1''' פֿון '''$3'''| רעזולטאַטן '''$1 - $2''' פֿון '''$3'''}} פֿאַר '''$4'''",
"search-nonefound": "נישטא קיין רעזולטאטן פֿאַר דער שאלה.",
"powersearch-legend": "ווײַטהאלטן זוכן",
"powersearch-ns": "זוכן אין נאמענטיילן:",
"movepagetalktext": "דער רעדן בלאט וועט ווערן באַוועגט אויטאמאֵטיש מיט אים, '''אחוץ:'''\n* ס'איז שוין דא א נישט-ליידיגער בלאט מיטן נייעם נאמען, אדער.\n* איר נעמט אראפ דעם צייכן פונעם קעסטל אונטן.\n\nאין די פֿעלער, וועט איר דארפֿן באַוועגן אדער צונויפֿגיסן דעם בלאט האַנטלעך, ווען איר ווילט.",
"movearticle": "באוועג בלאט:",
"moveuserpage-warning": "'''ווארענונג:''' איר האלט ביי באוועגן א באניצער בלאט. ביטע באמערקט אז נאר דער בלאט ווערט באוועגט אבער דער באניצער נאמען ווערט ''נישט'' געענדערט.",
+ "movecategorypage-warning": "<strong>ווארענונג:</strong> איר האלט ביי באוועגן א קאטעגאריע בלאט. גיט אכט אז נאר דער בלאט וועט ווערן באוועגט, אבער די בלעטער אין דער אלטער קאטעגאריע וועט מען <em>נישט</em> be ארײַנשטעלן אין דער נייער קאטעגאריע.",
"movenologintext": "איר דארפֿט זיך אײַנשרײַבן און זײַן [[Special:UserLogin|אַרײַנלאגירט]] צו באַוועגן א בלאַט.",
"movenotallowed": "איר זענט נישט דערלויבט צו באוועגן בלעטער.",
"movenotallowedfile": "איר האט נישט קיין רשות צו באוועגן טעקעס.",
"october-date": "10月$1日",
"november-date": "11月$1日",
"december-date": "12月$1日",
- "pagecategories": "{{PLURAL:$1|分类|$1个分类}}",
+ "pagecategories": "{{PLURAL:$1|分类}}",
"category_header": "分类“$1”中的页面",
"subcategories": "子分类",
"category-media-header": "分类“$1”中的媒体文件",
"hidetoc": "隐藏",
"collapsible-collapse": "折叠",
"collapsible-expand": "展开",
- "confirmable-confirm": "{{GENDER:$1|你}}确定么?",
+ "confirmable-confirm": "{{GENDER:$1|您}}确定么?",
"confirmable-yes": "是",
"confirmable-no": "否",
"thisisdeleted": "查看或还原$1?",
"throttled-mailpassword": "密碼重設的電子郵件已經在最近 $1 小時內寄出。\n為防止濫用,$1 小時內只能寄出一次密碼重設信件。",
"mailerror": "傳送電子郵件錯誤:$1",
"acct_creation_throttle_hit": "使用您目前的 IP 位址的訪客在最近一天建立了 {{PLURAL:$1|1 個帳號|$1 個帳號}},已超出系統允許的上限。\n因此,目前無法讓使用此 IP 位址的訪客建立帳號。",
- "emailauthenticated": "您的電子郵件位址已確認於 $2 的 $3。",
+ "emailauthenticated": "您的電子郵件位址已於 $2 $3 確認。",
"emailnotauthenticated": "您的電子郵件位址尚未確認,\n尚不會寄出以下功能的電子郵件給您。",
"noemailprefs": "在您的偏好設定中設定電子郵件位址,讓您可以使用這些功能。",
"emailconfirmlink": "確認您的電子郵件位址",
* basis if needed.
*/
$imageFiles = array(
- 'button-bold' => 'button_bold.png',
- 'button-italic' => 'button_italic.png',
- 'button-link' => 'button_link.png',
- 'button-extlink' => 'button_extlink.png',
- 'button-headline' => 'button_headline.png',
- 'button-image' => 'button_image.png',
- 'button-media' => 'button_media.png',
- 'button-nowiki' => 'button_nowiki.png',
- 'button-sig' => 'button_sig.png',
- 'button-hr' => 'button_hr.png',
+ 'button-bold' => 'en/button_bold.png',
+ 'button-italic' => 'en/button_italic.png',
+ 'button-link' => 'en/button_link.png',
+ 'button-extlink' => 'en/button_extlink.png',
+ 'button-headline' => 'en/button_headline.png',
+ 'button-image' => 'en/button_image.png',
+ 'button-media' => 'en/button_media.png',
+ 'button-nowiki' => 'en/button_nowiki.png',
+ 'button-sig' => 'en/button_sig.png',
+ 'button-hr' => 'en/button_hr.png',
);
/**
);
$imageFiles = array(
- 'button-italic' => 'ksh/button_S_italic.png',
+ 'button-italic' => 'ksh/button_italic.png',
);
$linkPrefixExtension = false;
$imageFiles = array(
- 'button-bold' => 'cyrl/button_bold.png',
- 'button-italic' => 'cyrl/button_italic.png',
- 'button-link' => 'cyrl/button_link.png',
+ 'button-bold' => 'ru/button_bold.png',
+ 'button-italic' => 'ru/button_italic.png',
+ 'button-link' => 'ru/button_link.png',
);
$linkTrail = '/^([a-zабвгдеёжзийклмнопрстуфхцчшщъыьэюя]+)(.*)$/sDu';
bmysql
bname
bodycontent
+bogo
boldening
bolding
booksources
categoryviewer
catids
catlinks
+catmsg
catpage
catrope
cattitles
geodata
geosearch
gerrit
+geshi
getcookie
getenv
getheader
hitcount
hitcounter
hits
+hlist
hmac
hobby
homelink
loginfo
loginlanguagelinks
loginlink
+loginout
loginprompt
loginreqlink
loginreqpagetext
moredotdotdot
morelinkstoimage
morethan
+mouseup
move
movedarticleprotection
moveddeleted
newquery
newrevid
news
+newsectionheaderdefaultlevel
newsectionlink
newsectionsummary
newset
testclean
testdata
testmailuser
+teston
testpass
testrunner
testswarm
troff
true
truespeed
+truncatedtext
trustworthy
truteq
truthy
unserialization
unserialize
unserialized
+unserializes
unserializing
unsetting
unstub
viewhelppage
viewmyprivateinfo
viewmywatchlist
+viewport
viewprevnext
viewsource
viewsourcelink
xmldoublequote
xmlfm
xmlimport
+xmlmeta
xmlns
xmlselect
xor
* Important tertiary weights from UTS #10 section 7.2
*/
const NORMAL_UPPERCASE = 0x08;
- const NORMAL_HIRAGANA = 0X0E;
+ const NORMAL_HIRAGANA = 0x0E;
public function __construct() {
parent::__construct();
'mediawiki.action.edit' => array(
'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.js',
+ 'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.css',
'dependencies' => array(
'mediawiki.action.edit.styles',
+ 'mediawiki.action.edit.toolbar',
'jquery.textSelection',
'jquery.byteLimit',
),
'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.styles.css',
'position' => 'top',
),
+ 'mediawiki.action.edit.toolbar' => array(
+ 'class' => 'ResourceLoaderEditToolbarModule',
+ 'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.toolbar/mediawiki.action.edit.toolbar.less',
+ ),
'mediawiki.action.edit.collapsibleFooter' => array(
'scripts' => 'resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.js',
'styles' => 'resources/src/mediawiki.action/mediawiki.action.edit.collapsibleFooter.css',
--- /dev/null
+/*!
+ * Styles for elements of the editing form, loaded only when JavaScript is enabled.
+ */
+
+.mw-toolbar-editbutton {
+ width: 23px;
+ height: 22px;
+ cursor: pointer;
+ vertical-align: middle;
+ /* Cross-browser inline-block */
+ /* Firefox 2 */
+ display: -moz-inline-block;
+ /* Modern browsers */
+ display: inline-block;
+ /* IE7 */
+ zoom: 1;
+ *display: inline;
+}
* @private
*/
function insertButton( b, speedTip, tagOpen, tagClose, sampleText, imageId ) {
+ var $button;
+
// Backwards compatibility
if ( typeof b !== 'object' ) {
b = {
imageId: imageId
};
}
- var $image = $( '<img>' ).attr( {
- width: 23,
- height: 22,
+
+ if ( b.imageFile ) {
+ $button = $( '<img>' ).attr( {
src: b.imageFile,
alt: b.speedTip,
title: b.speedTip,
id: b.imageId || undefined,
'class': 'mw-toolbar-editbutton'
- } ).click( function ( e ) {
+ } );
+ } else {
+ $button = $( '<div>' ).attr( {
+ title: b.speedTip,
+ id: b.imageId || undefined,
+ 'class': 'mw-toolbar-editbutton'
+ } );
+ }
+
+ $button.click( function ( e ) {
if ( b.onClick !== undefined ) {
b.onClick( e );
} else {
return false;
} );
- $toolbar.append( $image );
+ $toolbar.append( $button );
}
isReady = false;
* @param {Object} button Object with the following properties.
* You are required to provide *either* the `onClick` parameter, or the three parameters
* `tagOpen`, `tagClose` and `sampleText`, but not both (they're mutually exclusive).
- * @param {string} button.imageFile Image to use for the button.
+ * @param {string} [button.imageFile] Image to use for the button.
* @param {string} button.speedTip Tooltip displayed when user mouses over the button.
* @param {Function} [button.onClick] Function to be executed when the button is clicked.
* @param {string} [button.tagOpen]
* @param {string} [button.sampleText] Alternative to `onClick`. `tagOpen`, `tagClose` and
* `sampleText` together provide the markup that should be inserted into page text at
* current cursor position.
- * @param {string} [button.imageId] `id` attribute of the button HTML element.
+ * @param {string} [button.imageId] `id` attribute of the button HTML element. Can be
+ * used to define the image with CSS if it's not provided as `imageFile`.
*/
addButton: function () {
if ( isReady ) {
--- /dev/null
+
+button_italic.png
+-------------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_S_italic.png
+License: Public domain
+Author : Purodha Blissenbach, http://ksh.wikipedia.org/wiki/User:Purodha
+
--- /dev/null
+button_bold.png
+---------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_bold_ukr.png
+License: Public domain
+Author : Alexey Belomoev
+
+button_italic.png
+------------------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_italic_ukr.png
+License: Public domain
+Author : Alexey Belomoev
+
+button_link.png
+-----------------
+Source : http://commons.wikimedia.org/wiki/Image:Button_internal_link_ukr.png
+License: GPL
+Author : Saproj, Erik Möller
--- /dev/null
+@import "mediawiki.mixins";
+
+#mw-editbutton-bold {
+ .background-image("images/@{button-bold}");
+}
+
+#mw-editbutton-italic {
+ .background-image("images/@{button-italic}");
+}
+
+#mw-editbutton-link {
+ .background-image("images/@{button-link}");
+}
+
+#mw-editbutton-extlink {
+ .background-image("images/@{button-extlink}");
+}
+
+#mw-editbutton-headline {
+ .background-image("images/@{button-headline}");
+}
+
+#mw-editbutton-image {
+ .background-image("images/@{button-image}");
+}
+
+#mw-editbutton-media {
+ .background-image("images/@{button-media}");
+}
+
+#mw-editbutton-nowiki {
+ .background-image("images/@{button-nowiki}");
+}
+
+// Who decided to make only this single one different than the name of the data item?
+#mw-editbutton-signature {
+ .background-image("images/@{button-sig}");
+}
+
+#mw-editbutton-hr {
+ .background-image("images/@{button-hr}");
+}
clear: both;
}
-#toolbar img {
- cursor: pointer;
-}
-
/**
* File description page
*/
@import "mediawiki.ui/variables";
@import "mediawiki.ui/mixins";
-// Buttons
-//
-// All buttons start with mw-ui-button class, modified by other classes.
-// It can be any element. Due to a lack of a CSS reset, the exact styling of
-// the button depends on what type of element is used.
-// There are two kinds of buttons, the default is a "Call to Action" with an obvious border
-// and there is a quiet kind without a border.
-//
-// Styleguide 2.
-
-@buttonBorderRadius: 3px;
-@transitionDuration: .1s;
-@transitionFunction: ease-in-out;
-
-// Neutral button styling
-//
-// Markup:
-// <button class="mw-ui-button">.mw-ui-button</button>
-// <button class="mw-ui-button" disabled>.mw-ui-button</button>
-//
-// Styleguide 2.1.
-.mw-ui-button {
- // Inherit the font rather than apply user agent stylesheet (bug 70072)
- font-family: inherit;
- font-size: 1em;
+/*
+Buttons
+
+<h3>Guidelines:</h3>
+
+- .mw-ui-button can **only** be used on **A, INPUT, and BUTTON tags**. There is support for some input types, but this doesn't work in older browsers.
+- .mw-ui-progressive, .mw-ui-constructive, and .mw-ui-destructive can be applied alone on A (see Styleguide 4.0), but can be applied in tandem with .mw-ui-button. *The class order is important:* **base type** (mw-ui-button) must come **first**, **mode** (mw-ui-quiet) **second**, and **context** (mw-ui-progressive) comes **last**.
+- A .mw-ui-quiet button may **never** be the first or only button in a form.
+- Semantically, the **first button in a form should always be the affirmative action** (eg. Submit). This is for accessibility purposes. Where it appears visually is not as important.
+
+<h3>Notes:</h3>
+
+- IE6 does not apply any .mw-ui-button styles at all on BUTTON.
+- IE6 only applies the base .mw-ui-CONTEXT color on A, and doesn't care if you are combining it (ie. .mw-ui-destructive.mw-ui-quiet = always red text).
+- IE7 and IE8 look slightly different from other browsers when rendering certain modes of these buttons.
+
+Markup:
+<button class="mw-ui-button {$modifiers}">Default</button>
+<button class="mw-ui-button mw-ui-progressive {$modifiers}">Progressive</button>
+<button class="mw-ui-button mw-ui-constructive {$modifiers}">Constructive</button>
+<button class="mw-ui-button mw-ui-destructive {$modifiers}">Destructive</button>
+<button class="mw-ui-button mw-ui-progressive {$modifiers}" disabled>Disabled Progressive</button>
+
+.mw-ui-quiet - Quiet: A button that doesn't look like a button.
+.mw-ui-inline - Inline: An even smaller button (zero padding) which also inherits font weight.
+.mw-ui-big - Big: 1.3x font-size.
+
+Styleguide 2.
+*/
+
+// Helpers
+// Individual Button Contexts
+.mixin-mw-ui-button-context( @contextualColor ) {
+ @textShadowColor: spin( @colorTextLight, 180 );
+ @borderColor: mix( @contextualColor, #000, 75% );
+ @raisedColor: mix( @contextualColor, #fff, 92% );
+ @depressedColor: darken( @contextualColor, 8% );
+ @quietDepressedColor: darken( @contextualColor, 25% );
+
+ .mixin-mw-ui-button-disabled-state() {
+ &[disabled],
+ &[disabled]:hover,
+ &[disabled]:focus {
+ background: @colorGrayLight;
+ color: @colorWhite;
+ text-shadow: none;
+ .box-shadow( ~"none" );
+ }
+ }
+
+ .mixin-mw-ui-button-normal-mode() {
+ background: @contextualColor;
+ color: @colorWhite;
+ text-shadow: 0 1px fade( @textShadowColor, 10% );
+
+ .mixin-mw-ui-button-disabled-state();
+
+ &:hover,
+ &:focus {
+ background: @raisedColor;
+ text-shadow: 0 1px fade( @textShadowColor, 33% );
+ }
+
+ &:hover {
+ // Shadow under outer, 3D raising inner, edge shading inner
+ .box-shadow( ~"0 1px 0 0 rgba(0, 0, 0, .15), inset 0 -4px 0 0 @{borderColor}, inset 0 -1px 1px 0 rgba(0, 0, 0, .05)" );
+ }
+
+ &:focus {
+ // 3D raising inner, edge shading inner
+ .box-shadow( ~"inset 0 -4px 0 0 @{borderColor}, inset 0 -1px 1px 0 rgba(0, 0, 0, .05), inset 0 0 0 1px @{borderColor}" );
+ }
+
+ &:active {
+ background: @depressedColor;
+ // Slight 3D raising inner, deep edge shading inner
+ .box-shadow( ~"inset 0 -2px 0 0 @{depressedColor}, inset 0 2px 0 0 rgba(0, 0, 0, .25)" );
+ }
+ }
+
+ // Default mode (fully colored)
+ &:not(.mw-ui-quiet) {
+ .mixin-mw-ui-button-normal-mode();
+ }
+ .lte-ie8 & { // IE7 & IE8 do not support :not() selector
+ .mixin-mw-ui-button-normal-mode();
+ }
+
+ // Quiet mode (transparent bg, no border; text color on activity)
+ .lte-ie8 &.mw-ui-quiet,
+ &.mw-ui-quiet {
+ background: transparent;
+ color: @colorTextLight;
+
+ &:hover {
+ color: @contextualColor;
+ }
+
+ &:active {
+ color: @depressedColor;
+ }
+
+ &:focus {
+ color: @quietDepressedColor;
+ }
+
+ .mixin-mw-ui-button-disabled-state();
+ }
+}
+
+// Default button styles
+.mixin-mw-ui-button-default() {
+ background: @colorGrayLightest;
+ color: @colorTextLight;
+ @textShadowColor: spin( @colorTextLight, 180 );
+ @borderColor: mix( @colorGrayLightest, #000, 75% );
+ @raisedColor: mix( @colorGrayLightest, #fff, 92% );
+ @depressedColor: darken( @colorGrayLightest, 8% );
+ @quietDepressedColor: darken( @colorGrayLightest, 25% );
+
+ .mixin-mw-ui-button-normal-mode() {
+ &:hover,
+ &:focus {
+ background: @raisedColor;
+ text-shadow: 0 1px fade( @textShadowColor, 33% );
+ }
+
+ &:hover {
+ // Shadow under outer, 3D raising inner, edge shading inner
+ .box-shadow( ~"0 1px 0 0 rgba(0, 0, 0, .15), inset 0 -4px 0 0 @{borderColor}, inset 0 -1px 1px 0 rgba(0, 0, 0, .05)" );
+ }
+
+ &:focus {
+ // 3D raising inner, edge shading inner
+ .box-shadow( ~"inset 0 -4px 0 0 @{borderColor}, inset 0 -1px 1px 0 rgba(0, 0, 0, .05), inset 0 0 0 1px @{borderColor}" );
+ }
+
+ &:active {
+ background: @depressedColor;
+ // Slight 3D raising inner, deep edge shading inner
+ .box-shadow( ~"inset 0 -2px 0 0 @{depressedColor}, inset 0 2px 0 0 rgba(0, 0, 0, .25)" );
+ }
+ }
+
+ // Default mode (fully colored)
+ &:not(.mw-ui-quiet) {
+ .mixin-mw-ui-button-normal-mode();
+ }
+ .lte-ie8 & { // IE7 & IE8 do not support :not() selector
+ .mixin-mw-ui-button-normal-mode();
+ }
+
+ // Quiet mode (transparent bg, no border; text color on activity)
+ .lte-ie8 &.mw-ui-quiet,
+ &.mw-ui-quiet {
+ background: transparent;
+
+ &:hover,
+ &:focus,
+ &:active {
+ color: @colorText;
+ }
+ }
+}
+
+// Selector mixins, used for customization if needed
+.mixin-mw-ui-button() {
// Container layout
display: inline-block;
padding: .5em 1em;
margin: 0;
+ vertical-align: middle;
.box-sizing(border-box);
- // Disable weird iOS styling
- -webkit-appearance: none;
-
// IE6/IE7 hack
- // http://stackoverflow.com/a/5838575/365238
*display: inline;
zoom: 1;
- // Container styling
- .button-colors(#FFF);
- border-radius: @buttonBorderRadius;
-
- // Ensure that buttons and inputs are nicely aligned when they have differing heights
- vertical-align: middle;
+ // Disable weird iOS styling
+ -webkit-appearance: none;
- // Content styling
- text-align: center;
+ // Typography
+ font-family: inherit;
+ font-size: 1em;
font-weight: bold;
+ line-height: inherit;
+ text-decoration: none;
- // Interaction styling
+ // Design
+ border: 0px solid transparent;
+ border-radius: 3px;
cursor: pointer;
+ // Animation
+ .transition( ~"box-shadow .1s linear, background-color .1s linear, opacity .5s linear" );
+
+ // Disabled state (cursor fix)
&:disabled {
- text-shadow: none;
cursor: default;
}
+ // Focus/active state (outline fix)
+ &:focus, &:active {
+ outline: none;
+ }
- .transition(background @transitionDuration @transitionFunction, color @transitionDuration @transitionFunction, box-shadow @transitionDuration @transitionFunction;);
+ /*
+ * Button modes (continued in .mw-ui-button-context())
+ */
- // Styling for specific button types
- // -----------------------------------------
+ // Thin mode (no padding)
+ &.mw-ui-inline {
+ padding: 0;
+ font-weight: inherit;
+ vertical-align: inherit;
+ }
- // Big buttons
- //
- // Not all buttons are equal. You can emphasise certain actions over others
- // using the mw-ui-big class.
- //
- // Markup:
- // <button class="mw-ui-button mw-ui-big">.mw-ui-button</button>
- // <button class="mw-ui-button mw-ui-progressive mw-ui-big">.mw-ui-progressive</button>
- // <button class="mw-ui-button mw-ui-constructive mw-ui-big">.mw-ui-constructive</button>
- // <button class="mw-ui-button mw-ui-destructive mw-ui-big">.mw-ui-destructive</button>
- //
- // Styleguide 2.1.6.
+ // Big mode (1.3x font size)
&.mw-ui-big {
font-size: 1.3em;
}
- // Block buttons
- //
- // Some buttons might need to be stacked.
- //
- // Markup:
- // <button class="mw-ui-button mw-ui-block">.mw-ui-button</button>
- // <button class="mw-ui-button mw-ui-progressive mw-ui-block">.mw-ui-progressive</button>
- // <button class="mw-ui-button mw-ui-constructive mw-ui-block">.mw-ui-constructive</button>
- // <button class="mw-ui-button mw-ui-destructive mw-ui-block">.mw-ui-destructive</button>
- //
- // Styleguide 2.1.5.
- &.mw-ui-block {
- display: block;
- width: 100%;
- }
-
- // Progressive buttons
- //
- // Use progressive buttons for actions which lead to a next step in the process.
- // .mw-ui-primary is deprecated, kept for compatibility.
- //
- // Markup:
- // <button class="mw-ui-button mw-ui-progressive">.mw-ui-progressive</button>
- // <button class="mw-ui-button mw-ui-progressive" disabled>.mw-ui-progressive</button>
- //
- // Styleguide 2.1.1.
- &.mw-ui-progressive,
- &.mw-ui-primary {
- .button-colors(@colorProgressive);
-
- &.mw-ui-quiet {
- .button-colors-quiet(@colorProgressive);
- }
+ /*
+ * Default button styles
+ */
+
+ .mixin-mw-ui-button-default();
+
+ /*
+ * Contextual classes
+ */
+
+ // Progressive context
+ &.mw-ui-progressive {
+ .mixin-mw-ui-button-context( @colorProgressive );
}
- // Constructive buttons
- //
- // Use constructive buttons for actions which result in a final action in the process that results
- // in a change of state.
- // e.g. save changes button
- //
- // Markup:
- // <button class="mw-ui-button mw-ui-constructive">.mw-ui-constructive</button>
- // <button class="mw-ui-button mw-ui-constructive" disabled>.mw-ui-constructive</button>
- //
- // Styleguide 2.1.2.
+ // Constructive context
&.mw-ui-constructive {
- .button-colors(@colorConstructive);
-
- &.mw-ui-quiet {
- .button-colors-quiet(@colorConstructive);
- }
+ .mixin-mw-ui-button-context( @colorConstructive );
}
- // Destructive buttons
- //
- // Use destructive buttons for actions which result in the destruction of data.
- // e.g. deleting a page.
- // This should not be used for cancel buttons.
- //
- // Markup:
- // <button class="mw-ui-button mw-ui-destructive">.mw-ui-destructive</button>
- // <button class="mw-ui-button mw-ui-destructive" disabled>.mw-ui-destructive</button>
- //
- // Styleguide 2.1.3.
+ // Destructive context
&.mw-ui-destructive {
- .button-colors(@colorDestructive);
-
- &.mw-ui-quiet {
- .button-colors-quiet(@colorDestructive);
- }
+ .mixin-mw-ui-button-context( @colorDestructive );
}
+}
- // Quiet buttons
- //
- // Use quiet buttons when they are less important and alongisde other progressive/destructive/progressive buttons.
- //
- // Markup:
- // <button class="mw-ui-button mw-ui-quiet">.mw-ui-button</button>
- // <button class="mw-ui-button mw-ui-constructive mw-ui-quiet">.mw-ui-constructive</button>
- // <button class="mw-ui-button mw-ui-constructive mw-ui-quiet" disabled>.mw-ui-constructive</button>
- // <button class="mw-ui-button mw-ui-destructive mw-ui-quiet">.mw-ui-destructive</button>
- // <button class="mw-ui-button mw-ui-destructive mw-ui-quiet" disabled>.mw-ui-destructive</button>
- // <button class="mw-ui-button mw-ui-progressive mw-ui-quiet">.mw-ui-progressive</button>
- // <button class="mw-ui-button mw-ui-progressive mw-ui-quiet" disabled>.mw-ui-progressive</button>
- //
- // Styleguide 2.1.4.
- &.mw-ui-quiet {
- background: transparent;
- border: none;
- text-shadow: none;
- .button-colors-quiet(@colorButtonText);
+// Button selectors
+.mw-ui-button {
+ .mixin-mw-ui-button;
- &:hover,
- &:focus {
- box-shadow: none;
- }
+ // Default mw-ui-button implementation forces min dimensions for improved touch access
+ min-width: 48px;
+ min-height: 33px;
- &:active,
- &:disabled {
- background: transparent;
- }
+ // When these buttons are children of mw-ui-button-group, adjust accordingly
+ .mw-ui-button-group > & {
+ .mw-ui-button-group-child;
}
}
-a.mw-ui-button {
- text-decoration: none;
+/*
+Button groups
+
+Group of buttons.
+
+Markup:
+<div class="mw-ui-button-group">
+ <a class="mw-ui-button" href=javascript:void(0)>A</a>
+ <a class="mw-ui-button" href=javascript:void(0)>B</a>
+ <a class="mw-ui-button" href=javascript:void(0)>C</a>
+ <a class="mw-ui-button" href=javascript:void(0)>D</a>
+</div>
- // This overrides an underline declaration on a:hover and a:focus in
- // commonElements.css, which the class alone isn't specific enough to do.
- &:hover,
- &:focus {
- text-decoration: none;
+Styleguide 2.1.
+*/
+.mw-ui-button-group {
+ // Clearfix
+ zoom: 1;
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
}
}
-// Button groups
-//
-// Group of buttons. Make sure you clear the floating after using a mw-ui-button-group.
-//
-// Markup:
-// <div class="mw-ui-button-group">
-// <div class="mw-ui-button">A</div>
-// <div class="mw-ui-button">B</div>
-// <div class="mw-ui-button">C</div>
-// <div class="mw-ui-button">D</div>
-// </div><div style="clear:both"></div>
-//
-// Styleguide 2.2.
-.mw-ui-button-group > * {
+// To be used within .mw-ui-button selector
+.mw-ui-button-group-child() {
border-radius: 0;
float: left;
&:first-child {
- border-top-left-radius: @buttonBorderRadius;
- border-bottom-left-radius: @buttonBorderRadius;
- }
-
- &:not(:first-child) {
- border-left: none;
+ border-top-left-radius: 3px;
+ border-bottom-left-radius: 3px;
}
- &:last-child{
- border-top-right-radius: @buttonBorderRadius;
- border-bottom-right-radius: @buttonBorderRadius;
+ &:last-child {
+ border-top-right-radius: 3px;
+ border-bottom-right-radius: 3px;
}
}
+++ /dev/null
-button_bold.png
----------------
-Source : http://commons.wikimedia.org/wiki/Image:Button_bold_ukr.png
-License: Public domain
-Author : Alexey Belomoev
-
-button_italic.png
-------------------------
-Source : http://commons.wikimedia.org/wiki/Image:Button_italic_ukr.png
-License: Public domain
-Author : Alexey Belomoev
-
-button_link.png
------------------
-Source : http://commons.wikimedia.org/wiki/Image:Button_internal_link_ukr.png
-License: GPL
-Author : Saproj, Erik Möller
+++ /dev/null
-
-button_S_italic.png
--------------------
-Source : http://commons.wikimedia.org/wiki/Image:Button_S_italic.png
-License: Public domain
-Author : Purodha Blissenbach, http://ksh.wikipedia.org/wiki/User:Purodha
-
* @see https://github.com/sebastianbergmann/phpunit/blob/master/src/Extensions/PhptTestCase.php
* @author Sam Smith <samsmith@wikimedia.org>
*/
-class LessFileCompilationTest extends MediaWikiTestCase {
+class LessFileCompilationTest extends ResourceLoaderTestCase {
/**
* @var string $file
"$thisString must refer to a readable file"
);
- $compiler = ResourceLoader::getLessCompiler( RequestContext::getMain()->getConfig() );
+ $rlContext = static::getResourceLoaderContext();
+
+ // Bleh
+ $method = new ReflectionMethod( $this->module, 'getLessCompiler' );
+ $method->setAccessible( true );
+ $compiler = $method->invoke( $this->module, $rlContext );
+
$this->assertNotNull( $compiler->compileFile( $this->file ) );
}
* @dataProvider provideFiles
*/
public function testMetadata( $name, $type, $info ) {
- if ( !BitmapHandler::canRotate() ) {
+ if ( !$this->handler->canRotate() ) {
$this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." );
}
$file = $this->dataFile( $name, $type );
$this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
}
+ /**
+ * Same as before, but with auto-rotation set to auto.
+ *
+ * This sets scaler to image magick, which we should detect as
+ * supporting rotation.
+ * @dataProvider provideFiles
+ */
+ public function testMetadataAutoRotate( $name, $type, $info ) {
+ $this->setMwGlobals( 'wgEnableAutoRotation', null );
+ $this->setMwGlobals( 'wgUseImageMagick', true );
+ $this->setMwGlobals( 'wgUseImageResize', true );
+
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
/**
*
* @dataProvider provideFiles
*/
public function testRotationRendering( $name, $type, $info, $thumbs ) {
- if ( !BitmapHandler::canRotate() ) {
+ if ( !$this->handler->canRotate() ) {
$this->markTestSkipped( "This test needs a rasterizer that can auto-rotate." );
}
foreach ( $thumbs as $size => $out ) {
$this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
}
+ /**
+ * Same as before, but with auto-rotation set to auto and an image scaler that doesn't support it.
+ * @dataProvider provideFilesNoAutoRotate
+ */
+ public function testMetadataAutoRotateUnsupported( $name, $type, $info ) {
+ $this->setMwGlobals( 'wgEnableAutoRotation', null );
+ $this->setMwGlobals( 'wgUseImageResize', false );
+
+ $file = $this->dataFile( $name, $type );
+ $this->assertEquals( $info['width'], $file->getWidth(), "$name: width check" );
+ $this->assertEquals( $info['height'], $file->getHeight(), "$name: height check" );
+ }
+
/**
*
* @dataProvider provideFilesNoAutoRotate
. implode( ' ', $filesToTest ) );
$suite = new PHPUnit_Framework_TestSuite;
+ $testList = array();
+ $counter = 0;
foreach ( $filesToTest as $fileName ) {
- $testsName = basename( $fileName, '.txt' );
+ // Call the highest level directory the extension name.
+ // It may or may not actually be, but it should be close
+ // enough to cause there to be separate names for different
+ // things, which is good enough for our purposes.
+ $extensionName = basename( dirname( $fileName ) );
+ $testsName = $extensionName . '⁄' . basename( $fileName, '.txt' );
$escapedFileName = strtr( $fileName, array( "'" => "\\'", '\\' => '\\\\' ) );
- /* This used to be ucfirst( basename( dirname( $filename ) ) )
- * and then was ucfirst( basename( $filename, '.txt' )
- * but that didn't work with names like foo.tests.txt
- */
- $parserTestClassName = str_replace( '.', '_', ucfirst( $testsName ) );
+ $parserTestClassName = ucfirst( $testsName );
+ // Official spec for class names: http://php.net/manual/en/language.oop5.basic.php
+ // Prepend 'ParserTest_' to be paranoid about it not starting with a number
+ $parserTestClassName = 'ParserTest_' . preg_replace( '/[^a-zA-Z0-9_\x7f-\xff]/', '_', $parserTestClassName );
+ if ( isset( $testList[$parserTestClassName] ) ) {
+ // If a conflict happens, gives a very unclear fatal.
+ // So as a last ditch effort to prevent that eventuality, if there
+ // is a conflict, append a number.
+ $counter++;
+ $parserTestClassName .= $counter;
+ }
+ $testList[$parserTestClassName] = true;
$parserTestClassDefinition = <<<EOT
/**
* @group Database
),
), $out->getSections(), 'getSections() with proper value when <h2> is used' );
}
+
+ /**
+ * @dataProvider provideNormalizeLinkUrl
+ * @covers Parser::normalizeLinkUrl
+ * @covers Parser::normalizeUrlComponent
+ */
+ public function testNormalizeLinkUrl( $explanation, $url, $expected ) {
+ $this->assertEquals( $expected, Parser::normalizeLinkUrl( $url ), $explanation );
+ }
+
+ public static function provideNormalizeLinkUrl() {
+ return array(
+ array(
+ 'Escaping of unsafe characters',
+ 'http://example.org/foo bar?param[]="value"¶m[]=valüe',
+ 'http://example.org/foo%20bar?param%5B%5D=%22value%22¶m%5B%5D=val%C3%BCe',
+ ),
+ array(
+ 'Case normalization of percent-encoded characters',
+ 'http://example.org/%ab%cD%Ef%FF',
+ 'http://example.org/%AB%CD%EF%FF',
+ ),
+ array(
+ 'Unescaping of safe characters',
+ 'http://example.org/%3C%66%6f%6F%3E?%3C%66%6f%6F%3E#%3C%66%6f%6F%3E',
+ 'http://example.org/%3Cfoo%3E?%3Cfoo%3E#%3Cfoo%3E',
+ ),
+ array(
+ 'Context-sensitive replacement of sometimes-safe characters',
+ 'http://example.org/%23%2F%3F%26%3D%2B%3B?%23%2F%3F%26%3D%2B%3B#%23%2F%3F%26%3D%2B%3B',
+ 'http://example.org/%23%2F%3F&=+;?%23/?%26%3D%2B%3B#%23/?&=+;',
+ ),
+ );
+ }
+
// @todo Add tests for cleanSig() / cleanSigInSig(), getSection(),
// replaceSection(), getPreloadText()
}