MediaWiki  master
RawAction.php
Go to the documentation of this file.
1 <?php
35 
42 class RawAction extends FormlessAction {
43 
45  private $hookRunner;
46 
48  private $parser;
49 
52 
54  private $revisionLookup;
55 
64  public function __construct(
65  Page $page,
67  HookContainer $hookContainer,
71  ) {
72  parent::__construct( $page, $context );
73  $this->hookRunner = new HookRunner( $hookContainer );
74  $this->parser = $parser;
75  $this->permissionManager = $permissionManager;
76  $this->revisionLookup = $revisionLookup;
77  }
78 
79  public function getName() {
80  return 'raw';
81  }
82 
83  public function requiresWrite() {
84  return false;
85  }
86 
87  public function requiresUnblock() {
88  return false;
89  }
90 
95  public function onView() {
96  $this->getOutput()->disable();
97  $request = $this->getRequest();
98  $response = $request->response();
99  $config = $this->context->getConfig();
100 
101  if ( $this->getOutput()->checkLastModified(
102  $this->getWikiPage()->getTouched()
103  ) ) {
104  return null; // Client cache fresh and headers sent, nothing more to do.
105  }
106 
107  $contentType = $this->getContentType();
108 
109  $maxage = $request->getInt( 'maxage', $config->get( 'CdnMaxAge' ) );
110  $smaxage = $request->getIntOrNull( 'smaxage' );
111  if ( $smaxage === null ) {
112  if (
113  $contentType == 'text/css' ||
114  $contentType == 'application/json' ||
115  $contentType == 'text/javascript'
116  ) {
117  // CSS/JSON/JS raw content has its own CDN max age configuration.
118  // Note: HtmlCacheUpdater::getUrls() includes action=raw for css/json/js
119  // pages, so if using the canonical url, this will get HTCP purges.
120  $smaxage = intval( $config->get( 'ForcedRawSMaxage' ) );
121  } else {
122  // No CDN cache for anything else
123  $smaxage = 0;
124  }
125  }
126 
127  // Set standard Vary headers so cache varies on cookies and such (T125283)
128  $response->header( $this->getOutput()->getVaryHeader() );
129 
130  // Output may contain user-specific data;
131  // vary generated content for open sessions on private wikis
132  $privateCache = !$this->permissionManager->isEveryoneAllowed( 'read' ) &&
133  ( $smaxage == 0 || MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() );
134  // Don't accidentally cache cookies if user is registered (T55032)
135  $privateCache = $privateCache || $this->getUser()->isRegistered();
136  $mode = $privateCache ? 'private' : 'public';
137  $response->header(
138  'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage
139  );
140 
141  // In the event of user JS, don't allow loading a user JS/CSS/Json
142  // subpage that has no registered user associated with, as
143  // someone could register the account and take control of the
144  // JS/CSS/Json page.
145  $title = $this->getTitle();
146  if ( $title->isUserConfigPage() && $contentType !== 'text/x-wiki' ) {
147  // not using getRootText() as we want this to work
148  // even if subpages are disabled.
149  $rootPage = strtok( $title->getText(), '/' );
150  $userFromTitle = User::newFromName( $rootPage, 'usable' );
151  if ( !$userFromTitle || $userFromTitle->getId() === 0 ) {
152  $elevated = $this->getContext()->getAuthority()->isAllowed( 'editinterface' );
153  $elevatedText = $elevated ? 'by elevated ' : '';
154  $log = LoggerFactory::getInstance( "security" );
155  $log->warning(
156  "Unsafe JS/CSS/Json {$elevatedText}load - {user} loaded {title} with {ctype}",
157  [
158  'user' => $this->getUser()->getName(),
159  'title' => $title->getPrefixedDBkey(),
160  'ctype' => $contentType,
161  'elevated' => $elevated
162  ]
163  );
164  throw new HttpError( 403, wfMessage( 'unregistered-user-config' ) );
165  }
166  }
167 
168  // Don't allow loading non-protected pages as javascript.
169  // In future we may further restrict this to only CONTENT_MODEL_JAVASCRIPT
170  // in NS_MEDIAWIKI or NS_USER, as well as including other config types,
171  // but for now be more permissive. Allowing protected pages outside of
172  // NS_USER and NS_MEDIAWIKI in particular should be considered a temporary
173  // allowance.
174  if (
175  $contentType === 'text/javascript' &&
176  !$title->isUserJsConfigPage() &&
177  !$title->inNamespace( NS_MEDIAWIKI ) &&
178  !in_array( 'sysop', $title->getRestrictions( 'edit' ) ) &&
179  !in_array( 'editprotected', $title->getRestrictions( 'edit' ) )
180  ) {
181 
182  $log = LoggerFactory::getInstance( "security" );
183  $log->info( "Blocked loading unprotected JS {title} for {user}",
184  [
185  'user' => $this->getUser()->getName(),
186  'title' => $title->getPrefixedDBkey(),
187  ]
188  );
189  throw new HttpError( 403, wfMessage( 'unprotected-js' ) );
190  }
191 
192  $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
193 
194  $text = $this->getRawText();
195 
196  // Don't return a 404 response for CSS or JavaScript;
197  // 404s aren't generally cached and it would create
198  // extra hits when user CSS/JS are on and the user doesn't
199  // have the pages.
200  if ( $text === false && $contentType == 'text/x-wiki' ) {
201  $response->statusHeader( 404 );
202  }
203 
204  if ( !$this->hookRunner->onRawPageViewBeforeOutput( $this, $text ) ) {
205  wfDebug( __METHOD__ . ": RawPageViewBeforeOutput hook broke raw page output." );
206  }
207 
208  echo $text;
209 
210  return null;
211  }
212 
219  public function getRawText() {
220  $text = false;
221  $title = $this->getTitle();
222  $request = $this->getRequest();
223 
224  // Get it from the DB
225  $rev = $this->revisionLookup->getRevisionByTitle( $title, $this->getOldId() );
226  if ( $rev ) {
227  $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
228  $request->response()->header( "Last-modified: $lastmod" );
229 
230  // Public-only due to cache headers
231  $content = $rev->getContent( SlotRecord::MAIN );
232 
233  if ( $content === null ) {
234  // revision not found (or suppressed)
235  } elseif ( !$content instanceof TextContent ) {
236  // non-text content
237  wfHttpError( 415, "Unsupported Media Type", "The requested page uses the content model `"
238  . $content->getModel() . "` which is not supported via this interface." );
239  die();
240  } else {
241  // want a section?
242  $section = $request->getIntOrNull( 'section' );
243  if ( $section !== null ) {
244  $content = $content->getSection( $section );
245  }
246 
247  if ( $content === null || $content === false ) {
248  // section not found (or section not supported, e.g. for JS, JSON, and CSS)
249  } else {
250  $text = $content->getText();
251  }
252  }
253  }
254 
255  if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) {
256  $text = $this->parser->preprocess(
257  $text,
258  $title,
260  );
261  }
262 
263  return $text;
264  }
265 
271  public function getOldId() {
272  $oldid = $this->getRequest()->getInt( 'oldid' );
273  $rl = $this->revisionLookup;
274  switch ( $this->getRequest()->getText( 'direction' ) ) {
275  case 'next':
276  # output next revision, or nothing if there isn't one
277  $nextRev = null;
278  if ( $oldid ) {
279  $oldRev = $rl->getRevisionById( $oldid );
280  if ( $oldRev ) {
281  $nextRev = $rl->getNextRevision( $oldRev );
282  }
283  }
284  $oldid = $nextRev ? $nextRev->getId() : -1;
285  break;
286  case 'prev':
287  # output previous revision, or nothing if there isn't one
288  $prevRev = null;
289  if ( !$oldid ) {
290  # get the current revision so we can get the penultimate one
291  $oldid = $this->getWikiPage()->getLatest();
292  }
293  $oldRev = $rl->getRevisionById( $oldid );
294  if ( $oldRev ) {
295  $prevRev = $rl->getPreviousRevision( $oldRev );
296  }
297  $oldid = $prevRev ? $prevRev->getId() : -1;
298  break;
299  case 'cur':
300  $oldid = 0;
301  break;
302  }
303 
304  return $oldid;
305  }
306 
312  public function getContentType() {
313  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
314  $ctype = $this->getRequest()->getRawVal( 'ctype' );
315 
316  if ( $ctype == '' ) {
317  // Legacy compatibilty
318  $gen = $this->getRequest()->getRawVal( 'gen' );
319  if ( $gen == 'js' ) {
320  $ctype = 'text/javascript';
321  } elseif ( $gen == 'css' ) {
322  $ctype = 'text/css';
323  }
324  }
325 
326  $allowedCTypes = [
327  'text/x-wiki',
328  'text/javascript',
329  'text/css',
330  // FIXME: Should we still allow Zope editing? External editing feature was dropped
331  'application/x-zope-edit',
332  'application/json'
333  ];
334  if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) {
335  $ctype = 'text/x-wiki';
336  }
337 
338  return $ctype;
339  }
340 }
RawAction
A simple method to retrieve the plain source of an article, using "action=raw" in the GET request str...
Definition: RawAction.php:42
Page
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:29
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
RawAction\getOldId
getOldId()
Get the ID of the revision that should used to get the text.
Definition: RawAction.php:271
RawAction\getName
getName()
Return the name of the action this object responds to.
Definition: RawAction.php:79
FormlessAction
An action which just does something, without showing a form first.
Definition: FormlessAction.php:30
Action\getRequest
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:146
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1665
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:595
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1183
RawAction\getRawText
getRawText()
Get the text that should be returned, or false if the page or revision was not found.
Definition: RawAction.php:219
HttpError
Show an error that looks like an HTTP server error.
Definition: HttpError.php:32
MediaWiki\Revision\RevisionLookup
Service for looking up page revisions.
Definition: RevisionLookup.php:38
Action\getContext
getContext()
Get the IContextSource in use here.
Definition: Action.php:132
RawAction\getContentType
getContentType()
Get the content type to use for the response.
Definition: RawAction.php:312
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
RawAction\requiresUnblock
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition: RawAction.php:87
RawAction\$revisionLookup
RevisionLookup $revisionLookup
Definition: RawAction.php:54
$title
$title
Definition: testCompression.php:38
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
Action\getWikiPage
getWikiPage()
Get a WikiPage object.
Definition: Action.php:195
RawAction\__construct
__construct(Page $page, IContextSource $context, HookContainer $hookContainer, Parser $parser, PermissionManager $permissionManager, RevisionLookup $revisionLookup)
Definition: RawAction.php:64
RawAction\onView
onView()
Definition: RawAction.php:95
Action\getUser
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:166
MediaWiki\Session\SessionManager\getGlobalSession
static getGlobalSession()
If PHP's session_id() has been set, returns that session.
Definition: SessionManager.php:146
Action\$context
IContextSource $context
IContextSource if specified; otherwise we'll use the Context from the Page.
Definition: Action.php:66
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:52
$content
$content
Definition: router.php:76
ParserOptions\newFromContext
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
Definition: ParserOptions.php:1065
Action\getTitle
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:216
Parser
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition: Parser.php:92
TextContent
Content object implementation for representing flat text.
Definition: TextContent.php:39
RawAction\$hookRunner
HookRunner $hookRunner
Definition: RawAction.php:45
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
wfHttpError
wfHttpError( $code, $label, $desc)
Provide a simple HTTP error.
Definition: GlobalFunctions.php:1564
Action\$page
WikiPage Article ImagePage CategoryPage Page $page
Page on which we're performing the action.
Definition: Action.php:53
RawAction\$permissionManager
PermissionManager $permissionManager
Definition: RawAction.php:51
RawAction\$parser
Parser $parser
Definition: RawAction.php:48
Action\getOutput
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:156
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:554
RawAction\requiresWrite
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition: RawAction.php:83
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
MediaWiki\Revision\RevisionLookup\getRevisionById
getRevisionById( $id, $flags=0, PageIdentity $page=null)
Load a page revision from a given revision ID number.