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