*/
public $mParserOptions;
- var $mContent; // !<
+ var $mContent; // !< #FIXME: use Content object!
var $mContentLoaded = false; // !<
var $mOldId; // !<
# Pages containing custom CSS or JavaScript get special treatment
if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) {
+ #FIXME: use Content object instead!
wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
$this->showCssOrJsPage();
$outputDone = true;
* This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
* page views.
*/
- protected function showCssOrJsPage() {
+ protected function showCssOrJsPage() { #FIXME: deprecate, keep for BC
global $wgOut;
$dir = $this->getContext()->getLanguage()->getDir();
$lang = $this->getContext()->getLanguage()->getCode();
$wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
- 'clearyourcache' );
+ 'clearyourcache' ); #FIXME: get this from handler
// Give hooks a chance to customise the output
if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) {
+ #FIXME: use content object instead
// Wrap the whole lot in a <pre> and don't parse
$m = array();
preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
--- /dev/null
+<?php
+
+/**
+ * A content object represents page content, e.g. the text to show on a page.
+ *
+ */
+abstract class Content {
+
+ public function __construct( Title $title, $revId, $modelName ) {
+ $this->mModelName = $modelName;
+ $this->mTitle = $title;
+ $this->mRevId = $revId;
+ }
+
+ public function getModelName() {
+ return $this->mModelName;
+ }
+
+ public function getTitle() {
+ return $this->mTitle;
+ }
+
+ public function getRevId() {
+ return $this->mRevId;
+ }
+
+ public abstract function getSearchText( $obj );
+
+ public abstract function getWikitextForTransclusion( $obj );
+
+ public abstract function getParserOutput( ParserOptions $options = NULL );
+
+ public abstract function getRawData( );
+
+ public function getHtml( ParserOptions $options ) {
+ $po = $this->getParserOutput( $options );
+ return $po->getText();
+ }
+
+ public function getIndexUpdateJobs( ParserOptions $options , $recursive = true ) {
+ $po = $this->getParserOutput( $options );
+ $update = new LinksUpdate( $this->mTitle, $po, $recursive );
+ return $update;
+ }
+
+ #XXX: is the native model for wikitext a string or the parser output? parse early or parse late?
+}
+
+class TextContent extends Content {
+ public function __construct( $text, Title $title, $revId, $modelName ) {
+ parent::__construct($title, $revId, $modelName);
+
+ $this->mText = $text;
+ }
+
+ public function getSearchText( $obj ) {
+ return $this->getRawData();
+ }
+
+ public function getWikitextForTransclusion( $obj ) {
+ return $this->getRawData();
+ }
+
+
+ public function getParserOutput( ParserOptions $options = null ) {
+ # generic implementation, relying on $this->getHtml()
+
+ $html = $this->getHtml( $options );
+ $po = new ParserOutput( $html );
+
+ #TODO: cache settings, etc?
+
+ return $po;
+ }
+
+ public function getHtml( ParserOptions $options ) {
+ $html = "";
+ $html .= "<pre class=\"mw-code\" dir=\"ltr\">\n";
+ $html .= htmlspecialchars( $this->getRawData() );
+ $html .= "\n</pre>\n";
+
+ return $html;
+ }
+
+
+ public function getRawData( ) {
+ global $wgParser, $wgUser;
+
+ $text = $this->mText;
+ return $text;
+ }
+
+}
+
+class WikitextContent extends TextContent {
+ public function __construct( $text, Title $title, $revId = null) {
+ parent::__construct($text, $title, $revId, CONTENT_MODEL_WIKITEXT);
+
+ $this->mDefaultParserOptions = null;
+ }
+
+ public function getDefaultParserOptions() {
+ global $wgUser, $wgContLang;
+
+ if ( !$this->mDefaultParserOptions ) {
+ #TODO: use static member?!
+ $this->mDefaultParserOptions = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+ }
+
+ return $this->mDefaultParserOptions;
+ }
+
+ public function getParserOutput( ParserOptions $options = null ) {
+ global $wgParser;
+
+ #TODO: quick local cache: if $options is NULL, use ->mParserOutput!
+ #FIXME: need setParserOutput, so we can use stuff from the parser cache??
+ #FIXME: ...or we somehow need to know the parser cache key??
+
+ if ( !$options ) {
+ $options = $this->getDefaultParserOptions();
+ }
+
+ $po = $wgParser->parse( $this->mText, $this->getTitle(), $options );
+
+ return $po;
+ }
+
+}
+
+
+class JavaScriptContent extends TextContent {
+ public function __construct( $text, Title $title, $revId = null ) {
+ parent::__construct($text, $title, $revId, CONTENT_MODEL_JAVASCRIPT);
+ }
+
+ public function getHtml( ParserOptions $options ) {
+ $html = "";
+ $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
+ $html .= htmlspecialchars( $this->getRawData() );
+ $html .= "\n</pre>\n";
+
+ return $html;
+ }
+
+}
+
+class CssContent extends TextContent {
+ public function __construct( $text, Title $title, $revId = null ) {
+ parent::__construct($text, $title, $revId, CONTENT_MODEL_CSS);
+ }
+
+ public function getHtml( ParserOptions $options ) {
+ $html = "";
+ $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
+ $html .= htmlspecialchars( $this->getRawData() );
+ $html .= "\n</pre>\n";
+
+ return $html;
+ }
+}
+
+#TODO: MultipartMultipart < WikipageContent (Main + Links + X)
+#TODO: LinksContent < LanguageLinksContent, CategoriesContent
+#EXAMPLE: CoordinatesContent
+#EXAMPLE: WikidataContent
*
*/
abstract class ContentHandler {
-
+
+ public static function getDefaultModelFor( Title $title ) {
+ global $wgNamespaceContentModels;
+
+ # NOTE: this method must not rely on $title->getContentModelName() directly or indirectly,
+ # because it is used to initialized the mContentModelName memebr.
+
+ $ns = $title->getNamespace();
+
+ $ext = false;
+ $m = null;
+ $model = null;
+
+ if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
+ $model = $wgNamespaceContentModels[ $ns ];
+ }
+
+ # hook can determin default model
+ if ( !wfRunHooks( 'DefaultModelFor', array( $title, &$model ) ) ) { #FIXME: document new hook!
+ if ( $model ) return $model;
+ }
+
+ # Could this page contain custom CSS or JavaScript, based on the title?
+ $isCssOrJsPage = ( NS_MEDIAWIKI == $ns && preg_match( "!\.(css|js)$!u", $title->getText(), $m ) );
+ if ( $isCssOrJsPage ) $ext = $m[1];
+
+ # hook can force js/css
+ wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage, &$ext ) ); #FIXME: add $ext to hook interface spec
+
+ # Is this a .css subpage of a user page?
+ $isJsCssSubpage = ( NS_USER == $ns && !$isCssOrJsPage && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m ) );
+ if ( $isJsCssSubpage ) $ext = $m[1];
+
+ # is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
+ $isWikitext = ( $model == CONTENT_MODEL_WIKITEXT || $model === null );
+ $isWikitext = ( $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage );
+
+ # hook can override $isWikitext
+ wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
+
+ if ( !$isWikitext ) {
+
+ if ( $ext == 'js' )
+ return CONTENT_MODEL_JAVASCRIPT;
+ else if ( $ext == 'css' )
+ return CONTENT_MODEL_CSS;
+
+ if ( $model )
+ return $model;
+ else
+ return CONTENT_MODEL_TEXT;
+ }
+
+ # we established that is must be wikitext
+ return CONTENT_MODEL_WIKITEXT;
+ }
+
+ public static function getForTitle( Title $title ) {
+ $modelName = $title->getContentModelName();
+ return ContenteHandler::getForModelName( $modelName );
+ }
+
+ public static function getForContent( Content $content ) {
+ $modelName = $content->getModelName();
+ return ContenteHandler::getForModelName( $modelName );
+ }
+
+ public static function getForModelName( $modelName ) {
+ global $wgContentHandlers;
+
+ if ( empty( $wgContentHandlers[$modelName] ) ) {
+ #FIXME: hook here!
+ throw new MWException( "No handler for model $modelName registered in \$wgContentHandlers" );
+ }
+
+ if ( is_string( $wgContentHandlers[$modelName] ) ) {
+ $class = $wgContentHandlers[$modelName];
+ $wgContentHandlers[$modelName] = new $class( $modelName );
+ }
+
+ return $wgContentHandlers[$modelName];
+ }
+
+
public function __construct( $modelName, $formats ) {
$this->mModelName = $modelName;
$this->mSupportedFormats = $formats;
# returns a ParserOutput instance!
# are parser options, generic?!
- public function doPreSaveTransform( $title, $obj );
+ public abstract function doPreSaveTransform( $title, $obj );
# TODO: getPreloadText()
# TODO: preprocess()
'image/x-djvu' => 'DjVuHandler', // compat
);
+/**
+ * Plugins for page content model handling.
+ * Each entry in the array maps a model name type to a class name
+ */
+$wgContentHandlers = array(
+ CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
+ CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
+ CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
+ CONTENT_MODEL_TEXT => 'TextContentHandler', // dumb plain text in <pre>
+);
+
/**
* Resizing can be done using PHP's internal image libraries or using
* ImageMagick or another third-party converter, e.g. GraphicMagick.
define( 'PROTO_CURRENT', null );
define( 'PROTO_CANONICAL', 1 );
define( 'PROTO_INTERNAL', 2 );
+
+/**
+ * Content model names, used by Content and ContentHandler
+ */
+define('CONTENT_MODEL_WIKITEXT', 'wikitext');
+define('CONTENT_MODEL_JAVASCRIPT', 'javascript');
+define('CONTENT_MODEL_CSS', 'css');
+define('CONTENT_MODEL_TEXT', 'text');
+
# don't parse non-wikitext pages, show message about preview
# XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago?
+ #FIXME: get appropriate content handler!
if ( $this->isCssJsSubpage || !$this->mTitle->isWikitextPage() ) {
if( $this->mTitle->isCssJsSubpage() ) {
$level = 'user';
# Used messages to make sure grep find them:
# Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
if( $level ) {
+ #FIXME: move this crud into ContentHandler class!
if (preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
$previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
$class = "mw-code mw-css";
protected $mTextRow;
protected $mTitle;
protected $mCurrent;
+ protected $mContentModelName;
+ protected $mContentType;
const DELETED_TEXT = 1;
const DELETED_COMMENT = 2;
'deleted' => $row->ar_deleted,
'len' => $row->ar_len,
'sha1' => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
+ 'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
+ 'content_type' => isset( $row->ar_content_type ) ? $row->ar_content_type : null,
);
if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
// Pre-1.5 ar_text row
$this->mTitle = null;
}
+ if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
+ $this->mContentModelName = null; # determine on demand if needed
+ } else {
+ $this->mContentModelName = strval( $row->rev_content_model );
+ }
+
+ if( !isset( $row->rev_content_type ) || is_null( $row->rev_content_type ) ) {
+ $this->mContentType = null; # determine on demand if needed
+ } else {
+ $this->mContentType = strval( $row->rev_content_type );
+ }
+
// Lazy extraction...
$this->mText = null;
if( isset( $row->old_text ) ) {
$this->mParentId = isset( $row['parent_id'] ) ? intval( $row['parent_id'] ) : null;
$this->mSha1 = isset( $row['sha1'] ) ? strval( $row['sha1'] ) : null;
+ $this->mContentModelName = isset( $row['content_model'] ) ? strval( $row['content_model'] ) : null;
+ $this->mContentType = isset( $row['content_type'] ) ? strval( $row['content_type'] ) : null;
+
+ if( !isset( $row->rev_content_type ) || is_null( $row->rev_content_type ) ) {
+ $this->mContentType = null; # determine on demand if needed
+ } else {
+ $this->mContentType = $row->rev_content_type;
+ }
+
// Enforce spacing trimming on supplied text
$this->mComment = isset( $row['comment'] ) ? trim( strval( $row['comment'] ) ) : null;
$this->mText = isset( $row['text'] ) ? rtrim( strval( $row['text'] ) ) : null;
if ( $this->mSha1 === null ) {
$this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
}
+
+ $this->getContentModelName(); # force lazy init
+ $this->getContentType(); # force lazy init
} else {
throw new MWException( 'Revision constructor passed invalid row format.' );
}
* @param $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return String
+ * @deprectaed in 1.20, use getContent() instead
*/
- public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
- if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
- return '';
- } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
- return '';
- } else {
- return $this->getRawText();
- }
+ public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { #FIXME: deprecated, replace usage!
+ wfDeprecated( __METHOD__, '1.20' );
+ $content = $this->getContent();
+
+ if ( $content == null ) {
+ return ""; # not allowed
+ }
+
+ if ( $content instanceof TextContent ) {
+ #FIXME: or check by model name? or define $content->allowRawData()?
+ return $content->getRawData();
+ }
+
+ #TODO: log this failure!
+ return null;
}
+ /**
+ * Fetch revision content if it's available to the specified audience.
+ * If the specified audience does not have the ability to view this
+ * revision, null will be returned.
+ *
+ * @param $audience Integer: one of:
+ * Revision::FOR_PUBLIC to be displayed to all users
+ * Revision::FOR_THIS_USER to be displayed to $wgUser
+ * Revision::RAW get the text regardless of permissions
+ * @param $user User object to check for, only if FOR_THIS_USER is passed
+ * to the $audience parameter
+ * @return Content
+ */
+ public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
+ if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
+ return null;
+ } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+ return null;
+ } else {
+ return $this->getContentInternal();
+ }
+ }
+
/**
* Alias for getText(Revision::FOR_THIS_USER)
*
*
* @return String
*/
- public function getRawText() {
- if( is_null( $this->mText ) ) {
- // Revision text is immutable. Load on demand:
- $this->mText = $this->loadText();
- }
- return $this->mText;
+ public function getRawText() { #FIXME: deprecated, replace usage!
+ return $this->getText( self::RAW );
}
+ protected function getContentInternal() {
+ if( is_null( $this->mContent ) ) {
+ // Revision is immutable. Load on demand:
+
+ $handler = $this->getContentHandler();
+ $type = $this->getContentType();
+
+ if( is_null( $this->mText ) ) {
+ // Load text on demand:
+ $this->mText = $this->loadText();
+ }
+
+ $this->mContent = $handler->unserialize( $this->mText, $type );
+ }
+
+ return $this->mContent;
+ }
+
+ public function getContentModelName() {
+ if ( !$this->mContentModelName ) {
+ $title = $this->getTitle(); #XXX: never null?
+ $this->mContentModelName = $title->getContentModelName();
+ }
+
+ return $this->mContentModelName;
+ }
+
+ public function getContentType() {
+ if ( !$this->mContentType ) {
+ $handler = $this->getContentHandler();
+ $this->mContentType = $handler->getDefaultFormat();
+ }
+
+ return $this->mContentType;
+ }
+
+ public function getContentHandler() {
+ if ( !$this->mContentHandler ) {
+ $m = $this->getModelName();
+ $this->mContentHandler = ContentHandler::getForModelName( $m );
+ }
+
+ return $this->mContentHandler;
+ }
+
/**
* @return String
*/
: $this->mParentId,
'rev_sha1' => is_null( $this->mSha1 )
? Revision::base36Sha1( $this->mText )
- : $this->mSha1
+ : $this->mSha1,
+ 'rev_content_model' => $this->getContentModelName(),
+ 'rev_content_type' => $this->getContentType(),
), __METHOD__
);
$current = $dbw->selectRow(
array( 'page', 'revision' ),
- array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' ),
+ array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1',
+ 'rev_content_model', 'rev_content_type' ),
array(
'page_id' => $pageId,
'page_latest=rev_id',
'text_id' => $current->rev_text_id,
'parent_id' => $current->page_latest,
'len' => $current->rev_len,
- 'sha1' => $current->rev_sha1
+ 'sha1' => $current->rev_sha1,
+ 'content_model' => $current->rev_content_model,
+ 'content_type' => $current->rev_content_type
) );
} else {
$revision = null;
if ( isset( $row->page_is_redirect ) )
$this->mRedirect = (bool)$row->page_is_redirect;
if ( isset( $row->page_latest ) )
- $this->mLatestID = (int)$row->page_latest;
+ $this->mLatestID = (int)$row->page_latest; # FIXME: whene3ver page_latest is updated, also update page_content_model
+ if ( isset( $row->page_content_model ) )
+ $this->mContentModelName = $row->page_content_model;
+ else
+ $this->mContentModelName = null; # initialized lazily in getContentModelName()
} else { // page not found
$this->mArticleID = 0;
$this->mLength = 0;
$this->mRedirect = false;
$this->mLatestID = 0;
+ $this->mContentModelName = null; # initialized lazily in getContentModelName()
}
}
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
$t->mUrlform = wfUrlencode( $t->mDbkeyform );
$t->mTextform = str_replace( '_', ' ', $title );
+ $t->mContentModelName = null; # initialized lazily in getContentModelName()
return $t;
}
return $this->mNamespace;
}
+ /**
+ * Get the page's content model name
+ *
+ * @return Integer: Namespace index
+ */
+ public function getContentModelName() {
+ if ( empty( $this->mContentModelName ) ) {
+ $this->mContentModelName = ContentHandler::getDefaultModelFor( $this );
+ }
+
+ return $this->mContentModelName;
+ }
+
+ /**
+ * Conveniance method for checking a title's content model name
+ *
+ * @param $name
+ * @return true if $this->getContentModelName() == $name
+ */
+ public function hasContentModel( $name ) {
+ return $this->getContentModelName() == $name;
+ }
+
/**
* Get the namespace text
*
* @return Bool
*/
public function isWikitextPage() {
- $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
- wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
- return $retval;
+ return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
}
/**
* @return Bool
*/
public function isCssOrJsPage() {
- $retval = $this->mNamespace == NS_MEDIAWIKI
- && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
- wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
- return $retval;
+ return $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT );
}
/**
* @return Bool
*/
public function isCssJsSubpage() {
- return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && $this->isCssOrJsPage() );
}
/**
* @return Bool
*/
public function isCssSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_CSS ) );
}
/**
* @return Bool
*/
public function isJsSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
}
/**
&& $parserOptions->getStubThreshold() == 0
&& $this->mTitle->exists()
&& ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
- && $this->mTitle->isWikitextPage();
+ && $this->mTitle->isWikitextPage(); #FIXME: ask ContentHandler if cachable!
}
/**
*/
function setCacheTime( $t ) { return wfSetVar( $this->mCacheTime, $t ); }
- /**
+ /**abstract
* Sets the number of seconds after which this object should expire.
* This value is used with the ParserCache.
* If called with a value greater than the value provided at any previous call,