MediaWiki
REL1_23
|
00001 <?php 00033 class MWContentSerializationException extends MWException { 00034 } 00035 00055 abstract class ContentHandler { 00063 protected static $enableDeprecationWarnings = false; 00064 00094 public static function getContentText( Content $content = null ) { 00095 global $wgContentHandlerTextFallback; 00096 00097 if ( is_null( $content ) ) { 00098 return ''; 00099 } 00100 00101 if ( $content instanceof TextContent ) { 00102 return $content->getNativeData(); 00103 } 00104 00105 wfDebugLog( 'ContentHandler', 'Accessing ' . $content->getModel() . ' content as text!' ); 00106 00107 if ( $wgContentHandlerTextFallback == 'fail' ) { 00108 throw new MWException( 00109 "Attempt to get text from Content with model " . 00110 $content->getModel() 00111 ); 00112 } 00113 00114 if ( $wgContentHandlerTextFallback == 'serialize' ) { 00115 return $content->serialize(); 00116 } 00117 00118 return null; 00119 } 00120 00144 public static function makeContent( $text, Title $title = null, 00145 $modelId = null, $format = null ) { 00146 if ( is_null( $modelId ) ) { 00147 if ( is_null( $title ) ) { 00148 throw new MWException( "Must provide a Title object or a content model ID." ); 00149 } 00150 00151 $modelId = $title->getContentModel(); 00152 } 00153 00154 $handler = ContentHandler::getForModelID( $modelId ); 00155 00156 return $handler->unserializeContent( $text, $format ); 00157 } 00158 00193 public static function getDefaultModelFor( Title $title ) { 00194 // NOTE: this method must not rely on $title->getContentModel() directly or indirectly, 00195 // because it is used to initialize the mContentModel member. 00196 00197 $ns = $title->getNamespace(); 00198 00199 $ext = false; 00200 $m = null; 00201 $model = MWNamespace::getNamespaceContentModel( $ns ); 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 00259 public static function getForTitle( Title $title ) { 00260 $modelId = $title->getContentModel(); 00261 00262 return ContentHandler::getForModelID( $modelId ); 00263 } 00264 00275 public static function getForContent( Content $content ) { 00276 $modelId = $content->getModel(); 00277 00278 return ContentHandler::getForModelID( $modelId ); 00279 } 00280 00284 protected static $handlers; 00285 00311 public static function getForModelID( $modelId ) { 00312 global $wgContentHandlers; 00313 00314 if ( isset( ContentHandler::$handlers[$modelId] ) ) { 00315 return ContentHandler::$handlers[$modelId]; 00316 } 00317 00318 if ( empty( $wgContentHandlers[$modelId] ) ) { 00319 $handler = null; 00320 00321 wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) ); 00322 00323 if ( $handler === null ) { 00324 throw new MWException( "No handler for model '$modelId' registered in \$wgContentHandlers" ); 00325 } 00326 00327 if ( !( $handler instanceof ContentHandler ) ) { 00328 throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" ); 00329 } 00330 } else { 00331 $class = $wgContentHandlers[$modelId]; 00332 $handler = new $class( $modelId ); 00333 00334 if ( !( $handler instanceof ContentHandler ) ) { 00335 throw new MWException( "$class from \$wgContentHandlers is not " . 00336 "compatible with ContentHandler" ); 00337 } 00338 } 00339 00340 wfDebugLog( 'ContentHandler', 'Created handler for ' . $modelId 00341 . ': ' . get_class( $handler ) ); 00342 00343 ContentHandler::$handlers[$modelId] = $handler; 00344 00345 return ContentHandler::$handlers[$modelId]; 00346 } 00347 00360 public static function getLocalizedName( $name ) { 00361 // Messages: content-model-wikitext, content-model-text, 00362 // content-model-javascript, content-model-css 00363 $key = "content-model-$name"; 00364 00365 $msg = wfMessage( $key ); 00366 00367 return $msg->exists() ? $msg->plain() : $name; 00368 } 00369 00370 public static function getContentModels() { 00371 global $wgContentHandlers; 00372 00373 return array_keys( $wgContentHandlers ); 00374 } 00375 00376 public static function getAllContentFormats() { 00377 global $wgContentHandlers; 00378 00379 $formats = array(); 00380 00381 foreach ( $wgContentHandlers as $model => $class ) { 00382 $handler = ContentHandler::getForModelID( $model ); 00383 $formats = array_merge( $formats, $handler->getSupportedFormats() ); 00384 } 00385 00386 $formats = array_unique( $formats ); 00387 00388 return $formats; 00389 } 00390 00391 // ------------------------------------------------------------------------ 00392 00396 protected $mModelID; 00397 00401 protected $mSupportedFormats; 00402 00412 public function __construct( $modelId, $formats ) { 00413 $this->mModelID = $modelId; 00414 $this->mSupportedFormats = $formats; 00415 00416 $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) ); 00417 $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName ); 00418 $this->mModelName = strtolower( $this->mModelName ); 00419 } 00420 00431 abstract public function serializeContent( Content $content, $format = null ); 00432 00443 abstract public function unserializeContent( $blob, $format = null ); 00444 00453 abstract public function makeEmptyContent(); 00454 00472 public function makeRedirectContent( Title $destination, $text = '' ) { 00473 return null; 00474 } 00475 00484 public function getModelID() { 00485 return $this->mModelID; 00486 } 00487 00496 protected function checkModelID( $model_id ) { 00497 if ( $model_id !== $this->mModelID ) { 00498 throw new MWException( "Bad content model: " . 00499 "expected {$this->mModelID} " . 00500 "but got $model_id." ); 00501 } 00502 } 00503 00513 public function getSupportedFormats() { 00514 return $this->mSupportedFormats; 00515 } 00516 00528 public function getDefaultFormat() { 00529 return $this->mSupportedFormats[0]; 00530 } 00531 00545 public function isSupportedFormat( $format ) { 00546 if ( !$format ) { 00547 return true; // this means "use the default" 00548 } 00549 00550 return in_array( $format, $this->mSupportedFormats ); 00551 } 00552 00560 protected function checkFormat( $format ) { 00561 if ( !$this->isSupportedFormat( $format ) ) { 00562 throw new MWException( 00563 "Format $format is not supported for content model " 00564 . $this->getModelID() 00565 ); 00566 } 00567 } 00568 00579 public function getActionOverrides() { 00580 return array(); 00581 } 00582 00597 public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0, 00598 $rcid = 0, //FIXME: Deprecated, no longer used 00599 $refreshCache = false, $unhide = false ) { 00600 $diffEngineClass = $this->getDiffEngineClass(); 00601 00602 return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide ); 00603 } 00604 00624 public function getPageLanguage( Title $title, Content $content = null ) { 00625 global $wgContLang, $wgLang; 00626 $pageLang = $wgContLang; 00627 00628 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 00629 // Parse mediawiki messages with correct target language 00630 list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() ); 00631 $pageLang = wfGetLangObj( $lang ); 00632 } 00633 00634 wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) ); 00635 00636 return wfGetLangObj( $pageLang ); 00637 } 00638 00659 public function getPageViewLanguage( Title $title, Content $content = null ) { 00660 $pageLang = $this->getPageLanguage( $title, $content ); 00661 00662 if ( $title->getNamespace() !== NS_MEDIAWIKI ) { 00663 // If the user chooses a variant, the content is actually 00664 // in a language whose code is the variant code. 00665 $variant = $pageLang->getPreferredVariant(); 00666 if ( $pageLang->getCode() !== $variant ) { 00667 $pageLang = Language::factory( $variant ); 00668 } 00669 } 00670 00671 return $pageLang; 00672 } 00673 00690 public function canBeUsedOn( Title $title ) { 00691 $ok = true; 00692 00693 wfRunHooks( 'ContentModelCanBeUsedOn', array( $this->getModelID(), $title, &$ok ) ); 00694 00695 return $ok; 00696 } 00697 00705 protected function getDiffEngineClass() { 00706 return 'DifferenceEngine'; 00707 } 00708 00723 public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) { 00724 return false; 00725 } 00726 00738 public function getAutosummary( Content $oldContent = null, Content $newContent = null, 00739 $flags ) { 00740 // Decide what kind of auto-summary is needed. 00741 00742 // Redirect auto-summaries 00743 00749 $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null; 00750 $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null; 00751 00752 if ( is_object( $rt ) ) { 00753 if ( !is_object( $ot ) 00754 || !$rt->equals( $ot ) 00755 || $ot->getFragment() != $rt->getFragment() 00756 ) { 00757 $truncatedtext = $newContent->getTextForSummary( 00758 250 00759 - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) 00760 - strlen( $rt->getFullText() ) ); 00761 00762 return wfMessage( 'autoredircomment', $rt->getFullText() ) 00763 ->rawParams( $truncatedtext )->inContentLanguage()->text(); 00764 } 00765 } 00766 00767 // New page auto-summaries 00768 if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) { 00769 // If they're making a new article, give its text, truncated, in 00770 // the summary. 00771 00772 $truncatedtext = $newContent->getTextForSummary( 00773 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ); 00774 00775 return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) 00776 ->inContentLanguage()->text(); 00777 } 00778 00779 // Blanking auto-summaries 00780 if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) { 00781 return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); 00782 } elseif ( !empty( $oldContent ) 00783 && $oldContent->getSize() > 10 * $newContent->getSize() 00784 && $newContent->getSize() < 500 00785 ) { 00786 // Removing more than 90% of the article 00787 00788 $truncatedtext = $newContent->getTextForSummary( 00789 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ); 00790 00791 return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) 00792 ->inContentLanguage()->text(); 00793 } 00794 00795 // If we reach this point, there's no applicable auto-summary for our 00796 // case, so our auto-summary is empty. 00797 return ''; 00798 } 00799 00815 public function getAutoDeleteReason( Title $title, &$hasHistory ) { 00816 $dbw = wfGetDB( DB_MASTER ); 00817 00818 // Get the last revision 00819 $rev = Revision::newFromTitle( $title ); 00820 00821 if ( is_null( $rev ) ) { 00822 return false; 00823 } 00824 00825 // Get the article's contents 00826 $content = $rev->getContent(); 00827 $blank = false; 00828 00829 // If the page is blank, use the text from the previous revision, 00830 // which can only be blank if there's a move/import/protect dummy 00831 // revision involved 00832 if ( !$content || $content->isEmpty() ) { 00833 $prev = $rev->getPrevious(); 00834 00835 if ( $prev ) { 00836 $rev = $prev; 00837 $content = $rev->getContent(); 00838 $blank = true; 00839 } 00840 } 00841 00842 $this->checkModelID( $rev->getContentModel() ); 00843 00844 // Find out if there was only one contributor 00845 // Only scan the last 20 revisions 00846 $res = $dbw->select( 'revision', 'rev_user_text', 00847 array( 00848 'rev_page' => $title->getArticleID(), 00849 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' 00850 ), 00851 __METHOD__, 00852 array( 'LIMIT' => 20 ) 00853 ); 00854 00855 if ( $res === false ) { 00856 // This page has no revisions, which is very weird 00857 return false; 00858 } 00859 00860 $hasHistory = ( $res->numRows() > 1 ); 00861 $row = $dbw->fetchObject( $res ); 00862 00863 if ( $row ) { // $row is false if the only contributor is hidden 00864 $onlyAuthor = $row->rev_user_text; 00865 // Try to find a second contributor 00866 foreach ( $res as $row ) { 00867 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 00868 $onlyAuthor = false; 00869 break; 00870 } 00871 } 00872 } else { 00873 $onlyAuthor = false; 00874 } 00875 00876 // Generate the summary with a '$1' placeholder 00877 if ( $blank ) { 00878 // The current revision is blank and the one before is also 00879 // blank. It's just not our lucky day 00880 $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text(); 00881 } else { 00882 if ( $onlyAuthor ) { 00883 $reason = wfMessage( 00884 'excontentauthor', 00885 '$1', 00886 $onlyAuthor 00887 )->inContentLanguage()->text(); 00888 } else { 00889 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text(); 00890 } 00891 } 00892 00893 if ( $reason == '-' ) { 00894 // Allow these UI messages to be blanked out cleanly 00895 return ''; 00896 } 00897 00898 // Max content length = max comment length - length of the comment (excl. $1) 00899 $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : ''; 00900 00901 // Now replace the '$1' placeholder 00902 $reason = str_replace( '$1', $text, $reason ); 00903 00904 return $reason; 00905 } 00906 00920 public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) { 00921 $cur_content = $current->getContent(); 00922 00923 if ( empty( $cur_content ) ) { 00924 return false; // no page 00925 } 00926 00927 $undo_content = $undo->getContent(); 00928 $undoafter_content = $undoafter->getContent(); 00929 00930 if ( !$undo_content || !$undoafter_content ) { 00931 return false; // no content to undo 00932 } 00933 00934 $this->checkModelID( $cur_content->getModel() ); 00935 $this->checkModelID( $undo_content->getModel() ); 00936 $this->checkModelID( $undoafter_content->getModel() ); 00937 00938 if ( $cur_content->equals( $undo_content ) ) { 00939 // No use doing a merge if it's just a straight revert. 00940 return $undoafter_content; 00941 } 00942 00943 $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content ); 00944 00945 return $undone_content; 00946 } 00947 00962 public function makeParserOptions( $context ) { 00963 global $wgContLang, $wgEnableParserLimitReporting; 00964 00965 if ( $context instanceof IContextSource ) { 00966 $options = ParserOptions::newFromContext( $context ); 00967 } elseif ( $context instanceof User ) { // settings per user (even anons) 00968 $options = ParserOptions::newFromUser( $context ); 00969 } elseif ( $context === 'canonical' ) { // canonical settings 00970 $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); 00971 } else { 00972 throw new MWException( "Bad context for parser options: $context" ); 00973 } 00974 00975 $options->enableLimitReport( $wgEnableParserLimitReporting ); // show inclusion/loop reports 00976 $options->setTidy( true ); // fix bad HTML 00977 00978 return $options; 00979 } 00980 00989 public function isParserCacheSupported() { 00990 return false; 00991 } 00992 01002 public function supportsSections() { 01003 return false; 01004 } 01005 01015 public function supportsRedirects() { 01016 return false; 01017 } 01018 01032 public static function deprecated( $func, $version, $component = false ) { 01033 if ( self::$enableDeprecationWarnings ) { 01034 wfDeprecated( $func, $version, $component, 3 ); 01035 } 01036 } 01037 01055 public static function runLegacyHooks( $event, $args = array(), 01056 $warn = null 01057 ) { 01058 01059 if ( $warn === null ) { 01060 $warn = self::$enableDeprecationWarnings; 01061 } 01062 01063 if ( !Hooks::isRegistered( $event ) ) { 01064 return true; // nothing to do here 01065 } 01066 01067 if ( $warn ) { 01068 // Log information about which handlers are registered for the legacy hook, 01069 // so we can find and fix them. 01070 01071 $handlers = Hooks::getHandlers( $event ); 01072 $handlerInfo = array(); 01073 01074 wfSuppressWarnings(); 01075 01076 foreach ( $handlers as $handler ) { 01077 if ( is_array( $handler ) ) { 01078 if ( is_object( $handler[0] ) ) { 01079 $info = get_class( $handler[0] ); 01080 } else { 01081 $info = $handler[0]; 01082 } 01083 01084 if ( isset( $handler[1] ) ) { 01085 $info .= '::' . $handler[1]; 01086 } 01087 } elseif ( is_object( $handler ) ) { 01088 $info = get_class( $handler[0] ); 01089 $info .= '::on' . $event; 01090 } else { 01091 $info = $handler; 01092 } 01093 01094 $handlerInfo[] = $info; 01095 } 01096 01097 wfRestoreWarnings(); 01098 01099 wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " . 01100 implode( ', ', $handlerInfo ), 2 ); 01101 } 01102 01103 // convert Content objects to text 01104 $contentObjects = array(); 01105 $contentTexts = array(); 01106 01107 foreach ( $args as $k => $v ) { 01108 if ( $v instanceof Content ) { 01109 /* @var Content $v */ 01110 01111 $contentObjects[$k] = $v; 01112 01113 $v = $v->serialize(); 01114 $contentTexts[$k] = $v; 01115 $args[$k] = $v; 01116 } 01117 } 01118 01119 // call the hook functions 01120 $ok = wfRunHooks( $event, $args ); 01121 01122 // see if the hook changed the text 01123 foreach ( $contentTexts as $k => $orig ) { 01124 /* @var Content $content */ 01125 01126 $modified = $args[$k]; 01127 $content = $contentObjects[$k]; 01128 01129 if ( $modified !== $orig ) { 01130 // text was changed, create updated Content object 01131 $content = $content->getContentHandler()->unserializeContent( $modified ); 01132 } 01133 01134 $args[$k] = $content; 01135 } 01136 01137 return $ok; 01138 } 01139 }