MediaWiki  REL1_23
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     abstract public function unserializeContent( $blob, $format = null );
00444 
00453     abstract public function makeEmptyContent();
00454 
00472     public function makeRedirectContent( Title $destination, $text = '' ) {
00473         return null;
00474     }
00475 
00484     public function getModelID() {
00485         return $this->mModelID;
00486     }
00487 
00496     protected function checkModelID( $model_id ) {
00497         if ( $model_id !== $this->mModelID ) {
00498             throw new MWException( "Bad content model: " .
00499                 "expected {$this->mModelID} " .
00500                 "but got $model_id." );
00501         }
00502     }
00503 
00513     public function getSupportedFormats() {
00514         return $this->mSupportedFormats;
00515     }
00516 
00528     public function getDefaultFormat() {
00529         return $this->mSupportedFormats[0];
00530     }
00531 
00545     public function isSupportedFormat( $format ) {
00546         if ( !$format ) {
00547             return true; // this means "use the default"
00548         }
00549 
00550         return in_array( $format, $this->mSupportedFormats );
00551     }
00552 
00560     protected function checkFormat( $format ) {
00561         if ( !$this->isSupportedFormat( $format ) ) {
00562             throw new MWException(
00563                 "Format $format is not supported for content model "
00564                 . $this->getModelID()
00565             );
00566         }
00567     }
00568 
00579     public function getActionOverrides() {
00580         return array();
00581     }
00582 
00597     public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0,
00598         $rcid = 0, //FIXME: Deprecated, no longer used
00599         $refreshCache = false, $unhide = false ) {
00600         $diffEngineClass = $this->getDiffEngineClass();
00601 
00602         return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
00603     }
00604 
00624     public function getPageLanguage( Title $title, Content $content = null ) {
00625         global $wgContLang, $wgLang;
00626         $pageLang = $wgContLang;
00627 
00628         if ( $title->getNamespace() == NS_MEDIAWIKI ) {
00629             // Parse mediawiki messages with correct target language
00630             list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
00631             $pageLang = wfGetLangObj( $lang );
00632         }
00633 
00634         wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) );
00635 
00636         return wfGetLangObj( $pageLang );
00637     }
00638 
00659     public function getPageViewLanguage( Title $title, Content $content = null ) {
00660         $pageLang = $this->getPageLanguage( $title, $content );
00661 
00662         if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
00663             // If the user chooses a variant, the content is actually
00664             // in a language whose code is the variant code.
00665             $variant = $pageLang->getPreferredVariant();
00666             if ( $pageLang->getCode() !== $variant ) {
00667                 $pageLang = Language::factory( $variant );
00668             }
00669         }
00670 
00671         return $pageLang;
00672     }
00673 
00690     public function canBeUsedOn( Title $title ) {
00691         $ok = true;
00692 
00693         wfRunHooks( 'ContentModelCanBeUsedOn', array( $this->getModelID(), $title, &$ok ) );
00694 
00695         return $ok;
00696     }
00697 
00705     protected function getDiffEngineClass() {
00706         return 'DifferenceEngine';
00707     }
00708 
00723     public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
00724         return false;
00725     }
00726 
00738     public function getAutosummary( Content $oldContent = null, Content $newContent = null,
00739         $flags ) {
00740         // Decide what kind of auto-summary is needed.
00741 
00742         // Redirect auto-summaries
00743 
00749         $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
00750         $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
00751 
00752         if ( is_object( $rt ) ) {
00753             if ( !is_object( $ot )
00754                 || !$rt->equals( $ot )
00755                 || $ot->getFragment() != $rt->getFragment()
00756             ) {
00757                 $truncatedtext = $newContent->getTextForSummary(
00758                     250
00759                     - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
00760                     - strlen( $rt->getFullText() ) );
00761 
00762                 return wfMessage( 'autoredircomment', $rt->getFullText() )
00763                     ->rawParams( $truncatedtext )->inContentLanguage()->text();
00764             }
00765         }
00766 
00767         // New page auto-summaries
00768         if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
00769             // If they're making a new article, give its text, truncated, in
00770             // the summary.
00771 
00772             $truncatedtext = $newContent->getTextForSummary(
00773                 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
00774 
00775             return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
00776                 ->inContentLanguage()->text();
00777         }
00778 
00779         // Blanking auto-summaries
00780         if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
00781             return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
00782         } elseif ( !empty( $oldContent )
00783             && $oldContent->getSize() > 10 * $newContent->getSize()
00784             && $newContent->getSize() < 500
00785         ) {
00786             // Removing more than 90% of the article
00787 
00788             $truncatedtext = $newContent->getTextForSummary(
00789                 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
00790 
00791             return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
00792                 ->inContentLanguage()->text();
00793         }
00794 
00795         // If we reach this point, there's no applicable auto-summary for our
00796         // case, so our auto-summary is empty.
00797         return '';
00798     }
00799 
00815     public function getAutoDeleteReason( Title $title, &$hasHistory ) {
00816         $dbw = wfGetDB( DB_MASTER );
00817 
00818         // Get the last revision
00819         $rev = Revision::newFromTitle( $title );
00820 
00821         if ( is_null( $rev ) ) {
00822             return false;
00823         }
00824 
00825         // Get the article's contents
00826         $content = $rev->getContent();
00827         $blank = false;
00828 
00829         // If the page is blank, use the text from the previous revision,
00830         // which can only be blank if there's a move/import/protect dummy
00831         // revision involved
00832         if ( !$content || $content->isEmpty() ) {
00833             $prev = $rev->getPrevious();
00834 
00835             if ( $prev ) {
00836                 $rev = $prev;
00837                 $content = $rev->getContent();
00838                 $blank = true;
00839             }
00840         }
00841 
00842         $this->checkModelID( $rev->getContentModel() );
00843 
00844         // Find out if there was only one contributor
00845         // Only scan the last 20 revisions
00846         $res = $dbw->select( 'revision', 'rev_user_text',
00847             array(
00848                 'rev_page' => $title->getArticleID(),
00849                 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
00850             ),
00851             __METHOD__,
00852             array( 'LIMIT' => 20 )
00853         );
00854 
00855         if ( $res === false ) {
00856             // This page has no revisions, which is very weird
00857             return false;
00858         }
00859 
00860         $hasHistory = ( $res->numRows() > 1 );
00861         $row = $dbw->fetchObject( $res );
00862 
00863         if ( $row ) { // $row is false if the only contributor is hidden
00864             $onlyAuthor = $row->rev_user_text;
00865             // Try to find a second contributor
00866             foreach ( $res as $row ) {
00867                 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
00868                     $onlyAuthor = false;
00869                     break;
00870                 }
00871             }
00872         } else {
00873             $onlyAuthor = false;
00874         }
00875 
00876         // Generate the summary with a '$1' placeholder
00877         if ( $blank ) {
00878             // The current revision is blank and the one before is also
00879             // blank. It's just not our lucky day
00880             $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
00881         } else {
00882             if ( $onlyAuthor ) {
00883                 $reason = wfMessage(
00884                     'excontentauthor',
00885                     '$1',
00886                     $onlyAuthor
00887                 )->inContentLanguage()->text();
00888             } else {
00889                 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
00890             }
00891         }
00892 
00893         if ( $reason == '-' ) {
00894             // Allow these UI messages to be blanked out cleanly
00895             return '';
00896         }
00897 
00898         // Max content length = max comment length - length of the comment (excl. $1)
00899         $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
00900 
00901         // Now replace the '$1' placeholder
00902         $reason = str_replace( '$1', $text, $reason );
00903 
00904         return $reason;
00905     }
00906 
00920     public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
00921         $cur_content = $current->getContent();
00922 
00923         if ( empty( $cur_content ) ) {
00924             return false; // no page
00925         }
00926 
00927         $undo_content = $undo->getContent();
00928         $undoafter_content = $undoafter->getContent();
00929 
00930         if ( !$undo_content || !$undoafter_content ) {
00931             return false; // no content to undo
00932         }
00933 
00934         $this->checkModelID( $cur_content->getModel() );
00935         $this->checkModelID( $undo_content->getModel() );
00936         $this->checkModelID( $undoafter_content->getModel() );
00937 
00938         if ( $cur_content->equals( $undo_content ) ) {
00939             // No use doing a merge if it's just a straight revert.
00940             return $undoafter_content;
00941         }
00942 
00943         $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
00944 
00945         return $undone_content;
00946     }
00947 
00962     public function makeParserOptions( $context ) {
00963         global $wgContLang, $wgEnableParserLimitReporting;
00964 
00965         if ( $context instanceof IContextSource ) {
00966             $options = ParserOptions::newFromContext( $context );
00967         } elseif ( $context instanceof User ) { // settings per user (even anons)
00968             $options = ParserOptions::newFromUser( $context );
00969         } elseif ( $context === 'canonical' ) { // canonical settings
00970             $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
00971         } else {
00972             throw new MWException( "Bad context for parser options: $context" );
00973         }
00974 
00975         $options->enableLimitReport( $wgEnableParserLimitReporting ); // show inclusion/loop reports
00976         $options->setTidy( true ); // fix bad HTML
00977 
00978         return $options;
00979     }
00980 
00989     public function isParserCacheSupported() {
00990         return false;
00991     }
00992 
01002     public function supportsSections() {
01003         return false;
01004     }
01005 
01015     public function supportsRedirects() {
01016         return false;
01017     }
01018 
01032     public static function deprecated( $func, $version, $component = false ) {
01033         if ( self::$enableDeprecationWarnings ) {
01034             wfDeprecated( $func, $version, $component, 3 );
01035         }
01036     }
01037 
01055     public static function runLegacyHooks( $event, $args = array(),
01056         $warn = null
01057     ) {
01058 
01059         if ( $warn === null ) {
01060             $warn = self::$enableDeprecationWarnings;
01061         }
01062 
01063         if ( !Hooks::isRegistered( $event ) ) {
01064             return true; // nothing to do here
01065         }
01066 
01067         if ( $warn ) {
01068             // Log information about which handlers are registered for the legacy hook,
01069             // so we can find and fix them.
01070 
01071             $handlers = Hooks::getHandlers( $event );
01072             $handlerInfo = array();
01073 
01074             wfSuppressWarnings();
01075 
01076             foreach ( $handlers as $handler ) {
01077                 if ( is_array( $handler ) ) {
01078                     if ( is_object( $handler[0] ) ) {
01079                         $info = get_class( $handler[0] );
01080                     } else {
01081                         $info = $handler[0];
01082                     }
01083 
01084                     if ( isset( $handler[1] ) ) {
01085                         $info .= '::' . $handler[1];
01086                     }
01087                 } elseif ( is_object( $handler ) ) {
01088                     $info = get_class( $handler[0] );
01089                     $info .= '::on' . $event;
01090                 } else {
01091                     $info = $handler;
01092                 }
01093 
01094                 $handlerInfo[] = $info;
01095             }
01096 
01097             wfRestoreWarnings();
01098 
01099             wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " .
01100                 implode( ', ', $handlerInfo ), 2 );
01101         }
01102 
01103         // convert Content objects to text
01104         $contentObjects = array();
01105         $contentTexts = array();
01106 
01107         foreach ( $args as $k => $v ) {
01108             if ( $v instanceof Content ) {
01109                 /* @var Content $v */
01110 
01111                 $contentObjects[$k] = $v;
01112 
01113                 $v = $v->serialize();
01114                 $contentTexts[$k] = $v;
01115                 $args[$k] = $v;
01116             }
01117         }
01118 
01119         // call the hook functions
01120         $ok = wfRunHooks( $event, $args );
01121 
01122         // see if the hook changed the text
01123         foreach ( $contentTexts as $k => $orig ) {
01124             /* @var Content $content */
01125 
01126             $modified = $args[$k];
01127             $content = $contentObjects[$k];
01128 
01129             if ( $modified !== $orig ) {
01130                 // text was changed, create updated Content object
01131                 $content = $content->getContentHandler()->unserializeContent( $modified );
01132             }
01133 
01134             $args[$k] = $content;
01135         }
01136 
01137         return $ok;
01138     }
01139 }