MediaWiki  master
RawAction.php
Go to the documentation of this file.
1 <?php
31 
38 class RawAction extends FormlessAction {
39  public function getName() {
40  return 'raw';
41  }
42 
43  public function requiresWrite() {
44  return false;
45  }
46 
47  public function requiresUnblock() {
48  return false;
49  }
50 
55  function onView() {
56  $this->getOutput()->disable();
57  $request = $this->getRequest();
58  $response = $request->response();
59  $config = $this->context->getConfig();
60 
61  if ( !$request->checkUrlExtension() ) {
62  return null;
63  }
64 
65  if ( $this->getOutput()->checkLastModified( $this->page->getTouched() ) ) {
66  return null; // Client cache fresh and headers sent, nothing more to do.
67  }
68 
69  $contentType = $this->getContentType();
70 
71  $maxage = $request->getInt( 'maxage', $config->get( 'CdnMaxAge' ) );
72  $smaxage = $request->getIntOrNull( 'smaxage' );
73  if ( $smaxage === null ) {
74  if (
75  $contentType == 'text/css' ||
76  $contentType == 'application/json' ||
77  $contentType == 'text/javascript'
78  ) {
79  // CSS/JSON/JS raw content has its own CDN max age configuration.
80  // Note: Title::getCdnUrls() includes action=raw for css/json/js
81  // pages, so if using the canonical url, this will get HTCP purges.
82  $smaxage = intval( $config->get( 'ForcedRawSMaxage' ) );
83  } else {
84  // No CDN cache for anything else
85  $smaxage = 0;
86  }
87  }
88 
89  // Set standard Vary headers so cache varies on cookies and such (T125283)
90  $response->header( $this->getOutput()->getVaryHeader() );
91 
92  // Output may contain user-specific data;
93  // vary generated content for open sessions on private wikis
94  $privateCache = !User::isEveryoneAllowed( 'read' ) &&
95  ( $smaxage == 0 || MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() );
96  // Don't accidentally cache cookies if user is logged in (T55032)
97  $privateCache = $privateCache || $this->getUser()->isLoggedIn();
98  $mode = $privateCache ? 'private' : 'public';
99  $response->header(
100  'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage
101  );
102 
103  // In the event of user JS, don't allow loading a user JS/CSS/Json
104  // subpage that has no registered user associated with, as
105  // someone could register the account and take control of the
106  // JS/CSS/Json page.
107  $title = $this->getTitle();
108  if ( $title->isUserConfigPage() && $contentType !== 'text/x-wiki' ) {
109  // not using getRootText() as we want this to work
110  // even if subpages are disabled.
111  $rootPage = strtok( $title->getText(), '/' );
112  $userFromTitle = User::newFromName( $rootPage, 'usable' );
113  if ( !$userFromTitle || $userFromTitle->getId() === 0 ) {
114  $elevated = MediaWikiServices::getInstance()->getPermissionManager()
115  ->userHasRight( $this->getUser(), 'editinterface' );
116  $elevatedText = $elevated ? 'by elevated ' : '';
117  $log = LoggerFactory::getInstance( "security" );
118  $log->warning(
119  "Unsafe JS/CSS/Json {$elevatedText}load - {user} loaded {title} with {ctype}",
120  [
121  'user' => $this->getUser()->getName(),
122  'title' => $title->getPrefixedDBkey(),
123  'ctype' => $contentType,
124  'elevated' => $elevated
125  ]
126  );
127  $msg = wfMessage( 'unregistered-user-config' );
128  throw new HttpError( 403, $msg );
129  }
130  }
131 
132  // Don't allow loading non-protected pages as javascript.
133  // In future we may further restrict this to only CONTENT_MODEL_JAVASCRIPT
134  // in NS_MEDIAWIKI or NS_USER, as well as including other config types,
135  // but for now be more permissive. Allowing protected pages outside of
136  // NS_USER and NS_MEDIAWIKI in particular should be considered a temporary
137  // allowance.
138  if (
139  $contentType === 'text/javascript' &&
140  !$title->isUserJsConfigPage() &&
141  !$title->inNamespace( NS_MEDIAWIKI ) &&
142  !in_array( 'sysop', $title->getRestrictions( 'edit' ) ) &&
143  !in_array( 'editprotected', $title->getRestrictions( 'edit' ) )
144  ) {
145 
146  $log = LoggerFactory::getInstance( "security" );
147  $log->info( "Blocked loading unprotected JS {title} for {user}",
148  [
149  'user' => $this->getUser()->getName(),
150  'title' => $title->getPrefixedDBkey(),
151  ]
152  );
153  throw new HttpError( 403, wfMessage( 'unprotected-js' ) );
154  }
155 
156  $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
157 
158  $text = $this->getRawText();
159 
160  // Don't return a 404 response for CSS or JavaScript;
161  // 404s aren't generally cached and it would create
162  // extra hits when user CSS/JS are on and the user doesn't
163  // have the pages.
164  if ( $text === false && $contentType == 'text/x-wiki' ) {
165  $response->statusHeader( 404 );
166  }
167 
168  // Avoid PHP 7.1 warning of passing $this by reference
169  $rawAction = $this;
170  if ( !Hooks::run( 'RawPageViewBeforeOutput', [ &$rawAction, &$text ] ) ) {
171  wfDebug( __METHOD__ . ": RawPageViewBeforeOutput hook broke raw page output.\n" );
172  }
173 
174  echo $text;
175 
176  return null;
177  }
178 
185  public function getRawText() {
186  $text = false;
187  $title = $this->getTitle();
188  $request = $this->getRequest();
189 
190  // Get it from the DB
191  $rev = Revision::newFromTitle( $title, $this->getOldId() );
192  if ( $rev ) {
193  $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
194  $request->response()->header( "Last-modified: $lastmod" );
195 
196  // Public-only due to cache headers
197  $content = $rev->getContent();
198 
199  if ( $content === null ) {
200  // revision not found (or suppressed)
201  $text = false;
202  } elseif ( !$content instanceof TextContent ) {
203  // non-text content
204  wfHttpError( 415, "Unsupported Media Type", "The requested page uses the content model `"
205  . $content->getModel() . "` which is not supported via this interface." );
206  die();
207  } else {
208  // want a section?
209  $section = $request->getIntOrNull( 'section' );
210  if ( $section !== null ) {
211  $content = $content->getSection( $section );
212  }
213 
214  if ( $content === null || $content === false ) {
215  // section not found (or section not supported, e.g. for JS, JSON, and CSS)
216  $text = false;
217  } else {
218  $text = $content->getText();
219  }
220  }
221  }
222 
223  if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) {
224  $text = MediaWikiServices::getInstance()->getParser()->preprocess(
225  $text,
226  $title,
228  );
229  }
230 
231  return $text;
232  }
233 
239  public function getOldId() {
240  $oldid = $this->getRequest()->getInt( 'oldid' );
241  $rl = MediaWikiServices::getInstance()->getRevisionLookup();
242  switch ( $this->getRequest()->getText( 'direction' ) ) {
243  case 'next':
244  # output next revision, or nothing if there isn't one
245  $nextRev = null;
246  if ( $oldid ) {
247  $oldRev = $rl->getRevisionById( $oldid );
248  if ( $oldRev ) {
249  $nextRev = $rl->getNextRevision( $oldRev );
250  }
251  }
252  $oldid = $nextRev ? $nextRev->getId() : -1;
253  break;
254  case 'prev':
255  # output previous revision, or nothing if there isn't one
256  $prevRev = null;
257  if ( !$oldid ) {
258  # get the current revision so we can get the penultimate one
259  $oldid = $this->page->getLatest();
260  }
261  $oldRev = $rl->getRevisionById( $oldid );
262  if ( $oldRev ) {
263  $prevRev = $rl->getPreviousRevision( $oldRev );
264  }
265  $oldid = $prevRev ? $prevRev->getId() : -1;
266  break;
267  case 'cur':
268  $oldid = 0;
269  break;
270  }
271 
272  return $oldid;
273  }
274 
280  public function getContentType() {
281  // Optimisation: Avoid slow getVal(), this isn't user-generated content.
282  $ctype = $this->getRequest()->getRawVal( 'ctype' );
283 
284  if ( $ctype == '' ) {
285  // Legacy compatibilty
286  $gen = $this->getRequest()->getRawVal( 'gen' );
287  if ( $gen == 'js' ) {
288  $ctype = 'text/javascript';
289  } elseif ( $gen == 'css' ) {
290  $ctype = 'text/css';
291  }
292  }
293 
294  $allowedCTypes = [
295  'text/x-wiki',
296  'text/javascript',
297  'text/css',
298  // FIXME: Should we still allow Zope editing? External editing feature was dropped
299  'application/x-zope-edit',
300  'application/json'
301  ];
302  if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) {
303  $ctype = 'text/x-wiki';
304  }
305 
306  return $ctype;
307  }
308 }
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
$response
getOutput()
Get the OutputPage being used for this instance.
Definition: Action.php:208
getTitle()
Shortcut to get the Title object from the page.
Definition: Action.php:247
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition: User.php:4795
getUser()
Shortcut to get the User being used for this instance.
Definition: Action.php:218
getContentType()
Get the content type to use for the response.
Definition: RawAction.php:280
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
Definition: Revision.php:138
requiresUnblock()
Definition: RawAction.php:47
Show an error that looks like an HTTP server error.
Definition: HttpError.php:30
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
getRawText()
Get the text that should be returned, or false if the page or revision was not found.
Definition: RawAction.php:185
requiresWrite()
Definition: RawAction.php:43
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.
getContext()
Get the IContextSource in use here.
Definition: Action.php:179
const NS_MEDIAWIKI
Definition: Defines.php:68
An action which just does something, without showing a form first.
static getGlobalSession()
Get the "global" session.
onView()
SecurityCheck-XSS Non html mime type.
Definition: RawAction.php:55
getOldId()
Get the ID of the revision that should used to get the text.
Definition: RawAction.php:239
$content
Definition: router.php:78
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
A simple method to retrieve the plain source of an article, using "action=raw" in the GET request str...
Definition: RawAction.php:38
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:515
getRequest()
Get the WebRequest being used for this instance.
Definition: Action.php:198
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200