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