MediaWiki  master
RawAction.php
Go to the documentation of this file.
1 <?php
36 
43 class RawAction extends FormlessAction {
44 
45  private Parser $parser;
46  private PermissionManager $permissionManager;
47  private RevisionLookup $revisionLookup;
48  private RestrictionStore $restrictionStore;
49 
58  public function __construct(
59  Article $article,
61  Parser $parser,
62  PermissionManager $permissionManager,
63  RevisionLookup $revisionLookup,
64  RestrictionStore $restrictionStore
65  ) {
66  parent::__construct( $article, $context );
67  $this->parser = $parser;
68  $this->permissionManager = $permissionManager;
69  $this->revisionLookup = $revisionLookup;
70  $this->restrictionStore = $restrictionStore;
71  }
72 
73  public function getName() {
74  return 'raw';
75  }
76 
77  public function requiresWrite() {
78  return false;
79  }
80 
81  public function requiresUnblock() {
82  return false;
83  }
84 
89  public function onView() {
90  $this->getOutput()->disable();
91  $request = $this->getRequest();
92  $response = $request->response();
93  $config = $this->context->getConfig();
94 
95  if ( $this->getOutput()->checkLastModified(
96  $this->getWikiPage()->getTouched()
97  ) ) {
98  return null; // Client cache fresh and headers sent, nothing more to do.
99  }
100 
101  $contentType = $this->getContentType();
102 
103  $maxage = $request->getInt( 'maxage', $config->get( MainConfigNames::CdnMaxAge ) );
104  $smaxage = $request->getIntOrNull( 'smaxage' );
105  if ( $smaxage === null ) {
106  if (
107  $contentType == 'text/css' ||
108  $contentType == 'application/json' ||
109  $contentType == 'text/javascript'
110  ) {
111  // CSS/JSON/JS raw content has its own CDN max age configuration.
112  // Note: HtmlCacheUpdater::getUrls() includes action=raw for css/json/js
113  // pages, so if using the canonical url, this will get HTCP purges.
114  $smaxage = intval( $config->get( MainConfigNames::ForcedRawSMaxage ) );
115  } else {
116  // No CDN cache for anything else
117  $smaxage = 0;
118  }
119  }
120 
121  // Set standard Vary headers so cache varies on cookies and such (T125283)
122  $response->header( $this->getOutput()->getVaryHeader() );
123 
124  // Output may contain user-specific data;
125  // vary generated content for open sessions on private wikis
126  $privateCache = !$this->permissionManager->isEveryoneAllowed( 'read' ) &&
127  ( $smaxage == 0 || MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() );
128  // Don't accidentally cache cookies if user is registered (T55032)
129  $privateCache = $privateCache || $this->getUser()->isRegistered();
130  $mode = $privateCache ? 'private' : 'public';
131  $response->header(
132  'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage
133  );
134 
135  // In the event of user JS, don't allow loading a user JS/CSS/Json
136  // subpage that has no registered user associated with, as
137  // someone could register the account and take control of the
138  // JS/CSS/Json page.
139  $title = $this->getTitle();
140  if ( $title->isUserConfigPage() && $contentType !== 'text/x-wiki' ) {
141  // not using getRootText() as we want this to work
142  // even if subpages are disabled.
143  $rootPage = strtok( $title->getText(), '/' );
144  $userFromTitle = User::newFromName( $rootPage, 'usable' );
145  if ( !$userFromTitle || !$userFromTitle->isRegistered() ) {
146  $elevated = $this->getAuthority()->isAllowed( 'editinterface' );
147  $elevatedText = $elevated ? 'by elevated ' : '';
148  $log = LoggerFactory::getInstance( "security" );
149  $log->warning(
150  "Unsafe JS/CSS/Json {$elevatedText}load - {user} loaded {title} with {ctype}",
151  [
152  'user' => $this->getUser()->getName(),
153  'title' => $title->getPrefixedDBkey(),
154  'ctype' => $contentType,
155  'elevated' => $elevated
156  ]
157  );
158  throw new HttpError( 403, wfMessage( 'unregistered-user-config' ) );
159  }
160  }
161 
162  // Don't allow loading non-protected pages as javascript.
163  // In future we may further restrict this to only CONTENT_MODEL_JAVASCRIPT
164  // in NS_MEDIAWIKI or NS_USER, as well as including other config types,
165  // but for now be more permissive. Allowing protected pages outside of
166  // NS_USER and NS_MEDIAWIKI in particular should be considered a temporary
167  // allowance.
168  $pageRestrictions = $this->restrictionStore->getRestrictions( $title, 'edit' );
169  if (
170  $contentType === 'text/javascript' &&
171  !$title->isUserJsConfigPage() &&
172  !$title->inNamespace( NS_MEDIAWIKI ) &&
173  !in_array( 'sysop', $pageRestrictions ) &&
174  !in_array( 'editprotected', $pageRestrictions )
175  ) {
176 
177  $log = LoggerFactory::getInstance( "security" );
178  $log->info( "Blocked loading unprotected JS {title} for {user}",
179  [
180  'user' => $this->getUser()->getName(),
181  'title' => $title->getPrefixedDBkey(),
182  ]
183  );
184  throw new HttpError( 403, wfMessage( 'unprotected-js' ) );
185  }
186 
187  $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
188 
189  $text = $this->getRawText();
190 
191  // Don't return a 404 response for CSS or JavaScript;
192  // 404s aren't generally cached and it would create
193  // extra hits when user CSS/JS are on and the user doesn't
194  // have the pages.
195  if ( $text === false && $contentType == 'text/x-wiki' ) {
196  $response->statusHeader( 404 );
197  }
198 
199  if ( !$this->getHookRunner()->onRawPageViewBeforeOutput( $this, $text ) ) {
200  wfDebug( __METHOD__ . ": RawPageViewBeforeOutput hook broke raw page output." );
201  }
202 
203  echo $text;
204 
205  return null;
206  }
207 
214  public function getRawText() {
215  $text = false;
216  $title = $this->getTitle();
217  $request = $this->getRequest();
218 
219  // Get it from the DB
220  $rev = $this->revisionLookup->getRevisionByTitle( $title, $this->getOldId() );
221  if ( $rev ) {
222  $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
223  $request->response()->header( "Last-modified: $lastmod" );
224 
225  // Public-only due to cache headers
226  // Fetch specific slot if defined
227  $slot = $this->getRequest()->getText( 'slot' );
228  if ( $slot ) {
229  if ( $rev->hasSlot( $slot ) ) {
230  $content = $rev->getContent( $slot );
231  } else {
232  $content = null;
233  }
234  } else {
235  $content = $rev->getContent( SlotRecord::MAIN );
236  }
237 
238  if ( $content === null ) {
239  // revision or slot not found (or suppressed)
240  } elseif ( !$content instanceof TextContent && !method_exists( $content, 'getText' ) ) {
241  // non-text content
242  wfHttpError( 415, "Unsupported Media Type", "The requested page uses the content model `"
243  . $content->getModel() . "` which is not supported via this interface." );
244  die();
245  } else {
246  // want a section?
247  $section = $request->getIntOrNull( 'section' );
248  if ( $section !== null ) {
249  $content = $content->getSection( $section );
250  }
251 
252  if ( $content === null || $content === false ) {
253  // section not found (or section not supported, e.g. for JS, JSON, and CSS)
254  } else {
255  $text = $content->getText();
256  }
257  }
258  }
259 
260  if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) {
261  $text = $this->parser->preprocess(
262  $text,
263  $title,
265  );
266  }
267 
268  return $text;
269  }
270 
276  public function getOldId() {
277  $oldid = $this->getRequest()->getInt( 'oldid' );
278  $rl = $this->revisionLookup;
279  switch ( $this->getRequest()->getText( 'direction' ) ) {
280  case 'next':
281  # output next revision, or nothing if there isn't one
282  $nextRev = null;
283  if ( $oldid ) {
284  $oldRev = $rl->getRevisionById( $oldid );
285  if ( $oldRev ) {
286  $nextRev = $rl->getNextRevision( $oldRev );
287  }
288  }
289  $oldid = $nextRev ? $nextRev->getId() : -1;
290  break;
291  case 'prev':
292  # output previous revision, or nothing if there isn't one
293  $prevRev = null;
294  if ( !$oldid ) {
295  # get the current revision so we can get the penultimate one
296  $oldid = $this->getWikiPage()->getLatest();
297  }
298  $oldRev = $rl->getRevisionById( $oldid );
299  if ( $oldRev ) {
300  $prevRev = $rl->getPreviousRevision( $oldRev );
301  }
302  $oldid = $prevRev ? $prevRev->getId() : -1;
303  break;
304  case 'cur':
305  $oldid = 0;
306  break;
307  }
308 
309  // @phan-suppress-next-line PhanTypeMismatchReturnNullable RevisionRecord::getId does not return null here
310  return $oldid;
311  }
312 
318  public function getContentType() {
319  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
320  $ctype = $this->getRequest()->getRawVal( 'ctype' );
321 
322  if ( $ctype == '' ) {
323  // Legacy compatibility
324  $gen = $this->getRequest()->getRawVal( 'gen' );
325  if ( $gen == 'js' ) {
326  $ctype = 'text/javascript';
327  } elseif ( $gen == 'css' ) {
328  $ctype = 'text/css';
329  }
330  }
331 
332  $allowedCTypes = [
333  'text/x-wiki',
334  'text/javascript',
335  'text/css',
336  // FIXME: Should we still allow Zope editing? External editing feature was dropped
337  'application/x-zope-edit',
338  'application/json'
339  ];
340  if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) {
341  $ctype = 'text/x-wiki';
342  }
343 
344  return $ctype;
345  }
346 }
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:188
IContextSource null $context
IContextSource if specified; otherwise we'll use the Context from the Page.
Definition: Action.php:62
getHookRunner()
Definition: Action.php:253
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:209
getContext()
Get the IContextSource in use here.
Definition: Action.php:115
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:139
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:149
getAuthority()
Shortcut to get the Authority executing this instance.
Definition: Action.php:159
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:129
Legacy class representing an editable page and handling UI for some page actions.
Definition: Article.php:61
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
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.
internal since 1.36
Definition: User.php:98
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:115
A simple method to retrieve the plain source of an article, using "action=raw" in the GET request str...
Definition: RawAction.php:43
getContentType()
Get the content type to use for the response.
Definition: RawAction.php:318
getRawText()
Get the text that should be returned, or false if the page or revision was not found.
Definition: RawAction.php:214
getName()
Return the name of the action this object responds to.
Definition: RawAction.php:73
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition: RawAction.php:77
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition: RawAction.php:81
__construct(Article $article, IContextSource $context, Parser $parser, PermissionManager $permissionManager, RevisionLookup $revisionLookup, RestrictionStore $restrictionStore)
Definition: RawAction.php:58
getOldId()
Get the ID of the revision that should used to get the text.
Definition: RawAction.php:276
Content object implementation for representing flat text.
Definition: TextContent.php:41
Interface for objects which can provide a MediaWiki context on request.
Service for looking up page revisions.
$content
Definition: router.php:76