MediaWiki
REL1_24
|
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 public function exportTransform( $blob, $format = null ) { 00444 return $blob; 00445 } 00446 00457 abstract public function unserializeContent( $blob, $format = null ); 00458 00470 public function importTransform( $blob, $format = null ) { 00471 return $blob; 00472 } 00473 00482 abstract public function makeEmptyContent(); 00483 00501 public function makeRedirectContent( Title $destination, $text = '' ) { 00502 return null; 00503 } 00504 00513 public function getModelID() { 00514 return $this->mModelID; 00515 } 00516 00525 protected function checkModelID( $model_id ) { 00526 if ( $model_id !== $this->mModelID ) { 00527 throw new MWException( "Bad content model: " . 00528 "expected {$this->mModelID} " . 00529 "but got $model_id." ); 00530 } 00531 } 00532 00542 public function getSupportedFormats() { 00543 return $this->mSupportedFormats; 00544 } 00545 00557 public function getDefaultFormat() { 00558 return $this->mSupportedFormats[0]; 00559 } 00560 00574 public function isSupportedFormat( $format ) { 00575 if ( !$format ) { 00576 return true; // this means "use the default" 00577 } 00578 00579 return in_array( $format, $this->mSupportedFormats ); 00580 } 00581 00589 protected function checkFormat( $format ) { 00590 if ( !$this->isSupportedFormat( $format ) ) { 00591 throw new MWException( 00592 "Format $format is not supported for content model " 00593 . $this->getModelID() 00594 ); 00595 } 00596 } 00597 00608 public function getActionOverrides() { 00609 return array(); 00610 } 00611 00626 public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0, 00627 $rcid = 0, //FIXME: Deprecated, no longer used 00628 $refreshCache = false, $unhide = false ) { 00629 $diffEngineClass = $this->getDiffEngineClass(); 00630 00631 return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide ); 00632 } 00633 00653 public function getPageLanguage( Title $title, Content $content = null ) { 00654 global $wgContLang, $wgLang; 00655 $pageLang = $wgContLang; 00656 00657 if ( $title->getNamespace() == NS_MEDIAWIKI ) { 00658 // Parse mediawiki messages with correct target language 00659 list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() ); 00660 $pageLang = wfGetLangObj( $lang ); 00661 } 00662 00663 wfRunHooks( 'PageContentLanguage', array( $title, &$pageLang, $wgLang ) ); 00664 00665 return wfGetLangObj( $pageLang ); 00666 } 00667 00688 public function getPageViewLanguage( Title $title, Content $content = null ) { 00689 $pageLang = $this->getPageLanguage( $title, $content ); 00690 00691 if ( $title->getNamespace() !== NS_MEDIAWIKI ) { 00692 // If the user chooses a variant, the content is actually 00693 // in a language whose code is the variant code. 00694 $variant = $pageLang->getPreferredVariant(); 00695 if ( $pageLang->getCode() !== $variant ) { 00696 $pageLang = Language::factory( $variant ); 00697 } 00698 } 00699 00700 return $pageLang; 00701 } 00702 00719 public function canBeUsedOn( Title $title ) { 00720 $ok = true; 00721 00722 wfRunHooks( 'ContentModelCanBeUsedOn', array( $this->getModelID(), $title, &$ok ) ); 00723 00724 return $ok; 00725 } 00726 00734 protected function getDiffEngineClass() { 00735 return 'DifferenceEngine'; 00736 } 00737 00752 public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) { 00753 return false; 00754 } 00755 00767 public function getAutosummary( Content $oldContent = null, Content $newContent = null, 00768 $flags ) { 00769 // Decide what kind of auto-summary is needed. 00770 00771 // Redirect auto-summaries 00772 00778 $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null; 00779 $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null; 00780 00781 if ( is_object( $rt ) ) { 00782 if ( !is_object( $ot ) 00783 || !$rt->equals( $ot ) 00784 || $ot->getFragment() != $rt->getFragment() 00785 ) { 00786 $truncatedtext = $newContent->getTextForSummary( 00787 250 00788 - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() ) 00789 - strlen( $rt->getFullText() ) ); 00790 00791 return wfMessage( 'autoredircomment', $rt->getFullText() ) 00792 ->rawParams( $truncatedtext )->inContentLanguage()->text(); 00793 } 00794 } 00795 00796 // New page auto-summaries 00797 if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) { 00798 // If they're making a new article, give its text, truncated, in 00799 // the summary. 00800 00801 $truncatedtext = $newContent->getTextForSummary( 00802 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ); 00803 00804 return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext ) 00805 ->inContentLanguage()->text(); 00806 } 00807 00808 // Blanking auto-summaries 00809 if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) { 00810 return wfMessage( 'autosumm-blank' )->inContentLanguage()->text(); 00811 } elseif ( !empty( $oldContent ) 00812 && $oldContent->getSize() > 10 * $newContent->getSize() 00813 && $newContent->getSize() < 500 00814 ) { 00815 // Removing more than 90% of the article 00816 00817 $truncatedtext = $newContent->getTextForSummary( 00818 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ); 00819 00820 return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext ) 00821 ->inContentLanguage()->text(); 00822 } 00823 00824 // New blank article auto-summary 00825 if ( $flags & EDIT_NEW && $newContent->isEmpty() ) { 00826 return wfMessage( 'autosumm-newblank' )->inContentLanguage()->text(); 00827 } 00828 00829 // If we reach this point, there's no applicable auto-summary for our 00830 // case, so our auto-summary is empty. 00831 return ''; 00832 } 00833 00849 public function getAutoDeleteReason( Title $title, &$hasHistory ) { 00850 $dbw = wfGetDB( DB_MASTER ); 00851 00852 // Get the last revision 00853 $rev = Revision::newFromTitle( $title ); 00854 00855 if ( is_null( $rev ) ) { 00856 return false; 00857 } 00858 00859 // Get the article's contents 00860 $content = $rev->getContent(); 00861 $blank = false; 00862 00863 // If the page is blank, use the text from the previous revision, 00864 // which can only be blank if there's a move/import/protect dummy 00865 // revision involved 00866 if ( !$content || $content->isEmpty() ) { 00867 $prev = $rev->getPrevious(); 00868 00869 if ( $prev ) { 00870 $rev = $prev; 00871 $content = $rev->getContent(); 00872 $blank = true; 00873 } 00874 } 00875 00876 $this->checkModelID( $rev->getContentModel() ); 00877 00878 // Find out if there was only one contributor 00879 // Only scan the last 20 revisions 00880 $res = $dbw->select( 'revision', 'rev_user_text', 00881 array( 00882 'rev_page' => $title->getArticleID(), 00883 $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' 00884 ), 00885 __METHOD__, 00886 array( 'LIMIT' => 20 ) 00887 ); 00888 00889 if ( $res === false ) { 00890 // This page has no revisions, which is very weird 00891 return false; 00892 } 00893 00894 $hasHistory = ( $res->numRows() > 1 ); 00895 $row = $dbw->fetchObject( $res ); 00896 00897 if ( $row ) { // $row is false if the only contributor is hidden 00898 $onlyAuthor = $row->rev_user_text; 00899 // Try to find a second contributor 00900 foreach ( $res as $row ) { 00901 if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999 00902 $onlyAuthor = false; 00903 break; 00904 } 00905 } 00906 } else { 00907 $onlyAuthor = false; 00908 } 00909 00910 // Generate the summary with a '$1' placeholder 00911 if ( $blank ) { 00912 // The current revision is blank and the one before is also 00913 // blank. It's just not our lucky day 00914 $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text(); 00915 } else { 00916 if ( $onlyAuthor ) { 00917 $reason = wfMessage( 00918 'excontentauthor', 00919 '$1', 00920 $onlyAuthor 00921 )->inContentLanguage()->text(); 00922 } else { 00923 $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text(); 00924 } 00925 } 00926 00927 if ( $reason == '-' ) { 00928 // Allow these UI messages to be blanked out cleanly 00929 return ''; 00930 } 00931 00932 // Max content length = max comment length - length of the comment (excl. $1) 00933 $text = $content ? $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) ) : ''; 00934 00935 // Now replace the '$1' placeholder 00936 $reason = str_replace( '$1', $text, $reason ); 00937 00938 return $reason; 00939 } 00940 00954 public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) { 00955 $cur_content = $current->getContent(); 00956 00957 if ( empty( $cur_content ) ) { 00958 return false; // no page 00959 } 00960 00961 $undo_content = $undo->getContent(); 00962 $undoafter_content = $undoafter->getContent(); 00963 00964 if ( !$undo_content || !$undoafter_content ) { 00965 return false; // no content to undo 00966 } 00967 00968 $this->checkModelID( $cur_content->getModel() ); 00969 $this->checkModelID( $undo_content->getModel() ); 00970 $this->checkModelID( $undoafter_content->getModel() ); 00971 00972 if ( $cur_content->equals( $undo_content ) ) { 00973 // No use doing a merge if it's just a straight revert. 00974 return $undoafter_content; 00975 } 00976 00977 $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content ); 00978 00979 return $undone_content; 00980 } 00981 00996 public function makeParserOptions( $context ) { 00997 global $wgContLang, $wgEnableParserLimitReporting; 00998 00999 if ( $context instanceof IContextSource ) { 01000 $options = ParserOptions::newFromContext( $context ); 01001 } elseif ( $context instanceof User ) { // settings per user (even anons) 01002 $options = ParserOptions::newFromUser( $context ); 01003 } elseif ( $context === 'canonical' ) { // canonical settings 01004 $options = ParserOptions::newFromUserAndLang( new User, $wgContLang ); 01005 } else { 01006 throw new MWException( "Bad context for parser options: $context" ); 01007 } 01008 01009 $options->enableLimitReport( $wgEnableParserLimitReporting ); // show inclusion/loop reports 01010 $options->setTidy( true ); // fix bad HTML 01011 01012 return $options; 01013 } 01014 01023 public function isParserCacheSupported() { 01024 return false; 01025 } 01026 01036 public function supportsSections() { 01037 return false; 01038 } 01039 01049 public function supportsRedirects() { 01050 return false; 01051 } 01052 01066 public static function deprecated( $func, $version, $component = false ) { 01067 if ( self::$enableDeprecationWarnings ) { 01068 wfDeprecated( $func, $version, $component, 3 ); 01069 } 01070 } 01071 01089 public static function runLegacyHooks( $event, $args = array(), 01090 $warn = null 01091 ) { 01092 01093 if ( $warn === null ) { 01094 $warn = self::$enableDeprecationWarnings; 01095 } 01096 01097 if ( !Hooks::isRegistered( $event ) ) { 01098 return true; // nothing to do here 01099 } 01100 01101 if ( $warn ) { 01102 // Log information about which handlers are registered for the legacy hook, 01103 // so we can find and fix them. 01104 01105 $handlers = Hooks::getHandlers( $event ); 01106 $handlerInfo = array(); 01107 01108 wfSuppressWarnings(); 01109 01110 foreach ( $handlers as $handler ) { 01111 if ( is_array( $handler ) ) { 01112 if ( is_object( $handler[0] ) ) { 01113 $info = get_class( $handler[0] ); 01114 } else { 01115 $info = $handler[0]; 01116 } 01117 01118 if ( isset( $handler[1] ) ) { 01119 $info .= '::' . $handler[1]; 01120 } 01121 } elseif ( is_object( $handler ) ) { 01122 $info = get_class( $handler[0] ); 01123 $info .= '::on' . $event; 01124 } else { 01125 $info = $handler; 01126 } 01127 01128 $handlerInfo[] = $info; 01129 } 01130 01131 wfRestoreWarnings(); 01132 01133 wfWarn( "Using obsolete hook $event via ContentHandler::runLegacyHooks()! Handlers: " . 01134 implode( ', ', $handlerInfo ), 2 ); 01135 } 01136 01137 // convert Content objects to text 01138 $contentObjects = array(); 01139 $contentTexts = array(); 01140 01141 foreach ( $args as $k => $v ) { 01142 if ( $v instanceof Content ) { 01143 /* @var Content $v */ 01144 01145 $contentObjects[$k] = $v; 01146 01147 $v = $v->serialize(); 01148 $contentTexts[$k] = $v; 01149 $args[$k] = $v; 01150 } 01151 } 01152 01153 // call the hook functions 01154 $ok = wfRunHooks( $event, $args ); 01155 01156 // see if the hook changed the text 01157 foreach ( $contentTexts as $k => $orig ) { 01158 /* @var Content $content */ 01159 01160 $modified = $args[$k]; 01161 $content = $contentObjects[$k]; 01162 01163 if ( $modified !== $orig ) { 01164 // text was changed, create updated Content object 01165 $content = $content->getContentHandler()->unserializeContent( $modified ); 01166 } 01167 01168 $args[$k] = $content; 01169 } 01170 01171 return $ok; 01172 } 01173 }