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