MediaWiki master
RawAction.php
Go to the documentation of this file.
1<?php
39
47
48 private Parser $parser;
49 private PermissionManager $permissionManager;
50 private RevisionLookup $revisionLookup;
51 private RestrictionStore $restrictionStore;
52 private UserFactory $userFactory;
53
63 public function __construct(
64 Article $article,
65 IContextSource $context,
66 Parser $parser,
67 PermissionManager $permissionManager,
68 RevisionLookup $revisionLookup,
69 RestrictionStore $restrictionStore,
70 UserFactory $userFactory
71 ) {
72 parent::__construct( $article, $context );
73 $this->parser = $parser;
74 $this->permissionManager = $permissionManager;
75 $this->revisionLookup = $revisionLookup;
76 $this->restrictionStore = $restrictionStore;
77 $this->userFactory = $userFactory;
78 }
79
80 public function getName() {
81 return 'raw';
82 }
83
84 public function requiresWrite() {
85 return false;
86 }
87
88 public function requiresUnblock() {
89 return false;
90 }
91
96 public function onView() {
97 $this->getOutput()->disable();
98 $request = $this->getRequest();
99 $response = $request->response();
100 $config = $this->context->getConfig();
101
102 if ( $this->getOutput()->checkLastModified(
103 $this->getWikiPage()->getTouched()
104 ) ) {
105 return null; // Client cache fresh and headers sent, nothing more to do.
106 }
107
108 $contentType = $this->getContentType();
109
110 $maxage = $request->getInt( 'maxage', $config->get( MainConfigNames::CdnMaxAge ) );
111 $smaxage = $request->getIntOrNull( 'smaxage' );
112 if ( $smaxage === null ) {
113 if (
114 $contentType == 'text/css' ||
115 $contentType == 'application/json' ||
116 $contentType == 'text/javascript'
117 ) {
118 // CSS/JSON/JS raw content has its own CDN max age configuration.
119 // Note: HtmlCacheUpdater::getUrls() includes action=raw for css/json/js
120 // pages, so if using the canonical url, this will get HTCP purges.
121 $smaxage = intval( $config->get( MainConfigNames::ForcedRawSMaxage ) );
122 } else {
123 // No CDN cache for anything else
124 $smaxage = 0;
125 }
126 }
127
128 // Set standard Vary headers so cache varies on cookies and such (T125283)
129 $response->header( $this->getOutput()->getVaryHeader() );
130
131 // Output may contain user-specific data;
132 // vary generated content for open sessions on private wikis
133 $privateCache = !$this->permissionManager->isEveryoneAllowed( 'read' ) &&
134 ( $smaxage == 0 || MediaWiki\Session\SessionManager::getGlobalSession()->isPersistent() );
135 // Don't accidentally cache cookies if user is registered (T55032)
136 $privateCache = $privateCache || $this->getUser()->isRegistered();
137 $mode = $privateCache ? 'private' : 'public';
138 $response->header(
139 'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage
140 );
141
142 // In the event of user JS, don't allow loading a user JS/CSS/Json
143 // subpage that has no registered user associated with, as
144 // someone could register the account and take control of the
145 // JS/CSS/Json page.
146 $title = $this->getTitle();
147 if ( $title->isUserConfigPage() && $contentType !== 'text/x-wiki' ) {
148 // not using getRootText() as we want this to work
149 // even if subpages are disabled.
150 $rootPage = strtok( $title->getText(), '/' );
151 $userFromTitle = $this->userFactory->newFromName( $rootPage, UserRigorOptions::RIGOR_USABLE );
152 if ( !$userFromTitle || !$userFromTitle->isRegistered() ) {
153 $elevated = $this->getAuthority()->isAllowed( 'editinterface' );
154 $elevatedText = $elevated ? 'by elevated ' : '';
155 $log = LoggerFactory::getInstance( "security" );
156 $log->warning(
157 "Unsafe JS/CSS/Json {$elevatedText}load - {user} loaded {title} with {ctype}",
158 [
159 'user' => $this->getUser()->getName(),
160 'title' => $title->getPrefixedDBkey(),
161 'ctype' => $contentType,
162 'elevated' => $elevated
163 ]
164 );
165 throw new HttpError( 403, wfMessage( 'unregistered-user-config' ) );
166 }
167 }
168
169 // Don't allow loading non-protected pages as javascript.
170 // In future we may further restrict this to only CONTENT_MODEL_JAVASCRIPT
171 // in NS_MEDIAWIKI or NS_USER, as well as including other config types,
172 // but for now be more permissive. Allowing protected pages outside of
173 // NS_USER and NS_MEDIAWIKI in particular should be considered a temporary
174 // allowance.
175 $pageRestrictions = $this->restrictionStore->getRestrictions( $title, 'edit' );
176 if (
177 $contentType === 'text/javascript' &&
178 !$title->isUserJsConfigPage() &&
179 !$title->inNamespace( NS_MEDIAWIKI ) &&
180 !in_array( 'sysop', $pageRestrictions ) &&
181 !in_array( 'editprotected', $pageRestrictions )
182 ) {
183
184 $log = LoggerFactory::getInstance( "security" );
185 $log->info( "Blocked loading unprotected JS {title} for {user}",
186 [
187 'user' => $this->getUser()->getName(),
188 'title' => $title->getPrefixedDBkey(),
189 ]
190 );
191 throw new HttpError( 403, wfMessage( 'unprotected-js' ) );
192 }
193
194 $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
195
196 $text = $this->getRawText();
197
198 // Don't return a 404 response for CSS or JavaScript;
199 // 404s aren't generally cached and it would create
200 // extra hits when user CSS/JS are on and the user doesn't
201 // have the pages.
202 if ( $text === false && $contentType == 'text/x-wiki' ) {
203 $response->statusHeader( 404 );
204 }
205
206 if ( !$this->getHookRunner()->onRawPageViewBeforeOutput( $this, $text ) ) {
207 wfDebug( __METHOD__ . ": RawPageViewBeforeOutput hook broke raw page output." );
208 }
209
210 echo $text;
211
212 return null;
213 }
214
221 public function getRawText() {
222 $text = false;
223 $title = $this->getTitle();
224 $request = $this->getRequest();
225
226 // Get it from the DB
227 $rev = $this->revisionLookup->getRevisionByTitle( $title, $this->getOldId() );
228 if ( $rev ) {
229 $lastmod = wfTimestamp( TS_RFC2822, $rev->getTimestamp() );
230 $request->response()->header( "Last-modified: $lastmod" );
231
232 // Public-only due to cache headers
233 // Fetch specific slot if defined
234 $slot = $this->getRequest()->getText( 'slot' );
235 if ( $slot ) {
236 if ( $rev->hasSlot( $slot ) ) {
237 $content = $rev->getContent( $slot );
238 } else {
239 $content = null;
240 }
241 } else {
242 $content = $rev->getContent( SlotRecord::MAIN );
243 }
244
245 if ( $content === null ) {
246 // revision or slot not found (or suppressed)
247 } elseif ( !$content instanceof TextContent && !method_exists( $content, 'getText' ) ) {
248 // non-text content
249 wfHttpError( 415, "Unsupported Media Type", "The requested page uses the content model `"
250 . $content->getModel() . "` which is not supported via this interface." );
251 die();
252 } else {
253 // want a section?
254 $section = $request->getIntOrNull( 'section' );
255 if ( $section !== null ) {
256 $content = $content->getSection( $section );
257 }
258
259 if ( $content === null || $content === false ) {
260 // section not found (or section not supported, e.g. for JS, JSON, and CSS)
261 } else {
262 $text = $content->getText();
263 }
264 }
265 }
266
267 if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) {
268 $text = $this->parser->preprocess(
269 $text,
270 $title,
271 ParserOptions::newFromContext( $this->getContext() )
272 );
273 }
274
275 return $text;
276 }
277
283 public function getOldId() {
284 $oldid = $this->getRequest()->getInt( 'oldid' );
285 $rl = $this->revisionLookup;
286 switch ( $this->getRequest()->getText( 'direction' ) ) {
287 case 'next':
288 # output next revision, or nothing if there isn't one
289 $nextRev = null;
290 if ( $oldid ) {
291 $oldRev = $rl->getRevisionById( $oldid );
292 if ( $oldRev ) {
293 $nextRev = $rl->getNextRevision( $oldRev );
294 }
295 }
296 $oldid = $nextRev ? $nextRev->getId() : -1;
297 break;
298 case 'prev':
299 # output previous revision, or nothing if there isn't one
300 $prevRev = null;
301 if ( !$oldid ) {
302 # get the current revision so we can get the penultimate one
303 $oldid = $this->getWikiPage()->getLatest();
304 }
305 $oldRev = $rl->getRevisionById( $oldid );
306 if ( $oldRev ) {
307 $prevRev = $rl->getPreviousRevision( $oldRev );
308 }
309 $oldid = $prevRev ? $prevRev->getId() : -1;
310 break;
311 case 'cur':
312 $oldid = 0;
313 break;
314 }
315
316 // @phan-suppress-next-line PhanTypeMismatchReturnNullable RevisionRecord::getId does not return null here
317 return $oldid;
318 }
319
325 public function getContentType() {
326 // Optimisation: Avoid slow getVal(), this isn't user-generated content.
327 $ctype = $this->getRequest()->getRawVal( 'ctype' );
328
329 if ( $ctype == '' ) {
330 // Legacy compatibility
331 $gen = $this->getRequest()->getRawVal( 'gen' );
332 if ( $gen == 'js' ) {
333 $ctype = 'text/javascript';
334 } elseif ( $gen == 'css' ) {
335 $ctype = 'text/css';
336 }
337 }
338
339 $allowedCTypes = [
340 'text/x-wiki',
341 'text/javascript',
342 'text/css',
343 // FIXME: Should we still allow Zope editing? External editing feature was dropped
344 'application/x-zope-edit',
345 'application/json'
346 ];
347 if ( $ctype == '' || !in_array( $ctype, $allowedCTypes ) ) {
348 $ctype = 'text/x-wiki';
349 }
350
351 return $ctype;
352 }
353}
getUser()
getRequest()
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:190
getHookRunner()
Definition Action.php:255
getOutput()
Get the OutputPage being used for this instance.
Definition Action.php:141
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:67
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
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:155
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Value object representing a content slot associated with a page revision.
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:46
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:80
requiresWrite()
Whether this action requires the wiki not to be locked.
Definition RawAction.php:84
__construct(Article $article, IContextSource $context, Parser $parser, PermissionManager $permissionManager, RevisionLookup $revisionLookup, RestrictionStore $restrictionStore, UserFactory $userFactory)
Definition RawAction.php:63
requiresUnblock()
Whether this action can still be executed by a blocked user.
Definition RawAction.php:88
getOldId()
Get the ID of the revision that should used to get the text.
Content object implementation for representing flat 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.