MediaWiki REL1_39
RawAction.php
Go to the documentation of this file.
1<?php
37
45
47 private $hookRunner;
48
50 private $parser;
51
53 private $permissionManager;
54
56 private $revisionLookup;
57
59 private $restrictionStore;
60
70 public function __construct(
71 Page $page,
72 IContextSource $context,
73 HookContainer $hookContainer,
74 Parser $parser,
75 PermissionManager $permissionManager,
76 RevisionLookup $revisionLookup,
77 RestrictionStore $restrictionStore
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->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 // Fetch specific slot if defined
241 $slot = $this->getRequest()->getText( 'slot' );
242 if ( $slot ) {
243 if ( $rev->hasSlot( $slot ) ) {
244 $content = $rev->getContent( $slot );
245 } else {
246 $content = null;
247 }
248 } else {
249 $content = $rev->getContent( SlotRecord::MAIN );
250 }
251
252 if ( $content === null ) {
253 // revision or slot not found (or suppressed)
254 } elseif ( !$content instanceof TextContent ) {
255 // non-text content
256 wfHttpError( 415, "Unsupported Media Type", "The requested page uses the content model `"
257 . $content->getModel() . "` which is not supported via this interface." );
258 die();
259 } else {
260 // want a section?
261 $section = $request->getIntOrNull( 'section' );
262 if ( $section !== null ) {
263 $content = $content->getSection( $section );
264 }
265
266 if ( $content === null || $content === false ) {
267 // section not found (or section not supported, e.g. for JS, JSON, and CSS)
268 } else {
269 $text = $content->getText();
270 }
271 }
272 }
273
274 if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) {
275 $text = $this->parser->preprocess(
276 $text,
277 $title,
278 ParserOptions::newFromContext( $this->getContext() )
279 );
280 }
281
282 return $text;
283 }
284
290 public function getOldId() {
291 $oldid = $this->getRequest()->getInt( 'oldid' );
292 $rl = $this->revisionLookup;
293 switch ( $this->getRequest()->getText( 'direction' ) ) {
294 case 'next':
295 # output next revision, or nothing if there isn't one
296 $nextRev = null;
297 if ( $oldid ) {
298 $oldRev = $rl->getRevisionById( $oldid );
299 if ( $oldRev ) {
300 $nextRev = $rl->getNextRevision( $oldRev );
301 }
302 }
303 $oldid = $nextRev ? $nextRev->getId() : -1;
304 break;
305 case 'prev':
306 # output previous revision, or nothing if there isn't one
307 $prevRev = null;
308 if ( !$oldid ) {
309 # get the current revision so we can get the penultimate one
310 $oldid = $this->getWikiPage()->getLatest();
311 }
312 $oldRev = $rl->getRevisionById( $oldid );
313 if ( $oldRev ) {
314 $prevRev = $rl->getPreviousRevision( $oldRev );
315 }
316 $oldid = $prevRev ? $prevRev->getId() : -1;
317 break;
318 case 'cur':
319 $oldid = 0;
320 break;
321 }
322
323 // @phan-suppress-next-line PhanTypeMismatchReturnNullable RevisionRecord::getId does not return null here
324 return $oldid;
325 }
326
332 public function getContentType() {
333 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
334 $ctype = $this->getRequest()->getRawVal( 'ctype' );
335
336 if ( $ctype == '' ) {
337 // Legacy compatibility
338 $gen = $this->getRequest()->getRawVal( 'gen' );
339 if ( $gen == 'js' ) {
340 $ctype = 'text/javascript';
341 } elseif ( $gen == 'css' ) {
342 $ctype = 'text/css';
343 }
344 }
345
346 $allowedCTypes = [
347 'text/x-wiki',
348 'text/javascript',
349 'text/css',
350 // FIXME: Should we still allow Zope editing? External editing feature was dropped
351 'application/x-zope-edit',
352 'application/json'
353 ];
354 if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) {
355 $ctype = 'text/x-wiki';
356 }
357
358 return $ctype;
359 }
360}
getUser()
getAuthority()
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.
getContext()
getWikiPage()
Get a WikiPage object.
Definition Action.php:209
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:160
getRequest()
Get the WebRequest being used for this instance.
Definition Action.php:150
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...
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.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:96
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.
getRawText()
Get the text that should be returned, or false if the page or revision was not found.
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
__construct(Page $page, IContextSource $context, HookContainer $hookContainer, Parser $parser, PermissionManager $permissionManager, RevisionLookup $revisionLookup, RestrictionStore $restrictionStore)
Definition RawAction.php:70
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.
Content object implementation for representing flat text.
static newFromName( $name, $validate='valid')
Definition User.php:598
Interface for objects which can provide a MediaWiki context on request.
Service for looking up page revisions.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:29
$content
Definition router.php:76