MediaWiki  REL1_24
ContentHandler.php
Go to the documentation of this file.
00001 <?php
00033 class MWContentSerializationException extends MWException {
00034 }
00035 
00055 abstract class ContentHandler {
00063     protected static $enableDeprecationWarnings = false;
00064 
00094     public static function getContentText( Content $content = null ) {
00095         global $wgContentHandlerTextFallback;
00096 
00097         if ( is_null( $content ) ) {
00098             return '';
00099         }
00100 
00101         if ( $content instanceof TextContent ) {
00102             return $content->getNativeData();
00103         }
00104 
00105         wfDebugLog( 'ContentHandler', 'Accessing ' . $content->getModel() . ' content as text!' );
00106 
00107         if ( $wgContentHandlerTextFallback == 'fail' ) {
00108             throw new MWException(
00109                 "Attempt to get text from Content with model " .
00110                 $content->getModel()
00111             );
00112         }
00113 
00114         if ( $wgContentHandlerTextFallback == 'serialize' ) {
00115             return $content->serialize();
00116         }
00117 
00118         return null;
00119     }
00120 
00144     public static function makeContent( $text, Title $title = null,
00145         $modelId = null, $format = null ) {
00146         if ( is_null( $modelId ) ) {
00147             if ( is_null( $title ) ) {
00148                 throw new MWException( "Must provide a Title object or a content model ID." );
00149             }
00150 
00151             $modelId = $title->getContentModel();
00152         }
00153 
00154         $handler = ContentHandler::getForModelID( $modelId );
00155 
00156         return $handler->unserializeContent( $text, $format );
00157     }
00158 
00193     public static function getDefaultModelFor( Title $title ) {
00194         // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
00195         //       because it is used to initialize the mContentModel member.
00196 
00197         $ns = $title->getNamespace();
00198 
00199         $ext = false;
00200         $m = null;
00201         $model = MWNamespace::getNamespaceContentModel( $ns );
00202 
00203         // Hook can determine default model
00204         if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) {
00205             if ( !is_null( $model ) ) {
00206                 return $model;
00207             }
00208         }
00209 
00210         // Could this page contain custom CSS or JavaScript, based on the title?
00211         $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m );
00212         if ( $isCssOrJsPage ) {
00213             $ext = $m[1];
00214         }
00215 
00216         // Hook can force JS/CSS
00217         wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) );
00218 
00219         // Is this a .css subpage of a user page?
00220         $isJsCssSubpage = NS_USER == $ns
00221             && !$isCssOrJsPage
00222             && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m );
00223         if ( $isJsCssSubpage ) {
00224             $ext = $m[1];
00225         }
00226 
00227         // Is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
00228         $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
00229         $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage;
00230 
00231         // Hook can override $isWikitext
00232         wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
00233 
00234         if ( !$isWikitext ) {
00235             switch ( $ext ) {
00236                 case 'js':
00237                     return CONTENT_MODEL_JAVASCRIPT;
00238                 case 'css':
00239                     return CONTENT_MODEL_CSS;
00240                 default:
00241                     return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
00242             }
00243         }
00244 
00245         // We established that it must be wikitext
00246 
00247         return CONTENT_MODEL_WIKITEXT;
00248     }
00249 
00259     public static function getForTitle( Title $title ) {
00260         $modelId = $title->getContentModel();
00261 
00262         return ContentHandler::getForModelID( $modelId );
00263     }
00264 
00275     public static function getForContent( Content $content ) {
00276         $modelId = $content->getModel();
00277 
00278         return ContentHandler::getForModelID( $modelId );
00279     }
00280 
00284     protected static $handlers;
00285 
00311     public static function getForModelID( $modelId ) {
00312         global $wgContentHandlers;
00313 
00314         if ( isset( ContentHandler::$handlers[$modelId] ) ) {
00315             return ContentHandler::$handlers[$modelId];
00316         }
00317 
00318         if ( empty( $wgContentHandlers[$modelId] ) ) {
00319             $handler = null;
00320 
00321             wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
00322 
00323             if ( $handler === null ) {
00324                 throw new MWException( "No handler for model '$modelId' registered in \$wgContentHandlers" );
00325             }
00326 
00327             if ( !( $handler instanceof ContentHandler ) ) {
00328                 throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
00329             }
00330         } else {
00331             $class = $wgContentHandlers[$modelId];
00332             $handler = new $class( $modelId );
00333 
00334             if ( !( $handler instanceof ContentHandler ) ) {
00335                 throw new MWException( "$class from \$wgContentHandlers is not " .
00336                     "compatible with ContentHandler" );
00337             }
00338         }
00339 
00340         wfDebugLog( 'ContentHandler', 'Created handler for ' . $modelId
00341             . ': ' . get_class( $handler ) );
00342 
00343         ContentHandler::$handlers[$modelId] = $handler;
00344 
00345         return ContentHandler::$handlers[$modelId];
00346     }
00347 
00360     public static function getLocalizedName( $name ) {
00361         // Messages: content-model-wikitext, content-model-text,
00362         // content-model-javascript, content-model-css
00363         $key = "content-model-$name";
00364 
00365         $msg = wfMessage( $key );
00366 
00367         return $msg->exists() ? $msg->plain() : $name;
00368     }
00369 
00370     public static function getContentModels() {
00371         global $wgContentHandlers;
00372 
00373         return array_keys( $wgContentHandlers );
00374     }
00375 
00376     public static function getAllContentFormats() {
00377         global $wgContentHandlers;
00378 
00379         $formats = array();
00380 
00381         foreach ( $wgContentHandlers as $model => $class ) {
00382             $handler = ContentHandler::getForModelID( $model );
00383             $formats = array_merge( $formats, $handler->getSupportedFormats() );
00384         }
00385 
00386         $formats = array_unique( $formats );
00387 
00388         return $formats;
00389     }
00390 
00391     // ------------------------------------------------------------------------
00392 
00396     protected $mModelID;
00397 
00401     protected $mSupportedFormats;
00402 
00412     public function __construct( $modelId, $formats ) {
00413         $this->mModelID = $modelId;
00414         $this->mSupportedFormats = $formats;
00415 
00416         $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
00417         $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
00418         $this->mModelName = strtolower( $this->mModelName );
00419     }
00420 
00431     abstract public function serializeContent( Content $content, $format = null );
00432 
00443     public function exportTransform( $blob, $format = null ) {
00444         return $blob;
00445     }
00446 
00457     abstract public function unserializeContent( $blob, $format = null );
00458 
00470     public function importTransform( $blob, $format = null ) {
00471         return $blob;
00472     }
00473 
00482     abstract public function makeEmptyContent();
00483 
00501     public function makeRedirectContent( Title $destination, $text = '' ) {
00502         return null;
00503     }
00504 
00513     public function getModelID() {
00514         return $this->mModelID;
00515     }
00516 
00525     protected function checkModelID( $model_id ) {
00526         if ( $model_id !== $this->mModelID ) {
00527             throw new MWException( "Bad content model: " .
00528                 "expected {$this->mModelID} " .
00529                 "but got $model_id." );
00530         }
00531     }
00532 
00542     public function getSupportedFormats() {
00543         return $this->mSupportedFormats;
00544     }
00545 
00557     public function getDefaultFormat() {
00558         return $this->mSupportedFormats[0];
00559     }
00560 
00574     public function isSupportedFormat( $format ) {
00575         if ( !$format ) {
00576             return true; // this means "use the default"
00577         }
00578 
00579         return in_array( $format, $this->mSupportedFormats );
00580     }
00581 
00589     protected function checkFormat( $format ) {
00590         if ( !$this->isSupportedFormat( $format ) ) {
00591             throw new MWException(
00592                 "Format $format is not supported for content model "
00593                 . $this->getModelID()
00594             );
00595         }
00596     }
00597 
00608     public function getActionOverrides() {
00609         return array();
00610     }
00611 
00626     public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0,
00627         $rcid = 0, //FIXME: Deprecated, no longer used
00628         $refreshCache = false, $unhide = false ) {
00629         $diffEngineClass = $this->getDiffEngineClass();
00630 
00631         return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
00632     }
00633 
00653     public function getPageLanguage( Title $title, Content $content = null ) {
00654         global $wgContLang, $wgLang;
00655         $pageLang = $wgContLang;
00656 
00657         if ( $title->getNamespace() == NS_MEDIAWIKI ) {
00658             // Parse mediawiki messages with correct target language
00659             list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
00660             $pageLang = wfGetLangObj( $lang );
00661         }
00662 
00663         wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) );
00664 
00665         return wfGetLangObj( $pageLang );
00666     }
00667 
00688     public function getPageViewLanguage( Title $title, Content $content = null ) {
00689         $pageLang = $this->getPageLanguage( $title, $content );
00690 
00691         if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
00692             // If the user chooses a variant, the content is actually
00693             // in a language whose code is the variant code.
00694             $variant = $pageLang->getPreferredVariant();
00695             if ( $pageLang->getCode() !== $variant ) {
00696                 $pageLang = Language::factory( $variant );
00697             }
00698         }
00699 
00700         return $pageLang;
00701     }
00702 
00719     public function canBeUsedOn( Title $title ) {
00720         $ok = true;
00721 
00722         wfRunHooks( 'ContentModelCanBeUsedOn', array( $this->getModelID(), $title, &$ok ) );
00723 
00724         return $ok;
00725     }
00726 
00734     protected function getDiffEngineClass() {
00735         return 'DifferenceEngine';
00736     }
00737 
00752     public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
00753         return false;
00754     }
00755 
00767     public function getAutosummary( Content $oldContent = null, Content $newContent = null,
00768         $flags ) {
00769         // Decide what kind of auto-summary is needed.
00770 
00771         // Redirect auto-summaries
00772 
00778         $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
00779         $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
00780 
00781         if ( is_object( $rt ) ) {
00782             if ( !is_object( $ot )
00783                 || !$rt->equals( $ot )
00784                 || $ot->getFragment() != $rt->getFragment()
00785             ) {
00786                 $truncatedtext = $newContent->getTextForSummary(
00787                     250
00788                     - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
00789                     - strlen( $rt->getFullText() ) );
00790 
00791                 return wfMessage( 'autoredircomment', $rt->getFullText() )
00792                     ->rawParams( $truncatedtext )->inContentLanguage()->text();
00793             }
00794         }
00795 
00796         // New page auto-summaries
00797         if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
00798             // If they're making a new article, give its text, truncated, in
00799             // the summary.
00800 
00801             $truncatedtext = $newContent->getTextForSummary(
00802                 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
00803 
00804             return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
00805                 ->inContentLanguage()->text();
00806         }
00807 
00808         // Blanking auto-summaries
00809         if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
00810             return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
00811         } elseif ( !empty( $oldContent )
00812             && $oldContent->getSize() > 10 * $newContent->getSize()
00813             && $newContent->getSize() < 500
00814         ) {
00815             // Removing more than 90% of the article
00816 
00817             $truncatedtext = $newContent->getTextForSummary(
00818                 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
00819 
00820             return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
00821                 ->inContentLanguage()->text();
00822         }
00823 
00824         // New blank article auto-summary
00825         if ( $flags & EDIT_NEW && $newContent->isEmpty() ) {
00826             return wfMessage( 'autosumm-newblank' )->inContentLanguage()->text();
00827         }
00828 
00829         // If we reach this point, there's no applicable auto-summary for our
00830         // case, so our auto-summary is empty.
00831         return '';
00832     }
00833 
00849     public function getAutoDeleteReason( Title $title, &$hasHistory ) {
00850         $dbw = wfGetDB( DB_MASTER );
00851 
00852         // Get the last revision
00853         $rev = Revision::newFromTitle( $title );
00854 
00855         if ( is_null( $rev ) ) {
00856             return false;
00857         }
00858 
00859         // Get the article's contents
00860         $content = $rev->getContent();
00861         $blank = false;
00862 
00863         // If the page is blank, use the text from the previous revision,
00864         // which can only be blank if there's a move/import/protect dummy
00865         // revision involved
00866         if ( !$content || $content->isEmpty() ) {
00867             $prev = $rev->getPrevious();
00868 
00869             if ( $prev ) {
00870                 $rev = $prev;
00871                 $content = $rev->getContent();
00872                 $blank = true;
00873             }
00874         }
00875 
00876         $this->checkModelID( $rev->getContentModel() );
00877 
00878         // Find out if there was only one contributor
00879         // Only scan the last 20 revisions
00880         $res = $dbw->select( 'revision', 'rev_user_text',
00881             array(
00882                 'rev_page' => $title->getArticleID(),
00883                 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
00884             ),
00885             __METHOD__,
00886             array( 'LIMIT' => 20 )
00887         );
00888 
00889         if ( $res === false ) {
00890             // This page has no revisions, which is very weird
00891             return false;
00892         }
00893 
00894         $hasHistory = ( $res->numRows() > 1 );
00895         $row = $dbw->fetchObject( $res );
00896 
00897         if ( $row ) { // $row is false if the only contributor is hidden
00898             $onlyAuthor = $row->rev_user_text;
00899             // Try to find a second contributor
00900             foreach ( $res as $row ) {
00901                 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
00902                     $onlyAuthor = false;
00903                     break;
00904                 }
00905             }
00906         } else {
00907             $onlyAuthor = false;
00908         }
00909 
00910         // Generate the summary with a '$1' placeholder
00911         if ( $blank ) {
00912             // The current revision is blank and the one before is also
00913             // blank. It's just not our lucky day
00914             $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
00915         } else {
00916             if ( $onlyAuthor ) {
00917                 $reason = wfMessage(
00918                     'excontentauthor',
00919                     '$1',
00920                     $onlyAuthor
00921                 )->inContentLanguage()->text();
00922             } else {
00923                 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
00924             }
00925         }
00926 
00927         if ( $reason == '-' ) {
00928             // Allow these UI messages to be blanked out cleanly
00929             return '';
00930         }
00931 
00932         // Max content length = max comment length - length of the comment (excl. $1)
00933         $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
00934 
00935         // Now replace the '$1' placeholder
00936         $reason = str_replace( '$1', $text, $reason );
00937 
00938         return $reason;
00939     }
00940 
00954     public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
00955         $cur_content = $current->getContent();
00956 
00957         if ( empty( $cur_content ) ) {
00958             return false; // no page
00959         }
00960 
00961         $undo_content = $undo->getContent();
00962         $undoafter_content = $undoafter->getContent();
00963 
00964         if ( !$undo_content || !$undoafter_content ) {
00965             return false; // no content to undo
00966         }
00967 
00968         $this->checkModelID( $cur_content->getModel() );
00969         $this->checkModelID( $undo_content->getModel() );
00970         $this->checkModelID( $undoafter_content->getModel() );
00971 
00972         if ( $cur_content->equals( $undo_content ) ) {
00973             // No use doing a merge if it's just a straight revert.
00974             return $undoafter_content;
00975         }
00976 
00977         $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
00978 
00979         return $undone_content;
00980     }
00981 
00996     public function makeParserOptions( $context ) {
00997         global $wgContLang, $wgEnableParserLimitReporting;
00998 
00999         if ( $context instanceof IContextSource ) {
01000             $options = ParserOptions::newFromContext( $context );
01001         } elseif ( $context instanceof User ) { // settings per user (even anons)
01002             $options = ParserOptions::newFromUser( $context );
01003         } elseif ( $context === 'canonical' ) { // canonical settings
01004             $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
01005         } else {
01006             throw new MWException( "Bad context for parser options: $context" );
01007         }
01008 
01009         $options->enableLimitReport( $wgEnableParserLimitReporting ); // show inclusion/loop reports
01010         $options->setTidy( true ); // fix bad HTML
01011 
01012         return $options;
01013     }
01014 
01023     public function isParserCacheSupported() {
01024         return false;
01025     }
01026 
01036     public function supportsSections() {
01037         return false;
01038     }
01039 
01049     public function supportsRedirects() {
01050         return false;
01051     }
01052 
01066     public static function deprecated( $func, $version, $component = false ) {
01067         if ( self::$enableDeprecationWarnings ) {
01068             wfDeprecated( $func, $version, $component, 3 );
01069         }
01070     }
01071 
01089     public static function runLegacyHooks( $event, $args = array(),
01090         $warn = null
01091     ) {
01092 
01093         if ( $warn === null ) {
01094             $warn = self::$enableDeprecationWarnings;
01095         }
01096 
01097         if ( !Hooks::isRegistered( $event ) ) {
01098             return true; // nothing to do here
01099         }
01100 
01101         if ( $warn ) {
01102             // Log information about which handlers are registered for the legacy hook,
01103             // so we can find and fix them.
01104 
01105             $handlers = Hooks::getHandlers( $event );
01106             $handlerInfo = array();
01107 
01108             wfSuppressWarnings();
01109 
01110             foreach ( $handlers as $handler ) {
01111                 if ( is_array( $handler ) ) {
01112                     if ( is_object( $handler[0] ) ) {
01113                         $info = get_class( $handler[0] );
01114                     } else {
01115                         $info = $handler[0];
01116                     }
01117 
01118                     if ( isset( $handler[1] ) ) {
01119                         $info .= '::' . $handler[1];
01120                     }
01121                 } elseif ( is_object( $handler ) ) {
01122                     $info = get_class( $handler[0] );
01123                     $info .= '::on' . $event;
01124                 } else {
01125                     $info = $handler;
01126                 }
01127 
01128                 $handlerInfo[] = $info;
01129             }
01130 
01131             wfRestoreWarnings();
01132 
01133             wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " .
01134                 implode( ', ', $handlerInfo ), 2 );
01135         }
01136 
01137         // convert Content objects to text
01138         $contentObjects = array();
01139         $contentTexts = array();
01140 
01141         foreach ( $args as $k => $v ) {
01142             if ( $v instanceof Content ) {
01143                 /* @var Content $v */
01144 
01145                 $contentObjects[$k] = $v;
01146 
01147                 $v = $v->serialize();
01148                 $contentTexts[$k] = $v;
01149                 $args[$k] = $v;
01150             }
01151         }
01152 
01153         // call the hook functions
01154         $ok = wfRunHooks( $event, $args );
01155 
01156         // see if the hook changed the text
01157         foreach ( $contentTexts as $k => $orig ) {
01158             /* @var Content $content */
01159 
01160             $modified = $args[$k];
01161             $content = $contentObjects[$k];
01162 
01163             if ( $modified !== $orig ) {
01164                 // text was changed, create updated Content object
01165                 $content = $content->getContentHandler()->unserializeContent( $modified );
01166             }
01167 
01168             $args[$k] = $content;
01169         }
01170 
01171         return $ok;
01172     }
01173 }