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