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