MediaWiki
REL1_21
|
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 }