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