MediaWiki master
RawAction.php
Go to the documentation of this file.
1<?php
42
50
51 private Parser $parser;
52 private PermissionManager $permissionManager;
53 private RevisionLookup $revisionLookup;
54 private RestrictionStore $restrictionStore;
55 private UserFactory $userFactory;
56
66 public function __construct(
67 Article $article,
68 IContextSource $context,
69 Parser $parser,
70 PermissionManager $permissionManager,
71 RevisionLookup $revisionLookup,
72 RestrictionStore $restrictionStore,
73 UserFactory $userFactory
74 ) {
75 parent::__construct( $article, $context );
76 $this->parser = $parser;
77 $this->permissionManager = $permissionManager;
78 $this->revisionLookup = $revisionLookup;
79 $this->restrictionStore = $restrictionStore;
80 $this->userFactory = $userFactory;
81 }
82
84 public function getName() {
85 return 'raw';
86 }
87
88 public function requiresWrite() {
89 return false;
90 }
91
92 public function requiresUnblock() {
93 return false;
94 }
95
100 public function onView() {
101 $this->getOutput()->disable();
102 $request = $this->getRequest();
103 $response = $request->response();
104 $config = $this->context->getConfig();
105
106 if ( $this->getOutput()->checkLastModified(
107 $this->getWikiPage()->getTouched()
108 ) ) {
109 // Client cache fresh and headers sent, nothing more to do.
110 return null;
111 }
112
113 $contentType = $this->getContentType();
114
115 $maxage = $request->getInt( 'maxage', $config->get( MainConfigNames::CdnMaxAge ) );
116 $smaxage = $request->getIntOrNull( 'smaxage' );
117 if ( $smaxage === null ) {
118 if (
119 $contentType === 'text/css' ||
120 $contentType === 'application/json' ||
121 $contentType === 'text/javascript'
122 ) {
123 // CSS/JSON/JS raw content has its own CDN max age configuration.
124 // Note: HTMLCacheUpdater::getUrls() includes action=raw for css/json/js
125 // pages, so if using the canonical url, this will get HTCP purges.
126 $smaxage = intval( $config->get( MainConfigNames::ForcedRawSMaxage ) );
127 } else {
128 // No CDN cache for anything else
129 $smaxage = 0;
130 }
131 }
132
133 // Set standard Vary headers so cache varies on cookies and such (T125283)
134 $response->header( $this->getOutput()->getVaryHeader() );
135
136 // Output may contain user-specific data;
137 // vary generated content for open sessions on private wikis
138 $privateCache = !$this->permissionManager->isEveryoneAllowed( 'read' ) &&
139 ( $smaxage === 0 || SessionManager::getGlobalSession()->isPersistent() );
140 // Don't accidentally cache cookies if the user is registered (T55032)
141 $privateCache = $privateCache || $this->getUser()->isRegistered();
142 $mode = $privateCache ? 'private' : 'public';
143 $response->header(
144 'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage
145 );
146
147 // In the event of user JS, don't allow loading a user JS/CSS/Json
148 // subpage that has no registered user associated with, as
149 // someone could register the account and take control of the
150 // JS/CSS/Json page.
151 $title = $this->getTitle();
152 if ( $title->isUserConfigPage() && $contentType !== 'text/x-wiki' ) {
153 // not using getRootText() as we want this to work
154 // even if subpages are disabled.
155 $rootPage = strtok( $title->getText(), '/' );
156 $userFromTitle = $this->userFactory->newFromName( $rootPage, UserRigorOptions::RIGOR_USABLE );
157 if ( !$userFromTitle || !$userFromTitle->isRegistered() ) {
158 $elevated = $this->getAuthority()->isAllowed( 'editinterface' );
159 $elevatedText = $elevated ? 'by elevated ' : '';
160 $log = LoggerFactory::getInstance( "security" );
161 $log->warning(
162 "Unsafe JS/CSS/Json {$elevatedText}load - {user} loaded {title} with {ctype}",
163 [
164 'user' => $this->getUser()->getName(),
165 'title' => $title->getPrefixedDBkey(),
166 'ctype' => $contentType,
167 'elevated' => $elevated
168 ]
169 );
170 throw new HttpError( 403, wfMessage( 'unregistered-user-config' ) );
171 }
172 }
173
174 // Don't allow loading non-protected pages as javascript.
175 // In the future, we may further restrict this to only CONTENT_MODEL_JAVASCRIPT
176 // in NS_MEDIAWIKI or NS_USER, as well as including other config types,
177 // but for now be more permissive. Allowing protected pages outside
178 // NS_USER and NS_MEDIAWIKI in particular should be considered a temporary
179 // allowance.
180 $pageRestrictions = $this->restrictionStore->getRestrictions( $title, 'edit' );
181 if (
182 $contentType === 'text/javascript' &&
183 !$title->isUserJsConfigPage() &&
184 !$title->inNamespace( NS_MEDIAWIKI ) &&
185 !in_array( 'sysop', $pageRestrictions ) &&
186 !in_array( 'editprotected', $pageRestrictions )
187 ) {
188
189 $log = LoggerFactory::getInstance( "security" );
190 $log->info( "Blocked loading unprotected JS {title} for {user}",
191 [
192 'user' => $this->getUser()->getName(),
193 'title' => $title->getPrefixedDBkey(),
194 ]
195 );
196 throw new HttpError( 403, wfMessage( 'unprotected-js' ) );
197 }
198
199 $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
200
201 $text = $this->getRawText();
202
203 // Don't return a 404 response for CSS or JavaScript;
204 // 404s aren't generally cached, and it would create
205 // extra hits when user CSS/JS are on and the user doesn't
206 // have the pages.
207 if ( $text === false && $contentType === 'text/x-wiki' ) {
208 $response->statusHeader( 404 );
209 }
210
211 if ( !$this->getHookRunner()->onRawPageViewBeforeOutput( $this, $text ) ) {
212 wfDebug( __METHOD__ . ": RawPageViewBeforeOutput hook broke raw page output." );
213 }
214
215 echo $text;
216
217 return null;
218 }
219
226 public function getRawText() {
227 $text = false;
228 $title = $this->getTitle();
229 $request = $this->getRequest();
230
231 // Get it from the DB
232 $rev = $this->revisionLookup->getRevisionByTitle( $title, $this->getOldId() );
233 if ( $rev ) {
234 $lastMod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
235 $request->response()->header( "Last-modified: $lastMod" );
236
237 // Public-only due to cache headers
238 // Fetch specific slot if defined
239 $slot = $this->getRequest()->getText( 'slot' );
240 if ( $slot ) {
241 if ( $rev->hasSlot( $slot ) ) {
242 $content = $rev->getContent( $slot );
243 } else {
244 $content = null;
245 }
246 } else {
247 $content = $rev->getContent( SlotRecord::MAIN );
248 }
249
250 if ( $content === null ) {
251 // revision or slot was not found (or suppressed)
252 } elseif ( !$content instanceof TextContent && !method_exists( $content, 'getText' ) ) {
253 // non-text content
255 415,
256 "Unsupported Media Type", "The requested page uses the content model `"
257 . $content->getModel() . "` which is not supported via this interface."
258 );
259 die();
260 } else {
261 // want a section?
262 $section = $request->getIntOrNull( 'section' );
263 if ( $section !== null ) {
264 $content = $content->getSection( $section );
265 }
266
267 if ( $content !== null && $content !== false ) {
268 // section found (and section supported, e.g. not for JS, JSON, and CSS)
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 static $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}
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.
getWikiPage()
Get a WikiPage object.
Definition Action.php:192
getHookRunner()
Definition Action.php:257
getContext()
Get the IContextSource in use here.
Definition Action.php:119
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:143
getUser()
Shortcut to get the User being used for this instance.
Definition Action.php:153
getAuthority()
Shortcut to get the Authority executing this instance.
Definition Action.php:163
getRequest()
Get the WebRequest being used for this instance.
Definition Action.php:133
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:72
An action which just does something, without showing a form first.
Show an error that looks like an HTTP server error.
Definition HttpError.php:33
Content object implementation for representing flat text.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Set options of the Parser.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:143
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.
Creates User objects.
A simple method to retrieve the plain source of an article, using "action=raw" in the GET request str...
Definition RawAction.php:49
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.
getName()
Return the name of the action this object responds to.1.17string Lowercase name
Definition RawAction.php:84
requiresWrite()
Indicates whether this action page write access to the wiki.
Definition RawAction.php:88
__construct(Article $article, IContextSource $context, Parser $parser, PermissionManager $permissionManager, RevisionLookup $revisionLookup, RestrictionStore $restrictionStore, UserFactory $userFactory)
Definition RawAction.php:66
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition RawAction.php:92
getOldId()
Get the ID of the revision that should be used to get the text.
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.