MediaWiki
master
|
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