MediaWiki  REL1_22
ContentHandler.php
Go to the documentation of this file.
00001 <?php
00033 class MWContentSerializationException extends MWException {
00034 
00035 }
00036 
00056 abstract class ContentHandler {
00057 
00065     protected static $enableDeprecationWarnings = false;
00066 
00095     public static function getContentText( Content $content = null ) {
00096         global $wgContentHandlerTextFallback;
00097 
00098         if ( is_null( $content ) ) {
00099             return '';
00100         }
00101 
00102         if ( $content instanceof TextContent ) {
00103             return $content->getNativeData();
00104         }
00105 
00106         wfDebugLog( 'ContentHandler', 'Accessing ' . $content->getModel() . ' content as text!' );
00107 
00108         if ( $wgContentHandlerTextFallback == 'fail' ) {
00109             throw new MWException(
00110                 "Attempt to get text from Content with model " .
00111                 $content->getModel()
00112             );
00113         }
00114 
00115         if ( $wgContentHandlerTextFallback == 'serialize' ) {
00116             return $content->serialize();
00117         }
00118 
00119         return null;
00120     }
00121 
00147     public static function makeContent( $text, Title $title = null,
00148         $modelId = null, $format = null )
00149     {
00150         if ( is_null( $modelId ) ) {
00151             if ( is_null( $title ) ) {
00152                 throw new MWException( "Must provide a Title object or a content model ID." );
00153             }
00154 
00155             $modelId = $title->getContentModel();
00156         }
00157 
00158         $handler = ContentHandler::getForModelID( $modelId );
00159         return $handler->unserializeContent( $text, $format );
00160     }
00161 
00195     public static function getDefaultModelFor( Title $title ) {
00196         // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
00197         //       because it is used to initialize the mContentModel member.
00198 
00199         $ns = $title->getNamespace();
00200 
00201         $ext = false;
00202         $m = null;
00203         $model = MWNamespace::getNamespaceContentModel( $ns );
00204 
00205         // Hook can determine default model
00206         if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) {
00207             if ( !is_null( $model ) ) {
00208                 return $model;
00209             }
00210         }
00211 
00212         // Could this page contain custom CSS or JavaScript, based on the title?
00213         $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m );
00214         if ( $isCssOrJsPage ) {
00215             $ext = $m[1];
00216         }
00217 
00218         // Hook can force JS/CSS
00219         wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) );
00220 
00221         // Is this a .css subpage of a user page?
00222         $isJsCssSubpage = NS_USER == $ns
00223             && !$isCssOrJsPage
00224             && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m );
00225         if ( $isJsCssSubpage ) {
00226             $ext = $m[1];
00227         }
00228 
00229         // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
00230         $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
00231         $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage;
00232 
00233         // Hook can override $isWikitext
00234         wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
00235 
00236         if ( !$isWikitext ) {
00237             switch ( $ext ) {
00238                 case 'js':
00239                     return CONTENT_MODEL_JAVASCRIPT;
00240                 case 'css':
00241                     return CONTENT_MODEL_CSS;
00242                 default:
00243                     return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
00244             }
00245         }
00246 
00247         // We established that it must be wikitext
00248 
00249         return CONTENT_MODEL_WIKITEXT;
00250     }
00251 
00260     public static function getForTitle( Title $title ) {
00261         $modelId = $title->getContentModel();
00262         return ContentHandler::getForModelID( $modelId );
00263     }
00264 
00274     public static function getForContent( Content $content ) {
00275         $modelId = $content->getModel();
00276         return ContentHandler::getForModelID( $modelId );
00277     }
00278 
00282     static $handlers;
00283 
00309     public static function getForModelID( $modelId ) {
00310         global $wgContentHandlers;
00311 
00312         if ( isset( ContentHandler::$handlers[$modelId] ) ) {
00313             return ContentHandler::$handlers[$modelId];
00314         }
00315 
00316         if ( empty( $wgContentHandlers[$modelId] ) ) {
00317             $handler = null;
00318 
00319             wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
00320 
00321             if ( $handler === null ) {
00322                 throw new MWException( "No handler for model '$modelId' registered in \$wgContentHandlers" );
00323             }
00324 
00325             if ( !( $handler instanceof ContentHandler ) ) {
00326                 throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
00327             }
00328         } else {
00329             $class = $wgContentHandlers[$modelId];
00330             $handler = new $class( $modelId );
00331 
00332             if ( !( $handler instanceof ContentHandler ) ) {
00333                 throw new MWException( "$class from \$wgContentHandlers is not compatible with ContentHandler" );
00334             }
00335         }
00336 
00337         wfDebugLog( 'ContentHandler', 'Created handler for ' . $modelId
00338                     . ': ' . get_class( $handler ) );
00339 
00340         ContentHandler::$handlers[$modelId] = $handler;
00341         return ContentHandler::$handlers[$modelId];
00342     }
00343 
00356     public static function getLocalizedName( $name ) {
00357         $key = "content-model-$name";
00358 
00359         $msg = wfMessage( $key );
00360 
00361         return $msg->exists() ? $msg->plain() : $name;
00362     }
00363 
00364     public static function getContentModels() {
00365         global $wgContentHandlers;
00366 
00367         return array_keys( $wgContentHandlers );
00368     }
00369 
00370     public static function getAllContentFormats() {
00371         global $wgContentHandlers;
00372 
00373         $formats = array();
00374 
00375         foreach ( $wgContentHandlers as $model => $class ) {
00376             $handler = ContentHandler::getForModelID( $model );
00377             $formats = array_merge( $formats, $handler->getSupportedFormats() );
00378         }
00379 
00380         $formats = array_unique( $formats );
00381         return $formats;
00382     }
00383 
00384     // ------------------------------------------------------------------------
00385 
00386     protected $mModelID;
00387     protected $mSupportedFormats;
00388 
00398     public function __construct( $modelId, $formats ) {
00399         $this->mModelID = $modelId;
00400         $this->mSupportedFormats = $formats;
00401 
00402         $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
00403         $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
00404         $this->mModelName = strtolower( $this->mModelName );
00405     }
00406 
00416     abstract public function serializeContent( Content $content, $format = null );
00417 
00427     abstract public function unserializeContent( $blob, $format = null );
00428 
00437     abstract public function makeEmptyContent();
00438 
00456     public function makeRedirectContent( Title $destination, $text = '' ) {
00457         return null;
00458     }
00459 
00468     public function getModelID() {
00469         return $this->mModelID;
00470     }
00471 
00482     protected function checkModelID( $model_id ) {
00483         if ( $model_id !== $this->mModelID ) {
00484             throw new MWException( "Bad content model: " .
00485                 "expected {$this->mModelID} " .
00486                 "but got $model_id." );
00487         }
00488     }
00489 
00499     public function getSupportedFormats() {
00500         return $this->mSupportedFormats;
00501     }
00502 
00514     public function getDefaultFormat() {
00515         return $this->mSupportedFormats[0];
00516     }
00517 
00530     public function isSupportedFormat( $format ) {
00531 
00532         if ( !$format ) {
00533             return true; // this means "use the default"
00534         }
00535 
00536         return in_array( $format, $this->mSupportedFormats );
00537     }
00538 
00548     protected function checkFormat( $format ) {
00549         if ( !$this->isSupportedFormat( $format ) ) {
00550             throw new MWException(
00551                 "Format $format is not supported for content model "
00552                 . $this->getModelID()
00553             );
00554         }
00555     }
00556 
00567     public function getActionOverrides() {
00568         return array();
00569     }
00570 
00586     public function createDifferenceEngine( IContextSource $context,
00587         $old = 0, $new = 0,
00588         $rcid = 0, # FIXME: use everywhere!
00589         $refreshCache = false, $unhide = false
00590     ) {
00591         $diffEngineClass = $this->getDiffEngineClass();
00592 
00593         return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
00594     }
00595 
00613     public function getPageLanguage( Title $title, Content $content = null ) {
00614         global $wgContLang, $wgLang;
00615         $pageLang = $wgContLang;
00616 
00617         if ( $title->getNamespace() == NS_MEDIAWIKI ) {
00618             // Parse mediawiki messages with correct target language
00619             list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
00620             $pageLang = wfGetLangObj( $lang );
00621         }
00622 
00623         wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) );
00624         return wfGetLangObj( $pageLang );
00625     }
00626 
00647     public function getPageViewLanguage( Title $title, Content $content = null ) {
00648         $pageLang = $this->getPageLanguage( $title, $content );
00649 
00650         if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
00651             // If the user chooses a variant, the content is actually
00652             // in a language whose code is the variant code.
00653             $variant = $pageLang->getPreferredVariant();
00654             if ( $pageLang->getCode() !== $variant ) {
00655                 $pageLang = Language::factory( $variant );
00656             }
00657         }
00658 
00659         return $pageLang;
00660     }
00661 
00675     public function canBeUsedOn( Title $title ) {
00676         return true;
00677     }
00678 
00686     protected function getDiffEngineClass() {
00687         return 'DifferenceEngine';
00688     }
00689 
00705     public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
00706         return false;
00707     }
00708 
00720     public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
00721         // Decide what kind of auto-summary is needed.
00722 
00723         // Redirect auto-summaries
00724 
00730         $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
00731         $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
00732 
00733         if ( is_object( $rt ) ) {
00734             if ( !is_object( $ot )
00735                 || !$rt->equals( $ot )
00736                 || $ot->getFragment() != $rt->getFragment() )
00737             {
00738                 $truncatedtext = $newContent->getTextForSummary(
00739                     250
00740                         - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
00741                         - strlen( $rt->getFullText() ) );
00742 
00743                 return wfMessage( 'autoredircomment', $rt->getFullText() )
00744                         ->rawParams( $truncatedtext )->inContentLanguage()->text();
00745             }
00746         }
00747 
00748         // New page auto-summaries
00749         if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
00750             // If they're making a new article, give its text, truncated, in
00751             // the summary.
00752 
00753             $truncatedtext = $newContent->getTextForSummary(
00754                 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
00755 
00756             return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
00757                     ->inContentLanguage()->text();
00758         }
00759 
00760         // Blanking auto-summaries
00761         if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
00762             return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
00763         } elseif ( !empty( $oldContent )
00764             && $oldContent->getSize() > 10 * $newContent->getSize()
00765             && $newContent->getSize() < 500 )
00766         {
00767             // Removing more than 90% of the article
00768 
00769             $truncatedtext = $newContent->getTextForSummary(
00770                 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
00771 
00772             return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
00773                     ->inContentLanguage()->text();
00774         }
00775 
00776         // If we reach this point, there's no applicable auto-summary for our
00777         // case, so our auto-summary is empty.
00778         return '';
00779     }
00780 
00795     public function getAutoDeleteReason( Title $title, &$hasHistory ) {
00796         $dbw = wfGetDB( DB_MASTER );
00797 
00798         // Get the last revision
00799         $rev = Revision::newFromTitle( $title );
00800 
00801         if ( is_null( $rev ) ) {
00802             return false;
00803         }
00804 
00805         // Get the article's contents
00806         $content = $rev->getContent();
00807         $blank = false;
00808 
00809         // If the page is blank, use the text from the previous revision,
00810         // which can only be blank if there's a move/import/protect dummy
00811         // revision involved
00812         if ( !$content || $content->isEmpty() ) {
00813             $prev = $rev->getPrevious();
00814 
00815             if ( $prev ) {
00816                 $rev = $prev;
00817                 $content = $rev->getContent();
00818                 $blank = true;
00819             }
00820         }
00821 
00822         $this->checkModelID( $rev->getContentModel() );
00823 
00824         // Find out if there was only one contributor
00825         // Only scan the last 20 revisions
00826         $res = $dbw->select( 'revision', 'rev_user_text',
00827             array(
00828                 'rev_page' => $title->getArticleID(),
00829                 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
00830             ),
00831             __METHOD__,
00832             array( 'LIMIT' => 20 )
00833         );
00834 
00835         if ( $res === false ) {
00836             // This page has no revisions, which is very weird
00837             return false;
00838         }
00839 
00840         $hasHistory = ( $res->numRows() > 1 );
00841         $row = $dbw->fetchObject( $res );
00842 
00843         if ( $row ) { // $row is false if the only contributor is hidden
00844             $onlyAuthor = $row->rev_user_text;
00845             // Try to find a second contributor
00846             foreach ( $res as $row ) {
00847                 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
00848                     $onlyAuthor = false;
00849                     break;
00850                 }
00851             }
00852         } else {
00853             $onlyAuthor = false;
00854         }
00855 
00856         // Generate the summary with a '$1' placeholder
00857         if ( $blank ) {
00858             // The current revision is blank and the one before is also
00859             // blank. It's just not our lucky day
00860             $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
00861         } else {
00862             if ( $onlyAuthor ) {
00863                 $reason = wfMessage(
00864                     'excontentauthor',
00865                     '$1',
00866                     $onlyAuthor
00867                 )->inContentLanguage()->text();
00868             } else {
00869                 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
00870             }
00871         }
00872 
00873         if ( $reason == '-' ) {
00874             // Allow these UI messages to be blanked out cleanly
00875             return '';
00876         }
00877 
00878         // Max content length = max comment length - length of the comment (excl. $1)
00879         $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
00880 
00881         // Now replace the '$1' placeholder
00882         $reason = str_replace( '$1', $text, $reason );
00883 
00884         return $reason;
00885     }
00886 
00900     public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
00901         $cur_content = $current->getContent();
00902 
00903         if ( empty( $cur_content ) ) {
00904             return false; // no page
00905         }
00906 
00907         $undo_content = $undo->getContent();
00908         $undoafter_content = $undoafter->getContent();
00909 
00910         $this->checkModelID( $cur_content->getModel() );
00911         $this->checkModelID( $undo_content->getModel() );
00912         $this->checkModelID( $undoafter_content->getModel() );
00913 
00914         if ( $cur_content->equals( $undo_content ) ) {
00915             // No use doing a merge if it's just a straight revert.
00916             return $undoafter_content;
00917         }
00918 
00919         $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
00920 
00921         return $undone_content;
00922     }
00923 
00940     public function makeParserOptions( $context ) {
00941         global $wgContLang, $wgEnableParserLimitReporting;
00942 
00943         if ( $context instanceof IContextSource ) {
00944             $options = ParserOptions::newFromContext( $context );
00945         } elseif ( $context instanceof User ) { // settings per user (even anons)
00946             $options = ParserOptions::newFromUser( $context );
00947         } elseif ( $context === 'canonical' ) { // canonical settings
00948             $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
00949         } else {
00950             throw new MWException( "Bad context for parser options: $context" );
00951         }
00952 
00953         $options->enableLimitReport( $wgEnableParserLimitReporting ); // show inclusion/loop reports
00954         $options->setTidy( true ); // fix bad HTML
00955 
00956         return $options;
00957     }
00958 
00967     public function isParserCacheSupported() {
00968         return false;
00969     }
00970 
00980     public function supportsSections() {
00981         return false;
00982     }
00983 
00993     public function supportsRedirects() {
00994         return false;
00995     }
00996 
01010     public static function deprecated( $func, $version, $component = false ) {
01011         if ( self::$enableDeprecationWarnings ) {
01012             wfDeprecated( $func, $version, $component, 3 );
01013         }
01014     }
01015 
01033     public static function runLegacyHooks( $event, $args = array(),
01034             $warn = null ) {
01035 
01036         if ( $warn === null ) {
01037             $warn = self::$enableDeprecationWarnings;
01038         }
01039 
01040         if ( !Hooks::isRegistered( $event ) ) {
01041             return true; // nothing to do here
01042         }
01043 
01044         if ( $warn ) {
01045             // Log information about which handlers are registered for the legacy hook,
01046             // so we can find and fix them.
01047 
01048             $handlers = Hooks::getHandlers( $event );
01049             $handlerInfo = array();
01050 
01051             wfSuppressWarnings();
01052 
01053             foreach ( $handlers as $handler ) {
01054                 if ( is_array( $handler ) ) {
01055                     if ( is_object( $handler[0] ) ) {
01056                         $info = get_class( $handler[0] );
01057                     } else {
01058                         $info = $handler[0];
01059                     }
01060 
01061                     if ( isset( $handler[1] ) ) {
01062                         $info .= '::' . $handler[1];
01063                     }
01064                 } elseif ( is_object( $handler ) ) {
01065                     $info = get_class( $handler[0] );
01066                     $info .= '::on' . $event;
01067                 } else {
01068                     $info = $handler;
01069                 }
01070 
01071                 $handlerInfo[] = $info;
01072             }
01073 
01074             wfRestoreWarnings();
01075 
01076             wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " . implode( ', ', $handlerInfo ), 2 );
01077         }
01078 
01079         // convert Content objects to text
01080         $contentObjects = array();
01081         $contentTexts = array();
01082 
01083         foreach ( $args as $k => $v ) {
01084             if ( $v instanceof Content ) {
01085                 /* @var Content $v */
01086 
01087                 $contentObjects[$k] = $v;
01088 
01089                 $v = $v->serialize();
01090                 $contentTexts[$k] = $v;
01091                 $args[$k] = $v;
01092             }
01093         }
01094 
01095         // call the hook functions
01096         $ok = wfRunHooks( $event, $args );
01097 
01098         // see if the hook changed the text
01099         foreach ( $contentTexts as $k => $orig ) {
01100             /* @var Content $content */
01101 
01102             $modified = $args[$k];
01103             $content = $contentObjects[$k];
01104 
01105             if ( $modified !== $orig ) {
01106                 // text was changed, create updated Content object
01107                 $content = $content->getContentHandler()->unserializeContent( $modified );
01108             }
01109 
01110             $args[$k] = $content;
01111         }
01112 
01113         return $ok;
01114     }
01115 }