MediaWiki  REL1_21
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 
00455         public function makeRedirectContent( Title $destination ) {
00456                 return null;
00457         }
00458 
00467         public function getModelID() {
00468                 return $this->mModelID;
00469         }
00470 
00481         protected function checkModelID( $model_id ) {
00482                 if ( $model_id !== $this->mModelID ) {
00483                         throw new MWException( "Bad content model: " .
00484                                 "expected {$this->mModelID} " .
00485                                 "but got $model_id." );
00486                 }
00487         }
00488 
00498         public function getSupportedFormats() {
00499                 return $this->mSupportedFormats;
00500         }
00501 
00513         public function getDefaultFormat() {
00514                 return $this->mSupportedFormats[0];
00515         }
00516 
00529         public function isSupportedFormat( $format ) {
00530 
00531                 if ( !$format ) {
00532                         return true; // this means "use the default"
00533                 }
00534 
00535                 return in_array( $format, $this->mSupportedFormats );
00536         }
00537 
00547         protected function checkFormat( $format ) {
00548                 if ( !$this->isSupportedFormat( $format ) ) {
00549                         throw new MWException(
00550                                 "Format $format is not supported for content model "
00551                                 . $this->getModelID()
00552                         );
00553                 }
00554         }
00555 
00566         public function getActionOverrides() {
00567                 return array();
00568         }
00569 
00585         public function createDifferenceEngine( IContextSource $context,
00586                 $old = 0, $new = 0,
00587                 $rcid = 0, # FIXME: use everywhere!
00588                 $refreshCache = false, $unhide = false
00589         ) {
00590                 $diffEngineClass = $this->getDiffEngineClass();
00591 
00592                 return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
00593         }
00594 
00612         public function getPageLanguage( Title $title, Content $content = null ) {
00613                 global $wgContLang, $wgLang;
00614                 $pageLang = $wgContLang;
00615 
00616                 if ( $title->getNamespace() == NS_MEDIAWIKI ) {
00617                         // Parse mediawiki messages with correct target language
00618                         list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
00619                         $pageLang = wfGetLangObj( $lang );
00620                 }
00621 
00622                 wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) );
00623                 return wfGetLangObj( $pageLang );
00624         }
00625 
00646         public function getPageViewLanguage( Title $title, Content $content = null ) {
00647                 $pageLang = $this->getPageLanguage( $title, $content );
00648 
00649                 if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
00650                         // If the user chooses a variant, the content is actually
00651                         // in a language whose code is the variant code.
00652                         $variant = $pageLang->getPreferredVariant();
00653                         if ( $pageLang->getCode() !== $variant ) {
00654                                 $pageLang = Language::factory( $variant );
00655                         }
00656                 }
00657 
00658                 return $pageLang;
00659         }
00660 
00674         public function canBeUsedOn( Title $title ) {
00675                 return true;
00676         }
00677 
00685         protected function getDiffEngineClass() {
00686                 return 'DifferenceEngine';
00687         }
00688 
00704         public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
00705                 return false;
00706         }
00707 
00719         public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
00720                 // Decide what kind of auto-summary is needed.
00721 
00722                 // Redirect auto-summaries
00723 
00729                 $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
00730                 $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
00731 
00732                 if ( is_object( $rt ) ) {
00733                         if ( !is_object( $ot )
00734                                 || !$rt->equals( $ot )
00735                                 || $ot->getFragment() != $rt->getFragment() )
00736                         {
00737                                 $truncatedtext = $newContent->getTextForSummary(
00738                                         250
00739                                                 - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
00740                                                 - strlen( $rt->getFullText() ) );
00741 
00742                                 return wfMessage( 'autoredircomment', $rt->getFullText() )
00743                                                 ->rawParams( $truncatedtext )->inContentLanguage()->text();
00744                         }
00745                 }
00746 
00747                 // New page auto-summaries
00748                 if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
00749                         // If they're making a new article, give its text, truncated, in
00750                         // the summary.
00751 
00752                         $truncatedtext = $newContent->getTextForSummary(
00753                                 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
00754 
00755                         return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
00756                                         ->inContentLanguage()->text();
00757                 }
00758 
00759                 // Blanking auto-summaries
00760                 if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
00761                         return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
00762                 } elseif ( !empty( $oldContent )
00763                         && $oldContent->getSize() > 10 * $newContent->getSize()
00764                         && $newContent->getSize() < 500 )
00765                 {
00766                         // Removing more than 90% of the article
00767 
00768                         $truncatedtext = $newContent->getTextForSummary(
00769                                 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
00770 
00771                         return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
00772                                         ->inContentLanguage()->text();
00773                 }
00774 
00775                 // If we reach this point, there's no applicable auto-summary for our
00776                 // case, so our auto-summary is empty.
00777                 return '';
00778         }
00779 
00794         public function getAutoDeleteReason( Title $title, &$hasHistory ) {
00795                 $dbw = wfGetDB( DB_MASTER );
00796 
00797                 // Get the last revision
00798                 $rev = Revision::newFromTitle( $title );
00799 
00800                 if ( is_null( $rev ) ) {
00801                         return false;
00802                 }
00803 
00804                 // Get the article's contents
00805                 $content = $rev->getContent();
00806                 $blank = false;
00807 
00808                 // If the page is blank, use the text from the previous revision,
00809                 // which can only be blank if there's a move/import/protect dummy
00810                 // revision involved
00811                 if ( !$content || $content->isEmpty() ) {
00812                         $prev = $rev->getPrevious();
00813 
00814                         if ( $prev ) {
00815                                 $rev = $prev;
00816                                 $content = $rev->getContent();
00817                                 $blank = true;
00818                         }
00819                 }
00820 
00821                 $this->checkModelID( $rev->getContentModel() );
00822 
00823                 // Find out if there was only one contributor
00824                 // Only scan the last 20 revisions
00825                 $res = $dbw->select( 'revision', 'rev_user_text',
00826                         array(
00827                                 'rev_page' => $title->getArticleID(),
00828                                 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
00829                         ),
00830                         __METHOD__,
00831                         array( 'LIMIT' => 20 )
00832                 );
00833 
00834                 if ( $res === false ) {
00835                         // This page has no revisions, which is very weird
00836                         return false;
00837                 }
00838 
00839                 $hasHistory = ( $res->numRows() > 1 );
00840                 $row = $dbw->fetchObject( $res );
00841 
00842                 if ( $row ) { // $row is false if the only contributor is hidden
00843                         $onlyAuthor = $row->rev_user_text;
00844                         // Try to find a second contributor
00845                         foreach ( $res as $row ) {
00846                                 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
00847                                         $onlyAuthor = false;
00848                                         break;
00849                                 }
00850                         }
00851                 } else {
00852                         $onlyAuthor = false;
00853                 }
00854 
00855                 // Generate the summary with a '$1' placeholder
00856                 if ( $blank ) {
00857                         // The current revision is blank and the one before is also
00858                         // blank. It's just not our lucky day
00859                         $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
00860                 } else {
00861                         if ( $onlyAuthor ) {
00862                                 $reason = wfMessage(
00863                                         'excontentauthor',
00864                                         '$1',
00865                                         $onlyAuthor
00866                                 )->inContentLanguage()->text();
00867                         } else {
00868                                 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
00869                         }
00870                 }
00871 
00872                 if ( $reason == '-' ) {
00873                         // Allow these UI messages to be blanked out cleanly
00874                         return '';
00875                 }
00876 
00877                 // Max content length = max comment length - length of the comment (excl. $1)
00878                 $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : '';
00879 
00880                 // Now replace the '$1' placeholder
00881                 $reason = str_replace( '$1', $text, $reason );
00882 
00883                 return $reason;
00884         }
00885 
00899         public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
00900                 $cur_content = $current->getContent();
00901 
00902                 if ( empty( $cur_content ) ) {
00903                         return false; // no page
00904                 }
00905 
00906                 $undo_content = $undo->getContent();
00907                 $undoafter_content = $undoafter->getContent();
00908 
00909                 $this->checkModelID( $cur_content->getModel() );
00910                 $this->checkModelID( $undo_content->getModel() );
00911                 $this->checkModelID( $undoafter_content->getModel() );
00912 
00913                 if ( $cur_content->equals( $undo_content ) ) {
00914                         // No use doing a merge if it's just a straight revert.
00915                         return $undoafter_content;
00916                 }
00917 
00918                 $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
00919 
00920                 return $undone_content;
00921         }
00922 
00939         public function makeParserOptions( $context ) {
00940                 global $wgContLang;
00941 
00942                 if ( $context instanceof IContextSource ) {
00943                         $options = ParserOptions::newFromContext( $context );
00944                 } elseif ( $context instanceof User ) { // settings per user (even anons)
00945                         $options = ParserOptions::newFromUser( $context );
00946                 } elseif ( $context === 'canonical' ) { // canonical settings
00947                         $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
00948                 } else {
00949                         throw new MWException( "Bad context for parser options: $context" );
00950                 }
00951 
00952                 $options->enableLimitReport(); // show inclusion/loop reports
00953                 $options->setTidy( true ); // fix bad HTML
00954 
00955                 return $options;
00956         }
00957 
00966         public function isParserCacheSupported() {
00967                 return false;
00968         }
00969 
00979         public function supportsSections() {
00980                 return false;
00981         }
00982 
00992         public function supportsRedirects() {
00993                 return false;
00994         }
00995 
01009         public static function deprecated( $func, $version, $component = false ) {
01010                 if ( self::$enableDeprecationWarnings ) {
01011                         wfDeprecated( $func, $version, $component, 3 );
01012                 }
01013         }
01014 
01032         public static function runLegacyHooks( $event, $args = array(),
01033                         $warn = null ) {
01034 
01035                 if ( $warn === null ) {
01036                         $warn = self::$enableDeprecationWarnings;
01037                 }
01038 
01039                 if ( !Hooks::isRegistered( $event ) ) {
01040                         return true; // nothing to do here
01041                 }
01042 
01043                 if ( $warn ) {
01044                         // Log information about which handlers are registered for the legacy hook,
01045                         // so we can find and fix them.
01046 
01047                         $handlers = Hooks::getHandlers( $event );
01048                         $handlerInfo = array();
01049 
01050                         wfSuppressWarnings();
01051 
01052                         foreach ( $handlers as $handler ) {
01053                                 if ( is_array( $handler ) ) {
01054                                         if ( is_object( $handler[0] ) ) {
01055                                                 $info = get_class( $handler[0] );
01056                                         } else {
01057                                                 $info = $handler[0];
01058                                         }
01059 
01060                                         if ( isset( $handler[1] ) ) {
01061                                                 $info .= '::' . $handler[1];
01062                                         }
01063                                 } else if ( is_object( $handler ) ) {
01064                                         $info = get_class( $handler[0] );
01065                                         $info .= '::on' . $event;
01066                                 } else {
01067                                         $info = $handler;
01068                                 }
01069 
01070                                 $handlerInfo[] = $info;
01071                         }
01072 
01073                         wfRestoreWarnings();
01074 
01075                         wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " . implode( ', ', $handlerInfo ), 2 );
01076                 }
01077 
01078                 // convert Content objects to text
01079                 $contentObjects = array();
01080                 $contentTexts = array();
01081 
01082                 foreach ( $args as $k => $v ) {
01083                         if ( $v instanceof Content ) {
01084                                 /* @var Content $v */
01085 
01086                                 $contentObjects[$k] = $v;
01087 
01088                                 $v = $v->serialize();
01089                                 $contentTexts[ $k ] = $v;
01090                                 $args[ $k ] = $v;
01091                         }
01092                 }
01093 
01094                 // call the hook functions
01095                 $ok = wfRunHooks( $event, $args );
01096 
01097                 // see if the hook changed the text
01098                 foreach ( $contentTexts as $k => $orig ) {
01099                         /* @var Content $content */
01100 
01101                         $modified = $args[ $k ];
01102                         $content = $contentObjects[$k];
01103 
01104                         if ( $modified !== $orig ) {
01105                                 // text was changed, create updated Content object
01106                                 $content = $content->getContentHandler()->unserializeContent( $modified );
01107                         }
01108 
01109                         $args[ $k ] = $content;
01110                 }
01111 
01112                 return $ok;
01113         }
01114 }