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