diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2009-06-10 13:00:47 +0200 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2009-06-10 13:00:47 +0200 |
commit | 72e90545454c0e014318fa3c81658e035aac58c1 (patch) | |
tree | 9212e3f46868989c4d57ae9a5c8a1a80e4dc0702 /includes | |
parent | 565a0ccc371ec1a2a0e9b39487cbac18e6f60e25 (diff) |
applying patch to version 1.15.0
Diffstat (limited to 'includes')
231 files changed, 35363 insertions, 17570 deletions
diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 63468a14..26b6f443 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -45,7 +45,7 @@ class AjaxResponse { $this->mText = ''; $this->mResponseCode = '200 OK'; $this->mLastModified = false; - $this->mContentType= 'text/html; charset=utf-8'; + $this->mContentType= 'application/x-wiki'; if ( $text ) { $this->addText( $text ); @@ -178,6 +178,7 @@ class AjaxResponse { wfDebug( "$fname: -- client send If-Modified-Since: " . $modsince . "\n", false ); wfDebug( "$fname: -- we might send Last-Modified : $lastmod\n", false ); if( ($ismodsince >= $timestamp ) && $wgUser->validateCache( $ismodsince ) && $ismodsince >= $wgCacheEpoch ) { + ini_set('zlib.output_compression', 0); $this->setResponseCode( "304 Not Modified" ); $this->disable(); $this->mLastModified = $lastmod; diff --git a/includes/Article.php b/includes/Article.php index 3d9c2147..ef219ea3 100644 --- a/includes/Article.php +++ b/includes/Article.php @@ -84,12 +84,12 @@ class Article { return $this->mRedirectTarget; # Query the redirect table $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'redirect', + $row = $dbr->selectRow( 'redirect', array('rd_namespace', 'rd_title'), - array('rd_from' => $this->getID()), + array('rd_from' => $this->getID() ), __METHOD__ ); - if( $row = $dbr->fetchObject($res) ) { + if( $row ) { return $this->mRedirectTarget = Title::makeTitle($row->rd_namespace, $row->rd_title); } # This page doesn't have an entry in the redirect table @@ -135,7 +135,7 @@ class Article { * @return mixed false, Title of in-wiki target, or string with URL */ public function followRedirectText( $text ) { - $rt = Title::newFromRedirect( $text ); + $rt = Title::newFromRedirectRecurse( $text ); // recurse through to only get the final target # process if title object is valid and not special:userlogout if( $rt ) { if( $rt->getInterwiki() != '' ) { @@ -218,7 +218,7 @@ class Article { if( wfEmptyMsg( $message, $text ) ) $text = ''; } else { - $text = wfMsg( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon' ); + $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' ); } wfProfileOut( __METHOD__ ); return $text; @@ -228,6 +228,21 @@ class Article { return $this->mContent; } } + + /** + * Get the text of the current revision. No side-effects... + * + * @return Return the text of the current revision + */ + public function getRawText() { + // Check process cache for current revision + if( $this->mContentLoaded && $this->mOldId == 0 ) { + return $this->mContent; + } + $rev = Revision::newFromTitle( $this->mTitle ); + $text = $rev ? $rev->getRawText() : false; + return $text; + } /** * This function returns the text of a section, specified by a number ($section). @@ -245,6 +260,28 @@ class Article { global $wgParser; return $wgParser->getSection( $text, $section ); } + + /** + * Get the text that needs to be saved in order to undo all revisions + * between $undo and $undoafter. Revisions must belong to the same page, + * must exist and must not be deleted + * @param $undo Revision + * @param $undoafter Revision Must be an earlier revision than $undo + * @return mixed string on success, false on failure + */ + public function getUndoText( Revision $undo, Revision $undoafter = null ) { + $undo_text = $undo->getText(); + $undoafter_text = $undoafter->getText(); + $cur_text = $this->getContent(); + if ( $cur_text == $undo_text ) { + # No use doing a merge if it's just a straight revert. + return $undoafter_text; + } + $undone_text = ''; + if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) + return false; + return $undone_text; + } /** * @return int The oldid of the article that is to be shown, 0 for the @@ -569,7 +606,7 @@ class Article { } // Apparently loadPageData was never called $this->loadContent(); - $titleObj = Title::newFromRedirect( $this->fetchContent() ); + $titleObj = Title::newFromRedirectRecurse( $this->fetchContent() ); } else { $titleObj = Title::newFromRedirect( $text ); } @@ -660,10 +697,13 @@ class Article { $user = $this->getUser(); $pageId = $this->getId(); + $hideBit = Revision::DELETED_USER; // username hidden? + $sql = "SELECT {$userTable}.*, MAX(rev_timestamp) as timestamp FROM $revTable LEFT JOIN $userTable ON rev_user = user_id WHERE rev_page = $pageId AND rev_user != $user + AND rev_deleted & $hideBit = 0 GROUP BY rev_user, rev_user_text, user_real_name ORDER BY timestamp DESC"; @@ -687,21 +727,28 @@ class Article { global $wgUseTrackbacks, $wgNamespaceRobotPolicies, $wgArticleRobotPolicies; global $wgDefaultRobotPolicy; + # Let the parser know if this is the printable version + if( $wgOut->isPrintable() ) { + $wgOut->parserOptions()->setIsPrintable( true ); + } + wfProfileIn( __METHOD__ ); # Get variables from query string $oldid = $this->getOldID(); - # Try file cache + # Try client and file cache if( $oldid === 0 && $this->checkTouched() ) { global $wgUseETag; if( $wgUseETag ) { $parserCache = ParserCache::singleton(); - $wgOut->setETag( $parserCache->getETag($this,$wgUser) ); + $wgOut->setETag( $parserCache->getETag($this, $wgOut->parserOptions()) ); } + # Is is client cached? if( $wgOut->checkLastModified( $this->getTouched() ) ) { wfProfileOut( __METHOD__ ); return; + # Try file cache } else if( $this->tryFileCache() ) { # tell wgOut that output is taken care of $wgOut->disable(); @@ -743,15 +790,17 @@ class Article { } $wgOut->setRobotPolicy( $policy ); + # Allow admins to see deleted content if explicitly requested + $delId = $diff ? $diff : $oldid; + $unhide = $wgRequest->getInt('unhide') == 1 + && $wgUser->matchEditToken( $wgRequest->getVal('token'), $delId ); # If we got diff and oldid in the query, we want to see a # diff page instead of the article. - if( !is_null( $diff ) ) { $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); - $diff = $wgRequest->getVal( 'diff' ); $htmldiff = $wgRequest->getVal( 'htmldiff' , false); - $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff); + $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $htmldiff, $unhide ); // DifferenceEngine directly fetched the revision: $this->mRevIdFetched = $de->mNewid; $de->showDiffPage( $diffOnly ); @@ -765,6 +814,16 @@ class Article { wfProfileOut( __METHOD__ ); return; } + + if( $ns == NS_USER || $ns == NS_USER_TALK ) { + # User/User_talk subpages are not modified. (bug 11443) + if( !$this->mTitle->isSubpage() ) { + $block = new Block(); + if( $block->load( $this->mTitle->getBaseText() ) ) { + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + } + } + } # Should the parser cache be used? $pcache = $this->useParserCache( $oldid ); @@ -787,6 +846,11 @@ class Article { $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() ); $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" ); } + + // Add a <link rel="canonical"> tag + $wgOut->addLink( array( 'rel' => 'canonical', + 'href' => $this->mTitle->getLocalURL() ) + ); $wasRedirected = true; } } elseif( !empty( $rdfrom ) ) { @@ -803,7 +867,7 @@ class Article { $outputDone = false; wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$pcache ) ); - if( $pcache && $wgOut->tryParserCache( $this, $wgUser ) ) { + if( $pcache && $wgOut->tryParserCache( $this ) ) { // Ensure that UI elements requiring revision ID have // the correct version information. $wgOut->setRevisionId( $this->mLatest ); @@ -816,14 +880,18 @@ class Article { $this->showDeletionLog(); } $text = $this->getContent(); - if( $text === false ) { + // For now, check also for ID until getContent actually returns + // false for pages that do not exists + if( $text === false || $this->getID() === 0 ) { # Failed to load, replace text with error message $t = $this->mTitle->getPrefixedText(); if( $oldid ) { - $d = wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ); - $text = wfMsg( 'missing-article', $t, $d ); - } else { - $text = wfMsg( 'noarticletext' ); + $d = wfMsgExt( 'missingarticle-rev', 'escape', $oldid ); + $text = wfMsgExt( 'missing-article', 'parsemag', $t, $d ); + // Always use page content for pages in the MediaWiki namespace + // since it contains the default message + } elseif ( $this->mTitle->getNamespace() != NS_MEDIAWIKI ) { + $text = wfMsgExt( 'noarticletext', 'parsemag' ); } } @@ -836,7 +904,7 @@ class Article { // for better machine handling of broken links. $return404 = true; } - } + } if( $return404 ) { $wgRequest->response()->header( "HTTP/1.x 404 Not Found" ); @@ -862,24 +930,35 @@ class Article { // FIXME: This would be a nice place to load the 'no such page' text. } else { $this->setOldSubtitle( isset($this->mOldId) ? $this->mOldId : $oldid ); + # Allow admins to see deleted content if explicitly requested if( $this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - if( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + if( !$unhide || !$this->mRevision->userCan(Revision::DELETED_TEXT) ) { + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); wfProfileOut( __METHOD__ ); return; } else { - $wgOut->addWikiMsg( 'rev-deleted-text-view' ); + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' ); // and we are allowed to see... } } + // Is this the current revision and otherwise cacheable? Try the parser cache... + if( $oldid === $this->getLatest() && $this->useParserCache( false ) + && $wgOut->tryParserCache( $this ) ) + { + $outputDone = true; + } } } + // Ensure that UI elements requiring revision ID have + // the correct version information. $wgOut->setRevisionId( $this->getRevIdFetched() ); - // Pages containing custom CSS or JavaScript get special treatment - if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { + if( $outputDone ) { + // do nothing... + // Pages containing custom CSS or JavaScript get special treatment + } else if( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) { $wgOut->addHTML( wfMsgExt( 'clearyourcache', 'parse' ) ); // Give hooks a chance to customise the output if( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) { @@ -890,7 +969,7 @@ class Article { $wgOut->addHTML( htmlspecialchars( $this->mContent ) ); $wgOut->addHTML( "\n</pre>\n" ); } - } else if( $rt = Title::newFromRedirect( $text ) ) { + } else if( $rt = Title::newFromRedirectArray( $text ) ) { # get an array of redirect targets # Don't append the subtitle if this was an old revision $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) ); $parseout = $wgParser->parse($text, $this->mTitle, ParserOptions::newFromUser($wgUser)); @@ -942,7 +1021,7 @@ class Article { # If we have been passed an &rcid= parameter, we want to give the user a # chance to mark this new article as patrolled. - if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->userCan('patrol') ) { + if( !empty($rcid) && $this->mTitle->exists() && $this->mTitle->quickUserCan('patrol') ) { $wgOut->addHTML( "<div class='patrollink'>" . wfMsgHtml( 'markaspatrolledlink', @@ -1003,24 +1082,41 @@ class Article { /** * View redirect - * @param $target Title object of destination to redirect + * @param $target Title object or Array of destination(s) to redirect * @param $appendSubtitle Boolean [optional] * @param $forceKnown Boolean: should the image be shown as a bluelink regardless of existence? */ public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) { global $wgParser, $wgOut, $wgContLang, $wgStylePath, $wgUser; # Display redirect + if( !is_array( $target ) ) { + $target = array( $target ); + } $imageDir = $wgContLang->isRTL() ? 'rtl' : 'ltr'; - $imageUrl = $wgStylePath.'/common/images/redirect' . $imageDir . '.png'; - + $imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png'; + $imageUrl2 = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png'; + $alt2 = $wgContLang->isRTL() ? '←' : '→'; // should -> and <- be used instead of entities? + if( $appendSubtitle ) { $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) ); } $sk = $wgUser->getSkin(); + // the loop prepends the arrow image before the link, so the first case needs to be outside + $title = array_shift( $target ); if( $forceKnown ) { - $link = $sk->makeKnownLinkObj( $target, htmlspecialchars( $target->getFullText() ) ); + $link = $sk->makeKnownLinkObj( $title, htmlspecialchars( $title->getFullText() ) ); } else { - $link = $sk->makeLinkObj( $target, htmlspecialchars( $target->getFullText() ) ); + $link = $sk->makeLinkObj( $title, htmlspecialchars( $title->getFullText() ) ); + } + // automatically append redirect=no to each link, since most of them are redirect pages themselves + foreach( $target as $rt ) { + if( $forceKnown ) { + $link .= '<img src="'.$imageUrl2.'" alt="'.$alt2.' " />' + . $sk->makeKnownLinkObj( $rt, htmlspecialchars( $rt->getFullText() ) ); + } else { + $link .= '<img src="'.$imageUrl2.'" alt="'.$alt2.' " />' + . $sk->makeLinkObj( $rt, htmlspecialchars( $rt->getFullText() ) ); + } } return '<img src="'.$imageUrl.'" alt="#REDIRECT " />' . '<span class="redirectText">'.$link.'</span>'; @@ -1052,7 +1148,7 @@ class Article { $o->tb_name, $rmvtxt); } - $wgOut->addWikiMsg( 'trackbackbox', $tbtext ); + $wgOut->wrapWikiMsg( "<div id='mw_trackbacks'>$1</div>\n", array( 'trackbackbox', $tbtext ) ); $this->mTitle->invalidateCache(); } @@ -1128,7 +1224,7 @@ class Article { if( $this->getID() == 0 ) { $text = false; } else { - $text = $this->getContent(); + $text = $this->getRawText(); } $wgMessageCache->replace( $this->mTitle->getDBkey(), $text ); } @@ -1490,7 +1586,7 @@ class Article { $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed('minoredit'); $bot = $flags & EDIT_FORCE_BOT; - $oldtext = $this->getContent(); + $oldtext = $this->getRawText(); // current revision $oldsize = strlen( $oldtext ); # Provide autosummaries if one is not provided and autosummaries are enabled. @@ -1601,8 +1697,8 @@ class Article { } # Invalidate cache of this article and all pages using this article - # as a template. Partly deferred. Leave templatelinks for editUpdates(). - Article::onArticleEdit( $this->mTitle, 'skiptransclusions' ); + # as a template. Partly deferred. + Article::onArticleEdit( $this->mTitle ); # Update links tables, site stats, etc. $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed ); } else { @@ -1680,7 +1776,7 @@ class Article { $status->value['revision'] = $revision; wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status ) ); + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) ); wfProfileOut( __METHOD__ ); return $status; @@ -1731,7 +1827,7 @@ class Article { #It would be nice to see where the user had actually come from, but for now just guess $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges'; - $return = Title::makeTitle( NS_SPECIAL, $returnto ); + $return = SpecialPage::getTitleFor( $returnto ); $dbw = wfGetDB( DB_MASTER ); $errors = $rc->doMarkPatrolled(); @@ -1867,7 +1963,18 @@ class Article { global $wgUser, $wgRestrictionTypes, $wgContLang; $id = $this->mTitle->getArticleID(); - if( $id <= 0 || wfReadOnly() || !$this->mTitle->userCan('protect') ) { + if ( $id <= 0 ) { + wfDebug( "updateRestrictions failed: $id <= 0\n" ); + return false; + } + + if ( wfReadOnly() ) { + wfDebug( "updateRestrictions failed: read-only\n" ); + return false; + } + + if ( !$this->mTitle->userCan( 'protect' ) ) { + wfDebug( "updateRestrictions failed: insufficient permissions\n" ); return false; } @@ -1938,6 +2045,9 @@ class Article { $encodedExpiry = array(); $protect_description = ''; foreach( $limit as $action => $restrictions ) { + if ( !isset($expiry[$action]) ) + $expiry[$action] = 'infinite'; + $encodedExpiry[$action] = Block::encodeExpiry($expiry[$action], $dbw ); if( $restrictions != '' ) { $protect_description .= "[$action=$restrictions] ("; @@ -2099,7 +2209,7 @@ class Article { // Calculate the maximum amount of chars to get // Max content length = max comment length - length of the comment (excl. $1) - '...' $maxLength = 255 - (strlen( $reason ) - 2) - 3; - $contents = $wgContLang->truncate( $contents, $maxLength, '...' ); + $contents = $wgContLang->truncate( $contents, $maxLength ); // Remove possible unfinished links $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents ); // Now replace the '$1' placeholder @@ -2124,7 +2234,7 @@ class Article { if( $reason != 'other' && $this->DeleteReason != '' ) { // Entry from drop down menu + additional comment - $reason .= ': ' . $this->DeleteReason; + $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason; } elseif( $reason == 'other' ) { $reason = $this->DeleteReason; } @@ -2398,7 +2508,7 @@ class Article { return false; } - $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getContent() ), -1 ); + $u = new SiteStatsUpdate( 0, 1, -(int)$this->isCountable( $this->getRawText() ), -1 ); array_push( $wgDeferredUpdateList, $u ); // Bitfields to further suppress the content @@ -2500,7 +2610,6 @@ class Article { # Clear the cached article id so the interface doesn't act like we exist $this->mTitle->resetArticleID( 0 ); - $this->mTitle->mArticleID = 0; # Log the deletion, if the page was suppressed, log it at Oversight instead $logtype = $suppress ? 'suppress' : 'delete'; @@ -2812,13 +2921,15 @@ class Article { # Save it to the parser cache if( $wgEnableParserCache ) { + $popts = new ParserOptions; + $popts->setTidy( true ); + $popts->enableLimitReport(); $parserCache = ParserCache::singleton(); - $parserCache->save( $editInfo->output, $this, $wgUser ); + $parserCache->save( $editInfo->output, $this, $popts ); } # Update the links tables - $u = new LinksUpdate( $this->mTitle, $editInfo->output, false ); - $u->setRecursiveTouch( $changed ); // refresh/invalidate including pages too + $u = new LinksUpdate( $this->mTitle, $editInfo->output ); $u->doUpdate(); wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) ); @@ -2903,7 +3014,7 @@ class Article { * @param $oldid String: revision ID of this article revision */ public function setOldSubtitle( $oldid = 0 ) { - global $wgLang, $wgOut, $wgUser; + global $wgLang, $wgOut, $wgUser, $wgRequest; if( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) { return; @@ -2954,16 +3065,17 @@ class Article { } $cdel = "(<small>$cdel</small>) "; } - # Show user links if allowed to see them. Normally they - # are hidden regardless, but since we can already see the text here... - $userlinks = $sk->revUserTools( $revision, false ); + $unhide = $wgRequest->getInt('unhide') == 1 && $wgUser->matchEditToken( $wgRequest->getVal('token'), $oldid ); + # Show user links if allowed to see them. If hidden, then show them only if requested... + $userlinks = $sk->revUserTools( $revision, !$unhide ); $m = wfMsg( 'revision-info-current' ); $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-' ? 'revision-info-current' : 'revision-info'; - $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsgExt( $infomsg, array( 'parseinline', 'replaceafter' ), $td, $userlinks, $revision->getID() ) . "</div>\n" . + $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" . wfMsgExt( $infomsg, array( 'parseinline', 'replaceafter' ), + $td, $userlinks, $revision->getID() ) . "</div>\n" . "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ), $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t"; @@ -3056,7 +3168,7 @@ class Article { if( !$this->mDataLoaded ) { $this->loadPageData(); } - return $this->mLatest; + return (int)$this->mLatest; } /** @@ -3206,17 +3318,18 @@ class Article { $user = User::newFromName( $title->getText(), false ); $user->setNewtalk( false ); } + # Image redirects + RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title ); } /** * Purge caches on page update etc */ - public static function onArticleEdit( $title, $transclusions = 'transclusions' ) { + public static function onArticleEdit( $title, $flags = '' ) { global $wgDeferredUpdateList; // Invalidate caches of articles which include this page - if( $transclusions !== 'skiptransclusions' ) - $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); + $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' ); // Invalidate the caches of all pages which redirect here $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' ); @@ -3405,8 +3518,7 @@ class Article { global $wgContLang; $truncatedtext = $wgContLang->truncate( str_replace("\n", ' ', $newtext), - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ), - '...' ); + max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) ); return wfMsgForContent( 'autosumm-new', $truncatedtext ); } @@ -3418,9 +3530,7 @@ class Article { global $wgContLang; $truncatedtext = $wgContLang->truncate( $newtext, - max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ), - '...' - ); + max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) ); return wfMsgForContent( 'autosumm-replace', $truncatedtext ); } @@ -3449,7 +3559,7 @@ class Article { $popts->enableLimitReport( false ); if( $wgEnableParserCache && $cache && $this && $parserOutput->getCacheTime() != -1 ) { $parserCache = ParserCache::singleton(); - $parserCache->save( $parserOutput, $this, $wgUser ); + $parserCache->save( $parserOutput, $this, $popts ); } // Make sure file cache is not used on uncacheable content. // Output that has magic words in it can still use the parser cache @@ -3478,27 +3588,26 @@ class Article { __METHOD__ ); global $wgContLang; - - if( $res !== false ) { - foreach( $res as $row ) { - $tlTemplates[] = $wgContLang->getNsText( $row->tl_namespace ) . ':' . $row->tl_title ; - } + foreach( $res as $row ) { + $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true; } # Get templates from parser output. - $poTemplates_allns = $parserOutput->getTemplates(); - - $poTemplates = array (); - foreach ( $poTemplates_allns as $ns_templates ) { - $poTemplates = array_merge( $poTemplates, $ns_templates ); + $poTemplates = array(); + foreach ( $parserOutput->getTemplates() as $ns => $templates ) { + foreach ( $templates as $dbk => $id ) { + $key = $row->tl_namespace . ':'. $row->tl_title; + $poTemplates["$ns:$dbk"] = true; + } } # Get the diff - $templates_diff = array_diff( $poTemplates, $tlTemplates ); + # Note that we simulate array_diff_key in PHP <5.0.x + $templates_diff = array_diff_key( $poTemplates, $tlTemplates ); if( count( $templates_diff ) > 0 ) { # Whee, link updates time. - $u = new LinksUpdate( $this->mTitle, $parserOutput ); + $u = new LinksUpdate( $this->mTitle, $parserOutput, false ); $u->doUpdate(); } } diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index ce1912ea..85e7e668 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -19,6 +19,7 @@ $wgAutoloadLocalClasses = array( 'AuthPlugin' => 'includes/AuthPlugin.php', 'AuthPluginUser' => 'includes/AuthPlugin.php', 'Autopromote' => 'includes/Autopromote.php', + 'BacklinkCache' => 'includes/BacklinkCache.php', 'BagOStuff' => 'includes/BagOStuff.php', 'Block' => 'includes/Block.php', 'CacheDependency' => 'includes/CacheDependency.php', @@ -28,6 +29,7 @@ $wgAutoloadLocalClasses = array( 'CategoryViewer' => 'includes/CategoryPage.php', 'ChangesList' => 'includes/ChangesList.php', 'ChangesFeed' => 'includes/ChangesFeed.php', + 'ChangeTags' => 'includes/ChangeTags.php', 'ChannelFeed' => 'includes/Feed.php', 'ConcatenatedGzipHistoryBlob' => 'includes/HistoryBlob.php', 'ConstantDependency' => 'includes/CacheDependency.php', @@ -73,6 +75,7 @@ $wgAutoloadLocalClasses = array( 'FileDependency' => 'includes/CacheDependency.php', 'FileRevertForm' => 'includes/FileRevertForm.php', 'FileStore' => 'includes/FileStore.php', + 'ForkController' => 'includes/ForkController.php', 'FormatExif' => 'includes/Exif.php', 'FormOptions' => 'includes/FormOptions.php', 'FSException' => 'includes/FileStore.php', @@ -186,6 +189,7 @@ $wgAutoloadLocalClasses = array( 'StringUtils' => 'includes/StringUtils.php', 'TablePager' => 'includes/Pager.php', 'ThumbnailImage' => 'includes/MediaTransformOutput.php', + 'TiffHandler' => 'includes/media/Tiff.php', 'TitleDependency' => 'includes/CacheDependency.php', 'Title' => 'includes/Title.php', 'TitleArray' => 'includes/TitleArray.php', @@ -193,10 +197,6 @@ $wgAutoloadLocalClasses = array( 'TransformParameterError' => 'includes/MediaTransformOutput.php', 'TurckBagOStuff' => 'includes/BagOStuff.php', 'UnlistedSpecialPage' => 'includes/SpecialPage.php', - 'UploadBase' => 'includes/UploadBase.php', - 'UploadFromStash' => 'includes/UploadFromStash.php', - 'UploadFromUpload' => 'includes/UploadFromUpload.php', - 'UploadFromUrl' => 'includes/UploadFromUrl.php', 'User' => 'includes/User.php', 'UserArray' => 'includes/UserArray.php', 'UserArrayFromResult' => 'includes/UserArray.php', @@ -231,11 +231,14 @@ $wgAutoloadLocalClasses = array( 'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php', 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', 'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php', + 'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php', 'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php', 'ApiFormatWddx' => 'includes/api/ApiFormatWddx.php', 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', 'ApiHelp' => 'includes/api/ApiHelp.php', + 'ApiImport' => 'includes/api/ApiImport.php', + 'ApiImportReporter' => 'includes/api/ApiImport.php', 'ApiLogin' => 'includes/api/ApiLogin.php', 'ApiLogout' => 'includes/api/ApiLogout.php', 'ApiMain' => 'includes/api/ApiMain.php', @@ -273,6 +276,7 @@ $wgAutoloadLocalClasses = array( 'ApiQueryLangLinks' => 'includes/api/ApiQueryLangLinks.php', 'ApiQueryLinks' => 'includes/api/ApiQueryLinks.php', 'ApiQueryLogEvents' => 'includes/api/ApiQueryLogEvents.php', + 'ApiQueryProtectedTitles' => 'includes/api/ApiQueryProtectedTitles.php', 'ApiQueryRandom' => 'includes/api/ApiQueryRandom.php', 'ApiQueryRecentChanges'=> 'includes/api/ApiQueryRecentChanges.php', 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', @@ -320,6 +324,11 @@ $wgAutoloadLocalClasses = array( 'PostgresField' => 'includes/db/DatabasePostgres.php', 'ResultWrapper' => 'includes/db/Database.php', 'SQLiteField' => 'includes/db/DatabaseSqlite.php', + + 'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php', + 'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php', + 'IBM_DB2SearchResultSet' => 'includes/SearchIBM_DB2.php', + 'SearchIBM_DB2' => 'includes/SearchIBM_DB2.php', # includes/diff 'AncestorComparator' => 'includes/diff/HTMLDiff.php', @@ -431,6 +440,7 @@ $wgAutoloadLocalClasses = array( 'Preprocessor_DOM' => 'includes/parser/Preprocessor_DOM.php', 'Preprocessor_Hash' => 'includes/parser/Preprocessor_Hash.php', 'StripState' => 'includes/parser/Parser.php', + 'MWTidy' => 'includes/parser/Tidy.php', # includes/specials 'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php', @@ -474,11 +484,12 @@ $wgAutoloadLocalClasses = array( 'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php', 'PreferencesForm' => 'includes/specials/SpecialPreferences.php', 'RandomPage' => 'includes/specials/SpecialRandompage.php', - 'RevisionDeleteForm' => 'includes/specials/SpecialRevisiondelete.php', + 'SpecialRevisionDelete' => 'includes/specials/SpecialRevisiondelete.php', 'RevisionDeleter' => 'includes/specials/SpecialRevisiondelete.php', 'ShortPagesPage' => 'includes/specials/SpecialShortpages.php', 'SpecialAllpages' => 'includes/specials/SpecialAllpages.php', 'SpecialBookSources' => 'includes/specials/SpecialBooksources.php', + 'SpecialExport' => 'includes/specials/SpecialExport.php', 'SpecialImport' => 'includes/specials/SpecialImport.php', 'SpecialListGroupRights' => 'includes/specials/SpecialListgrouprights.php', 'SpecialMostlinkedtemplates' => 'includes/specials/SpecialMostlinkedtemplates.php', @@ -489,6 +500,7 @@ $wgAutoloadLocalClasses = array( 'SpecialSearch' => 'includes/specials/SpecialSearch.php', 'SpecialSearchOld' => 'includes/specials/SpecialSearch.php', 'SpecialStatistics' => 'includes/specials/SpecialStatistics.php', + 'SpecialTags' => 'includes/specials/SpecialTags.php', 'SpecialVersion' => 'includes/specials/SpecialVersion.php', 'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php', 'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php', @@ -556,7 +568,7 @@ class AutoLoader { } if ( !$filename ) { if( function_exists( 'wfDebug' ) ) - wfDebug( "Class {$className} not found; skipped loading" ); + wfDebug( "Class {$className} not found; skipped loading\n" ); # Give up return false; } @@ -575,7 +587,7 @@ class AutoLoader { global $wgAutoloadClasses; foreach( $wgAutoloadClasses as $class => $file ) { - if( !( class_exists( $class ) || interface_exists( $class ) ) ) { + if( !( class_exists( $class, false ) || interface_exists( $class, false ) ) ) { require( $file ); } } diff --git a/includes/BacklinkCache.php b/includes/BacklinkCache.php new file mode 100644 index 00000000..a7bcd858 --- /dev/null +++ b/includes/BacklinkCache.php @@ -0,0 +1,232 @@ +<?php + +/** + * Class for fetching backlink lists, approximate backlink counts and partitions. + * Instances of this class should typically be fetched with $title->getBacklinkCache(). + * + * Ideally you should only get your backlinks from here when you think there is some + * advantage in caching them. Otherwise it's just a waste of memory. + */ +class BacklinkCache { + var $partitionCache = array(); + var $fullResultCache = array(); + var $title; + var $db; + + const CACHE_EXPIRY = 3600; + + /** + * Create a new BacklinkCache + */ + function __construct( $title ) { + $this->title = $title; + } + + /** + * Clear locally stored data + */ + function clear() { + $this->partitionCache = array(); + $this->fullResultCache = array(); + unset( $this->db ); + } + + /** + * Set the Database object to use + */ + public function setDB( $db ) { + $this->db = $db; + } + + protected function getDB() { + if ( !isset( $this->db ) ) { + $this->db = wfGetDB( DB_SLAVE ); + } + return $this->db; + } + + /** + * Get the backlinks for a given table. Cached in process memory only. + * @param string $table + * @return TitleArray + */ + public function getLinks( $table, $startId = false, $endId = false ) { + wfProfileIn( __METHOD__ ); + + if ( $startId || $endId ) { + // Partial range, not cached + wfDebug( __METHOD__.": from DB (uncacheable range)\n" ); + $conds = $this->getConditions( $table ); + // Use the from field in the condition rather than the joined page_id, + // because databases are stupid and don't necessarily propagate indexes. + $fromField = $this->getPrefix( $table ) . '_from'; + if ( $startId ) { + $conds[] = "$fromField >= " . intval( $startId ); + } + if ( $endId ) { + $conds[] = "$fromField <= " . intval( $endId ); + } + $res = $this->getDB()->select( + array( $table, 'page' ), + array( 'page_namespace', 'page_title', 'page_id'), + $conds, + __METHOD__, + array('STRAIGHT_JOIN') ); + $ta = TitleArray::newFromResult( $res ); + wfProfileOut( __METHOD__ ); + return $ta; + } + + if ( !isset( $this->fullResultCache[$table] ) ) { + wfDebug( __METHOD__.": from DB\n" ); + $res = $this->getDB()->select( + array( $table, 'page' ), + array( 'page_namespace', 'page_title', 'page_id' ), + $this->getConditions( $table ), + __METHOD__, + array('STRAIGHT_JOIN') ); + $this->fullResultCache[$table] = $res; + } + $ta = TitleArray::newFromResult( $this->fullResultCache[$table] ); + wfProfileOut( __METHOD__ ); + return $ta; + } + + /** + * Get the field name prefix for a given table + */ + protected function getPrefix( $table ) { + static $prefixes = array( + 'pagelinks' => 'pl', + 'imagelinks' => 'il', + 'categorylinks' => 'cl', + 'templatelinks' => 'tl', + 'redirect' => 'rd', + ); + if ( isset( $prefixes[$table] ) ) { + return $prefixes[$table]; + } else { + throw new MWException( "Invalid table \"$table\" in " . __CLASS__ ); + } + } + + /** + * Get the SQL condition array for selecting backlinks, with a join on the page table + */ + protected function getConditions( $table ) { + $prefix = $this->getPrefix( $table ); + switch ( $table ) { + case 'pagelinks': + case 'templatelinks': + case 'redirect': + $conds = array( + "{$prefix}_namespace" => $this->title->getNamespace(), + "{$prefix}_title" => $this->title->getDBkey(), + "page_id={$prefix}_from" + ); + break; + case 'imagelinks': + $conds = array( + 'il_to' => $this->title->getDBkey(), + 'page_id=il_from' + ); + break; + case 'categorylinks': + $conds = array( + 'cl_to' => $this->title->getDBkey(), + 'page_id=cl_from', + ); + break; + default: + throw new MWException( "Invalid table \"$table\" in " . __CLASS__ ); + } + return $conds; + } + + /** + * Get the approximate number of backlinks + */ + public function getNumLinks( $table ) { + if ( isset( $this->fullResultCache[$table] ) ) { + return $this->fullResultCache[$table]->numRows(); + } + if ( isset( $this->partitionCache[$table] ) ) { + $entry = reset( $this->partitionCache[$table] ); + return $entry['numRows']; + } + $titleArray = $this->getLinks( $table ); + return $titleArray->count(); + } + + /** + * Partition the backlinks into batches. + * Returns an array giving the start and end of each range. The first batch has + * a start of false, and the last batch has an end of false. + * + * @param string $table The links table name + * @param integer $batchSize + * @return array + */ + public function partition( $table, $batchSize ) { + // Try cache + if ( isset( $this->partitionCache[$table][$batchSize] ) ) { + wfDebug( __METHOD__.": got from partition cache\n" ); + return $this->partitionCache[$table][$batchSize]['batches']; + } + $this->partitionCache[$table][$batchSize] = false; + $cacheEntry =& $this->partitionCache[$table][$batchSize]; + + // Try full result cache + if ( isset( $this->fullResultCache[$table] ) ) { + $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize ); + wfDebug( __METHOD__.": got from full result cache\n" ); + return $cacheEntry['batches']; + } + // Try memcached + global $wgMemc; + $memcKey = wfMemcKey( 'backlinks', md5( $this->title->getPrefixedDBkey() ), + $table, $batchSize ); + $memcValue = $wgMemc->get( $memcKey ); + if ( is_array( $memcValue ) ) { + $cacheEntry = $memcValue; + wfDebug( __METHOD__.": got from memcached $memcKey\n" ); + return $cacheEntry['batches']; + } + // Fetch from database + $this->getLinks( $table ); + $cacheEntry = $this->partitionResult( $this->fullResultCache[$table], $batchSize ); + // Save to memcached + $wgMemc->set( $memcKey, $cacheEntry, self::CACHE_EXPIRY ); + wfDebug( __METHOD__.": got from database\n" ); + return $cacheEntry['batches']; + } + + /** + * Partition a DB result with backlinks in it into batches + */ + protected function partitionResult( $res, $batchSize ) { + $batches = array(); + $numRows = $res->numRows(); + $numBatches = ceil( $numRows / $batchSize ); + for ( $i = 0; $i < $numBatches; $i++ ) { + if ( $i == 0 ) { + $start = false; + } else { + $rowNum = intval( $numRows * $i / $numBatches ); + $res->seek( $rowNum ); + $row = $res->fetchObject(); + $start = $row->page_id; + } + if ( $i == $numBatches - 1 ) { + $end = false; + } else { + $rowNum = intval( $numRows * ( $i + 1 ) / $numBatches ); + $res->seek( $rowNum ); + $row = $res->fetchObject(); + $end = $row->page_id - 1; + } + $batches[] = array( $start, $end ); + } + return array( 'numRows' => $numRows, 'batches' => $batches ); + } +} diff --git a/includes/Block.php b/includes/Block.php index 2c2227e2..a44941f1 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -105,6 +105,7 @@ class Block { && $this->mHideName == $block->mHideName && $this->mBlockEmail == $block->mBlockEmail && $this->mAllowUsertalk == $block->mAllowUsertalk + && $this->mReason == $block->mReason ); } @@ -570,7 +571,7 @@ class Block { ## Allow hooks to cancel the autoblock. if (!wfRunHooks( 'AbortAutoblock', array( $autoblockIP, &$this ) )) { - wfDebug( "Autoblock aborted by hook." ); + wfDebug( "Autoblock aborted by hook.\n" ); return false; } diff --git a/includes/Category.php b/includes/Category.php index 78567add..50efdbc1 100644 --- a/includes/Category.php +++ b/includes/Category.php @@ -41,8 +41,7 @@ class Category { $dbr = wfGetDB( DB_SLAVE ); $row = $dbr->selectRow( 'category', - array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', - 'cat_files' ), + array( 'cat_id', 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files' ), $where, __METHOD__ ); @@ -70,8 +69,7 @@ class Category { # (bug 13683) If the count is negative, then 1) it's obviously wrong # and should not be kept, and 2) we *probably* don't have to scan many # rows to obtain the correct figure, so let's risk a one-time recount. - if( $this->mPages < 0 || $this->mSubcats < 0 || - $this->mFiles < 0 ) { + if( $this->mPages < 0 || $this->mSubcats < 0 || $this->mFiles < 0 ) { $this->refreshCounts(); } diff --git a/includes/CategoryPage.php b/includes/CategoryPage.php index 4ac24b5f..03ecb5dc 100644 --- a/includes/CategoryPage.php +++ b/includes/CategoryPage.php @@ -20,7 +20,8 @@ class CategoryPage extends Article { if ( isset( $diff ) && $diffOnly ) return Article::view(); - if(!wfRunHooks('CategoryPageView', array(&$this))) return; + if( !wfRunHooks( 'CategoryPageView', array( &$this ) ) ) + return; if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { $this->openShowCategory(); @@ -28,10 +29,6 @@ class CategoryPage extends Article { Article::view(); - # If the article we've just shown is in the "Image" namespace, - # follow it with the history list and link list for the image - # it describes. - if ( NS_CATEGORY == $this->mTitle->getNamespace() ) { $this->closeShowCategory(); } @@ -79,7 +76,7 @@ class CategoryViewer { $this->from = $from; $this->until = $until; $this->limit = $wgCategoryPagingLimit; - $this->cat = Category::newFromName( $title->getDBKey() ); + $this->cat = Category::newFromTitle( $title ); } /** @@ -192,9 +189,10 @@ class CategoryViewer { */ function addPage( $title, $sortkey, $pageLength, $isRedirect = false ) { global $wgContLang; + $titletext = $wgContLang->convert( $title->getPrefixedText() ); $this->articles[] = $isRedirect - ? '<span class="redirect-in-category">' . $this->getSkin()->makeKnownLinkObj( $title ) . '</span>' - : $this->getSkin()->makeSizeLinkObj( $pageLength, $title ); + ? '<span class="redirect-in-category">' . $this->getSkin()->makeKnownLinkObj( $title, $titletext ) . '</span>' + : $this->getSkin()->makeSizeLinkObj( $pageLength, $title, $titletext ); $this->articles_start_char[] = $wgContLang->convert( $wgContLang->firstChar( $sortkey ) ); } @@ -208,7 +206,7 @@ class CategoryViewer { } function doCategoryQuery() { - $dbr = wfGetDB( DB_SLAVE ); + $dbr = wfGetDB( DB_SLAVE, 'category' ); if( $this->from != '' ) { $pageCondition = 'cl_sortkey >= ' . $dbr->addQuotes( $this->from ); $this->flip = false; @@ -316,7 +314,7 @@ class CategoryViewer { $countmsg = $this->getCountMessage( $rescnt, $dbcnt, 'file' ); return "<div id=\"mw-category-media\">\n" . - '<h2>' . wfMsg( 'category-media-header', htmlspecialchars($this->title->getText()) ) . "</h2>\n" . + '<h2>' . wfMsg( 'category-media-header', htmlspecialchars( $this->title->getText() ) ) . "</h2>\n" . $countmsg . $this->gallery->toHTML() . "\n</div>"; } else { return ''; @@ -451,12 +449,12 @@ class CategoryViewer { $sk = $this->getSkin(); $limitText = $wgLang->formatNum( $limit ); - $prevLink = htmlspecialchars( wfMsg( 'prevn', $limitText ) ); + $prevLink = wfMsgExt( 'prevn', array( 'escape', 'parsemag' ), $limitText ); if( $first != '' ) { $prevLink = $sk->makeLinkObj( $title, $prevLink, wfArrayToCGI( $query + array( 'until' => $first ) ) ); } - $nextLink = htmlspecialchars( wfMsg( 'nextn', $limitText ) ); + $nextLink = wfMsgExt( 'nextn', array( 'escape', 'parsemag' ), $limitText ); if( $last != '' ) { $nextLink = $sk->makeLinkObj( $title, $nextLink, wfArrayToCGI( $query + array( 'from' => $last ) ) ); diff --git a/includes/Categoryfinder.php b/includes/Categoryfinder.php index 4413bd1a..7c1c2856 100644 --- a/includes/Categoryfinder.php +++ b/includes/Categoryfinder.php @@ -53,9 +53,11 @@ class Categoryfinder { # Set the list of target categories; convert them to DBKEY form first $this->targets = array () ; foreach ( $categories AS $c ) { - $ct = Title::newFromText ( $c , NS_CATEGORY ) ; - $c = $ct->getDBkey () ; - $this->targets[$c] = $c ; + $ct = Title::makeTitleSafe( NS_CATEGORY, $c ); + if( $ct ) { + $c = $ct->getDBkey(); + $this->targets[$c] = $c; + } } } diff --git a/includes/ChangeTags.php b/includes/ChangeTags.php new file mode 100644 index 00000000..de804c5c --- /dev/null +++ b/includes/ChangeTags.php @@ -0,0 +1,183 @@ +<?php + +if (!defined( 'MEDIAWIKI' )) + die; + +class ChangeTags { + static function formatSummaryRow( $tags, $page ) { + if (!$tags) + return array('',array()); + + $classes = array(); + + $tags = explode( ',', $tags ); + $displayTags = array(); + foreach( $tags as $tag ) { + $displayTags[] = self::tagDescription( $tag ); + $classes[] = "mw-tag-$tag"; + } + + return array( '(' . implode( ', ', $displayTags ) . ')', $classes ); + } + + static function tagDescription( $tag ) { + $msg = wfMsgExt( "tag-$tag", 'parseinline' ); + if ( wfEmptyMsg( "tag-$tag", $msg ) ) { + return htmlspecialchars($tag); + } + return $msg; + } + + ## Basic utility method to add tags to a particular change, given its rc_id, rev_id and/or log_id. + static function addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params = null ) { + if ( !is_array($tags) ) { + $tags = array( $tags ); + } + + $tags = array_filter( $tags ); // Make sure we're submitting all tags... + + if (!$rc_id && !$rev_id && !$log_id) { + throw new MWException( "At least one of: RCID, revision ID, and log ID MUST be specified when adding a tag to a change!" ); + } + + $dbr = wfGetDB( DB_SLAVE ); + + // Might as well look for rcids and so on. + if (!$rc_id) { + $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave. + if ($log_id) { + $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_logid' => $log_id ), __METHOD__ ); + } elseif ($rev_id) { + $rc_id = $dbr->selectField( 'recentchanges', 'rc_id', array( 'rc_this_oldid' => $rev_id ), __METHOD__ ); + } + } elseif (!$log_id && !$rev_id) { + $dbr = wfGetDB( DB_MASTER ); // Info might be out of date, somewhat fractionally, on slave. + $log_id = $dbr->selectField( 'recentchanges', 'rc_logid', array( 'rc_id' => $rc_id ), __METHOD__ ); + $rev_id = $dbr->selectField( 'recentchanges', 'rc_this_oldid', array( 'rc_id' => $rc_id ), __METHOD__ ); + } + + $tsConds = array_filter( array( 'ts_rc_id' => $rc_id, 'ts_rev_id' => $rev_id, 'ts_log_id' => $log_id ) ); + + ## Update the summary row. + $prevTags = $dbr->selectField( 'tag_summary', 'ts_tags', $tsConds, __METHOD__ ); + $prevTags = $prevTags ? $prevTags : ''; + $prevTags = array_filter( explode( ',', $prevTags ) ); + $newTags = array_unique( array_merge( $prevTags, $tags ) ); + sort($prevTags); + sort($newTags); + + if ( $prevTags == $newTags ) { + // No change. + return false; + } + + $dbw = wfGetDB( DB_MASTER ); + $dbw->replace( 'tag_summary', array( 'ts_rev_id', 'ts_rc_id', 'ts_log_id' ), array_filter( array_merge( $tsConds, array( 'ts_tags' => implode( ',', $newTags ) ) ) ), __METHOD__ ); + + // Insert the tags rows. + $tagsRows = array(); + foreach( $tags as $tag ) { // Filter so we don't insert NULLs as zero accidentally. + $tagsRows[] = array_filter( array( 'ct_tag' => $tag, 'ct_rc_id' => $rc_id, 'ct_log_id' => $log_id, 'ct_rev_id' => $rev_id, 'ct_params' => $params ) ); + } + + $dbw->insert( 'change_tag', $tagsRows, __METHOD__, array('IGNORE') ); + + return true; + } + + /** + * Applies all tags-related changes to a query. + * Handles selecting tags, and filtering. + * Needs $tables to be set up properly, so we can figure out which join conditions to use. + */ + static function modifyDisplayQuery( &$tables, &$fields, &$conds, + &$join_conds, &$options, $filter_tag = false ) { + global $wgRequest, $wgUseTagFilter; + + if ($filter_tag === false) { + $filter_tag = $wgRequest->getVal( 'tagfilter' ); + } + + // Figure out which conditions can be done. + $join_field = ''; + if ( in_array('recentchanges', $tables) ) { + $join_cond = 'rc_id'; + } elseif( in_array('logging', $tables) ) { + $join_cond = 'log_id'; + } elseif ( in_array('revision', $tables) ) { + $join_cond = 'rev_id'; + } else { + throw new MWException( "Unable to determine appropriate JOIN condition for tagging." ); + } + + // JOIN on tag_summary + $tables[] = 'tag_summary'; + $join_conds['tag_summary'] = array( 'LEFT JOIN', "ts_$join_cond=$join_cond" ); + $fields[] = 'ts_tags'; + + if ($wgUseTagFilter && $filter_tag) { + // Somebody wants to filter on a tag. + // Add an INNER JOIN on change_tag + + // FORCE INDEX -- change_tags will almost ALWAYS be the correct query plan. + $options['USE INDEX'] = array( 'change_tag' => 'change_tag_tag_id' ); + unset( $options['FORCE INDEX'] ); + $tables[] = 'change_tag'; + $join_conds['change_tag'] = array( 'INNER JOIN', "ct_$join_cond=$join_cond" ); + $conds['ct_tag'] = $filter_tag; + } + } + + /** + * If $fullForm is set to false, then it returns an array of (label, form). + * If $fullForm is true, it returns an entire form. + */ + static function buildTagFilterSelector( $selected='', $fullForm = false /* used to put a full form around the selector */ ) { + global $wgUseTagFilter; + + if ( !$wgUseTagFilter || !count( self::listDefinedTags() ) ) + return $fullForm ? '' : array(); + + global $wgTitle; + + $data = array( wfMsgExt( 'tag-filter', 'parseinline' ), Xml::input( 'tagfilter', 20, $selected ) ); + + if (!$fullForm) { + return $data; + } + + $html = implode( ' ', $data ); + $html .= "\n" . Xml::element( 'input', array( 'type' => 'submit', 'value' => wfMsg( 'tag-filter-submit' ) ) ); + $html .= "\n" . Xml::hidden( 'title', $wgTitle-> getPrefixedText() ); + $html = Xml::tags( 'form', array( 'action' => $wgTitle->getLocalURL(), 'method' => 'get' ), $html ); + + return $html; + } + + /** Basically lists defined tags which count even if they aren't applied to anything */ + static function listDefinedTags() { + // Caching... + global $wgMemc; + $key = wfMemcKey( 'valid-tags' ); + + if ($tags = $wgMemc->get( $key )) + return $tags; + + $emptyTags = array(); + + // Some DB stuff + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'valid_tag', 'vt_tag', array(), __METHOD__ ); + while( $row = $res->fetchObject() ) { + $emptyTags[] = $row->vt_tag; + } + + wfRunHooks( 'ListDefinedTags', array(&$emptyTags) ); + + $emptyTags = array_filter( array_unique( $emptyTags ) ); + + // Short-term caching. + $wgMemc->set( $key, $emptyTags, 300 ); + return $emptyTags; + } +} diff --git a/includes/ChangesFeed.php b/includes/ChangesFeed.php index f3c3e429..a0c2767a 100644 --- a/includes/ChangesFeed.php +++ b/includes/ChangesFeed.php @@ -18,16 +18,16 @@ class ChangesFeed { $feedTitle, htmlspecialchars( $description ), $wgTitle->getFullUrl() ); } - public function execute( $feed, $rows, $limit = 0 , $hideminor = false, $lastmod = false ) { + public function execute( $feed, $rows, $limit=0, $hideminor=false, $lastmod=false, $target='' ) { global $messageMemc, $wgFeedCacheTimeout; - global $wgFeedClasses, $wgSitename, $wgContLanguageCode; + global $wgSitename, $wgContLanguageCode; if ( !FeedUtils::checkFeedOutput( $this->format ) ) { return; } $timekey = wfMemcKey( $this->type, $this->format, 'timestamp' ); - $key = wfMemcKey( $this->type, $this->format, 'limit', $limit, 'minor', $hideminor ); + $key = wfMemcKey( $this->type, $this->format, $limit, $hideminor, $target ); FeedUtils::checkPurge($timekey, $key); diff --git a/includes/ChangesList.php b/includes/ChangesList.php index a8f5fff0..4eda1dbd 100644 --- a/includes/ChangesList.php +++ b/includes/ChangesList.php @@ -108,7 +108,7 @@ class ChangesList { public static function showCharacterDifference( $old, $new ) { global $wgRCChangedSizeThreshold, $wgLang; $szdiff = $new - $old; - $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape'), $wgLang->formatNum($szdiff) ); + $formatedSize = wfMsgExt( 'rc-change-size', array( 'parsemag', 'escape' ), $wgLang->formatNum( $szdiff ) ); if( abs( $szdiff ) > abs( $wgRCChangedSizeThreshold ) ) { $tag = 'strong'; } else { @@ -223,9 +223,9 @@ class ChangesList { } /** Insert links to user page, user talk page and eventually a blocking link */ - public function insertUserRelatedLinks(&$s, &$rc) { - if( $this->isDeleted($rc,Revision::DELETED_USER) ) { - $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>'; + public function insertUserRelatedLinks( &$s, &$rc ) { + if( $this->isDeleted( $rc, Revision::DELETED_USER ) ) { + $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; } else { $s .= $this->skin->userLink( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); $s .= $this->skin->userToolLinks( $rc->mAttribs['rc_user'], $rc->mAttribs['rc_user_text'] ); @@ -233,22 +233,22 @@ class ChangesList { } /** insert a formatted action */ - protected function insertAction(&$s, &$rc) { + protected function insertAction( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] == RC_LOG ) { - if( $this->isDeleted($rc,LogPage::DELETED_ACTION) ) { - $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; + if( $this->isDeleted( $rc, LogPage::DELETED_ACTION ) ) { + $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>'; } else { $s .= ' '.LogPage::actionText( $rc->mAttribs['rc_log_type'], $rc->mAttribs['rc_log_action'], - $rc->getTitle(), $this->skin, LogPage::extractParams($rc->mAttribs['rc_params']), true, true ); + $rc->getTitle(), $this->skin, LogPage::extractParams( $rc->mAttribs['rc_params'] ), true, true ); } } } /** insert a formatted comment */ - protected function insertComment(&$s, &$rc) { + protected function insertComment( &$s, &$rc ) { if( $rc->mAttribs['rc_type'] != RC_MOVE && $rc->mAttribs['rc_type'] != RC_MOVE_OVER_REDIRECT ) { - if( $this->isDeleted($rc,Revision::DELETED_COMMENT) ) { - $s .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-comment') . '</span>'; + if( $this->isDeleted( $rc, Revision::DELETED_COMMENT ) ) { + $s .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-comment' ) . '</span>'; } else { $s .= $this->skin->commentBlock( $rc->mAttribs['rc_comment'], $rc->getTitle() ); } @@ -272,8 +272,8 @@ class ChangesList { static $cache = array(); if( $count > 0 ) { if( !isset( $cache[$count] ) ) { - $cache[$count] = wfMsgExt('number_of_watching_users_RCview', - array('parsemag', 'escape'), $wgLang->formatNum($count)); + $cache[$count] = wfMsgExt( 'number_of_watching_users_RCview', + array('parsemag', 'escape' ), $wgLang->formatNum( $count ) ); } return $cache[$count]; } else { @@ -288,7 +288,7 @@ class ChangesList { * @return bool */ public static function isDeleted( $rc, $field ) { - return ($rc->mAttribs['rc_deleted'] & $field) == $field; + return ( $rc->mAttribs['rc_deleted'] & $field ) == $field; } /** @@ -318,6 +318,40 @@ class ChangesList { return '<span class="mw-rc-unwatched">' . $link . '</span>'; } } + + /** Inserts a rollback link */ + protected function insertRollback( &$s, &$rc ) { + global $wgUser; + if( !$rc->mAttribs['rc_new'] && $rc->mAttribs['rc_this_oldid'] && $rc->mAttribs['rc_cur_id'] ) { + $page = $rc->getTitle(); + /** Check for rollback and edit permissions, disallow special pages, and only + * show a link on the top-most revision */ + if ($wgUser->isAllowed('rollback') && $rc->mAttribs['page_latest'] == $rc->mAttribs['rc_this_oldid'] ) + { + $rev = new Revision( array( + 'id' => $rc->mAttribs['rc_this_oldid'], + 'user' => $rc->mAttribs['rc_user'], + 'user_text' => $rc->mAttribs['rc_user_text'], + 'deleted' => $rc->mAttribs['rc_deleted'] + ) ); + $rev->setTitle( $page ); + $s .= ' '.$this->skin->generateRollback( $rev ); + } + } + } + + protected function insertTags( &$s, &$rc, &$classes ) { + if ( empty($rc->mAttribs['ts_tags']) ) + return; + + list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $rc->mAttribs['ts_tags'], 'changeslist' ); + $classes = array_merge( $classes, $newClasses ); + $s .= ' ' . $tagSummary; + } + + protected function insertExtra( &$s, &$rc, &$classes ) { + ## Empty, used for subclassers to add anything special. + } } @@ -328,8 +362,8 @@ class OldChangesList extends ChangesList { /** * Format a line using the old system (aka without any javascript). */ - public function recentChangesLine( &$rc, $watched = false ) { - global $wgContLang, $wgRCShowChangedSize, $wgUser; + public function recentChangesLine( &$rc, $watched = false, $linenumber = NULL ) { + global $wgContLang, $wgLang, $wgRCShowChangedSize, $wgUser; wfProfileIn( __METHOD__ ); # Should patrol-related stuff be shown? $unpatrolled = $wgUser->useRCPatrol() && !$rc->mAttribs['rc_patrolled']; @@ -338,6 +372,17 @@ class OldChangesList extends ChangesList { $this->insertDateHeader( $dateheader, $rc->mAttribs['rc_timestamp'] ); $s = ''; + $classes = array(); + // use mw-line-even/mw-line-odd class only if linenumber is given (feature from bug 14468) + if( $linenumber ) { + if( $linenumber & 1 ) { + $classes[] = 'mw-line-odd'; + } + else { + $classes[] = 'mw-line-even'; + } + } + // Moved pages if( $rc->mAttribs['rc_type'] == RC_MOVE || $rc->mAttribs['rc_type'] == RC_MOVE_OVER_REDIRECT ) { $this->insertMove( $s, $rc ); @@ -369,25 +414,32 @@ class OldChangesList extends ChangesList { } } # User tool links - $this->insertUserRelatedLinks($s,$rc); + $this->insertUserRelatedLinks( $s, $rc ); # Log action text (if any) - $this->insertAction($s, $rc); + $this->insertAction( $s, $rc ); # Edit or log comment - $this->insertComment($s, $rc); + $this->insertComment( $s, $rc ); + # Tags + $this->insertTags( $s, $rc, $classes ); + # Rollback + $this->insertRollback( $s, $rc ); + # For subclasses + $this->insertExtra( $s, $rc, $classes ); + # Mark revision as deleted if so if( !$rc->mAttribs['rc_log_type'] && $this->isDeleted($rc,Revision::DELETED_TEXT) ) { $s .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; } # How many users watch this page if( $rc->numberofWatchingusers > 0 ) { - $s .= ' ' . wfMsg( 'number_of_watching_users_RCview', - $wgContLang->formatNum($rc->numberofWatchingusers) ); + $s .= ' ' . wfMsgExt( 'number_of_watching_users_RCview', + array( 'parsemag', 'escape' ), $wgLang->formatNum( $rc->numberofWatchingusers ) ); } wfRunHooks( 'OldChangesListRecentChangesLine', array(&$this, &$s, $rc) ); wfProfileOut( __METHOD__ ); - return "$dateheader<li>$s</li>\n"; + return "$dateheader<li class=\"".implode( ' ', $classes )."\">$s</li>\n"; } } @@ -417,6 +469,8 @@ class EnhancedChangesList extends ChangesList { */ public function recentChangesLine( &$baseRC, $watched = false ) { global $wgLang, $wgContLang, $wgUser; + + wfProfileIn( __METHOD__ ); # Create a specialised object $rc = RCCacheEntry::newFromParent( $baseRC ); @@ -508,10 +562,8 @@ class EnhancedChangesList extends ChangesList { if( !$showdifflinks ) { $curLink = $this->message['cur']; $diffLink = $this->message['diff']; - } else if( $rc_type == RC_NEW || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { - if( $rc_type != RC_NEW ) { - $curLink = $this->message['cur']; - } + } else if( in_array( $rc_type, array(RC_NEW,RC_LOG,RC_MOVE,RC_MOVE_OVER_REDIRECT) ) ) { + $curLink = ($rc_type != RC_NEW) ? $this->message['cur'] : $curLink; $diffLink = $this->message['diff']; } else { $diffLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['diff'], @@ -519,9 +571,9 @@ class EnhancedChangesList extends ChangesList { } # Make "last" link - if( !$showdifflinks ) { + if( !$showdifflinks || !$rc_last_oldid ) { $lastLink = $this->message['last']; - } else if( $rc_last_oldid == 0 || $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { + } else if( $rc_type == RC_LOG || $rc_type == RC_MOVE || $rc_type == RC_MOVE_OVER_REDIRECT ) { $lastLink = $this->message['last']; } else { $lastLink = $this->skin->makeKnownLinkObj( $rc->getTitle(), $this->message['last'], @@ -530,7 +582,7 @@ class EnhancedChangesList extends ChangesList { # Make user links if( $this->isDeleted($rc,Revision::DELETED_USER) ) { - $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-user') . '</span>'; + $rc->userlink = ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>'; } else { $rc->userlink = $this->skin->userLink( $rc_user, $rc_user_text ); $rc->usertalklink = $this->skin->userToolLinks( $rc_user, $rc_user_text ); @@ -555,8 +607,12 @@ class EnhancedChangesList extends ChangesList { if( !isset( $this->rc_cache[$secureName] ) ) { $this->rc_cache[$secureName] = array(); } + array_push( $this->rc_cache[$secureName], $rc ); } + + wfProfileOut( __METHOD__ ); + return $ret; } @@ -565,6 +621,9 @@ class EnhancedChangesList extends ChangesList { */ protected function recentChangesBlockGroup( $block ) { global $wgLang, $wgContLang, $wgRCShowChangedSize; + + wfProfileIn( __METHOD__ ); + $r = '<table cellpadding="0" cellspacing="0" border="0" style="background: none"><tr>'; # Collate list of users @@ -630,10 +689,10 @@ class EnhancedChangesList extends ChangesList { # onclick handler to toggle hidden/expanded $toggleLink = "onclick='toggleVisibility($jsid); return false'"; # Title for <a> tags - $expandTitle = htmlspecialchars( wfMsg('rc-enhanced-expand') ); - $closeTitle = htmlspecialchars( wfMsg('rc-enhanced-hide') ); + $expandTitle = htmlspecialchars( wfMsg( 'rc-enhanced-expand' ) ); + $closeTitle = htmlspecialchars( wfMsg( 'rc-enhanced-hide' ) ); - $tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>"; + $tl = "<span id='mw-rc-openarrow-$jsid' class='mw-changeslist-expanded' style='visibility:hidden'><a href='#' $toggleLink title='$expandTitle'>" . $this->sideArrow() . "</a></span>"; $tl .= "<span id='mw-rc-closearrow-$jsid' class='mw-changeslist-hidden' style='display:none'><a href='#' $toggleLink title='$closeTitle'>" . $this->downArrow() . "</a></span>"; $r .= '<td valign="top" style="white-space: nowrap"><tt>'.$tl.' '; @@ -645,7 +704,7 @@ class EnhancedChangesList extends ChangesList { # Article link if( $namehidden ) { - $r .= ' <span class="history-deleted">' . wfMsgHtml('rev-deleted-event') . '</span>'; + $r .= ' <span class="history-deleted">' . wfMsgHtml( 'rev-deleted-event' ) . '</span>'; } else if( $allLogs ) { $r .= $this->maybeWatchedLink( $block[0]->link, $block[0]->watched ); } else { @@ -665,7 +724,7 @@ class EnhancedChangesList extends ChangesList { $r .= ' '; if( !$allLogs ) { $r .= '('; - if( !ChangesList::userCan($rcObj,Revision::DELETED_TEXT) ) { + if( !ChangesList::userCan( $rcObj, Revision::DELETED_TEXT ) ) { $r .= $nchanges[$n]; } else if( $isnew ) { $r .= $nchanges[$n]; @@ -720,6 +779,8 @@ class EnhancedChangesList extends ChangesList { # Extract fields from DB into the function scope (rc_xxxx variables) // FIXME: Would be good to replace this extract() call with something // that explicitly initializes variables. + # Classes to apply -- TODO implement + $classes = array(); extract( $rcObj->mAttribs ); #$r .= '<tr><td valign="top">'.$this->spacerArrow(); @@ -765,9 +826,14 @@ class EnhancedChangesList extends ChangesList { $r .= $rcObj->userlink; $r .= $rcObj->usertalklink; // log action - parent::insertAction( $r, $rcObj ); + $this->insertAction( $r, $rcObj ); // log comment - parent::insertComment( $r, $rcObj ); + $this->insertComment( $r, $rcObj ); + # Rollback + $this->insertRollback( $r, $rcObj ); + # Tags + $this->insertTags( $r, $rcObj, $classes ); + # Mark revision as deleted if( !$rc_log_type && $this->isDeleted($rcObj,Revision::DELETED_TEXT) ) { $r .= ' <tt>' . wfMsgHtml( 'deletedrev' ) . '</tt>'; @@ -778,6 +844,9 @@ class EnhancedChangesList extends ChangesList { $r .= "</table></div>\n"; $this->rcCacheIndex++; + + wfProfileOut( __METHOD__ ); + return $r; } @@ -804,7 +873,7 @@ class EnhancedChangesList extends ChangesList { protected function sideArrow() { global $wgContLang; $dir = $wgContLang->isRTL() ? 'l' : 'r'; - return $this->arrow( $dir, '+', wfMsg('rc-enhanced-expand') ); + return $this->arrow( $dir, '+', wfMsg( 'rc-enhanced-expand' ) ); } /** @@ -813,7 +882,7 @@ class EnhancedChangesList extends ChangesList { * @return string HTML <img> tag */ protected function downArrow() { - return $this->arrow( 'd', '-', wfMsg('rc-enhanced-hide') ); + return $this->arrow( 'd', '-', wfMsg( 'rc-enhanced-hide' ) ); } /** @@ -838,9 +907,13 @@ class EnhancedChangesList extends ChangesList { */ protected function recentChangesBlockLine( $rcObj ) { global $wgContLang, $wgRCShowChangedSize; + + wfProfileIn( __METHOD__ ); + # Extract fields from DB into the function scope (rc_xxxx variables) // FIXME: Would be good to replace this extract() call with something // that explicitly initializes variables. + $classes = array(); // TODO implement extract( $rcObj->mAttribs ); $curIdEq = "curid={$rc_cur_id}"; @@ -864,7 +937,7 @@ class EnhancedChangesList extends ChangesList { # Diff and hist links if ( $rc_type != RC_LOG ) { $r .= ' ('. $rcObj->difflink . $this->message['semicolon-separator']; - $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), wfMsg( 'hist' ), + $r .= $this->skin->makeKnownLinkObj( $rcObj->getTitle(), $this->message['hist'], $curIdEq.'&action=history' ) . ')'; } $r .= ' . . '; @@ -883,19 +956,17 @@ class EnhancedChangesList extends ChangesList { $this->skin, LogPage::extractParams($rc_params), true, true ); } } - # Edit or log comment - if( $rc_type != RC_MOVE && $rc_type != RC_MOVE_OVER_REDIRECT ) { - // log comment - if ( $this->isDeleted($rcObj,LogPage::DELETED_COMMENT) ) { - $r .= ' <span class="history-deleted">' . wfMsg('rev-deleted-comment') . '</span>'; - } else { - $r .= $this->skin->commentBlock( $rc_comment, $rcObj->getTitle() ); - } - } + $this->insertComment( $r, $rcObj ); + $this->insertRollback( $r, $rcObj ); + # Tags + $this->insertTags( $r, $rcObj, $classes ); # Show how many people are watching this if enabled $r .= $this->numberofWatchingusers($rcObj->numberofWatchingusers); $r .= "</td></tr></table>\n"; + + wfProfileOut( __METHOD__ ); + return $r; } @@ -907,6 +978,9 @@ class EnhancedChangesList extends ChangesList { if( count ( $this->rc_cache ) == 0 ) { return ''; } + + wfProfileIn( __METHOD__ ); + $blockOut = ''; foreach( $this->rc_cache as $block ) { if( count( $block ) < 2 ) { @@ -915,6 +989,9 @@ class EnhancedChangesList extends ChangesList { $blockOut .= $this->recentChangesBlockGroup( $block ); } } + + wfProfileOut( __METHOD__ ); + return '<div>'.$blockOut.'</div>'; } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index ed68fe7a..19878f76 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -33,7 +33,7 @@ if ( !defined( 'MW_PHP4' ) ) { } /** MediaWiki version number */ -$wgVersion = '1.14.0'; +$wgVersion = '1.15.0'; /** Name of the site. It must be changed in LocalSettings.php */ $wgSitename = 'MediaWiki'; @@ -224,6 +224,10 @@ $wgFileStore['deleted']['hash'] = 3; ///< 3-level subdirectory split * equivalent to the corresponding member of $wgDBservers * tablePrefix Table prefix, the foreign wiki's $wgDBprefix * hasSharedCache True if the wiki's shared cache is accessible via the local $wgMemc + * + * ForeignAPIRepo: + * apibase Use for the foreign API's URL + * apiThumbCacheExpiry How long to locally cache thumbs for * * The default is to initialise these arrays from the MW<1.11 backwards compatible settings: * $wgUploadPath, $wgThumbnailScriptPath, $wgSharedUploadDirectory, etc. @@ -274,7 +278,8 @@ $wgUrlProtocols = array( 'nntp://', // @bug 3808 RFC 1738 'worldwind://', 'mailto:', - 'news:' + 'news:', + 'svn://', ); /** internal name of virus scanner. This servers as a key to the $wgAntivirusSetup array. @@ -521,6 +526,11 @@ $wgUserEmailUseReplyTo = false; $wgPasswordReminderResendTime = 24; /** + * The time, in seconds, when an emailed temporary password expires. + */ +$wgNewPasswordExpiry = 3600 * 24 * 7; + +/** * SMTP Mode * For using a direct (authenticated) SMTP server connection. * Default to false or fill an array : @@ -977,7 +987,7 @@ $wgReadOnly = null; $wgReadOnlyFile = false; ///< defaults to "{$wgUploadDirectory}/lock_yBgMBwiR"; /** - * Filename for debug logging. + * Filename for debug logging. See http://www.mediawiki.org/wiki/How_to_debug * The debug log file should be not be publicly accessible if it is used, as it * may contain private data. */ @@ -1029,6 +1039,13 @@ $wgDebugDumpSql = false; $wgDebugLogGroups = array(); /** + * Display debug data at the bottom of the main content area. + * + * Useful for developers and technical users trying to working on a closed wiki. + */ +$wgShowDebug = false; + +/** * Show the contents of $wgHooks in Special:Version */ $wgSpecialVersionShowHooks = false; @@ -1240,6 +1257,8 @@ $wgGroupPermissions['bureaucrat']['userrights'] = true; $wgGroupPermissions['bureaucrat']['noratelimit'] = true; // Permission to change users' groups assignments across wikis #$wgGroupPermissions['bureaucrat']['userrights-interwiki'] = true; +// Permission to export pages including linked pages regardless of $wgExportMaxLinkDepth +#$wgGroupPermissions['bureaucrat']['override-export-depth'] = true; #$wgGroupPermissions['sysop']['deleterevision'] = true; // To hide usernames from users and Sysops @@ -1287,7 +1306,7 @@ $wgGroupsRemoveFromSelf = array(); /** * Set of available actions that can be restricted via action=protect * You probably shouldn't change this. - * Translated trough restriction-* messages. + * Translated through restriction-* messages. */ $wgRestrictionTypes = array( 'edit', 'move' ); @@ -1349,6 +1368,10 @@ $wgAutoConfirmCount = 0; * array( APCOND_EMAILCONFIRMED ), *OR* * array( APCOND_EDITCOUNT, number of edits ), *OR* * array( APCOND_AGE, seconds since registration ), *OR* + * array( APCOND_INGROUPS, group1, group2, ... ), *OR* + * array( APCOND_ISIP, ip ), *OR* + * array( APCOND_IPINRANGE, range ), *OR* + * array( APCOND_AGE_FROM_EDIT, seconds since first edit ), *OR* * similar constructs defined by extensions. * * If $wgEmailAuthentication is off, APCOND_EMAILCONFIRMED will be true for any @@ -1446,7 +1469,7 @@ $wgCacheEpoch = '20030516000000'; * to ensure that client-side caches don't keep obsolete copies of global * styles. */ -$wgStyleVersion = '195'; +$wgStyleVersion = '207'; # Server-side caching: @@ -1974,6 +1997,7 @@ $wgMediaHandlers = array( 'image/jpeg' => 'BitmapHandler', 'image/png' => 'BitmapHandler', 'image/gif' => 'BitmapHandler', + 'image/tiff' => 'TiffHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', 'image/svg+xml' => 'SvgHandler', // official @@ -2052,6 +2076,16 @@ $wgMaxImageArea = 1.25e7; */ $wgMaxAnimatedGifArea = 1.0e6; /** + * Browsers don't support TIFF inline generally... + * For inline display, we need to convert to PNG or JPEG. + * Note scaling should work with ImageMagick, but may not with GD scaling. + * // PNG is lossless, but inefficient for photos + * $wgTiffThumbnailType = array( 'png', 'image/png' ); + * // JPEG is good for photos, but has no transparency support. Bad for diagrams. + * $wgTiffThumbnailType = array( 'jpg', 'image/jpeg' ); + */ +$wgTiffThumbnailType = false; +/** * If rendered thumbnail files are older than this timestamp, they * will be rerendered on demand as if the file didn't already exist. * Update if there is some need to force thumbs and SVG rasterizations @@ -2081,10 +2115,9 @@ $wgIgnoreImageErrors = false; */ $wgGenerateThumbnailOnParse = true; -/** Obsolete, always true, kept for compatibility with extensions */ +/** Whether or not to use image resizing */ $wgUseImageResize = true; - /** Set $wgCommandLineMode if it's not set already, to avoid notices */ if( !isset( $wgCommandLineMode ) ) { $wgCommandLineMode = false; @@ -2252,9 +2285,26 @@ $wgExportMaxHistory = 0; $wgExportAllowListContributors = false ; +/** + * If non-zero, Special:Export accepts a "pagelink-depth" parameter + * up to this specified level, which will cause it to include all + * pages linked to from the pages you specify. Since this number + * can become *insanely large* and could easily break your wiki, + * it's disabled by default for now. + * + * There's a HARD CODED limit of 5 levels of recursion to prevent a + * crazy-big export from being done by someone setting the depth + * number too high. In other words, last resort safety net. + */ +$wgExportMaxLinkDepth = 0; /** - * Edits matching these regular expressions in body text or edit summary + * Whether to allow the "export all pages in namespace" option + */ +$wgExportFromNamespaces = false; + +/** + * Edits matching these regular expressions in body text * will be recognised as spam and rejected automatically. * * There's no administrator override on-wiki, so be careful what you set. :) @@ -2264,6 +2314,9 @@ $wgExportAllowListContributors = false ; */ $wgSpamRegex = array(); +/** Same as the above except for edit summaries */ +$wgSummarySpamRegex = array(); + /** Similarly you can get a function to do the job. The function will be given * the following args: * - a Title object for the article the edit is made on @@ -2374,6 +2427,8 @@ $wgDefaultUserOptions = array( 'rclimit' => 50, 'wllimit' => 250, 'hideminor' => 0, + 'hidepatrolled' => 0, + 'newpageshidepatrolled' => 0, 'highlightbroken' => 1, 'stubthreshold' => 0, 'previewontop' => 1, @@ -2413,11 +2468,13 @@ $wgDefaultUserOptions = array( 'watchlisthideown' => 0, 'watchlisthideanons' => 0, 'watchlisthideliu' => 0, + 'watchlisthidepatrolled' => 0, 'watchcreations' => 0, 'watchdefault' => 0, 'watchmoves' => 0, 'watchdeletion' => 0, 'noconvertlink' => 0, + 'gender' => 'unknown', ); /** Whether or not to allow and use real name fields. Defaults to true. */ @@ -2504,7 +2561,7 @@ $wgAutoloadClasses = array(); * $wgExtensionCredits[$type][] = array( * 'name' => 'Example extension', * 'version' => 1.9, - * 'svn-revision' => '$LastChangedRevision: 47653 $', + * 'svn-revision' => '$LastChangedRevision: 51678 $', * 'author' => 'Foo Barstein', * 'url' => 'http://wwww.example.com/Example%20Extension/', * 'description' => 'An example extension', @@ -2724,6 +2781,9 @@ $wgBrowserBlackList = array( * * This variable is currently used ONLY for signature formatting, not for * anything else. + * + * Timezones can be translated by editing MediaWiki messages of type + * timezone-nameinlowercase like timezone-utc. */ # $wgLocaltimezone = 'GMT'; # $wgLocaltimezone = 'PST8PDT'; @@ -2754,17 +2814,17 @@ $wgLocalTZoffset = null; /** - * When translating messages with wfMsg(), it is not always clear what should be - * considered UI messages and what shoud be content messages. + * When translating messages with wfMsg(), it is not always clear what should + * be considered UI messages and what should be content messages. * - * For example, for regular wikipedia site like en, there should be only one - * 'mainpage', therefore when getting the link of 'mainpage', we should treate - * it as content of the site and call wfMsgForContent(), while for rendering the - * text of the link, we call wfMsg(). The code in default behaves this way. - * However, sites like common do offer different versions of 'mainpage' and the - * like for different languages. This array provides a way to override the - * default behavior. For example, to allow language specific mainpage and - * community portal, set + * For example, for the English Wikipedia, there should be only one 'mainpage', + * so when getting the link for 'mainpage', we should treat it as site content + * and call wfMsgForContent(), but for rendering the text of the link, we call + * wfMsg(). The code behaves this way by default. However, sites like the + * Wikimedia Commons do offer different versions of 'mainpage' and the like for + * different languages. This array provides a way to override the default + * behavior. For example, to allow language-specific main page and community + * portal, set * * $wgForceUIMsgAsContentMsg = array( 'mainpage', 'portal-url' ); */ @@ -2965,6 +3025,7 @@ $wgSpecialPageGroups = array( 'Newimages' => 'changes', 'Newpages' => 'changes', 'Log' => 'changes', + 'Tags' => 'changes', 'Upload' => 'media', 'Listfiles' => 'media', @@ -3073,6 +3134,19 @@ $wgNoFollowLinks = true; $wgNoFollowNsExceptions = array(); /** + * If this is set to an array of domains, external links to these domain names + * (or any subdomains) will not be set to rel="nofollow" regardless of the + * value of $wgNoFollowLinks. For instance: + * + * $wgNoFollowDomainExceptions = array( 'en.wikipedia.org', 'wiktionary.org' ); + * + * This would add rel="nofollow" to links to de.wikipedia.org, but not + * en.wikipedia.org, wiktionary.org, en.wiktionary.org, us.en.wikipedia.org, + * etc. + */ +$wgNoFollowDomainExceptions = array(); + +/** * Default robot policy. The default policy is to encourage indexing and fol- * lowing of links. It may be overridden on a per-namespace and/or per-page * basis. @@ -3216,6 +3290,12 @@ $wgRateLimitLog = null; $wgRateLimitsExcludedGroups = array(); /** + * Array of IPs which should be excluded from rate limits. + * This may be useful for whitelisting NAT gateways for conferences, etc. + */ +$wgRateLimitsExcludedIPs = array(); + +/** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. */ @@ -3490,6 +3570,18 @@ $wgAPIListModules = array(); $wgAPIMaxDBRows = 5000; /** + * The maximum size (in bytes) of an API result. + * Don't set this lower than $wgMaxArticleSize*1024 + */ +$wgAPIMaxResultSize = 8388608; + +/** + * The maximum number of uncached diffs that can be retrieved in one API + * request. Set this to 0 to disable API diffs altogether + */ +$wgAPIMaxUncachedDiffs = 1; + +/** * Parser test suite files to be run by parserTests.php when no specific * filename is passed to it. * @@ -3600,6 +3692,25 @@ $wgMaximumMovedPages = 100; $wgFixDoubleRedirects = false; /** + * Max number of redirects to follow when resolving redirects. + * 1 means only the first redirect is followed (default behavior). + * 0 or less means no redirects are followed. + */ +$wgMaxRedirects = 1; + +/** + * Array of invalid page redirect targets. + * Attempting to create a redirect to any of the pages in this array + * will make the redirect fail. + * Userlogout is hard-coded, so it does not need to be listed here. + * (bug 10569) Disallow Mypage and Mytalk as well. + * + * As of now, this only checks special pages. Redirects to pages in + * other namespaces cannot be invalidated by this variable. + */ +$wgInvalidRedirectTargets = array( 'Filepath', 'Mypage', 'Mytalk' ); + +/** * Array of namespaces to generate a sitemap for when the * maintenance/generateSitemap.php script is run, or false if one is to be ge- * nerated for all namespaces. @@ -3626,10 +3737,10 @@ $wgPasswordAttemptThrottle = array( 'count' => 5, 'seconds' => 300 ); $wgEdititis = false; /** -* Enable the UniversalEditButton for browsers that support it -* (currently only Firefox with an extension) -* See http://universaleditbutton.org for more background information -*/ + * Enable the UniversalEditButton for browsers that support it + * (currently only Firefox with an extension) + * See http://universaleditbutton.org for more background information + */ $wgUniversalEditButton = true; /** @@ -3638,3 +3749,45 @@ $wgUniversalEditButton = true; * and the functionality will be enabled universally. */ $wgEnforceHtmlIds = true; + +/** + * Search form behavior + * true = use Go & Search buttons + * false = use Go button & Advanced search link + */ +$wgUseTwoButtonsSearchForm = true; + +/** + * Preprocessor caching threshold + */ +$wgPreprocessorCacheThreshold = 1000; + +/** + * Allow filtering by change tag in recentchanges, history, etc + * Has no effect if no tags are defined in valid_tag. + */ +$wgUseTagFilter = true; + +/** + * Allow redirection to another page when a user logs in. + * To enable, set to a string like 'Main Page' + */ +$wgRedirectOnLogin = null; + +/** + * Characters to prevent during new account creations. + * This is used in a regular expression character class during + * registration (regex metacharacters like / are escaped). + */ +$wgInvalidUsernameCharacters = '@'; + +/** + * Character used as a delimiter when testing for interwiki userrights + * (In Special:UserRights, it is possible to modify users on different + * databases if the delimiter is used, e.g. Someuser@enwiki). + * + * It is recommended that you have this delimiter in + * $wgInvalidUsernameCharacters above, or you will not be able to + * modify the user rights of those users via Special:UserRights + */ +$wgUserrightsInterwikiDelimiter = '@'; diff --git a/includes/EditPage.php b/includes/EditPage.php index 0193dc38..3589b52d 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -84,6 +84,7 @@ class EditPage { /* $didSave should be set to true whenever an article was succesfully altered. */ public $didSave = false; + public $undidRev = 0; public $suppressIntro = false; @@ -164,35 +165,28 @@ class EditPage { $undorev->getPage() == $this->mArticle->getID() && !$undorev->isDeleted( Revision::DELETED_TEXT ) && !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - $undorev_text = $undorev->getText(); - $oldrev_text = $oldrev->getText(); - $currev_text = $text; - - if ( $currev_text != $undorev_text ) { - $result = wfMerge( $undorev_text, $oldrev_text, $currev_text, $text ); + + $undotext = $this->mArticle->getUndoText( $undorev, $oldrev ); + if ( $undotext === false ) { + # Warn the user that something went wrong + $this->editFormPageTop .= $wgOut->parse( '<div class="error mw-undo-failure">' . wfMsgNoTrans( 'undo-failure' ) . '</div>' ); } else { - # No use doing a merge if it's just a straight revert. - $text = $oldrev_text; - $result = true; - } - if ( $result ) { + $text = $undotext; # Inform the user of our success and set an automatic edit summary - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-success' ) ); + $this->editFormPageTop .= $wgOut->parse( '<div class="mw-undo-success">' . wfMsgNoTrans( 'undo-success' ) . '</div>' ); $firstrev = $oldrev->getNext(); # If we just undid one rev, use an autosummary if ( $firstrev->mId == $undo ) { - $this->summary = wfMsgForContent('undo-summary', $undo, $undorev->getUserText()); + $this->summary = wfMsgForContent( 'undo-summary', $undo, $undorev->getUserText() ); + $this->undidRev = $undo; } $this->formtype = 'diff'; - } else { - # Warn the user that something went wrong - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-failure' ) ); } } else { // Failed basic sanity checks. // Older revisions may have been removed since the link // was created, or we may simply have got bogus input. - $this->editFormPageTop .= $wgOut->parse( wfMsgNoTrans( 'undo-norev' ) ); + $this->editFormPageTop .= $wgOut->parse( '<div class="error mw-undo-norev">' . wfMsgNoTrans( 'undo-norev' ) . '</div>' ); } } else if ( $section != '' ) { if ( $section == 'new' ) { @@ -330,7 +324,7 @@ class EditPage { protected function wasDeletedSinceLastEdit() { if ( $this->deletedSinceEdit ) return true; - if ( $this->mTitle->isDeleted() ) { + if ( $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); @@ -409,6 +403,11 @@ class EditPage { } } } + + // If they used redlink=1 and the page exists, redirect to the main article + if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) { + $wgOut->redirect( $this->mTitle->getFullURL() ); + } wfProfileIn( __METHOD__."-business-end" ); @@ -427,7 +426,6 @@ class EditPage { # Optional notices on a per-namespace and per-page basis $editnotice_ns = 'editnotice-'.$this->mTitle->getNamespace(); - $editnotice_page = $editnotice_ns.'-'.$this->mTitle->getDBkey(); if ( !wfEmptyMsg( $editnotice_ns, wfMsgForContent( $editnotice_ns ) ) ) { $wgOut->addWikiText( wfMsgForContent( $editnotice_ns ) ); } @@ -440,8 +438,6 @@ class EditPage { $wgOut->addWikiText( wfMsgForContent( $editnotice_base ) ); } } - } else if ( !wfEmptyMsg( $editnotice_page, wfMsgForContent( $editnotice_page ) ) ) { - $wgOut->addWikiText( wfMsgForContent( $editnotice_page ) ); } # Attempt submission here. This will check for edit conflicts, @@ -529,7 +525,7 @@ class EditPage { } elseif ( $this->section == 'new' ) { // Nothing *to* preview for new sections return false; - } elseif ( ( $wgRequest->getVal( 'preload' ) !== '' || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { + } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() ) && $wgUser->getOption( 'previewonfirst' ) ) { // Standard preference behaviour return true; } elseif ( !$this->mTitle->exists() && $this->mTitle->getNamespace() == NS_CATEGORY ) { @@ -560,7 +556,7 @@ class EditPage { $this->textbox2 = $this->safeUnicodeInput( $request, 'wpTextbox2' ); $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); # Truncate for whole multibyte characters. +5 bytes for ellipsis - $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250 ); + $this->summary = $wgLang->truncate( $request->getText( 'wpSummary' ), 250, '' ); # Remove extra headings from summaries and new sections. $this->summary = preg_replace('/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary); @@ -574,7 +570,7 @@ class EditPage { # If the form is incomplete, force to preview. wfDebug( "$fname: Form data appears to be incomplete\n" ); wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" ); - $this->preview = true; + $this->preview = true; } else { /* Fallback for live preview */ $this->preview = $request->getCheck( 'wpPreview' ) || $request->getCheck( 'wpLivePreview' ); @@ -644,6 +640,13 @@ class EditPage { if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) { $this->summary = $request->getVal( 'preloadtitle' ); } + elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) { + $this->summary = $request->getText( 'summary' ); + } + + if ( $request->getVal( 'minor' ) ) { + $this->minoredit = true; + } } $this->oldid = $request->getInt( 'oldid' ); @@ -677,8 +680,16 @@ class EditPage { if ( $this->suppressIntro ) { return; } + + $namespace = $this->mTitle->getNamespace(); + + if ( $namespace == NS_MEDIAWIKI ) { + # Show a warning if editing an interface message + $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1</div>", 'editinginterface' ); + } + # Show a warning message when someone creates/edits a user (talk) page but the user does not exists - if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) { + if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) { $parts = explode( '/', $this->mTitle->getText(), 2 ); $username = $parts[0]; $id = User::idFromName( $username ); @@ -737,7 +748,7 @@ class EditPage { if ( !wfRunHooks( 'EditPage::attemptSave', array( &$this ) ) ) { - wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" ); + wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" ); return self::AS_HOOK_ERROR; } @@ -757,7 +768,7 @@ class EditPage { $this->mMetaData = '' ; # Check for spam - $match = self::matchSpamRegex( $this->summary ); + $match = self::matchSummarySpamRegex( $this->summary ); if ( $match === false ) { $match = self::matchSpamRegex( $this->textbox1 ); } @@ -859,11 +870,20 @@ class EditPage { wfProfileOut( $fname ); return self::AS_HOOK_ERROR; } + + # Handle the user preference to force summaries here. Check if it's not a redirect. + if ( !$this->allowBlankSummary && !Title::newFromRedirect( $this->textbox1 ) ) { + if ( md5( $this->summary ) == $this->autoSumm ) { + $this->missingSummary = true; + wfProfileOut( $fname ); + return self::AS_SUMMARY_NEEDED; + } + } $isComment = ( $this->section == 'new' ); $this->mArticle->insertNewArticle( $this->textbox1, $this->summary, - $this->minoredit, $this->watchthis, false, $isComment, $bot); + $this->minoredit, $this->watchthis, false, $isComment, $bot ); wfProfileOut( $fname ); return self::AS_SUCCESS_NEW_ARTICLE; @@ -893,39 +913,35 @@ class EditPage { } } $userid = $wgUser->getId(); + + # Suppress edit conflict with self, except for section edits where merging is required. + if ( $this->isConflict && $this->section == '' && $this->userWasLastToEdit($userid,$this->edittime) ) { + wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); + $this->isConflict = false; + } if ( $this->isConflict ) { wfDebug( "EditPage::editForm conflict! getting section '$this->section' for time '$this->edittime' (article time '" . $this->mArticle->getTimestamp() . "')\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime); - } - else { + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary, $this->edittime ); + } else { wfDebug( "EditPage::editForm getting section '$this->section'\n" ); - $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary); + $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $this->summary ); } if ( is_null( $text ) ) { wfDebug( "EditPage::editForm activating conflict; section replace failed.\n" ); $this->isConflict = true; - $text = $this->textbox1; - } - - # Suppress edit conflict with self, except for section edits where merging is required. - if ( $this->section == '' && $userid && $this->userWasLastToEdit($userid,$this->edittime) ) { - wfDebug( "EditPage::editForm Suppressing edit conflict, same user.\n" ); - $this->isConflict = false; - } else { - # switch from section editing to normal editing in edit conflict - if ( $this->isConflict ) { - # Attempt merge - if ( $this->mergeChangesInto( $text ) ) { - // Successful merge! Maybe we should tell the user the good news? - $this->isConflict = false; - wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); - } else { - $this->section = ''; - $this->textbox1 = $text; - wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" ); - } + $text = $this->textbox1; // do not try to merge here! + } else if ( $this->isConflict ) { + # Attempt merge + if ( $this->mergeChangesInto( $text ) ) { + // Successful merge! Maybe we should tell the user the good news? + $this->isConflict = false; + wfDebug( "EditPage::editForm Suppressing edit conflict, successful merge.\n" ); + } else { + $this->section = ''; + $this->textbox1 = $text; + wfDebug( "EditPage::editForm Keeping edit conflict, failed merge.\n" ); } } @@ -944,9 +960,9 @@ class EditPage { } # Handle the user preference to force summaries here, but not for null edits - if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext, $text) && - !is_object( Title::newFromRedirect( $text ) ) # check if it's not a redirect - ) { + if ( $this->section != 'new' && !$this->allowBlankSummary && 0 != strcmp($oldtext,$text) + && !Title::newFromRedirect( $text ) ) # check if it's not a redirect + { if ( md5( $this->summary ) == $this->autoSumm ) { $this->missingSummary = true; wfProfileOut( $fname ); @@ -1008,7 +1024,8 @@ class EditPage { # update the article here if ( $this->mArticle->updateArticle( $text, $this->summary, $this->minoredit, - $this->watchthis, $bot, $sectionanchor ) ) { + $this->watchthis, $bot, $sectionanchor ) ) + { wfProfileOut( $fname ); return self::AS_SUCCESS_UPDATE; } else { @@ -1024,6 +1041,7 @@ class EditPage { * 50 revisions for the sake of performance. */ protected function userWasLastToEdit( $id, $edittime ) { + if( !$id ) return false; $dbw = wfGetDB( DB_MASTER ); $res = $dbw->select( 'revision', 'rev_user', @@ -1047,14 +1065,26 @@ class EditPage { */ public static function matchSpamRegex( $text ) { global $wgSpamRegex; - if ( $wgSpamRegex ) { - // For back compatibility, $wgSpamRegex may be a single string or an array of regexes. - $regexes = (array)$wgSpamRegex; - foreach( $regexes as $regex ) { - $matches = array(); - if ( preg_match( $regex, $text, $matches ) ) { - return $matches[0]; - } + // For back compatibility, $wgSpamRegex may be a single string or an array of regexes. + $regexes = (array)$wgSpamRegex; + return self::matchSpamRegexInternal( $text, $regexes ); + } + + /** + * Check given input text against $wgSpamRegex, and return the text of the first match. + * @return mixed -- matching string or false + */ + public static function matchSummarySpamRegex( $text ) { + global $wgSummarySpamRegex; + $regexes = (array)$wgSummarySpamRegex; + return self::matchSpamRegexInternal( $text, $regexes ); + } + + protected static function matchSpamRegexInternal( $text, $regexes ) { + foreach( $regexes as $regex ) { + $matches = array(); + if( preg_match( $regex, $text, $matches ) ) { + return $matches[0]; } } return false; @@ -1133,7 +1163,7 @@ class EditPage { $wgOut->setArticleRelated( true ); if ( $this->isConflict ) { - $wgOut->addWikiMsg( 'explainconflict' ); + $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1</div>", 'explainconflict' ); $this->textbox2 = $this->textbox1; $this->textbox1 = $this->getContent(); @@ -1142,9 +1172,7 @@ class EditPage { if ( $this->section != '' && $this->section != 'new' ) { $matches = array(); if ( !$this->summary && !$this->preview && !$this->diff ) { - preg_match( "/^(=+)(.+)\\1/mi", - $this->textbox1, - $matches ); + preg_match( "/^(=+)(.+)\\1/mi", $this->textbox1, $matches ); if ( !empty( $matches[2] ) ) { global $wgParser; $this->summary = "/* " . @@ -1155,7 +1183,7 @@ class EditPage { } if ( $this->missingComment ) { - $wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>', 'missingcommenttext' ); + $wgOut->wrapWikiMsg( '<div id="mw-missingcommenttext">$1</div>', 'missingcommenttext' ); } if ( $this->missingSummary && $this->section != 'new' ) { @@ -1177,9 +1205,9 @@ class EditPage { // Let sysop know that this will make private content public if saved if ( !$this->mArticle->mRevision->userCan( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-permission' ); + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' ); } else if ( $this->mArticle->mRevision->isDeleted( Revision::DELETED_TEXT ) ) { - $wgOut->addWikiMsg( 'rev-deleted-text-view' ); + $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' ); } if ( !$this->mArticle->mRevision->isCurrent() ) { @@ -1208,8 +1236,6 @@ class EditPage { $classes = array(); // Textarea CSS if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { - # Show a warning if editing an interface message - $wgOut->addWikiMsg( 'editinginterface' ); } elseif ( $this->mTitle->isProtected( 'edit' ) ) { # Is the title semi-protected? if ( $this->mTitle->isSemiProtected() ) { @@ -1228,17 +1254,19 @@ class EditPage { if ( $this->mTitle->isCascadeProtected() ) { # Is this page under cascading protection from some source pages? list($cascadeSources, /* $restrictions */) = $this->mTitle->getCascadeProtectionSources(); - $notice = "$1\n"; - if ( count($cascadeSources) > 0 ) { + $notice = "<div class='mw-cascadeprotectedwarning'>$1\n"; + $cascadeSourcesCount = count( $cascadeSources ); + if ( $cascadeSourcesCount > 0 ) { # Explain, and list the titles responsible foreach( $cascadeSources as $page ) { $notice .= '* [[:' . $page->getPrefixedText() . "]]\n"; } } - $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', count($cascadeSources) ) ); + $notice .= '</div>'; + $wgOut->wrapWikiMsg( $notice, array( 'cascadeprotectedwarning', $cascadeSourcesCount ) ); } if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) { - $wgOut->addWikiMsg( 'titleprotectedwarning' ); + $wgOut->wrapWikiMsg( '<div class="mw-titleprotectedwarning">$1</div>', 'titleprotectedwarning' ); } if ( $this->kblength === false ) { @@ -1263,6 +1291,7 @@ class EditPage { $cancel = $sk->makeKnownLink( $wgTitle->getPrefixedText(), wfMsgExt('cancel', array('parseinline')) ); + $separator = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); $edithelpurl = Skin::makeInternalOrExternalUrl( wfMsgForContent( 'edithelppage' )); $edithelp = '<a target="helpwindow" href="'.$edithelpurl.'">'. htmlspecialchars( wfMsg( 'edithelp' ) ).'</a> '. @@ -1318,7 +1347,7 @@ class EditPage { # if this is a comment, show a subject line at the top, which is also the edit summary. # Otherwise, show a summary field at the bottom - $summarytext = htmlspecialchars( $wgContLang->recodeForEdit( $this->summary ) ); # FIXME + $summarytext = $wgContLang->recodeForEdit( $this->summary ); # If a blank edit summary was previously provided, and the appropriate # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the @@ -1332,7 +1361,26 @@ class EditPage { $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary ); $summaryhiddens .= Xml::hidden( 'wpAutoSummary', $autosumm ); if ( $this->section == 'new' ) { - $commentsubject="<span id='wpSummaryLabel'><label for='wpSummary'>{$subject}</label></span>\n<input tabindex='1' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; + $commentsubject = ''; + if ( !$wgRequest->getBool( 'nosummary' ) ) { + # Add a class if 'missingsummary' is triggered to allow styling of the summary line + $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; + + $commentsubject = + Xml::tags( 'label', array( 'for' => 'wpSummary' ), $subject ); + $commentsubject = + Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), + $commentsubject ); + $commentsubject .= ' '; + $commentsubject .= Xml::input( 'wpSummary', + 60, + $summarytext, + array( + 'id' => 'wpSummary', + 'maxlength' => '200', + 'tabindex' => '1' + ) ); + } $editsummary = "<div class='editOptions'>\n"; global $wgParser; $formattedSummary = wfMsgForContent( 'newsectionsummary', $wgParser->stripSectionName( $this->summary ) ); @@ -1340,10 +1388,39 @@ class EditPage { $summarypreview = ''; } else { $commentsubject = ''; - $editsummary="<div class='editOptions'>\n<span id='wpSummaryLabel'><label for='wpSummary'>{$summary}</label></span>\n<input tabindex='2' type='text' value=\"$summarytext\" name='wpSummary' id='wpSummary' maxlength='200' size='60' />{$summaryhiddens}<br />"; - $summarypreview = $summarytext && $this->preview ? "<div class=\"mw-summary-preview\">". wfMsg('summary-preview') .$sk->commentBlock( $this->summary, $this->mTitle )."</div>\n" : ''; + + # Add a class if 'missingsummary' is triggered to allow styling of the summary line + $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary'; + + $editsummary = Xml::tags( 'label', array( 'for' => 'wpSummary' ), $summary ); + $editsummary = Xml::tags( 'span', array( 'class' => $summaryClass, 'id' => "wpSummaryLabel" ), + $editsummary ) . ' '; + + $editsummary .= Xml::input( 'wpSummary', + 60, + $summarytext, + array( + 'id' => 'wpSummary', + 'maxlength' => '200', + 'tabindex' => '1' + ) ); + + // No idea where this is closed. + $editsummary = Xml::openElement( 'div', array( 'class' => 'editOptions' ) ) + . $editsummary . '<br/>'; + + $summarypreview = ''; + if ( $summarytext && $this->preview ) { + $summarypreview = + Xml::tags( 'div', + array( 'class' => 'mw-summary-preview' ), + wfMsg( 'summary-preview' ) . + $sk->commentBlock( $this->summary, $this->mTitle ) + ); + } $subjectpreview = ''; } + $commentsubject .= $summaryhiddens; # Set focus to the edit box on load, except on preview or diff, where it would interfere with the display if ( !$this->preview && !$this->diff ) { @@ -1373,15 +1450,18 @@ class EditPage { $recreate = ''; if ( $this->wasDeletedSinceLastEdit() ) { if ( 'save' != $this->formtype ) { - $wgOut->addWikiMsg('deletedwhileediting'); + $wgOut->wrapWikiMsg( + "<div class='error mw-deleted-while-editing'>\n$1</div>", + 'deletedwhileediting' ); } else { - // Hide the toolbar and edit area, use can click preview to get it back + // Hide the toolbar and edit area, user can click preview to get it back // Add an confirmation checkbox and explanation. $toolbar = ''; - $recreate = $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment )); - $recreate .= - "<br /><input tabindex='1' type='checkbox' value='1' name='wpRecreate' id='wpRecreate' />". - "<label for='wpRecreate' title='".wfMsg('tooltip-recreate')."'>". wfMsg('recreate')."</label>"; + $recreate = '<div class="mw-confirm-recreate">' . + $wgOut->parse( wfMsg( 'confirmrecreate', $this->lastDelete->user_name , $this->lastDelete->log_comment ) ) . + Xml::checkLabel( wfMsg( 'recreate' ), 'wpRecreate', 'wpRecreate', false, + array( 'title' => $sk->titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ) + ) . '</div>'; } } @@ -1436,7 +1516,7 @@ END $wgOut->addHTML( "<div class='editButtons'> {$buttonshtml} - <span class='editHelp'>{$cancel} | {$edithelp}</span> + <span class='editHelp'>{$cancel}{$separator}{$edithelp}</span> </div><!-- editButtons --> </div><!-- editOptions -->"); @@ -1606,7 +1686,7 @@ END $wgOut->addHTML( '</div>' ); } - function getLastDelete() { + protected function getLastDelete() { $dbr = wfGetDB( DB_SLAVE ); $data = $dbr->selectRow( array( 'logging', 'user' ), @@ -1618,15 +1698,23 @@ END 'log_title', 'log_comment', 'log_params', - 'user_name', ), + 'log_deleted', + 'user_name' ), array( 'log_namespace' => $this->mTitle->getNamespace(), 'log_title' => $this->mTitle->getDBkey(), 'log_type' => 'delete', 'log_action' => 'delete', 'user_id=log_user' ), __METHOD__, - array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) ); - + array( 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ) + ); + // Quick paranoid permission checks... + if( is_object($data) ) { + if( $data->log_deleted & LogPage::DELETED_USER ) + $data->user_name = wfMsgHtml('rev-deleted-user'); + if( $data->log_deleted & LogPage::DELETED_COMMENT ) + $data->log_comment = wfMsgHtml('rev-deleted-comment'); + } return $data; } @@ -1651,6 +1739,8 @@ END $parserOptions = ParserOptions::newFromUser( $wgUser ); $parserOptions->setEditSection( false ); + $parserOptions->setIsPreview( true ); + $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' ); global $wgRawHtml; if ( $wgRawHtml && !$this->mTokenOk ) { @@ -1672,7 +1762,7 @@ END $parserOptions->setTidy(true); $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions ); $previewHTML = $parserOutput->mText; - } elseif ( $rt = Title::newFromRedirect( $this->textbox1 ) ) { + } elseif ( $rt = Title::newFromRedirectArray( $this->textbox1 ) ) { $previewHTML = $this->mArticle->viewRedirect( $rt, false ); } else { $toparse = $this->textbox1; @@ -1834,8 +1924,7 @@ END $baseText = $baseRevision->getText(); // The current state, we want to merge updates into it - $currentRevision = Revision::loadFromTitle( - $db, $this->mTitle ); + $currentRevision = Revision::loadFromTitle( $db, $this->mTitle ); if ( is_null( $currentRevision ) ) { wfProfileOut( $fname ); return false; @@ -2389,7 +2478,9 @@ END global $wgUser, $wgOut, $wgTitle, $wgRequest; $resultDetails = false; - $value = $this->internalAttemptSave( $resultDetails, $wgUser->isAllowed('bot') && $wgRequest->getBool('bot', true) ); + # Allow bots to exempt some edits from bot flagging + $bot = $wgUser->isAllowed('bot') && $wgRequest->getBool('bot',true); + $value = $this->internalAttemptSave( $resultDetails, $bot ); if ( $value == self::AS_SUCCESS_UPDATE || $value == self::AS_SUCCESS_NEW_ARTICLE ) { $this->didSave = true; diff --git a/includes/EnotifNotifyJob.php b/includes/EnotifNotifyJob.php index 31fcb0d5..f7178d0f 100644 --- a/includes/EnotifNotifyJob.php +++ b/includes/EnotifNotifyJob.php @@ -26,7 +26,8 @@ class EnotifNotifyJob extends Job { $this->params['timestamp'], $this->params['summary'], $this->params['minorEdit'], - $this->params['oldid'] + $this->params['oldid'], + $this->params['watchers'] ); return true; } diff --git a/includes/Exception.php b/includes/Exception.php index eb715986..5f808b20 100644 --- a/includes/Exception.php +++ b/includes/Exception.php @@ -161,23 +161,26 @@ class MWException extends Exception { if( $hookResult = $this->runHooks( get_class( $this ) . "Raw" ) ) { die( $hookResult ); } - echo $this->htmlHeader(); - echo $this->getHTML(); - echo $this->htmlFooter(); + if ( defined( 'MEDIAWIKI_INSTALL' ) || $this->htmlBodyOnly() ) { + echo $this->getHTML(); + } else { + echo $this->htmlHeader(); + echo $this->getHTML(); + echo $this->htmlFooter(); + } } } /** * Output a report about the exception and takes care of formatting. - * It will be either HTML or plain text based on $wgCommandLineMode. + * It will be either HTML or plain text based on isCommandLine(). */ function report() { - global $wgCommandLineMode; $log = $this->getLogMessage(); if ( $log ) { wfDebugLog( 'exception', $log ); } - if ( $wgCommandLineMode ) { + if ( self::isCommandLine() ) { wfPrintError( $this->getText() ); } else { $this->reportHTML(); @@ -204,7 +207,7 @@ class MWException extends Exception { <title>$title</title> </head> <body> - <h1><img src='$wgLogo' style='float:left;margin-right:1em' alt=''>$title</h1> + <h1><img src='$wgLogo' style='float:left;margin-right:1em' alt=''/>$title</h1> "; } @@ -214,6 +217,17 @@ class MWException extends Exception { function htmlFooter() { echo "</body></html>"; } + + /** + * headers handled by subclass? + */ + function htmlBodyOnly() { + return false; + } + + static function isCommandLine() { + return !empty( $GLOBALS['wgCommandLineMode'] ) && !defined( 'MEDIAWIKI_INSTALL' ); + } } /** @@ -264,41 +278,44 @@ function wfInstallExceptionHandler() { * Report an exception to the user */ function wfReportException( Exception $e ) { - if ( $e instanceof MWException ) { - try { - $e->report(); - } catch ( Exception $e2 ) { - // Exception occurred from within exception handler - // Show a simpler error message for the original exception, - // don't try to invoke report() - $message = "MediaWiki internal error.\n\n" . - "Original exception: " . $e->__toString() . - "\n\nException caught inside exception handler: " . - $e2->__toString() . "\n"; - - if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { - wfPrintError( $message ); - } else { - echo nl2br( htmlspecialchars( $message ) ). "\n"; - } - } - } else { - $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" . - $e->__toString() . "\n"; - if ( $GLOBALS['wgShowExceptionDetails'] ) { - $message .= "\n" . $e->getTraceAsString() ."\n"; - } - if ( !empty( $GLOBALS['wgCommandLineMode'] ) ) { - wfPrintError( $message ); - } else { - echo nl2br( htmlspecialchars( $message ) ). "\n"; - } - } + $cmdLine = MWException::isCommandLine(); + if ( $e instanceof MWException ) { + try { + $e->report(); + } catch ( Exception $e2 ) { + // Exception occurred from within exception handler + // Show a simpler error message for the original exception, + // don't try to invoke report() + $message = "MediaWiki internal error.\n\n"; + if ( $GLOBALS['wgShowExceptionDetails'] ) + $message .= "Original exception: " . $e->__toString(); + $message .= "\n\nException caught inside exception handler"; + if ( $GLOBALS['wgShowExceptionDetails'] ) + $message .= ": " . $e2->__toString(); + $message .= "\n"; + if ( $cmdLine ) { + wfPrintError( $message ); + } else { + echo nl2br( htmlspecialchars( $message ) ). "\n"; + } + } + } else { + $message = "Unexpected non-MediaWiki exception encountered, of type \"" . get_class( $e ) . "\"\n" . + $e->__toString() . "\n"; + if ( $GLOBALS['wgShowExceptionDetails'] ) { + $message .= "\n" . $e->getTraceAsString() ."\n"; + } + if ( $cmdLine ) { + wfPrintError( $message ); + } else { + echo nl2br( htmlspecialchars( $message ) ). "\n"; + } + } } /** * Print a message, if possible to STDERR. - * Use this in command line mode only (see wgCommandLineMode) + * Use this in command line mode only (see isCommandLine) */ function wfPrintError( $message ) { #NOTE: STDERR may not be available, especially if php-cgi is used from the command line (bug #15602). diff --git a/includes/Exif.php b/includes/Exif.php index d5cf09cf..9e54bd55 100644 --- a/includes/Exif.php +++ b/includes/Exif.php @@ -1,10 +1,5 @@ <?php /** - * @ingroup Media - * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> - * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason - * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License - * * 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 @@ -20,7 +15,12 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html * + * @ingroup Media + * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com> + * @copyright Copyright © 2005, Ævar Arnfjörð Bjarmason + * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License * @see http://exif.org/Exif2-2.PDF The Exif 2.2 specification + * @file */ /** @@ -28,23 +28,21 @@ * @ingroup Media */ class Exif { + + const BYTE = 1; //!< An 8-bit (1-byte) unsigned integer. + const ASCII = 2; //!< An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. + const SHORT = 3; //!< A 16-bit (2-byte) unsigned integer. + const LONG = 4; //!< A 32-bit (4-byte) unsigned integer. + const RATIONAL = 5; //!< Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator + const UNDEFINED = 7; //!< An 8-bit byte that can take any value depending on the field definition + const SLONG = 9; //!< A 32-bit (4-byte) signed integer (2's complement notation), + const SRATIONAL = 10; //!< Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. + //@{ /* @var array * @private */ - /**#@+ - * Exif tag type definition - */ - const BYTE = 1; # An 8-bit (1-byte) unsigned integer. - const ASCII = 2; # An 8-bit byte containing one 7-bit ASCII code. The final byte is terminated with NULL. - const SHORT = 3; # A 16-bit (2-byte) unsigned integer. - const LONG = 4; # A 32-bit (4-byte) unsigned integer. - const RATIONAL = 5; # Two LONGs. The first LONG is the numerator and the second LONG expresses the denominator - const UNDEFINED = 7; # An 8-bit byte that can take any value depending on the field definition - const SLONG = 9; # A 32-bit (4-byte) signed integer (2's complement notation), - const SRATIONAL = 10; # Two SLONGs. The first SLONG is the numerator and the second SLONG is the denominator. - /** * Exif tags grouped by category, the tagname itself is the key and the type * is the value, in the case of more than one possible value type they are diff --git a/includes/Export.php b/includes/Export.php index 5f040b13..909804cf 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -30,9 +30,10 @@ class WikiExporter { var $dumpUploads = false; - const FULL = 0; - const CURRENT = 1; - const LOGS = 2; + const FULL = 1; + const CURRENT = 2; + const STABLE = 4; // extension defined + const LOGS = 8; const BUFFER = 0; const STREAM = 1; @@ -175,93 +176,103 @@ class WikiExporter { } protected function dumpFrom( $cond = '' ) { - $fname = 'WikiExporter::dumpFrom'; - wfProfileIn( $fname ); - - # For logs dumps... + wfProfileIn( __METHOD__ ); + # For logging dumps... if( $this->history & self::LOGS ) { + if( $this->buffer == WikiExporter::STREAM ) { + $prev = $this->db->bufferResults( false ); + } $where = array( 'user_id = log_user' ); # Hide private logs - $where[] = LogEventsList::getExcludeClause( $this->db ); + $hideLogs = LogEventsList::getExcludeClause( $this->db ); + if( $hideLogs ) $where[] = $hideLogs; + # Add on any caller specified conditions if( $cond ) $where[] = $cond; + # Get logging table name for logging.* clause + $logging = $this->db->tableName('logging'); $result = $this->db->select( array('logging','user'), - '*', + array( "{$logging}.*", 'user_name' ), // grab the user name $where, - $fname, + __METHOD__, array( 'ORDER BY' => 'log_id', 'USE INDEX' => array('logging' => 'PRIMARY') ) ); $wrapper = $this->db->resultObject( $result ); + if( $this->buffer == WikiExporter::STREAM ) { + $this->db->bufferResults( $prev ); + } $this->outputLogStream( $wrapper ); # For page dumps... } else { - list($page,$revision,$text) = $this->db->tableNamesN('page','revision','text'); - - $order = 'ORDER BY page_id'; - $limit = ''; - - if( $this->history == WikiExporter::FULL ) { - $join = 'page_id=rev_page'; - } elseif( $this->history == WikiExporter::CURRENT ) { - if ( $this->list_authors && $cond != '' ) { // List authors, if so desired - $this->do_list_authors ( $page , $revision , $cond ); + $tables = array( 'page', 'revision' ); + $opts = array( 'ORDER BY' => 'page_id ASC' ); + $opts['USE INDEX'] = array(); + $join = array(); + # Full history dumps... + if( $this->history & WikiExporter::FULL ) { + $join['revision'] = array('INNER JOIN','page_id=rev_page'); + # Latest revision dumps... + } elseif( $this->history & WikiExporter::CURRENT ) { + if( $this->list_authors && $cond != '' ) { // List authors, if so desired + list($page,$revision) = $this->db->tableNamesN('page','revision'); + $this->do_list_authors( $page, $revision, $cond ); + } + $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id'); + # "Stable" revision dumps... + } elseif( $this->history & WikiExporter::STABLE ) { + # Default JOIN, to be overridden... + $join['revision'] = array('INNER JOIN','page_id=rev_page AND page_latest=rev_id'); + # One, and only one hook should set this, and return false + if( wfRunHooks( 'WikiExporter::dumpStableQuery', array(&$tables,&$opts,&$join) ) ) { + wfProfileOut( __METHOD__ ); + return new WikiError( __METHOD__." given invalid history dump type." ); } - $join = 'page_id=rev_page AND page_latest=rev_id'; - } elseif ( is_array( $this->history ) ) { - $join = 'page_id=rev_page'; - if ( $this->history['dir'] == 'asc' ) { + # Time offset/limit for all pages/history... + } elseif( is_array( $this->history ) ) { + $revJoin = 'page_id=rev_page'; + # Set time order + if( $this->history['dir'] == 'asc' ) { $op = '>'; - $order .= ', rev_timestamp'; + $opts['ORDER BY'] = 'rev_timestamp ASC'; } else { $op = '<'; - $order .= ', rev_timestamp DESC'; + $opts['ORDER BY'] = 'rev_timestamp DESC'; } - if ( !empty( $this->history['offset'] ) ) { - $join .= " AND rev_timestamp $op " . $this->db->addQuotes( - $this->db->timestamp( $this->history['offset'] ) ); + # Set offset + if( !empty( $this->history['offset'] ) ) { + $revJoin .= " AND rev_timestamp $op " . + $this->db->addQuotes( $this->db->timestamp( $this->history['offset'] ) ); } - if ( !empty( $this->history['limit'] ) ) { - $limitNum = intval( $this->history['limit'] ); - if ( $limitNum > 0 ) { - $limit = "LIMIT $limitNum"; - } + $join['revision'] = array('INNER JOIN',$revJoin); + # Set query limit + if( !empty( $this->history['limit'] ) ) { + $opts['LIMIT'] = intval( $this->history['limit'] ); } + # Uknown history specification parameter? } else { - wfProfileOut( $fname ); - return new WikiError( "$fname given invalid history dump type." ); + wfProfileOut( __METHOD__ ); + return new WikiError( __METHOD__." given invalid history dump type." ); + } + # Query optimization hacks + if( $cond == '' ) { + $opts[] = 'STRAIGHT_JOIN'; + $opts['USE INDEX']['page'] = 'PRIMARY'; + } + # Build text join options + if( $this->text != WikiExporter::STUB ) { // 1-pass + $tables[] = 'text'; + $join['text'] = array('INNER JOIN','rev_text_id=old_id'); } - $where = ( $cond == '' ) ? '' : "$cond AND"; if( $this->buffer == WikiExporter::STREAM ) { $prev = $this->db->bufferResults( false ); } - if( $cond == '' ) { - // Optimization hack for full-database dump - $revindex = $pageindex = $this->db->useIndexClause("PRIMARY"); - $straight = ' /*! STRAIGHT_JOIN */ '; - } else { - $pageindex = ''; - $revindex = ''; - $straight = ''; - } - if( $this->text == WikiExporter::STUB ) { - $sql = "SELECT $straight * FROM - $page $pageindex, - $revision $revindex - WHERE $where $join - $order $limit"; - } else { - $sql = "SELECT $straight * FROM - $page $pageindex, - $revision $revindex, - $text - WHERE $where $join AND rev_text_id=old_id - $order $limit"; - } - $result = $this->db->query( $sql, $fname ); + + # Do the query! + $result = $this->db->select( $tables, '*', $cond, __METHOD__, $opts, $join ); $wrapper = $this->db->resultObject( $result ); + # Output dump results $this->outputPageStream( $wrapper ); - - if ( $this->list_authors ) { + if( $this->list_authors ) { $this->outputPageStream( $wrapper ); } @@ -269,7 +280,7 @@ class WikiExporter { $this->db->bufferResults( $prev ); } } - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); } /** @@ -399,7 +410,7 @@ class XmlDumpWriter { function namespaces() { global $wgContLang; - $spaces = " <namespaces>\n"; + $spaces = "<namespaces>\n"; foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) { $spaces .= ' ' . Xml::element( 'namespace', array( 'key' => $ns ), $title ) . "\n"; } diff --git a/includes/ExternalStore.php b/includes/ExternalStore.php index d095aba0..1e750bb5 100644 --- a/includes/ExternalStore.php +++ b/includes/ExternalStore.php @@ -92,6 +92,8 @@ class ExternalStore { $url = $store->store( $params, $data ); // Try to save the object } catch ( DBConnectionError $error ) { $url = false; + } catch( DBQueryError $error ) { + $url = false; } if ( $url ) { return $url; // Done! diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index 66086b0f..5177d35f 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -67,7 +67,7 @@ class FileDeleteForm { $reason = $this->DeleteReasonList; if ( $reason != 'other' && $this->DeleteReason != '') { // Entry from drop down menu + additional comment - $reason .= ': ' . $this->DeleteReason; + $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason; } elseif ( $reason == 'other' ) { $reason = $this->DeleteReason; } @@ -108,7 +108,8 @@ class FileDeleteForm { $id = $title->getArticleID( GAID_FOR_UPDATE ); // Need to delete the associated article $article = new Article( $title ); - if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason)) ) { + $error = ''; + if( wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason, &$error)) ) { if( $article->doDeleteArticle( $reason, $suppress, $id ) ) { global $wgRequest; if( $wgRequest->getCheck( 'wpWatch' ) ) { diff --git a/includes/ForkController.php b/includes/ForkController.php new file mode 100644 index 00000000..09e1788b --- /dev/null +++ b/includes/ForkController.php @@ -0,0 +1,160 @@ +<?php + +/** + * Class for managing forking command line scripts. + * Currently just does forking and process control, but it could easily be extended + * to provide IPC and job dispatch. + * + * This class requires the posix and pcntl extensions. + */ +class ForkController { + var $children = array(); + var $termReceived = false; + var $flags = 0, $procsToStart = 0; + + static $restartableSignals = array( + SIGFPE, + SIGILL, + SIGSEGV, + SIGBUS, + SIGABRT, + SIGSYS, + SIGPIPE, + SIGXCPU, + SIGXFSZ, + ); + + /** + * Pass this flag to __construct() to cause the class to automatically restart + * workers that exit with non-zero exit status or a signal such as SIGSEGV. + */ + const RESTART_ON_ERROR = 1; + + public function __construct( $numProcs, $flags = 0 ) { + if ( php_sapi_name() != 'cli' ) { + throw new MWException( "MultiProcess cannot be used from the web." ); + } + $this->procsToStart = $numProcs; + $this->flags = $flags; + } + + /** + * Start the child processes. + * + * This should only be called from the command line. It should be called + * as early as possible during execution. + * + * This will return 'child' in the child processes. In the parent process, + * it will run until all the child processes exit or a TERM signal is + * received. It will then return 'done'. + */ + public function start() { + // Trap SIGTERM + pcntl_signal( SIGTERM, array( $this, 'handleTermSignal' ), false ); + + do { + // Start child processes + if ( $this->procsToStart ) { + if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) { + return 'child'; + } + $this->procsToStart = 0; + } + + // Check child status + $status = false; + $deadPid = pcntl_wait( $status ); + + if ( $deadPid > 0 ) { + // Respond to child process termination + unset( $this->children[$deadPid] ); + if ( $this->flags & self::RESTART_ON_ERROR ) { + if ( pcntl_wifsignaled( $status ) ) { + // Restart if the signal was abnormal termination + // Don't restart if it was deliberately killed + $signal = pcntl_wtermsig( $status ); + if ( in_array( $signal, self::$restartableSignals ) ) { + echo "Worker exited with signal $signal, restarting\n"; + $this->procsToStart++; + } + } elseif ( pcntl_wifexited( $status ) ) { + // Restart on non-zero exit status + $exitStatus = pcntl_wexitstatus( $status ); + if ( $exitStatus > 0 ) { + echo "Worker exited with status $exitStatus, restarting\n"; + $this->procsToStart++; + } + } + } + // Throttle restarts + if ( $this->procsToStart ) { + usleep( 500000 ); + } + } + + // Run signal handlers + if ( function_exists( 'pcntl_signal_dispatch' ) ) { + pcntl_signal_dispatch(); + } else { + declare (ticks=1) { $status = $status; } + } + // Respond to TERM signal + if ( $this->termReceived ) { + foreach ( $this->children as $childPid => $unused ) { + posix_kill( $childPid, SIGTERM ); + } + $this->termReceived = false; + } + } while ( count( $this->children ) ); + pcntl_signal( SIGTERM, SIG_DFL ); + return 'done'; + } + + protected function prepareEnvironment() { + global $wgCaches, $wgMemc; + // Don't share DB or memcached connections + wfGetLBFactory()->destroyInstance(); + $wgCaches = array(); + unset( $wgMemc ); + } + + /** + * Fork a number of worker processes. + */ + protected function forkWorkers( $numProcs ) { + global $wgMemc, $wgCaches, $wgMainCacheType; + + $this->prepareEnvironment(); + + // Create the child processes + for ( $i = 0; $i < $numProcs; $i++ ) { + // Do the fork + $pid = pcntl_fork(); + if ( $pid === -1 || $pid === false ) { + echo "Error creating child processes\n"; + exit( 1 ); + } + + if ( !$pid ) { + $this->initChild(); + return 'child'; + } else { + // This is the parent process + $this->children[$pid] = true; + } + } + + return 'parent'; + } + + protected function initChild() { + global $wgMemc, $wgMainCacheType; + $wgMemc = wfGetCache( $wgMainCacheType ); + $this->children = null; + pcntl_signal( SIGTERM, SIG_DFL ); + } + + protected function handleTermSignal( $signal ) { + $this->termReceived = true; + } +} diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index 33f5831d..0807f0be 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -89,6 +89,19 @@ if ( !function_exists( 'array_diff_key' ) ) { } } +// Support for Wietse Venema's taint feature +if ( !function_exists( 'istainted' ) ) { + function istainted( $var ) { + return 0; + } + function taint( $var, $level = 0 ) {} + function untaint( $var, $level = 0 ) {} + define( 'TC_HTML', 1 ); + define( 'TC_SHELL', 1 ); + define( 'TC_MYSQL', 1 ); + define( 'TC_PCRE', 1 ); + define( 'TC_SELF', 1 ); +} /// @endcond @@ -337,12 +350,14 @@ function wfErrorLog( $text, $file ) { */ function wfLogProfilingData() { global $wgRequestTime, $wgDebugLogFile, $wgDebugRawPage, $wgRequest; - global $wgProfiler, $wgUser; - if ( !isset( $wgProfiler ) ) - return; - + global $wgProfiler, $wgProfileLimit, $wgUser; + # Profiling must actually be enabled... + if( !isset( $wgProfiler ) ) return; + # Get total page request time $now = wfTime(); $elapsed = $now - $wgRequestTime; + # Only show pages that longer than $wgProfileLimit time (default is 0) + if( $elapsed <= $wgProfileLimit ) return; $prof = wfGetProfilingOutput( $wgRequestTime, $elapsed ); $forward = ''; if( !empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) @@ -431,7 +446,7 @@ function wfGetLangObj( $langcode = false ){ return Language::factory( $langcode ); # $langcode is a string, but not a valid language code; use content language. - wfDebug( 'Invalid language code passed to wfGetLangObj, falling back to content language.' ); + wfDebug( "Invalid language code passed to wfGetLangObj, falling back to content language.\n" ); return $wgContLang; } @@ -771,7 +786,7 @@ function wfAbruptExit( $error = false ){ wfDebug("WARNING: Abrupt exit in $file at line $line\n"); } } else { - wfDebug('WARNING: Abrupt exit\n'); + wfDebug("WARNING: Abrupt exit\n"); } wfLogProfilingData(); @@ -860,18 +875,35 @@ function wfHostname() { * murky circumstances, which may be triggered in part by stub objects * or other fancy talkin'. * - * Will return an empty array if Zend Optimizer is detected, otherwise - * the output from debug_backtrace() (trimmed). + * Will return an empty array if Zend Optimizer is detected or if + * debug_backtrace is disabled, otherwise the output from + * debug_backtrace() (trimmed). * * @return array of backtrace information */ function wfDebugBacktrace() { + static $disabled = null; + if( extension_loaded( 'Zend Optimizer' ) ) { wfDebug( "Zend Optimizer detected; skipping debug_backtrace for safety.\n" ); return array(); - } else { - return array_slice( debug_backtrace(), 1 ); } + + if ( is_null( $disabled ) ) { + $disabled = false; + $functions = explode( ',', ini_get( 'disable_functions' ) ); + $functions = array_map( 'trim', $functions ); + $functions = array_map( 'strtolower', $functions ); + if ( in_array( 'debug_backtrace', $functions ) ) { + wfDebug( "debug_backtrace is in disabled_functions\n" ); + $disabled = true; + } + } + if ( $disabled ) { + return array(); + } + + return array_slice( debug_backtrace(), 1 ); } function wfBacktrace() { @@ -927,7 +959,8 @@ function wfBacktrace() { */ function wfShowingResults( $offset, $limit ) { global $wgLang; - return wfMsgExt( 'showingresults', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ) ); + return wfMsgExt( 'showingresults', array( 'parseinline' ), $wgLang->formatNum( $limit ), + $wgLang->formatNum( $offset+1 ) ); } /** @@ -935,18 +968,28 @@ function wfShowingResults( $offset, $limit ) { */ function wfShowingResultsNum( $offset, $limit, $num ) { global $wgLang; - return wfMsgExt( 'showingresultsnum', array( 'parseinline' ), $wgLang->formatNum( $limit ), $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); + return wfMsgExt( 'showingresultsnum', array( 'parseinline' ), $wgLang->formatNum( $limit ), + $wgLang->formatNum( $offset+1 ), $wgLang->formatNum( $num ) ); } /** - * @todo document + * Generate (prev x| next x) (20|50|100...) type links for paging + * @param $offset string + * @param $limit int + * @param $link string + * @param $query string, optional URL query parameter string + * @param $atend bool, optional param for specified if this is the last page */ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { global $wgLang; $fmtLimit = $wgLang->formatNum( $limit ); - $prev = wfMsg( 'prevn', $fmtLimit ); - $next = wfMsg( 'nextn', $fmtLimit ); - + # Get prev/next link display text + $prev = wfMsgHtml( 'prevn', $fmtLimit ); + $next = wfMsgHtml( 'nextn', $fmtLimit ); + # Get prev/next link title text + $pTitle = wfMsgExt( 'prevn-title', array('parsemag','escape'), $fmtLimit ); + $nTitle = wfMsgExt( 'nextn-title', array('parsemag','escape'), $fmtLimit ); + # Fetch the title object if( is_object( $link ) ) { $title =& $link; } else { @@ -955,44 +998,58 @@ function wfViewPrevNext( $offset, $limit, $link, $query = '', $atend = false ) { return false; } } - - if ( 0 != $offset ) { + # Make 'previous' link + if( 0 != $offset ) { $po = $offset - $limit; - if ( $po < 0 ) { $po = 0; } + $po = max($po,0); $q = "limit={$limit}&offset={$po}"; - if ( '' != $query ) { $q .= '&'.$query; } - $plink = '<a href="' . $title->escapeLocalUrl( $q ) . "\" class=\"mw-prevlink\">{$prev}</a>"; - } else { $plink = $prev; } - + if( $query != '' ) { + $q .= '&'.$query; + } + $plink = '<a href="' . $title->escapeLocalUrl( $q ) . "\" title=\"{$pTitle}\" class=\"mw-prevlink\">{$prev}</a>"; + } else { + $plink = $prev; + } + # Make 'next' link $no = $offset + $limit; - $q = 'limit='.$limit.'&offset='.$no; - if ( '' != $query ) { $q .= '&'.$query; } - - if ( $atend ) { + $q = "limit={$limit}&offset={$no}"; + if( $query != '' ) { + $q .= '&'.$query; + } + if( $atend ) { $nlink = $next; } else { - $nlink = '<a href="' . $title->escapeLocalUrl( $q ) . "\" class=\"mw-nextlink\">{$next}</a>"; - } - $nums = wfNumLink( $offset, 20, $title, $query ) . ' | ' . - wfNumLink( $offset, 50, $title, $query ) . ' | ' . - wfNumLink( $offset, 100, $title, $query ) . ' | ' . - wfNumLink( $offset, 250, $title, $query ) . ' | ' . - wfNumLink( $offset, 500, $title, $query ); - + $nlink = '<a href="' . $title->escapeLocalUrl( $q ) . "\" title=\"{$nTitle}\" class=\"mw-nextlink\">{$next}</a>"; + } + # Make links to set number of items per page + $nums = $wgLang->pipeList( array( + wfNumLink( $offset, 20, $title, $query ), + wfNumLink( $offset, 50, $title, $query ), + wfNumLink( $offset, 100, $title, $query ), + wfNumLink( $offset, 250, $title, $query ), + wfNumLink( $offset, 500, $title, $query ) + ) ); return wfMsg( 'viewprevnext', $plink, $nlink, $nums ); } /** - * @todo document + * Generate links for (20|50|100...) items-per-page links + * @param $offset string + * @param $limit int + * @param $title Title + * @param $query string, optional URL query parameter string */ -function wfNumLink( $offset, $limit, &$title, $query = '' ) { +function wfNumLink( $offset, $limit, $title, $query = '' ) { global $wgLang; - if ( '' == $query ) { $q = ''; } - else { $q = $query.'&'; } - $q .= 'limit='.$limit.'&offset='.$offset; - + if( $query == '' ) { + $q = ''; + } else { + $q = $query.'&'; + } + $q .= "limit={$limit}&offset={$offset}"; $fmtLimit = $wgLang->formatNum( $limit ); - $s = '<a href="' . $title->escapeLocalUrl( $q ) . "\" class=\"mw-numlink\">{$fmtLimit}</a>"; + $lTitle = wfMsgExt('shown-title',array('parsemag','escape'),$limit); + $s = '<a href="' . $title->escapeLocalUrl( $q ) . "\" title=\"{$lTitle}\" class=\"mw-numlink\">{$fmtLimit}</a>"; return $s; } @@ -1693,6 +1750,11 @@ define('TS_ORACLE', 6); define('TS_POSTGRES', 7); /** + * DB2 format time + */ +define('TS_DB2', 8); + +/** * @param mixed $outputtype A timestamp in one of the supported formats, the * function will autodetect which format is supplied * and act accordingly. @@ -1753,6 +1815,8 @@ function wfTimestamp($outputtype=TS_UNIX,$ts=0) { return gmdate( 'd-M-y h.i.s A', $uts) . ' +00:00'; case TS_POSTGRES: return gmdate( 'Y-m-d H:i:s', $uts) . ' GMT'; + case TS_DB2: + return gmdate( 'Y-m-d H:i:s', $uts); default: throw new MWException( 'wfTimestamp() called with illegal output type.'); } @@ -1837,7 +1901,7 @@ function wfGetCachedNotice( $name ) { $parserMemc->set( $key, array( 'html' => $parsed, 'hash' => md5( $notice ) ), 600 ); $notice = $parsed; } else { - wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available' ); + wfDebug( 'wfGetCachedNotice called for ' . $name . ' with no $wgOut available'."\n" ); $notice = ''; } } @@ -1929,11 +1993,16 @@ function wfTempDir() { * * @param string $dir Full path to directory to create * @param int $mode Chmod value to use, default is $wgDirectoryMode + * @param string $caller Optional caller param for debugging. * @return bool */ -function wfMkdirParents( $dir, $mode = null ) { +function wfMkdirParents( $dir, $mode = null, $caller = null ) { global $wgDirectoryMode; + if ( !is_null( $caller ) ) { + wfDebug( "$caller: called wfMkdirParents($dir)" ); + } + if( strval( $dir ) === '' || file_exists( $dir ) ) return true; @@ -2101,11 +2170,26 @@ function wfIniGetBool( $setting ) { function wfShellExec( $cmd, &$retval=null ) { global $IP, $wgMaxShellMemory, $wgMaxShellFileSize, $wgMaxShellTime; - if( wfIniGetBool( 'safe_mode' ) ) { - wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); + static $disabled; + if ( is_null( $disabled ) ) { + $disabled = false; + if( wfIniGetBool( 'safe_mode' ) ) { + wfDebug( "wfShellExec can't run in safe_mode, PHP's exec functions are too broken.\n" ); + $disabled = true; + } + $functions = explode( ',', ini_get( 'disable_functions' ) ); + $functions = array_map( 'trim', $functions ); + $functions = array_map( 'strtolower', $functions ); + if ( in_array( 'passthru', $functions ) ) { + wfDebug( "passthru is in disabled_functions\n" ); + $disabled = true; + } + } + if ( $disabled ) { $retval = 1; return "Unable to run external programs in safe mode."; } + wfInitShellLocale(); if ( php_uname( 's' ) == 'Linux' ) { @@ -2317,9 +2401,16 @@ function wfMergeErrorArrays(/*...*/) { } /** - * Make a URL index, appropriate for the el_index field of externallinks. + * parse_url() work-alike, but non-broken. Differences: + * + * 1) Does not raise warnings on bad URLs (just returns false) + * 2) Handles protocols that don't use :// (e.g., mailto: and news:) correctly + * 3) Adds a "delimiter" element to the array, either '://' or ':' (see (2)) + * + * @param string $url A URL to parse + * @return array Bits of the URL in an associative array, per PHP docs */ -function wfMakeUrlIndex( $url ) { +function wfParseUrl( $url ) { global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php wfSuppressWarnings(); $bits = parse_url( $url ); @@ -2327,12 +2418,12 @@ function wfMakeUrlIndex( $url ) { if ( !$bits ) { return false; } + // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it - $delimiter = ''; - if ( in_array( $bits['scheme'] . '://' , $wgUrlProtocols ) ) { - $delimiter = '://'; - } elseif ( in_array( $bits['scheme'] .':' , $wgUrlProtocols ) ) { - $delimiter = ':'; + if ( in_array( $bits['scheme'] . '://', $wgUrlProtocols ) ) { + $bits['delimiter'] = '://'; + } elseif ( in_array( $bits['scheme'] . ':', $wgUrlProtocols ) ) { + $bits['delimiter'] = ':'; // parse_url detects for news: and mailto: the host part of an url as path // We have to correct this wrong detection if ( isset ( $bits['path'] ) ) { @@ -2343,6 +2434,15 @@ function wfMakeUrlIndex( $url ) { return false; } + return $bits; +} + +/** + * Make a URL index, appropriate for the el_index field of externallinks. + */ +function wfMakeUrlIndex( $url ) { + $bits = wfParseUrl( $url ); + // Reverse the labels in the hostname, convert to lower case // For emails reverse domainpart only if ( $bits['scheme'] == 'mailto' ) { @@ -2364,7 +2464,7 @@ function wfMakeUrlIndex( $url ) { } // Reconstruct the pseudo-URL $prot = $bits['scheme']; - $index = "$prot$delimiter$reversedHost"; + $index = $prot . $bits['delimiter'] . $reversedHost; // Leave out user and password. Add the port, path, query and fragment if ( isset( $bits['port'] ) ) $index .= ':' . $bits['port']; if ( isset( $bits['path'] ) ) { diff --git a/includes/HTMLCacheUpdate.php b/includes/HTMLCacheUpdate.php index 402102ea..bd63c072 100644 --- a/includes/HTMLCacheUpdate.php +++ b/includes/HTMLCacheUpdate.php @@ -35,146 +35,76 @@ class HTMLCacheUpdate $this->mTable = $table; $this->mRowsPerJob = $wgUpdateRowsPerJob; $this->mRowsPerQuery = $wgUpdateRowsPerQuery; + $this->mCache = $this->mTitle->getBacklinkCache(); } public function doUpdate() { # Fetch the IDs - $cond = $this->getToCondition(); - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( $this->mTable, $this->getFromField(), $cond, __METHOD__ ); + $numRows = $this->mCache->getNumLinks( $this->mTable ); - if ( $dbr->numRows( $res ) != 0 ) { - if ( $dbr->numRows( $res ) > $this->mRowsPerJob ) { - $this->insertJobs( $res ); + if ( $numRows != 0 ) { + if ( $numRows > $this->mRowsPerJob ) { + $this->insertJobs(); } else { - $this->invalidateIDs( $res ); + $this->invalidate(); } } wfRunHooks( 'HTMLCacheUpdate::doUpdate', array($this->mTitle) ); } - protected function insertJobs( ResultWrapper $res ) { - $numRows = $res->numRows(); - $numBatches = ceil( $numRows / $this->mRowsPerJob ); - $realBatchSize = $numRows / $numBatches; - $start = false; - $jobs = array(); - do { - for ( $i = 0; $i <= $realBatchSize - 1; $i++ ) { - $row = $res->fetchRow(); - if ( $row ) { - $id = $row[0]; - } else { - $id = false; - break; - } - } - + protected function insertJobs() { + $batches = $this->mCache->partition( $this->mTable, $this->mRowsPerJob ); + if ( !$batches ) { + return; + } + foreach ( $batches as $batch ) { $params = array( 'table' => $this->mTable, - 'start' => $start, - 'end' => ( $id !== false ? $id - 1 : false ), + 'start' => $batch[0], + 'end' => $batch[1], ); $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); - - $start = $id; - } while ( $start ); - - Job::batchInsert( $jobs ); - } - - protected function getPrefix() { - static $prefixes = array( - 'pagelinks' => 'pl', - 'imagelinks' => 'il', - 'categorylinks' => 'cl', - 'templatelinks' => 'tl', - 'redirect' => 'rd', - ); - - if ( is_null( $this->mPrefix ) ) { - $this->mPrefix = $prefixes[$this->mTable]; - if ( is_null( $this->mPrefix ) ) { - throw new MWException( "Invalid table type \"{$this->mTable}\" in " . __CLASS__ ); - } } - return $this->mPrefix; - } - - public function getFromField() { - return $this->getPrefix() . '_from'; + Job::batchInsert( $jobs ); } - public function getToCondition() { - $prefix = $this->getPrefix(); - switch ( $this->mTable ) { - case 'pagelinks': - case 'templatelinks': - case 'redirect': - return array( - "{$prefix}_namespace" => $this->mTitle->getNamespace(), - "{$prefix}_title" => $this->mTitle->getDBkey() - ); - case 'imagelinks': - return array( 'il_to' => $this->mTitle->getDBkey() ); - case 'categorylinks': - return array( 'cl_to' => $this->mTitle->getDBkey() ); - } - throw new MWException( 'Invalid table type in ' . __CLASS__ ); - } /** - * Invalidate a set of IDs, right now + * Invalidate a set of pages, right now */ - public function invalidateIDs( ResultWrapper $res ) { + public function invalidate( $startId = false, $endId = false ) { global $wgUseFileCache, $wgUseSquid; - if ( $res->numRows() == 0 ) { + $titleArray = $this->mCache->getLinks( $this->mTable, $startId, $endId ); + if ( $titleArray->count() == 0 ) { return; } $dbw = wfGetDB( DB_MASTER ); $timestamp = $dbw->timestamp(); - $done = false; - - while ( !$done ) { - # Get all IDs in this query into an array - $ids = array(); - for ( $i = 0; $i < $this->mRowsPerQuery; $i++ ) { - $row = $res->fetchRow(); - if ( $row ) { - $ids[] = $row[0]; - } else { - $done = true; - break; - } - } - if ( !count( $ids ) ) { - break; - } + # Get all IDs in this query into an array + $ids = array(); + foreach ( $titleArray as $title ) { + $ids[] = $title->getArticleID(); + } + # Update page_touched + $dbw->update( 'page', + array( 'page_touched' => $timestamp ), + array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ), + __METHOD__ + ); - # Update page_touched - $dbw->update( 'page', - array( 'page_touched' => $timestamp ), - array( 'page_id IN (' . $dbw->makeList( $ids ) . ')' ), - __METHOD__ - ); + # Update squid + if ( $wgUseSquid ) { + $u = SquidUpdate::newFromTitles( $titleArray ); + $u->doUpdate(); + } - # Update squid - if ( $wgUseSquid || $wgUseFileCache ) { - $titles = Title::newFromIDs( $ids ); - if ( $wgUseSquid ) { - $u = SquidUpdate::newFromTitles( $titles ); - $u->doUpdate(); - } - - # Update file cache - if ( $wgUseFileCache ) { - foreach ( $titles as $title ) { - HTMLFileCache::clearFileCache( $title ); - } - } + # Update file cache + if ( $wgUseFileCache ) { + foreach ( $titleArray as $title ) { + HTMLFileCache::clearFileCache( $title ); } } } @@ -204,20 +134,7 @@ class HTMLCacheUpdateJob extends Job { public function run() { $update = new HTMLCacheUpdate( $this->title, $this->table ); - - $fromField = $update->getFromField(); - $conds = $update->getToCondition(); - if ( $this->start ) { - $conds[] = "$fromField >= {$this->start}"; - } - if ( $this->end ) { - $conds[] = "$fromField <= {$this->end}"; - } - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( $this->table, $fromField, $conds, __METHOD__ ); - $update->invalidateIDs( $res ); - + $update->invalidate( $this->start, $this->end ); return true; } } diff --git a/includes/HTMLFileCache.php b/includes/HTMLFileCache.php index e267962c..68cafa24 100644 --- a/includes/HTMLFileCache.php +++ b/includes/HTMLFileCache.php @@ -128,7 +128,6 @@ class HTMLFileCache { public function loadFromFileCache() { global $wgOut, $wgMimeType, $wgOutputEncoding, $wgContLanguageCode; wfDebug(" loadFromFileCache()\n"); - $filename = $this->fileCacheName(); // Raw pages should handle cache control on their own, // even when using file cache. This reduces hits from clients. @@ -148,6 +147,7 @@ class HTMLFileCache { } } readfile( $filename ); + $wgOut->disable(); // tell $wgOut that output is taken care of } protected function checkCacheDirs() { @@ -159,13 +159,12 @@ class HTMLFileCache { wfMkdirParents( $mydir2 ); } - public function saveToFileCache( $origtext ) { + public function saveToFileCache( $text ) { global $wgUseFileCache; - if( !$wgUseFileCache ) { - return $origtext; // return to output + if( !$wgUseFileCache || strlen( $text ) < 512 ) { + // Disabled or empty/broken output (OOM and PHP errors) + return $text; } - $text = $origtext; - if( strcmp($text,'') == 0 ) return ''; wfDebug(" saveToFileCache()\n", false); diff --git a/includes/ImageGallery.php b/includes/ImageGallery.php index f3f525c1..8a38bed7 100644 --- a/includes/ImageGallery.php +++ b/includes/ImageGallery.php @@ -289,7 +289,7 @@ class ImageGallery } $textlink = $this->mShowFilename ? - $sk->makeKnownLinkObj( $nt, htmlspecialchars( $wgLang->truncate( $nt->getText(), 20, '...' ) ) ) . "<br />\n" : + $sk->makeKnownLinkObj( $nt, htmlspecialchars( $wgLang->truncate( $nt->getText(), 20 ) ) ) . "<br />\n" : '' ; # ATTENTION: The newline after <div class="gallerytext"> is needed to accommodate htmltidy which diff --git a/includes/ImagePage.php b/includes/ImagePage.php index 314d478e..4f3b859a 100644 --- a/includes/ImagePage.php +++ b/includes/ImagePage.php @@ -105,7 +105,6 @@ class ImagePage extends Article { } else { # Just need to set the right headers $wgOut->setArticleFlag( true ); - $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->setPageTitle( $this->mTitle->getPrefixedText() ); $this->viewUpdates(); } @@ -117,8 +116,6 @@ class ImagePage extends Article { $wgOut->addWikiText( $fol ); } $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . '</div>' ); - } else { - $this->checkSharedConflict(); } $this->closeShowImage(); @@ -129,11 +126,9 @@ class ImagePage extends Article { array( 'id' => 'filelinks' ), wfMsg( 'imagelinks' ) ) . "\n" ); $this->imageDupes(); - // TODO: We may want to find local images redirecting to a foreign - // file: "The following local files redirect to this file" - if( $this->img->isLocal() ) { - $this->imageRedirects(); - } + # TODO! FIXME! For some freaky reason, we can't redirect to foreign images. + # Yet we return metadata about the target. Definitely an issue in the FileRepo + $this->imageRedirects(); $this->imageLinks(); if( $showmeta ) { @@ -473,6 +468,7 @@ EOT $title = SpecialPage::getTitleFor( 'Upload' ); $link = $sk->makeKnownLinkObj($title, wfMsgHtml('noimage-linktext'), 'wpDestFile=' . urlencode( $this->displayImg->getName() ) ); + $wgOut->setRobotPolicy( 'noindex,nofollow' ); $wgOut->addHTML( wfMsgWikiHtml( 'noimage', $link ) ); } } @@ -487,17 +483,18 @@ EOT $descUrl = $this->img->getDescriptionUrl(); $descText = $this->img->getDescriptionText(); - $s = "<div class='sharedUploadNotice'>" . wfMsgWikiHtml( 'sharedupload' ); + $msg = ''; if( $descUrl ) { $sk = $wgUser->getSkin(); $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadwiki-linktext' ) ); $msg = ( $descText ) ? 'shareduploadwiki-desc' : 'shareduploadwiki'; $msg = wfMsgExt( $msg, array( 'parseinline', 'replaceafter' ), $link ); - if( $msg != '-' ) { - # Show message only if not voided by local sysops - $s .= $msg; + if( $msg == '-' ) { + $msg = ''; } } + $s = "<div class='sharedUploadNotice'>"; + $s .= wfMsgWikiHtml( 'sharedupload', $this->img->getRepo()->getDisplayName(), $msg ); $s .= "</div>"; $wgOut->addHTML( $s ); @@ -506,58 +503,10 @@ EOT } } - /* - * Check for files with the same name on the foreign repos. - */ - protected function checkSharedConflict() { - global $wgOut, $wgUser; - - $repoGroup = RepoGroup::singleton(); - if( !$repoGroup->hasForeignRepos() ) { - return; - } - - $this->loadFile(); - if( !$this->img->isLocal() ) { - return; - } - - $this->dupFile = null; - $repoGroup->forEachForeignRepo( array( $this, 'checkSharedConflictCallback' ) ); - - if( !$this->dupFile ) - return; - $dupfile = $this->dupFile; - $same = ( - ($this->img->getSha1() == $dupfile->getSha1()) && - ($this->img->getSize() == $dupfile->getSize()) - ); - - $sk = $wgUser->getSkin(); - $descUrl = $dupfile->getDescriptionUrl(); - if( $same ) { - $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadduplicate-linktext' ) ); - $wgOut->addHTML( '<div id="shared-image-dup">' . wfMsgWikiHtml( 'shareduploadduplicate', $link ) . '</div>' ); - } else { - $link = $sk->makeExternalLink( $descUrl, wfMsg( 'shareduploadconflict-linktext' ) ); - $wgOut->addHTML( '<div id="shared-image-conflict">' . wfMsgWikiHtml( 'shareduploadconflict', $link ) . '</div>' ); - } - } - - public function checkSharedConflictCallback( $repo ) { - $this->loadFile(); - $dupfile = $repo->newFile( $this->img->getTitle() ); - if( $dupfile && $dupfile->exists() ) { - $this->dupFile = $dupfile; - return $dupfile->exists(); - } - return false; - } - public function getUploadUrl() { $this->loadFile(); $uploadTitle = SpecialPage::getTitleFor( 'Upload' ); - return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) ); + return $uploadTitle->getFullUrl( 'wpDestFile=' . urlencode( $this->img->getName() ) . '&wpForReUpload=1' ); } /** @@ -581,10 +530,6 @@ EOT $wgOut->addHTML( "<li><div class='plainlinks'>{$ulink}</div></li>" ); } - # Link to Special:FileDuplicateSearch - $dupeLink = $sk->makeKnownLinkObj( SpecialPage::getTitleFor( 'FileDuplicateSearch', $this->mTitle->getDBkey() ), wfMsgHtml( 'imagepage-searchdupe' ) ); - $wgOut->addHTML( "<li>{$dupeLink}</li>" ); - # External editing link $elink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'edit-externally' ), 'action=edit&externaledit=true&mode=file' ); $wgOut->addHTML( '<li>' . $elink . ' <small>' . wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) . '</small></li>' ); @@ -698,19 +643,21 @@ EOT $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" ); $wgOut->addWikiMsg( 'duplicatesoffile', - $wgLang->formatNum( count( $dupes ) ) + $wgLang->formatNum( count( $dupes ) ), $this->mTitle->getDBkey() ); $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" ); $sk = $wgUser->getSkin(); foreach ( $dupes as $file ) { + $fromSrc = ''; if( $file->isLocal() ) $link = $sk->makeKnownLinkObj( $file->getTitle(), "" ); else { $link = $sk->makeExternalLink( $file->getDescriptionUrl(), $file->getTitle()->getPrefixedText() ); + $fromSrc = wfMsg( 'shared-repo-from', $file->getRepo()->getDisplayName() ); } - $wgOut->addHTML( "<li>{$link}</li>\n" ); + $wgOut->addHTML( "<li>{$link} {$fromSrc}</li>\n" ); } $wgOut->addHTML( "</ul></div>\n" ); } @@ -922,7 +869,8 @@ class ImageHistoryList { 'alt' => wfMsg( 'filehist-thumbtext', $wgLang->timeAndDate( $timestamp, true ) ), 'file-link' => true, ); - $row .= '</td><td>' . $thumbnail->toHtml( $options ); + $row .= '</td><td>' . ( $thumbnail ? $thumbnail->toHtml( $options ) : + wfMsgHtml( 'filehist-nothumb' ) ); } else { $row .= '</td><td>' . wfMsgHtml( 'filehist-nothumb' ); } diff --git a/includes/Import.php b/includes/Import.php index 56e7a7fb..973866df 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -223,7 +223,7 @@ class WikiRevision { } elseif( $changed ) { wfDebug( __METHOD__ . ": running onArticleEdit\n" ); - Article::onArticleEdit( $this->title, 'skiptransclusions' ); // leave templatelinks for editUpdates() + Article::onArticleEdit( $this->title ); wfDebug( __METHOD__ . ": running edit updates\n" ); $article->editUpdates( @@ -1116,7 +1116,7 @@ class ImportStreamSource { } } - public static function newFromInterwiki( $interwiki, $page, $history=false ) { + public static function newFromInterwiki( $interwiki, $page, $history = false, $templates = false, $pageLinkDepth = 0 ) { if( $page == '' ) { return new WikiErrorMsg( 'import-noarticle' ); } @@ -1124,7 +1124,10 @@ class ImportStreamSource { if( is_null( $link ) || $link->getInterwiki() == '' ) { return new WikiErrorMsg( 'importbadinterwiki' ); } else { - $params = $history ? 'history=1' : ''; + $params = array(); + if ( $history ) $params['history'] = 1; + if ( $templates ) $params['templates'] = 1; + if ( $pageLinkDepth ) $params['pagelink-depth'] = $pageLinkDepth; $url = $link->getFullUrl( $params ); # For interwikis, use POST to avoid redirects. return ImportStreamSource::newFromURL( $url, "POST" ); diff --git a/includes/Linker.php b/includes/Linker.php index f116fb4a..b739244b 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -618,7 +618,7 @@ class Linker { $img = ''; $success = wfRunHooks('LinkerMakeExternalImage', array( &$url, &$alt, &$img ) ); if(!$success) { - wfDebug("Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}", true); + wfDebug("Hook LinkerMakeExternalImage changed the output of external image with url {$url} and alt text {$alt} to {$img}\n", true); return $img; } return Xml::element( 'img', @@ -882,10 +882,13 @@ class Linker { } } - if( $page ) { - $query = $query ? '&page=' . urlencode( $page ) : 'page=' . urlencode( $page ); - } + # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs + # So we don't need to pass it here in $query. However, the URL for the + # zoom icon still needs it, so we make a unique query for it. See bug 14771 $url = $title->getLocalURL( $query ); + if( $page ) { + $url = wfAppendQuery( $url, 'page=' . urlencode( $page ) ); + } $more = htmlspecialchars( wfMsg( 'thumbnail-more' ) ); @@ -1007,22 +1010,37 @@ class Linker { wfMsg( $key ) ); } - /** @todo document */ + /** + * Make an external link + * @param String $url URL to link to + * @param String $text text of link + * @param boolean $escape Do we escape the link text? + * @param String $linktype Type of external link. Gets added to the classes + * @param array $attribs Array of extra attributes to <a> + * + * @TODO! @FIXME! This is a really crappy implementation. $linktype and + * 'external' are mashed into the class attrib for the link (which is made + * into a string). Then, if we've got additional params in $attribs, we + * add to it. People using this might want to change the classes (or other + * default link attributes), but passing $attribsText is just messy. Would + * make a lot more sense to make put the classes into $attribs, let the + * hook play with them, *then* expand it all at once. + */ function makeExternalLink( $url, $text, $escape = true, $linktype = '', $attribs = array() ) { $attribsText = $this->getExternalLinkAttributes( $url, $text, 'external ' . $linktype ); - if ( $attribs ) { - $attribsText .= Xml::expandAttributes( $attribs ); - } $url = htmlspecialchars( $url ); if( $escape ) { $text = htmlspecialchars( $text ); } $link = ''; - $success = wfRunHooks('LinkerMakeExternalLink', array( &$url, &$text, &$link ) ); + $success = wfRunHooks('LinkerMakeExternalLink', array( &$url, &$text, &$link, &$attribs, $linktype ) ); if(!$success) { - wfDebug("Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}", true); + wfDebug("Hook LinkerMakeExternalLink changed the output of link with url {$url} and text {$text} to {$link}\n", true); return $link; } + if ( $attribs ) { + $attribsText .= Xml::expandAttributes( $attribs ); + } return '<a href="'.$url.'"'.$attribsText.'>'.$text.'</a>'; } @@ -1053,7 +1071,7 @@ class Linker { * @return string */ public function userToolLinks( $userId, $userText, $redContribsWhenNoEdits = false, $flags = 0, $edits=null ) { - global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans; + global $wgUser, $wgDisableAnonTalk, $wgSysopUserBans, $wgLang; $talkable = !( $wgDisableAnonTalk && 0 == $userId ); $blockable = ( $wgSysopUserBans || 0 == $userId ) && !$flags & self::TOOL_LINKS_NOBLOCK; @@ -1079,7 +1097,7 @@ class Linker { } if( $items ) { - return ' <span class="mw-usertoollinks">(' . implode( ' | ', $items ) . ')</span>'; + return ' <span class="mw-usertoollinks">(' . $wgLang->pipeList( $items ) . ')</span>'; } else { return ''; } @@ -1783,9 +1801,7 @@ class Linker { # FIXME: Per standard MW behavior, a value of '-' means to suppress the # attribute, but this is broken for accesskey: that might be a useful # value. - if( $accesskey != '' - && $accesskey != '-' - && !wfEmptyMsg( "accesskey-$name", $accesskey ) ) { + if( $accesskey != '' && $accesskey != '-' && !wfEmptyMsg( "accesskey-$name", $accesskey ) ) { wfProfileOut( __METHOD__ ); return $accesskey; } @@ -1793,4 +1809,21 @@ class Linker { wfProfileOut( __METHOD__ ); return false; } + + /** + * Creates a (show/hide) link for deleting revisions/log entries + * + * @param array $query Query parameters to be passed to link() + * @param bool $restricted Set to true to use a <strong> instead of a <span> + * + * @return string HTML <a> link to Special:Revisiondelete, wrapped in a + * span to allow for customization of appearance with CSS + */ + public function revDeleteLink( $query = array(), $restricted = false ) { + $sp = SpecialPage::getTitleFor( 'Revisiondelete' ); + $text = wfMsgHtml( 'rev-delundel' ); + $tag = $restricted ? 'strong' : 'span'; + $link = $this->link( $sp, $text, array(), $query, array( 'known', 'noclasses' ) ); + return Xml::tags( $tag, array( 'class' => 'mw-revdelundel-link' ), "($link)" ); + } } diff --git a/includes/LinksUpdate.php b/includes/LinksUpdate.php index 13f35b5a..caacb49c 100644 --- a/includes/LinksUpdate.php +++ b/includes/LinksUpdate.php @@ -20,8 +20,7 @@ class LinksUpdate { $mProperties, //!< Map of arbitrary name to value $mDb, //!< Database connection reference $mOptions, //!< SELECT options to be used (array) - $mRecursive, //!< Whether to queue jobs for recursive updates - $mTouchTmplLinks; //!< Whether to queue HTMLCacheUpdate jobs IF recursive + $mRecursive; //!< Whether to queue jobs for recursive updates /**@}}*/ /** @@ -72,15 +71,6 @@ class LinksUpdate { wfRunHooks( 'LinksUpdateConstructed', array( &$this ) ); } - - /** - * Invalidate HTML cache of pages that include this page? - */ - public function setRecursiveTouch( $val ) { - $this->mTouchTmplLinks = (bool)$val; - if( $val ) // Cannot invalidate without queueRecursiveJobs() - $this->mRecursive = true; - } /** * Update link tables with outgoing links from an updated article @@ -95,7 +85,6 @@ class LinksUpdate { $this->doIncrementalUpdate(); } wfRunHooks( 'LinksUpdateComplete', array( &$this ) ); - } protected function doIncrementalUpdate() { @@ -207,49 +196,21 @@ class LinksUpdate { global $wgUpdateRowsPerJob; wfProfileIn( __METHOD__ ); - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'templatelinks', - array( 'tl_from' ), - array( - 'tl_namespace' => $this->mTitle->getNamespace(), - 'tl_title' => $this->mTitle->getDBkey() - ), __METHOD__ - ); - - $numRows = $res->numRows(); - if( !$numRows ) { + $cache = $this->mTitle->getBacklinkCache(); + $batches = $cache->partition( 'templatelinks', $wgUpdateRowsPerJob ); + if ( !$batches ) { wfProfileOut( __METHOD__ ); - return; // nothing to do + return; } - $numBatches = ceil( $numRows / $wgUpdateRowsPerJob ); - $realBatchSize = $numRows / $numBatches; - $start = false; $jobs = array(); - do { - for( $i = 0; $i <= $realBatchSize - 1; $i++ ) { - $row = $res->fetchRow(); - if( $row ) { - $id = $row[0]; - } else { - $id = false; - break; - } - } + foreach ( $batches as $batch ) { + list( $start, $end ) = $batch; $params = array( 'start' => $start, - 'end' => ( $id !== false ? $id - 1 : false ), + 'end' => $end, ); $jobs[] = new RefreshLinksJob2( $this->mTitle, $params ); - # Hit page caches while we're at it if set to do so... - if( $this->mTouchTmplLinks ) { - $params['table'] = 'templatelinks'; - $jobs[] = new HTMLCacheUpdateJob( $this->mTitle, $params ); - } - $start = $id; - } while ( $start ); - - $dbr->freeResult( $res ); - + } Job::batchInsert( $jobs ); wfProfileOut( __METHOD__ ); @@ -465,9 +426,12 @@ class LinksUpdate { * @private */ function getCategoryInsertions( $existing = array() ) { + global $wgContLang; $diffs = array_diff_assoc( $this->mCategories, $existing ); $arr = array(); foreach ( $diffs as $name => $sortkey ) { + $nt = Title::makeTitleSafe( NS_CATEGORY, $name ); + $wgContLang->findVariantLink( $name, $nt, true ); $arr[] = array( 'cl_from' => $this->mId, 'cl_to' => $name, diff --git a/includes/LogEventsList.php b/includes/LogEventsList.php index 528bd3aa..95109eb5 100644 --- a/includes/LogEventsList.php +++ b/includes/LogEventsList.php @@ -39,9 +39,10 @@ class LogEventsList { // Precache various messages if( !isset( $this->message ) ) { $messages = array( 'revertmerge', 'protect_change', 'unblocklink', 'change-blocklink', - 'revertmove', 'undeletelink', 'revdel-restore', 'rev-delundel', 'hist', 'pipe-separator' ); + 'revertmove', 'undeletelink', 'revdel-restore', 'rev-delundel', 'hist', 'diff', + 'pipe-separator' ); foreach( $messages as $msg ) { - $this->message[$msg] = wfMsgExt( $msg, array( 'escape' ) ); + $this->message[$msg] = wfMsgExt( $msg, array( 'escapenoentities' ) ); } } } @@ -65,16 +66,19 @@ class LogEventsList { * @param $pattern String * @param $year Integer: year * @param $month Integer: month - * @param $filter Boolean + * @param $filter: array + * @param $tagFilter: array? */ public function showOptions( $type = '', $user = '', $page = '', $pattern = '', $year = '', - $month = '', $filter = null ) + $month = '', $filter = null, $tagFilter='' ) { global $wgScript, $wgMiserMode; $action = htmlspecialchars( $wgScript ); $title = SpecialPage::getTitleFor( 'Log' ); $special = htmlspecialchars( $title->getPrefixedDBkey() ); + $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter ); + $this->out->addHTML( "<form action=\"$action\" method=\"get\"><fieldset>" . Xml::element( 'legend', array(), wfMsg( 'log' ) ) . Xml::hidden( 'title', $special ) . "\n" . @@ -82,28 +86,31 @@ class LogEventsList { $this->getUserInput( $user ) . "\n" . $this->getTitleInput( $page ) . "\n" . ( !$wgMiserMode ? ($this->getTitlePattern( $pattern )."\n") : "" ) . - "<p>" . $this->getDateMenu( $year, $month ) . "\n" . - ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) . + "<p>" . Xml::dateMenu( $year, $month ) . "\n" . + ( $tagSelector ? Xml::tags( 'p', null, implode( ' ', $tagSelector ) ) :'' ). "\n" . + ( $filter ? "</p><p>".$this->getFilterLinks( $type, $filter )."\n" : "" ) . "\n" . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "</p>\n" . "</fieldset></form>" ); } private function getFilterLinks( $logType, $filter ) { - global $wgTitle; + global $wgTitle, $wgLang; // show/hide links $messages = array( wfMsgHtml( 'show' ), wfMsgHtml( 'hide' ) ); // Option value -> message mapping $links = array(); + $hiddens = ''; // keep track for "go" button foreach( $filter as $type => $val ) { $hideVal = 1 - intval($val); $link = $this->skin->makeKnownLinkObj( $wgTitle, $messages[$hideVal], wfArrayToCGI( array( "hide_{$type}_log" => $hideVal ), $this->getDefaultQuery() ) ); $links[$type] = wfMsgHtml( "log-show-hide-{$type}", $link ); + $hiddens .= Xml::hidden( "hide_{$type}_log", $val ) . "\n"; } // Build links - return implode( ' | ', $links ); + return '<small>'.$wgLang->pipeList( $links ) . '</small>' . $hiddens; } private function getDefaultQuery() { @@ -163,7 +170,7 @@ class LogEventsList { * @return String: Formatted HTML */ private function getUserInput( $user ) { - return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'user', 15, $user ); + return Xml::inputLabel( wfMsg( 'specialloguserlabel' ), 'user', 'mw-log-user', 15, $user ); } /** @@ -171,38 +178,7 @@ class LogEventsList { * @return String: Formatted HTML */ private function getTitleInput( $title ) { - return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'page', 20, $title ); - } - - /** - * @param $year Integer - * @param $month Integer - * @return string Formatted HTML - */ - private function getDateMenu( $year, $month ) { - # Offset overrides year/month selection - if( $month && $month !== -1 ) { - $encMonth = intval( $month ); - } else { - $encMonth = ''; - } - if ( $year ) { - $encYear = intval( $year ); - } else if( $encMonth ) { - $thisMonth = intval( gmdate( 'n' ) ); - $thisYear = intval( gmdate( 'Y' ) ); - if( intval($encMonth) > $thisMonth ) { - $thisYear--; - } - $encYear = $thisYear; - } else { - $encYear = ''; - } - return Xml::label( wfMsg( 'year' ), 'year' ) . ' '. - Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . - ' '. - Xml::label( wfMsg( 'month' ), 'month' ) . ' '. - Xml::monthSelector( $encMonth, -1 ); + return Xml::inputLabel( wfMsg( 'speciallogtitlelabel' ), 'page', 'mw-log-page', 20, $title ); } /** @@ -230,6 +206,7 @@ class LogEventsList { global $wgLang, $wgUser, $wgContLang; $title = Title::makeTitle( $row->log_namespace, $row->log_title ); + $classes = array( "mw-logline-{$row->log_type}" ); $time = $wgLang->timeanddate( wfTimestamp(TS_MW, $row->log_timestamp), true ); // User links if( self::isDeleted($row,LogPage::DELETED_USER) ) { @@ -276,7 +253,7 @@ class LogEventsList { array(), array( 'action' => 'unblock', 'ip' => $row->log_title ), 'known' ) - . ' ' . $this->message['pipe-separator'] . ' ' . + . $this->message['pipe-separator'] . $this->skin->link( SpecialPage::getTitleFor( 'Blockip', $row->log_title ), $this->message['change-blocklink'], array(), array(), 'known' ) . @@ -289,7 +266,7 @@ class LogEventsList { array(), array( 'action' => 'history', 'offset' => $row->log_timestamp ) ); if( $wgUser->isAllowed( 'protect' ) ) { - $revert .= ' ' . $this->message['pipe-separator'] . ' ' . + $revert .= $this->message['pipe-separator'] . $this->skin->link( $title, $this->message['protect_change'], array(), @@ -315,8 +292,17 @@ class LogEventsList { foreach( $Ids as $n => $id ) { $revParams .= '&' . urlencode($key) . '[]=' . urlencode($id); } - $revert = '(' . $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], - 'target=' . $title->getPrefixedUrl() . $revParams ) . ')'; + $revert = array(); + // Diff link for single rev deletions + if( $key === 'oldid' && count($Ids) == 1 ) { + $token = urlencode( $wgUser->editToken( intval($Ids[0]) ) ); + $revert[] = $this->skin->makeKnownLinkObj( $title, $this->message['diff'], + 'diff='.intval($Ids[0])."&unhide=1&token=$token" ); + } + // View/modify link... + $revert[] = $this->skin->makeKnownLinkObj( $revdel, $this->message['revdel-restore'], + 'target=' . $title->getPrefixedUrl() . $revParams ); + $revert = '(' . implode(' | ',$revert) . ')'; } // Hidden log items, give review link } else if( self::typeAction($row,array('delete','suppress'),'event','deleterevision') ) { @@ -357,12 +343,16 @@ class LogEventsList { $this->skin, $paramArray, true ); } + // Any tags... + list($tagDisplay, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'logevent' ); + $classes = array_merge( $classes, $newClasses ); + if( $revert != '' ) { $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>'; } - return Xml::tags( 'li', array( "class" => "mw-logline-$row->log_type" ), - $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert ); + return Xml::tags( 'li', array( "class" => implode( ' ', $classes ) ), + $del . $time . ' ' . $userLink . ' ' . $action . ' ' . $comment . ' ' . $revert . " $tagDisplay" ) . "\n"; } /** @@ -373,19 +363,18 @@ class LogEventsList { $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); // If event was hidden from sysops if( !self::userCan( $row, LogPage::DELETED_RESTRICTED ) ) { - $del = $this->message['rev-delundel']; + $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' ); } else if( $row->log_type == 'suppress' ) { // No one should be hiding from the oversight log - $del = $this->message['rev-delundel']; + $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' ); } else { $target = SpecialPage::getTitleFor( 'Log', $row->log_type ); - $del = $this->skin->makeKnownLinkObj( $revdel, $this->message['rev-delundel'], - 'target=' . $target->getPrefixedUrl() . '&logid='.$row->log_id ); - // Bolden oversighted content - if( self::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) - $del = "<strong>$del</strong>"; + $query = array( 'target' => $target->getPrefixedDBkey(), + 'logid[]' => $row->log_id + ); + $del = $this->skin->revDeleteLink( $query, self::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ); } - return "<tt>(<small>$del</small>)</tt>"; + return $del; } /** @@ -468,15 +457,16 @@ class LogEventsList { /** * SQL clause to skip forbidden log types for this user * @param $db Database + * @param $audience string, public/user * @return mixed (string or false) */ - public static function getExcludeClause( $db ) { + public static function getExcludeClause( $db, $audience = 'public' ) { global $wgLogRestrictions, $wgUser; // Reset the array, clears extra "where" clauses when $par is used $hiddenLogs = array(); // Don't show private logs to unprivileged users foreach( $wgLogRestrictions as $logType => $right ) { - if( !$wgUser->isAllowed($right) ) { + if( $audience == 'public' || !$wgUser->isAllowed($right) ) { $safeType = $db->strencode( $logType ); $hiddenLogs[] = $safeType; } @@ -509,17 +499,18 @@ class LogPager extends ReverseChronologicalPager { * @param $month Integer */ public function __construct( $list, $type = '', $user = '', $title = '', $pattern = '', - $conds = array(), $year = false, $month = false ) + $conds = array(), $year = false, $month = false, $tagFilter = '' ) { parent::__construct(); $this->mConds = $conds; $this->mLogEventsList = $list; - $this->limitType( $type ); + $this->limitType( $type ); // also excludes hidden types $this->limitUser( $user ); $this->limitTitle( $title, $pattern ); $this->getDateCond( $year, $month ); + $this->mTagFilter = $tagFilter; } public function getDefaultQuery() { @@ -560,16 +551,17 @@ class LogPager extends ReverseChronologicalPager { if( isset($wgLogRestrictions[$type]) && !$wgUser->isAllowed($wgLogRestrictions[$type]) ) { $type = ''; } - // Don't show private logs to unpriviledged users - $hideLogs = LogEventsList::getExcludeClause( $this->mDb ); + // Don't show private logs to unpriviledged users. + // Also, only show them upon specific request to avoid suprises. + $audience = $type ? 'user' : 'public'; + $hideLogs = LogEventsList::getExcludeClause( $this->mDb, $audience ); if( $hideLogs !== false ) { $this->mConds[] = $hideLogs; } - if( !$type ) { - return false; + if( $type ) { + $this->type = $type; + $this->mConds['log_type'] = $type; } - $this->type = $type; - $this->mConds['log_type'] = $type; } /** @@ -591,7 +583,12 @@ class LogPager extends ReverseChronologicalPager { but for now it won't pass anywhere behind the optimizer */ $this->mConds[] = "NULL"; } else { + global $wgUser; $this->mConds['log_user'] = $userid; + // Paranoia: avoid brute force searches (bug 17342) + if( !$wgUser->isAllowed( 'suppressrevision' ) ) { + $this->mConds[] = 'log_deleted & ' . LogPage::DELETED_USER . ' = 0'; + } $this->user = $usertitle->getText(); } } @@ -603,7 +600,7 @@ class LogPager extends ReverseChronologicalPager { * @param $pattern String */ private function limitTitle( $page, $pattern ) { - global $wgMiserMode; + global $wgMiserMode, $wgUser; $title = Title::newFromText( $page ); if( strlen($page) == 0 || !$title instanceof Title ) @@ -632,6 +629,10 @@ class LogPager extends ReverseChronologicalPager { $this->mConds['log_namespace'] = $ns; $this->mConds['log_title'] = $title->getDBkey(); } + // Paranoia: avoid brute force searches (bug 17342) + if( !$wgUser->isAllowed( 'suppressrevision' ) ) { + $this->mConds[] = 'log_deleted & ' . LogPage::DELETED_ACTION . ' = 0'; + } } public function getQueryInfo() { @@ -644,13 +645,19 @@ class LogPager extends ReverseChronologicalPager { } else { $index = array( 'USE INDEX' => array( 'logging' => 'times' ) ); } - return array( + $info = array( 'tables' => array( 'logging', 'user' ), 'fields' => array( 'log_type', 'log_action', 'log_user', 'log_namespace', 'log_title', 'log_params', 'log_comment', 'log_id', 'log_deleted', 'log_timestamp', 'user_name', 'user_editcount' ), 'conds' => $this->mConds, - 'options' => $index + 'options' => $index, + 'join_conds' => array( 'user' => array( 'INNER JOIN', 'user_id=log_user' ) ), ); + + ChangeTags::modifyDisplayQuery( $info['tables'], $info['fields'], $info['conds'], + $info['join_conds'], $info['options'], $this->mTagFilter ); + + return $info; } function getIndexField() { @@ -701,6 +708,10 @@ class LogPager extends ReverseChronologicalPager { public function getMonth() { return $this->mMonth; } + + public function getTagFilter() { + return $this->mTagFilter; + } } /** @@ -722,6 +733,7 @@ class LogReader { $pattern = $request->getBool( 'pattern' ); $year = $request->getIntOrNull( 'year' ); $month = $request->getIntOrNull( 'month' ); + $tagFilter = $request->getVal( 'tagfilter' ); # Don't let the user get stuck with a certain date $skip = $request->getText( 'offset' ) || $request->getText( 'dir' ) == 'prev'; if( $skip ) { @@ -730,7 +742,7 @@ class LogReader { } # Use new list class to output results $loglist = new LogEventsList( $wgUser->getSkin(), $wgOut, 0 ); - $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month ); + $this->pager = new LogPager( $loglist, $type, $user, $title, $pattern, $year, $month, $tagFilter ); } /** diff --git a/includes/LogPage.php b/includes/LogPage.php index 50a9a232..0d572385 100644 --- a/includes/LogPage.php +++ b/includes/LogPage.php @@ -37,7 +37,7 @@ class LogPage { /* @access private */ var $type, $action, $comment, $params, $target, $doer; /* @acess public */ - var $updateRecentChanges; + var $updateRecentChanges, $sendToUDP; /** * Constructor @@ -45,15 +45,16 @@ class LogPage { * @param string $type One of '', 'block', 'protect', 'rights', 'delete', * 'upload', 'move' * @param bool $rc Whether to update recent changes as well as the logging table + * @param bool $udp Whether to send to the UDP feed if NOT sent to RC */ - function __construct( $type, $rc = true ) { + public function __construct( $type, $rc = true, $udp = 'skipUDP' ) { $this->type = $type; $this->updateRecentChanges = $rc; + $this->sendToUDP = ($udp == 'UDP'); } protected function saveContent() { - global $wgUser, $wgLogRestrictions; - $fname = 'LogPage::saveContent'; + global $wgLogRestrictions; $dbw = wfGetDB( DB_MASTER ); $log_id = $dbw->nextSequenceValue( 'log_log_id_seq' ); @@ -70,21 +71,25 @@ class LogPage { 'log_comment' => $this->comment, 'log_params' => $this->params ); - $dbw->insert( 'logging', $data, $fname ); + $dbw->insert( 'logging', $data, __METHOD__ ); $newId = !is_null($log_id) ? $log_id : $dbw->insertId(); - if( !($dbw->affectedRows() > 0) ) { - wfDebugLog( "logging", "LogPage::saveContent failed to insert row - Error {$dbw->lastErrno()}: {$dbw->lastError()}" ); - } # And update recentchanges if( $this->updateRecentChanges ) { - # Don't add private logs to RC! - if( !isset($wgLogRestrictions[$this->type]) || $wgLogRestrictions[$this->type]=='*' ) { - $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); - $rcComment = $this->getRcComment(); - RecentChange::notifyLog( $now, $titleObj, $this->doer, $rcComment, '', - $this->type, $this->action, $this->target, $this->comment, $this->params, $newId ); + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); + RecentChange::notifyLog( $now, $titleObj, $this->doer, $this->getRcComment(), '', $this->type, + $this->action, $this->target, $this->comment, $this->params, $newId ); + } else if( $this->sendToUDP ) { + # Don't send private logs to UDP + if( isset($wgLogRestrictions[$this->type]) && $wgLogRestrictions[$this->type] !='*' ) { + return true; } + # Notify external application via UDP. + # We send this to IRC but do not want to add it the RC table. + $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); + $rc = RecentChange::newLogEntry( $now, $titleObj, $this->doer, $this->getRcComment(), '', + $this->type, $this->action, $this->target, $this->comment, $this->params, $newId ); + $rc->notifyRC2UDP(); } return true; } @@ -98,7 +103,7 @@ class LogPage { if ($rcComment == '') $rcComment = $this->comment; else - $rcComment .= ': ' . $this->comment; + $rcComment .= wfMsgForContent( 'colon-separator' ) . $this->comment; } return $rcComment; } @@ -145,7 +150,7 @@ class LogPage { * @param string $type logtype * @return string Headertext of this logtype */ - static function logHeader( $type ) { + public static function logHeader( $type ) { global $wgLogHeaders, $wgMessageCache; $wgMessageCache->loadAllMessages(); return wfMsgExt($wgLogHeaders[$type],array('parseinline')); @@ -155,7 +160,7 @@ class LogPage { * @static * @return HTML string */ - static function actionText( $type, $action, $title = NULL, $skin = NULL, + public static function actionText( $type, $action, $title = NULL, $skin = NULL, $params = array(), $filterWikilinks = false ) { global $wgLang, $wgContLang, $wgLogActions, $wgMessageCache; @@ -196,7 +201,7 @@ class LogPage { } else { $details = ''; array_unshift( $params, $titleLink ); - if ( $key == 'block/block' || $key == 'suppress/block' || $key == 'block/reblock' ) { + if ( preg_match( '/^(block|suppress)\/(block|reblock)$/', $key ) ) { if ( $skin ) { $params[1] = '<span title="' . htmlspecialchars( $params[1] ). '">' . $wgLang->translateBlockExpiry( $params[1] ) . '</span>'; @@ -208,11 +213,19 @@ class LogPage { } else if ( $type == 'protect' && count($params) == 3 ) { $details .= " {$params[1]}"; // restrictions and expiries if( $params[2] ) { - $details .= ' ['.wfMsg('protect-summary-cascade').']'; + if ( $skin ) { + $details .= ' ['.wfMsg('protect-summary-cascade').']'; + } else { + $details .= ' ['.wfMsgForContent('protect-summary-cascade').']'; + } } } else if ( $type == 'move' && count( $params ) == 3 ) { if( $params[2] ) { - $details .= ' [' . wfMsg( 'move-redirect-suppressed' ) . ']'; + if ( $skin ) { + $details .= ' [' . wfMsg( 'move-redirect-suppressed' ) . ']'; + } else { + $details .= ' [' . wfMsgForContent( 'move-redirect-suppressed' ) . ']'; + } } } $rv = wfMsgReal( $wgLogActions[$key], $params, true, !$skin ) . $details; @@ -228,6 +241,17 @@ class LogPage { $rv = "$action"; } } + + // For the perplexed, this feature was added in r7855 by Erik. + // The feature was added because we liked adding [[$1]] in our log entries + // but the log entries are parsed as Wikitext on RecentChanges but as HTML + // on Special:Log. The hack is essentially that [[$1]] represented a link + // to the title in question. The first parameter to the HTML version (Special:Log) + // is that link in HTML form, and so this just gets rid of the ugly [[]]. + // However, this is a horrible hack and it doesn't work like you expect if, say, + // you want to link to something OTHER than the title of the log entry. + // The real problem, which Erik was trying to fix (and it sort-of works now) is + // that the same messages are being treated as both wikitext *and* HTML. if( $filterWikilinks ) { $rv = str_replace( "[[", "", $rv ); $rv = str_replace( "]]", "", $rv ); @@ -296,7 +320,7 @@ class LogPage { * @param array $params Parameters passed later to wfMsg.* functions * @param User $doer The user doing the action */ - function addEntry( $action, $target, $comment, $params = array(), $doer = null ) { + public function addEntry( $action, $target, $comment, $params = array(), $doer = null ) { if ( !is_array( $params ) ) { $params = array( $params ); } @@ -324,7 +348,7 @@ class LogPage { * Create a blob from a parameter array * @static */ - static function makeParamBlob( $params ) { + public static function makeParamBlob( $params ) { return implode( "\n", $params ); } @@ -332,7 +356,7 @@ class LogPage { * Extract a parameter array from a blob * @static */ - static function extractParams( $blob ) { + public static function extractParams( $blob ) { if ( $blob === '' ) { return array(); } else { @@ -350,11 +374,13 @@ class LogPage { * @return string */ public static function formatBlockFlags( $flags, $forContent = false ) { + global $wgLang; + $flags = explode( ',', trim( $flags ) ); if( count( $flags ) > 0 ) { for( $i = 0; $i < count( $flags ); $i++ ) $flags[$i] = self::formatBlockFlag( $flags[$i], $forContent ); - return '(' . implode( ', ', $flags ) . ')'; + return '(' . $wgLang->commaList( $flags ) . ')'; } else { return ''; } diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 5b5b77f0..4e97016d 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -78,6 +78,7 @@ class MagicWord { 'revisionmonth', 'revisionyear', 'revisiontimestamp', + 'revisionuser', 'subpagename', 'subpagenamee', 'displaytitle', @@ -90,7 +91,9 @@ class MagicWord { 'subjectpagename', 'subjectpagenamee', 'numberofusers', + 'numberofactiveusers', 'newsectionlink', + 'nonewsectionlink', 'numberofpages', 'currentversion', 'basepagename', @@ -141,6 +144,7 @@ class MagicWord { 'localweek' => 3600, 'localdow' => 3600, 'numberofusers' => 3600, + 'numberofactiveusers' => 3600, 'numberofpages' => 3600, 'currentversion' => 86400, 'currenttimestamp' => 3600, @@ -158,6 +162,7 @@ class MagicWord { 'toc', 'noeditsection', 'newsectionlink', + 'nonewsectionlink', 'hiddencat', 'index', 'noindex', diff --git a/includes/MessageCache.php b/includes/MessageCache.php index a06b0cb9..2236bdd7 100644 --- a/includes/MessageCache.php +++ b/includes/MessageCache.php @@ -702,7 +702,10 @@ class MessageCache { * @param string $lang The messages language, English by default */ function addMessage( $key, $value, $lang = 'en' ) { - $this->mExtensionMessages[$lang][$key] = $value; + global $wgContLang; + # Normalise title-case input + $lckey = str_replace( ' ', '_', $wgContLang->lcfirst( $key ) ); + $this->mExtensionMessages[$lang][$lckey] = $value; } /** @@ -800,6 +803,7 @@ class MessageCache { */ function loadMessagesFile( $filename, $langcode = false ) { global $wgLang, $wgContLang; + wfProfileIn( __METHOD__ ); $messages = $magicWords = false; require( $filename ); @@ -822,6 +826,7 @@ class MessageCache { global $wgContLang; $wgContLang->addMagicWordsByLang( $magicWords ); } + wfProfileOut( __METHOD__ ); } /** @@ -831,6 +836,7 @@ class MessageCache { * @param string $langcode Language code to process. */ function processMessagesArray( $messages, $langcode ) { + wfProfileIn( __METHOD__ ); $fallbackCode = $langcode; $mergedMessages = array(); do { @@ -842,6 +848,7 @@ class MessageCache { if ( !empty($mergedMessages) ) $this->addMessages( $mergedMessages, $langcode ); + wfProfileOut( __METHOD__ ); } public function figureMessage( $key ) { diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index 4797752d..d52de994 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -579,22 +579,22 @@ class MimeMagic { */ function detectZipType( $header ) { $opendocTypes = array( - 'chart', 'chart-template', - 'formula', + 'chart', 'formula-template', - 'graphics', + 'formula', 'graphics-template', - 'image', + 'graphics', 'image-template', - 'presentation', + 'image', 'presentation-template', - 'spreadsheet', + 'presentation', 'spreadsheet-template', - 'text', + 'spreadsheet', 'text-template', 'text-master', - 'text-web' ); + 'text-web', + 'text' ); // http://lists.oasis-open.org/archives/office/200505/msg00006.html $types = '(?:' . implode( '|', $opendocTypes ) . ')'; diff --git a/includes/OutputHandler.php b/includes/OutputHandler.php index 2b3e9fae..1f4798b7 100644 --- a/includes/OutputHandler.php +++ b/includes/OutputHandler.php @@ -10,7 +10,7 @@ function wfOutputHandler( $s ) { $headers = apache_response_headers(); $isHTML = true; foreach ( $headers as $name => $value ) { - if ( strtolower( $name ) == 'content-type' && strpos( $value, 'text/html' ) === false ) { + if ( strtolower( $name ) == 'content-type' && strpos( $value, 'text/html' ) === false && strpos( $value, 'application/xhtml+xml' ) === false ) { $isHTML = false; break; } @@ -123,10 +123,9 @@ function wfDoContentLength( $length ) { * Replace the output with an error if the HTML is not valid */ function wfHtmlValidationHandler( $s ) { - global $IP; - $tidy = new tidy; - $tidy->parseString( $s, "$IP/includes/tidy.conf", 'utf8' ); - if ( $tidy->getStatus() == 0 ) { + + $errors = ''; + if ( MWTidy::checkErrors( $s, $errors ) ) { return $s; } @@ -134,7 +133,7 @@ function wfHtmlValidationHandler( $s ) { $out = <<<EOT <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr"> <head> <title>HTML validation error</title> <style> @@ -147,7 +146,7 @@ li { white-space: pre } <ul> EOT; - $error = strtok( $tidy->errorBuffer, "\n" ); + $error = strtok( $errors, "\n" ); $badLines = array(); while ( $error !== false ) { if ( preg_match( '/^line (\d+)/', $error, $m ) ) { @@ -158,8 +157,9 @@ EOT; $error = strtok( "\n" ); } - $out .= '<pre>' . htmlspecialchars( $tidy->errorBuffer ) . '</pre>'; - $out .= '<ol>'; + $out .= '</ul>'; + $out .= '<pre>' . htmlspecialchars( $errors ) . '</pre>'; + $out .= "<ol>\n"; $line = strtok( $s, "\n" ); $i = 1; while ( $line !== false ) { @@ -168,7 +168,7 @@ EOT; } else { $out .= '<li>'; } - $out .= htmlspecialchars( $line ) . '</li>'; + $out .= htmlspecialchars( $line ) . "</li>\n"; $line = strtok( "\n" ); $i++; } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index f8dba714..ed9a43d3 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -29,6 +29,7 @@ class OutputPage { var $mArticleBodyOnly = false; var $mNewSectionLink = false; + var $mHideNewSectionLink = false; var $mNoGallery = false; var $mPageTitleActionText = ''; var $mParseWarnings = array(); @@ -165,7 +166,7 @@ class OutputPage { * * @return bool True iff cache-ok headers was sent. */ - function checkLastModified ( $timestamp ) { + function checkLastModified( $timestamp ) { global $wgCachePages, $wgCacheEpoch, $wgUser, $wgRequest; if ( !$timestamp || $timestamp == '19700101000000' ) { @@ -232,6 +233,7 @@ class OutputPage { # Not modified # Give a 304 response code and disable body output wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", false ); + ini_set('zlib.output_compression', 0); $wgRequest->response()->header( "HTTP/1.1 304 Not Modified" ); $this->sendCacheControl(); $this->disable(); @@ -309,20 +311,20 @@ class OutputPage { } } - public function setHTMLTitle( $name ) {$this->mHTMLtitle = $name; } + public function setHTMLTitle( $name ) { $this->mHTMLtitle = $name; } public function setPageTitle( $name ) { - global $action, $wgContLang; - $name = $wgContLang->convert($name, true); + global $wgContLang; + $name = $wgContLang->convert( $name, true ); $this->mPagetitle = $name; - if(!empty($action)) { - $taction = $this->getPageTitleActionText(); - if( !empty( $taction ) ) { - $name .= ' - '.$taction; - } + + $taction = $this->getPageTitleActionText(); + if( !empty( $taction ) ) { + $name .= ' - '.$taction; } $this->setHTMLTitle( wfMsg( 'pagetitle', $name ) ); } + public function getHTMLTitle() { return $this->mHTMLtitle; } public function getPageTitle() { return $this->mPagetitle; } public function setSubtitle( $str ) { $this->mSubtitle = /*$this->parse(*/$str/*)*/; } // @bug 2514 @@ -338,6 +340,7 @@ class OutputPage { public function setOnloadHandler( $js ) { $this->mOnloadHandler = $js; } public function getOnloadHandler() { return $this->mOnloadHandler; } public function disable() { $this->mDoNothing = true; } + public function isDisabled() { return $this->mDoNothing; } public function setArticleRelated( $v ) { $this->mIsArticleRelated = $v; @@ -408,7 +411,12 @@ class OutputPage { if ( wfRunHooks( 'OutputPageMakeCategoryLinks', array( &$this, $categories, &$this->mCategoryLinks ) ) ) { $sk = $wgUser->getSkin(); foreach ( $categories as $category => $type ) { + $origcategory = $category; $title = Title::makeTitleSafe( NS_CATEGORY, $category ); + $wgContLang->findVariantLink( $category, $title, true ); + if ( $category != $origcategory ) + if ( array_key_exists( $category, $categories ) ) + continue; $text = $wgContLang->convertHtml( $title->getText() ); $this->mCategoryLinks[$type][] = $sk->makeLinkObj( $title, $text ); } @@ -511,6 +519,7 @@ class OutputPage { $this->mLanguageLinks += $parserOutput->getLanguageLinks(); $this->addCategoryLinks( $parserOutput->getCategories() ); $this->mNewSectionLink = $parserOutput->getNewSection(); + $this->mHideNewSectionLink = $parserOutput->getHideNewSection(); if( is_null( $wgExemptFromUserRobotsControl ) ) { $bannedNamespaces = $wgContentNamespaces; @@ -538,9 +547,11 @@ class OutputPage { $this->mTemplateIds[$ns] = $dbks; } } - // Display title + // Page title if( ( $dt = $parserOutput->getDisplayTitle() ) !== false ) $this->setPageTitle( $dt ); + else if ( ( $title = $parserOutput->getTitleText() ) != '' ) + $this->setPageTitle( $title ); // Hooks registered in the object global $wgParserOutputHooks; @@ -586,7 +597,7 @@ class OutputPage { $popts->setTidy(false); if ( $cache && $article && $parserOutput->getCacheTime() != -1 ) { $parserCache = ParserCache::singleton(); - $parserCache->save( $parserOutput, $article, $wgUser ); + $parserCache->save( $parserOutput, $article, $popts); } $this->addParserOutput( $parserOutput ); @@ -642,15 +653,27 @@ class OutputPage { return $parserOutput->getText(); } + /** Parse wikitext, strip paragraphs, and return the HTML. */ + public function parseInline( $text, $linestart = true, $interface = false ) { + $parsed = $this->parse( $text, $linestart, $interface ); + + $m = array(); + if ( preg_match( '/^<p>(.*)\n?<\/p>\n?/sU', $parsed, $m ) ) { + $parsed = $m[1]; + } + + return $parsed; + } + /** * @param Article $article * @param User $user * * @return bool True if successful, else false. */ - public function tryParserCache( &$article, $user ) { + public function tryParserCache( &$article ) { $parserCache = ParserCache::singleton(); - $parserOutput = $parserCache->get( $article, $user ); + $parserOutput = $parserCache->get( $article, $this->parserOptions() ); if ( $parserOutput !== false ) { $this->addParserOutput( $parserOutput ); return true; @@ -917,13 +940,13 @@ class OutputPage { 'rel' => 'alternate', 'type' => 'application/x-wiki', 'title' => wfMsg( 'edit' ), - 'href' => $wgTitle->getFullURL( 'action=edit' ) + 'href' => $wgTitle->getLocalURL( 'action=edit' ) ) ); // Alternate edit link $this->addLink( array( 'rel' => 'edit', 'title' => wfMsg( 'edit' ), - 'href' => $wgTitle->getFullURL( 'action=edit' ) + 'href' => $wgTitle->getLocalURL( 'action=edit' ) ) ); } } @@ -1132,7 +1155,7 @@ class OutputPage { * @param string $permission key required */ public function permissionRequired( $permission ) { - global $wgUser; + global $wgUser, $wgLang; $this->setPageTitle( wfMsg( 'badaccess' ) ); $this->setHTMLTitle( wfMsg( 'errorpagetitle' ) ); @@ -1144,7 +1167,7 @@ class OutputPage { User::getGroupsWithPermission( $permission ) ); if( $groups ) { $this->addWikiMsg( 'badaccess-groups', - implode( ', ', $groups ), + $wgLang->commaList( $groups ), count( $groups) ); } else { $this->addWikiMsg( 'badaccess-group0' ); @@ -1457,7 +1480,7 @@ class OutputPage { $ret = ''; if( $wgMimeType == 'text/xml' || $wgMimeType == 'application/xhtml+xml' || $wgMimeType == 'application/xml' ) { - $ret .= "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?>\n"; + $ret .= "<?xml version=\"1.0\" encoding=\"$wgOutputEncoding\" ?" . ">\n"; } $ret .= "<!DOCTYPE html PUBLIC \"$wgDocType\"\n \"$wgDTD\">\n"; @@ -1505,7 +1528,7 @@ class OutputPage { if ( count( $this->mKeywords ) > 0 ) { $strip = array( - "/<.*?>/" => '', + "/<.*?" . ">/" => '', "/_/" => ' ' ); $this->addMeta( 'keywords', preg_replace(array_keys($strip), array_values($strip),implode( ",", $this->mKeywords ) ) ); @@ -1576,7 +1599,7 @@ class OutputPage { foreach( $wgFeedClasses as $format => $class ) { $tags[] = $this->feedLink( $format, - $rctitle->getFullURL( "feed={$format}" ), + $rctitle->getLocalURL( "feed={$format}" ), wfMsg( "site-{$format}-feed", $wgSitename ) ); # For grep: 'site-rss-feed', 'site-atom-feed'. } } @@ -1759,6 +1782,15 @@ class OutputPage { } /** + * Forcibly hide the new section link? + * + * @return bool + */ + public function forceHideNewSectionLink() { + return $this->mHideNewSectionLink; + } + + /** * Show a warning about slave lag * * If the lag is higher than $wgSlaveLagCritical seconds, @@ -1843,7 +1875,7 @@ class OutputPage { $args = array(); $name = $spec; } - $s = str_replace( '$' . ($n+1), wfMsgExt( $name, $options, $args ), $s ); + $s = str_replace( '$' . ( $n + 1 ), wfMsgExt( $name, $options, $args ), $s ); } $this->addHTML( $this->parse( $s, /*linestart*/true, /*uilang*/true ) ); } diff --git a/includes/PageHistory.php b/includes/PageHistory.php index b01b485e..9477981f 100644 --- a/includes/PageHistory.php +++ b/includes/PageHistory.php @@ -23,6 +23,8 @@ class PageHistory { var $lastdate; var $linesonpage; var $mLatestId = null; + + private $mOldIdChecked = 0; /** * Construct a new PageHistory. @@ -112,6 +114,8 @@ class PageHistory { */ $year = $wgRequest->getInt( 'year' ); $month = $wgRequest->getInt( 'month' ); + $tagFilter = $wgRequest->getVal( 'tagfilter' ); + $tagSelector = ChangeTags::buildTagFilterSelector( $tagFilter ); $action = htmlspecialchars( $wgScript ); $wgOut->addHTML( @@ -119,7 +123,8 @@ class PageHistory { Xml::fieldset( wfMsg( 'history-fieldset-title' ), false, array( 'id' => 'mw-history-search' ) ) . Xml::hidden( 'title', $this->mTitle->getPrefixedDBKey() ) . "\n" . Xml::hidden( 'action', 'history' ) . "\n" . - $this->getDateMenu( $year, $month ) . ' ' . + xml::dateMenu( $year, $month ) . ' ' . + ( $tagSelector ? ( implode( ' ', $tagSelector ) . ' ' ) : '' ) . Xml::submitButton( wfMsg( 'allpagessubmit' ) ) . "\n" . '</fieldset></form>' ); @@ -129,7 +134,7 @@ class PageHistory { /** * Do the list */ - $pager = new PageHistoryPager( $this, $year, $month ); + $pager = new PageHistoryPager( $this, $year, $month, $tagFilter ); $this->linesonpage = $pager->getNumRows(); $wgOut->addHTML( $pager->getNavigationBar() . @@ -143,37 +148,6 @@ class PageHistory { } /** - * @return string Formatted HTML - * @param int $year - * @param int $month - */ - private function getDateMenu( $year, $month ) { - # Offset overrides year/month selection - if( $month && $month !== -1 ) { - $encMonth = intval( $month ); - } else { - $encMonth = ''; - } - if( $year ) { - $encYear = intval( $year ); - } else if( $encMonth ) { - $thisMonth = intval( gmdate( 'n' ) ); - $thisYear = intval( gmdate( 'Y' ) ); - if( intval($encMonth) > $thisMonth ) { - $thisYear--; - } - $encYear = $thisYear; - } else { - $encYear = ''; - } - return Xml::label( wfMsg( 'year' ), 'year' ) . ' '. - Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . - ' '. - Xml::label( wfMsg( 'month' ), 'month' ) . ' '. - Xml::monthSelector( $encMonth, -1 ); - } - - /** * Creates begin of history list with a submit button * * @return string HTML output @@ -287,37 +261,34 @@ class PageHistory { $lastlink = $this->lastLink( $rev, $next, $counter ); $arbitrary = $this->diffButtons( $rev, $firstInList, $counter ); $link = $this->revLink( $rev ); + $classes = array(); $s = "($curlink) ($lastlink) $arbitrary"; if( $wgUser->isAllowed( 'deleterevision' ) ) { - $revdel = SpecialPage::getTitleFor( 'Revisiondelete' ); - if( $firstInList ) { + if( $latest ) { // We don't currently handle well changing the top revision's settings - $del = $this->message['rev-delundel']; + $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' ); } else if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) { // If revision was hidden from sysops - $del = $this->message['rev-delundel']; + $del = Xml::tags( 'span', array( 'class'=>'mw-revdelundel-link' ), '('.$this->message['rev-delundel'].')' ); } else { - $del = $this->mSkin->makeKnownLinkObj( $revdel, - $this->message['rev-delundel'], - 'target=' . urlencode( $this->mTitle->getPrefixedDbkey() ) . - '&oldid=' . urlencode( $rev->getId() ) ); - // Bolden oversighted content - if( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) - $del = "<strong>$del</strong>"; + $query = array( 'target' => $this->mTitle->getPrefixedDbkey(), + 'oldid' => $rev->getId() + ); + $del = $this->mSkin->revDeleteLink( $query, $rev->isDeleted( Revision::DELETED_RESTRICTED ) ); } - $s .= " <tt>(<small>$del</small>)</tt> "; + $s .= " $del "; } $s .= " $link"; $s .= " <span class='history-user'>" . $this->mSkin->revUserTools( $rev, true ) . "</span>"; - if( $row->rev_minor_edit ) { + if( $rev->isMinor() ) { $s .= ' ' . Xml::element( 'span', array( 'class' => 'minor' ), wfMsg( 'minoreditletter') ); } - if( !is_null( $size = $rev->getSize() ) && $rev->userCan( Revision::DELETED_TEXT ) ) { + if( !is_null( $size = $rev->getSize() ) && !$rev->isDeleted( Revision::DELETED_TEXT ) ) { $s .= ' ' . $this->mSkin->formatRevisionSize( $size ); } @@ -356,12 +327,19 @@ class PageHistory { } if( $tools ) { - $s .= ' (' . implode( ' | ', $tools ) . ')'; + $s .= ' (' . $wgLang->pipeList( $tools ) . ')'; } + # Tags + list($tagSummary, $newClasses) = ChangeTags::formatSummaryRow( $row->ts_tags, 'history' ); + $classes = array_merge( $classes, $newClasses ); + $s .= " $tagSummary"; + wfRunHooks( 'PageHistoryLineEnding', array( $this, &$row , &$s ) ); - return "<li>$s</li>\n"; + $classes = implode( ' ', $classes ); + + return "<li class=\"$classes\">$s</li>\n"; } /** @@ -372,14 +350,10 @@ class PageHistory { function revLink( $rev ) { global $wgLang; $date = $wgLang->timeanddate( wfTimestamp(TS_MW, $rev->getTimestamp()), true ); - if( $rev->userCan( Revision::DELETED_TEXT ) ) { - $link = $this->mSkin->makeKnownLinkObj( - $this->mTitle, $date, "oldid=" . $rev->getId() ); + if( !$rev->isDeleted( Revision::DELETED_TEXT ) ) { + $link = $this->mSkin->makeKnownLinkObj( $this->mTitle, $date, "oldid=" . $rev->getId() ); } else { - $link = $date; - } - if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { - return '<span class="history-deleted">' . $link . '</span>'; + $link = '<span class="history-deleted">' . $date . '</span>'; } return $link; } @@ -392,7 +366,7 @@ class PageHistory { */ function curLink( $rev, $latest ) { $cur = $this->message['cur']; - if( $latest || !$rev->userCan( Revision::DELETED_TEXT ) ) { + if( $latest || $rev->isDeleted( Revision::DELETED_TEXT ) ) { return $cur; } else { return $this->mSkin->makeKnownLinkObj( $this->mTitle, $cur, @@ -418,7 +392,7 @@ class PageHistory { # Next row probably exists but is unknown, use an oldid=prev link return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last, "diff=" . $prevRev->getId() . "&oldid=prev" ); - } elseif( !$prevRev->userCan(Revision::DELETED_TEXT) || !$nextRev->userCan(Revision::DELETED_TEXT) ) { + } elseif( $prevRev->isDeleted(Revision::DELETED_TEXT) || $nextRev->isDeleted(Revision::DELETED_TEXT) ) { return $last; } else { return $this->mSkin->makeKnownLinkObj( $this->mTitle, $last, @@ -435,40 +409,29 @@ class PageHistory { * @return string HTML output for the radio buttons */ function diffButtons( $rev, $firstInList, $counter ) { - if( $this->linesonpage > 1) { - $radio = array( - 'type' => 'radio', - 'value' => $rev->getId(), - ); - - if( !$rev->userCan( Revision::DELETED_TEXT ) ) { - $radio['disabled'] = 'disabled'; - } - + if( $this->linesonpage > 1 ) { + $radio = array( 'type' => 'radio', 'value' => $rev->getId() ); /** @todo: move title texts to javascript */ if( $firstInList ) { - $first = Xml::element( 'input', array_merge( - $radio, - array( - 'style' => 'visibility:hidden', - 'name' => 'oldid' ) ) ); + $first = Xml::element( 'input', + array_merge( $radio, array( 'style' => 'visibility:hidden', 'name' => 'oldid' ) ) + ); $checkmark = array( 'checked' => 'checked' ); } else { - if( $counter == 2 ) { + # Check visibility of old revisions + if( $rev->isDeleted( Revision::DELETED_TEXT ) ) { + $radio['disabled'] = 'disabled'; + $checkmark = array(); // We will check the next possible one + } else if( $counter == 2 || !$this->mOldIdChecked ) { $checkmark = array( 'checked' => 'checked' ); + $this->mOldIdChecked = $rev->getId(); } else { $checkmark = array(); } - $first = Xml::element( 'input', array_merge( - $radio, - $checkmark, - array( 'name' => 'oldid' ) ) ); + $first = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'oldid' ) ) ); $checkmark = array(); } - $second = Xml::element( 'input', array_merge( - $radio, - $checkmark, - array( 'name' => 'diff' ) ) ); + $second = Xml::element( 'input', array_merge( $radio, $checkmark, array( 'name' => 'diff' ) ) ); return $first . $second; } else { return ''; @@ -573,7 +536,7 @@ class PageHistory { $rev->getUserText(), $wgContLang->timeanddate( $rev->getTimestamp() ) ); } else { - $title = $rev->getUserText() . ": " . FeedItem::stripComment( $rev->getComment() ); + $title = $rev->getUserText() . wfMsgForContent( 'colon-separator' ) . FeedItem::stripComment( $rev->getComment() ); } return new FeedItem( @@ -593,20 +556,28 @@ class PageHistory { class PageHistoryPager extends ReverseChronologicalPager { public $mLastRow = false, $mPageHistory, $mTitle; - function __construct( $pageHistory, $year='', $month='' ) { + function __construct( $pageHistory, $year='', $month='', $tagFilter = '' ) { parent::__construct(); $this->mPageHistory = $pageHistory; $this->mTitle =& $this->mPageHistory->mTitle; + $this->tagFilter = $tagFilter; $this->getDateCond( $year, $month ); } function getQueryInfo() { $queryInfo = array( 'tables' => array('revision'), - 'fields' => Revision::selectFields(), + 'fields' => array_merge( Revision::selectFields(), array('ts_tags') ), 'conds' => array('rev_page' => $this->mPageHistory->mTitle->getArticleID() ), - 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ) + 'options' => array( 'USE INDEX' => array('revision' => 'page_timestamp') ), + 'join_conds' => array( 'tag_summary' => array( 'LEFT JOIN', 'ts_rev_id=rev_id' ) ), ); + ChangeTags::modifyDisplayQuery( $queryInfo['tables'], + $queryInfo['fields'], + $queryInfo['conds'], + $queryInfo['join_conds'], + $queryInfo['options'], + $this->tagFilter ); wfRunHooks( 'PageHistoryPager::getQueryInfo', array( &$this, &$queryInfo ) ); return $queryInfo; } diff --git a/includes/Pager.php b/includes/Pager.php index 8ec32ff4..8faec533 100644 --- a/includes/Pager.php +++ b/includes/Pager.php @@ -304,20 +304,18 @@ abstract class IndexPager implements Pager { if ( $query === null ) { return $text; } - if( $type == 'prev' || $type == 'next' ) { - $attrs = "rel=\"$type\""; - } elseif( $type == 'first' ) { - $attrs = "rel=\"start\""; - } else { - # HTML 4 has no rel="end" . . . - $attrs = ''; + + $attrs = array(); + if( in_array( $type, array( 'first', 'prev', 'next', 'last' ) ) ) { + # HTML5 rel attributes + $attrs['rel'] = $type; } if( $type ) { - $attrs .= " class=\"mw-{$type}link\"" ; + $attrs['class'] = "mw-{$type}link"; } - return $this->getSkin()->makeKnownLinkObj( $this->getTitle(), $text, - wfArrayToCGI( $query, $this->getDefaultQuery() ), '', '', $attrs ); + return $this->getSkin()->link( $this->getTitle(), $text, + $attrs, $query + $this->getDefaultQuery(), array('noclasses','known') ); } /** @@ -532,10 +530,10 @@ abstract class AlphabeticPager extends IndexPager { $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); - $limits = implode( ' | ', $limitLinks ); + $limits = $wgLang->pipeList( $limitLinks ); $this->mNavigationBar = - "({$pagingLinks['first']} | {$pagingLinks['last']}) " . + "(" . $wgLang->pipeList( array( $pagingLinks['first'], $pagingLinks['last'] ) ) . ") " . wfMsgHtml( 'viewprevnext', $pagingLinks['prev'], $pagingLinks['next'], $limits ); @@ -551,7 +549,7 @@ abstract class AlphabeticPager extends IndexPager { if( $first ) { $first = false; } else { - $extra .= ' | '; + $extra .= wfMsgExt( 'pipe-separator' , 'escapenoentities' ); } if( $order == $this->mOrderType ) { @@ -612,9 +610,9 @@ abstract class ReverseChronologicalPager extends IndexPager { $pagingLinks = $this->getPagingLinks( $linkTexts ); $limitLinks = $this->getLimitLinks(); - $limits = implode( ' | ', $limitLinks ); + $limits = $wgLang->pipeList( $limitLinks ); - $this->mNavigationBar = "({$pagingLinks['first']} | {$pagingLinks['last']}) " . + $this->mNavigationBar = "({$pagingLinks['first']}" . wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . "{$pagingLinks['last']}) " . wfMsgHtml("viewprevnext", $pagingLinks['prev'], $pagingLinks['next'], $limits); return $this->mNavigationBar; } @@ -747,7 +745,8 @@ abstract class TablePager extends IndexPager { } function formatRow( $row ) { - $s = "<tr>\n"; + $rowClass = $this->getRowClass( $row ); + $s = "<tr class=\"$rowClass\">\n"; $fieldNames = $this->getFieldNames(); $this->mCurrentRow = $row; # In case formatValue needs to know foreach ( $fieldNames as $field => $name ) { @@ -763,6 +762,10 @@ abstract class TablePager extends IndexPager { return $s; } + function getRowClass($row) { + return ''; + } + function getIndexField() { return $this->mSort; } diff --git a/includes/PatrolLog.php b/includes/PatrolLog.php index 5f305c10..978821c1 100644 --- a/includes/PatrolLog.php +++ b/includes/PatrolLog.php @@ -14,22 +14,20 @@ class PatrolLog { * @param mixed $change Change identifier or RecentChange object * @param bool $auto Was this patrol event automatic? */ - public static function record( $change, $auto = false ) { - if( !( is_object( $change ) && $change instanceof RecentChange ) ) { - $change = RecentChange::newFromId( $change ); - if( !is_object( $change ) ) + public static function record( $rc, $auto = false ) { + if( !( $rc instanceof RecentChange ) ) { + $rc = RecentChange::newFromId( $rc ); + if( !is_object( $rc ) ) return false; } - $title = Title::makeTitleSafe( $change->getAttribute( 'rc_namespace' ), - $change->getAttribute( 'rc_title' ) ); + $title = Title::makeTitleSafe( $rc->getAttribute( 'rc_namespace' ), $rc->getAttribute( 'rc_title' ) ); if( is_object( $title ) ) { - $params = self::buildParams( $change, $auto ); - $log = new LogPage( 'patrol', false ); # False suppresses RC entries + $params = self::buildParams( $rc, $auto ); + $log = new LogPage( 'patrol', false, $auto ? "skipUDP" : "UDP" ); # False suppresses RC entries $log->addEntry( 'patrol', $title, '', $params ); return true; - } else { - return false; } + return false; } /** @@ -41,12 +39,8 @@ class PatrolLog { * @return string */ public static function makeActionText( $title, $params, $skin ) { - # This is a bit of a hack, but...if $skin is not a Skin, then *do nothing* - # -- this is fine, because the action text we would be queried for under - # these conditions would have gone into recentchanges, which we aren't - # supposed to be updating + list( $cur, /* $prev */, $auto ) = $params; if( is_object( $skin ) ) { - list( $cur, /* $prev */, $auto ) = $params; # Standard link to the page in question $link = $skin->makeLinkObj( $title ); if( $title->exists() ) { @@ -64,7 +58,8 @@ class PatrolLog { # Put it all together return wfMsgHtml( 'patrol-log-line', $diff, $link, $auto ); } else { - return ''; + $text = $title->getPrefixedText(); + return wfMsgForContent( 'patrol-log-line', wfMsgHtml('patrol-log-diff',$cur), "[[$text]]", '' ); } } diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index af569112..10c85930 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -135,7 +135,7 @@ class PrefixSearch { // Reformat useful data for future printing by JSON engine $srchres = array (); - foreach ($data['query']['allpages'] as & $pageinfo) { + foreach ((array)$data['query']['allpages'] as $pageinfo) { // Note: this data will no be printable by the xml engine // because it does not support lists of unnamed items $srchres[] = $pageinfo['title']; diff --git a/includes/Profiler.php b/includes/Profiler.php index ffb48978..80a6a68a 100644 --- a/includes/Profiler.php +++ b/includes/Profiler.php @@ -78,8 +78,8 @@ class Profiler { * @param $functionname string */ function profileIn( $functionname ) { - global $wgDebugFunctionEntry; - + global $wgDebugFunctionEntry, $wgProfiling; + if( !$wgProfiling ) return; if( $wgDebugFunctionEntry ){ $this->debug( str_repeat( ' ', count( $this->mWorkStack ) ) . 'Entering ' . $functionname . "\n" ); } @@ -92,8 +92,8 @@ class Profiler { * @param $functionname string */ function profileOut($functionname) { - global $wgDebugFunctionEntry; - + global $wgDebugFunctionEntry, $wgProfiling; + if( !$wgProfiling ) return; $memory = memory_get_usage(); $time = $this->getTime(); @@ -145,7 +145,12 @@ class Profiler { } $this->close(); - if( $wgProfileCallTree ){ + if( $wgProfileCallTree ) { + global $wgProfileToDatabase; + # XXX: We must call $this->getFunctionReport() to log to the DB + if( $wgProfileToDatabase ) { + $this->getFunctionReport(); + } return $this->getCallTree(); } else { return $this->getFunctionReport(); @@ -202,16 +207,13 @@ class Profiler { /** * Callback to get a formatted line for the call tree */ - function getCallTreeLine($entry) { + function getCallTreeLine( $entry ) { list( $fname, $level, $start, /* $x */, $end) = $entry; $delta = $end - $start; $space = str_repeat(' ', $level); - # The ugly double sprintf is to work around a PHP bug, # which has been fixed in recent releases. - return sprintf( "%10s %s %s\n", - trim( sprintf( "%7.3f", $delta * 1000.0 ) ), - $space, $fname ); + return sprintf( "%10s %s %s\n", trim( sprintf( "%7.3f", $delta * 1000.0 ) ), $space, $fname ); } function getTime() { @@ -316,8 +318,8 @@ class Profiler { $percent = $total ? 100. * $elapsed / $total : 0; $memory = $this->mMemory[$fname]; $prof .= sprintf($format, substr($fname, 0, $nameWidth), $calls, (float) ($elapsed * 1000), (float) ($elapsed * 1000) / $calls, $percent, $memory, ($this->mMin[$fname] * 1000.0), ($this->mMax[$fname] * 1000.0), $this->mOverhead[$fname]); - - if( $wgProfileToDatabase ){ + # Log to the DB + if( $wgProfileToDatabase ) { self::logToDB($fname, (float) ($elapsed * 1000), $calls, (float) ($memory) ); } } diff --git a/includes/ProfilerSimple.php b/includes/ProfilerSimple.php index 349a7cac..5989061d 100644 --- a/includes/ProfilerSimple.php +++ b/includes/ProfilerSimple.php @@ -4,7 +4,9 @@ * @ingroup Profiler */ -require_once(dirname(__FILE__).'/Profiler.php'); +if ( !class_exists( 'Profiler' ) ) { + require_once(dirname(__FILE__).'/Profiler.php'); +} /** * Simple profiler base class. diff --git a/includes/ProfilerSimpleTrace.php b/includes/ProfilerSimpleTrace.php new file mode 100644 index 00000000..63119228 --- /dev/null +++ b/includes/ProfilerSimpleTrace.php @@ -0,0 +1,73 @@ +<?php +/** + * @file + * @ingroup Profiler + */ + +if ( !class_exists( 'ProfilerSimple' ) ) { + require_once(dirname(__FILE__).'/ProfilerSimple.php'); +} + +/** + * Execution trace + * @todo document methods (?) + * @ingroup Profiler + */ +class ProfilerSimpleTrace extends ProfilerSimple { + var $mMinimumTime = 0; + var $mProfileID = false; + var $trace = ""; + var $memory = 0; + + function __construct() { + global $wgRequestTime, $wgRUstart; + if (!empty($wgRequestTime) && !empty($wgRUstart)) { + $this->mWorkStack[] = array( '-total', 0, $wgRequestTime,$this->getCpuTime($wgRUstart)); + $elapsedcpu = $this->getCpuTime() - $this->getCpuTime($wgRUstart); + $elapsedreal = microtime(true) - $wgRequestTime; + } + $this->trace .= "Beginning trace: \n"; + } + + function profileIn($functionname) { + global $wgDebugFunctionEntry; + $this->mWorkStack[] = array($functionname, count( $this->mWorkStack ), microtime(true), $this->getCpuTime()); + $this->trace .= " " . sprintf("%6.1f",$this->memoryDiff()) . str_repeat( " ", count($this->mWorkStack)) . " > " . $functionname . "\n"; + } + + function profileOut($functionname) { + global $wgDebugFunctionEntry; + + if ($wgDebugFunctionEntry) { + $this->debug(str_repeat(' ', count($this->mWorkStack) - 1).'Exiting '.$functionname."\n"); + } + + list($ofname, /* $ocount */ ,$ortime,$octime) = array_pop($this->mWorkStack); + + if (!$ofname) { + $this->trace .= "Profiling error: $functionname\n"; + } else { + if ($functionname == 'close') { + $message = "Profile section ended by close(): {$ofname}"; + $functionname = $ofname; + $this->trace .= $message . "\n"; + } + elseif ($ofname != $functionname) { + $self->trace .= "Profiling error: in({$ofname}), out($functionname)"; + } + $elapsedcpu = $this->getCpuTime() - $octime; + $elapsedreal = microtime(true) - $ortime; + $this->trace .= sprintf("%03.6f %6.1f",$elapsedreal,$this->memoryDiff()) . str_repeat(" ",count($this->mWorkStack)+1) . " < " . $functionname . "\n"; + } + } + + function memoryDiff() { + $diff = memory_get_usage() - $this->memory; + $this->memory = memory_get_usage(); + return $diff/1024; + } + + function getOutput() { + print "<!-- \n {$this->trace} \n -->"; + } +} diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index 372edfcd..5fe3cbc7 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -235,7 +235,7 @@ class ProtectionForm { $reasonstr = $this->mReasonSelection; if ( $reasonstr != 'other' && $this->mReason != '' ) { // Entry from drop down menu + additional comment - $reasonstr .= ': ' . $this->mReason; + $reasonstr .= wfMsgForContent( 'colon-separator' ) . $this->mReason; } elseif ( $reasonstr == 'other' ) { $reasonstr = $this->mReason; } @@ -374,7 +374,8 @@ class ProtectionForm { </tr></table>"; } # Add custom expiry field - $attribs = array( 'id' => "mwProtect-$action-expires", 'onkeyup' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib; + $attribs = array( 'id' => "mwProtect-$action-expires", + 'onkeyup' => 'ProtectionForm.updateExpiry(this)' ) + $this->disabledAttrib; $out .= "<table><tr> <td class='mw-label'>" . $mProtectother . diff --git a/includes/QueryPage.php b/includes/QueryPage.php index 0b587508..1cef31ea 100644 --- a/includes/QueryPage.php +++ b/includes/QueryPage.php @@ -204,7 +204,7 @@ class QueryPage { * Clear the cache and save new results */ function recache( $limit, $ignoreErrors = true ) { - $fname = get_class($this) . '::recache'; + $fname = get_class( $this ) . '::recache'; $dbw = wfGetDB( DB_MASTER ); $dbr = wfGetDB( DB_SLAVE, array( $this->getName(), 'QueryPage::recache', 'vslow' ) ); if ( !$dbw || !$dbr ) { @@ -222,9 +222,9 @@ class QueryPage { $dbw->delete( 'querycache', array( 'qc_type' => $this->getName() ), $fname ); # Do query $sql = $this->getSQL() . $this->getOrder(); - if ($limit !== false) - $sql = $dbr->limitResult($sql, $limit, 0); - $res = $dbr->query($sql, $fname); + if ( $limit !== false ) + $sql = $dbr->limitResult( $sql, $limit, 0 ); + $res = $dbr->query( $sql, $fname ); $num = false; if ( $res ) { $num = $dbr->numRows( $res ); @@ -238,7 +238,7 @@ class QueryPage { $insertSql .= ','; } if ( isset( $row->value ) ) { - $value = $row->value; + $value = intval( $row->value ); // @bug 14414 } else { $value = 0; } diff --git a/includes/RawPage.php b/includes/RawPage.php index 7093367f..b422d49e 100644 --- a/includes/RawPage.php +++ b/includes/RawPage.php @@ -127,6 +127,15 @@ class RawPage { $url = $_SERVER['PHP_SELF']; } + if( $url == '' ) { + # This will make the next check fail with a confusing error + # message, so we should mention it separately. + wfHttpError( 500, 'Internal Server Error', + "\$_SERVER['PHP_SELF'] is not set. Perhaps you're using CGI" . + " and haven't set cgi.fix_pathinfo = 1 in php.ini?" ); + return; + } + if( strcmp( $wgScript, $url ) ) { # Internet Explorer will ignore the Content-Type header if it # thinks it sees a file extension it recognizes. Make sure that @@ -164,7 +173,7 @@ class RawPage { $text = $this->getRawText(); if( !wfRunHooks( 'RawPageViewBeforeOutput', array( &$this, &$text ) ) ) { - wfDebug( __METHOD__ . ': RawPageViewBeforeOutput hook broke raw page output.' ); + wfDebug( __METHOD__ . ": RawPageViewBeforeOutput hook broke raw page output.\n" ); } echo $text; diff --git a/includes/RecentChange.php b/includes/RecentChange.php index f03fbcbb..8e3f1107 100644 --- a/includes/RecentChange.php +++ b/includes/RecentChange.php @@ -166,6 +166,9 @@ class RecentChange # Set the ID $this->mAttribs['rc_id'] = $dbw->insertId(); + + # Notify extensions + wfRunHooks( 'RecentChange_save', array( &$this ) ); # Notify external application via UDP if( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) { @@ -193,9 +196,14 @@ class RecentChange $this->mAttribs['rc_minor'], $this->mAttribs['rc_last_oldid'] ); } - - # Notify extensions - wfRunHooks( 'RecentChange_save', array( &$this ) ); + } + + public function notifyRC2UDP() { + global $wgRC2UDPAddress, $wgRC2UDPOmitBots; + # Notify external application via UDP + if( $wgRC2UDPAddress && ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) { + self::sendToUDP( $this->getIRCLine() ); + } } /** @@ -227,12 +235,12 @@ class RecentChange } /** - * Remove newlines and carriage returns + * Remove newlines, carriage returns and decode html entites * @param string $line * @return string */ public static function cleanupForIRC( $text ) { - return str_replace(array("\n", "\r"), array("", ""), $text); + return Sanitizer::decodeCharReferences( str_replace( array( "\n", "\r" ), array( "", "" ), $text ) ); } /** @@ -318,9 +326,7 @@ class RecentChange { if( !$ip ) { $ip = wfGetIP(); - if( !$ip ) { - $ip = ''; - } + if( !$ip ) $ip = ''; } $rc = new RecentChange; @@ -372,9 +378,7 @@ class RecentChange { if( !$ip ) { $ip = wfGetIP(); - if( !$ip ) { - $ip = ''; - } + if( !$ip ) $ip = ''; } $rc = new RecentChange; @@ -420,12 +424,9 @@ class RecentChange public static function notifyMove( $timestamp, &$oldTitle, &$newTitle, &$user, $comment, $ip='', $overRedir = false ) { global $wgRequest; - if( !$ip ) { $ip = wfGetIP(); - if( !$ip ) { - $ip = ''; - } + if( !$ip ) $ip = ''; } $rc = new RecentChange; @@ -473,16 +474,27 @@ class RecentChange RecentChange::notifyMove( $timestamp, $oldTitle, $newTitle, $user, $comment, $ip, true ); } - public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='', - $type, $action, $target, $logComment, $params, $newId=0 ) + public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip='', $type, + $action, $target, $logComment, $params, $newId=0 ) { - global $wgRequest; + global $wgLogRestrictions; + # Don't add private logs to RC! + if( isset($wgLogRestrictions[$type]) && $wgLogRestrictions[$type] != '*' ) { + return false; + } + $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action, + $target, $logComment, $params, $newId ); + $rc->save(); + return true; + } + public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip='', + $type, $action, $target, $logComment, $params, $newId=0 ) + { + global $wgRequest; if( !$ip ) { $ip = wfGetIP(); - if( !$ip ) { - $ip = ''; - } + if( !$ip ) $ip = ''; } $rc = new RecentChange; @@ -518,7 +530,7 @@ class RecentChange 'lastTimestamp' => 0, 'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage ); - $rc->save(); + return $rc; } # Initialises the members of this object from a mysql row object @@ -589,7 +601,7 @@ class RecentChange return $trail; } - protected function getIRCLine() { + public function getIRCLine() { global $wgUseRCPatrol, $wgUseNPPatrol, $wgRC2UDPInterwikiPrefix, $wgLocalInterwiki; // FIXME: Would be good to replace these 2 extract() calls with something more explicit @@ -643,7 +655,11 @@ class RecentChange $flag = $rc_log_action; } else { $comment = self::cleanupForIRC( $rc_comment ); - $flag = ($rc_new ? "N" : "") . ($rc_minor ? "M" : "") . ($rc_bot ? "B" : ""); + $flag = ''; + if( !$rc_patrolled && ($wgUseRCPatrol || $rc_new && $wgUseNPPatrol) ) { + $flag .= '!'; + } + $flag .= ($rc_new ? "N" : "") . ($rc_minor ? "M" : "") . ($rc_bot ? "B" : ""); } if ( $wgRC2UDPInterwikiPrefix === true ) { diff --git a/includes/RefreshLinksJob.php b/includes/RefreshLinksJob.php index 1c119a8d..91cff40b 100644 --- a/includes/RefreshLinksJob.php +++ b/includes/RefreshLinksJob.php @@ -82,35 +82,21 @@ class RefreshLinksJob2 extends Job { wfProfileOut( __METHOD__ ); return false; } - $start = intval($this->params['start']); - $end = intval($this->params['end']); - - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( array( 'templatelinks', 'page' ), - array( 'page_namespace', 'page_title' ), - array( - 'page_id=tl_from', - "tl_from >= '$start'", - "tl_from <= '$end'", - 'tl_namespace' => $this->title->getNamespace(), - 'tl_title' => $this->title->getDBkey() - ), __METHOD__ - ); + $titles = $this->title->getBacklinkCache()->getLinks( + 'templatelinks', $this->params['start'], $this->params['end']); # Not suitable for page load triggered job running! # Gracefully switch to refreshLinks jobs if this happens. if( php_sapi_name() != 'cli' ) { $jobs = array(); - while( $row = $dbr->fetchObject( $res ) ) { - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + foreach ( $titles as $title ) { $jobs[] = new RefreshLinksJob( $title, '' ); } Job::batchInsert( $jobs ); return true; } # Re-parse each page that transcludes this page and update their tracking links... - while( $row = $dbr->fetchObject( $res ) ) { - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + foreach ( $titles as $title ) { $revision = Revision::newFromTitle( $title ); if ( !$revision ) { $this->error = 'refreshLinks: Article not found "' . $title->getPrefixedDBkey() . '"'; diff --git a/includes/Revision.php b/includes/Revision.php index 7938d88a..8a2149c0 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -53,6 +53,10 @@ class Revision { // Get the latest revision ID from the master $dbw = wfGetDB( DB_MASTER ); $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ ); + if ( $latest === false ) { + // Page does not exist + return null; + } $conds['rev_id'] = $latest; } else { // Use a join to get the latest revision @@ -363,6 +367,7 @@ class Revision { } else { throw new MWException( 'Revision constructor passed invalid row format.' ); } + $this->mUnpatrolled = NULL; } /**#@+ @@ -536,6 +541,27 @@ class Revision { public function isMinor() { return (bool)$this->mMinorEdit; } + + /** + * @return int rcid of the unpatrolled row, zero if there isn't one + */ + public function isUnpatrolled() { + if( $this->mUnpatrolled !== NULL ) { + return $this->mUnpatrolled; + } + $dbr = wfGetDB( DB_SLAVE ); + $this->mUnpatrolled = $dbr->selectField( 'recentchanges', + 'rc_id', + array( // Add redundant user,timestamp condition so we can use the existing index + 'rc_user_text' => $this->getRawUserText(), + 'rc_timestamp' => $dbr->timestamp( $this->getTimestamp() ), + 'rc_this_oldid' => $this->getId(), + 'rc_patrolled' => 0 + ), + __METHOD__ + ); + return (int)$this->mUnpatrolled; + } /** * int $field one of DELETED_* bitfield constants @@ -819,7 +845,8 @@ class Revision { 'rev_timestamp' => $dbw->timestamp( $this->mTimestamp ), 'rev_deleted' => $this->mDeleted, 'rev_len' => $this->mSize, - 'rev_parent_id' => $this->mParentId ? $this->mParentId : $this->getPreviousRevisionId( $dbw ) + 'rev_parent_id' => is_null($this->mParentId) ? + $this->getPreviousRevisionId( $dbw ) : $this->mParentId ), __METHOD__ ); @@ -961,6 +988,10 @@ class Revision { */ static function getTimestampFromId( $title, $id ) { $dbr = wfGetDB( DB_SLAVE ); + // Casting fix for DB2 + if ($id == '') { + $id = 0; + } $conds = array( 'rev_id' => $id ); $conds['rev_page'] = $title->getArticleId(); $timestamp = $dbr->selectField( 'revision', 'rev_timestamp', $conds, __METHOD__ ); diff --git a/includes/SearchEngine.php b/includes/SearchEngine.php index 3ea0341d..e5392f7c 100644 --- a/includes/SearchEngine.php +++ b/includes/SearchEngine.php @@ -80,8 +80,13 @@ class SearchEngine { if (is_null($title)) return NULL; - if ( $title->getNamespace() == NS_SPECIAL || $title->isExternal() - || $title->exists() ) { + if ( $title->getNamespace() == NS_SPECIAL || $title->isExternal() || $title->exists() ) { + return $title; + } + + # See if it still otherwise has content is some sane sense + $article = MediaWiki::articleFromTitle( $title ); + if( $article->hasViewableContent() ) { return $title; } @@ -403,7 +408,7 @@ class SearchEngine { if($wgMWSuggestTemplate) return $wgMWSuggestTemplate; else - return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace={namespaces}'; + return $wgServer . $wgScriptPath . '/api.php?action=opensearch&search={searchTerms}&namespace={namespaces}&suggest'; } } @@ -1165,12 +1170,12 @@ class SearchHighlighter { continue; } --$contextlines; - $pre = $wgContLang->truncate( $m[1], -$contextchars, ' ... ' ); + $pre = $wgContLang->truncate( $m[1], -$contextchars ); if ( count( $m ) < 3 ) { $post = ''; } else { - $post = $wgContLang->truncate( $m[3], $contextchars, ' ... ' ); + $post = $wgContLang->truncate( $m[3], $contextchars ); } $found = $m[2]; diff --git a/includes/SearchIBM_DB2.php b/includes/SearchIBM_DB2.php new file mode 100644 index 00000000..57813a73 --- /dev/null +++ b/includes/SearchIBM_DB2.php @@ -0,0 +1,247 @@ +<?php +# Copyright (C) 2004 Brion Vibber <brion@pobox.com> +# http://www.mediawiki.org/ +# +# 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 Search + */ + +/** + * Search engine hook base class for IBM DB2 + * @ingroup Search + */ +class SearchIBM_DB2 extends SearchEngine { + function __construct($db) { + $this->db = $db; + } + + /** + * Perform a full text search query and return a result set. + * + * @param string $term - Raw search term + * @return IBM_DB2SearchResultSet + * @access public + */ + function searchText( $term ) { + $resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), true))); + return new IBM_DB2SearchResultSet($resultSet, $this->searchTerms); + } + + /** + * Perform a title-only search query and return a result set. + * + * @param string $term - Raw search term + * @return IBM_DB2SearchResultSet + * @access public + */ + function searchTitle($term) { + $resultSet = $this->db->resultObject($this->db->query($this->getQuery($this->filter($term), false))); + return new MySQLSearchResultSet($resultSet, $this->searchTerms); + } + + + /** + * Return a partial WHERE clause to exclude redirects, if so set + * @return string + * @private + */ + function queryRedirect() { + if ($this->showRedirects) { + return ''; + } else { + return 'AND page_is_redirect=0'; + } + } + + /** + * Return a partial WHERE clause to limit the search to the given namespaces + * @return string + * @private + */ + function queryNamespaces() { + if( is_null($this->namespaces) ) + return ''; + $namespaces = implode(',', $this->namespaces); + if ($namespaces == '') { + $namespaces = '0'; + } + return 'AND page_namespace IN (' . $namespaces . ')'; + } + + /** + * Return a LIMIT clause to limit results on the query. + * @return string + * @private + */ + function queryLimit($sql) { + return $this->db->limitResult($sql, $this->limit, $this->offset); + } + + /** + * Does not do anything for generic search engine + * subclasses may define this though + * @return string + * @private + */ + function queryRanking($filteredTerm, $fulltext) { + // requires Net Search Extender or equivalent + // return ' ORDER BY score(1)'; + return ''; + } + + /** + * Construct the full SQL query to do the search. + * The guts shoulds be constructed in queryMain() + * @param string $filteredTerm + * @param bool $fulltext + * @private + */ + function getQuery( $filteredTerm, $fulltext ) { + return $this->queryLimit($this->queryMain($filteredTerm, $fulltext) . ' ' . + $this->queryRedirect() . ' ' . + $this->queryNamespaces() . ' ' . + $this->queryRanking( $filteredTerm, $fulltext ) . ' '); + } + + + /** + * Picks which field to index on, depending on what type of query. + * @param bool $fulltext + * @return string + */ + function getIndexField($fulltext) { + return $fulltext ? 'si_text' : 'si_title'; + } + + /** + * Get the base part of the search query. + * + * @param string $filteredTerm + * @param bool $fulltext + * @return string + * @private + */ + function queryMain( $filteredTerm, $fulltext ) { + $match = $this->parseQuery($filteredTerm, $fulltext); + $page = $this->db->tableName('page'); + $searchindex = $this->db->tableName('searchindex'); + return 'SELECT page_id, page_namespace, page_title ' . + "FROM $page,$searchindex " . + 'WHERE page_id=si_page AND ' . $match; + } + + /** @todo document */ + function parseQuery($filteredText, $fulltext) { + global $wgContLang; + $lc = SearchEngine::legalSearchChars(); + $this->searchTerms = array(); + + # FIXME: This doesn't handle parenthetical expressions. + $m = array(); + $q = array(); + + if (preg_match_all('/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/', + $filteredText, $m, PREG_SET_ORDER)) { + foreach($m as $terms) { + $q[] = $terms[1] . $wgContLang->stripForSearch($terms[2]); + + if (!empty($terms[3])) { + $regexp = preg_quote( $terms[3], '/' ); + if ($terms[4]) + $regexp .= "[0-9A-Za-z_]+"; + } else { + $regexp = preg_quote(str_replace('"', '', $terms[2]), '/'); + } + $this->searchTerms[] = $regexp; + } + } + + $searchon = $this->db->strencode(join(',', $q)); + $field = $this->getIndexField($fulltext); + + // requires Net Search Extender or equivalent + //return " CONTAINS($field, '$searchon') > 0 "; + + return " lcase($field) LIKE lcase('%$searchon%')"; + } + + /** + * Create or update the search index record for the given page. + * Title and text should be pre-processed. + * + * @param int $id + * @param string $title + * @param string $text + */ + function update($id, $title, $text) { + $dbw = wfGetDB(DB_MASTER); + $dbw->replace('searchindex', + array('si_page'), + array( + 'si_page' => $id, + 'si_title' => $title, + 'si_text' => $text + ), 'SearchIBM_DB2::update' ); + // ? + //$dbw->query("CALL ctx_ddl.sync_index('si_text_idx')"); + //$dbw->query("CALL ctx_ddl.sync_index('si_title_idx')"); + } + + /** + * Update a search index record's title only. + * Title should be pre-processed. + * + * @param int $id + * @param string $title + */ + function updateTitle($id, $title) { + $dbw = wfGetDB(DB_MASTER); + + $dbw->update('searchindex', + array('si_title' => $title), + array('si_page' => $id), + 'SearchIBM_DB2::updateTitle', + array()); + } +} + +/** + * @ingroup Search + */ +class IBM_DB2SearchResultSet extends SearchResultSet { + function __construct($resultSet, $terms) { + $this->mResultSet = $resultSet; + $this->mTerms = $terms; + } + + function termMatches() { + return $this->mTerms; + } + + function numRows() { + return $this->mResultSet->numRows(); + } + + function next() { + $row = $this->mResultSet->fetchObject(); + if ($row === false) + return false; + return new SearchResult($row); + } +} diff --git a/includes/SearchPostgres.php b/includes/SearchPostgres.php index 4862a44e..fa9d8420 100644 --- a/includes/SearchPostgres.php +++ b/includes/SearchPostgres.php @@ -70,7 +70,7 @@ class SearchPostgres extends SearchEngine { */ function parseQuery( $term ) { - wfDebug( "parseQuery received: $term" ); + wfDebug( "parseQuery received: $term \n" ); ## No backslashes allowed $term = preg_replace('/\\\/', '', $term); @@ -122,7 +122,7 @@ class SearchPostgres extends SearchEngine { ## Quote the whole thing $searchstring = $this->db->addQuotes($searchstring); - wfDebug( "parseQuery returned: $searchstring" ); + wfDebug( "parseQuery returned: $searchstring \n" ); return $searchstring; @@ -194,7 +194,7 @@ class SearchPostgres extends SearchEngine { $query .= $this->db->limitResult( '', $this->limit, $this->offset ); - wfDebug( "searchQuery returned: $query" ); + wfDebug( "searchQuery returned: $query \n" ); return $query; } diff --git a/includes/Setup.php b/includes/Setup.php index 859ad008..d450dfdb 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -197,6 +197,10 @@ if($wgMetaNamespace === FALSE) { # To determine the user language, use $wgLang->getCode() $wgContLanguageCode = $wgLanguageCode; +# Easy to forget to falsify $wgShowIPinHeader for static caches. +# If file cache or squid cache is on, just disable this (DWIMD). +if( $wgUseFileCache || $wgUseSquid ) $wgShowIPinHeader = false; + wfProfileOut( $fname.'-misc1' ); wfProfileIn( $fname.'-memcached' ); diff --git a/includes/SiteStats.php b/includes/SiteStats.php index ab0caa7e..9427536f 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -222,7 +222,7 @@ class SiteStatsUpdate { if ( $updates ) { $site_stats = $dbw->tableName( 'site_stats' ); - $sql = $dbw->limitResultForUpdate("UPDATE $site_stats SET $updates", 1); + $sql = "UPDATE $site_stats SET $updates"; # Need a separate transaction because this a global lock $dbw->begin(); @@ -240,7 +240,7 @@ class SiteStatsUpdate { __METHOD__ ); $dbw->update( 'site_stats', array( 'ss_active_users' => intval($activeUsers) ), - array( 'ss_row_id' => 1 ), __METHOD__, array( 'LIMIT' => 1 ) + array( 'ss_row_id' => 1 ), __METHOD__ ); } } diff --git a/includes/Skin.php b/includes/Skin.php index 636b96bf..47285acc 100644 --- a/includes/Skin.php +++ b/includes/Skin.php @@ -401,7 +401,7 @@ class Skin extends Linker { $vars['wgLivepreviewMessageError'] = wfMsg( 'livepreview-error' ); } - if($wgUseAjax && $wgAjaxWatch && $wgUser->isLoggedIn() ) { + if ( $wgOut->isArticleRelated() && $wgUseAjax && $wgAjaxWatch && $wgUser->isLoggedIn() ) { $msgs = (object)array(); foreach ( array( 'watch', 'unwatch', 'watching', 'unwatching' ) as $msgName ) { $msgs->{$msgName . 'Msg'} = wfMsg( $msgName ); @@ -758,7 +758,7 @@ END; if( count( $wgOut->mCategoryLinks ) == 0 ) return ''; # Separator - $sep = wfMsgHtml( 'catseparator' ); + $sep = wfMsgExt( 'catseparator', array( 'parsemag', 'escapenoentities' ) ); // Use Unicode bidi embedding override characters, // to make sure links don't smash each other up in ugly ways. @@ -890,12 +890,27 @@ END; } /** + * Generate debug data HTML for displaying at the bottom of the main content + * area. + * @return String HTML containing debug data, if enabled (otherwise empty). + */ + protected function generateDebugHTML() { + global $wgShowDebug, $wgOut; + if ( $wgShowDebug ) { + $listInternals = str_replace( "\n", "</li>\n<li>", htmlspecialchars( $wgOut->mDebugtext ) ); + return "\n<hr>\n<strong>Debug data:</strong><ul style=\"font-family:monospace;\"><li>" . + $listInternals . "</li></ul>\n"; + } + return ''; + } + + /** * This gets called shortly before the </body> tag. * @return String HTML to be put before </body> */ function afterContent() { $printfooter = "<div class=\"printfooter\">\n" . $this->printFooter() . "</div>\n"; - return $printfooter . $this->doAfterContent(); + return $printfooter . $this->generateDebugHTML() . $this->doAfterContent(); } /** @@ -925,20 +940,20 @@ END; function doAfterContent() { return "</div></div>"; } function pageTitleLinks() { - global $wgOut, $wgTitle, $wgUser, $wgRequest; + global $wgOut, $wgTitle, $wgUser, $wgRequest, $wgLang; $oldid = $wgRequest->getVal( 'oldid' ); $diff = $wgRequest->getVal( 'diff' ); $action = $wgRequest->getText( 'action' ); - $s = $this->printableLink(); + $s[] = $this->printableLink(); $disclaimer = $this->disclaimerLink(); # may be empty if( $disclaimer ) { - $s .= ' | ' . $disclaimer; + $s[] = $disclaimer; } $privacy = $this->privacyLink(); # may be empty too if( $privacy ) { - $s .= ' | ' . $privacy; + $s[] = $privacy; } if ( $wgOut->isArticleRelated() ) { @@ -948,12 +963,12 @@ END; if( $image ) { $link = htmlspecialchars( $image->getURL() ); $style = $this->getInternalLinkAttributes( $link, $name ); - $s .= " | <a href=\"{$link}\"{$style}>{$name}</a>"; + $s[] = "<a href=\"{$link}\"{$style}>{$name}</a>"; } } } if ( 'history' == $action || isset( $diff ) || isset( $oldid ) ) { - $s .= ' | ' . $this->makeKnownLinkObj( $wgTitle, + $s[] .= $this->makeKnownLinkObj( $wgTitle, wfMsg( 'currentrev' ) ); } @@ -963,7 +978,7 @@ END; if( !$wgTitle->equals( $wgUser->getTalkPage() ) ) { $tl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), wfMsgHtml( 'newmessageslink' ), 'redirect=no' ); $dl = $this->makeKnownLinkObj( $wgUser->getTalkPage(), wfMsgHtml( 'newmessagesdifflink' ), 'diff=cur' ); - $s.= ' | <strong>'. wfMsg( 'youhavenewmessages', $tl, $dl ) . '</strong>'; + $s[] = '<strong>'. wfMsg( 'youhavenewmessages', $tl, $dl ) . '</strong>'; # disable caching $wgOut->setSquidMaxage(0); $wgOut->enableClientCache(false); @@ -972,9 +987,9 @@ END; $undelete = $this->getUndeleteLink(); if( !empty( $undelete ) ) { - $s .= ' | '.$undelete; + $s[] = $undelete; } - return $s; + return $wgLang->pipeList( $s ); } function getUndeleteLink() { @@ -997,18 +1012,19 @@ END; } function printableLink() { - global $wgOut, $wgFeedClasses, $wgRequest; + global $wgOut, $wgFeedClasses, $wgRequest, $wgLang; $printurl = $wgRequest->escapeAppendQuery( 'printable=yes' ); - $s = "<a href=\"$printurl\">" . wfMsg( 'printableversion' ) . '</a>'; + $s[] = "<a href=\"$printurl\" rel=\"alternate\">" . wfMsg( 'printableversion' ) . '</a>'; if( $wgOut->isSyndicated() ) { foreach( $wgFeedClasses as $format => $class ) { $feedurl = $wgRequest->escapeAppendQuery( "feed=$format" ); - $s .= " | <a href=\"$feedurl\">{$format}</a>"; + $s[] = "<a href=\"$feedurl\" rel=\"alternate\" type=\"application/{$format}+xml\"" + . " class=\"feedlink\">" . wfMsgHtml( "feed-$format" ) . "</a>"; } } - return $s; + return $wgLang->pipeList( $s ); } function pageTitle() { @@ -1053,7 +1069,7 @@ END; $getlink = $this->makeKnownLinkObj( $linkObj, htmlspecialchars( $display ) ); $c++; if ($c>1) { - $subpages .= ' | '; + $subpages .= wfMsgExt( 'pipe-separator' , 'escapenoentities' ); } else { $subpages .= '< '; } @@ -1116,16 +1132,21 @@ END; $ret .= $this->link( $wgUser->getUserPage(), htmlspecialchars( $wgUser->getName() ) ); $ret .= " ($talkLink)<br />"; - $ret .= $this->link( - SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ), - array(), array( 'returnto' => $returnTo ) - ); - $ret .= ' | ' . $this->specialLink( 'preferences' ); - } - $ret .= ' | ' . $this->link( - Title::newFromText( wfMsgForContent( 'helppage' ) ), - wfMsg( 'help' ) - ); + $ret .= $wgLang->pipeList( array( + $this->link( + SpecialPage::getTitleFor( 'Userlogout' ), wfMsg( 'logout' ), + array(), array( 'returnto' => $returnTo ) + ), + $this->specialLink( 'preferences' ), + ) ); + } + $ret = $wgLang->pipeList( array( + $ret, + $this->link( + Title::newFromText( wfMsgForContent( 'helppage' ) ), + wfMsg( 'help' ) + ), + ) ); return $ret; } @@ -1140,16 +1161,22 @@ END; } function searchForm() { - global $wgRequest; + global $wgRequest, $wgUseTwoButtonsSearchForm; $search = $wgRequest->getText( 'search' ); $s = '<form id="searchform'.$this->searchboxes.'" name="search" class="inline" method="post" action="' . $this->escapeSearchLink() . "\">\n" . '<input type="text" id="searchInput'.$this->searchboxes.'" name="search" size="19" value="' . htmlspecialchars(substr($search,0,256)) . "\" />\n" - . '<input type="submit" name="go" value="' . wfMsg ('searcharticle') . '" /> ' - . '<input type="submit" name="fulltext" value="' . wfMsg ('searchbutton') . "\" />\n</form>"; - + . '<input type="submit" name="go" value="' . wfMsg ('searcharticle') . '" />'; + + if ($wgUseTwoButtonsSearchForm) + $s .= ' <input type="submit" name="fulltext" value="' . wfMsg ('searchbutton') . "\" />\n"; + else + $s .= ' <a href="' . $this->escapeSearchLink() . '" rel="search">' . wfMsg ('powersearch-legend') . "</a>\n"; + + $s .= '</form>'; + // Ensure unique id's for search boxes made after the first $this->searchboxes = $this->searchboxes == '' ? 2 : $this->searchboxes + 1; @@ -1158,23 +1185,29 @@ END; function topLinks() { global $wgOut; - $sep = " |\n"; - $s = $this->mainPageLink() . $sep - . $this->specialLink( 'recentchanges' ); + $s = array( + $this->mainPageLink(), + $this->specialLink( 'recentchanges' ) + ); if ( $wgOut->isArticleRelated() ) { - $s .= $sep . $this->editThisPage() - . $sep . $this->historyLink(); + $s[] = $this->editThisPage(); + $s[] = $this->historyLink(); } # Many people don't like this dropdown box - #$s .= $sep . $this->specialPagesList(); + #$s[] = $this->specialPagesList(); - $s .= $this->variantLinks(); + if( $this->variantLinks() ) { + $s[] = $this->variantLinks(); + } - $s .= $this->extensionTabLinks(); + if( $this->extensionTabLinks() ) { + $s[] = $this->extensionTabLinks(); + } - return $s; + // FIXME: Is using Language::pipeList impossible here? Do not quite understand the use of the newline + return implode( $s, wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . "\n" ); } /** @@ -1185,14 +1218,23 @@ END; */ function extensionTabLinks() { $tabs = array(); - $s = ''; + $out = ''; + $s = array(); wfRunHooks( 'SkinTemplateTabs', array( $this, &$tabs ) ); foreach( $tabs as $tab ) { - $s .= ' | ' . Xml::element( 'a', + $s[] = Xml::element( 'a', array( 'href' => $tab['href'] ), $tab['text'] ); } - return $s; + + if( count( $s ) ) { + global $wgLang; + + $out = wfMsgExt( 'pipe-separator' , 'escapenoentities' ); + $out .= $wgLang->pipeList( $s ); + } + + return $out; } /** @@ -1202,14 +1244,14 @@ END; function variantLinks() { $s = ''; /* show links to different language variants */ - global $wgDisableLangConversion, $wgContLang, $wgTitle; + global $wgDisableLangConversion, $wgLang, $wgContLang, $wgTitle; $variants = $wgContLang->getVariants(); if( !$wgDisableLangConversion && sizeof( $variants ) > 1 ) { foreach( $variants as $code ) { $varname = $wgContLang->getVariantname( $code ); if( $varname == 'disable' ) continue; - $s .= ' | <a href="' . $wgTitle->escapeLocalUrl( 'variant=' . $code ) . '">' . htmlspecialchars( $varname ) . '</a>'; + $s = $wgLang->pipeList( array( $s, '<a href="' . $wgTitle->escapeLocalUrl( 'variant=' . $code ) . '">' . htmlspecialchars( $varname ) . '</a>' ) ); } } return $s; @@ -1217,21 +1259,21 @@ END; function bottomLinks() { global $wgOut, $wgUser, $wgTitle, $wgUseTrackbacks; - $sep = " |\n"; + $sep = wfMsgExt( 'pipe-separator' , 'escapenoentities' ) . "\n"; $s = ''; if ( $wgOut->isArticleRelated() ) { - $s .= '<strong>' . $this->editThisPage() . '</strong>'; + $element[] = '<strong>' . $this->editThisPage() . '</strong>'; if ( $wgUser->isLoggedIn() ) { - $s .= $sep . $this->watchThisPage(); + $element[] = $this->watchThisPage(); } - $s .= $sep . $this->talkLink() - . $sep . $this->historyLink() - . $sep . $this->whatLinksHere() - . $sep . $this->watchPageLinksLink(); + $element[] = $this->talkLink(); + $element[] = $this->historyLink(); + $element[] = $this->whatLinksHere(); + $element[] = $this->watchPageLinksLink(); if ($wgUseTrackbacks) - $s .= $sep . $this->trackbackLink(); + $element[] = $this->trackbackLink(); if ( $wgTitle->getNamespace() == NS_USER || $wgTitle->getNamespace() == NS_USER_TALK ) @@ -1241,12 +1283,15 @@ END; $ip=User::isIP($wgTitle->getText()); if($id || $ip) { # both anons and non-anons have contri list - $s .= $sep . $this->userContribsLink(); + $element[] = $this->userContribsLink(); } if( $this->showEmailUser( $id ) ) { - $s .= $sep . $this->emailUserLink(); + $element[] = $this->emailUserLink(); } } + + $s = implode( $element, $sep ); + if ( $wgTitle->getArticleId() ) { $s .= "\n<br />"; if($wgUser->isAllowed('delete')) { $s .= $this->deleteThisPage(); } @@ -1255,6 +1300,7 @@ END; } $s .= "<br />\n" . $this->otherLanguages(); } + return $s; } @@ -1566,8 +1612,8 @@ END; function historyLink() { global $wgTitle; - return $this->makeKnownLinkObj( $wgTitle, - wfMsg( 'history' ), 'action=history' ); + return $this->link( $wgTitle, wfMsg( 'history' ), + array( 'rel' => 'archives' ), array( 'action' => 'history' ) ); } function whatLinksHere() { @@ -1632,11 +1678,11 @@ END; return ''; } - $s = wfMsg( 'otherlanguages' ) . ': '; + $s = wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' ); $first = true; if($wgContLang->isRTL()) $s .= '<span dir="LTR">'; foreach( $a as $l ) { - if ( ! $first ) { $s .= ' | '; } + if ( ! $first ) { $s .= wfMsgExt( 'pipe-separator' , 'escapenoentities' ); } $first = false; $nt = Title::newFromText( $l ); diff --git a/includes/SkinTemplate.php b/includes/SkinTemplate.php index 4f13571a..4317a93e 100644 --- a/includes/SkinTemplate.php +++ b/includes/SkinTemplate.php @@ -138,7 +138,7 @@ class SkinTemplate extends Skin { global $wgScript, $wgStylePath, $wgContLanguageCode; global $wgMimeType, $wgJsMimeType, $wgOutputEncoding, $wgRequest; global $wgXhtmlDefaultNamespace, $wgXhtmlNamespaces; - global $wgDisableCounters, $wgLogo, $action, $wgFeedClasses, $wgHideInterlanguageLinks; + global $wgDisableCounters, $wgLogo, $wgHideInterlanguageLinks; global $wgMaxCredits, $wgShowCreditsIfMax; global $wgPageShowWatchingUsers; global $wgUseTrackbacks, $wgUseSiteJs; @@ -148,6 +148,7 @@ class SkinTemplate extends Skin { $oldid = $wgRequest->getVal( 'oldid' ); $diff = $wgRequest->getVal( 'diff' ); + $action = $wgRequest->getVal( 'action', 'view' ); wfProfileIn( __METHOD__."-init" ); $this->initPage( $out ); @@ -258,6 +259,7 @@ class SkinTemplate extends Skin { $tpl->set( "helppage", wfMsg('helppage')); */ $tpl->set( 'searchaction', $this->escapeSearchLink() ); + $tpl->set( 'searchtitle', SpecialPage::getTitleFor('search')->getPrefixedDBKey() ); $tpl->set( 'search', trim( $wgRequest->getVal( 'search' ) ) ); $tpl->setRef( 'stylepath', $wgStylePath ); $tpl->setRef( 'articlepath', $wgArticlePath ); @@ -401,7 +403,7 @@ class SkinTemplate extends Skin { $tpl->set( 'bottomscripts', $this->bottomScripts() ); $printfooter = "<div class=\"printfooter\">\n" . $this->printSource() . "</div>\n"; - $out->mBodytext .= $printfooter ; + $out->mBodytext .= $printfooter . $this->generateDebugHTML(); $tpl->setRef( 'bodytext', $out->mBodytext ); # Language links @@ -449,7 +451,7 @@ class SkinTemplate extends Skin { // original version by hansm if( !wfRunHooks( 'SkinTemplateOutputPageBeforeExec', array( &$this, &$tpl ) ) ) { - wfDebug( __METHOD__ . ': Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!' ); + wfDebug( __METHOD__ . ": Hook SkinTemplateOutputPageBeforeExec broke outputPage execution!\n" ); } // allow extensions adding stuff after the page content. @@ -647,12 +649,12 @@ class SkinTemplate extends Skin { * @private */ function buildContentActionUrls() { - global $wgContLang, $wgLang, $wgOut; + global $wgContLang, $wgLang, $wgOut, $wgUser, $wgRequest; + wfProfileIn( __METHOD__ ); - global $wgUser, $wgRequest; - $action = $wgRequest->getText( 'action' ); - $section = $wgRequest->getText( 'section' ); + $action = $wgRequest->getVal( 'action', 'view' ); + $section = $wgRequest->getVal( 'section' ); $content_actions = array(); $prevent_active_tabs = false ; @@ -689,11 +691,13 @@ class SkinTemplate extends Skin { ); if ( $istalk || $wgOut->showNewSectionLink() ) { - $content_actions['addsection'] = array( - 'class' => $section == 'new'?'selected':false, - 'text' => wfMsg('addsection'), - 'href' => $this->mTitle->getLocalUrl( 'action=edit§ion=new' ) - ); + if ( !$wgOut->forceHideNewSectionLink() ) { + $content_actions['addsection'] = array( + 'class' => $section == 'new' ? 'selected' : false, + 'text' => wfMsg('addsection'), + 'href' => $this->mTitle->getLocalUrl( 'action=edit§ion=new' ) + ); + } } } elseif ( $this->mTitle->isKnown() ) { $content_actions['viewsource'] = array( @@ -710,7 +714,8 @@ class SkinTemplate extends Skin { $content_actions['history'] = array( 'class' => ($action == 'history') ? 'selected' : false, 'text' => wfMsg('history_short'), - 'href' => $this->mTitle->getLocalUrl( 'action=history') + 'href' => $this->mTitle->getLocalUrl( 'action=history' ), + 'rel' => 'archives', ); if( $wgUser->isAllowed('delete') ) { @@ -848,7 +853,7 @@ class SkinTemplate extends Skin { wfProfileIn( __METHOD__ ); - $action = $wgRequest->getText( 'action' ); + $action = $wgRequest->getVal( 'action', 'view' ); $nav_urls = array(); $nav_urls['mainpage'] = array( 'href' => self::makeMainPageUrl() ); @@ -871,7 +876,7 @@ class SkinTemplate extends Skin { // A print stylesheet is attached to all pages, but nobody ever // figures that out. :) Add a link... - if( $this->iscontent && ($action == '' || $action == 'view' || $action == 'purge' ) ) { + if( $this->iscontent && ( $action == 'view' || $action == 'purge' ) ) { $nav_urls['print'] = array( 'text' => wfMsg( 'printableversion' ), 'href' => $wgRequest->appendQuery( 'printable=yes' ) @@ -965,10 +970,11 @@ class SkinTemplate extends Skin { * @private */ function setupUserJs( $allowUserJs ) { + global $wgRequest, $wgJsMimeType; + wfProfileIn( __METHOD__ ); - global $wgRequest, $wgJsMimeType; - $action = $wgRequest->getText('action'); + $action = $wgRequest->getVal( 'action', 'view' ); if( $allowUserJs && $this->loggedin ) { if( $this->mTitle->isJsSubpage() and $this->userCanPreview( $action ) ) { diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index 00eacd1e..31b43839 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -80,92 +80,112 @@ class SpecialPage ** array( 'SpecialRedirectToSpecial', name, page to redirect to, special page param, ... ) */ static public $mList = array( - 'DoubleRedirects' => array( 'SpecialPage', 'DoubleRedirects' ), + # Maintenance Reports 'BrokenRedirects' => array( 'SpecialPage', 'BrokenRedirects' ), + 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ), + 'DoubleRedirects' => array( 'SpecialPage', 'DoubleRedirects' ), + 'Longpages' => array( 'SpecialPage', 'Longpages' ), + 'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ), + 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ), + 'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ), + 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ), + 'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ), + 'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ), + 'Shortpages' => array( 'SpecialPage', 'Shortpages' ), + 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ), + 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ), + 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ), + 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ), + 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ), + 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), + 'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ), + 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ), + 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), + 'Wantedfiles' => array( 'SpecialPage', 'Wantedfiles' ), + 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), + 'Wantedtemplates' => array( 'SpecialPage', 'Wantedtemplates' ), + + # List of pages + 'Allpages' => 'SpecialAllpages', + 'Prefixindex' => 'SpecialPrefixindex', + 'Categories' => array( 'SpecialPage', 'Categories' ), 'Disambiguations' => array( 'SpecialPage', 'Disambiguations' ), + 'Listredirects' => array( 'SpecialPage', 'Listredirects' ), - 'Userlogin' => array( 'SpecialPage', 'Userlogin' ), - 'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ), + # Login/create account + 'Userlogin' => array( 'SpecialPage', 'Userlogin' ), 'CreateAccount' => array( 'SpecialRedirectToSpecial', 'CreateAccount', 'Userlogin', 'signup', array( 'uselang' ) ), - 'Preferences' => array( 'SpecialPage', 'Preferences' ), - 'Watchlist' => array( 'SpecialPage', 'Watchlist' ), - 'Resetpass' => 'SpecialResetpass', + # Users and rights + 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), + 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), + 'Resetpass' => 'SpecialResetpass', + 'DeletedContributions' => 'DeletedContributionsPage', + 'Preferences' => array( 'SpecialPage', 'Preferences' ), + 'Contributions' => 'SpecialContributions', + 'Listgrouprights' => 'SpecialListGroupRights', + 'Listusers' => array( 'SpecialPage', 'Listusers' ), + 'Userrights' => 'UserrightsPage', + # Recent changes and logs + 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ), + 'Log' => array( 'SpecialPage', 'Log' ), + 'Watchlist' => array( 'SpecialPage', 'Watchlist' ), + 'Newpages' => 'SpecialNewpages', 'Recentchanges' => 'SpecialRecentchanges', - 'Upload' => array( 'SpecialPage', 'Upload' ), + 'Recentchangeslinked' => 'SpecialRecentchangeslinked', + 'Tags' => 'SpecialTags', + + # Media reports and uploads 'Listfiles' => array( 'SpecialPage', 'Listfiles' ), - 'Newimages' => array( 'IncludableSpecialPage', 'Newimages' ), - 'Listusers' => array( 'SpecialPage', 'Listusers' ), - 'Listgrouprights' => 'SpecialListGroupRights', - 'DeletedContributions' => 'DeletedContributionsPage', + 'Filepath' => array( 'SpecialPage', 'Filepath' ), + 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), + 'FileDuplicateSearch' => array( 'SpecialPage', 'FileDuplicateSearch' ), + 'Upload' => array( 'SpecialPage', 'Upload' ), + + # Wiki data and tools 'Statistics' => 'SpecialStatistics', + 'Allmessages' => array( 'SpecialPage', 'Allmessages' ), + 'Version' => 'SpecialVersion', + 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), + 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), + + # Redirecting special pages + 'LinkSearch' => array( 'SpecialPage', 'LinkSearch' ), 'Randompage' => 'Randompage', - 'Lonelypages' => array( 'SpecialPage', 'Lonelypages' ), - 'Uncategorizedpages' => array( 'SpecialPage', 'Uncategorizedpages' ), - 'Uncategorizedcategories' => array( 'SpecialPage', 'Uncategorizedcategories' ), - 'Uncategorizedimages' => array( 'SpecialPage', 'Uncategorizedimages' ), - 'Uncategorizedtemplates' => array( 'SpecialPage', 'Uncategorizedtemplates' ), - 'Unusedcategories' => array( 'SpecialPage', 'Unusedcategories' ), - 'Unusedimages' => array( 'SpecialPage', 'Unusedimages' ), - 'Wantedpages' => array( 'IncludableSpecialPage', 'Wantedpages' ), - 'Wantedcategories' => array( 'SpecialPage', 'Wantedcategories' ), - 'Wantedfiles' => array( 'SpecialPage', 'Wantedfiles' ), - 'Wantedtemplates' => array( 'SpecialPage', 'Wantedtemplates' ), - 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), + 'Randomredirect' => 'SpecialRandomredirect', + + # High use pages 'Mostlinkedcategories' => array( 'SpecialPage', 'Mostlinkedcategories' ), + 'Mostimages' => array( 'SpecialPage', 'Mostimages' ), + 'Mostlinked' => array( 'SpecialPage', 'Mostlinked' ), 'Mostlinkedtemplates' => array( 'SpecialPage', 'Mostlinkedtemplates' ), 'Mostcategories' => array( 'SpecialPage', 'Mostcategories' ), - 'Mostimages' => array( 'SpecialPage', 'Mostimages' ), 'Mostrevisions' => array( 'SpecialPage', 'Mostrevisions' ), - 'Fewestrevisions' => array( 'SpecialPage', 'Fewestrevisions' ), - 'Shortpages' => array( 'SpecialPage', 'Shortpages' ), - 'Longpages' => array( 'SpecialPage', 'Longpages' ), - 'Newpages' => 'SpecialNewpages', - 'Ancientpages' => array( 'SpecialPage', 'Ancientpages' ), - 'Deadendpages' => array( 'SpecialPage', 'Deadendpages' ), - 'Protectedpages' => array( 'SpecialPage', 'Protectedpages' ), - 'Protectedtitles' => array( 'SpecialPage', 'Protectedtitles' ), - 'Allpages' => 'SpecialAllpages', - 'Prefixindex' => 'SpecialPrefixindex', - 'Ipblocklist' => array( 'SpecialPage', 'Ipblocklist' ), - 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), - 'Contributions' => 'SpecialContributions', - 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), + + # Page tools + 'Export' => 'SpecialExport', + 'Import' => 'SpecialImport', + 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), 'Whatlinkshere' => array( 'SpecialPage', 'Whatlinkshere' ), - 'LinkSearch' => array( 'SpecialPage', 'LinkSearch' ), - 'Recentchangeslinked' => 'SpecialRecentchangeslinked', - 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ), - 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ), + 'MergeHistory' => array( 'SpecialPage', 'MergeHistory', 'mergehistory' ), + + # Other 'Booksources' => 'SpecialBookSources', - 'Categories' => array( 'SpecialPage', 'Categories' ), - 'Export' => array( 'SpecialPage', 'Export' ), - 'Version' => 'SpecialVersion', + + # Unlisted / redirects 'Blankpage' => array( 'UnlistedSpecialPage', 'Blankpage' ), - 'Allmessages' => array( 'SpecialPage', 'Allmessages' ), - 'Log' => array( 'SpecialPage', 'Log' ), - 'Blockip' => array( 'SpecialPage', 'Blockip', 'block' ), - 'Undelete' => array( 'SpecialPage', 'Undelete', 'deletedhistory' ), - 'Import' => 'SpecialImport', - 'Lockdb' => array( 'SpecialPage', 'Lockdb', 'siteadmin' ), - 'Unlockdb' => array( 'SpecialPage', 'Unlockdb', 'siteadmin' ), - 'Userrights' => 'UserrightsPage', - 'MIMEsearch' => array( 'SpecialPage', 'MIMEsearch' ), - 'FileDuplicateSearch' => array( 'SpecialPage', 'FileDuplicateSearch' ), - 'Unwatchedpages' => array( 'SpecialPage', 'Unwatchedpages', 'unwatchedpages' ), - 'Listredirects' => array( 'SpecialPage', 'Listredirects' ), - 'Revisiondelete' => array( 'UnlistedSpecialPage', 'Revisiondelete', 'deleterevision' ), - 'Unusedtemplates' => array( 'SpecialPage', 'Unusedtemplates' ), - 'Randomredirect' => 'SpecialRandomredirect', - 'Withoutinterwiki' => array( 'SpecialPage', 'Withoutinterwiki' ), - 'Filepath' => array( 'SpecialPage', 'Filepath' ), - - 'Mypage' => array( 'SpecialMypage' ), - 'Mytalk' => array( 'SpecialMytalk' ), - 'Mycontributions' => array( 'SpecialMycontributions' ), + 'Blockme' => array( 'UnlistedSpecialPage', 'Blockme' ), + 'Emailuser' => array( 'UnlistedSpecialPage', 'Emailuser' ), 'Listadmins' => array( 'SpecialRedirectToSpecial', 'Listadmins', 'Listusers', 'sysop' ), - 'MergeHistory' => array( 'SpecialPage', 'MergeHistory', 'mergehistory' ), 'Listbots' => array( 'SpecialRedirectToSpecial', 'Listbots', 'Listusers', 'bot' ), + 'Movepage' => array( 'UnlistedSpecialPage', 'Movepage' ), + 'Mycontributions' => array( 'SpecialMycontributions' ), + 'Mypage' => array( 'SpecialMypage' ), + 'Mytalk' => array( 'SpecialMytalk' ), + 'Revisiondelete' => 'SpecialRevisionDelete', + 'Specialpages' => array( 'UnlistedSpecialPage', 'Specialpages' ), + 'Userlogout' => array( 'UnlistedSpecialPage', 'Userlogout' ), ); static public $mAliases; @@ -695,7 +715,9 @@ class SpecialPage * pages? */ public function isRestricted() { - return $this->mRestriction != ''; + global $wgGroupPermissions; + // DWIM: If all anons can do something, then it is not restricted + return $this->mRestriction != '' && empty($wgGroupPermissions['*'][$this->mRestriction]); } /** @@ -752,13 +774,25 @@ class SpecialPage } } - function outputHeader() { + /** + * Outputs a summary message on top of special pages + * Per default the message key is the canonical name of the special page + * May be overriden, i.e. by extensions to stick with the naming conventions + * for message keys: 'extensionname-xxx' + * + * @param string message key of the summary + */ + function outputHeader( $summaryMessageKey = '' ) { global $wgOut, $wgContLang; - $msg = $wgContLang->lc( $this->name() ) . '-summary'; + if( $summaryMessageKey == '' ) { + $msg = $wgContLang->lc( $this->name() ) . '-summary'; + } else { + $msg = $summaryMessageKey; + } $out = wfMsgNoTrans( $msg ); if ( ! wfEmptyMsg( $msg, $out ) and $out !== '' and ! $this->including() ) { - $wgOut->addWikiMsg( $msg ); + $wgOut->wrapWikiMsg( "<div class='mw-specialpage-summary'>\n$1</div>", $msg ); } } diff --git a/includes/SquidUpdate.php b/includes/SquidUpdate.php index c8497a83..b1f01924 100644 --- a/includes/SquidUpdate.php +++ b/includes/SquidUpdate.php @@ -52,13 +52,17 @@ class SquidUpdate { return new SquidUpdate( $blurlArr ); } - static function newFromTitles( &$titles, $urlArr = array() ) { + /** + * Create a SquidUpdate from an array of Title objects, or a TitleArray object + */ + static function newFromTitles( $titles, $urlArr = array() ) { global $wgMaxSquidPurgeTitles; - if ( count( $titles ) > $wgMaxSquidPurgeTitles ) { - $titles = array_slice( $titles, 0, $wgMaxSquidPurgeTitles ); - } + $i = 0; foreach ( $titles as $title ) { $urlArr[] = $title->getInternalURL(); + if ( $i++ > $wgMaxSquidPurgeTitles ) { + break; + } } return new SquidUpdate( $urlArr ); } diff --git a/includes/StreamFile.php b/includes/StreamFile.php index 4abd7364..bdd2a2e5 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -48,6 +48,7 @@ function wfStreamFile( $fname, $headers = array() ) { $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); $sinceTime = strtotime( $modsince ); if ( $stat['mtime'] <= $sinceTime ) { + ini_set('zlib.output_compression', 0); header( "HTTP/1.0 304 Not Modified" ); return; } diff --git a/includes/StubObject.php b/includes/StubObject.php index e27f0b25..f1847a39 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -154,7 +154,7 @@ class StubUserLang extends StubObject { } # Validate $code - if( empty( $code ) || !preg_match( '/^[a-z-]+$/', $code ) ) { + if( empty( $code ) || !preg_match( '/^[a-z-]+$/', $code ) || ( $code === 'qqq' ) ) { wfDebug( "Invalid user language code\n" ); $code = $wgContLanguageCode; } diff --git a/includes/Title.php b/includes/Title.php index 515a3b65..f6c0d5de 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -69,6 +69,7 @@ class Title { var $mLength = -1; ///< The page length, 0 for special pages var $mRedirect = null; ///< Is the article at this title a redirect? var $mNotificationTimestamp = array(); ///< Associative array of user ID -> timestamp/false + var $mBacklinkCache = null; ///< Cache of links to this title //@} @@ -294,11 +295,77 @@ class Title { /** * Extract a redirect destination from a string and return the * Title, or null if the text doesn't contain a valid redirect + * This will only return the very next target, useful for + * the redirect table and other checks that don't need full recursion * - * @param $text \type{String} Text with possible redirect + * @param $text \type{\string} Text with possible redirect * @return \type{Title} The corresponding Title */ public static function newFromRedirect( $text ) { + return self::newFromRedirectInternal( $text ); + } + + /** + * Extract a redirect destination from a string and return the + * Title, or null if the text doesn't contain a valid redirect + * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit + * in order to provide (hopefully) the Title of the final destination instead of another redirect + * + * @param $text \type{\string} Text with possible redirect + * @return \type{Title} The corresponding Title + */ + public static function newFromRedirectRecurse( $text ) { + $titles = self::newFromRedirectArray( $text ); + return $titles ? array_pop( $titles ) : null; + } + + /** + * Extract a redirect destination from a string and return an + * array of Titles, or null if the text doesn't contain a valid redirect + * The last element in the array is the final destination after all redirects + * have been resolved (up to $wgMaxRedirects times) + * + * @param $text \type{\string} Text with possible redirect + * @return \type{\array} Array of Titles, with the destination last + */ + public static function newFromRedirectArray( $text ) { + global $wgMaxRedirects; + // are redirects disabled? + if( $wgMaxRedirects < 1 ) + return null; + $title = self::newFromRedirectInternal( $text ); + if( is_null( $title ) ) + return null; + // recursive check to follow double redirects + $recurse = $wgMaxRedirects; + $titles = array( $title ); + while( --$recurse > 0 ) { + if( $title->isRedirect() ) { + $article = new Article( $title, 0 ); + $newtitle = $article->getRedirectTarget(); + } else { + break; + } + // Redirects to some special pages are not permitted + if( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) { + // the new title passes the checks, so make that our current title so that further recursion can be checked + $title = $newtitle; + $titles[] = $newtitle; + } else { + break; + } + } + return $titles; + } + + /** + * Really extract the redirect destination + * Do not call this function directly, use one of the newFromRedirect* functions above + * + * @param $text \type{\string} Text with possible redirect + * @return \type{Title} The corresponding Title + */ + protected static function newFromRedirectInternal( $text ) { $redir = MagicWord::get( 'redirect' ); $text = trim($text); if( $redir->matchStartAndRemove( $text ) ) { @@ -316,13 +383,11 @@ class Title { $m[1] = urldecode( ltrim( $m[1], ':' ) ); } $title = Title::newFromText( $m[1] ); - // Redirects to some special pages are not permitted - if( $title instanceof Title - && !$title->isSpecial( 'Userlogout' ) - && !$title->isSpecial( 'Filepath' ) ) - { - return $title; + // If the title is a redirect to bad special pages or is invalid, return null + if( !$title instanceof Title || !$title->isValidRedirectTarget() ) { + return null; } + return $title; } } return null; @@ -802,19 +867,21 @@ class Title { * @return \type{\string} the URL */ public function getLinkUrl( $query = array(), $variant = false ) { + wfProfileIn( __METHOD__ ); if( !is_array( $query ) ) { + wfProfileOut( __METHOD__ ); throw new MWException( 'Title::getLinkUrl passed a non-array for '. '$query' ); } if( $this->isExternal() ) { - return $this->getFullURL( $query ); - } elseif( $this->getPrefixedText() === '' - and $this->getFragment() !== '' ) { - return $this->getFragmentForURL(); + $ret = $this->getFullURL( $query ); + } elseif( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { + $ret = $this->getFragmentForURL(); } else { - return $this->getLocalURL( $query, $variant ) - . $this->getFragmentForURL(); + $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(); } + wfProfileOut( __METHOD__ ); + return $ret; } /** @@ -992,7 +1059,7 @@ class Title { */ public function userCan( $action, $doExpensiveQueries = true ) { global $wgUser; - return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries ) === array()); + return ($this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries, true ) === array()); } /** @@ -1024,7 +1091,7 @@ class Title { } // Edit blocks should not affect reading. Account creation blocks handled at userlogin. - if ( $user->isBlockedFrom( $this ) && $action != 'read' && $action != 'createaccount' ) { + if ( $action != 'read' && $action != 'createaccount' && $user->isBlockedFrom( $this ) ) { $block = $user->mBlock; // This is from OutputPage::blockedPage @@ -1094,19 +1161,73 @@ class Title { * @param $action \type{\string} action that permission needs to be checked for * @param $user \type{User} user to check * @param $doExpensiveQueries \type{\bool} Set this to false to avoid doing unnecessary queries. + * @param $short \type{\bool} Set this to true to stop after the first permission error. * @return \type{\array} Array of arrays of the arguments to wfMsg to explain permissions problems. */ - private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) { + private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries=true, $short=false ) { wfProfileIn( __METHOD__ ); $errors = array(); + // First stop is permissions checks, which fail most often, and which are easiest to test. + if ( $action == 'move' ) { + if( !$user->isAllowed( 'move-rootuserpages' ) + && $this->getNamespace() == NS_USER && !$this->isSubpage() ) + { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-user-page' ); + } + + // Check if user is allowed to move files if it's a file + if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { + $errors[] = array( 'movenotallowedfile' ); + } + + if( !$user->isAllowed( 'move' ) ) { + // User can't move anything + $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); + } + } elseif ( $action == 'create' ) { + if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || + ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) + { + $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin'); + } + } elseif( $action == 'move-target' ) { + if( !$user->isAllowed( 'move' ) ) { + // User can't move anything + $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); + } elseif( !$user->isAllowed( 'move-rootuserpages' ) + && $this->getNamespace() == NS_USER && !$this->isSubpage() ) + { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-to-user-page' ); + } + } elseif( !$user->isAllowed( $action ) ) { + $return = null; + $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), + User::getGroupsWithPermission( $action ) ); + if( $groups ) { + $return = array( 'badaccess-groups', + array( implode( ', ', $groups ), count( $groups ) ) ); + } else { + $return = array( "badaccess-group0" ); + } + $errors[] = $return; + } + + # Short-circuit point + if( $short && count($errors) > 0 ) { + wfProfileOut( __METHOD__ ); + return $errors; + } + // Use getUserPermissionsErrors instead if( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { wfProfileOut( __METHOD__ ); return $result ? array() : array( array( 'badaccess-group0' ) ); } - + // Check getUserPermissionsErrors hook if( !wfRunHooks( 'getUserPermissionsErrors', array(&$this,&$user,$action,&$result) ) ) { if( is_array($result) && count($result) && !is_array($result[0]) ) $errors[] = $result; # A single array representing an error @@ -1117,6 +1238,12 @@ class Title { else if( $result === false ) $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" } + # Short-circuit point + if( $short && count($errors) > 0 ) { + wfProfileOut( __METHOD__ ); + return $errors; + } + // Check getUserPermissionsErrorsExpensive hook if( $doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array(&$this,&$user,$action,&$result) ) ) { if( is_array($result) && count($result) && !is_array($result[0]) ) $errors[] = $result; # A single array representing an error @@ -1127,13 +1254,20 @@ class Title { else if( $result === false ) $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" } + # Short-circuit point + if( $short && count($errors) > 0 ) { + wfProfileOut( __METHOD__ ); + return $errors; + } - // TODO: document + # Only 'createaccount' and 'execute' can be performed on + # special pages, which don't actually exist in the DB. $specialOKActions = array( 'createaccount', 'execute' ); if( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions) ) { $errors[] = array('ns-specialprotected'); } + # Check $wgNamespaceProtection for restricted namespaces if( $this->isNamespaceProtected() ) { $ns = $this->getNamespace() == NS_MAIN ? wfMsg( 'nstab-main' ) : $this->getNsText(); @@ -1141,7 +1275,7 @@ class Title { array('protectedinterface') : array( 'namespaceprotected', $ns ); } - # protect css/js subpages of user pages + # Protect css/js subpages of user pages # XXX: this might be better using restrictions # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working if( $this->isCssJsSubpage() && !$user->isAllowed('editusercssjs') @@ -1150,6 +1284,32 @@ class Title { $errors[] = array('customcssjsprotected'); } + # Check against page_restrictions table requirements on this + # page. The user must possess all required rights for this action. + foreach( $this->getRestrictions($action) as $right ) { + // Backwards compatibility, rewrite sysop -> protect + if( $right == 'sysop' ) { + $right = 'protect'; + } + if( '' != $right && !$user->isAllowed( $right ) ) { + // Users with 'editprotected' permission can edit protected pages + if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) { + // Users with 'editprotected' permission cannot edit protected pages + // with cascading option turned on. + if( $this->mCascadeRestriction ) { + $errors[] = array( 'protectedpagetext', $right ); + } + } else { + $errors[] = array( 'protectedpagetext', $right ); + } + } + } + # Short-circuit point + if( $short && count($errors) > 0 ) { + wfProfileOut( __METHOD__ ); + return $errors; + } + if( $doExpensiveQueries && !$this->isCssJsSubpage() ) { # We /could/ use the protection level on the source page, but it's fairly ugly # as we have to establish a precedence hierarchy for pages included by multiple @@ -1172,26 +1332,10 @@ class Title { } } } - - foreach( $this->getRestrictions($action) as $right ) { - // Backwards compatibility, rewrite sysop -> protect - if( $right == 'sysop' ) { - $right = 'protect'; - } - if( '' != $right && !$user->isAllowed( $right ) ) { - // Users with 'editprotected' permission can edit protected pages - if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) { - // Users with 'editprotected' permission cannot edit protected pages - // with cascading option turned on. - if( $this->mCascadeRestriction ) { - $errors[] = array( 'protectedpagetext', $right ); - } else { - // Nothing, user can edit! - } - } else { - $errors[] = array( 'protectedpagetext', $right ); - } - } + # Short-circuit point + if( $short && count($errors) > 0 ) { + wfProfileOut( __METHOD__ ); + return $errors; } if( $action == 'protect' ) { @@ -1212,26 +1356,7 @@ class Title { $errors[] = array( 'titleprotected', User::whoIs($pt_user), $pt_reason ); } } - - if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || - ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) - { - $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin'); - } } elseif( $action == 'move' ) { - if( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); - } elseif( !$user->isAllowed( 'move-rootuserpages' ) - && $this->getNamespace() == NS_USER && !$this->isSubpage() ) - { - // Show user page-specific message only if the user can move other pages - $errors[] = array( 'cant-move-user-page' ); - } - // Check if user is allowed to move files if it's a file - if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { - $errors[] = array( 'movenotallowedfile' ); - } // Check for immobile pages if( !MWNamespace::isMovable( $this->getNamespace() ) ) { // Specific message for this case @@ -1241,31 +1366,11 @@ class Title { $errors[] = array( 'immobile-page' ); } } elseif( $action == 'move-target' ) { - if( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); - } elseif( !$user->isAllowed( 'move-rootuserpages' ) - && $this->getNamespace() == NS_USER && !$this->isSubpage() ) - { - // Show user page-specific message only if the user can move other pages - $errors[] = array( 'cant-move-to-user-page' ); - } if( !MWNamespace::isMovable( $this->getNamespace() ) ) { $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); } elseif( !$this->isMovable() ) { $errors[] = array( 'immobile-target-page' ); } - } elseif( !$user->isAllowed( $action ) ) { - $return = null; - $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), - User::getGroupsWithPermission( $action ) ); - if( $groups ) { - $return = array( 'badaccess-groups', - array( implode( ', ', $groups ), count( $groups ) ) ); - } else { - $return = array( "badaccess-group0" ); - } - $errors[] = $return; } wfProfileOut( __METHOD__ ); @@ -1412,7 +1517,7 @@ class Title { } # Shortcut for public wikis, allows skipping quite a bit of code - if ($wgGroupPermissions['*']['read']) + if ( !empty( $wgGroupPermissions['*']['read'] ) ) return true; if( $wgUser->isAllowed( 'read' ) ) { @@ -1510,11 +1615,36 @@ class Title { return $this->mHasSubpages; } - $db = wfGetDB( DB_SLAVE ); - return $this->mHasSubpages = (bool)$db->selectField( 'page', '1', - "page_namespace = {$this->mNamespace} AND page_title LIKE '" - . $db->escapeLike( $this->mDbkeyform ) . "/%'", - __METHOD__ + $subpages = $this->getSubpages( 1 ); + if( $subpages instanceof TitleArray ) + return $this->mHasSubpages = (bool)$subpages->count(); + return $this->mHasSubpages = false; + } + + /** + * Get all subpages of this page. + * @param $limit Maximum number of subpages to fetch; -1 for no limit + * @return mixed TitleArray, or empty array if this page's namespace + * doesn't allow subpages + */ + public function getSubpages( $limit = -1 ) { + if( !MWNamespace::hasSubpages( $this->getNamespace() ) ) + return array(); + + $dbr = wfGetDB( DB_SLAVE ); + $conds['page_namespace'] = $this->getNamespace(); + $conds[] = 'page_title LIKE ' . $dbr->addQuotes( + $dbr->escapeLike( $this->getDBkey() ) . '/%' ); + $options = array(); + if( $limit > -1 ) + $options['LIMIT'] = $limit; + return $this->mSubpages = TitleArray::newFromResult( + $dbr->select( 'page', + array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), + $conds, + __METHOD__, + $options + ) ); } @@ -1849,20 +1979,45 @@ class Title { * @return \type{\int} the number of archived revisions */ public function isDeleted() { - $fname = 'Title::isDeleted'; - if ( $this->getNamespace() < 0 ) { + if( $this->getNamespace() < 0 ) { $n = 0; } else { $dbr = wfGetDB( DB_SLAVE ); - $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(), - 'ar_title' => $this->getDBkey() ), $fname ); + $n = $dbr->selectField( 'archive', 'COUNT(*)', + array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), + __METHOD__ + ); if( $this->getNamespace() == NS_FILE ) { $n += $dbr->selectField( 'filearchive', 'COUNT(*)', - array( 'fa_name' => $this->getDBkey() ), $fname ); + array( 'fa_name' => $this->getDBkey() ), + __METHOD__ + ); } } return (int)$n; } + + /** + * Is there a version of this page in the deletion archive? + * @return bool + */ + public function isDeletedQuick() { + if( $this->getNamespace() < 0 ) { + return false; + } + $dbr = wfGetDB( DB_SLAVE ); + $deleted = (bool)$dbr->selectField( 'archive', '1', + array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), + __METHOD__ + ); + if( !$deleted && $this->getNamespace() == NS_FILE ) { + $deleted = (bool)$dbr->selectField( 'filearchive', '1', + array( 'fa_name' => $this->getDBkey() ), + __METHOD__ + ); + } + return $deleted; + } /** * Get the article ID for this Title from the link cache, @@ -1955,7 +2110,7 @@ class Title { $linkCache = LinkCache::singleton(); $linkCache->clearBadLink( $this->getPrefixedDBkey() ); - if ( 0 == $newid ) { $this->mArticleID = -1; } + if ( $newid === false ) { $this->mArticleID = -1; } else { $this->mArticleID = $newid; } $this->mRestrictionsLoaded = false; $this->mRestrictions = array(); @@ -2064,14 +2219,22 @@ class Title { # Namespace or interwiki prefix $firstPass = true; + $prefixRegexp = "/^(.+?)_*:_*(.*)$/S"; do { $m = array(); - if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $dbkey, $m ) ) { + if ( preg_match( $prefixRegexp, $dbkey, $m ) ) { $p = $m[1]; - if ( $ns = $wgContLang->getNsIndex( $p )) { + if ( $ns = $wgContLang->getNsIndex( $p ) ) { # Ordinary namespace $dbkey = $m[2]; $this->mNamespace = $ns; + # For Talk:X pages, check if X has a "namespace" prefix + if( $ns == NS_TALK && preg_match( $prefixRegexp, $dbkey, $x ) ) { + if( $wgContLang->getNsIndex( $x[1] ) ) + return false; # Disallow Talk:File:x type titles... + else if( Interwiki::isValidInterwiki( $x[1] ) ) + return false; # Disallow Talk:Interwiki:x type titles... + } } elseif( Interwiki::isValidInterwiki( $p ) ) { if( !$firstPass ) { # Can't make a local interwiki link to an interwiki link. @@ -2254,13 +2417,13 @@ class Title { * WARNING: do not use this function on arbitrary user-supplied titles! * On heavily-used templates it will max out the memory. * - * @param $options \type{\string} may be FOR UPDATE + * @param array $options may be FOR UPDATE * @return \type{\arrayof{Title}} the Title objects linking here */ - public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) { + public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { $linkCache = LinkCache::singleton(); - if ( $options ) { + if ( count( $options ) > 0 ) { $db = wfGetDB( DB_MASTER ); } else { $db = wfGetDB( DB_SLAVE ); @@ -2295,10 +2458,10 @@ class Title { * WARNING: do not use this function on arbitrary user-supplied titles! * On heavily-used templates it will max out the memory. * - * @param $options \type{\string} may be FOR UPDATE + * @param array $options may be FOR UPDATE * @return \type{\arrayof{Title}} the Title objects linking here */ - public function getTemplateLinksTo( $options = '' ) { + public function getTemplateLinksTo( $options = array() ) { return $this->getLinksTo( $options, 'templatelinks', 'tl' ); } @@ -2306,42 +2469,35 @@ class Title { * Get an array of Title objects referring to non-existent articles linked from this page * * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case) - * @param $options \type{\string} may be FOR UPDATE * @return \type{\arrayof{Title}} the Title objects */ - public function getBrokenLinksFrom( $options = '' ) { + public function getBrokenLinksFrom() { if ( $this->getArticleId() == 0 ) { # All links from article ID 0 are false positives return array(); } - if ( $options ) { - $db = wfGetDB( DB_MASTER ); - } else { - $db = wfGetDB( DB_SLAVE ); - } - - $res = $db->safeQuery( - "SELECT pl_namespace, pl_title - FROM ! - LEFT JOIN ! - ON pl_namespace=page_namespace - AND pl_title=page_title - WHERE pl_from=? - AND page_namespace IS NULL - !", - $db->tableName( 'pagelinks' ), - $db->tableName( 'page' ), - $this->getArticleId(), - $options ); + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( + array( 'page', 'pagelinks' ), + array( 'pl_namespace', 'pl_title' ), + array( + 'pl_from' => $this->getArticleId(), + 'page_namespace IS NULL' + ), + __METHOD__, array(), + array( + 'page' => array( + 'LEFT JOIN', + array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) + ) + ) + ); $retVal = array(); - if ( $db->numRows( $res ) ) { - foreach( $res as $row ) { - $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); - } + foreach( $res as $row ) { + $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); } - $db->freeResult( $res ); return $retVal; } @@ -2459,7 +2615,7 @@ class Title { $nt->getUserPermissionsErrors('edit', $wgUser) ); } - $match = EditPage::matchSpamRegex( $reason ); + $match = EditPage::matchSummarySpamRegex( $reason ); if( $match !== false ) { // This is kind of lame, won't display nice $errors[] = array('spamprotectiontext'); @@ -2559,8 +2715,8 @@ class Title { ); # Update the protection log $log = new LogPage( 'protect' ); - $comment = wfMsgForContent('prot_1movedto2',$this->getPrefixedText(), $nt->getPrefixedText() ); - if( $reason ) $comment .= ': ' . $reason; + $comment = wfMsgForContent( 'prot_1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); + if( $reason ) $comment .= wfMsgForContent( 'colon-separator' ) . $reason; $log->addEntry( 'move_prot', $nt, $comment, array($this->getPrefixedText()) ); // FIXME: $params? } @@ -2601,8 +2757,16 @@ class Title { # Update message cache for interface messages if( $nt->getNamespace() == NS_MEDIAWIKI ) { global $wgMessageCache; - $oldarticle = new Article( $this ); - $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() ); + + # @bug 17860: old article can be deleted, if this the case, + # delete it from message cache + if ( $this->getArticleID === 0 ) { + $wgMessageCache->replace( $this->getDBkey(), false ); + } else { + $oldarticle = new Article( $this ); + $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() ); + } + $newarticle = new Article( $nt ); $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() ); } @@ -2830,6 +2994,67 @@ class Title { } /** + * Move this page's subpages to be subpages of $nt + * @param $nt Title Move target + * @param $auth bool Whether $wgUser's permissions should be checked + * @param $reason string The reason for the move + * @param $createRedirect bool Whether to create redirects from the old subpages to the new ones + * Ignored if the user doesn't have the 'suppressredirect' right + * @return mixed array with old page titles as keys, and strings (new page titles) or + * arrays (errors) as values, or an error array with numeric indices if no pages were moved + */ + public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { + global $wgUser, $wgMaximumMovedPages; + // Check permissions + if( !$this->userCan( 'move-subpages' ) ) + return array( 'cant-move-subpages' ); + // Do the source and target namespaces support subpages? + if( !MWNamespace::hasSubpages( $this->getNamespace() ) ) + return array( 'namespace-nosubpages', + MWNamespace::getCanonicalName( $this->getNamespace() ) ); + if( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) + return array( 'namespace-nosubpages', + MWNamespace::getCanonicalName( $nt->getNamespace() ) ); + + $subpages = $this->getSubpages($wgMaximumMovedPages + 1); + $retval = array(); + $count = 0; + foreach( $subpages as $oldSubpage ) { + $count++; + if( $count > $wgMaximumMovedPages ) { + $retval[$oldSubpage->getPrefixedTitle()] = + array( 'movepage-max-pages', + $wgMaximumMovedPages ); + break; + } + + if( $oldSubpage->getArticleId() == $this->getArticleId() ) + // When moving a page to a subpage of itself, + // don't move it twice + continue; + $newPageName = preg_replace( + '#^'.preg_quote( $this->getDBKey(), '#' ).'#', + $nt->getDBKey(), $oldSubpage->getDBKey() ); + if( $oldSubpage->isTalkPage() ) { + $newNs = $nt->getTalkPage()->getNamespace(); + } else { + $newNs = $nt->getSubjectPage()->getNamespace(); + } + # Bug 14385: we need makeTitleSafe because the new page names may + # be longer than 255 characters. + $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); + + $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); + if( $success === true ) { + $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); + } else { + $retval[$oldSubpage->getPrefixedText()] = $success; + } + } + return $retval; + } + + /** * Checks if this page is just a one-rev redirect. * Adds lock, so don't use just for light purposes. * @@ -2842,7 +3067,7 @@ class Title { array( 'page_is_redirect', 'page_latest', 'page_id' ), $this->pageCond(), __METHOD__, - 'FOR UPDATE' + array( 'FOR UPDATE' ) ); # Cache some fields we may want $this->mArticleID = $row ? intval($row->page_id) : 0; @@ -2860,7 +3085,7 @@ class Title { 'page_latest != rev_id' ), __METHOD__, - 'FOR UPDATE' + array( 'FOR UPDATE' ) ); # Return true if there was no history return ($row === false); @@ -3034,6 +3259,28 @@ class Title { } /** + * Get the first revision of the page + * + * @param $flags \type{\int} GAID_FOR_UPDATE + * @return Revision (or NULL if page doesn't exist) + */ + public function getFirstRevision( $flags=0 ) { + $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); + $pageId = $this->getArticleId($flags); + if( !$pageId ) return NULL; + $row = $db->selectRow( 'revision', '*', + array( 'rev_page' => $pageId ), + __METHOD__, + array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ) + ); + if( !$row ) { + return NULL; + } else { + return new Revision( $row ); + } + } + + /** * Check if this is a new page * * @return bool @@ -3074,8 +3321,8 @@ class Title { 'rev_page = ' . intval( $this->getArticleId() ) . ' AND rev_id > ' . intval( $old ) . ' AND rev_id < ' . intval( $new ), - __METHOD__, - array( 'USE INDEX' => 'PRIMARY' ) ); + __METHOD__ + ); } /** @@ -3094,7 +3341,7 @@ class Title { /** * Callback for usort() to do title sorts by (namespace, title) */ - static function compare( $a, $b ) { + public static function compare( $a, $b ) { if( $a->getNamespace() == $b->getNamespace() ) { return strcmp( $a->getText(), $b->getText() ); } else { @@ -3144,7 +3391,7 @@ class Title { if( $this->mInterwiki != '' ) { return true; // any interwiki link might be viewable, for all we know } - switch( $this->mNamespace ) { + switch( $this->mNamespace ) { case NS_MEDIA: case NS_FILE: return wfFindFile( $this ); // file exists, possibly in a foreign repo @@ -3250,9 +3497,9 @@ class Title { * @return \type{\string} Trackback URL */ public function trackbackURL() { - global $wgScriptPath, $wgServer; + global $wgScriptPath, $wgServer, $wgScriptExtension; - return "$wgServer$wgScriptPath/trackback.php?article=" + return "$wgServer$wgScriptPath/trackback$wgScriptExtension?article=" . htmlspecialchars(urlencode($this->getPrefixedDBkey())); } @@ -3396,4 +3643,36 @@ class Title { } return $redirs; } + + /** + * Check if this Title is a valid redirect target + * + * @return \type{\bool} TRUE or FALSE + */ + public function isValidRedirectTarget() { + global $wgInvalidRedirectTargets; + + // invalid redirect targets are stored in a global array, but explicity disallow Userlogout here + if( $this->isSpecial( 'Userlogout' ) ) { + return false; + } + + foreach( $wgInvalidRedirectTargets as $target ) { + if( $this->isSpecial( $target ) ) { + return false; + } + } + + return true; + } + + /** + * Get a backlink cache object + */ + function getBacklinkCache() { + if ( is_null( $this->mBacklinkCache ) ) { + $this->mBacklinkCache = new BacklinkCache( $this ); + } + return $this->mBacklinkCache; + } } diff --git a/includes/UploadBase.php b/includes/UploadBase.php deleted file mode 100644 index 91155a1b..00000000 --- a/includes/UploadBase.php +++ /dev/null @@ -1,867 +0,0 @@ -<?php - -class UploadBase { - var $mTempPath; - var $mDesiredDestName, $mDestName, $mRemoveTempFile, $mSourceType; - var $mTitle = false, $mTitleError = 0; - var $mFilteredName, $mFinalExtension; - - const SUCCESS = 0; - const OK = 0; - const BEFORE_PROCESSING = 1; - const LARGE_FILE_SERVER = 2; - const EMPTY_FILE = 3; - const MIN_LENGTH_PARTNAME = 4; - const ILLEGAL_FILENAME = 5; - const PROTECTED_PAGE = 6; - const OVERWRITE_EXISTING_FILE = 7; - const FILETYPE_MISSING = 8; - const FILETYPE_BADTYPE = 9; - const VERIFICATION_ERROR = 10; - const UPLOAD_VERIFICATION_ERROR = 11; - const UPLOAD_WARNING = 12; - const INTERNAL_ERROR = 13; - - const SESSION_VERSION = 2; - - /** - * Returns true if uploads are enabled. - * Can be overriden by subclasses. - */ - static function isEnabled() { - global $wgEnableUploads; - return $wgEnableUploads; - } - /** - * Returns true if the user can use this upload module or else a string - * identifying the missing permission. - * Can be overriden by subclasses. - */ - static function isAllowed( $user ) { - if( !$user->isAllowed( 'upload' ) ) - return 'upload'; - return true; - } - - // Upload handlers. Should probably just be a global - static $uploadHandlers = array( 'Stash', 'Upload', 'Url' ); - /** - * Create a form of UploadBase depending on wpSourceType and initializes it - */ - static function createFromRequest( &$request, $type = null ) { - $type = $type ? $type : $request->getVal( 'wpSourceType' ); - if( !$type ) - return null; - $type = ucfirst($type); - $className = 'UploadFrom'.$type; - if( !in_array( $type, self::$uploadHandlers ) ) - return null; - if( !call_user_func( array( $className, 'isEnabled' ) ) ) - return null; - if( !call_user_func( array( $className, 'isValidRequest' ), $request ) ) - return null; - - $handler = new $className; - $handler->initializeFromRequest( $request ); - return $handler; - } - - /** - * Check whether a request if valid for this handler - */ - static function isValidRequest( $request ) { - return false; - } - - function __construct() {} - - /** - * Do the real variable initialization - */ - function initialize( $name, $tempPath, $fileSize, $removeTempFile = false ) { - $this->mDesiredDestName = $name; - $this->mTempPath = $tempPath; - $this->mFileSize = $fileSize; - $this->mRemoveTempFile = $removeTempFile; - } - - /** - * Fetch the file. Usually a no-op - */ - function fetchFile() { - return self::OK; - } - - /** - * Verify whether the upload is sane. - * Returns self::OK or else an array with error information - */ - function verifyUpload() { - global $wgUser; - - /** - * If there was no filename or a zero size given, give up quick. - */ - if( empty( $this->mFileSize ) ) - return array( 'status' => self::EMPTY_FILE ); - - $nt = $this->getTitle(); - if( is_null( $nt ) ) { - $result = array( 'status' => $this->mTitleError ); - if( $this->mTitleError == self::ILLEGAL_FILENAME ) - $resul['filtered'] = $this->mFilteredName; - if ( $this->mTitleError == self::FILETYPE_BADTYPE ) - $result['finalExt'] = $this->mFinalExtension; - return $result; - } - $this->mLocalFile = wfLocalFile( $nt ); - $this->mDestName = $this->mLocalFile->getName(); - - /** - * In some cases we may forbid overwriting of existing files. - */ - $overwrite = $this->checkOverwrite( $this->mDestName ); - if( $overwrite !== true ) - return array( 'status' => self::OVERWRITE_EXISTING_FILE, 'overwrite' => $overwrite ); - - /** - * Look at the contents of the file; if we can recognize the - * type but it's corrupt or data of the wrong type, we should - * probably not accept it. - */ - $verification = $this->verifyFile( $this->mTempPath ); - - if( $verification !== true ) { - if( !is_array( $verification ) ) - $verification = array( $verification ); - $verification['status'] = self::VERIFICATION_ERROR; - return $verification; - } - - $error = ''; - if( !wfRunHooks( 'UploadVerification', - array( $this->mDestName, $this->mTempPath, &$error ) ) ) { - return array( 'status' => self::UPLOAD_VERIFICATION_ERROR, 'error' => $error ); - } - - return self::OK; - } - - /** - * Verifies that it's ok to include the uploaded file - * - * @param string $tmpfile the full path of the temporary file to verify - * @return mixed true of the file is verified, a string or array otherwise. - */ - protected function verifyFile( $tmpfile ) { - $this->mFileProps = File::getPropsFromPath( $this->mTempPath, - $this->mFinalExtension ); - $this->checkMacBinary(); - - #magically determine mime type - $magic = MimeMagic::singleton(); - $mime = $magic->guessMimeType( $tmpfile, false ); - - #check mime type, if desired - global $wgVerifyMimeType; - if ( $wgVerifyMimeType ) { - - wfDebug ( "\n\nmime: <$mime> extension: <{$this->mFinalExtension}>\n\n"); - #check mime type against file extension - if( !self::verifyExtension( $mime, $this->mFinalExtension ) ) { - return 'uploadcorrupt'; - } - - #check mime type blacklist - global $wgMimeTypeBlacklist; - if( isset($wgMimeTypeBlacklist) && !is_null($wgMimeTypeBlacklist) - && $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) { - return array( 'filetype-badmime', $mime ); - } - } - - #check for htmlish code and javascript - if( $this->detectScript ( $tmpfile, $mime, $this->mFinalExtension ) ) { - return 'uploadscripted'; - } - - /** - * Scan the uploaded file for viruses - */ - $virus = $this->detectVirus($tmpfile); - if ( $virus ) { - return array( 'uploadvirus', $virus ); - } - - wfDebug( __METHOD__.": all clear; passing.\n" ); - return true; - } - - /** - * Check whether the user can edit, upload and create the image - */ - function verifyPermissions( $user ) { - /** - * If the image is protected, non-sysop users won't be able - * to modify it by uploading a new revision. - */ - $nt = $this->getTitle(); - if( is_null( $nt ) ) - return true; - $permErrors = $nt->getUserPermissionsErrors( 'edit', $user ); - $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user ); - $permErrorsCreate = ( $nt->exists() ? array() : $nt->getUserPermissionsErrors( 'create', $user ) ); - if( $permErrors || $permErrorsUpload || $permErrorsCreate ) { - $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) ); - $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) ); - return $permErrors; - } - return true; - } - - /** - * Check for non fatal problems with the file - */ - function checkWarnings() { - $warning = array(); - - $filename = $this->mLocalFile->getName(); - $n = strrpos( $filename, '.' ); - $partname = $n ? substr( $filename, 0, $n ) : $filename; - - // Check whether the resulting filename is different from the desired one - if( $this->mDesiredDestName != $filename ) - $warning['badfilename'] = $filename; - - // Check whether the file extension is on the unwanted list - global $wgCheckFileExtensions, $wgFileExtensions; - if ( $wgCheckFileExtensions ) { - if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) - $warning['filetype-unwanted-type'] = $this->mFinalExtension; - } - - global $wgUploadSizeWarning; - if ( $wgUploadSizeWarning && ( $this->mFileSize > $wgUploadSizeWarning ) ) - $warning['large-file'] = $wgUploadSizeWarning; - - if ( $this->mFileSize == 0 ) - $warning['emptyfile'] = true; - - $exists = self::getExistsWarning( $this->mLocalFile ); - if( $exists !== false ) - $warning['exists'] = $exists; - - // Check whether this may be a thumbnail - if( $exists !== false && $exists[0] != 'thumb' - && self::isThumbName( $this->mLocalFile->getName() ) ) - $warning['file-thumbnail-no'] = substr( $filename , 0, - strpos( $nt->getText() , '-' ) +1 ); - - $hash = File::sha1Base36( $this->mTempPath ); - $dupes = RepoGroup::singleton()->findBySha1( $hash ); - if( $dupes ) - $warning['duplicate'] = $dupes; - - $filenamePrefixBlacklist = self::getFilenamePrefixBlacklist(); - foreach( $filenamePrefixBlacklist as $prefix ) { - if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) { - $warning['filename-bad-prefix'] = $prefix; - break; - } - } - - # If the file existed before and was deleted, warn the user of this - # Don't bother doing so if the file exists now, however - if( $this->mLocalFile->wasDeleted() && !$this->mLocalFile->exists() ) - $warning['filewasdeleted'] = $this->mLocalFile->getTitle(); - - return $warning; - } - - /** - * Really perform the upload. - */ - function performUpload( $comment, $pageText, $watch, $user ) { - $status = $this->mLocalFile->upload( $this->mTempPath, $comment, $pageText, - File::DELETE_SOURCE, $this->mFileProps, false, $user ); - - if( $status->isGood() && $watch ) { - $user->addWatch( $this->mLocalFile->getTitle() ); - } - - if( $status->isGood() ) - wfRunHooks( 'UploadComplete', array( &$this ) ); - - return $status; - } - - /** - * Returns a title or null - */ - function getTitle() { - if ( $this->mTitle !== false ) - return $this->mTitle; - - /** - * Chop off any directories in the given filename. Then - * filter out illegal characters, and try to make a legible name - * out of it. We'll strip some silently that Title would die on. - */ - - $basename = $this->mDesiredDestName; - - $this->mFilteredName = wfStripIllegalFilenameChars( $basename ); - - /** - * We'll want to blacklist against *any* 'extension', and use - * only the final one for the whitelist. - */ - list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName ); - - if( count( $ext ) ) { - $this->mFinalExtension = $ext[count( $ext ) - 1]; - } else { - $this->mFinalExtension = ''; - } - - /* Don't allow users to override the blacklist (check file extension) */ - global $wgCheckFileExtensions, $wgStrictFileExtensions; - global $wgFileExtensions, $wgFileBlacklist; - if ( $this->mFinalExtension == '' ) { - $this->mTitleError = self::FILETYPE_MISSING; - return $this->mTitle = null; - } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) || - ( $wgCheckFileExtensions && $wgStrictFileExtensions && - !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) { - $this->mTitleError = self::FILETYPE_BADTYPE; - return $this->mTitle = null; - } - - # If there was more than one "extension", reassemble the base - # filename to prevent bogus complaints about length - if( count( $ext ) > 1 ) { - for( $i = 0; $i < count( $ext ) - 1; $i++ ) - $partname .= '.' . $ext[$i]; - } - - if( strlen( $partname ) < 1 ) { - $this->mTitleError = self::MIN_LENGTH_PARTNAME; - return $this->mTitle = null; - } - - $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName ); - if( is_null( $nt ) ) { - $this->mTitleError = self::ILLEGAL_FILENAME; - return $this->mTitle = null; - } - return $this->mTitle = $nt; - } - - function getLocalFile() { - if( is_null( $this->mLocalFile ) ) { - $nt = $this->getTitle(); - $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt ); - } - return $this->mLocalFile; - } - - /** - * Stash a file in a temporary directory for later processing - * after the user has confirmed it. - * - * If the user doesn't explicitly cancel or accept, these files - * can accumulate in the temp directory. - * - * @param string $saveName - the destination filename - * @param string $tempName - the source temporary file to save - * @return string - full path the stashed file, or false on failure - * @access private - */ - function saveTempUploadedFile( $saveName, $tempName ) { - global $wgOut; - $repo = RepoGroup::singleton()->getLocalRepo(); - $status = $repo->storeTemp( $saveName, $tempName ); - return $status; - } - - /** - * Stash a file in a temporary directory for later processing, - * and save the necessary descriptive info into the session. - * Returns a key value which will be passed through a form - * to pick up the path info on a later invocation. - * - * @return int - * @access private - */ - function stashSession() { - $status = $this->saveTempUploadedFile( $this->mDestName, $this->mTempPath ); - - if( !$status->isGood() ) { - # Couldn't save the file. - return false; - } - - return array( - 'mTempPath' => $status->value, - 'mFileSize' => $this->mFileSize, - 'mFileProps' => $this->mFileProps, - 'version' => self::SESSION_VERSION, - ); - } - - /** - * Remove a temporarily kept file stashed by saveTempUploadedFile(). - * @return success - */ - function unsaveUploadedFile() { - $repo = RepoGroup::singleton()->getLocalRepo(); - $success = $repo->freeTemp( $this->mTempPath ); - return $success; - } - - /** - * If we've modified the upload file we need to manually remove it - * on exit to clean up. - * @access private - */ - function cleanupTempFile() { - if ( $this->mRemoveTempFile && file_exists( $this->mTempPath ) ) { - wfDebug( __METHOD__.": Removing temporary file {$this->mTempPath}\n" ); - unlink( $this->mTempPath ); - } - } - - function getTempPath() { - return $this->mTempPath; - } - - - /** - * Split a file into a base name and all dot-delimited 'extensions' - * on the end. Some web server configurations will fall back to - * earlier pseudo-'extensions' to determine type and execute - * scripts, so the blacklist needs to check them all. - * - * @return array - */ - function splitExtensions( $filename ) { - $bits = explode( '.', $filename ); - $basename = array_shift( $bits ); - return array( $basename, $bits ); - } - - /** - * Perform case-insensitive match against a list of file extensions. - * Returns true if the extension is in the list. - * - * @param string $ext - * @param array $list - * @return bool - */ - function checkFileExtension( $ext, $list ) { - return in_array( strtolower( $ext ), $list ); - } - - /** - * Perform case-insensitive match against a list of file extensions. - * Returns true if any of the extensions are in the list. - * - * @param array $ext - * @param array $list - * @return bool - */ - function checkFileExtensionList( $ext, $list ) { - foreach( $ext as $e ) { - if( in_array( strtolower( $e ), $list ) ) { - return true; - } - } - return false; - } - - - /** - * Checks if the mime type of the uploaded file matches the file extension. - * - * @param string $mime the mime type of the uploaded file - * @param string $extension The filename extension that the file is to be served with - * @return bool - */ - public static function verifyExtension( $mime, $extension ) { - $magic = MimeMagic::singleton(); - - if ( ! $mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) - if ( ! $magic->isRecognizableExtension( $extension ) ) { - wfDebug( __METHOD__.": passing file with unknown detected mime type; " . - "unrecognized extension '$extension', can't verify\n" ); - return true; - } else { - wfDebug( __METHOD__.": rejecting file with unknown detected mime type; ". - "recognized extension '$extension', so probably invalid file\n" ); - return false; - } - - $match= $magic->isMatchingExtension($extension,$mime); - - if ($match===NULL) { - wfDebug( __METHOD__.": no file extension known for mime type $mime, passing file\n" ); - return true; - } elseif ($match===true) { - wfDebug( __METHOD__.": mime type $mime matches extension $extension, passing file\n" ); - - #TODO: if it's a bitmap, make sure PHP or ImageMagic resp. can handle it! - return true; - - } else { - wfDebug( __METHOD__.": mime type $mime mismatches file extension $extension, rejecting file\n" ); - return false; - } - } - - /** - * Heuristic for detecting files that *could* contain JavaScript instructions or - * things that may look like HTML to a browser and are thus - * potentially harmful. The present implementation will produce false positives in some situations. - * - * @param string $file Pathname to the temporary upload file - * @param string $mime The mime type of the file - * @param string $extension The extension of the file - * @return bool true if the file contains something looking like embedded scripts - */ - function detectScript($file, $mime, $extension) { - global $wgAllowTitlesInSVG; - - #ugly hack: for text files, always look at the entire file. - #For binary field, just check the first K. - - if (strpos($mime,'text/')===0) $chunk = file_get_contents( $file ); - else { - $fp = fopen( $file, 'rb' ); - $chunk = fread( $fp, 1024 ); - fclose( $fp ); - } - - $chunk= strtolower( $chunk ); - - if (!$chunk) return false; - - #decode from UTF-16 if needed (could be used for obfuscation). - if (substr($chunk,0,2)=="\xfe\xff") $enc= "UTF-16BE"; - elseif (substr($chunk,0,2)=="\xff\xfe") $enc= "UTF-16LE"; - else $enc= NULL; - - if ($enc) $chunk= iconv($enc,"ASCII//IGNORE",$chunk); - - $chunk= trim($chunk); - - #FIXME: convert from UTF-16 if necessarry! - - wfDebug("SpecialUpload::detectScript: checking for embedded scripts and HTML stuff\n"); - - #check for HTML doctype - if (eregi("<!DOCTYPE *X?HTML",$chunk)) return true; - - /** - * Internet Explorer for Windows performs some really stupid file type - * autodetection which can cause it to interpret valid image files as HTML - * and potentially execute JavaScript, creating a cross-site scripting - * attack vectors. - * - * Apple's Safari browser also performs some unsafe file type autodetection - * which can cause legitimate files to be interpreted as HTML if the - * web server is not correctly configured to send the right content-type - * (or if you're really uploading plain text and octet streams!) - * - * Returns true if IE is likely to mistake the given file for HTML. - * Also returns true if Safari would mistake the given file for HTML - * when served with a generic content-type. - */ - - $tags = array( - '<body', - '<head', - '<html', #also in safari - '<img', - '<pre', - '<script', #also in safari - '<table' - ); - if( ! $wgAllowTitlesInSVG && $extension !== 'svg' && $mime !== 'image/svg' ) { - $tags[] = '<title'; - } - - foreach( $tags as $tag ) { - if( false !== strpos( $chunk, $tag ) ) { - return true; - } - } - - /* - * look for javascript - */ - - #resolve entity-refs to look at attributes. may be harsh on big files... cache result? - $chunk = Sanitizer::decodeCharReferences( $chunk ); - - #look for script-types - if (preg_match('!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim',$chunk)) return true; - - #look for html-style script-urls - if (preg_match('!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true; - - #look for css-style script-urls - if (preg_match('!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim',$chunk)) return true; - - wfDebug("SpecialUpload::detectScript: no scripts found\n"); - return false; - } - - /** - * Generic wrapper function for a virus scanner program. - * This relies on the $wgAntivirus and $wgAntivirusSetup variables. - * $wgAntivirusRequired may be used to deny upload if the scan fails. - * - * @param string $file Pathname to the temporary upload file - * @return mixed false if not virus is found, NULL if the scan fails or is disabled, - * or a string containing feedback from the virus scanner if a virus was found. - * If textual feedback is missing but a virus was found, this function returns true. - */ - function detectVirus($file) { - global $wgAntivirus, $wgAntivirusSetup, $wgAntivirusRequired, $wgOut; - - if ( !$wgAntivirus ) { - wfDebug( __METHOD__.": virus scanner disabled\n"); - return NULL; - } - - if ( !$wgAntivirusSetup[$wgAntivirus] ) { - wfDebug( __METHOD__.": unknown virus scanner: $wgAntivirus\n" ); - $wgOut->wrapWikiMsg( '<div class="error">$1</div>', array( 'virus-badscanner', $wgAntivirus ) ); - return wfMsg('virus-unknownscanner') . " $wgAntivirus"; - } - - # look up scanner configuration - $command = $wgAntivirusSetup[$wgAntivirus]["command"]; - $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]["codemap"]; - $msgPattern = isset( $wgAntivirusSetup[$wgAntivirus]["messagepattern"] ) ? - $wgAntivirusSetup[$wgAntivirus]["messagepattern"] : null; - - if ( strpos( $command,"%f" ) === false ) { - # simple pattern: append file to scan - $command .= " " . wfEscapeShellArg( $file ); - } else { - # complex pattern: replace "%f" with file to scan - $command = str_replace( "%f", wfEscapeShellArg( $file ), $command ); - } - - wfDebug( __METHOD__.": running virus scan: $command \n" ); - - # execute virus scanner - $exitCode = false; - - #NOTE: there's a 50 line workaround to make stderr redirection work on windows, too. - # that does not seem to be worth the pain. - # Ask me (Duesentrieb) about it if it's ever needed. - $output = array(); - if ( wfIsWindows() ) { - exec( "$command", $output, $exitCode ); - } else { - exec( "$command 2>&1", $output, $exitCode ); - } - - # map exit code to AV_xxx constants. - $mappedCode = $exitCode; - if ( $exitCodeMap ) { - if ( isset( $exitCodeMap[$exitCode] ) ) { - $mappedCode = $exitCodeMap[$exitCode]; - } elseif ( isset( $exitCodeMap["*"] ) ) { - $mappedCode = $exitCodeMap["*"]; - } - } - - if ( $mappedCode === AV_SCAN_FAILED ) { - # scan failed (code was mapped to false by $exitCodeMap) - wfDebug( __METHOD__.": failed to scan $file (code $exitCode).\n" ); - - if ( $wgAntivirusRequired ) { - return wfMsg('virus-scanfailed', array( $exitCode ) ); - } else { - return NULL; - } - } else if ( $mappedCode === AV_SCAN_ABORTED ) { - # scan failed because filetype is unknown (probably imune) - wfDebug( __METHOD__.": unsupported file type $file (code $exitCode).\n" ); - return NULL; - } else if ( $mappedCode === AV_NO_VIRUS ) { - # no virus found - wfDebug( __METHOD__.": file passed virus scan.\n" ); - return false; - } else { - $output = join( "\n", $output ); - $output = trim( $output ); - - if ( !$output ) { - $output = true; #if there's no output, return true - } elseif ( $msgPattern ) { - $groups = array(); - if ( preg_match( $msgPattern, $output, $groups ) ) { - if ( $groups[1] ) { - $output = $groups[1]; - } - } - } - - wfDebug( __METHOD__.": FOUND VIRUS! scanner feedback: $output" ); - return $output; - } - } - - /** - * Check if the temporary file is MacBinary-encoded, as some uploads - * from Internet Explorer on Mac OS Classic and Mac OS X will be. - * If so, the data fork will be extracted to a second temporary file, - * which will then be checked for validity and either kept or discarded. - * - * @access private - */ - function checkMacBinary() { - $macbin = new MacBinary( $this->mTempPath ); - if( $macbin->isValid() ) { - $dataFile = tempnam( wfTempDir(), "WikiMacBinary" ); - $dataHandle = fopen( $dataFile, 'wb' ); - - wfDebug( "SpecialUpload::checkMacBinary: Extracting MacBinary data fork to $dataFile\n" ); - $macbin->extractData( $dataHandle ); - - $this->mTempPath = $dataFile; - $this->mFileSize = $macbin->dataForkLength(); - - // We'll have to manually remove the new file if it's not kept. - $this->mRemoveTempFile = true; - } - $macbin->close(); - } - - /** - * Check if there's an overwrite conflict and, if so, if restrictions - * forbid this user from performing the upload. - * - * @return mixed true on success, WikiError on failure - * @access private - */ - function checkOverwrite() { - global $wgUser; - // First check whether the local file can be overwritten - if( $this->mLocalFile->exists() ) - if( !self::userCanReUpload( $wgUser, $this->mLocalFile ) ) - return 'fileexists-forbidden'; - - // Check shared conflicts - $file = wfFindFile( $this->mLocalFile->getName() ); - if ( $file && ( !$wgUser->isAllowed( 'reupload' ) || - !$wgUser->isAllowed( 'reupload-shared' ) ) ) - return 'fileexists-shared-forbidden'; - - return true; - - } - - /** - * Check if a user is the last uploader - * - * @param User $user - * @param string $img, image name - * @return bool - */ - public static function userCanReUpload( User $user, $img ) { - if( $user->isAllowed( 'reupload' ) ) - return true; // non-conditional - if( !$user->isAllowed( 'reupload-own' ) ) - return false; - if( is_string( $img ) ) - $img = wfLocalFile( $img ); - if ( !( $img instanceof LocalFile ) ) - return false; - - return $user->getId() == $img->getUser( 'id' ); - } - - public static function getExistsWarning( $file ) { - if( $file->exists() ) - return array( 'exists', $file ); - - if( $file->getTitle()->getArticleID() ) - return array( 'page-exists', $file ); - - if( strpos( $file->getName(), '.' ) == false ) { - $partname = $file->getName(); - $rawExtension = ''; - } else { - $n = strrpos( $file->getName(), '.' ); - $rawExtension = substr( $file->getName(), $n + 1 ); - $partname = substr( $file->getName(), 0, $n ); - } - - if ( $rawExtension != $file->getExtension() ) { - // We're not using the normalized form of the extension. - // Normal form is lowercase, using most common of alternate - // extensions (eg 'jpg' rather than 'JPEG'). - // - // Check for another file using the normalized form... - $nt_lc = Title::makeTitle( NS_FILE, $partname . '.' . $file->getExtension() ); - $file_lc = wfLocalFile( $nt_lc ); - - if( $file_lc->exists() ) - return array( 'exists-normalized', $file_lc ); - } - - if ( self::isThumbName( $file->getName() ) ) { - # Check for filenames like 50px- or 180px-, these are mostly thumbnails - $nt_thb = Title::newFromText( substr( $partname , strpos( $partname , '-' ) +1 ) . '.' . $rawExtension ); - $file_thb = wfLocalFile( $nt_thb ); - if( $file_thb->exists() ) - return array( 'thumb', $file_thb ); - } - - return false; - } - - public static function isThumbName( $filename ) { - $n = strrpos( $filename, '.' ); - $partname = $n ? substr( $filename, 0, $n ) : $filename; - return ( - substr( $partname , 3, 3 ) == 'px-' || - substr( $partname , 2, 3 ) == 'px-' - ) && - ereg( "[0-9]{2}" , substr( $partname , 0, 2) ); - } - - /** - * Get a list of blacklisted filename prefixes from [[MediaWiki:filename-prefix-blacklist]] - * - * @return array list of prefixes - */ - public static function getFilenamePrefixBlacklist() { - $blacklist = array(); - $message = wfMsgForContent( 'filename-prefix-blacklist' ); - if( $message && !( wfEmptyMsg( 'filename-prefix-blacklist', $message ) || $message == '-' ) ) { - $lines = explode( "\n", $message ); - foreach( $lines as $line ) { - // Remove comment lines - $comment = substr( trim( $line ), 0, 1 ); - if ( $comment == '#' || $comment == '' ) { - continue; - } - // Remove additional comments after a prefix - $comment = strpos( $line, '#' ); - if ( $comment > 0 ) { - $line = substr( $line, 0, $comment-1 ); - } - $blacklist[] = trim( $line ); - } - } - return $blacklist; - } - - -} diff --git a/includes/UploadFromStash.php b/includes/UploadFromStash.php deleted file mode 100644 index 8bff3b49..00000000 --- a/includes/UploadFromStash.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -class UploadFromStash extends UploadBase { - static function isValidSessionKey( $key, $sessionData ) { - return !empty( $key ) && - is_array( $sessionData ) && - isset( $sessionData[$key] ) && - isset( $sessionData[$key]['version'] ) && - $sessionData[$key]['version'] == self::SESSION_VERSION - ; - } - static function isValidRequest( $request ) { - $sessionData = $request->getSessionData('wsUploadData'); - return self::isValidSessionKey( - $request->getInt( 'wpSessionKey' ), - $sessionData - ); - } - - function initialize( $name, $sessionData ) { - /** - * Confirming a temporarily stashed upload. - * We don't want path names to be forged, so we keep - * them in the session on the server and just give - * an opaque key to the user agent. - */ - $this->initialize( $name, - $sessionData['mTempPath'], - $sessionData['mFileSize'], - false - ); - - $this->mFileProps = $sessionData['mFileProps']; - } - function initializeFromRequest( &$request ) { - $sessionKey = $request->getInt( 'wpSessionKey' ); - $sessionData = $request->getSessionData('wsUploadData'); - - $desiredDestName = $request->getText( 'wpDestFile' ); - if( !$desiredDestName ) - $desiredDestName = $request->getText( 'wpUploadFile' ); - - return $this->initialize( $desiredDestName, $sessionData[$sessionKey] ); - } - - /** - * File has been previously verified so no need to do so again. - */ - protected function verifyFile( $tmpfile ) { - return true; - } - /** - * We're here from "ignore warnings anyway" so return just OK - */ - function checkWarnings() { - return array(); - } -} diff --git a/includes/UploadFromUpload.php b/includes/UploadFromUpload.php deleted file mode 100644 index 1b6762c6..00000000 --- a/includes/UploadFromUpload.php +++ /dev/null @@ -1,20 +0,0 @@ -<?php - -class UploadFromUpload extends UploadBase { - - function initializeFromRequest( &$request ) { - $desiredDestName = $request->getText( 'wpDestFile' ); - if( !$desiredDestName ) - $desiredDestName = $request->getText( 'wpUploadFile' ); - - return $this->initialize( - $desiredDestName, - $request->getFileTempName( 'wpUploadFile' ), - $request->getFileSize( 'wpUploadFile' ) - ); - } - - static function isValidRequest( $request ) { - return (bool)$request->getFileTempName( 'wpUploadFile' ); - } -} diff --git a/includes/UploadFromUrl.php b/includes/UploadFromUrl.php deleted file mode 100644 index 7e23b8cd..00000000 --- a/includes/UploadFromUrl.php +++ /dev/null @@ -1,92 +0,0 @@ -<?php - - -class UploadFromUrl extends UploadBase { - static function isAllowed( $user ) { - if( !$user->isAllowed( 'upload_by_url' ) ) - return 'upload_by_url'; - return parent::isAllowed( $user ); - } - static function isEnabled() { - global $wgAllowCopyUploads; - return $wgAllowCopyUploads && parent::isEnabled(); - } - - function initialize( $name, $url ) { - global $wgTmpDirectory; - $local_file = tempnam( $wgTmpDirectory, 'WEBUPLOAD' ); - $this-initialize( $name, $local_file, 0, true ); - - $this->mUrl = trim( $url ); - } - - /** - * Do the real fetching stuff - */ - function fetchFile() { - if( stripos($this->mUrl, 'http://') !== 0 && stripos($this->mUrl, 'ftp://') !== 0 ) { - return array( - 'status' => self::BEFORE_PROCESSING, - 'error' => 'upload-proto-error', - ); - } - $res = $this->curlCopy(); - if( $res !== true ) { - return array( - 'status' => self::BEFORE_PROCESSING, - 'error' => $res, - ); - } - return self::OK; - } - - /** - * Safe copy from URL - * Returns true if there was an error, false otherwise - */ - private function curlCopy() { - global $wgUser, $wgOut; - - # Open temporary file - $this->mCurlDestHandle = @fopen( $this->mTempPath, "wb" ); - if( $this->mCurlDestHandle === false ) { - # Could not open temporary file to write in - return 'upload-file-error'; - } - - $ch = curl_init(); - curl_setopt( $ch, CURLOPT_HTTP_VERSION, 1.0); # Probably not needed, but apparently can work around some bug - curl_setopt( $ch, CURLOPT_TIMEOUT, 10); # 10 seconds timeout - curl_setopt( $ch, CURLOPT_LOW_SPEED_LIMIT, 512); # 0.5KB per second minimum transfer speed - curl_setopt( $ch, CURLOPT_URL, $this->mUrl); - curl_setopt( $ch, CURLOPT_WRITEFUNCTION, array( $this, 'uploadCurlCallback' ) ); - curl_exec( $ch ); - $error = curl_errno( $ch ); - curl_close( $ch ); - - fclose( $this->mCurlDestHandle ); - unset( $this->mCurlDestHandle ); - - if( $error ) - return "upload-curl-error$errornum"; - - return true; - } - - /** - * Callback function for CURL-based web transfer - * Write data to file unless we've passed the length limit; - * if so, abort immediately. - * @access private - */ - function uploadCurlCallback( $ch, $data ) { - global $wgMaxUploadSize; - $length = strlen( $data ); - $this->mFileSize += $length; - if( $this->mFileSize > $wgMaxUploadSize ) { - return 0; - } - fwrite( $this->mCurlDestHandle, $data ); - return $length; - } -} diff --git a/includes/User.php b/includes/User.php index 9fee089c..cc861ad4 100644 --- a/includes/User.php +++ b/includes/User.php @@ -141,9 +141,11 @@ class User { 'createtalk', 'delete', 'deletedhistory', + 'deleterevision', 'edit', 'editinterface', 'editusercssjs', + 'hideuser', 'import', 'importupload', 'ipblock-exempt', @@ -155,6 +157,7 @@ class User { 'move-subpages', 'nominornewtalk', 'noratelimit', + 'override-export-depth', 'patrol', 'protect', 'proxyunbannable', @@ -164,13 +167,17 @@ class User { 'reupload-shared', 'rollback', 'siteadmin', + 'suppressionlog', 'suppressredirect', + 'suppressrevision', 'trackback', 'undelete', 'unwatchedpages', 'upload', 'upload_by_url', 'userrights', + 'userrights-interwiki', + 'writeapi', ); /** * \string Cached results of getAllRights() @@ -581,11 +588,12 @@ class User { * @return \bool True or false */ static function isCreatableName( $name ) { + global $wgInvalidUsernameCharacters; return self::isUsableName( $name ) && // Registration-time character blacklisting... - strpos( $name, '@' ) === false; + !preg_match( '/[' . preg_quote( $wgInvalidUsernameCharacters, '/' ) . ']/', $name ); } /** @@ -886,6 +894,8 @@ class User { $dbr = wfGetDB( DB_MASTER ); $s = $dbr->selectRow( 'user', '*', array( 'user_id' => $this->mId ), __METHOD__ ); + wfRunHooks( 'UserLoadFromDatabase', array( $this, &$s ) ); + if ( $s !== false ) { # Initialise user table data $this->loadFromRow( $s ); @@ -909,7 +919,7 @@ class User { $this->mDataLoaded = true; if ( isset( $row->user_id ) ) { - $this->mId = $row->user_id; + $this->mId = intval( $row->user_id ); } $this->mName = $row->user_name; $this->mRealName = $row->user_real_name; @@ -1013,9 +1023,14 @@ class User { * @return \type{\arrayof{\string}} Array of user toggle names */ static function getToggles() { - global $wgContLang; + global $wgContLang, $wgUseRCPatrol; $extraToggles = array(); wfRunHooks( 'UserToggles', array( &$extraToggles ) ); + if( $wgUseRCPatrol ) { + $extraToggles[] = 'hidepatrolled'; + $extraToggles[] = 'newpageshidepatrolled'; + $extraToggles[] = 'watchlisthidepatrolled'; + } return array_merge( self::$mToggles, $extraToggles, $wgContLang->getExtraUserToggles() ); } @@ -1149,10 +1164,17 @@ class User { */ public function isPingLimitable() { global $wgRateLimitsExcludedGroups; + global $wgRateLimitsExcludedIPs; if( array_intersect( $this->getEffectiveGroups(), $wgRateLimitsExcludedGroups ) ) { // Deprecated, but kept for backwards-compatibility config return false; } + if( in_array( wfGetIP(), $wgRateLimitsExcludedIPs ) ) { + // No other good way currently to disable rate limits + // for specific IPs. :P + // But this is a crappy hack and should die. + return false; + } return !$this->isAllowed('noratelimit'); } @@ -1309,6 +1331,15 @@ class User { } /** + * If user is blocked, return the ID for the block + * @return \int Block ID + */ + function getBlockId() { + $this->getBlockedStatus(); + return ($this->mBlock ? $this->mBlock->mId : false); + } + + /** * Check if user is blocked on all wikis. * Do not use for actual edit permission checks! * This is intented for quick UI checks. @@ -1909,6 +1940,13 @@ class User { } $this->mOptions[$oname] = $val; } + + /** + * Reset all options to the site defaults + */ + function restoreOptions() { + $this->mOptions = User::getDefaultOptions(); + } /** * Get the user's preferred date format. @@ -1983,7 +2021,7 @@ class User { * @return \int User'e edit count */ function getEditCount() { - if ($this->mId) { + if ($this->getId()) { if ( !isset( $this->mEditCount ) ) { /* Populate the count, if it has not been populated yet */ $this->mEditCount = User::edits($this->mId); @@ -2073,11 +2111,15 @@ class User { * @param $action \string action to be checked * @return \bool True if action is allowed, else false */ - function isAllowed($action='') { + function isAllowed( $action = '' ) { if ( $action === '' ) - // In the spirit of DWIM - return true; - + return true; // In the spirit of DWIM + # Patrolling may not be enabled + if( $action === 'patrol' || $action === 'autopatrol' ) { + global $wgUseRCPatrol, $wgUseNPPatrol; + if( !$wgUseRCPatrol && !$wgUseNPPatrol ) + return false; + } # Use strict parameter to avoid matching numeric 0 accidentally inserted # by misconfiguration: 0 == 'foo' return in_array( $action, $this->getRights(), true ); @@ -2705,7 +2747,14 @@ class User { * @return \bool True if matches, false otherwise */ function checkTemporaryPassword( $plaintext ) { - return self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ); + global $wgNewPasswordExpiry; + if( self::comparePasswords( $this->mNewpassword, $plaintext, $this->getId() ) ) { + $this->load(); + $expiry = wfTimestamp( TS_UNIX, $this->mNewpassTime ) + $wgNewPasswordExpiry; + return ( time() < $expiry ); + } else { + return false; + } } /** diff --git a/includes/UserArray.php b/includes/UserArray.php index a2f54b7f..d48a4440 100644 --- a/includes/UserArray.php +++ b/includes/UserArray.php @@ -12,6 +12,17 @@ abstract class UserArray implements Iterator { return $userArray; } + static function newFromIDs( $ids ) { + $ids = array_map( 'intval', (array)$ids ); // paranoia + if ( !$ids ) + // Database::select() doesn't like empty arrays + return new ArrayIterator(array()); + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'user', '*', array( 'user_id' => $ids ), + __METHOD__ ); + return self::newFromResult( $res ); + } + protected static function newFromResult_internal( $res ) { $userArray = new UserArrayFromResult( $res ); return $userArray; diff --git a/includes/UserMailer.php b/includes/UserMailer.php index ab1a740b..b6484935 100644 --- a/includes/UserMailer.php +++ b/includes/UserMailer.php @@ -40,7 +40,7 @@ class MailAddress { } else { $this->address = strval( $address ); $this->name = strval( $name ); - $this->reaName = strval( $realName ); + $this->realName = strval( $realName ); } } @@ -101,7 +101,7 @@ class UserMailer { * @param $from MailAddress: sender's email * @param $subject String: email's subject. * @param $body String: email's text. - * @param $replyto String: optional reply-to email (default: null). + * @param $replyto MailAddress: optional reply-to email (default: null). * @param $contentType String: optional custom Content-Type * @return mixed True on success, a WikiError object on failure. */ @@ -281,11 +281,44 @@ class EmailNotification { * @param $oldid (default: false) */ function notifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid = false) { - global $wgEnotifUseJobQ; + global $wgEnotifUseJobQ, $wgEnotifWatchlist, $wgShowUpdatedMarker; - if( $title->getNamespace() < 0 ) + if ($title->getNamespace() < 0) return; + // Build a list of users to notfiy + $watchers = array(); + if ($wgEnotifWatchlist || $wgShowUpdatedMarker) { + $dbw = wfGetDB( DB_MASTER ); + $res = $dbw->select( array( 'watchlist' ), + array( 'wl_user' ), + array( + 'wl_title' => $title->getDBkey(), + 'wl_namespace' => $title->getNamespace(), + 'wl_user != ' . intval( $editor->getID() ), + 'wl_notificationtimestamp IS NULL', + ), __METHOD__ + ); + while ($row = $dbw->fetchObject( $res ) ) { + $watchers[] = intval( $row->wl_user ); + } + if ($watchers) { + // Update wl_notificationtimestamp for all watching users except + // the editor + $dbw->begin(); + $dbw->update( 'watchlist', + array( /* SET */ + 'wl_notificationtimestamp' => $dbw->timestamp( $timestamp ) + ), array( /* WHERE */ + 'wl_title' => $title->getDBkey(), + 'wl_namespace' => $title->getNamespace(), + 'wl_user' => $watchers + ), __METHOD__ + ); + $dbw->commit(); + } + } + if ($wgEnotifUseJobQ) { $params = array( "editor" => $editor->getName(), @@ -293,11 +326,12 @@ class EmailNotification { "timestamp" => $timestamp, "summary" => $summary, "minorEdit" => $minorEdit, - "oldid" => $oldid); + "oldid" => $oldid, + "watchers" => $watchers); $job = new EnotifNotifyJob( $title, $params ); $job->insert(); } else { - $this->actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid); + $this->actuallyNotifyOnPageChange( $editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers ); } } @@ -310,16 +344,16 @@ class EmailNotification { * * @param $editor User object * @param $title Title object - * @param $timestamp - * @param $summary - * @param $minorEdit - * @param $oldid (default: false) + * @param $timestamp string Edit timestamp + * @param $summary string Edit summary + * @param $minorEdit bool + * @param $oldid int Revision ID + * @param $watchers array of user IDs */ - function actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid=false) { - + function actuallyNotifyOnPageChange($editor, $title, $timestamp, $summary, $minorEdit, $oldid, $watchers) { # we use $wgPasswordSender as sender's address global $wgEnotifWatchlist; - global $wgEnotifMinorEdits, $wgEnotifUserTalk, $wgShowUpdatedMarker; + global $wgEnotifMinorEdits, $wgEnotifUserTalk; global $wgEnotifImpersonal; wfProfileIn( __METHOD__ ); @@ -364,30 +398,12 @@ class EmailNotification { if ( $wgEnotifWatchlist ) { // Send updates to watchers other than the current editor - $userCondition = 'wl_user != ' . $editor->getID(); - if ( $userTalkId !== false ) { - // Already sent an email to this person - $userCondition .= ' AND wl_user != ' . intval( $userTalkId ); - } - $dbr = wfGetDB( DB_SLAVE ); - - list( $user ) = $dbr->tableNamesN( 'user' ); - - $res = $dbr->select( array( 'watchlist', 'user' ), - array( "$user.*" ), - array( - 'wl_user=user_id', - 'wl_title' => $title->getDBkey(), - 'wl_namespace' => $title->getNamespace(), - $userCondition, - 'wl_notificationtimestamp IS NULL', - ), __METHOD__ ); - $userArray = UserArray::newFromResult( $res ); - + $userArray = UserArray::newFromIDs( $watchers ); foreach ( $userArray as $watchingUser ) { if ( $watchingUser->getOption( 'enotifwatchlistpages' ) && ( !$minorEdit || $watchingUser->getOption('enotifminoredits') ) && - $watchingUser->isEmailConfirmed() ) + $watchingUser->isEmailConfirmed() && + $watchingUser->getID() != $userTalkId ) { $this->compose( $watchingUser ); } @@ -402,28 +418,8 @@ class EmailNotification { } $this->sendMails(); - - $latestTimestamp = Revision::getTimestampFromId( $title, $title->getLatestRevID() ); - // Do not update watchlists if something else already did. - if ( $timestamp >= $latestTimestamp && ($wgShowUpdatedMarker || $wgEnotifWatchlist) ) { - # Mark the changed watch-listed page with a timestamp, so that the page is - # listed with an "updated since your last visit" icon in the watch list. Do - # not do this to users for their own edits. - $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'watchlist', - array( /* SET */ - 'wl_notificationtimestamp' => $dbw->timestamp($timestamp) - ), array( /* WHERE */ - 'wl_title' => $title->getDBkey(), - 'wl_namespace' => $title->getNamespace(), - 'wl_notificationtimestamp IS NULL', - 'wl_user != ' . $editor->getID() - ), __METHOD__ - ); - } - wfProfileOut( __METHOD__ ); - } # function NotifyOnChange + } /** * @private @@ -563,7 +559,7 @@ class EmailNotification { * @private */ function sendPersonalised( $watchingUser ) { - global $wgLang, $wgEnotifUseRealName; + global $wgContLang, $wgEnotifUseRealName; // From the PHP manual: // Note: The to parameter cannot be an address in the form of "Something <someone@example.com>". // The mail command will not parse this properly while talking with the MTA. @@ -577,7 +573,7 @@ class EmailNotification { # expressed in terms of individual local time of the notification # recipient, i.e. watching user $body = str_replace('$PAGEEDITDATE', - $wgLang->timeanddate( $this->timestamp, true, false, $timecorrection ), + $wgContLang->timeanddate( $this->timestamp, true, false, $timecorrection ), $body); return UserMailer::send($to, $this->from, $this->subject, $body, $this->replyto); @@ -588,7 +584,7 @@ class EmailNotification { * mailing. Takes an array of MailAddress objects. */ function sendImpersonal( $addresses ) { - global $wgLang; + global $wgContLang; if (empty($addresses)) return; @@ -597,7 +593,7 @@ class EmailNotification { array( '$WATCHINGUSERNAME', '$PAGEEDITDATE'), array( wfMsgForContent('enotif_impersonal_salutation'), - $wgLang->timeanddate($this->timestamp, true, false, false)), + $wgContLang->timeanddate($this->timestamp, true, false, false)), $this->body); return UserMailer::send($addresses, $this->from, $this->subject, $body, $this->replyto); diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php index 2d2d34f1..a2c1f036 100644 --- a/includes/WatchedItem.php +++ b/includes/WatchedItem.php @@ -38,11 +38,10 @@ class WatchedItem { public function isWatched() { # Pages and their talk pages are considered equivalent for watching; # remember that talk namespaces are numbered as page namespace+1. - $fname = 'WatchedItem::isWatched'; $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'watchlist', 1, array( 'wl_user' => $this->id, 'wl_namespace' => $this->ns, - 'wl_title' => $this->ti ), $fname ); + 'wl_title' => $this->ti ), __METHOD__ ); $iswatched = ($dbr->numRows( $res ) > 0) ? 1 : 0; return $iswatched; } @@ -53,31 +52,30 @@ class WatchedItem { * @return bool (always true) */ public function addWatch() { - $fname = 'WatchedItem::addWatch'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); // Use INSERT IGNORE to avoid overwriting the notification timestamp // if there's already an entry for this page $dbw = wfGetDB( DB_MASTER ); $dbw->insert( 'watchlist', array( - 'wl_user' => $this->id, - 'wl_namespace' => ($this->ns & ~1), + 'wl_user' => $this->id, + 'wl_namespace' => MWNamespace::getSubject($this->ns), 'wl_title' => $this->ti, 'wl_notificationtimestamp' => NULL - ), $fname, 'IGNORE' ); + ), __METHOD__, 'IGNORE' ); // Every single watched page needs now to be listed in watchlist; // namespace:page and namespace_talk:page need separate entries: $dbw->insert( 'watchlist', array( 'wl_user' => $this->id, - 'wl_namespace' => ($this->ns | 1 ), + 'wl_namespace' => MWNamespace::getTalk($this->ns), 'wl_title' => $this->ti, 'wl_notificationtimestamp' => NULL - ), $fname, 'IGNORE' ); + ), __METHOD__, 'IGNORE' ); - wfProfileOut( $fname ); + wfProfileOut( __METHOD__ ); return true; } @@ -86,16 +84,14 @@ class WatchedItem { * @return bool */ public function removeWatch() { - $fname = 'WatchedItem::removeWatch'; - $success = false; $dbw = wfGetDB( DB_MASTER ); $dbw->delete( 'watchlist', array( 'wl_user' => $this->id, - 'wl_namespace' => ($this->ns & ~1), + 'wl_namespace' => MWNamespace::getSubject($this->ns), 'wl_title' => $this->ti - ), $fname + ), __METHOD__ ); if ( $dbw->affectedRows() ) { $success = true; @@ -108,9 +104,9 @@ class WatchedItem { $dbw->delete( 'watchlist', array( 'wl_user' => $this->id, - 'wl_namespace' => ($this->ns | 1), + 'wl_namespace' => MWNamespace::getTalk($this->ns), 'wl_title' => $this->ti - ), $fname + ), __METHOD__ ); if ( $dbw->affectedRows() ) { @@ -134,8 +130,7 @@ class WatchedItem { /** * Handle duplicate entries. Backend for duplicateEntries(). */ - private static function doDuplicateEntries( $ot, $nt ) { - $fname = "WatchedItem::duplicateEntries"; + private static function doDuplicateEntries( $ot, $nt ) { $oldnamespace = $ot->getNamespace(); $newnamespace = $nt->getNamespace(); $oldtitle = $ot->getDBkey(); @@ -144,7 +139,7 @@ class WatchedItem { $dbw = wfGetDB( DB_MASTER ); $res = $dbw->select( 'watchlist', 'wl_user', array( 'wl_namespace' => $oldnamespace, 'wl_title' => $oldtitle ), - $fname, 'FOR UPDATE' + __METHOD__, 'FOR UPDATE' ); # Construct array to replace into the watchlist $values = array(); @@ -165,7 +160,7 @@ class WatchedItem { # Perform replace # Note that multi-row replace is very efficient for MySQL but may be inefficient for # some other DBMSes, mostly due to poor simulation by us - $dbw->replace( 'watchlist', array(array( 'wl_user', 'wl_namespace', 'wl_title')), $values, $fname ); + $dbw->replace( 'watchlist', array( array( 'wl_user', 'wl_namespace', 'wl_title' ) ), $values, __METHOD__ ); return true; } } diff --git a/includes/WatchlistEditor.php b/includes/WatchlistEditor.php index e49851bd..82f62f6a 100644 --- a/includes/WatchlistEditor.php +++ b/includes/WatchlistEditor.php @@ -407,6 +407,8 @@ class WatchlistEditor { * @return string */ private function buildRemoveLine( $title, $redirect, $skin ) { + global $wgLang; + $link = $skin->makeLinkObj( $title ); if( $redirect ) $link = '<span class="watchlistredir">' . $link . '</span>'; @@ -419,7 +421,7 @@ class WatchlistEditor { } return "<li>" . Xml::check( 'titles[]', false, array( 'value' => $title->getPrefixedText() ) ) - . $link . " (" . implode( ' | ', $tools ) . ")" . "</li>\n"; + . $link . " (" . $wgLang->pipeList( $tools ) . ")" . "</li>\n"; } /** @@ -480,11 +482,13 @@ class WatchlistEditor { * @return string */ public static function buildTools( $skin ) { + global $wgLang; + $tools = array(); $modes = array( 'view' => false, 'edit' => 'edit', 'raw' => 'raw' ); foreach( $modes as $mode => $subpage ) { $tools[] = $skin->makeKnownLinkObj( SpecialPage::getTitleFor( 'Watchlist', $subpage ), wfMsgHtml( "watchlisttools-{$mode}" ) ); } - return implode( ' | ', $tools ); + return $wgLang->pipeList( $tools ); } } diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 46747125..0928e4d5 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -231,6 +231,7 @@ class WebRequest { $data = $this->normalizeUnicode( $data ); return $data; } else { + taint( $default ); return $default; } } @@ -251,7 +252,7 @@ class WebRequest { $val = $default; } if( is_null( $val ) ) { - return null; + return $val; } else { return (string)$val; } diff --git a/includes/Wiki.php b/includes/Wiki.php index ce4ce67e..38f19c96 100644 --- a/includes/Wiki.php +++ b/includes/Wiki.php @@ -115,12 +115,15 @@ class MediaWiki { if( count( $wgContLang->getVariants() ) > 1 && !is_null( $ret ) && $ret->getArticleID() == 0 ) $wgContLang->findVariantLink( $title, $ret ); } - if( ( $oldid = $wgRequest->getInt( 'oldid' ) ) - && ( is_null( $ret ) || $ret->getNamespace() != NS_SPECIAL ) ) { - // Allow oldid to override a changed or missing title. - $rev = Revision::newFromId( $oldid ); - if( $rev ) { - $ret = $rev->getTitle(); + # For non-special titles, check for implicit titles + if( is_null( $ret ) || $ret->getNamespace() != NS_SPECIAL ) { + // We can have urls with just ?diff=,?oldid= or even just ?diff= + $oldid = $wgRequest->getInt( 'oldid' ); + $oldid = $oldid ? $oldid : $wgRequest->getInt( 'diff' ); + // Allow oldid to override a changed or missing title + if( $oldid ) { + $rev = Revision::newFromId( $oldid ); + $ret = $rev ? $rev->getTitle() : $ret; } } return $ret; @@ -447,8 +450,10 @@ class MediaWiki { $article->view(); break; case 'raw': // includes JS/CSS + wfProfileIn( __METHOD__.'-raw' ); $raw = new RawPage( $article ); $raw->view(); + wfProfileOut( __METHOD__.'-raw' ); break; case 'watch': case 'unwatch': diff --git a/includes/WikiError.php b/includes/WikiError.php index 41edb2f3..251c1742 100644 --- a/includes/WikiError.php +++ b/includes/WikiError.php @@ -75,6 +75,16 @@ class WikiErrorMsg extends WikiError { $args = func_get_args(); array_shift( $args ); $this->mMessage = wfMsgReal( $message, $args, true ); + $this->mMsgKey = $message; + $this->mMsgArgs = $args; + } + + function getMessageKey() { + return $this->mMsgKey; + } + + function getMessageArgs() { + return $this->mMsgArgs; } } diff --git a/includes/Xml.php b/includes/Xml.php index 68990d86..bbe0717c 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -172,6 +172,36 @@ class Xml { . implode( "\n", $options ) . self::closeElement( 'select' ); } + + /** + * @param $year Integer + * @param $month Integer + * @return string Formatted HTML + */ + public static function dateMenu( $year, $month ) { + # Offset overrides year/month selection + if( $month && $month !== -1 ) { + $encMonth = intval( $month ); + } else { + $encMonth = ''; + } + if( $year ) { + $encYear = intval( $year ); + } else if( $encMonth ) { + $thisMonth = intval( gmdate( 'n' ) ); + $thisYear = intval( gmdate( 'Y' ) ); + if( intval($encMonth) > $thisMonth ) { + $thisYear--; + } + $encYear = $thisYear; + } else { + $encYear = ''; + } + return Xml::label( wfMsg( 'year' ), 'year' ) . ' '. + Xml::input( 'year', 4, $encYear, array('id' => 'year', 'maxlength' => 4) ) . ' '. + Xml::label( wfMsg( 'month' ), 'month' ) . ' '. + Xml::monthSelector( $encMonth, -1 ); + } /** * @@ -641,7 +671,6 @@ class Xml { foreach( $fields as $labelmsg => $input ) { $id = "mw-$labelmsg"; - $form .= Xml::openElement( 'tr', array( 'id' => $id ) ); $form .= Xml::tags( 'td', array('class' => 'mw-label'), wfMsgExt( $labelmsg, array('parseinline') ) ); $form .= Xml::openElement( 'td', array( 'class' => 'mw-input' ) ) . $input . Xml::closeElement( 'td' ); @@ -649,7 +678,7 @@ class Xml { } if( $submitLabel ) { - $form .= Xml::openElement( 'tr', array( 'id' => $id ) ); + $form .= Xml::openElement( 'tr' ); $form .= Xml::tags( 'td', array(), '' ); $form .= Xml::openElement( 'td', array( 'class' => 'mw-submit' ) ) . Xml::submitButton( wfMsg( $submitLabel ) ) . Xml::closeElement( 'td' ); $form .= Xml::closeElement( 'tr' ); diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php index 4c1e0ae8..d7655df0 100644 --- a/includes/ZhConversion.php +++ b/includes/ZhConversion.php @@ -7,11356 +7,16956 @@ */ $zh2Hant = array( -"画"=>"畫", -"丰"=>"豐", -"帘"=>"簾", -"愿"=>"願", -"云"=>"雲", -"筑"=>"築", -"厂"=>"廠", -"广"=>"廣", -"别"=>"別", -"冲"=>"沖", -"种"=>"種", -"虫"=>"蟲", -"担"=>"擔", -"党"=>"黨", -"儿"=>"兒", -"柜"=>"櫃", -"坏"=>"壞", -"几"=>"幾", -"价"=>"價", -"据"=>"據", -"适"=>"適", -"蜡"=>"蠟", -"腊"=>"臘", -"万"=>"萬", -"宁"=>"寧", -"苹"=>"蘋", -"确"=>"確", -"胜"=>"勝", -"术"=>"術", -"体"=>"體", -"涂"=>"塗", -"叶"=>"葉", -"与"=>"與", -"恶"=>"惡", -"发"=>"發", -"复"=>"復", -"汇"=>"匯", -"获"=>"獲", -"饥"=>"飢", -"尽"=>"盡", -"历"=>"歷", -"卤"=>"鹵", -"弥"=>"彌", -"签"=>"簽", -"纤"=>"纖", -"苏"=>"蘇", -"坛"=>"壇", -"团"=>"團", -"须"=>"須", -"脏"=>"臟", -"钟"=>"鍾", -"药"=>"葯", -"当"=>"當", -"蕴"=>"蘊", -"线"=>"線", -"为"=>"為", -"产"=>"產", -"众"=>"眾", -"伪"=>"偽", -"凫"=>"鳧", -"厕"=>"廁", -"启"=>"啟", -"墙"=>"牆", -"壳"=>"殼", -"奖"=>"獎", -"妫"=>"媯", -"并"=>"並", -"录"=>"錄", -"悫"=>"愨", -"极"=>"極", -"沩"=>"溈", -"瘘"=>"瘺", -"硷"=>"礆", -"竖"=>"豎", -"绝"=>"絕", -"绣"=>"綉", -"绦"=>"絛", -"绱"=>"緔", -"绷"=>"綳", -"绿"=>"綠", -"缰"=>"韁", -"苧"=>"苎", -"莼"=>"蒓", -"说"=>"說", -"谣"=>"謠", -"谫"=>"譾", -"赃"=>"贓", -"赍"=>"齎", -"赝"=>"贗", -"酝"=>"醞", -"钩"=>"鉤", -"钵"=>"缽", -"锈"=>"銹", -"锐"=>"銳", -"锨"=>"杴", -"镌"=>"鐫", -"镢"=>"钁", -"阅"=>"閱", -"颓"=>"頹", -"颜"=>"顏", -"骂"=>"罵", -"鲇"=>"鯰", -"鲞"=>"鯗", -"鳄"=>"鱷", -"鸡"=>"雞", -"鹚"=>"鶿", -"仑"=>"侖", -"赞"=>"贊", -"荡"=>"盪", -"锤"=>"錘", -"㟆"=>"㠏", -"㛟"=>"𡞵", -"专"=>"專", -"业"=>"業", -"丛"=>"叢", -"东"=>"東", -"丝"=>"絲", -"丢"=>"丟", -"两"=>"兩", -"严"=>"嚴", -"丧"=>"喪", -"个"=>"個", -"临"=>"臨", -"丽"=>"麗", -"举"=>"舉", -"义"=>"義", -"乌"=>"烏", -"乐"=>"樂", -"乔"=>"喬", -"习"=>"習", -"乡"=>"鄉", -"书"=>"書", -"买"=>"買", -"乱"=>"亂", -"争"=>"爭", -"亏"=>"虧", -"亚"=>"亞", -"亩"=>"畝", -"亲"=>"親", -"亵"=>"褻", -"亸"=>"嚲", -"亿"=>"億", -"仅"=>"僅", -"从"=>"從", -"仓"=>"倉", -"仪"=>"儀", -"们"=>"們", -"优"=>"優", -"会"=>"會", -"伛"=>"傴", -"伞"=>"傘", -"伟"=>"偉", -"传"=>"傳", -"伣"=>"俔", -"伤"=>"傷", -"伥"=>"倀", -"伦"=>"倫", -"伧"=>"傖", -"伫"=>"佇", -"佥"=>"僉", -"侠"=>"俠", -"侣"=>"侶", -"侥"=>"僥", -"侦"=>"偵", -"侧"=>"側", -"侨"=>"僑", -"侩"=>"儈", -"侪"=>"儕", -"侬"=>"儂", -"俣"=>"俁", -"俦"=>"儔", -"俨"=>"儼", -"俩"=>"倆", -"俪"=>"儷", -"俫"=>"倈", -"俭"=>"儉", -"债"=>"債", -"倾"=>"傾", -"偬"=>"傯", -"偻"=>"僂", -"偾"=>"僨", -"偿"=>"償", -"傥"=>"儻", -"傧"=>"儐", -"储"=>"儲", -"傩"=>"儺", -"㑩"=>"儸", -"兑"=>"兌", -"兖"=>"兗", -"兰"=>"蘭", -"关"=>"關", -"兴"=>"興", -"兹"=>"茲", -"养"=>"養", -"兽"=>"獸", -"冁"=>"囅", -"内"=>"內", -"冈"=>"岡", -"册"=>"冊", -"写"=>"寫", -"军"=>"軍", -"农"=>"農", -"冯"=>"馮", -"决"=>"決", -"况"=>"況", -"冻"=>"凍", -"净"=>"凈", -"凉"=>"涼", -"减"=>"減", -"凑"=>"湊", -"凛"=>"凜", -"凤"=>"鳳", -"凭"=>"憑", -"凯"=>"凱", -"击"=>"擊", -"凿"=>"鑿", -"刍"=>"芻", -"刘"=>"劉", -"则"=>"則", -"刚"=>"剛", -"创"=>"創", -"删"=>"刪", -"刬"=>"剗", -"刭"=>"剄", -"刹"=>"剎", -"刽"=>"劊", -"刿"=>"劌", -"剀"=>"剴", -"剂"=>"劑", -"剐"=>"剮", -"剑"=>"劍", -"剥"=>"剝", -"剧"=>"劇", -"㓥"=>"劏", -"㔉"=>"劚", -"劝"=>"勸", -"办"=>"辦", -"务"=>"務", -"劢"=>"勱", -"动"=>"動", -"励"=>"勵", -"劲"=>"勁", -"劳"=>"勞", -"势"=>"勢", -"勋"=>"勛", -"勚"=>"勩", -"匀"=>"勻", -"匦"=>"匭", -"匮"=>"匱", -"区"=>"區", -"医"=>"醫", -"华"=>"華", -"协"=>"協", -"单"=>"單", -"卖"=>"賣", -"卢"=>"盧", -"卫"=>"衛", -"却"=>"卻", -"厅"=>"廳", -"厉"=>"厲", -"压"=>"壓", -"厌"=>"厭", -"厍"=>"厙", -"厐"=>"龎", -"厢"=>"廂", -"厣"=>"厴", -"厦"=>"廈", -"厨"=>"廚", -"厩"=>"廄", -"厮"=>"廝", -"县"=>"縣", -"叁"=>"叄", -"参"=>"參", -"双"=>"雙", -"变"=>"變", -"叙"=>"敘", -"叠"=>"疊", -"号"=>"號", -"叹"=>"嘆", -"叽"=>"嘰", -"吓"=>"嚇", -"吕"=>"呂", -"吗"=>"嗎", -"吣"=>"唚", -"吨"=>"噸", -"听"=>"聽", -"吴"=>"吳", -"呐"=>"吶", -"呒"=>"嘸", -"呓"=>"囈", -"呕"=>"嘔", -"呖"=>"嚦", -"呗"=>"唄", -"员"=>"員", -"呙"=>"咼", -"呛"=>"嗆", -"呜"=>"嗚", -"咏"=>"詠", -"咙"=>"嚨", -"咛"=>"嚀", -"咝"=>"噝", -"咤"=>"吒", -"响"=>"響", -"哑"=>"啞", -"哒"=>"噠", -"哓"=>"嘵", -"哔"=>"嗶", -"哕"=>"噦", -"哗"=>"嘩", -"哙"=>"噲", -"哜"=>"嚌", -"哝"=>"噥", -"哟"=>"喲", -"唛"=>"嘜", -"唝"=>"嗊", -"唠"=>"嘮", -"唡"=>"啢", -"唢"=>"嗩", -"唤"=>"喚", -"啧"=>"嘖", -"啬"=>"嗇", -"啭"=>"囀", -"啮"=>"嚙", -"啴"=>"嘽", -"啸"=>"嘯", -"㖞"=>"喎", -"喷"=>"噴", -"喽"=>"嘍", -"喾"=>"嚳", -"嗫"=>"囁", -"嗳"=>"噯", -"嘘"=>"噓", -"嘤"=>"嚶", -"嘱"=>"囑", -"㖊"=>"噚", -"噜"=>"嚕", -"嚣"=>"囂", -"园"=>"園", -"囱"=>"囪", -"围"=>"圍", -"囵"=>"圇", -"国"=>"國", -"图"=>"圖", -"圆"=>"圓", -"圣"=>"聖", -"圹"=>"壙", -"场"=>"場", -"块"=>"塊", -"坚"=>"堅", -"坜"=>"壢", -"坝"=>"壩", -"坞"=>"塢", -"坟"=>"墳", -"坠"=>"墜", -"垄"=>"壟", -"垅"=>"壠", -"垆"=>"壚", -"垒"=>"壘", -"垦"=>"墾", -"垩"=>"堊", -"垫"=>"墊", -"垭"=>"埡", -"垱"=>"壋", -"垲"=>"塏", -"垴"=>"堖", -"埘"=>"塒", -"埙"=>"塤", -"埚"=>"堝", -"埯"=>"垵", -"堑"=>"塹", -"堕"=>"墮", -"𡒄"=>"壈", -"壮"=>"壯", -"声"=>"聲", -"壶"=>"壺", -"壸"=>"壼", -"处"=>"處", -"备"=>"備", -"够"=>"夠", -"头"=>"頭", -"夸"=>"誇", -"夹"=>"夾", -"夺"=>"奪", -"奁"=>"奩", -"奂"=>"奐", -"奋"=>"奮", -"奥"=>"奧", -"妆"=>"妝", -"妇"=>"婦", -"妈"=>"媽", -"妩"=>"嫵", -"妪"=>"嫗", -"姗"=>"姍", -"姹"=>"奼", -"娄"=>"婁", -"娅"=>"婭", -"娆"=>"嬈", -"娇"=>"嬌", -"娈"=>"孌", -"娱"=>"娛", -"娲"=>"媧", -"娴"=>"嫻", -"婳"=>"嫿", -"婴"=>"嬰", -"婵"=>"嬋", -"婶"=>"嬸", -"媪"=>"媼", -"嫒"=>"嬡", -"嫔"=>"嬪", -"嫱"=>"嬙", -"嬷"=>"嬤", -"孙"=>"孫", -"学"=>"學", -"孪"=>"孿", -"宝"=>"寶", -"实"=>"實", -"宠"=>"寵", -"审"=>"審", -"宪"=>"憲", -"宫"=>"宮", -"宽"=>"寬", -"宾"=>"賓", -"寝"=>"寢", -"对"=>"對", -"寻"=>"尋", -"导"=>"導", -"寿"=>"壽", -"将"=>"將", -"尔"=>"爾", -"尘"=>"塵", -"尝"=>"嘗", -"尧"=>"堯", -"尴"=>"尷", -"尸"=>"屍", -"层"=>"層", -"屃"=>"屓", -"屉"=>"屜", -"届"=>"屆", -"属"=>"屬", -"屡"=>"屢", -"屦"=>"屨", -"屿"=>"嶼", -"岁"=>"歲", -"岂"=>"豈", -"岖"=>"嶇", -"岗"=>"崗", -"岘"=>"峴", -"岙"=>"嶴", -"岚"=>"嵐", -"岛"=>"島", -"岭"=>"嶺", -"岽"=>"崬", -"岿"=>"巋", -"峄"=>"嶧", -"峡"=>"峽", -"峣"=>"嶢", -"峤"=>"嶠", -"峥"=>"崢", -"峦"=>"巒", -"崂"=>"嶗", -"崃"=>"崍", -"崄"=>"嶮", -"崭"=>"嶄", -"嵘"=>"嶸", -"嵚"=>"嶔", -"嵝"=>"嶁", -"巅"=>"巔", -"巩"=>"鞏", -"巯"=>"巰", -"币"=>"幣", -"帅"=>"帥", -"师"=>"師", -"帏"=>"幃", -"帐"=>"帳", -"帜"=>"幟", -"带"=>"帶", -"帧"=>"幀", -"帮"=>"幫", -"帱"=>"幬", -"帻"=>"幘", -"帼"=>"幗", -"幂"=>"冪", -"庆"=>"慶", -"庐"=>"廬", -"庑"=>"廡", -"库"=>"庫", -"应"=>"應", -"庙"=>"廟", -"庞"=>"龐", -"废"=>"廢", -"廪"=>"廩", -"开"=>"開", -"异"=>"異", -"弃"=>"棄", -"弑"=>"弒", -"张"=>"張", -"弪"=>"弳", -"弯"=>"彎", -"弹"=>"彈", -"强"=>"強", -"归"=>"歸", -"彦"=>"彥", -"彻"=>"徹", -"径"=>"徑", -"徕"=>"徠", -"忆"=>"憶", -"忏"=>"懺", -"忧"=>"憂", -"忾"=>"愾", -"怀"=>"懷", -"态"=>"態", -"怂"=>"慫", -"怃"=>"憮", -"怄"=>"慪", -"怅"=>"悵", -"怆"=>"愴", -"怜"=>"憐", -"总"=>"總", -"怼"=>"懟", -"怿"=>"懌", -"恋"=>"戀", -"恒"=>"恆", -"恳"=>"懇", -"恸"=>"慟", -"恹"=>"懨", -"恺"=>"愷", -"恻"=>"惻", -"恼"=>"惱", -"恽"=>"惲", -"悦"=>"悅", -"悬"=>"懸", -"悭"=>"慳", -"悮"=>"悞", -"悯"=>"憫", -"惊"=>"驚", -"惧"=>"懼", -"惨"=>"慘", -"惩"=>"懲", -"惫"=>"憊", -"惬"=>"愜", -"惭"=>"慚", -"惮"=>"憚", -"惯"=>"慣", -"愠"=>"慍", -"愤"=>"憤", -"愦"=>"憒", -"慑"=>"懾", -"懑"=>"懣", -"懒"=>"懶", -"懔"=>"懍", -"戆"=>"戇", -"戋"=>"戔", -"戏"=>"戲", -"戗"=>"戧", -"战"=>"戰", -"戬"=>"戩", -"戯"=>"戱", -"户"=>"戶", -"扑"=>"撲", -"执"=>"執", -"扩"=>"擴", -"扪"=>"捫", -"扫"=>"掃", -"扬"=>"揚", -"扰"=>"擾", -"抚"=>"撫", -"抛"=>"拋", -"抟"=>"摶", -"抠"=>"摳", -"抡"=>"掄", -"抢"=>"搶", -"护"=>"護", -"报"=>"報", -"拟"=>"擬", -"拢"=>"攏", -"拣"=>"揀", -"拥"=>"擁", -"拦"=>"攔", -"拧"=>"擰", -"拨"=>"撥", -"择"=>"擇", -"挂"=>"掛", -"挚"=>"摯", -"挛"=>"攣", -"挜"=>"掗", -"挝"=>"撾", -"挞"=>"撻", -"挟"=>"挾", -"挠"=>"撓", -"挡"=>"擋", -"挢"=>"撟", -"挣"=>"掙", -"挤"=>"擠", -"挥"=>"揮", -"挦"=>"撏", -"捝"=>"挩", -"捞"=>"撈", -"损"=>"損", -"捡"=>"撿", -"换"=>"換", -"捣"=>"搗", -"掳"=>"擄", -"掴"=>"摑", -"掷"=>"擲", -"掸"=>"撣", -"掺"=>"摻", -"掼"=>"摜", -"揽"=>"攬", -"揾"=>"搵", -"揿"=>"撳", -"搀"=>"攙", -"搁"=>"擱", -"搂"=>"摟", -"搅"=>"攪", -"携"=>"攜", -"摄"=>"攝", -"摅"=>"攄", -"摆"=>"擺", -"摇"=>"搖", -"摈"=>"擯", -"摊"=>"攤", -"撄"=>"攖", -"撑"=>"撐", -"㧑"=>"撝", -"撵"=>"攆", -"撷"=>"擷", -"撸"=>"擼", -"撺"=>"攛", -"㧟"=>"擓", -"擞"=>"擻", -"攒"=>"攢", -"敌"=>"敵", -"敛"=>"斂", -"数"=>"數", -"斋"=>"齋", -"斓"=>"斕", -"斩"=>"斬", -"断"=>"斷", -"无"=>"無", -"旧"=>"舊", -"时"=>"時", -"旷"=>"曠", -"旸"=>"暘", -"昙"=>"曇", -"昼"=>"晝", -"昽"=>"曨", -"显"=>"顯", -"晋"=>"晉", -"晒"=>"曬", -"晓"=>"曉", -"晔"=>"曄", -"晕"=>"暈", -"晖"=>"暉", -"暂"=>"暫", -"暧"=>"曖", -"机"=>"機", -"杀"=>"殺", -"杂"=>"雜", -"权"=>"權", -"杆"=>"桿", -"条"=>"條", -"来"=>"來", -"杨"=>"楊", -"杩"=>"榪", -"杰"=>"傑", -"构"=>"構", -"枞"=>"樅", -"枢"=>"樞", -"枣"=>"棗", -"枥"=>"櫪", -"枧"=>"梘", -"枨"=>"棖", -"枪"=>"槍", -"枫"=>"楓", -"枭"=>"梟", -"柠"=>"檸", -"柽"=>"檉", -"栀"=>"梔", -"栅"=>"柵", -"标"=>"標", -"栈"=>"棧", -"栉"=>"櫛", -"栊"=>"櫳", -"栋"=>"棟", -"栌"=>"櫨", -"栎"=>"櫟", -"栏"=>"欄", -"树"=>"樹", -"栖"=>"棲", -"样"=>"樣", -"栾"=>"欒", -"桠"=>"椏", -"桡"=>"橈", -"桢"=>"楨", -"档"=>"檔", -"桤"=>"榿", -"桥"=>"橋", -"桦"=>"樺", -"桧"=>"檜", -"桨"=>"槳", -"桩"=>"樁", -"梦"=>"夢", -"梼"=>"檮", -"梾"=>"棶", -"梿"=>"槤", -"检"=>"檢", -"棁"=>"梲", -"棂"=>"欞", -"椁"=>"槨", -"椟"=>"櫝", -"椠"=>"槧", -"椤"=>"欏", -"椭"=>"橢", -"楼"=>"樓", -"榄"=>"欖", -"榅"=>"榲", -"榇"=>"櫬", -"榈"=>"櫚", -"榉"=>"櫸", -"槚"=>"檟", -"槛"=>"檻", -"槟"=>"檳", -"槠"=>"櫧", -"横"=>"橫", -"樯"=>"檣", -"樱"=>"櫻", -"橥"=>"櫫", -"橱"=>"櫥", -"橹"=>"櫓", -"橼"=>"櫞", -"檩"=>"檁", -"欢"=>"歡", -"欤"=>"歟", -"欧"=>"歐", -"歼"=>"殲", -"殁"=>"歿", -"殇"=>"殤", -"残"=>"殘", -"殒"=>"殞", -"殓"=>"殮", -"殚"=>"殫", -"殡"=>"殯", -"㱮"=>"殨", -"㱩"=>"殰", -"殴"=>"毆", -"毁"=>"毀", -"毂"=>"轂", -"毕"=>"畢", -"毙"=>"斃", -"毡"=>"氈", -"毵"=>"毿", -"氇"=>"氌", -"气"=>"氣", -"氢"=>"氫", -"氩"=>"氬", -"氲"=>"氳", -"汉"=>"漢", -"汤"=>"湯", -"汹"=>"洶", -"沟"=>"溝", -"没"=>"沒", -"沣"=>"灃", -"沤"=>"漚", -"沥"=>"瀝", -"沦"=>"淪", -"沧"=>"滄", -"沪"=>"滬", -"泞"=>"濘", -"泪"=>"淚", -"泶"=>"澩", -"泷"=>"瀧", -"泸"=>"瀘", -"泺"=>"濼", -"泻"=>"瀉", -"泼"=>"潑", -"泽"=>"澤", -"泾"=>"涇", -"洁"=>"潔", -"洒"=>"灑", -"洼"=>"窪", -"浃"=>"浹", -"浅"=>"淺", -"浆"=>"漿", -"浇"=>"澆", -"浈"=>"湞", -"浊"=>"濁", -"测"=>"測", -"浍"=>"澮", -"济"=>"濟", -"浏"=>"瀏", -"浐"=>"滻", -"浑"=>"渾", -"浒"=>"滸", -"浓"=>"濃", -"浔"=>"潯", -"涛"=>"濤", -"涝"=>"澇", -"涞"=>"淶", -"涟"=>"漣", -"涠"=>"潿", -"涡"=>"渦", -"涣"=>"渙", -"涤"=>"滌", -"润"=>"潤", -"涧"=>"澗", -"涨"=>"漲", -"涩"=>"澀", -"渊"=>"淵", -"渌"=>"淥", -"渍"=>"漬", -"渎"=>"瀆", -"渐"=>"漸", -"渑"=>"澠", -"渔"=>"漁", -"渖"=>"瀋", -"渗"=>"滲", -"温"=>"溫", -"湾"=>"灣", -"湿"=>"濕", -"溃"=>"潰", -"溅"=>"濺", -"溆"=>"漵", -"滗"=>"潷", -"滚"=>"滾", -"滞"=>"滯", -"滟"=>"灧", -"滠"=>"灄", -"满"=>"滿", -"滢"=>"瀅", -"滤"=>"濾", -"滥"=>"濫", -"滦"=>"灤", -"滨"=>"濱", -"滩"=>"灘", -"滪"=>"澦", -"漤"=>"灠", -"潆"=>"瀠", -"潇"=>"瀟", -"潋"=>"瀲", -"潍"=>"濰", -"潜"=>"潛", -"潴"=>"瀦", -"澜"=>"瀾", -"濑"=>"瀨", -"濒"=>"瀕", -"㲿"=>"瀇", -"灏"=>"灝", -"灭"=>"滅", -"灯"=>"燈", -"灵"=>"靈", -"灾"=>"災", -"灿"=>"燦", -"炀"=>"煬", -"炉"=>"爐", -"炖"=>"燉", -"炜"=>"煒", -"炝"=>"熗", -"点"=>"點", -"炼"=>"煉", -"炽"=>"熾", -"烁"=>"爍", -"烂"=>"爛", -"烃"=>"烴", -"烛"=>"燭", -"烟"=>"煙", -"烦"=>"煩", -"烧"=>"燒", -"烨"=>"燁", -"烩"=>"燴", -"烫"=>"燙", -"烬"=>"燼", -"热"=>"熱", -"焕"=>"煥", -"焖"=>"燜", -"焘"=>"燾", -"㶽"=>"煱", -"煴"=>"熅", -"㶶"=>"燶", -"爱"=>"愛", -"爷"=>"爺", -"牍"=>"牘", -"牦"=>"氂", -"牵"=>"牽", -"牺"=>"犧", -"犊"=>"犢", -"状"=>"狀", -"犷"=>"獷", -"犸"=>"獁", -"犹"=>"猶", -"狈"=>"狽", -"狝"=>"獮", -"狞"=>"獰", -"独"=>"獨", -"狭"=>"狹", -"狮"=>"獅", -"狯"=>"獪", -"狰"=>"猙", -"狱"=>"獄", -"狲"=>"猻", -"猃"=>"獫", -"猎"=>"獵", -"猕"=>"獼", -"猡"=>"玀", -"猪"=>"豬", -"猫"=>"貓", -"猬"=>"蝟", -"献"=>"獻", -"獭"=>"獺", -"㺍"=>"獱", -"玑"=>"璣", -"玚"=>"瑒", -"玛"=>"瑪", -"玮"=>"瑋", -"环"=>"環", -"现"=>"現", -"玱"=>"瑲", -"玺"=>"璽", -"珐"=>"琺", -"珑"=>"瓏", -"珰"=>"璫", -"珲"=>"琿", -"琏"=>"璉", -"琐"=>"瑣", -"琼"=>"瓊", -"瑶"=>"瑤", -"瑷"=>"璦", -"璎"=>"瓔", -"瓒"=>"瓚", -"瓯"=>"甌", -"电"=>"電", -"画"=>"畫", -"畅"=>"暢", -"畴"=>"疇", -"疖"=>"癤", -"疗"=>"療", -"疟"=>"瘧", -"疠"=>"癘", -"疡"=>"瘍", -"疬"=>"癧", -"疭"=>"瘲", -"疮"=>"瘡", -"疯"=>"瘋", -"疱"=>"皰", -"疴"=>"痾", -"痈"=>"癰", -"痉"=>"痙", -"痒"=>"癢", -"痖"=>"瘂", -"痨"=>"癆", -"痪"=>"瘓", -"痫"=>"癇", -"瘅"=>"癉", -"瘆"=>"瘮", -"瘗"=>"瘞", -"瘪"=>"癟", -"瘫"=>"癱", -"瘾"=>"癮", -"瘿"=>"癭", -"癞"=>"癩", -"癣"=>"癬", -"癫"=>"癲", -"皑"=>"皚", -"皱"=>"皺", -"皲"=>"皸", -"盏"=>"盞", -"盐"=>"鹽", -"监"=>"監", -"盖"=>"蓋", -"盗"=>"盜", -"盘"=>"盤", -"眍"=>"瞘", -"眦"=>"眥", -"眬"=>"矓", -"睁"=>"睜", -"睐"=>"睞", -"睑"=>"瞼", -"瞆"=>"瞶", -"瞒"=>"瞞", -"䁖"=>"瞜", -"瞩"=>"矚", -"矫"=>"矯", -"矶"=>"磯", -"矾"=>"礬", -"矿"=>"礦", -"砀"=>"碭", -"码"=>"碼", -"砖"=>"磚", -"砗"=>"硨", -"砚"=>"硯", -"砜"=>"碸", -"砺"=>"礪", -"砻"=>"礱", -"砾"=>"礫", -"础"=>"礎", -"硁"=>"硜", -"硕"=>"碩", -"硖"=>"硤", -"硗"=>"磽", -"硙"=>"磑", -"碍"=>"礙", -"碛"=>"磧", -"碜"=>"磣", -"碱"=>"鹼", -"礼"=>"禮", -"祃"=>"禡", -"祎"=>"禕", -"祢"=>"禰", -"祯"=>"禎", -"祷"=>"禱", -"祸"=>"禍", -"禀"=>"稟", -"禄"=>"祿", -"禅"=>"禪", -"离"=>"離", -"秃"=>"禿", -"秆"=>"稈", -"积"=>"積", -"称"=>"稱", -"秽"=>"穢", -"秾"=>"穠", -"稆"=>"穭", -"税"=>"稅", -"䅉"=>"稏", -"稣"=>"穌", -"稳"=>"穩", -"穑"=>"穡", -"穷"=>"窮", -"窃"=>"竊", -"窍"=>"竅", -"窎"=>"窵", -"窑"=>"窯", -"窜"=>"竄", -"窝"=>"窩", -"窥"=>"窺", -"窦"=>"竇", -"窭"=>"窶", -"竞"=>"競", -"笃"=>"篤", -"笋"=>"筍", -"笔"=>"筆", -"笕"=>"筧", -"笺"=>"箋", -"笼"=>"籠", -"笾"=>"籩", -"筚"=>"篳", -"筛"=>"篩", -"筜"=>"簹", -"筝"=>"箏", -"䇲"=>"筴", -"筹"=>"籌", -"筼"=>"篔", -"简"=>"簡", -"箓"=>"籙", -"箦"=>"簀", -"箧"=>"篋", -"箨"=>"籜", -"箩"=>"籮", -"箪"=>"簞", -"箫"=>"簫", -"篑"=>"簣", -"篓"=>"簍", -"篮"=>"籃", -"篱"=>"籬", -"簖"=>"籪", -"籁"=>"籟", -"籴"=>"糴", -"类"=>"類", -"籼"=>"秈", -"粜"=>"糶", -"粝"=>"糲", -"粤"=>"粵", -"粪"=>"糞", -"粮"=>"糧", -"糁"=>"糝", -"糇"=>"餱", -"紧"=>"緊", -"䌷"=>"紬", -"䌹"=>"絅", -"絷"=>"縶", -"䌼"=>"綐", -"䌽"=>"綵", -"䌸"=>"縳", -"䍁"=>"繸", -"䍀"=>"繿", -"纟"=>"糹", -"纠"=>"糾", -"纡"=>"紆", -"红"=>"紅", -"纣"=>"紂", -"纥"=>"紇", -"约"=>"約", -"级"=>"級", -"纨"=>"紈", -"纩"=>"纊", -"纪"=>"紀", -"纫"=>"紉", -"纬"=>"緯", -"纭"=>"紜", -"纮"=>"紘", -"纯"=>"純", -"纰"=>"紕", -"纱"=>"紗", -"纲"=>"綱", -"纳"=>"納", -"纴"=>"紝", -"纵"=>"縱", -"纶"=>"綸", -"纷"=>"紛", -"纸"=>"紙", -"纹"=>"紋", -"纺"=>"紡", -"纻"=>"紵", -"纼"=>"紖", -"纽"=>"紐", -"纾"=>"紓", -"绀"=>"紺", -"绁"=>"紲", -"绂"=>"紱", -"练"=>"練", -"组"=>"組", -"绅"=>"紳", -"细"=>"細", -"织"=>"織", -"终"=>"終", -"绉"=>"縐", -"绊"=>"絆", -"绋"=>"紼", -"绌"=>"絀", -"绍"=>"紹", -"绎"=>"繹", -"经"=>"經", -"绐"=>"紿", -"绑"=>"綁", -"绒"=>"絨", -"结"=>"結", -"绔"=>"絝", -"绕"=>"繞", -"绖"=>"絰", -"绗"=>"絎", -"绘"=>"繪", -"给"=>"給", -"绚"=>"絢", -"绛"=>"絳", -"络"=>"絡", -"绞"=>"絞", -"统"=>"統", -"绠"=>"綆", -"绡"=>"綃", -"绢"=>"絹", -"绤"=>"綌", -"绥"=>"綏", -"继"=>"繼", -"绨"=>"綈", -"绩"=>"績", -"绪"=>"緒", -"绫"=>"綾", -"绬"=>"緓", -"续"=>"續", -"绮"=>"綺", -"绯"=>"緋", -"绰"=>"綽", -"绲"=>"緄", -"绳"=>"繩", -"维"=>"維", -"绵"=>"綿", -"绶"=>"綬", -"绸"=>"綢", -"绹"=>"綯", -"绺"=>"綹", -"绻"=>"綣", -"综"=>"綜", -"绽"=>"綻", -"绾"=>"綰", -"缀"=>"綴", -"缁"=>"緇", -"缂"=>"緙", -"缃"=>"緗", -"缄"=>"緘", -"缅"=>"緬", -"缆"=>"纜", -"缇"=>"緹", -"缈"=>"緲", -"缉"=>"緝", -"缊"=>"縕", -"缋"=>"繢", -"缌"=>"緦", -"缍"=>"綞", -"缎"=>"緞", -"缏"=>"緶", -"缑"=>"緱", -"缒"=>"縋", -"缓"=>"緩", -"缔"=>"締", -"缕"=>"縷", -"编"=>"編", -"缗"=>"緡", -"缘"=>"緣", -"缙"=>"縉", -"缚"=>"縛", -"缛"=>"縟", -"缜"=>"縝", -"缝"=>"縫", -"缞"=>"縗", -"缟"=>"縞", -"缠"=>"纏", -"缡"=>"縭", -"缢"=>"縊", -"缣"=>"縑", -"缤"=>"繽", -"缥"=>"縹", -"缦"=>"縵", -"缧"=>"縲", -"缨"=>"纓", -"缩"=>"縮", -"缪"=>"繆", -"缫"=>"繅", -"缬"=>"纈", -"缭"=>"繚", -"缮"=>"繕", -"缯"=>"繒", -"缱"=>"繾", -"缲"=>"繰", -"缳"=>"繯", -"缴"=>"繳", -"缵"=>"纘", -"罂"=>"罌", -"网"=>"網", -"罗"=>"羅", -"罚"=>"罰", -"罢"=>"罷", -"罴"=>"羆", -"羁"=>"羈", -"羟"=>"羥", -"翘"=>"翹", -"耢"=>"耮", -"耧"=>"耬", -"耸"=>"聳", -"耻"=>"恥", -"聂"=>"聶", -"聋"=>"聾", -"职"=>"職", -"聍"=>"聹", -"联"=>"聯", -"聩"=>"聵", -"聪"=>"聰", -"肃"=>"肅", -"肠"=>"腸", -"肤"=>"膚", -"肮"=>"骯", -"肴"=>"餚", -"肾"=>"腎", -"肿"=>"腫", -"胀"=>"脹", -"胁"=>"脅", -"胆"=>"膽", -"胧"=>"朧", -"胨"=>"腖", -"胪"=>"臚", -"胫"=>"脛", -"胶"=>"膠", -"脉"=>"脈", -"脍"=>"膾", -"脐"=>"臍", -"脑"=>"腦", -"脓"=>"膿", -"脔"=>"臠", -"脚"=>"腳", -"脱"=>"脫", -"脶"=>"腡", -"脸"=>"臉", -"腭"=>"齶", -"腻"=>"膩", -"腼"=>"靦", -"腽"=>"膃", -"腾"=>"騰", -"膑"=>"臏", -"臜"=>"臢", -"舆"=>"輿", -"舣"=>"艤", -"舰"=>"艦", -"舱"=>"艙", -"舻"=>"艫", -"艰"=>"艱", -"艳"=>"艷", -"艺"=>"藝", -"节"=>"節", -"芈"=>"羋", -"芗"=>"薌", -"芜"=>"蕪", -"芦"=>"蘆", -"苁"=>"蓯", -"苇"=>"葦", -"苈"=>"藶", -"苋"=>"莧", -"苌"=>"萇", -"苍"=>"蒼", -"苎"=>"苧", -"茎"=>"莖", -"茏"=>"蘢", -"茑"=>"蔦", -"茔"=>"塋", -"茕"=>"煢", -"茧"=>"繭", -"荆"=>"荊", -"荐"=>"薦", -"荙"=>"薘", -"荚"=>"莢", -"荛"=>"蕘", -"荜"=>"蓽", -"荞"=>"蕎", -"荟"=>"薈", -"荠"=>"薺", -"荣"=>"榮", -"荤"=>"葷", -"荥"=>"滎", -"荦"=>"犖", -"荧"=>"熒", -"荨"=>"蕁", -"荩"=>"藎", -"荪"=>"蓀", -"荫"=>"蔭", -"荬"=>"蕒", -"荭"=>"葒", -"荮"=>"葤", -"莅"=>"蒞", -"莱"=>"萊", -"莲"=>"蓮", -"莳"=>"蒔", -"莴"=>"萵", -"莶"=>"薟", -"莸"=>"蕕", -"莹"=>"瑩", -"莺"=>"鶯", -"萝"=>"蘿", -"萤"=>"螢", -"营"=>"營", -"萦"=>"縈", -"萧"=>"蕭", -"萨"=>"薩", -"葱"=>"蔥", -"蒇"=>"蕆", -"蒉"=>"蕢", -"蒋"=>"蔣", -"蒌"=>"蔞", -"蓝"=>"藍", -"蓟"=>"薊", -"蓠"=>"蘺", -"蓣"=>"蕷", -"蓥"=>"鎣", -"蓦"=>"驀", -"蔂"=>"虆", -"蔷"=>"薔", -"蔹"=>"蘞", -"蔺"=>"藺", -"蔼"=>"藹", -"蕰"=>"薀", -"蕲"=>"蘄", -"薮"=>"藪", -"䓕"=>"薳", -"藓"=>"蘚", -"蘖"=>"櫱", -"虏"=>"虜", -"虑"=>"慮", -"虚"=>"虛", -"虬"=>"虯", -"虮"=>"蟣", -"虽"=>"雖", -"虾"=>"蝦", -"虿"=>"蠆", -"蚀"=>"蝕", -"蚁"=>"蟻", -"蚂"=>"螞", -"蚕"=>"蠶", -"蚬"=>"蜆", -"蛊"=>"蠱", -"蛎"=>"蠣", -"蛏"=>"蟶", -"蛮"=>"蠻", -"蛰"=>"蟄", -"蛱"=>"蛺", -"蛲"=>"蟯", -"蛳"=>"螄", -"蛴"=>"蠐", -"蜕"=>"蛻", -"蜗"=>"蝸", -"蝇"=>"蠅", -"蝈"=>"蟈", -"蝉"=>"蟬", -"蝼"=>"螻", -"蝾"=>"蠑", -"螀"=>"螿", -"螨"=>"蟎", -"䗖"=>"螮", -"蟏"=>"蠨", -"衅"=>"釁", -"衔"=>"銜", -"补"=>"補", -"衬"=>"襯", -"衮"=>"袞", -"袄"=>"襖", -"袅"=>"裊", -"袆"=>"褘", -"袜"=>"襪", -"袭"=>"襲", -"袯"=>"襏", -"装"=>"裝", -"裆"=>"襠", -"裈"=>"褌", -"裢"=>"褳", -"裣"=>"襝", -"裤"=>"褲", -"裥"=>"襇", -"褛"=>"褸", -"褴"=>"襤", -"䙓"=>"襬", -"见"=>"見", -"观"=>"觀", -"觃"=>"覎", -"规"=>"規", -"觅"=>"覓", -"视"=>"視", -"觇"=>"覘", -"览"=>"覽", -"觉"=>"覺", -"觊"=>"覬", -"觋"=>"覡", -"觌"=>"覿", -"觍"=>"覥", -"觎"=>"覦", -"觏"=>"覯", -"觐"=>"覲", -"觑"=>"覷", -"觞"=>"觴", -"触"=>"觸", -"觯"=>"觶", -"訚"=>"誾", -"䜣"=>"訢", -"誉"=>"譽", -"誊"=>"謄", -"䜧"=>"譅", -"讠"=>"訁", -"计"=>"計", -"订"=>"訂", -"讣"=>"訃", -"认"=>"認", -"讥"=>"譏", -"讦"=>"訐", -"讧"=>"訌", -"讨"=>"討", -"让"=>"讓", -"讪"=>"訕", -"讫"=>"訖", -"讬"=>"託", -"训"=>"訓", -"议"=>"議", -"讯"=>"訊", -"记"=>"記", -"讱"=>"訒", -"讲"=>"講", -"讳"=>"諱", -"讴"=>"謳", -"讵"=>"詎", -"讶"=>"訝", -"讷"=>"訥", -"许"=>"許", -"讹"=>"訛", -"论"=>"論", -"讻"=>"訩", -"讼"=>"訟", -"讽"=>"諷", -"设"=>"設", -"访"=>"訪", -"诀"=>"訣", -"证"=>"證", -"诂"=>"詁", -"诃"=>"訶", -"评"=>"評", -"诅"=>"詛", -"识"=>"識", -"诇"=>"詗", -"诈"=>"詐", -"诉"=>"訴", -"诊"=>"診", -"诋"=>"詆", -"诌"=>"謅", -"词"=>"詞", -"诎"=>"詘", -"诏"=>"詔", -"诐"=>"詖", -"译"=>"譯", -"诒"=>"詒", -"诓"=>"誆", -"诔"=>"誄", -"试"=>"試", -"诖"=>"詿", -"诗"=>"詩", -"诘"=>"詰", -"诙"=>"詼", -"诚"=>"誠", -"诛"=>"誅", -"诜"=>"詵", -"话"=>"話", -"诞"=>"誕", -"诟"=>"詬", -"诠"=>"詮", -"诡"=>"詭", -"询"=>"詢", -"诣"=>"詣", -"诤"=>"諍", -"该"=>"該", -"详"=>"詳", -"诧"=>"詫", -"诨"=>"諢", -"诩"=>"詡", -"诪"=>"譸", -"诫"=>"誡", -"诬"=>"誣", -"语"=>"語", -"诮"=>"誚", -"误"=>"誤", -"诰"=>"誥", -"诱"=>"誘", -"诲"=>"誨", -"诳"=>"誑", -"诵"=>"誦", -"诶"=>"誒", -"请"=>"請", -"诸"=>"諸", -"诹"=>"諏", -"诺"=>"諾", -"读"=>"讀", -"诼"=>"諑", -"诽"=>"誹", -"课"=>"課", -"诿"=>"諉", -"谀"=>"諛", -"谁"=>"誰", -"谂"=>"諗", -"调"=>"調", -"谄"=>"諂", -"谅"=>"諒", -"谆"=>"諄", -"谇"=>"誶", -"谈"=>"談", -"谊"=>"誼", -"谋"=>"謀", -"谌"=>"諶", -"谍"=>"諜", -"谎"=>"謊", -"谏"=>"諫", -"谐"=>"諧", -"谑"=>"謔", -"谒"=>"謁", -"谓"=>"謂", -"谔"=>"諤", -"谕"=>"諭", -"谖"=>"諼", -"谗"=>"讒", -"谘"=>"諮", -"谙"=>"諳", -"谚"=>"諺", -"谛"=>"諦", -"谜"=>"謎", -"谝"=>"諞", -"谞"=>"諝", -"谟"=>"謨", -"谠"=>"讜", -"谡"=>"謖", -"谢"=>"謝", -"谤"=>"謗", -"谥"=>"謚", -"谦"=>"謙", -"谧"=>"謐", -"谨"=>"謹", -"谩"=>"謾", -"谪"=>"謫", -"谬"=>"謬", -"谭"=>"譚", -"谮"=>"譖", -"谯"=>"譙", -"谰"=>"讕", -"谱"=>"譜", -"谲"=>"譎", -"谳"=>"讞", -"谴"=>"譴", -"谵"=>"譫", -"谶"=>"讖", -"豮"=>"豶", -"䝙"=>"貙", -"䞐"=>"賰", -"贝"=>"貝", -"贞"=>"貞", -"负"=>"負", -"贠"=>"貟", -"贡"=>"貢", -"财"=>"財", -"责"=>"責", -"贤"=>"賢", -"败"=>"敗", -"账"=>"賬", -"货"=>"貨", -"质"=>"質", -"贩"=>"販", -"贪"=>"貪", -"贫"=>"貧", -"贬"=>"貶", -"购"=>"購", -"贮"=>"貯", -"贯"=>"貫", -"贰"=>"貳", -"贱"=>"賤", -"贲"=>"賁", -"贳"=>"貰", -"贴"=>"貼", -"贵"=>"貴", -"贶"=>"貺", -"贷"=>"貸", -"贸"=>"貿", -"费"=>"費", -"贺"=>"賀", -"贻"=>"貽", -"贼"=>"賊", -"贽"=>"贄", -"贾"=>"賈", -"贿"=>"賄", -"赀"=>"貲", -"赁"=>"賃", -"赂"=>"賂", -"资"=>"資", -"赅"=>"賅", -"赆"=>"贐", -"赇"=>"賕", -"赈"=>"賑", -"赉"=>"賚", -"赊"=>"賒", -"赋"=>"賦", -"赌"=>"賭", -"赎"=>"贖", -"赏"=>"賞", -"赐"=>"賜", -"赑"=>"贔", -"赒"=>"賙", -"赓"=>"賡", -"赔"=>"賠", -"赕"=>"賧", -"赖"=>"賴", -"赗"=>"賵", -"赘"=>"贅", -"赙"=>"賻", -"赚"=>"賺", -"赛"=>"賽", -"赜"=>"賾", -"赟"=>"贇", -"赠"=>"贈", -"赡"=>"贍", -"赢"=>"贏", -"赣"=>"贛", -"赪"=>"赬", -"赵"=>"趙", -"赶"=>"趕", -"趋"=>"趨", -"趱"=>"趲", -"趸"=>"躉", -"跃"=>"躍", -"跄"=>"蹌", -"跞"=>"躒", -"践"=>"踐", -"跶"=>"躂", -"跷"=>"蹺", -"跸"=>"蹕", -"跹"=>"躚", -"跻"=>"躋", -"踊"=>"踴", -"踌"=>"躊", -"踪"=>"蹤", -"踬"=>"躓", -"踯"=>"躑", -"蹑"=>"躡", -"蹒"=>"蹣", -"蹰"=>"躕", -"蹿"=>"躥", -"躏"=>"躪", -"躜"=>"躦", -"躯"=>"軀", -"车"=>"車", -"轧"=>"軋", -"轨"=>"軌", -"轩"=>"軒", -"轪"=>"軑", -"轫"=>"軔", -"转"=>"轉", -"轭"=>"軛", -"轮"=>"輪", -"软"=>"軟", -"轰"=>"轟", -"轱"=>"軲", -"轲"=>"軻", -"轳"=>"轤", -"轴"=>"軸", -"轵"=>"軹", -"轶"=>"軼", -"轷"=>"軤", -"轸"=>"軫", -"轹"=>"轢", -"轺"=>"軺", -"轻"=>"輕", -"轼"=>"軾", -"载"=>"載", -"轾"=>"輊", -"轿"=>"轎", -"辀"=>"輈", -"辁"=>"輇", -"辂"=>"輅", -"较"=>"較", -"辄"=>"輒", -"辅"=>"輔", -"辆"=>"輛", -"辇"=>"輦", -"辈"=>"輩", -"辉"=>"輝", -"辊"=>"輥", -"辋"=>"輞", -"辌"=>"輬", -"辍"=>"輟", -"辎"=>"輜", -"辏"=>"輳", -"辐"=>"輻", -"辑"=>"輯", -"辒"=>"轀", -"输"=>"輸", -"辔"=>"轡", -"辕"=>"轅", -"辖"=>"轄", -"辗"=>"輾", -"辘"=>"轆", -"辙"=>"轍", -"辚"=>"轔", -"辞"=>"辭", -"辩"=>"辯", -"辫"=>"辮", -"边"=>"邊", -"辽"=>"遼", -"达"=>"達", -"迁"=>"遷", -"过"=>"過", -"迈"=>"邁", -"运"=>"運", -"还"=>"還", -"这"=>"這", -"进"=>"進", -"远"=>"遠", -"违"=>"違", -"连"=>"連", -"迟"=>"遲", -"迩"=>"邇", -"迳"=>"逕", -"迹"=>"跡", -"选"=>"選", -"逊"=>"遜", -"递"=>"遞", -"逦"=>"邐", -"逻"=>"邏", -"遗"=>"遺", -"遥"=>"遙", -"邓"=>"鄧", -"邝"=>"鄺", -"邬"=>"鄔", -"邮"=>"郵", -"邹"=>"鄒", -"邺"=>"鄴", -"邻"=>"鄰", -"郏"=>"郟", -"郐"=>"鄶", -"郑"=>"鄭", -"郓"=>"鄆", -"郦"=>"酈", -"郧"=>"鄖", -"郸"=>"鄲", -"酂"=>"酇", -"酦"=>"醱", -"酱"=>"醬", -"酽"=>"釅", -"酾"=>"釃", -"酿"=>"釀", -"释"=>"釋", -"鉴"=>"鑒", -"銮"=>"鑾", -"錾"=>"鏨", -"𨱏"=>"鎝", -"钅"=>"釒", -"钆"=>"釓", -"钇"=>"釔", -"针"=>"針", -"钉"=>"釘", -"钊"=>"釗", -"钋"=>"釙", -"钌"=>"釕", -"钍"=>"釷", -"钎"=>"釺", -"钏"=>"釧", -"钐"=>"釤", -"钑"=>"鈒", -"钒"=>"釩", -"钓"=>"釣", -"钔"=>"鍆", -"钕"=>"釹", -"钖"=>"鍚", -"钗"=>"釵", -"钘"=>"鈃", -"钙"=>"鈣", -"钚"=>"鈈", -"钛"=>"鈦", -"钜"=>"鉅", -"钝"=>"鈍", -"钞"=>"鈔", -"钠"=>"鈉", -"钡"=>"鋇", -"钢"=>"鋼", -"钣"=>"鈑", -"钤"=>"鈐", -"钥"=>"鑰", -"钦"=>"欽", -"钧"=>"鈞", -"钨"=>"鎢", -"钪"=>"鈧", -"钫"=>"鈁", -"钬"=>"鈥", -"钭"=>"鈄", -"钮"=>"鈕", -"钯"=>"鈀", -"钰"=>"鈺", -"钱"=>"錢", -"钲"=>"鉦", -"钳"=>"鉗", -"钴"=>"鈷", -"钶"=>"鈳", -"钷"=>"鉕", -"钸"=>"鈽", -"钹"=>"鈸", -"钺"=>"鉞", -"钻"=>"鑽", -"钼"=>"鉬", -"钽"=>"鉭", -"钾"=>"鉀", -"钿"=>"鈿", -"铀"=>"鈾", -"铁"=>"鐵", -"铂"=>"鉑", -"铃"=>"鈴", -"铄"=>"鑠", -"铅"=>"鉛", -"铆"=>"鉚", -"铇"=>"鉋", -"铈"=>"鈰", -"铉"=>"鉉", -"铊"=>"鉈", -"铋"=>"鉍", -"铌"=>"鈮", -"铍"=>"鈹", -"铎"=>"鐸", -"铏"=>"鉶", -"铐"=>"銬", -"铑"=>"銠", -"铒"=>"鉺", -"铓"=>"鋩", -"铔"=>"錏", -"铕"=>"銪", -"铖"=>"鋮", -"铗"=>"鋏", -"铘"=>"鋣", -"铙"=>"鐃", -"铚"=>"銍", -"铛"=>"鐺", -"铜"=>"銅", -"铝"=>"鋁", -"铞"=>"銱", -"铟"=>"銦", -"铠"=>"鎧", -"铡"=>"鍘", -"铢"=>"銖", -"铣"=>"銑", -"铤"=>"鋌", -"铥"=>"銩", -"铦"=>"銛", -"铧"=>"鏵", -"铨"=>"銓", -"铩"=>"鎩", -"铪"=>"鉿", -"铫"=>"銚", -"铬"=>"鉻", -"铭"=>"銘", -"铮"=>"錚", -"铯"=>"銫", -"铰"=>"鉸", -"铱"=>"銥", -"铲"=>"鏟", -"铳"=>"銃", -"铴"=>"鐋", -"铵"=>"銨", -"银"=>"銀", -"铷"=>"銣", -"铸"=>"鑄", -"铹"=>"鐒", -"铺"=>"鋪", -"铻"=>"鋙", -"铼"=>"錸", -"铽"=>"鋱", -"链"=>"鏈", -"铿"=>"鏗", -"销"=>"銷", -"锁"=>"鎖", -"锂"=>"鋰", -"锃"=>"鋥", -"锄"=>"鋤", -"锅"=>"鍋", -"锆"=>"鋯", -"锇"=>"鋨", -"锉"=>"銼", -"锊"=>"鋝", -"锋"=>"鋒", -"锌"=>"鋅", -"锍"=>"鋶", -"锎"=>"鐦", -"锏"=>"鐧", -"锑"=>"銻", -"锒"=>"鋃", -"锓"=>"鋟", -"锔"=>"鋦", -"锕"=>"錒", -"锖"=>"錆", -"锗"=>"鍺", -"锘"=>"鍩", -"错"=>"錯", -"锚"=>"錨", -"锛"=>"錛", -"锜"=>"錡", -"锝"=>"鍀", -"锞"=>"錁", -"锟"=>"錕", -"锠"=>"錩", -"锡"=>"錫", -"锢"=>"錮", -"锣"=>"鑼", -"锥"=>"錐", -"锦"=>"錦", -"锧"=>"鑕", -"锩"=>"錈", -"锪"=>"鍃", -"锫"=>"錇", -"锬"=>"錟", -"锭"=>"錠", -"键"=>"鍵", -"锯"=>"鋸", -"锰"=>"錳", -"锱"=>"錙", -"锲"=>"鍥", -"锳"=>"鍈", -"锴"=>"鍇", -"锵"=>"鏘", -"锶"=>"鍶", -"锷"=>"鍔", -"锸"=>"鍤", -"锹"=>"鍬", -"锺"=>"鍾", -"锻"=>"鍛", -"锼"=>"鎪", -"锽"=>"鍠", -"锾"=>"鍰", -"锿"=>"鎄", -"镀"=>"鍍", -"镁"=>"鎂", -"镂"=>"鏤", -"镃"=>"鎡", -"镄"=>"鐨", -"镅"=>"鎇", -"镆"=>"鏌", -"镇"=>"鎮", -"镈"=>"鎛", -"镉"=>"鎘", -"镊"=>"鑷", -"镋"=>"鎲", -"镍"=>"鎳", -"镎"=>"鎿", -"镏"=>"鎦", -"镐"=>"鎬", -"镑"=>"鎊", -"镒"=>"鎰", -"镓"=>"鎵", -"镔"=>"鑌", -"镕"=>"鎔", -"镖"=>"鏢", -"镗"=>"鏜", -"镘"=>"鏝", -"镙"=>"鏍", -"镚"=>"鏰", -"镛"=>"鏞", -"镜"=>"鏡", -"镝"=>"鏑", -"镞"=>"鏃", -"镟"=>"鏇", -"镠"=>"鏐", -"镡"=>"鐔", -"镣"=>"鐐", -"镤"=>"鏷", -"镥"=>"鑥", -"镦"=>"鐓", -"镧"=>"鑭", -"镨"=>"鐠", -"镩"=>"鑹", -"镪"=>"鏹", -"镫"=>"鐙", -"镬"=>"鑊", -"镭"=>"鐳", -"镮"=>"鐶", -"镯"=>"鐲", -"镰"=>"鐮", -"镱"=>"鐿", -"镲"=>"鑔", -"镳"=>"鑣", -"镴"=>"鑞", -"镵"=>"鑱", -"镶"=>"鑲", -"长"=>"長", -"门"=>"門", -"闩"=>"閂", -"闪"=>"閃", -"闫"=>"閆", -"闬"=>"閈", -"闭"=>"閉", -"问"=>"問", -"闯"=>"闖", -"闰"=>"閏", -"闱"=>"闈", -"闲"=>"閑", -"闳"=>"閎", -"间"=>"間", -"闵"=>"閔", -"闶"=>"閌", -"闷"=>"悶", -"闸"=>"閘", -"闹"=>"鬧", -"闺"=>"閨", -"闻"=>"聞", -"闼"=>"闥", -"闽"=>"閩", -"闾"=>"閭", -"闿"=>"闓", -"阀"=>"閥", -"阁"=>"閣", -"阂"=>"閡", -"阃"=>"閫", -"阄"=>"鬮", -"阆"=>"閬", -"阇"=>"闍", -"阈"=>"閾", -"阉"=>"閹", -"阊"=>"閶", -"阋"=>"鬩", -"阌"=>"閿", -"阍"=>"閽", -"阎"=>"閻", -"阏"=>"閼", -"阐"=>"闡", -"阑"=>"闌", -"阒"=>"闃", -"阓"=>"闠", -"阔"=>"闊", -"阕"=>"闋", -"阖"=>"闔", -"阗"=>"闐", -"阘"=>"闒", -"阙"=>"闕", -"阚"=>"闞", -"阛"=>"闤", -"队"=>"隊", -"阳"=>"陽", -"阴"=>"陰", -"阵"=>"陣", -"阶"=>"階", -"际"=>"際", -"陆"=>"陸", -"陇"=>"隴", -"陈"=>"陳", -"陉"=>"陘", -"陕"=>"陝", -"陧"=>"隉", -"陨"=>"隕", -"险"=>"險", -"随"=>"隨", -"隐"=>"隱", -"隶"=>"隸", -"隽"=>"雋", -"难"=>"難", -"雏"=>"雛", -"雠"=>"讎", -"雳"=>"靂", -"雾"=>"霧", -"霁"=>"霽", -"霡"=>"霢", -"霭"=>"靄", -"靓"=>"靚", -"静"=>"靜", -"靥"=>"靨", -"䩄"=>"靦", -"鞑"=>"韃", -"鞒"=>"鞽", -"鞯"=>"韉", -"韦"=>"韋", -"韧"=>"韌", -"韨"=>"韍", -"韩"=>"韓", -"韪"=>"韙", -"韫"=>"韞", -"韬"=>"韜", -"韵"=>"韻", -"页"=>"頁", -"顶"=>"頂", -"顷"=>"頃", -"顸"=>"頇", -"项"=>"項", -"顺"=>"順", -"顼"=>"頊", -"顽"=>"頑", -"顾"=>"顧", -"顿"=>"頓", -"颀"=>"頎", -"颁"=>"頒", -"颂"=>"頌", -"颃"=>"頏", -"预"=>"預", -"颅"=>"顱", -"领"=>"領", -"颇"=>"頗", -"颈"=>"頸", -"颉"=>"頡", -"颊"=>"頰", -"颋"=>"頲", -"颌"=>"頜", -"颍"=>"潁", -"颎"=>"熲", -"颏"=>"頦", -"颐"=>"頤", -"频"=>"頻", -"颒"=>"頮", -"颔"=>"頷", -"颕"=>"頴", -"颖"=>"穎", -"颗"=>"顆", -"题"=>"題", -"颙"=>"顒", -"颚"=>"顎", -"颛"=>"顓", -"额"=>"額", -"颞"=>"顳", -"颟"=>"顢", -"颠"=>"顛", -"颡"=>"顙", -"颢"=>"顥", -"颤"=>"顫", -"颥"=>"顬", -"颦"=>"顰", -"颧"=>"顴", -"风"=>"風", -"飏"=>"颺", -"飐"=>"颭", -"飑"=>"颮", -"飒"=>"颯", -"飓"=>"颶", -"飔"=>"颸", -"飕"=>"颼", -"飖"=>"颻", -"飗"=>"飀", -"飘"=>"飄", -"飙"=>"飆", -"飚"=>"飈", -"飞"=>"飛", -"飨"=>"饗", -"餍"=>"饜", -"饣"=>"飠", -"饤"=>"飣", -"饦"=>"飥", -"饧"=>"餳", -"饨"=>"飩", -"饩"=>"餼", -"饪"=>"飪", -"饫"=>"飫", -"饬"=>"飭", -"饭"=>"飯", -"饮"=>"飲", -"饯"=>"餞", -"饰"=>"飾", -"饱"=>"飽", -"饲"=>"飼", -"饳"=>"飿", -"饴"=>"飴", -"饵"=>"餌", -"饶"=>"饒", -"饷"=>"餉", -"饸"=>"餄", -"饹"=>"餎", -"饺"=>"餃", -"饻"=>"餏", -"饼"=>"餅", -"饽"=>"餑", -"饾"=>"餖", -"饿"=>"餓", -"馀"=>"餘", -"馁"=>"餒", -"馂"=>"餕", -"馃"=>"餜", -"馄"=>"餛", -"馅"=>"餡", -"馆"=>"館", -"馇"=>"餷", -"馈"=>"饋", -"馉"=>"餶", -"馊"=>"餿", -"馋"=>"饞", -"馌"=>"饁", -"馍"=>"饃", -"馎"=>"餺", -"馏"=>"餾", -"馐"=>"饈", -"馑"=>"饉", -"馒"=>"饅", -"馓"=>"饊", -"馔"=>"饌", -"馕"=>"饢", -"䯄"=>"騧", -"马"=>"馬", -"驭"=>"馭", -"驮"=>"馱", -"驯"=>"馴", -"驰"=>"馳", -"驱"=>"驅", -"驲"=>"馹", -"驳"=>"駁", -"驴"=>"驢", -"驵"=>"駔", -"驶"=>"駛", -"驷"=>"駟", -"驸"=>"駙", -"驹"=>"駒", -"驺"=>"騶", -"驻"=>"駐", -"驼"=>"駝", -"驽"=>"駑", -"驾"=>"駕", -"驿"=>"驛", -"骀"=>"駘", -"骁"=>"驍", -"骃"=>"駰", -"骄"=>"驕", -"骅"=>"驊", -"骆"=>"駱", -"骇"=>"駭", -"骈"=>"駢", -"骉"=>"驫", -"骊"=>"驪", -"骋"=>"騁", -"验"=>"驗", -"骍"=>"騂", -"骎"=>"駸", -"骏"=>"駿", -"骐"=>"騏", -"骑"=>"騎", -"骒"=>"騍", -"骓"=>"騅", -"骔"=>"騌", -"骕"=>"驌", -"骖"=>"驂", -"骗"=>"騙", -"骘"=>"騭", -"骙"=>"騤", -"骚"=>"騷", -"骛"=>"騖", -"骜"=>"驁", -"骝"=>"騮", -"骞"=>"騫", -"骟"=>"騸", -"骠"=>"驃", -"骡"=>"騾", -"骢"=>"驄", -"骣"=>"驏", -"骤"=>"驟", -"骥"=>"驥", -"骦"=>"驦", -"骧"=>"驤", -"髅"=>"髏", -"髋"=>"髖", -"髌"=>"髕", -"鬓"=>"鬢", -"魇"=>"魘", -"魉"=>"魎", -"鱼"=>"魚", -"鱽"=>"魛", -"鱾"=>"魢", -"鱿"=>"魷", -"鲀"=>"魨", -"鲁"=>"魯", -"鲂"=>"魴", -"鲃"=>"䰾", -"鲄"=>"魺", -"鲅"=>"鮁", -"鲆"=>"鮃", -"鲈"=>"鱸", -"鲉"=>"鮋", -"鲊"=>"鮓", -"鲋"=>"鮒", -"鲌"=>"鮊", -"鲍"=>"鮑", -"鲎"=>"鱟", -"鲏"=>"鮍", -"鲐"=>"鮐", -"鲑"=>"鮭", -"鲒"=>"鮚", -"鲓"=>"鮳", -"鲔"=>"鮪", -"鲕"=>"鮞", -"鲖"=>"鮦", -"鲗"=>"鰂", -"鲘"=>"鮜", -"鲙"=>"鱠", -"鲚"=>"鱭", -"鲛"=>"鮫", -"鲜"=>"鮮", -"鲝"=>"鮺", -"鲟"=>"鱘", -"鲠"=>"鯁", -"鲡"=>"鱺", -"鲢"=>"鰱", -"鲣"=>"鰹", -"鲤"=>"鯉", -"鲥"=>"鰣", -"鲦"=>"鰷", -"鲧"=>"鯀", -"鲨"=>"鯊", -"鲩"=>"鯇", -"鲪"=>"鮶", -"鲫"=>"鯽", -"鲬"=>"鯒", -"鲭"=>"鯖", -"鲮"=>"鯪", -"鲯"=>"鯕", -"鲰"=>"鯫", -"鲱"=>"鯡", -"鲲"=>"鯤", -"鲳"=>"鯧", -"鲴"=>"鯝", -"鲵"=>"鯢", -"鲶"=>"鯰", -"鲷"=>"鯛", -"鲸"=>"鯨", -"鲹"=>"鰺", -"鲺"=>"鯴", -"鲻"=>"鯔", -"鲼"=>"鱝", -"鲽"=>"鰈", -"鲾"=>"鰏", -"鲿"=>"鱨", -"鳀"=>"鯷", -"鳁"=>"鰮", -"鳂"=>"鰃", -"鳃"=>"鰓", -"鳅"=>"鰍", -"鳆"=>"鰒", -"鳇"=>"鰉", -"鳈"=>"鰁", -"鳉"=>"鱂", -"鳊"=>"鯿", -"鳋"=>"鰠", -"鳌"=>"鰲", -"鳍"=>"鰭", -"鳎"=>"鰨", -"鳏"=>"鰥", -"鳐"=>"鰩", -"鳑"=>"鰟", -"鳒"=>"鰜", -"鳓"=>"鰳", -"鳔"=>"鰾", -"鳕"=>"鱈", -"鳖"=>"鱉", -"鳗"=>"鰻", -"鳘"=>"鰵", -"鳙"=>"鱅", -"鳚"=>"䲁", -"鳛"=>"鰼", -"鳜"=>"鱖", -"鳝"=>"鱔", -"鳞"=>"鱗", -"鳟"=>"鱒", -"鳠"=>"鱯", -"鳡"=>"鱤", -"鳢"=>"鱧", -"鳣"=>"鱣", -"䴓"=>"鳾", -"䴕"=>"鴷", -"䴔"=>"鵁", -"䴖"=>"鶄", -"䴗"=>"鶪", -"䴘"=>"鷈", -"䴙"=>"鷿", -"㶉"=>"鸂", -"鸟"=>"鳥", -"鸠"=>"鳩", -"鸢"=>"鳶", -"鸣"=>"鳴", -"鸤"=>"鳲", -"鸥"=>"鷗", -"鸦"=>"鴉", -"鸧"=>"鶬", -"鸨"=>"鴇", -"鸩"=>"鴆", -"鸪"=>"鴣", -"鸫"=>"鶇", -"鸬"=>"鸕", -"鸭"=>"鴨", -"鸮"=>"鴞", -"鸯"=>"鴦", -"鸰"=>"鴒", -"鸱"=>"鴟", -"鸲"=>"鴝", -"鸳"=>"鴛", -"鸴"=>"鷽", -"鸵"=>"鴕", -"鸶"=>"鷥", -"鸷"=>"鷙", -"鸸"=>"鴯", -"鸹"=>"鴰", -"鸺"=>"鵂", -"鸻"=>"鴴", -"鸼"=>"鵃", -"鸽"=>"鴿", -"鸾"=>"鸞", -"鸿"=>"鴻", -"鹀"=>"鵐", -"鹁"=>"鵓", -"鹂"=>"鸝", -"鹃"=>"鵑", -"鹄"=>"鵠", -"鹅"=>"鵝", -"鹆"=>"鵒", -"鹇"=>"鷳", -"鹈"=>"鵜", -"鹉"=>"鵡", -"鹊"=>"鵲", -"鹋"=>"鶓", -"鹌"=>"鵪", -"鹍"=>"鵾", -"鹎"=>"鵯", -"鹏"=>"鵬", -"鹐"=>"鵮", -"鹑"=>"鶉", -"鹒"=>"鶊", -"鹓"=>"鵷", -"鹔"=>"鷫", -"鹕"=>"鶘", -"鹖"=>"鶡", -"鹗"=>"鶚", -"鹘"=>"鶻", -"鹙"=>"鶖", -"鹛"=>"鶥", -"鹜"=>"鶩", -"鹝"=>"鷊", -"鹞"=>"鷂", -"鹟"=>"鶲", -"鹠"=>"鶹", -"鹡"=>"鶺", -"鹢"=>"鷁", -"鹣"=>"鶼", -"鹤"=>"鶴", -"鹥"=>"鷖", -"鹦"=>"鸚", -"鹧"=>"鷓", -"鹨"=>"鷚", -"鹩"=>"鷯", -"鹪"=>"鷦", -"鹫"=>"鷲", -"鹬"=>"鷸", -"鹭"=>"鷺", -"鹯"=>"鸇", -"鹰"=>"鷹", -"鹱"=>"鸌", -"鹲"=>"鸏", -"鹳"=>"鸛", -"鹴"=>"鸘", -"鹾"=>"鹺", -"麦"=>"麥", -"麸"=>"麩", -"黄"=>"黃", -"黉"=>"黌", -"黡"=>"黶", -"黩"=>"黷", -"黪"=>"黲", -"黾"=>"黽", -"鼋"=>"黿", -"鼍"=>"鼉", -"鼗"=>"鞀", -"鼹"=>"鼴", -"齐"=>"齊", -"齑"=>"齏", -"齿"=>"齒", -"龀"=>"齔", -"龁"=>"齕", -"龂"=>"齗", -"龃"=>"齟", -"龄"=>"齡", -"龅"=>"齙", -"龆"=>"齠", -"龇"=>"齜", -"龈"=>"齦", -"龉"=>"齬", -"龊"=>"齪", -"龋"=>"齲", -"龌"=>"齷", -"龙"=>"龍", -"龚"=>"龔", -"龛"=>"龕", -"龟"=>"龜", - -"0多只" => "0多隻", -"0天后" => "0天後", -"1天后" => "1天後", -"2天后" => "2天後", -"3天后" => "3天後", -"4天后" => "4天後", -"5天后" => "5天後", -"6天后" => "6天後", -"7天后" => "7天後", -"8天后" => "8天後", -"9天后" => "9天後", -"一干二净" => "一乾二淨", -"一并" => "一併", -"一前一后" => "一前一後", -"一划" => "一劃", -"一口钟" => "一口鐘", -"一地里" => "一地裡", -"一伙" => "一夥", -"一天后" => "一天後", -"一干人" => "一干人", -"一别头" => "一彆頭", -"一树百获" => "一樹百穫", -"一准" => "一準", -"一争两丑" => "一爭兩醜", -"一箭双雕" => "一箭雙鵰", -"一扎" => "一紮", -"一冲" => "一衝", -"一锅面" => "一鍋麵", -"一只" => "一隻", -"一发千钧" => "一髮千鈞", -"一哄而散" => "一鬨而散", -"丁丁当当" => "丁丁當當", -"七划" => "七劃", -"七天后" => "七天後", -"七情六欲" => "七情六慾", -"七扎" => "七紮", -"七只" => "七隻", -"万俟" => "万俟", -"万旗" => "万旗", -"三天后" => "三天後", -"三棱锥" => "三稜錐", -"三扎" => "三紮", -"三统历" => "三統曆", -"三复" => "三複", -"三只" => "三隻", -"三余" => "三餘", -"上梁" => "上樑", -"上签名" => "上簽名", -"上签字" => "上簽字", -"上签写" => "上簽寫", -"上签收" => "上簽收", -"上签" => "上籤", -"上药" => "上藥", -"下于" => "下於", -"下签" => "下籤", -"下药" => "下藥", -"下余" => "下餘", -"不下于" => "不下於", -"不亚于" => "不亞於", -"不占" => "不佔", -"不前不后" => "不前不後", -"不可救药" => "不可救藥", -"不同于" => "不同於", -"不嫌母丑" => "不嫌母醜", -"不寒而栗" => "不寒而慄", -"不屑于" => "不屑於", -"不干不净" => "不幹不淨", -"不干性油" => "不幹性油", -"不采" => "不採", -"不断发" => "不斷發", -"不准" => "不準", -"不为牛后" => "不為牛後", -"不知就里" => "不知就裡", -"不知所云" => "不知所云", -"不谷" => "不穀", -"不绝于耳" => "不絕於耳", -"不致于" => "不致於", -"不良于行" => "不良於行", -"不药而癒" => "不藥而癒", -"不讬" => "不託", -"不逊于" => "不遜於", -"不丑" => "不醜", -"不锈钢" => "不鏽鋼", -"世界杯" => "世界盃", -"丢丑" => "丟醜", -"并存着" => "並存著", -"并于" => "並於", -"并发动" => "並發動", -"并发展" => "並發展", -"并发现" => "並發現", -"并发表" => "並發表", -"中仑" => "中崙", -"中岳" => "中嶽", -"中于" => "中於", -"中美发表" => "中美發表", -"中药" => "中藥", -"丰仪" => "丰儀", -"丰南" => "丰南", -"丰台" => "丰台", -"丰姿" => "丰姿", -"丰度" => "丰度", -"丰情" => "丰情", -"丰标不凡" => "丰標不凡", -"丰神" => "丰神", -"丰茸" => "丰茸", -"丰采" => "丰采", -"丰韵" => "丰韵", -"丰韵" => "丰韻", -"丸药" => "丸藥", -"丹药" => "丹藥", -"主仆" => "主僕", -"主干" => "主幹", -"么么小丑" => "么麼小丑", -"之后" => "之後", -"之于" => "之於", -"之余" => "之餘", -"九世之雠" => "九世之讎", -"九划" => "九劃", -"九天后" => "九天後", -"九谷" => "九穀", -"九扎" => "九紮", -"九只" => "九隻", -"乳臭未干" => "乳臭未乾", -"干上" => "乾上", -"干干" => "乾乾", -"干干儿的" => "乾乾兒的", -"干干净净" => "乾乾淨淨", -"干了" => "乾了", -"干井" => "乾井", -"干个" => "乾個", -"干儿" => "乾兒", -"干儿子" => "乾兒子", -"干冰" => "乾冰", -"干冷" => "乾冷", -"干刻版" => "乾刻版", -"干剥剥" => "乾剝剝", -"干卦" => "乾卦", -"干吊着下巴" => "乾吊著下巴", -"干和" => "乾和", -"干咳" => "乾咳", -"干咽" => "乾咽", -"干哥" => "乾哥", -"干哭" => "乾哭", -"干唱" => "乾唱", -"干啼" => "乾啼", -"干乔" => "乾喬", -"干呕" => "乾嘔", -"干哕" => "乾噦", -"干嚎" => "乾嚎", -"干回付" => "乾回付", -"干圆洁净" => "乾圓潔淨", -"干地" => "乾地", -"干坤" => "乾坤", -"干坞" => "乾塢", -"干女" => "乾女", -"干女儿" => "乾女兒", -"干奴才" => "乾奴才", -"干妹" => "乾妹", -"干姊" => "乾姊", -"干娘" => "乾娘", -"干妈" => "乾媽", -"干子" => "乾子", -"干季" => "乾季", -"干尸" => "乾屍", -"干屎橛" => "乾屎橛", -"干巴" => "乾巴", -"干巴巴" => "乾巴巴", -"干式" => "乾式", -"干弟" => "乾弟", -"干得" => "乾得", -"干急" => "乾急", -"干性" => "乾性", -"干打雷" => "乾打雷", -"干折" => "乾折", -"干掉" => "乾掉", -"干撂台" => "乾撂台", -"干撇下" => "乾撇下", -"干擦" => "乾擦", -"干支剌" => "乾支剌", -"干支支" => "乾支支", -"干敲梆子不卖油" => "乾敲梆子不賣油", -"干料" => "乾料", -"干旱" => "乾旱", -"干暖" => "乾暖", -"干材" => "乾材", -"干村沙" => "乾村沙", -"干杯" => "乾杯", -"干果" => "乾果", -"干枯" => "乾枯", -"干柴" => "乾柴", -"干柴烈火" => "乾柴烈火", -"干梅" => "乾梅", -"干死" => "乾死", -"干池" => "乾池", -"干没" => "乾沒", -"干洗" => "乾洗", -"干涸" => "乾涸", -"干凉" => "乾涼", -"干净" => "乾淨", -"干渠" => "乾渠", -"干渴" => "乾渴", -"干沟" => "乾溝", -"干漆" => "乾漆", -"干涩" => "乾澀", -"干湿" => "乾濕", -"干熬" => "乾熬", -"干热" => "乾熱", -"干灯盏" => "乾燈盞", -"干燥" => "乾燥", -"干爸" => "乾爸", -"干爹" => "乾爹", -"干爽" => "乾爽", -"干片" => "乾片", -"干生受" => "乾生受", -"干生子" => "乾生子", -"干产" => "乾產", -"干田" => "乾田", -"干疥" => "乾疥", -"干瘦" => "乾瘦", -"干瘪" => "乾癟", -"干癣" => "乾癬", -"干白儿" => "乾白兒", -"干的" => "乾的", -"干眼" => "乾眼", -"干眼病" => "乾眼病", -"干瞪眼" => "乾瞪眼", -"干礼" => "乾禮", -"干稿" => "乾稿", -"干笑" => "乾笑", -"干等" => "乾等", -"干篾片" => "乾篾片", -"干粉" => "乾粉", -"干粮" => "乾糧", -"干结" => "乾結", -"干丝" => "乾絲", -"干绷" => "乾繃", -"干耗" => "乾耗", -"干肉片" => "乾肉片", -"干股" => "乾股", -"干肥" => "乾肥", -"干脆" => "乾脆", -"干花" => "乾花", -"干刍" => "乾芻", -"干苔" => "乾苔", -"干茨腊" => "乾茨臘", -"干茶钱" => "乾茶錢", -"干草" => "乾草", -"干菜" => "乾菜", -"干落" => "乾落", -"干着" => "乾著", -"干着急" => "乾著急", -"干姜" => "乾薑", -"干薪" => "乾薪", -"干虔" => "乾虔", -"干号" => "乾號", -"干衣" => "乾衣", -"干裂" => "乾裂", -"干亲" => "乾親", -"干贝" => "乾貝", -"干货" => "乾貨", -"干躁" => "乾躁", -"干逼" => "乾逼", -"干酪" => "乾酪", -"干酵母" => "乾酵母", -"干醋" => "乾醋", -"干量" => "乾量", -"干阿奶" => "乾阿奶", -"干隆" => "乾隆", -"干雷" => "乾雷", -"干电" => "乾電", -"干电池" => "乾電池", -"干霍乱" => "乾霍亂", -"干颡" => "乾顙", -"干台" => "乾颱", -"干饭" => "乾飯", -"干馆" => "乾館", -"干糇" => "乾餱", -"干馏" => "乾餾", -"干鱼" => "乾魚", -"干鲜" => "乾鮮", -"干面" => "乾麵", -"乱发" => "亂髮", -"乱哄" => "亂鬨", -"乱哄不过来" => "亂鬨不過來", -"事后" => "事後", -"二不棱登" => "二不稜登", -"二划" => "二劃", -"二天后" => "二天後", -"二缶钟惑" => "二缶鐘惑", -"二里头" => "二里頭", -"二只" => "二隻", -"于余曲折" => "于餘曲折", -"云乎" => "云乎", -"云云" => "云云", -"云为" => "云為", -"云然" => "云然", -"云尔" => "云爾", -"互于" => "互於", -"五划" => "五劃", -"五天后" => "五天後", -"五岳" => "五嶽", -"五谷" => "五穀", -"五扎" => "五紮", -"五行生克" => "五行生剋", -"五只" => "五隻", -"五出" => "五齣", -"井干摧败" => "井榦摧敗", -"亚于" => "亞於", -"交于" => "交於", -"交讬" => "交託", -"交游广阔" => "交遊廣闊", -"交哄" => "交鬨", -"亮丑" => "亮醜", -"亮钟" => "亮鐘", -"人云亦云" => "人云亦云", -"人参加" => "人參加", -"人参展" => "人參展", -"人参战" => "人參戰", -"人参拜" => "人參拜", -"人参政" => "人參政", -"人参照" => "人參照", -"人参看" => "人參看", -"人参禅" => "人參禪", -"人参考" => "人參考", -"人参与" => "人參與", -"人参见" => "人參見", -"人参观" => "人參觀", -"人参谋" => "人參謀", -"人参议" => "人參議", -"人参赞" => "人參贊", -"人参透" => "人參透", -"人参选" => "人參選", -"人参酌" => "人參酌", -"人参阅" => "人參閱", -"人后" => "人後", -"人欲" => "人慾", -"人物志" => "人物誌", -"人参" => "人蔘", -"什锦面" => "什錦麵", -"什么" => "什麼", -"今后" => "今後", -"介于" => "介於", -"付讬" => "付託", -"仙药" => "仙藥", -"以后" => "以後", -"任教于" => "任教於", -"任于" => "任於", -"仿制" => "仿製", -"企划" => "企劃", -"伊府面" => "伊府麵", -"伊斯兰教历" => "伊斯蘭教曆", -"伊斯兰历" => "伊斯蘭曆", -"伊郁" => "伊鬱", -"伏几" => "伏几", -"伙头" => "伙頭", -"似于" => "似於", -"但云" => "但云", -"布于" => "佈於", -"位于" => "位於", -"低于" => "低於", -"占上风" => "佔上風", -"占下" => "佔下", -"占了" => "佔了", -"占位" => "佔位", -"占住" => "佔住", -"占占" => "佔佔", -"占便宜" => "佔便宜", -"占个" => "佔個", -"占优势" => "佔優勢", -"占先" => "佔先", -"占光" => "佔光", -"占到" => "佔到", -"占去" => "佔去", -"占取" => "佔取", -"占在" => "佔在", -"占地" => "佔地", -"占多数" => "佔多數", -"占好" => "佔好", -"占得" => "佔得", -"占掉" => "佔掉", -"占据" => "佔據", -"占有" => "佔有", -"占满" => "佔滿", -"占为" => "佔為", -"占用" => "佔用", -"占尽" => "佔盡", -"占线" => "佔線", -"占起" => "佔起", -"占超过" => "佔超過", -"占过" => "佔過", -"占领" => "佔領", -"余光中" => "余光中", -"余光生" => "余光生", -"佛罗棱萨" => "佛羅稜薩", -"作奸犯科" => "作姦犯科", -"作准" => "作準", -"作庄" => "作莊", -"你才子发昏" => "你纔子發昏", -"并一不二" => "併一不二", -"并入" => "併入", -"并兼" => "併兼", -"并到" => "併到", -"并合" => "併合", -"并名" => "併名", -"并吞" => "併吞", -"并拢" => "併攏", -"并案" => "併案", -"并流" => "併流", -"并火" => "併火", -"并为" => "併為", -"并产" => "併產", -"并当" => "併當", -"并叠" => "併疊", -"并发" => "併發", -"并科" => "併科", -"并网" => "併網", -"并线" => "併線", -"并肩子" => "併肩子", -"并购" => "併購", -"并除" => "併除", -"并骨" => "併骨", -"来于" => "來於", -"来自于" => "來自於", -"来复" => "來複", -"侍仆" => "侍僕", -"供制" => "供製", -"依依不舍" => "依依不捨", -"依讬" => "依託", -"依附于" => "依附於", -"侵占" => "侵佔", -"侵并" => "侵併", -"便于" => "便於", -"便药" => "便藥", -"系数" => "係數", -"系为" => "係為", -"保险柜" => "保險柜", -"信讬" => "信託", -"修改后" => "修改後", -"修胡刀" => "修鬍刀", -"俯冲" => "俯衝", -"个里" => "個裡", -"幸免" => "倖免", -"幸存" => "倖存", -"幸幸" => "倖幸", -"倛丑" => "倛醜", -"借助于" => "借助於", -"借着" => "借著", -"借讬" => "借託", -"倦游" => "倦遊", -"假力于人" => "假力於人", -"假药" => "假藥", -"假讬" => "假託", -"假发" => "假髮", -"偎干" => "偎乾", -"偎干就湿" => "偎乾就濕", -"偏后" => "偏後", -"偏于" => "偏於", -"做庄" => "做莊", -"停停当当" => "停停當當", -"停征" => "停徵", -"停制" => "停製", -"偷鸡不着" => "偷雞不著", -"伪药" => "偽藥", -"备注" => "備註", -"家伙" => "傢伙", -"家俱" => "傢俱", -"家具" => "傢具", -"催并" => "催併", -"佣人" => "傭人", -"佣兵" => "傭兵", -"佣工" => "傭工", -"佣懒" => "傭懶", -"佣书" => "傭書", -"佣金" => "傭金", -"伤痕累累" => "傷痕纍纍", -"傻里傻气" => "傻裡傻氣", -"倾向于" => "傾向於", -"倾家荡产" => "傾家蕩產", -"倾复" => "傾複", -"仆人" => "僕人", -"仆使" => "僕使", -"仆仆" => "僕僕", -"仆仆风尘" => "僕僕風塵", -"仆僮" => "僕僮", -"仆吏" => "僕吏", -"仆固怀恩" => "僕固懷恩", -"仆夫" => "僕夫", -"仆姑" => "僕姑", -"仆妇" => "僕婦", -"仆射" => "僕射", -"仆少" => "僕少", -"仆役" => "僕役", -"仆从" => "僕從", -"仆憎" => "僕憎", -"仆欧" => "僕歐", -"仆程" => "僕程", -"仆虽罢驽" => "僕雖罷駑", -"侥幸" => "僥倖", -"僮仆" => "僮僕", -"雇主" => "僱主", -"雇人" => "僱人", -"雇佣" => "僱佣", -"雇到" => "僱到", -"雇员" => "僱員", -"雇工" => "僱工", -"雇用" => "僱用", -"雇农" => "僱農", -"仪范" => "儀範", -"仪表" => "儀錶", -"亿多只" => "億多隻", -"亿天后" => "億天後", -"亿只" => "億隻", -"俭朴" => "儉樸", -"儒略改革历" => "儒略改革曆", -"儒略历" => "儒略曆", -"尽尽" => "儘儘", -"尽先" => "儘先", -"尽其所有" => "儘其所有", -"尽力" => "儘力", -"尽快" => "儘快", -"尽早" => "儘早", -"尽是" => "儘是", -"尽管" => "儘管", -"尽速" => "儘速", -"优于" => "優於", -"优游" => "優遊", -"兀术" => "兀朮", -"元凶" => "元兇", -"充饥" => "充饑", -"凶器" => "兇器", -"凶徒" => "兇徒", -"凶手" => "兇手", -"凶案" => "兇案", -"凶残" => "兇殘", -"凶杀" => "兇殺", -"先占" => "先佔", -"先后" => "先後", -"先忧后乐" => "先憂後樂", -"先采" => "先採", -"先攻后守" => "先攻後守", -"先于" => "先於", -"先盛后衰" => "先盛後衰", -"先礼后兵" => "先禮後兵", -"先义后利" => "先義後利", -"先苦后甘" => "先苦後甘", -"先赢后输" => "先贏後輸", -"先进后出" => "先進後出", -"光采" => "光採", -"光致致" => "光緻緻", -"克药" => "克藥", -"克复" => "克複", -"免于" => "免於", -"党参" => "党參", -"党太尉" => "党太尉", -"党进" => "党進", -"党项" => "党項", -"入夜后" => "入夜後", -"入伙" => "入夥", -"内制" => "內製", -"内斗" => "內鬥", -"内哄" => "內鬨", -"全干" => "全乾", -"两天后" => "兩天後", -"两扎" => "兩紮", -"两只" => "兩隻", -"八天后" => "八天後", -"八字胡" => "八字鬍", -"八扎" => "八紮", -"八只" => "八隻", -"公仔面" => "公仔麵", -"公仆" => "公僕", -"公干" => "公幹", -"公历" => "公曆", -"公诸于世" => "公諸於世", -"公厘" => "公釐", -"公余" => "公餘", -"六划" => "六劃", -"六天后" => "六天後", -"六扎" => "六紮", -"六冲" => "六衝", -"六只" => "六隻", -"六出" => "六齣", -"其后" => "其後", -"其次辟地" => "其次辟地", -"其余" => "其餘", -"典范" => "典範", -"兼并" => "兼併", -"冉有仆" => "冉有僕", -"再于" => "再於", -"冤雠" => "冤讎", -"冥蒙" => "冥濛", -"冬山庄" => "冬山庄", -"冬游" => "冬遊", -"冶游" => "冶遊", -"冷面相" => "冷面相", -"冷面" => "冷麵", -"凌蒙初" => "凌濛初", -"凌藉" => "凌藉", -"几几" => "几几", -"几案" => "几案", -"几丝" => "几絲", -"凡于" => "凡於", -"凹洞里" => "凹洞裡", -"出乖露丑" => "出乖露醜", -"出于" => "出於", -"出自于" => "出自於", -"出谋划策" => "出謀劃策", -"出游" => "出遊", -"出丑" => "出醜", -"出锤" => "出鎚", -"刀削面" => "刀削麵", -"分布" => "分佈", -"分占" => "分佔", -"分钟" => "分鐘", -"刑余" => "刑餘", -"划着" => "划著", -"划着走" => "划著走", -"划龙舟" => "划龍舟", -"别后" => "別後", -"别日南鸿才北去" => "別日南鴻纔北去", -"别致" => "別緻", -"别着" => "別著", -"别辟" => "別闢", -"别只" => "別隻", -"利欲" => "利慾", -"利于" => "利於", -"刮着" => "刮著", -"刮风下雪倒便宜" => "刮風下雪倒便宜", -"刮胡刀" => "刮鬍刀", -"制签" => "制籤", -"刺绣" => "刺繡", -"刻划" => "刻劃", -"刻于" => "刻於", -"刻钟" => "刻鐘", -"剃发" => "剃髮", -"剃须" => "剃鬚", -"削发" => "削髮", -"削面" => "削麵", -"克制" => "剋制", -"克星" => "剋星", -"克死" => "剋死", -"克薄" => "剋薄", -"前仰后合" => "前仰後合", -"前倨后恭" => "前倨後恭", -"前呼后拥" => "前呼後擁", -"前后" => "前後", -"前思后想" => "前思後想", -"前挽后推" => "前挽後推", -"前短后长" => "前短後長", -"刚干" => "剛乾", -"刚雇" => "剛僱", -"刚才一载" => "剛纔一載", -"剩余" => "剩餘", -"剪牡丹喂牛" => "剪牡丹喂牛", -"剪䌽" => "剪綵", -"剪发" => "剪髮", -"割舍" => "割捨", -"创制" => "創製", -"划一" => "劃一", -"划上" => "劃上", -"划下" => "劃下", -"划了" => "劃了", -"划出" => "劃出", -"划分" => "劃分", -"划到" => "劃到", -"划划" => "劃劃", -"划去" => "劃去", -"划在" => "劃在", -"划地" => "劃地", -"划定" => "劃定", -"划得" => "劃得", -"划成" => "劃成", -"划掉" => "劃掉", -"划拨" => "劃撥", -"划时代" => "劃時代", -"划款" => "劃款", -"划归" => "劃歸", -"划法" => "劃法", -"划清" => "劃清", -"划界" => "劃界", -"划破" => "劃破", -"划线" => "劃線", -"划足" => "劃足", -"划开" => "劃開", -"力争上游" => "力爭上遊", -"功致" => "功緻", -"加害于" => "加害於", -"加工余量" => "加工餘量", -"加卷" => "加捲", -"加于" => "加於", -"加药" => "加藥", -"加注" => "加註", -"劣于" => "劣於", -"助于" => "助於", -"劫余" => "劫餘", -"勃郁" => "勃鬱", -"勇于" => "勇於", -"动荡" => "動蕩", -"胜于" => "勝於", -"劳力士表" => "勞力士錶", -"勤仆" => "勤僕", -"勤朴" => "勤樸", -"勾干" => "勾幹", -"勾心斗角" => "勾心鬥角", -"勿施于人" => "勿施於人", -"包谷" => "包穀", -"包扎" => "包紮", -"北岳" => "北嶽", -"北回线" => "北迴線", -"北回铁路" => "北迴鐵路", -"匡复" => "匡複", -"匪干" => "匪幹", -"匿于" => "匿於", -"区划" => "區劃", -"十划" => "十劃", -"十多只" => "十多隻", -"十天后" => "十天後", -"十卷" => "十捲", -"十扎" => "十紮", -"十只" => "十隻", -"十出" => "十齣", -"千多只" => "千多隻", -"千天后" => "千天後", -"千扎" => "千紮", -"千丝万缕" => "千絲萬縷", -"千回百折" => "千迴百折", -"千回百转" => "千迴百轉", -"千钧一发" => "千鈞一髮", -"千只" => "千隻", -"午后" => "午後", -"半于" => "半於", -"半只" => "半隻", -"南岳" => "南嶽", -"南筑" => "南筑", -"南回线" => "南迴線", -"南回铁路" => "南迴鐵路", -"南游" => "南遊", -"博汇" => "博彙", -"博采" => "博採", -"印累绶若" => "印纍綬若", -"印制" => "印製", -"危于" => "危於", -"卷发" => "卷髮", -"卷须" => "卷鬚", -"厂部" => "厂部", -"原子钟" => "原子鐘", -"原于" => "原於", -"历物之意" => "厤物之意", -"参与" => "參与", -"参与者" => "參与者", -"参合" => "參合", -"参考价值" => "參考價值", -"参与" => "參與", -"参与人员" => "參與人員", -"参与制" => "參與制", -"参与感" => "參與感", -"参与者" => "參與者", -"参观团" => "參觀團", -"参观团体" => "參觀團體", -"参阅" => "參閱", -"及于" => "及於", -"反于" => "反於", -"反朴" => "反樸", -"反冲" => "反衝", -"反复制" => "反複製", -"反复" => "反覆", -"取信于" => "取信於", -"取舍" => "取捨", -"取材于" => "取材於", -"取决于" => "取決於", -"取法于" => "取法於", -"受人之讬" => "受人之託", -"受制于人" => "受制於人", -"受讬" => "受託", -"受阻于" => "受阻於", -"口干" => "口乾", -"口燥脣干" => "口燥脣乾", -"口腹之欲" => "口腹之慾", -"口血未干" => "口血未乾", -"口里" => "口裡", -"古柯硷" => "古柯鹼", -"古朴" => "古樸", -"另于" => "另於", -"另辟" => "另闢", -"叩钟" => "叩鐘", -"只占" => "只佔", -"只采" => "只採", -"只冲" => "只衝", -"叮当" => "叮噹", -"可于" => "可於", -"可紧可松" => "可緊可鬆", -"台后" => "台後", -"台历" => "台曆", -"台制" => "台製", -"右后" => "右後", -"叶恭弘" => "叶恭弘", -"叶 恭弘" => "叶 恭弘", -"叶 恭弘" => "叶 恭弘", -"叶音" => "叶音", -"叶韵" => "叶韻", -"吃板刀面" => "吃板刀麵", -"吃着不尽" => "吃著不盡", -"吃姜" => "吃薑", -"吃药" => "吃藥", -"吃里扒外" => "吃裡扒外", -"吃里爬外" => "吃裡爬外", -"吃豆干" => "吃豆乾", -"吃辣面" => "吃辣麵", -"吃错药" => "吃錯藥", -"各辟" => "各闢", -"合并" => "合併", -"合伙" => "合夥", -"合采" => "合採", -"合于" => "合於", -"合历" => "合曆", -"合着" => "合著", -"合着者" => "合著者", -"吊带裤" => "吊帶褲", -"吊挂着" => "吊掛著", -"吊着" => "吊著", -"吊裤" => "吊褲", -"吊裤带" => "吊褲帶", -"吊钟" => "吊鐘", -"同伙" => "同夥", -"同于" => "同於", -"名闻于世" => "名聞於世", -"后发座" => "后髮座", -"向后" => "向後", -"向着" => "向著", -"吞并" => "吞併", -"吟游" => "吟遊", -"吹干" => "吹乾", -"吹发" => "吹髮", -"呆致致" => "呆緻緻", -"呆里呆气" => "呆裡呆氣", -"周历" => "周曆", -"周杰伦" => "周杰倫", -"周游" => "周遊", -"呼吁" => "呼籲", -"咬姜呷醋" => "咬薑呷醋", -"咯当" => "咯噹", -"咳嗽药" => "咳嗽藥", -"哀吊" => "哀弔", -"品汇" => "品彙", -"员山庄" => "員山庄", -"哪里" => "哪裡", -"哭脏" => "哭髒", -"唇干" => "唇乾", -"唱游" => "唱遊", -"唾面自干" => "唾面自乾", -"唾余" => "唾餘", -"商历" => "商曆", -"问政于民" => "問政於民", -"问道于盲" => "問道於盲", -"啷当" => "啷噹", -"善后" => "善後", -"善于" => "善於", -"喜形于色" => "喜形於色", -"喧哄" => "喧鬨", -"丧钟" => "喪鐘", -"单干" => "單幹", -"单打独斗" => "單打獨鬥", -"单只" => "單隻", -"嗑药" => "嗑藥", -"嗣后" => "嗣後", -"嘉谷" => "嘉穀", -"嘴里" => "嘴裡", -"恶心" => "噁心", -"噙齿戴发" => "噙齒戴髮", -"当啷" => "噹啷", -"当当" => "噹噹", -"噜苏" => "嚕囌", -"向导" => "嚮導", -"向往" => "嚮往", -"向应" => "嚮應", -"向迩" => "嚮邇", -"严于" => "嚴於", -"严丝合缝" => "嚴絲合縫", -"嚼谷" => "嚼穀", -"囉囉苏苏" => "囉囉囌囌", -"囉苏" => "囉囌", -"嘱讬" => "囑託", -"四分历" => "四分曆", -"四天后" => "四天後", -"四舍五入" => "四捨五入", -"四扎" => "四紮", -"四只" => "四隻", -"四出" => "四齣", -"回采" => "回採", -"回历" => "回曆", -"回丝" => "回絲", -"回着" => "回著", -"回荡" => "回蕩", -"回游" => "回遊", -"因于" => "因於", -"困于" => "困於", -"困兽之斗" => "困獸之鬥", -"困兽犹斗" => "困獸猶鬥", -"固于" => "固於", -"囿于" => "囿於", -"囿于一时" => "囿於一時", -"囿于成见" => "囿於成見", -"圈子里" => "圈子裡", -"圈梁" => "圈樑", -"圈里" => "圈裡", -"国之桢干" => "國之楨榦", -"国于" => "國於", -"国历" => "國曆", -"国历代" => "國歷代", -"国历史" => "國歷史", -"国雠" => "國讎", -"园里" => "園裡", -"园游会" => "園遊會", -"图里" => "圖裡", -"土里" => "土裡", -"土制" => "土製", -"在于" => "在於", -"地志" => "地誌", -"地丑德齐" => "地醜德齊", -"坏于" => "坏於", -"坐钟" => "坐鐘", -"坑里" => "坑裡", -"坤范" => "坤範", -"坦荡" => "坦蕩", -"坱郁" => "坱鬱", -"垂直于" => "垂直於", -"垂发" => "垂髮", -"型范" => "型範", -"埃及历" => "埃及曆", -"城里" => "城裡", -"基干" => "基幹", -"基于" => "基於", -"基准" => "基準", -"坚致" => "堅緻", -"涂着" => "塗著", -"涂药" => "塗藥", -"塞耳盗钟" => "塞耳盜鐘", -"塞药" => "塞藥", -"墓志" => "墓誌", -"增辟" => "增闢", -"墨沈" => "墨沈", -"堕胎药" => "墮胎藥", -"垦复" => "墾複", -"垦辟" => "墾闢", -"垄断价格" => "壟斷價格", -"垄断资产" => "壟斷資產", -"垄断集团" => "壟斷集團", -"壮面" => "壯麵", -"壹郁" => "壹鬱", -"壶里" => "壺裡", -"壸范" => "壼範", -"寿面" => "壽麵", -"夏天里" => "夏天裡", -"夏历" => "夏曆", -"夏历史" => "夏歷史", -"夏游" => "夏遊", -"外强中干" => "外強中乾", -"外制" => "外製", -"多划" => "多劃", -"多只是" => "多只是", -"多天后" => "多天後", -"多于" => "多於", -"多冲" => "多衝", -"多丑" => "多醜", -"多只" => "多隻", -"多余" => "多餘", -"多么" => "多麼", -"夜光表" => "夜光錶", -"夜里" => "夜裡", -"夜游" => "夜遊", -"梦里" => "夢裡", -"梦游" => "夢遊", -"伙伴" => "夥伴", -"伙友" => "夥友", -"伙同" => "夥同", -"伙众" => "夥眾", -"伙计" => "夥計", -"大伙" => "大夥", -"大干" => "大幹", -"大批涌到" => "大批湧到", -"大于" => "大於", -"大明历" => "大明曆", -"大历" => "大曆", -"大梁" => "大樑", -"大目干连" => "大目乾連", -"大衍历" => "大衍曆", -"大言非夸" => "大言非夸", -"大丑" => "大醜", -"大锤" => "大鎚", -"大只" => "大隻", -"天干物燥" => "天乾物燥", -"天克地冲" => "天克地衝", -"天后" => "天後", -"0天后" => "0天後", -"天文钟" => "天文鐘", -"天然硷" => "天然鹼", -"天翻地复" => "天翻地覆", -"天复地载" => "天覆地載", -"太仆" => "太僕", -"太初历" => "太初曆", -"夯干" => "夯幹", -"失信于人" => "失信於人", -"失于" => "失於", -"夸人" => "夸人", -"夸克" => "夸克", -"夸姣" => "夸姣", -"夸容" => "夸容", -"夸毗" => "夸毗", -"夸父" => "夸父", -"夸特" => "夸特", -"夸丽" => "夸麗", -"奇丑" => "奇醜", -"奏折" => "奏摺", -"奏于" => "奏於", -"夺斗" => "奪鬥", -"奋斗" => "奮鬥", -"女佣" => "女傭", -"女仆" => "女僕", -"奴仆" => "奴僕", -"好干" => "好乾", -"好家伙" => "好傢夥", -"好于" => "好於", -"好签" => "好籤", -"好丑" => "好醜", -"如于" => "如於", -"如果干" => "如果幹", -"如法泡制" => "如法泡製", -"妙药" => "妙藥", -"始于" => "始於", -"委罪于人" => "委罪於人", -"委讬" => "委託", -"姜丝" => "姜絲", -"奸夫" => "姦夫", -"奸妇" => "姦婦", -"奸情" => "姦情", -"奸杀" => "姦殺", -"奸污" => "姦汙", -"奸淫" => "姦淫", -"奸邪" => "姦邪", -"威棱" => "威稜", -"婚后" => "婚後", -"婢仆" => "婢僕", -"嫁于" => "嫁於", -"嫁祸于人" => "嫁禍於人", -"嫌好道丑" => "嫌好道醜", -"娴于" => "嫻於", -"嬉游" => "嬉遊", -"嬴余" => "嬴餘", -"子之丰兮" => "子之丰兮", -"字汇" => "字彙", -"字里行间" => "字裡行間", -"存十一于千百" => "存十一於千百", -"存折" => "存摺", -"季后赛" => "季後賽", -"孤寡不谷" => "孤寡不穀", -"宇宙志" => "宇宙誌", -"安于" => "安於", -"安沈铁路" => "安瀋鐵路", -"安眠药" => "安眠藥", -"安胎药" => "安胎藥", -"完工后" => "完工後", -"完成后" => "完成後", -"宗周钟" => "宗周鐘", -"官地为采" => "官地為寀", -"官历" => "官曆", -"官庄" => "官莊", -"定于" => "定於", -"定准" => "定準", -"定制" => "定製", -"定都于" => "定都於", -"宜于" => "宜於", -"宦游" => "宦遊", -"宫里" => "宮裡", -"害于" => "害於", -"宴游" => "宴遊", -"家仆" => "家僕", -"家庄" => "家莊", -"家里" => "家裡", -"家丑" => "家醜", -"容后说明" => "容後說明", -"容于" => "容於", -"容范" => "容範", -"寄于" => "寄於", -"寄讬" => "寄託", -"寇雠" => "寇讎", -"富于" => "富於", -"富余" => "富餘", -"寒栗" => "寒慄", -"寒于" => "寒於", -"寓兵于农" => "寓兵於農", -"寓教于乐" => "寓教於樂", -"寓于" => "寓於", -"寡欲" => "寡慾", -"实干" => "實幹", -"写字台" => "寫字檯", -"宽于" => "寬於", -"宽余" => "寬餘", -"宽松" => "寬鬆", -"寮采" => "寮寀", -"宝山庄" => "寶山庄", -"宝历" => "寶曆", -"封面里" => "封面裡", -"射雕" => "射鵰", -"将于" => "將於", -"专美于前" => "專美於前", -"专注" => "專註", -"对折" => "對摺", -"对于" => "對於", -"对准" => "對準", -"对华发动" => "對華發動", -"对表" => "對錶", -"导游" => "導遊", -"小仆" => "小僕", -"小伙子" => "小夥子", -"小于" => "小於", -"小米面" => "小米麵", -"小只" => "小隻", -"少采" => "少採", -"少于" => "少於", -"就于" => "就於", -"就范" => "就範", -"就读于" => "就讀於", -"尸魂界" => "尸魂界", -"尼克松" => "尼克鬆", -"局里" => "局裡", -"居于" => "居於", -"屈服于" => "屈服於", -"屋子里" => "屋子裡", -"屋梁" => "屋樑", -"屋里" => "屋裡", -"屑于" => "屑於", -"屡顾尔仆" => "屢顧爾僕", -"属意于" => "屬意於", -"属于" => "屬於", -"屯扎" => "屯紮", -"屯里" => "屯裡", -"山崩钟应" => "山崩鐘應", -"山岳" => "山嶽", -"山后" => "山後", -"山梁" => "山樑", -"山庄" => "山莊", -"山药" => "山藥", -"山里" => "山裡", -"峰回" => "峰迴", -"昆剧" => "崑劇", -"昆山" => "崑山", -"昆仑" => "崑崙", -"昆曲" => "崑曲", -"昆腔" => "崑腔", -"昆苏" => "崑蘇", -"昆调" => "崑調", -"仑背" => "崙背", -"嶒棱" => "嶒稜", -"岳麓山" => "嶽麓山", -"川谷" => "川穀", -"巡回" => "巡迴", -"巡游" => "巡遊", -"工于" => "工於", -"工致" => "工緻", -"左后" => "左後", -"左冲右突" => "左衝右突", -"巧妇做不得无面馎饦" => "巧婦做不得無麵餺飥", -"巧干" => "巧幹", -"巧历" => "巧曆", -"差于" => "差於", -"已于" => "已於", -"巴尔干" => "巴爾幹", -"巷里" => "巷裡", -"市里" => "市裡", -"布谷" => "布穀", -"希伯来历" => "希伯來曆", -"帘子" => "帘子", -"帘布" => "帘布", -"师范" => "師範", -"席卷" => "席捲", -"带团参加" => "帶團參加", -"带发修行" => "帶髮修行", -"幕后" => "幕後", -"帮佣" => "幫傭", -"干系" => "干係", -"干着急" => "干著急", -"平平当当" => "平平當當", -"平准" => "平準", -"年后" => "年後", -"年历" => "年曆", -"年历史" => "年歷史", -"年谷" => "年穀", -"年里" => "年裡", -"并州" => "并州", -"干上" => "幹上", -"干下去" => "幹下去", -"干了" => "幹了", -"干事" => "幹事", -"干些" => "幹些", -"干人" => "幹人", -"干什么" => "幹什麼", -"干个" => "幹個", -"干劲" => "幹勁", -"干吏" => "幹吏", -"干员" => "幹員", -"干吗" => "幹嗎", -"干嘛" => "幹嘛", -"干坏事" => "幹壞事", -"干完" => "幹完", -"干家" => "幹家", -"干得" => "幹得", -"干性油" => "幹性油", -"干才" => "幹才", -"干掉" => "幹掉", -"干探" => "幹探", -"干校" => "幹校", -"干活" => "幹活", -"干流" => "幹流", -"干济" => "幹濟", -"干营生" => "幹營生", -"干父之蛊" => "幹父之蠱", -"干球温度" => "幹球溫度", -"干当" => "幹當", -"干的停当" => "幹的停當", -"干细胞" => "幹細胞", -"干线" => "幹線", -"干练" => "幹練", -"干缺" => "幹缺", -"干蛊" => "幹蠱", -"干警" => "幹警", -"干起来" => "幹起來", -"干路" => "幹路", -"干道" => "幹道", -"干部" => "幹部", -"干革命" => "幹革命", -"干头" => "幹頭", -"干么" => "幹麼", -"几划" => "幾劃", -"几天后" => "幾天後", -"几于" => "幾於", -"几丝" => "幾絲", -"几只" => "幾隻", -"几出" => "幾齣", -"广部" => "广部", -"府干卿" => "府干卿", -"府干扰" => "府干擾", -"府干政" => "府干政", -"府干涉" => "府干涉", -"府干犯" => "府干犯", -"府干预" => "府干預", -"府干" => "府幹", -"府后" => "府後", -"座钟" => "座鐘", -"康采恩" => "康採恩", -"康庄" => "康莊", -"厨余" => "廚餘", -"庙里" => "廟裡", -"广舍" => "廣捨", -"延后" => "延後", -"建于" => "建於", -"建都于" => "建都於", -"弄干" => "弄乾", -"弄丑" => "弄醜", -"弄脏" => "弄髒", -"弄松" => "弄鬆", -"吊儿郎当" => "弔兒郎當", -"吊卷" => "弔卷", -"吊古" => "弔古", -"吊唁" => "弔唁", -"吊丧" => "弔喪", -"吊孝" => "弔孝", -"吊客" => "弔客", -"吊带" => "弔帶", -"吊慰" => "弔慰", -"吊挂" => "弔掛", -"吊文" => "弔文", -"吊死" => "弔死", -"吊民伐罪" => "弔民伐罪", -"吊祭" => "弔祭", -"弘历" => "弘曆", -"弱于" => "弱於", -"弱硷" => "弱鹼", -"张三丰" => "張三丰", -"强占" => "強佔", -"强奸" => "強姦", -"强干" => "強幹", -"强于" => "強於", -"强硷" => "強鹼", -"别口气" => "彆口氣", -"别强" => "彆強", -"别扭" => "彆扭", -"别拗" => "彆拗", -"别气" => "彆氣", -"别着" => "彆著", -"弹子台" => "彈子檯", -"弹药" => "彈藥", -"汇报" => "彙報", -"汇整" => "彙整", -"汇编" => "彙編", -"汇纂" => "彙纂", -"汇辑" => "彙輯", -"汇集" => "彙集", -"形单影只" => "形單影隻", -"形于" => "形於", -"役于" => "役於", -"役于外物" => "役於外物", -"往来于" => "往來於", -"往后" => "往後", -"往里" => "往裡", -"往复" => "往複", -"很干" => "很乾", -"律历志" => "律曆志", -"后上" => "後上", -"后下" => "後下", -"后世" => "後世", -"后主" => "後主", -"后事" => "後事", -"后人" => "後人", -"后代" => "後代", -"后仰" => "後仰", -"后件" => "後件", -"后任" => "後任", -"后作" => "後作", -"后来" => "後來", -"后偏" => "後偏", -"后备" => "後備", -"后传" => "後傳", -"后分" => "後分", -"后到" => "後到", -"后力不继" => "後力不繼", -"后劲" => "後勁", -"后勤" => "後勤", -"后区" => "後區", -"后半" => "後半", -"后印" => "後印", -"后去" => "後去", -"后台" => "後台", -"后向" => "後向", -"后周" => "後周", -"后唐" => "後唐", -"后嗣" => "後嗣", -"后园" => "後園", -"后图" => "後圖", -"后土" => "後土", -"后埔" => "後埔", -"后堂" => "後堂", -"后尘" => "後塵", -"后壁" => "後壁", -"后天" => "後天", -"后奏" => "後奏", -"后娘" => "後娘", -"后学" => "後學", -"后宫" => "後宮", -"后山" => "後山", -"后巷" => "後巷", -"后市" => "後市", -"后年" => "後年", -"后几" => "後幾", -"后庄" => "後庄", -"后序" => "後序", -"后座" => "後座", -"后悔" => "後悔", -"后患" => "後患", -"后房" => "後房", -"后手" => "後手", -"后排" => "後排", -"后掠角" => "後掠角", -"后接" => "後接", -"后援" => "後援", -"后撤" => "後撤", -"后攻" => "後攻", -"后放" => "後放", -"后效" => "後效", -"后文" => "後文", -"后方" => "後方", -"后于" => "後於", -"后日" => "後日", -"后晋" => "後晉", -"后晌" => "後晌", -"后晚" => "後晚", -"后景" => "後景", -"后会" => "後會", -"后有" => "後有", -"后望镜" => "後望鏡", -"后期" => "後期", -"后果" => "後果", -"后桅" => "後桅", -"后梁" => "後梁", -"后桥" => "後橋", -"后步" => "後步", -"后段" => "後段", -"后殿" => "後殿", -"后母" => "後母", -"后派" => "後派", -"后浪" => "後浪", -"后凉" => "後涼", -"后港" => "後港", -"后汉" => "後漢", -"后为" => "後為", -"后无来者" => "後無來者", -"后燕" => "後燕", -"后生" => "後生", -"后用" => "後用", -"后由" => "後由", -"后盾" => "後盾", -"后知" => "後知", -"后福" => "後福", -"后秃" => "後禿", -"后秦" => "後秦", -"后空翻" => "後空翻", -"后窗" => "後窗", -"后站" => "後站", -"后端" => "後端", -"后竹围" => "後竹圍", -"后节" => "後節", -"后篇" => "後篇", -"后继" => "後繼", -"后续" => "後續", -"后置" => "後置", -"后者" => "後者", -"后肢" => "後肢", -"后背" => "後背", -"后脑" => "後腦", -"后脚" => "後腳", -"后腿" => "後腿", -"后膛" => "後膛", -"后花园" => "後花園", -"后菜园" => "後菜園", -"后叶" => "後葉", -"后行" => "後行", -"后街" => "後街", -"后卫" => "後衛", -"后裔" => "後裔", -"后䙓" => "後襬", -"后视镜" => "後視鏡", -"后计" => "後計", -"后记" => "後記", -"后设" => "後設", -"后读" => "後讀", -"后走" => "後走", -"后起" => "後起", -"后赵" => "後趙", -"后足" => "後足", -"后跟" => "後跟", -"后路" => "後路", -"后身" => "後身", -"后车" => "後車", -"后辈" => "後輩", -"后轮" => "後輪", -"后转" => "後轉", -"后述" => "後述", -"后退" => "後退", -"后送" => "後送", -"后进" => "後進", -"后过" => "後過", -"后遗症" => "後遺症", -"后边" => "後邊", -"后部" => "後部", -"后镜" => "後鏡", -"后门" => "後門", -"后防" => "後防", -"后院" => "後院", -"后集" => "後集", -"后面" => "後面", -"后项" => "後項", -"后头" => "後頭", -"后颈" => "後頸", -"后顾" => "後顧", -"后魏" => "後魏", -"后点" => "後點", -"后龙" => "後龍", -"徐干" => "徐幹", -"徒讬空言" => "徒託空言", -"得于" => "得於", -"徜徉于" => "徜徉於", -"从事于" => "從事於", -"从于" => "從於", -"从里到外" => "從裡到外", -"从里向外" => "從裡向外", -"复始" => "復始", -"复雠" => "復讎", -"征信" => "徵信", -"征候" => "徵候", -"征兆" => "徵兆", -"征兵" => "徵兵", -"征到" => "徵到", -"征募" => "徵募", -"征友" => "徵友", -"征召" => "徵召", -"征引" => "徵引", -"征得" => "徵得", -"征收" => "徵收", -"征文" => "徵文", -"征求" => "徵求", -"征状" => "徵狀", -"征用" => "徵用", -"征税" => "徵稅", -"征稿" => "徵稿", -"征结" => "徵結", -"征聘" => "徵聘", -"征训" => "徵訓", -"征询" => "徵詢", -"征调" => "徵調", -"征象" => "徵象", -"征购" => "徵購", -"征集" => "徵集", -"征验出" => "徵驗出", -"心愿" => "心愿", -"心于" => "心於", -"心细如发" => "心細如髮", -"心荡神驰" => "心蕩神馳", -"心药" => "心藥", -"心里" => "心裡", -"心余" => "心餘", -"志于" => "志於", -"忙并" => "忙併", -"忙于" => "忙於", -"忙里" => "忙裡", -"忠仆" => "忠僕", -"忠于" => "忠於", -"快干" => "快乾", -"快干" => "快幹", -"快快当当" => "快快當當", -"快冲" => "快衝", -"忽前忽后" => "忽前忽後", -"怎么" => "怎麼", -"怎么着" => "怎麼著", -"怒形于色" => "怒形於色", -"怒于" => "怒於", -"怒发冲冠" => "怒髮衝冠", -"思前思后" => "思前思後", -"思前想后" => "思前想後", -"急于" => "急於", -"急冲而下" => "急衝而下", -"性征" => "性徵", -"性欲" => "性慾", -"怪里怪气" => "怪裡怪氣", -"怫郁" => "怫鬱", -"息谷" => "息穀", -"恰才" => "恰纔", -"悍药" => "悍藥", -"悒郁" => "悒鬱", -"悒郁寡欢" => "悒鬱寡歡", -"悠游" => "悠遊", -"闷着头儿干" => "悶著頭兒幹", -"悸栗" => "悸慄", -"情欲" => "情慾", -"惇朴" => "惇樸", -"恶直丑正" => "惡直醜正", -"惴栗" => "惴慄", -"意大利面" => "意大利麵", -"意面" => "意麵", -"爱困" => "愛睏", -"感冒药" => "感冒藥", -"感于" => "感於", -"愧于" => "愧於", -"愿朴" => "愿樸", -"愿而恭" => "愿而恭", -"慌里慌张" => "慌裡慌張", -"惯于" => "慣於", -"慰藉" => "慰藉", -"庆吊" => "慶弔", -"庆历" => "慶曆", -"欲令智昏" => "慾令智昏", -"欲壑难填" => "慾壑難填", -"欲念" => "慾念", -"欲望" => "慾望", -"欲海" => "慾海", -"欲火" => "慾火", -"欲障" => "慾障", -"忧形于色" => "憂形於色", -"忧郁" => "憂鬱", -"凭吊" => "憑弔", -"凭藉着" => "憑藉著", -"恳讬" => "懇託", -"懈松" => "懈鬆", -"应征" => "應徵", -"应钟" => "應鐘", -"蒙懂" => "懞懂", -"蒙蒙懂懂" => "懞懞懂懂", -"蒙直" => "懞直", -"惩前毖后" => "懲前毖後", -"懒于" => "懶於", -"怀里" => "懷裡", -"怀表" => "懷錶", -"悬梁" => "懸樑", -"悬臂梁" => "懸臂樑", -"悬钟" => "懸鐘", -"惧于" => "懼於", -"懿范" => "懿範", -"恋恋不舍" => "戀戀不捨", -"成于" => "成於", -"成药" => "成藥", -"或于" => "或於", -"戬谷" => "戩穀", -"截发" => "截髮", -"战天斗地" => "戰天鬥地", -"战后" => "戰後", -"战栗" => "戰慄", -"战斗" => "戰鬥", -"戴表" => "戴錶", -"房里" => "房裡", -"扁拟谷盗虫" => "扁擬穀盜蟲", -"手冢治虫" => "手塚治虫", -"手折" => "手摺", -"手里" => "手裡", -"手表" => "手錶", -"手松" => "手鬆", -"才干" => "才幹", -"打干哕" => "打乾噦", -"打并" => "打併", -"打卡钟" => "打卡鐘", -"打干" => "打幹", -"打拼" => "打拚", -"打谷" => "打穀", -"打钟" => "打鐘", -"打斗" => "打鬥", -"扞御" => "扞禦", -"扯面" => "扯麵", -"批准的" => "批准的", -"批复" => "批複", -"批注" => "批註", -"批斗" => "批鬥", -"承先启后" => "承先啟後", -"承制" => "承製", -"抑郁" => "抑鬱", -"抓奸" => "抓姦", -"抓药" => "抓藥", -"抓斗" => "抓鬥", -"投药" => "投藥", -"抗癌药" => "抗癌藥", -"抗御" => "抗禦", -"抗药" => "抗藥", -"抗硷" => "抗鹼", -"折冲" => "折衝", -"披榛采兰" => "披榛採蘭", -"披头散发" => "披頭散髮", -"披发" => "披髮", -"抱朴而长吟兮" => "抱朴而長吟兮", -"抱素怀朴" => "抱素懷樸", -"抵御" => "抵禦", -"抹干" => "抹乾", -"抽公签" => "抽公籤", -"抽签" => "抽籤", -"抿发" => "抿髮", -"拆伙" => "拆夥", -"拈须" => "拈鬚", -"拉纤" => "拉縴", -"拉面" => "拉麵", -"拒人于" => "拒人於", -"拒于" => "拒於", -"拓朴" => "拓樸", -"拗别" => "拗彆", -"拘于" => "拘於", -"拘泥于" => "拘泥於", -"拙于" => "拙於", -"拙朴" => "拙樸", -"拼命" => "拚命", -"拼舍" => "拚捨", -"拼死" => "拚死", -"拼斗" => "拚鬥", -"拜讬" => "拜託", -"括发" => "括髮", -"拭干" => "拭乾", -"拮据" => "拮据", -"拿准" => "拿準", -"拿破仑" => "拿破崙", -"指手划脚" => "指手劃腳", -"振荡" => "振蕩", -"捆扎" => "捆紮", -"捉奸" => "捉姦", -"捉发" => "捉髮", -"捍御" => "捍禦", -"捏面人" => "捏麵人", -"舍不得" => "捨不得", -"舍出" => "捨出", -"舍去" => "捨去", -"舍命" => "捨命", -"舍堕" => "捨墮", -"舍安就危" => "捨安就危", -"舍实" => "捨實", -"舍己从人" => "捨己從人", -"舍己救人" => "捨己救人", -"舍己为人" => "捨己為人", -"舍己为公" => "捨己為公", -"舍己为国" => "捨己為國", -"舍得" => "捨得", -"舍我其谁" => "捨我其誰", -"舍本逐末" => "捨本逐末", -"舍弃" => "捨棄", -"舍死忘生" => "捨死忘生", -"舍生" => "捨生", -"舍短取长" => "捨短取長", -"舍身" => "捨身", -"舍车保帅" => "捨車保帥", -"舍近求远" => "捨近求遠", -"卷住" => "捲住", -"卷来" => "捲來", -"卷儿" => "捲兒", -"卷入" => "捲入", -"卷动" => "捲動", -"卷去" => "捲去", -"卷图" => "捲圖", -"卷土重来" => "捲土重來", -"卷尺" => "捲尺", -"卷心菜" => "捲心菜", -"卷成" => "捲成", -"卷曲" => "捲曲", -"卷款逃走" => "捲款逃走", -"卷毛" => "捲毛", -"卷烟" => "捲煙", -"卷筒" => "捲筒", -"卷帘" => "捲簾", -"卷纸" => "捲紙", -"卷缩" => "捲縮", -"卷舌" => "捲舌", -"卷菸" => "捲菸", -"卷袖" => "捲袖", -"卷走" => "捲走", -"卷起" => "捲起", -"卷轴" => "捲軸", -"卷逃" => "捲逃", -"卷铺盖" => "捲鋪蓋", -"卷云" => "捲雲", -"卷风" => "捲風", -"卷发" => "捲髮", -"捵面" => "捵麵", -"扫荡" => "掃蕩", -"掌柜" => "掌柜", -"排骨面" => "排骨麵", -"挂帘" => "掛帘", -"挂钟" => "掛鐘", -"挂面" => "掛麵", -"采下" => "採下", -"采伐" => "採伐", -"采住" => "採住", -"采信" => "採信", -"采光" => "採光", -"采到" => "採到", -"采制" => "採制", -"采区" => "採區", -"采去" => "採去", -"采取" => "採取", -"采回" => "採回", -"采在" => "採在", -"采好" => "採好", -"采得" => "採得", -"采拾" => "採拾", -"采挖" => "採挖", -"采掘" => "採掘", -"采摘" => "採摘", -"采摭" => "採摭", -"采择" => "採擇", -"采撷" => "採擷", -"采收" => "採收", -"采料" => "採料", -"采暖" => "採暖", -"采桑" => "採桑", -"采样" => "採樣", -"采樵人" => "採樵人", -"采树种" => "採樹種", -"采气" => "採氣", -"采油" => "採油", -"采为" => "採為", -"采煤" => "採煤", -"采猎" => "採獵", -"采珠" => "採珠", -"采生折割" => "採生折割", -"采用" => "採用", -"采的" => "採的", -"采石" => "採石", -"采石场" => "採石場", -"采石厂" => "採石廠", -"采砂场" => "採砂場", -"采矿" => "採礦", -"采种" => "採種", -"采空区" => "採空區", -"采空采穗" => "採空採穗", -"采纳" => "採納", -"采给" => "採給", -"采花" => "採花", -"采芹人" => "採芹人", -"采茶" => "採茶", -"采菊" => "採菊", -"采莲" => "採蓮", -"采薇" => "採薇", -"采药" => "採藥", -"采行" => "採行", -"采补" => "採補", -"采访" => "採訪", -"采证" => "採證", -"采买" => "採買", -"采购" => "採購", -"采办" => "採辦", -"采运" => "採運", -"采过" => "採過", -"采选" => "採選", -"采金" => "採金", -"采录" => "採錄", -"采铁" => "採鐵", -"采集" => "採集", -"采风" => "採風", -"采风问俗" => "採風問俗", -"采食" => "採食", -"采盐" => "採鹽", -"掣签" => "掣籤", -"接着说" => "接著說", -"推讬" => "推託", -"提子干" => "提子乾", -"提心吊胆" => "提心弔膽", -"提摩太后书" => "提摩太後書", -"插于" => "插於", -"插足于" => "插足於", -"换签" => "換籤", -"换药" => "換藥", -"换只" => "換隻", -"换发" => "換髮", -"揩干" => "揩乾", -"揪采" => "揪採", -"揭丑" => "揭醜", -"挥手表" => "揮手表", -"搋面" => "搋麵", -"损于" => "損於", -"搏斗" => "搏鬥", -"摇荡" => "搖蕩", -"搭干铺" => "搭乾鋪", -"搭伙" => "搭夥", -"抢占" => "搶佔", -"搽药" => "搽藥", -"摧坚获丑" => "摧堅獲醜", -"摭采" => "摭採", -"摸棱" => "摸稜", -"折合" => "摺合", -"折奏" => "摺奏", -"折子" => "摺子", -"折尺" => "摺尺", -"折扇" => "摺扇", -"折梯" => "摺梯", -"折椅" => "摺椅", -"折叠" => "摺疊", -"折痕" => "摺痕", -"折篷" => "摺篷", -"折纸" => "摺紙", -"折裙" => "摺裙", -"捞干" => "撈乾", -"捞面" => "撈麵", -"撚须" => "撚鬚", -"撞木钟" => "撞木鐘", -"撞球台" => "撞球檯", -"撞钟" => "撞鐘", -"撞阵冲军" => "撞陣衝軍", -"撤并" => "撤併", -"撤后" => "撤後", -"拨谷" => "撥穀", -"播于" => "播於", -"扑冬" => "撲鼕", -"擀面" => "擀麵", -"擅于" => "擅於", -"击钟" => "擊鐘", -"担仔面" => "擔仔麵", -"担担面" => "擔擔麵", -"担着" => "擔著", -"担负着" => "擔負著", -"据云" => "據云", -"据干而窥井底" => "據榦而窺井底", -"挤身于" => "擠身於", -"擢发" => "擢髮", -"擢发难数" => "擢髮難數", -"擦干" => "擦乾", -"擦药" => "擦藥", -"拟于" => "擬於", -"拧干" => "擰乾", -"摆钟" => "擺鐘", -"摄于" => "攝於", -"摄制" => "攝製", -"支干" => "支幹", -"收获" => "收穫", -"改征" => "改徵", -"改于" => "改於", -"攻占" => "攻佔", -"放蒙挣" => "放懞掙", -"放荡" => "放蕩", -"放松" => "放鬆", -"故于" => "故於", -"敏于" => "敏於", -"败于" => "敗於", -"叙说着" => "敘說著", -"教于" => "教於", -"敢干" => "敢幹", -"敢于" => "敢於", -"散伙" => "散夥", -"散于" => "散於", -"散荡" => "散蕩", -"敦朴" => "敦樸", -"敬挽" => "敬輓", -"敲钟" => "敲鐘", -"整只" => "整隻", -"敌后" => "敵後", -"敷药" => "敷藥", -"数天后" => "數天後", -"数罪并罚" => "數罪併罰", -"数与虏确" => "數與虜确", -"文汇报" => "文匯報", -"文思泉涌" => "文思泉湧", -"斗转参横" => "斗轉參橫", -"斗哄" => "斗鬨", -"料斗" => "料鬥", -"斫雕为朴" => "斫雕為樸", -"新历" => "新曆", -"新历史" => "新歷史", -"新扎" => "新紮", -"新庄" => "新莊", -"斲雕为朴" => "斲雕為樸", -"断后" => "斷後", -"断发" => "斷髮", -"方便面" => "方便麵", -"方几" => "方几", -"于一役" => "於一役", -"于七" => "於七", -"于世" => "於世", -"于事" => "於事", -"于事无补" => "於事無補", -"于人" => "於人", -"于今" => "於今", -"于他" => "於他", -"于伏" => "於伏", -"于何" => "於何", -"于你" => "於你", -"于前" => "於前", -"于劣" => "於劣", -"于勤" => "於勤", -"于呼哀哉" => "於呼哀哉", -"于国" => "於國", -"于坏" => "於坏", -"于垂" => "於垂", -"于夫罗" => "於夫羅", -"于她" => "於她", -"于好" => "於好", -"于始" => "於始", -"于它" => "於它", -"于家" => "於家", -"于密" => "於密", -"于左" => "於左", -"于差" => "於差", -"于己" => "於己", -"于市" => "於市", -"于幕" => "於幕", -"于幼华" => "於幼華", -"于弱" => "於弱", -"于强" => "於強", -"于征" => "於征", -"于后" => "於後", -"于心" => "於心", -"于心何忍" => "於心何忍", -"于思" => "於思", -"于怀" => "於懷", -"于我" => "於我", -"于斯" => "於斯", -"于于" => "於於", -"于是" => "於是", -"于时" => "於時", -"于梨华" => "於梨華", -"于乐" => "於樂", -"于此" => "於此", -"于民" => "於民", -"于法" => "於法", -"于法无据" => "於法無據", -"于潜县" => "於潛縣", -"于火" => "於火", -"于焉" => "於焉", -"于墙" => "於牆", -"于物" => "於物", -"于田" => "於田", -"于毕" => "於畢", -"于尽" => "於盡", -"于盲" => "於盲", -"于祂" => "於祂", -"于穆" => "於穆", -"于终" => "於終", -"于美" => "於美", -"于色" => "於色", -"于行" => "於行", -"于衷" => "於衷", -"于该" => "於該", -"于农" => "於農", -"于途" => "於途", -"于丑" => "於醜", -"于野" => "於野", -"于陆" => "於陸", -"于飞" => "於飛", -"施舍" => "施捨", -"施于" => "施於", -"施舍之道" => "施舍之道", -"施药" => "施藥", -"旁征博引" => "旁徵博引", -"旁注" => "旁註", -"旅游" => "旅遊", -"旋干转坤" => "旋乾轉坤", -"旋绕着" => "旋繞著", -"旋回" => "旋迴", -"族里" => "族裡", -"日后" => "日後", -"日历" => "日曆", -"日志" => "日誌", -"早于" => "早於", -"旱干" => "旱乾", -"昆仑" => "昆崙", -"升平" => "昇平", -"升阳" => "昇陽", -"明征" => "明徵", -"明于" => "明於", -"明窗净几" => "明窗淨几", -"明范" => "明範", -"明里" => "明裡", -"易于" => "易於", -"星历" => "星曆", -"星辰表" => "星辰錶", -"星斗" => "星鬥", -"春假里" => "春假裡", -"春天里" => "春天裡", -"春药" => "春藥", -"春游" => "春遊", -"昧于" => "昧於", -"时钟" => "時鐘", -"晃荡" => "晃蕩", -"晋升" => "晉陞", -"晒干" => "晒乾", -"晞发" => "晞髮", -"晨钟" => "晨鐘", -"晨钟暮鼓" => "晨鐘暮鼓", -"景致" => "景緻", -"晾干" => "晾乾", -"晕船药" => "暈船藥", -"晕车药" => "暈車藥", -"暗地里" => "暗地裡", -"暗沟里" => "暗溝裡", -"暗里" => "暗裡", -"暗斗" => "暗鬥", -"畅游" => "暢遊", -"暂于" => "暫於", -"暮鼓晨钟" => "暮鼓晨鐘", -"历元" => "曆元", -"历命" => "曆命", -"历始" => "曆始", -"历室" => "曆室", -"历尾" => "曆尾", -"历数" => "曆數", -"历日" => "曆日", -"历书" => "曆書", -"历本" => "曆本", -"历法" => "曆法", -"历纪" => "曆紀", -"晒干" => "曬乾", -"晒谷" => "曬穀", -"更仆难数" => "更僕難數", -"更签" => "更籤", -"书后" => "書後", -"书呆子" => "書獃子", -"书签" => "書籤", -"曾于" => "曾於", -"曾朴" => "曾樸", -"最后" => "最後", -"会占" => "會佔", -"会干" => "會幹", -"会后" => "會後", -"会于" => "會於", -"会里" => "會裡", -"月历" => "月曆", -"有仆" => "有僕", -"有助于" => "有助於", -"有害于" => "有害於", -"有损于" => "有損於", -"有求于人" => "有求於人", -"有奖征答" => "有獎徵答", -"有益于" => "有益於", -"有棱有角" => "有稜有角", -"有只" => "有隻", -"有余" => "有餘", -"有发头陀寺" => "有髮頭陀寺", -"服务于" => "服務於", -"服从于" => "服從於", -"服于" => "服於", -"服药" => "服藥", -"朝干夕惕" => "朝乾夕惕", -"朝后" => "朝後", -"朝钟" => "朝鐘", -"朦胧" => "朦朧", -"木偶戏扎" => "木偶戲紮", -"木制" => "木製", -"未干" => "未乾", -"末药" => "末藥", -"术赤" => "朮赤", -"朱庆余" => "朱慶餘", -"朱理安历" => "朱理安曆", -"李连杰" => "李連杰", -"村庄" => "村莊", -"村落发" => "村落發", -"村里" => "村裡", -"束发" => "束髮", -"杯干" => "杯乾", -"杰特" => "杰特", -"东岳" => "東嶽", -"东冲西突" => "東衝西突", -"东游" => "東遊", -"松山庄" => "松山庄", -"松柏后凋" => "松柏後凋", -"板着脸" => "板著臉", -"板荡" => "板蕩", -"枕经藉史" => "枕經藉史", -"枕藉" => "枕藉", -"林宏岳" => "林宏嶽", -"林郁方" => "林郁方", -"林钟" => "林鐘", -"果干" => "果乾", -"果子干" => "果子乾", -"果于" => "果於", -"枝不得大于干" => "枝不得大於榦", -"枝干" => "枝幹", -"枯干" => "枯乾", -"某只" => "某隻", -"染指于" => "染指於", -"染发" => "染髮", -"柜上" => "柜上", -"柜台" => "柜台", -"柜子" => "柜子", -"柱梁" => "柱樑", -"校准" => "校準", -"校雠学" => "校讎學", -"核准的" => "核准的", -"格于" => "格於", -"格范" => "格範", -"格斗" => "格鬥", -"桂圆干" => "桂圓乾", -"案发后" => "案發後", -"桌历" => "桌曆", -"桑干" => "桑乾", -"条干" => "條幹", -"梨干" => "梨乾", -"械斗" => "械鬥", -"弃舍" => "棄捨", -"棉制" => "棉製", -"棒子面" => "棒子麵", -"枣庄" => "棗莊", -"栋梁" => "棟樑", -"棫朴" => "棫樸", -"栖于" => "棲於", -"植发" => "植髮", -"椰枣干" => "椰棗乾", -"楚庄王" => "楚莊王", -"桢干" => "楨幹", -"业余" => "業餘", -"榨干" => "榨乾", -"乐意于" => "樂意於", -"乐于" => "樂於", -"樊于期" => "樊於期", -"梁上" => "樑上", -"梁子" => "樑子", -"梁书" => "樑書", -"梁柱" => "樑柱", -"标志着" => "標志著", -"标标致致" => "標標致致", -"标准" => "標準", -"标签" => "標籤", -"标致" => "標緻", -"标志" => "標誌", -"模棱" => "模稜", -"模范" => "模範", -"模范棒棒堂" => "模范棒棒堂", -"模制" => "模製", -"样范" => "樣範", -"樵采" => "樵採", -"朴修斯" => "樸修斯", -"朴厚" => "樸厚", -"朴学" => "樸學", -"朴实" => "樸實", -"朴念仁" => "樸念仁", -"朴拙" => "樸拙", -"朴樕" => "樸樕", -"朴父" => "樸父", -"朴直" => "樸直", -"朴素" => "樸素", -"朴讷" => "樸訥", -"朴质" => "樸質", -"朴鄙" => "樸鄙", -"朴重" => "樸重", -"朴野" => "樸野", -"朴野无文" => "樸野無文", -"朴钝" => "樸鈍", -"朴陋" => "樸陋", -"朴马" => "樸馬", -"朴鲁" => "樸魯", -"树干" => "樹幹", -"树干" => "樹榦", -"树梁" => "樹樑", -"桥梁" => "橋樑", -"机绣" => "機繡", -"横征暴敛" => "橫徵暴斂", -"横梁" => "橫樑", -"横冲" => "橫衝", -"台子" => "檯子", -"台布" => "檯布", -"台灯" => "檯燈", -"台球" => "檯球", -"台面" => "檯面", -"柜台" => "櫃檯", -"栉发工" => "櫛髮工", -"次于" => "次於", -"欺蒙" => "欺矇", -"歇后" => "歇後", -"歌钟" => "歌鐘", -"欧游" => "歐遊", -"止咳药" => "止咳藥", -"止于" => "止於", -"止痛药" => "止痛藥", -"止血药" => "止血藥", -"正官庄" => "正官庄", -"正后" => "正後", -"正于" => "正於", -"正当着" => "正當著", -"此后" => "此後", -"武斗" => "武鬥", -"归并" => "歸併", -"归功于" => "歸功於", -"归咎于" => "歸咎於", -"归因于" => "歸因於", -"归于" => "歸於", -"归罪于" => "歸罪於", -"归随于" => "歸隨於", -"归顺于" => "歸順於", -"归余" => "歸餘", -"死伤相藉" => "死傷相藉", -"死后" => "死後", -"死于" => "死於", -"死里求生" => "死裡求生", -"死里逃生" => "死裡逃生", -"殖谷" => "殖穀", -"残余" => "殘餘", -"杀虫药" => "殺虫藥", -"杀虫药" => "殺蟲藥", -"壳里" => "殼裡", -"殿后" => "殿後", -"殿钟自鸣" => "殿鐘自鳴", -"毁于" => "毀於", -"毁钟为铎" => "毀鐘為鐸", -"母范" => "母範", -"每于" => "每於", -"每只" => "每隻", -"毒药" => "毒藥", -"比划" => "比劃", -"毛姜" => "毛薑", -"毛发" => "毛髮", -"毫厘" => "毫釐", -"毫发" => "毫髮", -"气郁" => "氣鬱", -"氤郁" => "氤鬱", -"氯霉素" => "氯黴素", -"水准" => "水準", -"水里" => "水裡", -"水里乡" => "水里鄉", -"水硷" => "水鹼", -"永历" => "永曆", -"求助于" => "求助於", -"求教于" => "求教於", -"求知欲" => "求知慾", -"求签" => "求籤", -"汗硷" => "汗鹼", -"污蔑" => "汙衊", -"池里" => "池裡", -"污蔑" => "污衊", -"汲于" => "汲於", -"汲汲于" => "汲汲於", -"决斗" => "決鬥", -"沈淀" => "沈澱", -"沈着" => "沈著", -"沈郁" => "沈鬱", -"沉湎于" => "沉湎於", -"沉溺于" => "沉溺於", -"沉淀" => "沉澱", -"沉迷于" => "沉迷於", -"沉醉于" => "沉醉於", -"沉郁" => "沉鬱", -"没干没净" => "沒乾沒淨", -"没事干" => "沒事幹", -"没干" => "沒幹", -"没梢干" => "沒梢幹", -"没样范" => "沒樣範", -"没药" => "沒藥", -"冲冠发怒" => "沖冠髮怒", -"冲着" => "沖著", -"沙里淘金" => "沙裡淘金", -"河流汇集" => "河流匯集", -"河里" => "河裡", -"油漆未干" => "油漆未乾", -"油面" => "油麵", -"泛游" => "泛遊", -"泡面" => "泡麵", -"波棱菜" => "波稜菜", -"波发藻" => "波髮藻", -"泥于" => "泥於", -"注云" => "注云", -"泱郁" => "泱鬱", -"泳气钟" => "泳氣鐘", -"洄游" => "洄遊", -"洋面" => "洋麵", -"洗手不干" => "洗手不幹", -"洗发" => "洗髮", -"洗发精" => "洗髮精", -"洛钟东应" => "洛鐘東應", -"洪范" => "洪範", -"洪钟" => "洪鐘", -"汹涌" => "洶湧", -"活动于" => "活動於", -"派团参加" => "派團參加", -"流于" => "流於", -"流荡" => "流蕩", -"流行于" => "流行於", -"浩荡" => "浩蕩", -"浪琴表" => "浪琴錶", -"浪荡" => "浪蕩", -"浮于" => "浮於", -"浮荡" => "浮蕩", -"浮游" => "浮遊", -"浮松" => "浮鬆", -"海干" => "海乾", -"浸于" => "浸於", -"涂着" => "涂著", -"涂谨申" => "涂謹申", -"消炎药" => "消炎藥", -"消肿药" => "消腫藥", -"涉足于" => "涉足於", -"液晶表" => "液晶錶", -"涳蒙" => "涳濛", -"涸干" => "涸乾", -"凉面" => "涼麵", -"淑范" => "淑範", -"泪干" => "淚乾", -"泪如泉涌" => "淚如泉湧", -"淡于" => "淡於", -"淡蒙蒙" => "淡濛濛", -"淡朱" => "淡硃", -"净余" => "淨餘", -"净发" => "淨髮", -"淫欲" => "淫慾", -"淫荡" => "淫蕩", -"深于" => "深於", -"淳朴" => "淳樸", -"港制" => "港製", -"游荡" => "游蕩", -"浑朴" => "渾樸", -"凑合着" => "湊合著", -"湖里" => "湖裡", -"湘绣" => "湘繡", -"湘累" => "湘纍", -"湟潦生苹" => "湟潦生苹", -"涌上" => "湧上", -"涌来" => "湧來", -"涌入" => "湧入", -"涌出" => "湧出", -"涌向" => "湧向", -"涌泉" => "湧泉", -"涌现" => "湧現", -"涌起" => "湧起", -"涌进" => "湧進", -"湮郁" => "湮鬱", -"汤下面" => "湯下麵", -"汤团" => "湯糰", -"汤药" => "湯藥", -"汤面" => "湯麵", -"源于" => "源於", -"准备" => "準備", -"准儿" => "準兒", -"准则" => "準則", -"准噶尔" => "準噶爾", -"准定" => "準定", -"准平原" => "準平原", -"准度" => "準度", -"准据" => "準據", -"准新娘" => "準新娘", -"准新郎" => "準新郎", -"准星" => "準星", -"准是" => "準是", -"准时" => "準時", -"准会" => "準會", -"准决赛" => "準決賽", -"准的" => "準的", -"准确" => "準確", -"准线" => "準線", -"准绳" => "準繩", -"准话" => "準話", -"准头" => "準頭", -"溟蒙" => "溟濛", -"溢于" => "溢於", -"溢于言表" => "溢於言表", -"溲面" => "溲麵", -"溶于" => "溶於", -"溺于" => "溺於", -"滃郁" => "滃鬱", -"滑藉" => "滑藉", -"汇丰" => "滙豐", -"卤味" => "滷味", -"卤水" => "滷水", -"卤汁" => "滷汁", -"卤湖" => "滷湖", -"卤肉" => "滷肉", -"卤菜" => "滷菜", -"卤蛋" => "滷蛋", -"卤制" => "滷製", -"卤鸡" => "滷雞", -"卤面" => "滷麵", -"满于" => "滿於", -"满满当当" => "滿滿當當", -"漂荡" => "漂蕩", -"沤郁" => "漚鬱", -"汉弥登钟" => "漢彌登鐘", -"漫游" => "漫遊", -"潜水钟" => "潛水鐘", -"潭里" => "潭裡", -"潮涌" => "潮湧", -"溃于" => "潰於", -"澄澹精致" => "澄澹精致", -"澒蒙" => "澒濛", -"泽渗漓而下降" => "澤滲灕而下降", -"淀粉" => "澱粉", -"澹台" => "澹臺", -"激于" => "激於", -"激荡" => "激蕩", -"浓于" => "濃於", -"浓发" => "濃髮", -"蒙汜" => "濛汜", -"蒙蒙细雨" => "濛濛細雨", -"蒙雾" => "濛霧", -"蒙松雨" => "濛鬆雨", -"泻药" => "瀉藥", -"沈吉线" => "瀋吉線", -"沈山线" => "瀋山線", -"沈州" => "瀋州", -"沈水" => "瀋水", -"沈河" => "瀋河", -"沈海" => "瀋海", -"沈海铁路" => "瀋海鐵路", -"沈阳" => "瀋陽", -"濒于" => "瀕於", -"弥山遍野" => "瀰山遍野", -"弥漫" => "瀰漫", -"弥漫着" => "瀰漫著", -"弥弥" => "瀰瀰", -"灌于" => "灌於", -"灌药" => "灌藥", -"漓湘" => "灕湘", -"漓然" => "灕然", -"火并" => "火併", -"火签" => "火籤", -"火药" => "火藥", -"灰蒙" => "灰濛", -"灰蒙蒙" => "灰濛濛", -"炆面" => "炆麵", -"炒面" => "炒麵", -"炮制" => "炮製", -"炸药" => "炸藥", -"炸酱面" => "炸醬麵", -"为准" => "為準", -"为着" => "為著", -"乌发" => "烏髮", -"乌龙面" => "烏龍麵", -"烘干" => "烘乾", -"烘制" => "烘製", -"烤干" => "烤乾", -"焙干" => "焙乾", -"无助于" => "無助於", -"无动于衷" => "無動於衷", -"无可救药" => "無可救藥", -"无后" => "無後", -"无损于" => "無損於", -"无梁楼盖" => "無樑樓蓋", -"无济于事" => "無濟於事", -"无畏于" => "無畏於", -"无补于事" => "無補於事", -"无视于" => "無視於", -"无余" => "無餘", -"然后" => "然後", -"然身死才数月耳" => "然身死纔數月耳", -"炼药" => "煉藥", -"炼制" => "煉製", -"煎药" => "煎藥", -"煎面" => "煎麵", -"烟卷" => "煙捲", -"烟斗丝" => "煙斗絲", -"烟斗" => "煙鬥", -"烟硷" => "煙鹼", -"照占" => "照佔", -"照入签" => "照入籤", -"照准" => "照準", -"煨干" => "煨乾", -"煮面" => "煮麵", -"熔于" => "熔於", -"熨斗" => "熨鬥", -"熬药" => "熬藥", -"热衷于" => "熱衷於", -"炖药" => "燉藥", -"燎发" => "燎髮", -"烧干" => "燒乾", -"烧硷" => "燒鹼", -"燕几" => "燕几", -"燕游" => "燕遊", -"烫发" => "燙髮", -"烫面" => "燙麵", -"营干" => "營幹", -"烬余" => "燼餘", -"争先恐后" => "爭先恐後", -"争奇斗艳" => "爭奇鬥艷", -"争奇斗豔" => "爭奇鬥豔", -"争妍斗胜" => "爭妍鬥勝", -"争妍斗豔" => "爭妍鬥豔", -"争斗" => "爭鬥", -"爰定祥历" => "爰定祥厤", -"爽荡" => "爽蕩", -"尔冬升" => "爾冬陞", -"尔后" => "爾後", -"片言只语" => "片言隻語", -"牙签" => "牙籤", -"牛肉面" => "牛肉麵", -"牛只" => "牛隻", -"物欲" => "物慾", -"特征" => "特徵", -"特效药" => "特效藥", -"特于" => "特於", -"特制" => "特製", -"牵一发" => "牽一髮", -"牵系" => "牽繫", -"荦确" => "犖确", -"狂并潮" => "狂併潮", -"狃于" => "狃於", -"狃于成见" => "狃於成見", -"狐藉虎威" => "狐藉虎威", -"狼藉" => "狼藉", -"猛于" => "猛於", -"猛冲" => "猛衝", -"猜三划五" => "猜三划五", -"奖杯" => "獎盃", -"独占" => "獨佔", -"独占鳌头" => "獨佔鰲頭", -"兽欲" => "獸慾", -"献丑" => "獻醜", -"率团参加" => "率團參加", -"玉历" => "玉曆", -"王庄" => "王莊", -"王余鱼" => "王餘魚", -"班里" => "班裡", -"理发" => "理髮", -"琴钟" => "琴鐘", -"瑶签" => "瑤籤", -"环游" => "環遊", -"甘于" => "甘於", -"甚于" => "甚於", -"甚么" => "甚麼", -"甜水面" => "甜水麵", -"甜面酱" => "甜麵醬", -"生力面" => "生力麵", -"生于" => "生於", -"生姜" => "生薑", -"生锈" => "生鏽", -"生发" => "生髮", -"产后" => "產後", -"用于" => "用於", -"用药" => "用藥", -"甩发" => "甩髮", -"田谷" => "田穀", -"田庄" => "田莊", -"田里" => "田裡", -"由于" => "由於", -"男仆" => "男僕", -"男用表" => "男用錶", -"畏于" => "畏於", -"留后" => "留後", -"留发" => "留髮", -"毕于" => "畢於", -"毕业于" => "畢業於", -"毕生发展" => "畢生發展", -"划着" => "畫著", -"异于" => "異於", -"当一天和尚撞一天钟" => "當一天和尚撞一天鐘", -"当家才知柴米价" => "當家纔知柴米價", -"当于" => "當於", -"当当丁丁" => "當當丁丁", -"当着" => "當著", -"疏于" => "疏於", -"疏松" => "疏鬆", -"疑凶" => "疑兇", -"疲于" => "疲於", -"疲于奔命" => "疲於奔命", -"疲困" => "疲睏", -"病后" => "病後", -"病征" => "病徵", -"病症" => "病癥", -"症候" => "癥候", -"症状" => "癥狀", -"症结" => "癥結", -"发干" => "發乾", -"发于" => "發於", -"发汗药" => "發汗藥", -"发呆" => "發獃", -"发签" => "發籤", -"发着" => "發著", -"发松" => "發鬆", -"发面" => "發麵", -"白干" => "白乾", -"白术" => "白朮", -"白朴" => "白樸", -"白发其事" => "白發其事", -"白粉面" => "白粉麵", -"白发" => "白髮", -"白霉" => "白黴", -"百多只" => "百多隻", -"百天后" => "百天後", -"百拙千丑" => "百拙千醜", -"百扎" => "百紮", -"百花历" => "百花曆", -"百只" => "百隻", -"皇历" => "皇曆", -"皇极历" => "皇極曆", -"皇庄" => "皇莊", -"皓发" => "皓髮", -"皮里阳秋" => "皮裏陽秋", -"皮里春秋" => "皮裡春秋", -"皮制" => "皮製", -"皮松" => "皮鬆", -"皱别" => "皺彆", -"皱折" => "皺摺", -"盈余" => "盈餘", -"益于" => "益於", -"盒里" => "盒裡", -"盛德遗范" => "盛德遺範", -"盛行于" => "盛行於", -"盛赞" => "盛讚", -"盗采" => "盜採", -"盗钟" => "盜鐘", -"监制" => "監製", -"盘里" => "盤裡", -"盘回" => "盤迴", -"卢棱伽" => "盧稜伽", -"盲干" => "盲幹", -"直接参与" => "直接參与", -"直于" => "直於", -"直冲" => "直衝", -"相并" => "相併", -"相克" => "相剋", -"相同于" => "相同於", -"相干" => "相干", -"相于" => "相於", -"相冲" => "相衝", -"相斗" => "相鬥", -"看准" => "看準", -"真凶" => "真兇", -"眼帘" => "眼帘", -"眼眶里" => "眼眶裡", -"眼药" => "眼藥", -"眼里" => "眼裡", -"困乏" => "睏乏", -"困觉" => "睏覺", -"睡着了" => "睡著了", -"瞄准" => "瞄準", -"瞠乎后矣" => "瞠乎後矣", -"了望" => "瞭望", -"了然" => "瞭然", -"了若指掌" => "瞭若指掌", -"瞻前顾后" => "瞻前顧後", -"蒙事" => "矇事", -"蒙昧无知" => "矇昧無知", -"蒙松雨" => "矇松雨", -"蒙混" => "矇混", -"蒙瞍" => "矇瞍", -"蒙眬" => "矇矓", -"蒙聩" => "矇聵", -"蒙着" => "矇著", -"蒙着锅儿" => "矇著鍋兒", -"蒙头转" => "矇頭轉", -"蒙骗" => "矇騙", -"瞩讬" => "矚託", -"短于" => "短於", -"短发" => "短髮", -"石棱棱" => "石稜稜", -"石英表" => "石英錶", -"石钟乳" => "石鐘乳", -"石硷" => "石鹼", -"研制" => "研製", -"砰当" => "砰噹", -"朱唇皓齿" => "硃唇皓齒", -"朱批" => "硃批", -"朱砂" => "硃砂", -"朱笔" => "硃筆", -"朱红色" => "硃紅色", -"朱色" => "硃色", -"硫化硷" => "硫化鹼", -"硬干" => "硬幹", -"确瘠" => "确瘠", -"碑志" => "碑誌", -"碰钟" => "碰鐘", -"磁制" => "磁製", -"磨制" => "磨製", -"硗确" => "磽确", -"碍于" => "礙於", -"砻谷机" => "礱穀機", -"示范" => "示範", -"社里" => "社裡", -"祝发" => "祝髮", -"神荼郁垒" => "神荼鬱壘", -"神游" => "神遊", -"神雕像" => "神雕像", -"神雕" => "神鵰", -"祭吊" => "祭弔", -"禁欲" => "禁慾", -"禁药" => "禁藥", -"祸于" => "禍於", -"御侮" => "禦侮", -"御寇" => "禦寇", -"御寒" => "禦寒", -"御敌" => "禦敵", -"礼赞" => "禮讚", -"禾谷" => "禾穀", -"秃妃之发" => "禿妃之髮", -"秃发" => "禿髮", -"秀发" => "秀髮", -"私下里" => "私下裡", -"私欲" => "私慾", -"私斗" => "私鬥", -"秋天里" => "秋天裡", -"秋后" => "秋後", -"秋裤" => "秋褲", -"秋游" => "秋遊", -"秋阴入井干" => "秋陰入井幹", -"秋发" => "秋髮", -"种师中" => "种師中", -"种师道" => "种師道", -"种放" => "种放", -"科范" => "科範", -"秒表" => "秒錶", -"秒钟" => "秒鐘", -"稀松" => "稀鬆", -"稍后" => "稍後", -"棱台" => "稜台", -"棱子" => "稜子", -"棱层" => "稜層", -"棱柱" => "稜柱", -"棱棱" => "稜稜", -"棱棱睁睁" => "稜稜睜睜", -"棱等登" => "稜等登", -"棱线" => "稜線", -"棱缝" => "稜縫", -"棱角" => "稜角", -"棱锥" => "稜錐", -"棱镜" => "稜鏡", -"棱体" => "稜體", -"种谷" => "種穀", -"称赞" => "稱讚", -"稻谷" => "稻穀", -"稽征" => "稽徵", -"谷仓" => "穀倉", -"谷圭" => "穀圭", -"谷场" => "穀場", -"谷子" => "穀子", -"谷日" => "穀日", -"谷旦" => "穀旦", -"谷壳" => "穀殼", -"谷物" => "穀物", -"谷皮" => "穀皮", -"谷神" => "穀神", -"谷米" => "穀米", -"谷粒" => "穀粒", -"谷舱" => "穀艙", -"谷苗" => "穀苗", -"谷草" => "穀草", -"谷贵饿农" => "穀貴餓農", -"谷贱伤农" => "穀賤傷農", -"谷道" => "穀道", -"谷雨" => "穀雨", -"谷类" => "穀類", -"谷食" => "穀食", -"穆罕默德历" => "穆罕默德曆", -"积极参与" => "積极參与", -"积极参加" => "積极參加", -"积谷" => "積穀", -"积郁" => "積鬱", -"稳扎" => "穩紮", -"空荡" => "空蕩", -"空钟" => "空鐘", -"空余" => "空餘", -"窗帘" => "窗帘", -"窗明几净" => "窗明几淨", -"窗台" => "窗檯", -"窝里" => "窩裡", -"穷于" => "窮於", -"穷追不舍" => "窮追不捨", -"窃钟掩耳" => "竊鐘掩耳", -"立于" => "立於", -"立范" => "立範", -"站干岸儿" => "站乾岸兒", -"竟于" => "竟於", -"童仆" => "童僕", -"端庄" => "端莊", -"竞斗" => "競鬥", -"竹签" => "竹籤", -"笆斗" => "笆鬥", -"笑里藏刀" => "笑裡藏刀", -"笔划" => "筆劃", -"等同于" => "等同於", -"等于" => "等於", -"筋斗" => "筋鬥", -"笋干" => "筍乾", -"筑前" => "筑前", -"筑北" => "筑北", -"筑州" => "筑州", -"筑后" => "筑後", -"筑波" => "筑波", -"筑紫" => "筑紫", -"筑肥" => "筑肥", -"筑西" => "筑西", -"筑邦" => "筑邦", -"筑阳" => "筑陽", -"答复" => "答覆", -"策划" => "策劃", -"筵几" => "筵几", -"箕斗" => "箕鬥", -"算发" => "算髮", -"管干" => "管幹", -"节欲" => "節慾", -"节余" => "節餘", -"范例" => "範例", -"范围" => "範圍", -"范式" => "範式", -"范文" => "範文", -"范本" => "範本", -"范畴" => "範疇", -"简朴" => "簡樸", -"签着" => "簽著", -"筹划" => "籌劃", -"签幐" => "籤幐", -"签押" => "籤押", -"签条" => "籤條", -"签诗" => "籤詩", -"吁天" => "籲天", -"吁求" => "籲求", -"吁请" => "籲請", -"米谷" => "米穀", -"粉拳绣腿" => "粉拳繡腿", -"粉签子" => "粉籤子", -"粗制" => "粗製", -"精干" => "精幹", -"精采" => "精採", -"精于" => "精於", -"精明强干" => "精明強幹", -"精准" => "精準", -"精致" => "精緻", -"精制" => "精製", -"精通于" => "精通於", -"精辟" => "精闢", -"精松" => "精鬆", -"糊里糊涂" => "糊裡糊塗", -"糕干" => "糕乾", -"粪秽蔑面" => "糞穢衊面", -"团子" => "糰子", -"系着" => "系著", -"纪元后" => "紀元後", -"纪历" => "紀曆", -"红发" => "紅髮", -"红霉素" => "紅黴素", -"纡回" => "紆迴", -"纡余" => "紆餘", -"纡郁" => "紆鬱", -"纯朴" => "純樸", -"纯硷" => "純鹼", -"纸扎" => "紙紮", -"素朴" => "素樸", -"素藉" => "素藉", -"素食面" => "素食麵", -"素面" => "素麵", -"索面" => "索麵", -"紫姜" => "紫薑", -"扎上" => "紮上", -"扎下" => "紮下", -"扎好" => "紮好", -"扎实" => "紮實", -"扎寨" => "紮寨", -"扎带子" => "紮帶子", -"扎成" => "紮成", -"扎根" => "紮根", -"扎营" => "紮營", -"扎紧" => "紮緊", -"扎起" => "紮起", -"扎铁" => "紮鐵", -"细不容发" => "細不容髮", -"细致" => "細緻", -"终于" => "終於", -"组里" => "組裡", -"结伴同游" => "結伴同遊", -"结伙" => "結夥", -"结扎" => "結紮", -"结余" => "結餘", -"结发" => "結髮", -"绝对参照" => "絕對參照", -"绝后" => "絕後", -"绝于" => "絕於", -"绞干" => "絞乾", -"络绎于途" => "絡繹於途", -"给于" => "給於", -"丝来线去" => "絲來線去", -"丝布" => "絲布", -"丝恩发怨" => "絲恩髮怨", -"丝板" => "絲板", -"丝瓜布" => "絲瓜布", -"丝绒布" => "絲絨布", -"丝线" => "絲線", -"丝织厂" => "絲織廠", -"丝虫" => "絲蟲", -"丝发" => "絲髮", -"绑扎" => "綁紮", -"綑扎" => "綑紮", -"绿发" => "綠髮", -"绿霉素" => "綠黴素", -"绸缎庄" => "綢緞莊", -"维系" => "維繫", -"绾发" => "綰髮", -"网里" => "網裡", -"网志" => "網誌", -"紧绷" => "緊繃", -"紧绷着" => "緊繃著", -"紧追不舍" => "緊追不捨", -"绪余" => "緒餘", -"缉凶" => "緝兇", -"编制" => "編製", -"编钟" => "編鐘", -"编余" => "編餘", -"编发" => "編髮", -"缓征" => "緩徵", -"缓冲" => "緩衝", -"致密" => "緻密", -"萦回" => "縈迴", -"缜致" => "縝緻", -"县里" => "縣裡", -"县志" => "縣誌", -"缝里" => "縫裡", -"缝制" => "縫製", -"缩栗" => "縮慄", -"纵欲" => "縱慾", -"纤夫" => "縴夫", -"繁复" => "繁複", -"绷住" => "繃住", -"绷子" => "繃子", -"绷带" => "繃帶", -"绷紧" => "繃緊", -"绷脸" => "繃臉", -"绷着" => "繃著", -"绷着脸" => "繃著臉", -"绷着脸儿" => "繃著臉兒", -"绷开" => "繃開", -"繐帏飘井干" => "繐幃飄井幹", -"绕梁" => "繞樑", -"绣得" => "繡得", -"绣房" => "繡房", -"绣毯" => "繡毯", -"绣球" => "繡球", -"绣的" => "繡的", -"绣花" => "繡花", -"绣衣" => "繡衣", -"绣起" => "繡起", -"绣阁" => "繡閣", -"绣鞋" => "繡鞋", -"绘制" => "繪製", -"系上" => "繫上", -"系到" => "繫到", -"系囚" => "繫囚", -"系心" => "繫心", -"系念" => "繫念", -"系怀" => "繫懷", -"系于" => "繫於", -"系系" => "繫系", -"系紧" => "繫緊", -"系绳" => "繫繩", -"系着" => "繫著", -"系辞" => "繫辭", -"累囚" => "纍囚", -"累堆" => "纍堆", -"累瓦结绳" => "纍瓦結繩", -"累绁" => "纍紲", -"累臣" => "纍臣", -"缠斗" => "纏鬥", -"才则" => "纔則", -"才可容颜十五余" => "纔可容顏十五餘", -"才得两年" => "纔得兩年", -"才此" => "纔此", -"坛子" => "罈子", -"坛坛罐罐" => "罈罈罐罐", -"坛騞" => "罈騞", -"置于" => "置於", -"骂着" => "罵著", -"美仑" => "美崙", -"美于" => "美於", -"美制" => "美製", -"美丑" => "美醜", -"美发" => "美髮", -"羞于" => "羞於", -"群丑" => "群醜", -"羨余" => "羨餘", -"义仆" => "義僕", -"义形于色" => "義形於色", -"义庄" => "義莊", -"习于" => "習於", -"翕辟" => "翕闢", -"翱游" => "翱遊", -"翻涌" => "翻湧", -"翻云复雨" => "翻雲覆雨", -"翻松" => "翻鬆", -"老干" => "老乾", -"老仆" => "老僕", -"老干部" => "老幹部", -"老蒙" => "老懞", -"老于" => "老於", -"老于世故" => "老於世故", -"老庄" => "老莊", -"老姜" => "老薑", -"老板" => "老闆", -"考后" => "考後", -"而后" => "而後", -"而于" => "而於", -"耐硷" => "耐鹼", -"耕佣" => "耕傭", -"耕获" => "耕穫", -"耳后" => "耳後", -"耳余" => "耳餘", -"耽于" => "耽於", -"耿于" => "耿於", -"耿耿于怀" => "耿耿於懷", -"聊斋志异" => "聊齋志異", -"联系" => "聯係", -"联于" => "聯於", -"联系" => "聯繫", -"声如洪钟" => "聲如洪鐘", -"听于" => "聽於", -"肉干" => "肉乾", -"肉欲" => "肉慾", -"肉丝面" => "肉絲麵", -"肉羹面" => "肉羹麵", -"肉松" => "肉鬆", -"肝郁" => "肝鬱", -"股栗" => "股慄", -"肥筑方言" => "肥筑方言", -"胃药" => "胃藥", -"背向着" => "背向著", -"背地里" => "背地裡", -"背后" => "背後", -"胎发" => "胎髮", -"胜肽" => "胜肽", -"胜键" => "胜鍵", -"胡云" => "胡云", -"胡朴安" => "胡樸安", -"胡里胡涂" => "胡裡胡塗", -"能干" => "能幹", -"脉冲" => "脈衝", -"脊梁" => "脊樑", -"脱谷机" => "脫穀機", -"脱发" => "脫髮", -"腊味" => "腊味", -"腊笔" => "腊筆", -"腐干" => "腐乾", -"脑子里" => "腦子裡", -"脑干" => "腦幹", -"脑后" => "腦後", -"腰里" => "腰裡", -"脚注" => "腳註", -"膏药" => "膏藥", -"臣仆" => "臣僕", -"臣服于" => "臣服於", -"卧游" => "臥遊", -"臧谷亡羊" => "臧穀亡羊", -"自于" => "自於", -"自制" => "自製", -"自觉自愿" => "自覺自愿", -"自鸣钟" => "自鳴鐘", -"至于" => "至於", -"致力于" => "致力於", -"致于" => "致於", -"臻于" => "臻於", -"臻于完善" => "臻於完善", -"舂谷" => "舂穀", -"兴致" => "興緻", -"举手表" => "舉手表", -"旧庄" => "舊庄", -"旧历" => "舊曆", -"旧药" => "舊藥", -"舌干脣焦" => "舌乾脣焦", -"舌后" => "舌後", -"舒卷" => "舒捲", -"航海历" => "航海曆", -"船只" => "船隻", -"舰只" => "艦隻", -"良药" => "良藥", -"色欲" => "色慾", -"艸木丰丰" => "艸木丰丰", -"芍药" => "芍藥", -"芒果干" => "芒果乾", -"花拳绣腿" => "花拳繡腿", -"花卷" => "花捲", -"花盆里" => "花盆裡", -"花药" => "花藥", -"花哄" => "花鬨", -"苑里" => "苑裡", -"苛性硷" => "苛性鹼", -"若于" => "若於", -"苦干" => "苦幹", -"苦于" => "苦於", -"苦药" => "苦藥", -"苦里" => "苦裏", -"苦斗" => "苦鬥", -"苹萦" => "苹縈", -"茵藉" => "茵藉", -"茶几" => "茶几", -"茶庄" => "茶莊", -"茶余" => "茶餘", -"茶面" => "茶麵", -"草丛里" => "草叢裡", -"草药" => "草藥", -"荷花淀" => "荷花澱", -"庄主" => "莊主", -"庄周" => "莊周", -"庄员" => "莊員", -"庄严" => "莊嚴", -"庄园" => "莊園", -"庄子" => "莊子", -"庄家" => "莊家", -"庄户" => "莊戶", -"庄敬" => "莊敬", -"庄田" => "莊田", -"庄稼" => "莊稼", -"庄里" => "莊裡", -"庄重" => "莊重", -"庄院" => "莊院", -"茎干" => "莖幹", -"莽荡" => "莽蕩", -"菌丝体" => "菌絲體", -"菜干" => "菜乾", -"菠棱菜" => "菠稜菜", -"菠萝干" => "菠蘿乾", -"华发" => "華髮", -"菸硷" => "菸鹼", -"万多只" => "萬多隻", -"万天后" => "萬天後", -"万历" => "萬曆", -"万签插架" => "萬籤插架", -"万扎" => "萬紮", -"万象" => "萬象", -"万只" => "萬隻", -"万余" => "萬餘", -"落后" => "落後", -"落于" => "落於", -"落发" => "落髮", -"着儿" => "著兒", -"着书立说" => "著書立說", -"着色软体" => "著色軟體", -"着重指出" => "著重指出", -"着录" => "著錄", -"着录规则" => "著錄規則", -"葡萄干" => "葡萄乾", -"董氏封发" => "董氏封髮", -"蒙汗药" => "蒙汗藥", -"蒙雾露" => "蒙霧露", -"蒜发" => "蒜髮", -"苍术" => "蒼朮", -"苍郁" => "蒼鬱", -"蓄发" => "蓄髮", -"蓄须" => "蓄鬚", -"蓊郁" => "蓊鬱", -"盖于" => "蓋於", -"蓬发" => "蓬髮", -"蓬松" => "蓬鬆", -"参绥" => "蔘綏", -"荞麦面" => "蕎麥麵", -"荡来荡去" => "蕩來蕩去", -"荡女" => "蕩女", -"荡妇" => "蕩婦", -"荡寇" => "蕩寇", -"荡平" => "蕩平", -"荡涤" => "蕩滌", -"荡漾" => "蕩漾", -"荡然" => "蕩然", -"荡舟" => "蕩舟", -"荡船" => "蕩船", -"荡荡" => "蕩蕩", -"萧参" => "蕭蔘", -"薄幸" => "薄倖", -"薄干" => "薄幹", -"姜是老的辣" => "薑是老的辣", -"姜末" => "薑末", -"姜桂" => "薑桂", -"姜母" => "薑母", -"姜汁" => "薑汁", -"姜汤" => "薑湯", -"姜片" => "薑片", -"姜糖" => "薑糖", -"姜丝" => "薑絲", -"姜老辣" => "薑老辣", -"姜蓉" => "薑蓉", -"姜饼" => "薑餅", -"姜黄" => "薑黃", -"薙发" => "薙髮", -"薝卜" => "薝蔔", -"藉以" => "藉以", -"藉卉" => "藉卉", -"藉寇兵" => "藉寇兵", -"藉手" => "藉手", -"藉槁" => "藉槁", -"藉机" => "藉機", -"藉此" => "藉此", -"藉甚" => "藉甚", -"藉由" => "藉由", -"藉箸代筹" => "藉箸代籌", -"藉草枕块" => "藉草枕塊", -"藉着" => "藉著", -"藉藉" => "藉藉", -"藉资" => "藉資", -"藏匿于" => "藏匿於", -"藏于" => "藏於", -"藏历" => "藏曆", -"藏蒙歌儿" => "藏矇歌兒", -"藤制" => "藤製", -"药丸" => "藥丸", -"药典" => "藥典", -"药到病除" => "藥到病除", -"药剂" => "藥劑", -"药力" => "藥力", -"药包" => "藥包", -"药名" => "藥名", -"药味" => "藥味", -"药品" => "藥品", -"药商" => "藥商", -"药单" => "藥單", -"药婆" => "藥婆", -"药学" => "藥學", -"药害" => "藥害", -"药专" => "藥專", -"药局" => "藥局", -"药师" => "藥師", -"药店" => "藥店", -"药厂" => "藥廠", -"药引" => "藥引", -"药性" => "藥性", -"药房" => "藥房", -"药效" => "藥效", -"药方" => "藥方", -"药材" => "藥材", -"药棉" => "藥棉", -"药水" => "藥水", -"药油" => "藥油", -"药液" => "藥液", -"药渣" => "藥渣", -"药片" => "藥片", -"药物" => "藥物", -"药王" => "藥王", -"药理" => "藥理", -"药瓶" => "藥瓶", -"药用" => "藥用", -"药皂" => "藥皂", -"药盒" => "藥盒", -"药石" => "藥石", -"药科" => "藥科", -"药箱" => "藥箱", -"药签" => "藥籤", -"药粉" => "藥粉", -"药糖" => "藥糖", -"药线" => "藥線", -"药罐" => "藥罐", -"药膏" => "藥膏", -"药舖" => "藥舖", -"药茶" => "藥茶", -"药草" => "藥草", -"药行" => "藥行", -"药贩" => "藥販", -"药费" => "藥費", -"药酒" => "藥酒", -"药量" => "藥量", -"药针" => "藥針", -"药铺" => "藥鋪", -"药头" => "藥頭", -"药饵" => "藥餌", -"药面儿" => "藥麵兒", -"苏昆" => "蘇崑", -"蕴含着" => "蘊含著", -"蕴涵着" => "蘊涵著", -"蕴藉" => "蘊藉", -"苹果干" => "蘋果乾", -"萝卜" => "蘿蔔", -"萝卜干" => "蘿蔔乾", -"虎须" => "虎鬚", -"处于" => "處於", -"号志" => "號誌", -"虫部" => "虫部", -"蛇发女妖" => "蛇髮女妖", -"蛔虫药" => "蛔蟲藥", -"蜂涌" => "蜂湧", -"蛏干" => "蟶乾", -"蛮干" => "蠻幹", -"血余" => "血餘", -"行事历" => "行事曆", -"行凶" => "行兇", -"行凶后" => "行兇後", -"行于" => "行於", -"胡同" => "衚衕", -"冲上" => "衝上", -"冲下" => "衝下", -"冲来" => "衝來", -"冲倒" => "衝倒", -"冲出" => "衝出", -"冲到" => "衝到", -"冲刺" => "衝刺", -"冲克" => "衝剋", -"冲力" => "衝力", -"冲劲" => "衝勁", -"冲动" => "衝動", -"冲去" => "衝去", -"冲口" => "衝口", -"冲垮" => "衝垮", -"冲堂" => "衝堂", -"冲坚陷阵" => "衝堅陷陣", -"冲压" => "衝壓", -"冲天" => "衝天", -"冲州撞府" => "衝州撞府", -"冲心" => "衝心", -"冲掉" => "衝掉", -"冲撞" => "衝撞", -"冲击" => "衝擊", -"冲散" => "衝散", -"冲杀" => "衝殺", -"冲决" => "衝決", -"冲波" => "衝波", -"冲浪" => "衝浪", -"冲激" => "衝激", -"冲然" => "衝然", -"冲盹" => "衝盹", -"冲破" => "衝破", -"冲程" => "衝程", -"冲突" => "衝突", -"冲线" => "衝線", -"冲着" => "衝著", -"冲要" => "衝要", -"冲起" => "衝起", -"冲车" => "衝車", -"冲进" => "衝進", -"冲过" => "衝過", -"冲量" => "衝量", -"冲锋" => "衝鋒", -"冲陷" => "衝陷", -"冲头阵" => "衝頭陣", -"冲风" => "衝風", -"表征" => "表徵", -"表里" => "表裡", -"衷于" => "衷於", -"袖里" => "袖裡", -"被里" => "被裡", -"被复" => "被複", -"被复着" => "被覆著", -"裁并" => "裁併", -"裁制" => "裁製", -"里勾外连" => "裏勾外連", -"里手" => "裏手", -"里海" => "裏海", -"里面" => "裏面", -"补药" => "補藥", -"补血药" => "補血藥", -"补注" => "補註", -"里外" => "裡外", -"里子" => "裡子", -"里屋" => "裡屋", -"里层" => "裡層", -"里布" => "裡布", -"里带" => "裡帶", -"里弦" => "裡弦", -"里应外合" => "裡應外合", -"里脊" => "裡脊", -"里衣" => "裡衣", -"里通外国" => "裡通外國", -"里通外敌" => "裡通外敵", -"里边" => "裡邊", -"里间" => "裡間", -"里面" => "裡面", -"里头" => "裡頭", -"制件" => "製件", -"制作" => "製作", -"制做" => "製做", -"制备" => "製備", -"制冰" => "製冰", -"制冷" => "製冷", -"制剂" => "製劑", -"制品" => "製品", -"制图" => "製圖", -"制成" => "製成", -"制法" => "製法", -"制为" => "製為", -"制片" => "製片", -"制版" => "製版", -"制程" => "製程", -"制糖" => "製糖", -"制纸" => "製紙", -"制药" => "製藥", -"制表" => "製表", -"制裁" => "製裁", -"制造" => "製造", -"制革" => "製革", -"制鞋" => "製鞋", -"制盐" => "製鹽", -"复仞年如" => "複仞年如", -"复以百万" => "複以百萬", -"复位" => "複位", -"复信" => "複信", -"复元音" => "複元音", -"复分数" => "複分數", -"复分析" => "複分析", -"复列" => "複列", -"复利" => "複利", -"复印" => "複印", -"复原" => "複原", -"复句" => "複句", -"复合" => "複合", -"复名" => "複名", -"复员" => "複員", -"复壁" => "複壁", -"复壮" => "複壯", -"复姓" => "複姓", -"复字键" => "複字鍵", -"复审" => "複審", -"复写" => "複寫", -"复平面" => "複平面", -"复式" => "複式", -"复复" => "複復", -"复数" => "複數", -"复本" => "複本", -"复查" => "複查", -"复核" => "複核", -"复检" => "複檢", -"复次" => "複次", -"复比" => "複比", -"复决" => "複決", -"复流" => "複流", -"复测" => "複測", -"复亩珍" => "複畝珍", -"复发" => "複發", -"复目" => "複目", -"复眼" => "複眼", -"复种" => "複種", -"复线" => "複線", -"复习" => "複習", -"复色" => "複色", -"复叶" => "複葉", -"复盖" => "複蓋", -"复制" => "複製", -"复诊" => "複診", -"复词" => "複詞", -"复试" => "複試", -"复课" => "複課", -"复议" => "複議", -"复变函数" => "複變函數", -"复赛" => "複賽", -"复辅音" => "複輔音", -"复述" => "複述", -"复选" => "複選", -"复钱" => "複錢", -"复杂" => "複雜", -"复电" => "複電", -"复音" => "複音", -"复韵" => "複韻", -"衬里" => "襯裡", -"西岳" => "西嶽", -"西历" => "西曆", -"西历史" => "西歷史", -"西药" => "西藥", -"西游" => "西遊", -"要冲" => "要衝", -"要么" => "要麼", -"复亡" => "覆亡", -"复命" => "覆命", -"复巢之下无完卵" => "覆巢之下無完卵", -"复水难收" => "覆水難收", -"复没" => "覆沒", -"复着" => "覆著", -"复盖" => "覆蓋", -"复盖着" => "覆蓋著", -"复辙" => "覆轍", -"复雨翻云" => "覆雨翻雲", -"见于" => "見於", -"见棱见角" => "見稜見角", -"见素抱朴" => "見素抱樸", -"见钟不打" => "見鐘不打", -"规划" => "規劃", -"规范" => "規範", -"视于" => "視於", -"观采" => "觀採", -"角落发" => "角落發", -"角落里" => "角落裡", -"觚棱" => "觚稜", -"解雇" => "解僱", -"解痛药" => "解痛藥", -"解药" => "解藥", -"解发佯狂" => "解髮佯狂", -"触须" => "觸鬚", -"言大而夸" => "言大而夸", -"言辩而确" => "言辯而确", -"订于" => "訂於", -"订制" => "訂製", -"计划" => "計劃", -"讬了" => "託了", -"讬事" => "託事", -"讬交" => "託交", -"讬人" => "託人", -"讬付" => "託付", -"讬古讽今" => "託古諷今", -"讬名" => "託名", -"讬咎" => "託咎", -"讬梦" => "託夢", -"讬大" => "託大", -"讬孤" => "託孤", -"讬故" => "託故", -"讬疾" => "託疾", -"讬病" => "託病", -"讬福" => "託福", -"讬管" => "託管", -"讬言" => "託言", -"讬词" => "託詞", -"讬买" => "託買", -"讬卖" => "託賣", -"讬身" => "託身", -"讬辞" => "託辭", -"讬运" => "託運", -"讬过" => "託過", -"设于" => "設於", -"许愿起经" => "許愿起經", -"诉说着" => "訴說著", -"注上" => "註上", -"注册" => "註冊", -"注失" => "註失", -"注定" => "註定", -"注明" => "註明", -"注标" => "註標", -"注生娘娘" => "註生娘娘", -"注疏" => "註疏", -"注脚" => "註腳", -"注解" => "註解", -"注译" => "註譯", -"注释" => "註釋", -"注销" => "註銷", -"评注" => "評註", -"词干" => "詞幹", -"词汇" => "詞彙", -"词余" => "詞餘", -"询于" => "詢於", -"诗云" => "詩云", -"诗钟" => "詩鐘", -"诗余" => "詩餘", -"话里有话" => "話裡有話", -"该于" => "該於", -"详注" => "詳註", -"夸赞" => "誇讚", -"志哀" => "誌哀", -"志喜" => "誌喜", -"志庆" => "誌慶", -"志异" => "誌異", -"诱奸" => "誘姦", -"语云" => "語云", -"语汇" => "語彙", -"诚征" => "誠徵", -"诚朴" => "誠樸", -"诬蔑" => "誣衊", -"说着" => "說著", -"课后" => "課後", -"课征" => "課徵", -"课余" => "課餘", -"调准" => "調準", -"调制" => "調製", -"请参阅" => "請參閱", -"请讬" => "請託", -"诸余" => "諸餘", -"谋干" => "謀幹", -"谢绝参观" => "謝絕參觀", -"谬采虚声" => "謬採虛聲", -"謷丑" => "謷醜", -"证于" => "證於", -"警世钟" => "警世鐘", -"警钟" => "警鐘", -"译注" => "譯註", -"护发" => "護髮", -"读后" => "讀後", -"变丑" => "變醜", -"雠隙" => "讎隙", -"赞不绝口" => "讚不絕口", -"赞佩" => "讚佩", -"赞同" => "讚同", -"赞叹" => "讚嘆", -"赞扬" => "讚揚", -"赞乐" => "讚樂", -"赞歌" => "讚歌", -"赞歎" => "讚歎", -"赞美" => "讚美", -"赞羨" => "讚羨", -"赞许" => "讚許", -"赞词" => "讚詞", -"赞誉" => "讚譽", -"赞赏" => "讚賞", -"赞辞" => "讚辭", -"赞颂" => "讚頌", -"豆干" => "豆乾", -"豆腐干" => "豆腐乾", -"竖着" => "豎著", -"丰滨" => "豐濱", -"丰滨乡" => "豐濱鄉", -"象征" => "象徵", -"象征着" => "象徵著", -"负债累累" => "負債纍纍", -"贪欲" => "貪慾", -"贵干" => "貴幹", -"买凶" => "買兇", -"贻范" => "貽範", -"贾后" => "賈後", -"赈饥" => "賑饑", -"质朴" => "質樸", -"赌台" => "賭檯", -"赖于" => "賴於", -"賸余" => "賸餘", -"购并" => "購併", -"购买欲" => "購買慾", -"赢余" => "贏餘", -"走后" => "走後", -"起因于" => "起因於", -"起复" => "起複", -"起哄" => "起鬨", -"赶制" => "趕製", -"赶面棍" => "趕麵棍", -"赵庄" => "趙莊", -"趋于" => "趨於", -"趱干" => "趲幹", -"足于" => "足於", -"跌荡" => "跌蕩", -"跟前跟后" => "跟前跟後", -"路签" => "路籤", -"跳荡" => "跳蕩", -"跳表" => "跳錶", -"蹭棱子" => "蹭稜子", -"躁郁" => "躁鬱", -"躏藉" => "躪藉", -"身后" => "身後", -"身于" => "身於", -"躯干" => "軀幹", -"车库里" => "車庫裡", -"车站里" => "車站裡", -"车里" => "車裡", -"轨范" => "軌範", -"轩辟" => "軒闢", -"载于" => "載於", -"挽曲" => "輓曲", -"挽歌" => "輓歌", -"挽联" => "輓聯", -"挽词" => "輓詞", -"挽诗" => "輓詩", -"轻于" => "輕於", -"轻松" => "輕鬆", -"轮奸" => "輪姦", -"轮回" => "輪迴", -"转台" => "轉檯", -"转讬" => "轉託", -"辟谷" => "辟穀", -"办公台" => "辦公檯", -"辞汇" => "辭彙", -"辫发" => "辮髮", -"农历" => "農曆", -"农民历" => "農民曆", -"农庄" => "農莊", -"农药" => "農藥", -"迂回" => "迂迴", -"近似于" => "近似於", -"近于" => "近於", -"近日里" => "近日裡", -"返朴" => "返樸", -"迥然回异" => "迥然迴異", -"迫于" => "迫於", -"回光返照" => "迴光返照", -"回向" => "迴向", -"回圈" => "迴圈", -"回廊" => "迴廊", -"回形夹" => "迴形夾", -"回文" => "迴文", -"回旋" => "迴旋", -"回流" => "迴流", -"回环" => "迴環", -"回荡" => "迴盪", -"回纹针" => "迴紋針", -"回绕" => "迴繞", -"回肠" => "迴腸", -"回荡" => "迴蕩", -"回诵" => "迴誦", -"回路" => "迴路", -"回转" => "迴轉", -"回递性" => "迴遞性", -"回避" => "迴避", -"回响" => "迴響", -"回风" => "迴風", -"迷幻药" => "迷幻藥", -"迷于" => "迷於", -"迷蒙" => "迷濛", -"迷药" => "迷藥", -"迷魂药" => "迷魂藥", -"追凶" => "追兇", -"退后" => "退後", -"退烧药" => "退燒藥", -"逋发" => "逋髮", -"透辟" => "透闢", -"这里" => "這裏", -"这里" => "這裡", -"这只" => "這隻", -"这么" => "這麼", -"这么着" => "這麼著", -"通奸" => "通姦", -"通心面" => "通心麵", -"通于" => "通於", -"通历" => "通曆", -"速食面" => "速食麵", -"连三并四" => "連三併四", -"连占" => "連佔", -"连采" => "連採", -"连于" => "連於", -"连系" => "連繫", -"连庄" => "連莊", -"周游" => "週遊", -"进占" => "進佔", -"逼并" => "逼併", -"游了" => "遊了", -"游人" => "遊人", -"游伴" => "遊伴", -"游侠" => "遊俠", -"游动" => "遊動", -"游园" => "遊園", -"游子" => "遊子", -"游学" => "遊學", -"游客" => "遊客", -"游宦" => "遊宦", -"游山玩水" => "遊山玩水", -"游必有方" => "遊必有方", -"游憩" => "遊憩", -"游戏" => "遊戲", -"游手好闲" => "遊手好閑", -"游手好闲" => "遊手好閒", -"游星" => "遊星", -"游乐" => "遊樂", -"游标卡尺" => "遊標卡尺", -"游历" => "遊歷", -"游民" => "遊民", -"游牧" => "遊牧", -"游猎" => "遊獵", -"游玩" => "遊玩", -"游荡" => "遊盪", -"游丝" => "遊絲", -"游兴" => "遊興", -"游船" => "遊船", -"游艇" => "遊艇", -"游荡" => "遊蕩", -"游艺" => "遊藝", -"游行" => "遊行", -"游街" => "遊街", -"游览" => "遊覽", -"游记" => "遊記", -"游说" => "遊說", -"游资" => "遊資", -"游走" => "遊走", -"游踪" => "遊蹤", -"游逛" => "遊逛", -"游错" => "遊錯", -"游离" => "遊離", -"游骑兵" => "遊騎兵", -"游魂" => "遊魂", -"遍于" => "遍於", -"过后" => "過後", -"过于" => "過於", -"过水面" => "過水麵", -"道范" => "道範", -"逊于" => "遜於", -"递回" => "遞迴", -"远于" => "遠於", -"远县才至" => "遠縣纔至", -"远游" => "遠遊", -"遨游" => "遨遊", -"适于" => "適於", -"遮丑" => "遮醜", -"迁怒于" => "遷怒於", -"迁于" => "遷於", -"遗范" => "遺範", -"遗余" => "遺餘", -"辽沈" => "遼瀋", -"避孕药" => "避孕藥", -"还占" => "還佔", -"还采" => "還採", -"还政于民" => "還政於民", -"还于" => "還於", -"还冲" => "還衝", -"邋里邋遢" => "邋裡邋遢", -"那里" => "那裡", -"那只" => "那隻", -"那么" => "那麼", -"那么着" => "那麼著", -"郁朴" => "郁樸", -"郊游" => "郊遊", -"郘钟" => "郘鐘", -"部落发" => "部落發", -"都于" => "都於", -"乡愿" => "鄉愿", -"郑凯云" => "鄭凱云", -"配合着" => "配合著", -"配水干管" => "配水幹管", -"配药" => "配藥", -"配制" => "配製", -"酒后" => "酒後", -"酒杯" => "酒盃", -"酒坛" => "酒罈", -"酒药" => "酒藥", -"酒麴" => "酒麴", -"酥松" => "酥鬆", -"酸硷" => "酸鹼", -"醇朴" => "醇樸", -"醉心于" => "醉心於", -"醉于" => "醉於", -"醋坛" => "醋罈", -"丑丫头" => "醜丫頭", -"丑事" => "醜事", -"丑人" => "醜人", -"丑八怪" => "醜八怪", -"丑剌剌" => "醜剌剌", -"丑剧" => "醜劇", -"丑化" => "醜化", -"丑名" => "醜名", -"丑咤" => "醜吒", -"丑地" => "醜地", -"丑夷" => "醜夷", -"丑女效颦" => "醜女效顰", -"丑妇" => "醜婦", -"丑媳" => "醜媳", -"丑小鸭" => "醜小鴨", -"丑巴怪" => "醜巴怪", -"丑恶" => "醜惡", -"丑态" => "醜態", -"丑于" => "醜於", -"丑末" => "醜末", -"丑样" => "醜樣", -"丑死" => "醜死", -"丑生" => "醜生", -"丑闻" => "醜聞", -"丑声四溢" => "醜聲四溢", -"丑声远播" => "醜聲遠播", -"丑脸" => "醜臉", -"丑行" => "醜行", -"丑诋" => "醜詆", -"丑话" => "醜話", -"丑语" => "醜語", -"丑丑" => "醜醜", -"丑陋" => "醜陋", -"丑头怪脸" => "醜頭怪臉", -"丑类" => "醜類", -"酝藉" => "醞藉", -"酝酿着" => "醞釀著", -"医药" => "醫藥", -"酿制" => "釀製", -"衅钟" => "釁鐘", -"采石之役" => "采石之役", -"采石之战" => "采石之戰", -"采石矶" => "采石磯", -"釉药" => "釉藥", -"里程表" => "里程錶", -"重划" => "重劃", -"重折" => "重摺", -"重于" => "重於", -"重罗面" => "重羅麵", -"重复" => "重複", -"重讬" => "重託", -"重游" => "重遊", -"重锤" => "重鎚", -"野姜" => "野薑", -"野游" => "野遊", -"厘出" => "釐出", -"厘定" => "釐定", -"厘正" => "釐正", -"厘清" => "釐清", -"厘订" => "釐訂", -"金仆姑" => "金僕姑", -"金仑溪" => "金崙溪", -"金表" => "金錶", -"金钟" => "金鐘", -"金鸡纳硷" => "金雞納鹼", -"金发" => "金髮", -"金斗" => "金鬥", -"金霉素" => "金黴素", -"钉锤" => "釘鎚", -"铃响后" => "鈴響後", -"钩心斗角" => "鉤心鬥角", -"银朱" => "銀硃", -"银发" => "銀髮", -"铜制" => "銅製", -"铜钟" => "銅鐘", -"铝制" => "鋁製", -"钢梁" => "鋼樑", -"钢制" => "鋼製", -"录着" => "錄著", -"录制" => "錄製", -"钱谷" => "錢穀", -"钱庄" => "錢莊", -"锦心绣口" => "錦心繡口", -"锦绣花园" => "錦綉花園", -"锦绣" => "錦繡", -"表带" => "錶帶", -"表店" => "錶店", -"表厂" => "錶廠", -"表板" => "錶板", -"表壳" => "錶殼", -"表盘" => "錶盤", -"表蒙子" => "錶蒙子", -"表针" => "錶針", -"表链" => "錶鏈", -"锻鍊出" => "鍛鍊出", -"锲而不舍" => "鍥而不捨", -"钟表" => "鍾錶", -"锤儿" => "鎚兒", -"锤子" => "鎚子", -"锤头" => "鎚頭", -"链霉素" => "鏈黴素", -"锈病" => "鏽病", -"锈菌" => "鏽菌", -"锈蚀" => "鏽蝕", -"钟不扣不鸣" => "鐘不扣不鳴", -"钟不撞不鸣" => "鐘不撞不鳴", -"钟乳洞" => "鐘乳洞", -"钟乳石" => "鐘乳石", -"钟在寺里" => "鐘在寺里", -"钟塔" => "鐘塔", -"钟山" => "鐘山", -"钟形虫" => "鐘形蟲", -"钟摆" => "鐘擺", -"钟楼" => "鐘樓", -"钟漏" => "鐘漏", -"钟琴" => "鐘琴", -"钟相" => "鐘相", -"钟磬" => "鐘磬", -"钟声" => "鐘聲", -"钟表" => "鐘表", -"钟表" => "鐘錶", -"钟关" => "鐘關", -"钟响" => "鐘響", -"钟头" => "鐘頭", -"钟鸣" => "鐘鳴", -"钟鸣漏尽" => "鐘鳴漏盡", -"钟点" => "鐘點", -"钟鼎" => "鐘鼎", -"钟鼓" => "鐘鼓", -"铁锤" => "鐵鎚", -"铁锈" => "鐵鏽", -"铸钟" => "鑄鐘", -"鑑于" => "鑑於", -"鉴于" => "鑒於", -"长于" => "長於", -"长历" => "長曆", -"长生药" => "長生藥", -"长须鲸" => "長鬚鯨", -"门前门后" => "門前門後", -"门帘" => "門帘", -"门里" => "門裡", -"门斗" => "門鬥", -"开列于后" => "開列於後", -"开吊" => "開弔", -"开征" => "開徵", -"开采" => "開採", -"开药" => "開藥", -"开辟" => "開闢", -"开哄" => "開鬨", -"闲情逸致" => "閒情逸緻", -"闲荡" => "閒蕩", -"闲游" => "閒遊", -"间不容发" => "間不容髮", -"闵采尔" => "閔採爾", -"合府" => "閤府", -"闺范" => "閨範", -"阃范" => "閫範", -"关系" => "關係", -"关系着" => "關係著", -"关弓与我确" => "關弓與我确", -"关于" => "關於", -"辟佛" => "闢佛", -"辟作" => "闢作", -"辟划" => "闢劃", -"辟土" => "闢土", -"辟地" => "闢地", -"辟室" => "闢室", -"辟建" => "闢建", -"辟为" => "闢為", -"辟田" => "闢田", -"辟筑" => "闢築", -"辟谣" => "闢謠", -"辟辟" => "闢辟", -"辟邪以律" => "闢邪以律", -"防御" => "防禦", -"防范" => "防範", -"防锈" => "防鏽", -"防台" => "防颱", -"阻于" => "阻於", -"附于" => "附於", -"附注" => "附註", -"降压药" => "降壓藥", -"降于" => "降於", -"限于" => "限於", -"升官" => "陞官", -"除臭药" => "除臭藥", -"阴干" => "陰乾", -"阴历" => "陰曆", -"阴郁" => "陰鬱", -"陷于" => "陷於", -"陆游" => "陸遊", -"阳春面" => "陽春麵", -"阳历" => "陽曆", -"随后" => "隨後", -"随于" => "隨於", -"隐几" => "隱几", -"隐于" => "隱於", -"只字" => "隻字", -"只影" => "隻影", -"只手遮天" => "隻手遮天", -"只眼" => "隻眼", -"只言片语" => "隻言片語", -"只身" => "隻身", -"雅范" => "雅範", -"雅致" => "雅緻", -"集于" => "集於", -"集于一身" => "集於一身", -"集游法" => "集遊法", -"雇佣" => "雇傭", -"雇于" => "雇於", -"雕梁划栋" => "雕樑畫棟", -"双折" => "雙摺", -"双胜类" => "雙胜類", -"杂合面儿" => "雜合麵兒", -"杂志" => "雜誌", -"杂面" => "雜麵", -"鸡奸" => "雞姦", -"鸡丝" => "雞絲", -"鸡丝面" => "雞絲麵", -"鸡腿面" => "雞腿麵", -"鸡只" => "雞隻", -"离于" => "離於", -"难容于" => "難容於", -"难舍" => "難捨", -"难于" => "難於", -"雨后" => "雨後", -"雪窗萤几" => "雪窗螢几", -"雪里" => "雪裡", -"云南白药" => "雲南白藥", -"云笈七签" => "雲笈七籤", -"云游" => "雲遊", -"云须" => "雲鬚", -"零多只" => "零多隻", -"零天后" => "零天後", -"零只" => "零隻", -"电子表" => "電子錶", -"电子钟" => "電子鐘", -"电冲" => "電衝", -"电表" => "電錶", -"电钟" => "電鐘", -"震栗" => "震慄", -"震于" => "震於", -"震荡" => "震蕩", -"雾里" => "霧裡", -"露丑" => "露醜", -"霸占" => "霸佔", -"霁范" => "霽範", -"灵药" => "靈藥", -"青山一发" => "青山一髮", -"青苹" => "青苹", -"青霉" => "青黴", -"非占不可" => "非佔不可", -"非于" => "非於", -"靠后" => "靠後", -"面朝着" => "面朝著", -"面临着" => "面臨著", -"鞋里" => "鞋裡", -"鞣制" => "鞣製", -"秋千" => "鞦韆", -"鞭辟入里" => "鞭辟入裡", -"韩国制" => "韓國製", -"韩制" => "韓製", -"音准" => "音準", -"音声如钟" => "音聲如鐘", -"韶山冲" => "韶山衝", -"顺于" => "順於", -"颂赞" => "頌讚", -"预制" => "預製", -"领袖欲" => "領袖慾", -"头里" => "頭裡", -"头发" => "頭髮", -"颊须" => "頰鬚", -"额我略历" => "額我略曆", -"颜范" => "顏範", -"颠干倒坤" => "顛乾倒坤", -"颠复" => "顛覆", -"类似于" => "類似於", -"顾藉" => "顧藉", -"颤栗" => "顫慄", -"显着标志" => "顯著標志", -"风干" => "風乾", -"风土志" => "風土誌", -"风尘仆仆" => "風塵僕僕", -"风卷残云" => "風捲殘雲", -"风物志" => "風物誌", -"风范" => "風範", -"风里" => "風裡", -"风起云涌" => "風起雲湧", -"风斗" => "風鬥", -"台风" => "颱風", -"刮了" => "颳了", -"刮倒" => "颳倒", -"刮去" => "颳去", -"刮得" => "颳得", -"刮着" => "颳著", -"刮走" => "颳走", -"刮起" => "颳起", -"刮雪" => "颳雪", -"刮风" => "颳風", -"飘荡" => "飄蕩", -"飘游" => "飄遊", -"食欲" => "食慾", -"食野之苹" => "食野之苹", -"食面" => "食麵", -"饭后" => "飯後", -"饭后钟" => "飯後鐘", -"饭团" => "飯糰", -"饭庄" => "飯莊", -"饲喂" => "飼餵", -"饼干" => "餅乾", -"馂余" => "餕餘", -"余下" => "餘下", -"余事" => "餘事", -"余人" => "餘人", -"余值" => "餘值", -"余僇" => "餘僇", -"余光" => "餘光", -"余函数" => "餘函數", -"余刃" => "餘刃", -"余切" => "餘切", -"余利" => "餘利", -"余剩" => "餘剩", -"余割" => "餘割", -"余力" => "餘力", -"余勇" => "餘勇", -"余味" => "餘味", -"余喘" => "餘喘", -"余地" => "餘地", -"余址" => "餘址", -"余墨" => "餘墨", -"余外" => "餘外", -"余妙" => "餘妙", -"余姚" => "餘姚", -"余威" => "餘威", -"余存" => "餘存", -"余孽" => "餘孽", -"余巾" => "餘巾", -"余式定理" => "餘式定理", -"余弦" => "餘弦", -"余思" => "餘思", -"余悸" => "餘悸", -"余庆" => "餘慶", -"余数" => "餘數", -"余日" => "餘日", -"余明" => "餘明", -"余映" => "餘映", -"余暇" => "餘暇", -"余晖" => "餘暉", -"余杭" => "餘杭", -"余杯" => "餘杯", -"余桃" => "餘桃", -"余桶" => "餘桶", -"余业" => "餘業", -"余款" => "餘款", -"余步" => "餘步", -"余殃" => "餘殃", -"余毒" => "餘毒", -"余气" => "餘氣", -"余氯" => "餘氯", -"余波" => "餘波", -"余温" => "餘溫", -"余泽" => "餘澤", -"余沥" => "餘瀝", -"余烈" => "餘烈", -"余热" => "餘熱", -"余烬" => "餘燼", -"余珍" => "餘珍", -"余生" => "餘生", -"余留" => "餘留", -"余众" => "餘眾", -"余窍" => "餘竅", -"余粮" => "餘糧", -"余绪" => "餘緒", -"余缺" => "餘缺", -"余罪" => "餘罪", -"余羨" => "餘羨", -"余声" => "餘聲", -"余膏" => "餘膏", -"余兴" => "餘興", -"余蓄" => "餘蓄", -"余荫" => "餘蔭", -"余裕" => "餘裕", -"余角" => "餘角", -"余论" => "餘論", -"余责" => "餘責", -"余辉" => "餘輝", -"余辜" => "餘辜", -"余酲" => "餘酲", -"余钱" => "餘錢", -"余闰" => "餘閏", -"余闲" => "餘閒", -"余震" => "餘震", -"余音" => "餘音", -"余韵" => "餘韻", -"余响" => "餘響", -"余额" => "餘額", -"余风" => "餘風", -"余食" => "餘食", -"余香" => "餘香", -"余党" => "餘黨", -"馄饨面" => "餛飩麵", -"馆谷" => "館穀", -"喂乳" => "餵乳", -"喂了" => "餵了", -"喂奶" => "餵奶", -"喂给" => "餵給", -"喂羊" => "餵羊", -"喂猪" => "餵豬", -"喂过" => "餵過", -"喂鸡" => "餵雞", -"喂食" => "餵食", -"喂饱" => "餵飽", -"喂养" => "餵養", -"喂驴" => "餵驢", -"喂鱼" => "餵魚", -"喂鸭" => "餵鴨", -"喂鹅" => "餵鵝", -"饥寒" => "饑寒", -"饥民" => "饑民", -"饥渴" => "饑渴", -"饥溺" => "饑溺", -"饥饱" => "饑飽", -"饥馑" => "饑饉", -"首当其冲" => "首當其衝", -"香干" => "香乾", -"马干" => "馬乾", -"马后" => "馬後", -"马表" => "馬錶", -"驻扎" => "駐紮", -"骀荡" => "駘蕩", -"骀藉" => "駘藉", -"腾冲" => "騰衝", -"惊赞" => "驚讚", -"骨子里" => "骨子裡", -"骨干" => "骨幹", -"骨灰坛" => "骨灰罈", -"骨坛" => "骨罈", -"骨头里挣出来的钱才做得肉" => "骨頭裡掙出來的錢纔做得肉", -"肮脏" => "骯髒", -"脏乱" => "髒亂", -"脏兮兮" => "髒兮兮", -"脏字" => "髒字", -"脏得" => "髒得", -"脏心" => "髒心", -"脏东西" => "髒東西", -"脏水" => "髒水", -"脏的" => "髒的", -"脏词" => "髒詞", -"脏话" => "髒話", -"脏钱" => "髒錢", -"体范" => "體範", -"高几" => "高几", -"高干" => "高幹", -"高于" => "高於", -"高升" => "高陞", -"髡发" => "髡髮", -"髭须" => "髭鬚", -"发上指冠" => "髮上指冠", -"发上冲冠" => "髮上沖冠", -"发乳" => "髮乳", -"发光可鉴" => "髮光可鑒", -"发匪" => "髮匪", -"发型" => "髮型", -"发夹" => "髮夾", -"发妻" => "髮妻", -"发姐" => "髮姐", -"发屋" => "髮屋", -"发带" => "髮帶", -"发廊" => "髮廊", -"发式" => "髮式", -"发引千钧" => "髮引千鈞", -"发指" => "髮指", -"发卷" => "髮捲", -"发根" => "髮根", -"发油" => "髮油", -"发漂" => "髮漂", -"发状" => "髮狀", -"发癣" => "髮癬", -"发短心长" => "髮短心長", -"发禁" => "髮禁", -"发笺" => "髮箋", -"发纱" => "髮紗", -"发结" => "髮結", -"发丝" => "髮絲", -"发网" => "髮網", -"发脚" => "髮腳", -"发肤" => "髮膚", -"发胶" => "髮膠", -"发菜" => "髮菜", -"发蜡" => "髮蠟", -"发踊冲冠" => "髮踴沖冠", -"发辫" => "髮辮", -"发针" => "髮針", -"发钗" => "髮釵", -"发长" => "髮長", -"发际" => "髮際", -"发霜" => "髮霜", -"发饰" => "髮飾", -"发髻" => "髮髻", -"发鬓" => "髮鬢", -"髼松" => "髼鬆", -"鬅松" => "鬅鬆", -"松一口气" => "鬆一口氣", -"松了" => "鬆了", -"松些" => "鬆些", -"松劲" => "鬆勁", -"松动" => "鬆動", -"松口" => "鬆口", -"松土" => "鬆土", -"松宽" => "鬆寬", -"松弛" => "鬆弛", -"松快" => "鬆快", -"松懈" => "鬆懈", -"松手" => "鬆手", -"松散" => "鬆散", -"松柔" => "鬆柔", -"松气" => "鬆氣", -"松浮" => "鬆浮", -"松绑" => "鬆綁", -"松紧" => "鬆緊", -"松缓" => "鬆緩", -"松脆" => "鬆脆", -"松脱" => "鬆脫", -"松蛋" => "鬆蛋", -"松起" => "鬆起", -"松软" => "鬆軟", -"松通" => "鬆通", -"松开" => "鬆開", -"松饼" => "鬆餅", -"松松" => "鬆鬆", -"鬈发" => "鬈髮", -"胡子" => "鬍子", -"胡梢" => "鬍梢", -"胡渣" => "鬍渣", -"胡髭" => "鬍髭", -"胡须" => "鬍鬚", -"鬒发" => "鬒髮", -"须根" => "鬚根", -"须毛" => "鬚毛", -"须生" => "鬚生", -"须眉" => "鬚眉", -"须发" => "鬚髮", -"须须" => "鬚鬚", -"须鲨" => "鬚鯊", -"须鲸" => "鬚鯨", -"鬓发" => "鬢髮", -"斗上" => "鬥上", -"斗不过" => "鬥不過", -"斗了" => "鬥了", -"斗来斗去" => "鬥來鬥去", -"斗倒" => "鬥倒", -"斗劲" => "鬥勁", -"斗口" => "鬥口", -"斗嘴" => "鬥嘴", -"斗士" => "鬥士", -"斗子" => "鬥子", -"斗弄" => "鬥弄", -"斗志" => "鬥志", -"斗成" => "鬥成", -"斗批改" => "鬥批改", -"斗技" => "鬥技", -"斗智" => "鬥智", -"斗殴" => "鬥毆", -"斗气" => "鬥氣", -"斗法" => "鬥法", -"斗争" => "鬥爭", -"斗牛" => "鬥牛", -"斗狠" => "鬥狠", -"斗眼" => "鬥眼", -"斗私批修" => "鬥私批修", -"斗笠" => "鬥笠", -"斗草" => "鬥草", -"斗着" => "鬥著", -"斗蟋蟀" => "鬥蟋蟀", -"斗起" => "鬥起", -"斗鸡" => "鬥雞", -"斗斗" => "鬥鬥", -"斗鱼" => "鬥魚", -"斗鹌鹑" => "鬥鵪鶉", -"闹着玩儿" => "鬧著玩兒", -"闹钟" => "鬧鐘", -"哄动" => "鬨動", -"哄堂" => "鬨堂", -"哄笑" => "鬨笑", -"郁伊" => "鬱伊", -"郁勃" => "鬱勃", -"郁卒" => "鬱卒", -"郁堙不偶" => "鬱堙不偶", -"郁塞" => "鬱塞", -"郁垒" => "鬱壘", -"郁律" => "鬱律", -"郁悒" => "鬱悒", -"郁闷" => "鬱悶", -"郁愤" => "鬱憤", -"郁抑" => "鬱抑", -"郁挹" => "鬱挹", -"郁江" => "鬱江", -"郁沉沉" => "鬱沉沉", -"郁泱" => "鬱泱", -"郁火" => "鬱火", -"郁热" => "鬱熱", -"郁燠" => "鬱燠", -"郁症" => "鬱症", -"郁积" => "鬱積", -"郁纡" => "鬱紆", -"郁结" => "鬱結", -"郁蒸" => "鬱蒸", -"郁蓊" => "鬱蓊", -"郁血" => "鬱血", -"郁邑" => "鬱邑", -"郁郁" => "鬱郁", -"郁金" => "鬱金", -"郁金香" => "鬱金香", -"郁闭" => "鬱閉", -"郁陶" => "鬱陶", -"郁郁不平" => "鬱鬱不平", -"郁郁不乐" => "鬱鬱不樂", -"郁郁寡欢" => "鬱鬱寡歡", -"郁郁而终" => "鬱鬱而終", -"郁郁葱葱" => "鬱鬱蔥蔥", -"郁黑" => "鬱黑", -"魏征" => "魏徵", -"鱼干" => "魚乾", -"鱼松" => "魚鬆", -"鲸须" => "鯨鬚", -"鲇鱼" => "鯰魚", -"鸠占鹊巢" => "鳩佔鵲巢", -"凤梨干" => "鳳梨乾", -"鸣钟" => "鳴鐘", -"鸿范" => "鴻範", -"鹄发" => "鵠髮", -"雕悍" => "鵰悍", -"鹤发" => "鶴髮", -"咸味" => "鹹味", -"咸嘴淡舌" => "鹹嘴淡舌", -"咸土" => "鹹土", -"咸度" => "鹹度", -"咸得" => "鹹得", -"咸批" => "鹹批", -"咸水" => "鹹水", -"咸派" => "鹹派", -"咸海" => "鹹海", -"咸淡" => "鹹淡", -"咸湖" => "鹹湖", -"咸汤" => "鹹湯", -"咸潟" => "鹹潟", -"咸的" => "鹹的", -"咸粥" => "鹹粥", -"咸肉" => "鹹肉", -"咸菜" => "鹹菜", -"咸蛋" => "鹹蛋", -"咸猪肉" => "鹹豬肉", -"咸类" => "鹹類", -"咸食" => "鹹食", -"咸鱼" => "鹹魚", -"咸鸭蛋" => "鹹鴨蛋", -"咸卤" => "鹹鹵", -"咸咸" => "鹹鹹", -"硷化" => "鹼化", -"硷土金属" => "鹼土金屬", -"硷地" => "鹼地", -"硷度" => "鹼度", -"硷性" => "鹼性", -"硷水" => "鹼水", -"硷液" => "鹼液", -"硷熔" => "鹼熔", -"硷石灰" => "鹼石灰", -"硷纤维素" => "鹼纖維素", -"硷金属" => "鹼金屬", -"硷类" => "鹼類", -"盐打怎么咸" => "鹽打怎麼鹹", -"盐卤" => "鹽滷", -"盐余" => "鹽餘", -"盐硷土" => "鹽鹼土", -"盐硷滩" => "鹽鹼灘", -"丽于" => "麗於", -"麴霉" => "麴黴", -"面人儿" => "麵人兒", -"面价" => "麵價", -"面包" => "麵包", -"面团" => "麵團", -"面坊" => "麵坊", -"面坯儿" => "麵坯兒", -"面塑" => "麵塑", -"面店" => "麵店", -"面厂" => "麵廠", -"面杖" => "麵杖", -"面条" => "麵條", -"面汤" => "麵湯", -"面浆" => "麵漿", -"面灰" => "麵灰", -"面疙瘩" => "麵疙瘩", -"面皮" => "麵皮", -"面码儿" => "麵碼兒", -"面筋" => "麵筋", -"面粉" => "麵粉", -"面糊" => "麵糊", -"面线" => "麵線", -"面缸" => "麵缸", -"面茶" => "麵茶", -"面食" => "麵食", -"面饺" => "麵餃", -"面饼" => "麵餅", -"面馆" => "麵館", -"麻药" => "麻藥", -"麻醉药" => "麻醉藥", -"麻酱面" => "麻醬麵", -"麻雀在后" => "麻雀在後", -"黄干黑廋" |