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