MediaWiki  master
ContentHandler.php
Go to the documentation of this file.
00001 <?php
00002 
00006 class MWContentSerializationException extends MWException {
00007 
00008 }
00009 
00049 abstract class ContentHandler {
00050 
00058         protected static $enableDeprecationWarnings = false;
00059 
00088         public static function getContentText( Content $content = null ) {
00089                 global $wgContentHandlerTextFallback;
00090 
00091                 if ( is_null( $content ) ) {
00092                         return '';
00093                 }
00094 
00095                 if ( $content instanceof TextContent ) {
00096                         return $content->getNativeData();
00097                 }
00098 
00099                 wfDebugLog( 'ContentHandler', 'Accessing ' . $content->getModel() . ' content as text!' );
00100 
00101                 if ( $wgContentHandlerTextFallback == 'fail' ) {
00102                         throw new MWException(
00103                                 "Attempt to get text from Content with model " .
00104                                 $content->getModel()
00105                         );
00106                 }
00107 
00108                 if ( $wgContentHandlerTextFallback == 'serialize' ) {
00109                         return $content->serialize();
00110                 }
00111 
00112                 return null;
00113         }
00114 
00139         public static function makeContent( $text, Title $title = null,
00140                 $modelId = null, $format = null )
00141         {
00142                 if ( is_null( $modelId ) ) {
00143                         if ( is_null( $title ) ) {
00144                                 throw new MWException( "Must provide a Title object or a content model ID." );
00145                         }
00146 
00147                         $modelId = $title->getContentModel();
00148                 }
00149 
00150                 $handler = ContentHandler::getForModelID( $modelId );
00151                 return $handler->unserializeContent( $text, $format );
00152         }
00153 
00187         public static function getDefaultModelFor( Title $title ) {
00188                 global $wgNamespaceContentModels;
00189 
00190                 // NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
00191                 //       because it is used to initialize the mContentModel member.
00192 
00193                 $ns = $title->getNamespace();
00194 
00195                 $ext = false;
00196                 $m = null;
00197                 $model = null;
00198 
00199                 if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
00200                         $model = $wgNamespaceContentModels[ $ns ];
00201                 }
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 
00258         public static function getForTitle( Title $title ) {
00259                 $modelId = $title->getContentModel();
00260                 return ContentHandler::getForModelID( $modelId );
00261         }
00262 
00272         public static function getForContent( Content $content ) {
00273                 $modelId = $content->getModel();
00274                 return ContentHandler::getForModelID( $modelId );
00275         }
00276 
00280         static $handlers;
00281 
00307         public static function getForModelID( $modelId ) {
00308                 global $wgContentHandlers;
00309 
00310                 if ( isset( ContentHandler::$handlers[$modelId] ) ) {
00311                         return ContentHandler::$handlers[$modelId];
00312                 }
00313 
00314                 if ( empty( $wgContentHandlers[$modelId] ) ) {
00315                         $handler = null;
00316 
00317                         wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
00318 
00319                         if ( $handler === null ) {
00320                                 throw new MWException( "No handler for model '$modelId'' registered in \$wgContentHandlers" );
00321                         }
00322 
00323                         if ( !( $handler instanceof ContentHandler ) ) {
00324                                 throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
00325                         }
00326                 } else {
00327                         $class = $wgContentHandlers[$modelId];
00328                         $handler = new $class( $modelId );
00329 
00330                         if ( !( $handler instanceof ContentHandler ) ) {
00331                                 throw new MWException( "$class from \$wgContentHandlers is not compatible with ContentHandler" );
00332                         }
00333                 }
00334 
00335                 wfDebugLog( 'ContentHandler', 'Created handler for ' . $modelId
00336                                         . ': ' . get_class( $handler ) );
00337 
00338                 ContentHandler::$handlers[$modelId] = $handler;
00339                 return ContentHandler::$handlers[$modelId];
00340         }
00341 
00354         public static function getLocalizedName( $name ) {
00355                 $key = "content-model-$name";
00356 
00357                 $msg = wfMessage( $key );
00358 
00359                 return $msg->exists() ? $msg->plain() : $name;
00360         }
00361 
00362         public static function getContentModels() {
00363                 global $wgContentHandlers;
00364 
00365                 return array_keys( $wgContentHandlers );
00366         }
00367 
00368         public static function getAllContentFormats() {
00369                 global $wgContentHandlers;
00370 
00371                 $formats = array();
00372 
00373                 foreach ( $wgContentHandlers as $model => $class ) {
00374                         $handler = ContentHandler::getForModelID( $model );
00375                         $formats = array_merge( $formats, $handler->getSupportedFormats() );
00376                 }
00377 
00378                 $formats = array_unique( $formats );
00379                 return $formats;
00380         }
00381 
00382         // ------------------------------------------------------------------------
00383 
00384         protected $mModelID;
00385         protected $mSupportedFormats;
00386 
00396         public function __construct( $modelId, $formats ) {
00397                 $this->mModelID = $modelId;
00398                 $this->mSupportedFormats = $formats;
00399 
00400                 $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
00401                 $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
00402                 $this->mModelName = strtolower( $this->mModelName );
00403         }
00404 
00414         public abstract function serializeContent( Content $content, $format = null );
00415 
00425         public abstract function unserializeContent( $blob, $format = null );
00426 
00435         public abstract function makeEmptyContent();
00436 
00450         public function makeRedirectContent( Title $destination ) {
00451                 return null;
00452         }
00453 
00462         public function getModelID() {
00463                 return $this->mModelID;
00464         }
00465 
00476         protected function checkModelID( $model_id ) {
00477                 if ( $model_id !== $this->mModelID ) {
00478                         throw new MWException( "Bad content model: " .
00479                                 "expected {$this->mModelID} " .
00480                                 "but got $model_id." );
00481                 }
00482         }
00483 
00493         public function getSupportedFormats() {
00494                 return $this->mSupportedFormats;
00495         }
00496 
00508         public function getDefaultFormat() {
00509                 return $this->mSupportedFormats[0];
00510         }
00511 
00524         public function isSupportedFormat( $format ) {
00525 
00526                 if ( !$format ) {
00527                         return true; // this means "use the default"
00528                 }
00529 
00530                 return in_array( $format, $this->mSupportedFormats );
00531         }
00532 
00542         protected function checkFormat( $format ) {
00543                 if ( !$this->isSupportedFormat( $format ) ) {
00544                         throw new MWException(
00545                                 "Format $format is not supported for content model "
00546                                 . $this->getModelID()
00547                         );
00548                 }
00549         }
00550 
00561         public function getActionOverrides() {
00562                 return array();
00563         }
00564 
00580         public function createDifferenceEngine( IContextSource $context,
00581                 $old = 0, $new = 0,
00582                 $rcid = 0, # FIXME: use everywhere!
00583                 $refreshCache = false, $unhide = false
00584         ) {
00585                 $diffEngineClass = $this->getDiffEngineClass();
00586 
00587                 return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
00588         }
00589 
00607         public function getPageLanguage( Title $title, Content $content = null ) {
00608                 global $wgContLang, $wgLang;
00609                 $pageLang = $wgContLang;
00610 
00611                 if ( $title->getNamespace() == NS_MEDIAWIKI ) {
00612                         // Parse mediawiki messages with correct target language
00613                         list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
00614                         $pageLang = wfGetLangObj( $lang );
00615                 }
00616 
00617                 wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) );
00618                 return wfGetLangObj( $pageLang );
00619         }
00620 
00641         public function getPageViewLanguage( Title $title, Content $content = null ) {
00642                 $pageLang = $this->getPageLanguage( $title, $content );
00643 
00644                 if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
00645                         // If the user chooses a variant, the content is actually
00646                         // in a language whose code is the variant code.
00647                         $variant = $pageLang->getPreferredVariant();
00648                         if ( $pageLang->getCode() !== $variant ) {
00649                                 $pageLang = Language::factory( $variant );
00650                         }
00651                 }
00652 
00653                 return $pageLang;
00654         }
00655 
00669         public function canBeUsedOn( Title $title ) {
00670                 return true;
00671         }
00672 
00680         protected function getDiffEngineClass() {
00681                 return 'DifferenceEngine';
00682         }
00683 
00699         public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
00700                 return false;
00701         }
00702 
00714         public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
00715                 global $wgContLang;
00716 
00717                 // Decide what kind of auto-summary is needed.
00718 
00719                 // Redirect auto-summaries
00720 
00726                 $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
00727                 $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
00728 
00729                 if ( is_object( $rt ) ) {
00730                         if ( !is_object( $ot )
00731                                 || !$rt->equals( $ot )
00732                                 || $ot->getFragment() != $rt->getFragment() )
00733                         {
00734                                 $truncatedtext = $newContent->getTextForSummary(
00735                                         250
00736                                                 - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
00737                                                 - strlen( $rt->getFullText() ) );
00738 
00739                                 return wfMessage( 'autoredircomment', $rt->getFullText() )
00740                                                 ->rawParams( $truncatedtext )->inContentLanguage()->text();
00741                         }
00742                 }
00743 
00744                 // New page auto-summaries
00745                 if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
00746                         // If they're making a new article, give its text, truncated, in
00747                         // the summary.
00748 
00749                         $truncatedtext = $newContent->getTextForSummary(
00750                                 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
00751 
00752                         return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
00753                                         ->inContentLanguage()->text();
00754                 }
00755 
00756                 // Blanking auto-summaries
00757                 if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
00758                         return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
00759                 } elseif ( !empty( $oldContent )
00760                         && $oldContent->getSize() > 10 * $newContent->getSize()
00761                         && $newContent->getSize() < 500 )
00762                 {
00763                         // Removing more than 90% of the article
00764 
00765                         $truncatedtext = $newContent->getTextForSummary(
00766                                 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
00767 
00768                         return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
00769                                         ->inContentLanguage()->text();
00770                 }
00771 
00772                 // If we reach this point, there's no applicable auto-summary for our
00773                 // case, so our auto-summary is empty.
00774                 return '';
00775         }
00776 
00791         public function getAutoDeleteReason( Title $title, &$hasHistory ) {
00792                 $dbw = wfGetDB( DB_MASTER );
00793 
00794                 // Get the last revision
00795                 $rev = Revision::newFromTitle( $title );
00796 
00797                 if ( is_null( $rev ) ) {
00798                         return false;
00799                 }
00800 
00801                 // Get the article's contents
00802                 $content = $rev->getContent();
00803                 $blank = false;
00804 
00805                 // If the page is blank, use the text from the previous revision,
00806                 // which can only be blank if there's a move/import/protect dummy
00807                 // revision involved
00808                 if ( !$content || $content->isEmpty() ) {
00809                         $prev = $rev->getPrevious();
00810 
00811                         if ( $prev ) {
00812                                 $rev = $prev;
00813                                 $content = $rev->getContent();
00814                                 $blank = true;
00815                         }
00816                 }
00817 
00818                 $this->checkModelID( $rev->getContentModel() );
00819 
00820                 // Find out if there was only one contributor
00821                 // Only scan the last 20 revisions
00822                 $res = $dbw->select( 'revision', 'rev_user_text',
00823                         array(
00824                                 'rev_page' => $title->getArticleID(),
00825                                 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
00826                         ),
00827                         __METHOD__,
00828                         array( 'LIMIT' => 20 )
00829                 );
00830 
00831                 if ( $res === false ) {
00832                         // This page has no revisions, which is very weird
00833                         return false;
00834                 }
00835 
00836                 $hasHistory = ( $res->numRows() > 1 );
00837                 $row = $dbw->fetchObject( $res );
00838 
00839                 if ( $row ) { // $row is false if the only contributor is hidden
00840                         $onlyAuthor = $row->rev_user_text;
00841                         // Try to find a second contributor
00842                         foreach ( $res as $row ) {
00843                                 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
00844                                         $onlyAuthor = false;
00845                                         break;
00846                                 }
00847                         }
00848                 } else {
00849                         $onlyAuthor = false;
00850                 }
00851 
00852                 // Generate the summary with a '$1' placeholder
00853                 if ( $blank ) {
00854                         // The current revision is blank and the one before is also
00855                         // blank. It's just not our lucky day
00856                         $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
00857                 } else {
00858                         if ( $onlyAuthor ) {
00859                                 $reason = wfMessage(
00860                                         'excontentauthor',
00861                                         '$1',
00862                                         $onlyAuthor
00863                                 )->inContentLanguage()->text();
00864                         } else {
00865                                 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
00866                         }
00867                 }
00868 
00869                 if ( $reason == '-' ) {
00870                         // Allow these UI messages to be blanked out cleanly
00871                         return '';
00872                 }
00873 
00874                 // Max content length = max comment length - length of the comment (excl. $1)
00875                 $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
00876 
00877                 // Now replace the '$1' placeholder
00878                 $reason = str_replace( '$1', $text, $reason );
00879 
00880                 return $reason;
00881         }
00882 
00896         public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
00897                 $cur_content = $current->getContent();
00898 
00899                 if ( empty( $cur_content ) ) {
00900                         return false; // no page
00901                 }
00902 
00903                 $undo_content = $undo->getContent();
00904                 $undoafter_content = $undoafter->getContent();
00905 
00906                 $this->checkModelID( $cur_content->getModel() );
00907                 $this->checkModelID( $undo_content->getModel() );
00908                 $this->checkModelID( $undoafter_content->getModel() );
00909 
00910                 if ( $cur_content->equals( $undo_content ) ) {
00911                         // No use doing a merge if it's just a straight revert.
00912                         return $undoafter_content;
00913                 }
00914 
00915                 $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
00916 
00917                 return $undone_content;
00918         }
00919 
00936         public function makeParserOptions( $context ) {
00937                 global $wgContLang;
00938 
00939                 if ( $context instanceof IContextSource ) {
00940                         $options = ParserOptions::newFromContext( $context );
00941                 } elseif ( $context instanceof User ) { // settings per user (even anons)
00942                         $options = ParserOptions::newFromUser( $context );
00943                 } elseif ( $context === 'canonical' ) { // canonical settings
00944                         $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
00945                 } else {
00946                         throw new MWException( "Bad context for parser options: $context" );
00947                 }
00948 
00949                 $options->enableLimitReport(); // show inclusion/loop reports
00950                 $options->setTidy( true ); // fix bad HTML
00951 
00952                 return $options;
00953         }
00954 
00963         public function isParserCacheSupported() {
00964                 return false;
00965         }
00966 
00974         public function supportsSections() {
00975                 return false;
00976         }
00977 
00991         public static function deprecated( $func, $version, $component = false ) {
00992                 if ( self::$enableDeprecationWarnings ) {
00993                         wfDeprecated( $func, $version, $component, 3 );
00994                 }
00995         }
00996 
01014         public static function runLegacyHooks( $event, $args = array(),
01015                         $warn = null ) {
01016 
01017                 if ( $warn === null ) {
01018                         $warn = self::$enableDeprecationWarnings;
01019                 }
01020 
01021                 if ( !Hooks::isRegistered( $event ) ) {
01022                         return true; // nothing to do here
01023                 }
01024 
01025                 if ( $warn ) {
01026                         // Log information about which handlers are registered for the legacy hook,
01027                         // so we can find and fix them.
01028 
01029                         $handlers = Hooks::getHandlers( $event );
01030                         $handlerInfo = array();
01031 
01032                         wfSuppressWarnings();
01033 
01034                         foreach ( $handlers as $handler ) {
01035                                 $info = '';
01036 
01037                                 if ( is_array( $handler ) ) {
01038                                         if ( is_object( $handler[0] ) ) {
01039                                                 $info = get_class( $handler[0] );
01040                                         } else {
01041                                                 $info = $handler[0];
01042                                         }
01043 
01044                                         if ( isset( $handler[1] ) ) {
01045                                                 $info .= '::' . $handler[1];
01046                                         }
01047                                 } else if ( is_object( $handler ) ) {
01048                                         $info = get_class( $handler[0] );
01049                                         $info .= '::on' . $event;
01050                                 } else {
01051                                         $info = $handler;
01052                                 }
01053 
01054                                 $handlerInfo[] = $info;
01055                         }
01056 
01057                         wfRestoreWarnings();
01058 
01059                         wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " . implode(', ', $handlerInfo), 2 );
01060                 }
01061 
01062                 // convert Content objects to text
01063                 $contentObjects = array();
01064                 $contentTexts = array();
01065 
01066                 foreach ( $args as $k => $v ) {
01067                         if ( $v instanceof Content ) {
01068                                 /* @var Content $v */
01069 
01070                                 $contentObjects[$k] = $v;
01071 
01072                                 $v = $v->serialize();
01073                                 $contentTexts[ $k ] = $v;
01074                                 $args[ $k ] = $v;
01075                         }
01076                 }
01077 
01078                 // call the hook functions
01079                 $ok = wfRunHooks( $event, $args );
01080 
01081                 // see if the hook changed the text
01082                 foreach ( $contentTexts as $k => $orig ) {
01083                         /* @var Content $content */
01084 
01085                         $modified = $args[ $k ];
01086                         $content = $contentObjects[$k];
01087 
01088                         if ( $modified !== $orig ) {
01089                                 // text was changed, create updated Content object
01090                                 $content = $content->getContentHandler()->unserializeContent( $modified );
01091                         }
01092 
01093                         $args[ $k ] = $content;
01094                 }
01095 
01096                 return $ok;
01097         }
01098 }
01099