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