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