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