Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
11.02% |
41 / 372 |
|
16.13% |
10 / 62 |
CRAP | |
0.00% |
0 / 1 |
MediaWikiEntryPoint | |
11.02% |
41 / 372 |
|
16.13% |
10 / 62 |
14548.46 | |
0.00% |
0 / 1 |
__construct | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
3.10 | |||
setup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
doSetup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prepareForOutput | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
doPrepareForOutput | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
handleTopLevelError | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
schedulePostSendJobs | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
110 | |||
commitMainTransaction | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
380 | |||
getUrlDomainDistance | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getRequestPathSuffix | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
postOutputShutdown | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
doPostOutputShutdown | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
shouldDoHttpRedirect | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
outputResponsePayload | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
132 | |||
restInPeace | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
6 | |||
emitBufferedStatsdData | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
triggerSyncJobs | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
triggerAsyncJobs | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
72 | |||
getServiceContainer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUrlUtils | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getReadOnlyMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getJobRunner | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDBLoadBalancerFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMessageCache | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getBlockManager | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStatsFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStatsdDataFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getJobQueueGroupFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSpecialPageFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getContext | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getResponse | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getConfig | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isCli | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hasFastCgi | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getServerInfo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
|
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | ||||
exit | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
startOutputBuffer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
drainOutputBuffer | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
enableOutputCapture | |
50.00% |
3 / 6 |
|
0.00% |
0 / 1 |
2.50 | |||
getOutputBufferLevel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
commitOutputBuffer | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getCapturedOutput | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
flushOutputBuffer | |
93.75% |
15 / 16 |
|
0.00% |
0 / 1 |
7.01 | |||
discardAllOutput | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getOutputBufferLength | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOutputBufferStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
discardOutputBuffer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableModDeflate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStatusCode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
inPostSendMode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
triggerError | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEnv | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getIni | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setIniOption | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
header | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
status | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fastCgiFinishRequest | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
getRequestURL | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enterPostSendMode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki; |
22 | |
23 | use Exception; |
24 | use HttpStatus; |
25 | use JobQueueGroup; |
26 | use JobRunner; |
27 | use Liuggio\StatsdClient\Sender\SocketSender; |
28 | use Liuggio\StatsdClient\StatsdClient; |
29 | use LogicException; |
30 | use MediaWiki\Block\BlockManager; |
31 | use MediaWiki\Config\Config; |
32 | use MediaWiki\Config\ConfigException; |
33 | use MediaWiki\Context\IContextSource; |
34 | use MediaWiki\Deferred\DeferredUpdates; |
35 | use MediaWiki\Deferred\TransactionRoundDefiningUpdate; |
36 | use MediaWiki\HookContainer\ProtectedHookAccessorTrait; |
37 | use MediaWiki\JobQueue\JobQueueGroupFactory; |
38 | use MediaWiki\Logger\LoggerFactory; |
39 | use MediaWiki\Request\WebRequest; |
40 | use MediaWiki\Request\WebResponse; |
41 | use MediaWiki\SpecialPage\SpecialPageFactory; |
42 | use MediaWiki\Specials\SpecialRunJobs; |
43 | use MediaWiki\Utils\UrlUtils; |
44 | use MediaWiki\WikiMap\WikiMap; |
45 | use MessageCache; |
46 | use MWExceptionHandler; |
47 | use Profiler; |
48 | use Psr\Log\LoggerInterface; |
49 | use RuntimeException; |
50 | use Throwable; |
51 | use Wikimedia\AtEase\AtEase; |
52 | use Wikimedia\Rdbms\ChronologyProtector; |
53 | use Wikimedia\Rdbms\LBFactory; |
54 | use Wikimedia\Rdbms\ReadOnlyMode; |
55 | use Wikimedia\ScopedCallback; |
56 | use Wikimedia\Stats\IBufferingStatsdDataFactory; |
57 | use Wikimedia\Stats\StatsFactory; |
58 | |
59 | /** |
60 | * @defgroup entrypoint Entry points |
61 | * |
62 | * Web entry points reside in top-level MediaWiki directory (i.e. installation path). |
63 | * These entry points handle web requests to interact with the wiki. Other PHP files |
64 | * in the repository are not accessed directly from the web, but instead included by |
65 | * an entry point. |
66 | */ |
67 | |
68 | /** |
69 | * Base class for entry point handlers. |
70 | * |
71 | * @note: This is not stable to extend by extensions, because MediaWiki does not |
72 | * allow extensions to define new entry points. |
73 | * |
74 | * @ingroup entrypoint |
75 | * @since 1.42, factored out of the previously existing MediaWiki class. |
76 | */ |
77 | abstract class MediaWikiEntryPoint { |
78 | use ProtectedHookAccessorTrait; |
79 | |
80 | private IContextSource $context; |
81 | private Config $config; |
82 | private ?int $outputCaptureLevel = null; |
83 | |
84 | private bool $postSendMode = false; |
85 | |
86 | /** @var int Class DEFER_* constant; how non-blocking post-response tasks should run */ |
87 | private int $postSendStrategy; |
88 | |
89 | /** Call fastcgi_finish_request() to make post-send updates async */ |
90 | private const DEFER_FASTCGI_FINISH_REQUEST = 1; |
91 | |
92 | /** Set Content-Length and call ob_end_flush()/flush() to make post-send updates async */ |
93 | private const DEFER_SET_LENGTH_AND_FLUSH = 2; |
94 | |
95 | /** Do not try to make post-send updates async (e.g. for CLI mode) */ |
96 | private const DEFER_CLI_MODE = 3; |
97 | |
98 | private bool $preparedForOutput = false; |
99 | |
100 | protected EntryPointEnvironment $environment; |
101 | |
102 | private MediaWikiServices $mediaWikiServices; |
103 | |
104 | /** |
105 | * @param IContextSource $context |
106 | * @param EntryPointEnvironment $environment |
107 | * @param MediaWikiServices $mediaWikiServices |
108 | */ |
109 | public function __construct( |
110 | IContextSource $context, |
111 | EntryPointEnvironment $environment, |
112 | MediaWikiServices $mediaWikiServices |
113 | ) { |
114 | $this->context = $context; |
115 | $this->config = $this->context->getConfig(); |
116 | $this->environment = $environment; |
117 | $this->mediaWikiServices = $mediaWikiServices; |
118 | |
119 | if ( $environment->isCli() ) { |
120 | $this->postSendStrategy = self::DEFER_CLI_MODE; |
121 | } elseif ( $environment->hasFastCgi() ) { |
122 | $this->postSendStrategy = self::DEFER_FASTCGI_FINISH_REQUEST; |
123 | } else { |
124 | $this->postSendStrategy = self::DEFER_SET_LENGTH_AND_FLUSH; |
125 | } |
126 | } |
127 | |
128 | /** |
129 | * Perform any setup needed before execute() is called. |
130 | * Final wrapper function for setup(). |
131 | * Will be called by doRun(). |
132 | */ |
133 | final protected function setup() { |
134 | // Much of the functionality in WebStart.php and Setup.php should be moved here eventually. |
135 | // As of MW 1.41, a lot of it still wants to run in file scope. |
136 | |
137 | // TODO: move define( 'MW_ENTRY_POINT' here ) |
138 | // TODO: move ProfilingContext::singleton()->init( ... ) here. |
139 | |
140 | $this->doSetup(); |
141 | } |
142 | |
143 | /** |
144 | * Perform any setup needed before execute() is called. |
145 | * Called by doRun() via doSetup(). |
146 | */ |
147 | protected function doSetup() { |
148 | // no-op |
149 | // TODO: move ob_start( [ MediaWiki\Output\OutputHandler::class, 'handle' ] ) here |
150 | // TODO: move MW_NO_OUTPUT_COMPRESSION handling here. |
151 | // TODO: move HeaderCallback::register() here |
152 | // TODO: move SessionManager::getGlobalSession() here (from Setup.php) |
153 | // TODO: move AuthManager::autoCreateUser here (from Setup.php) |
154 | // TODO: move pingback here (from Setup.php) |
155 | } |
156 | |
157 | /** |
158 | * Prepare for sending the output. Should be called by entry points before |
159 | * sending the response. |
160 | * Final wrapper function for doPrepareForOutput(). |
161 | * Will be called automatically at the end of doRun(), but will do nothing if it was |
162 | * already called from execute(). |
163 | */ |
164 | final protected function prepareForOutput() { |
165 | if ( $this->preparedForOutput ) { |
166 | // only do this once. |
167 | return; |
168 | } |
169 | |
170 | $this->preparedForOutput = true; |
171 | |
172 | $this->doPrepareForOutput(); |
173 | } |
174 | |
175 | /** |
176 | * Prepare for sending the output. Should be called by entry points before |
177 | * sending the response. |
178 | * Will be called automatically by run() via prepareForOutput(). |
179 | * Subclasses may override this method, but should not call it directly. |
180 | * |
181 | * @note arc-lamp profiling relies on the name of this method, |
182 | * it's hard coded in the arclamp-generate-svgs script! |
183 | */ |
184 | protected function doPrepareForOutput() { |
185 | // Commit any changes in the current transaction round so that: |
186 | // a) the transaction is not rolled back after success output was already sent |
187 | // b) error output is not jumbled together with success output in the response |
188 | // TODO: split this up and pull out stuff like spreading cookie blocks |
189 | $this->commitMainTransaction(); |
190 | } |
191 | |
192 | /** |
193 | * Main app life cycle: Calls doSetup(), execute(), |
194 | * prepareForOutput(), and postOutputShutdown(). |
195 | */ |
196 | final public function run() { |
197 | $this->setup(); |
198 | |
199 | try { |
200 | $this->execute(); |
201 | |
202 | // Prepare for flushing the output. Will do nothing if it was already called by execute(). |
203 | $this->prepareForOutput(); |
204 | } catch ( Throwable $e ) { |
205 | $this->status( 500 ); |
206 | $this->handleTopLevelError( $e ); |
207 | } |
208 | |
209 | $this->postOutputShutdown(); |
210 | } |
211 | |
212 | /** |
213 | * Report a top level error. |
214 | * Subclasses in core may override this to handle errors according |
215 | * to the expected output format. |
216 | * This method is not safe to override for extensions. |
217 | * |
218 | * @param Throwable $e |
219 | */ |
220 | protected function handleTopLevelError( Throwable $e ) { |
221 | // Type errors and such: at least handle it now and clean up the LBFactory state |
222 | MWExceptionHandler::handleException( $e, MWExceptionHandler::CAUGHT_BY_ENTRYPOINT ); |
223 | } |
224 | |
225 | /** |
226 | * Subclasses implement the entry point's functionality by overriding this method. |
227 | * This method is not safe to override for extensions. |
228 | */ |
229 | abstract protected function execute(); |
230 | |
231 | /** |
232 | * If enabled, after everything specific to this request is done, occasionally run jobs |
233 | */ |
234 | protected function schedulePostSendJobs() { |
235 | $jobRunRate = $this->config->get( MainConfigNames::JobRunRate ); |
236 | if ( |
237 | // Post-send job running disabled |
238 | $jobRunRate <= 0 || |
239 | // Jobs cannot run due to site read-only mode |
240 | $this->getReadOnlyMode()->isReadOnly() || |
241 | // HTTP response body and Content-Length headers likely to not match, |
242 | // causing post-send updates to block the client when using mod_php |
243 | $this->context->getRequest()->getMethod() === 'HEAD' || |
244 | $this->context->getRequest()->getHeader( 'If-Modified-Since' ) || |
245 | $this->context->getRequest()->getHeader( 'If-None-Match' ) |
246 | ) { |
247 | return; |
248 | } |
249 | |
250 | if ( $jobRunRate < 1 ) { |
251 | $max = mt_getrandmax(); |
252 | if ( mt_rand( 0, $max ) > $max * $jobRunRate ) { |
253 | return; // the higher the job run rate, the less likely we return here |
254 | } |
255 | $n = 1; |
256 | } else { |
257 | $n = intval( $jobRunRate ); |
258 | } |
259 | |
260 | // Note that DeferredUpdates will catch and log any errors (T88312) |
261 | DeferredUpdates::addUpdate( new TransactionRoundDefiningUpdate( function () use ( $n ) { |
262 | $logger = LoggerFactory::getInstance( 'runJobs' ); |
263 | if ( $this->config->get( MainConfigNames::RunJobsAsync ) ) { |
264 | // Send an HTTP request to the job RPC entry point if possible |
265 | $invokedWithSuccess = $this->triggerAsyncJobs( $n, $logger ); |
266 | if ( !$invokedWithSuccess ) { |
267 | // Fall back to blocking on running the job(s) |
268 | $logger->warning( "Jobs switched to blocking; Special:RunJobs disabled" ); |
269 | $this->triggerSyncJobs( $n ); |
270 | } |
271 | } else { |
272 | $this->triggerSyncJobs( $n ); |
273 | } |
274 | }, __METHOD__ ) ); |
275 | } |
276 | |
277 | /** |
278 | * This function commits all DB and session changes as needed *before* the |
279 | * client can receive a response (in case DB commit fails) and thus also before |
280 | * the response can trigger a subsequent related request by the client |
281 | */ |
282 | protected function commitMainTransaction() { |
283 | $context = $this->context; |
284 | |
285 | $config = $context->getConfig(); |
286 | $request = $context->getRequest(); |
287 | $output = $context->getOutput(); |
288 | |
289 | // Try to make sure that all RDBMs, session, and other storage updates complete |
290 | ignore_user_abort( true ); |
291 | |
292 | $lbFactory = $this->getDBLoadBalancerFactory(); |
293 | |
294 | // Commit all RDBMs changes from the main transaction round |
295 | $lbFactory->commitPrimaryChanges( |
296 | __METHOD__, |
297 | // Abort if any transaction was too big |
298 | $config->get( MainConfigNames::MaxUserDBWriteDuration ) |
299 | ); |
300 | wfDebug( __METHOD__ . ': primary transaction round committed' ); |
301 | |
302 | // Run updates that need to block the client or affect output (this is the last chance) |
303 | DeferredUpdates::doUpdates( |
304 | $config->get( MainConfigNames::ForceDeferredUpdatesPreSend ) |
305 | ? DeferredUpdates::ALL |
306 | : DeferredUpdates::PRESEND |
307 | ); |
308 | |
309 | wfDebug( __METHOD__ . ': pre-send deferred updates completed' ); |
310 | |
311 | if ( !defined( 'MW_NO_SESSION' ) ) { |
312 | // Persist the session to avoid race conditions on subsequent requests by the client |
313 | $request->getSession()->save(); // T214471 |
314 | wfDebug( __METHOD__ . ': session changes committed' ); |
315 | } |
316 | |
317 | // Subsequent requests by the client should see the DB replication positions, as written |
318 | // to ChronologyProtector during the shutdown() call below. |
319 | // Setting the cpPosIndex cookie is normally enough. However, this will not work for |
320 | // cross-wiki redirects within the same wiki farm, so set the ?cpPoxIndex in that case. |
321 | $isCrossWikiRedirect = ( |
322 | $output->getRedirect() && |
323 | $lbFactory->hasOrMadeRecentPrimaryChanges( INF ) && |
324 | self::getUrlDomainDistance( $output->getRedirect() ) === 'remote' |
325 | ); |
326 | |
327 | // Persist replication positions for DBs modified by this request (at this point). |
328 | // These help provide "session consistency" for the client on their next requests. |
329 | $cpIndex = null; |
330 | $cpClientId = null; |
331 | $lbFactory->shutdown( |
332 | $lbFactory::SHUTDOWN_NORMAL, |
333 | null, |
334 | $cpIndex, |
335 | $cpClientId |
336 | ); |
337 | $now = time(); |
338 | |
339 | $allowHeaders = !( $output->isDisabled() || $this->getResponse()->headersSent() ); |
340 | |
341 | if ( $cpIndex > 0 ) { |
342 | if ( $allowHeaders ) { |
343 | $expires = $now + ChronologyProtector::POSITION_COOKIE_TTL; |
344 | $options = [ 'prefix' => '' ]; |
345 | $value = ChronologyProtector::makeCookieValueFromCPIndex( $cpIndex, $now, $cpClientId ); |
346 | $request->response()->setCookie( 'cpPosIndex', $value, $expires, $options ); |
347 | } |
348 | |
349 | if ( $isCrossWikiRedirect ) { |
350 | if ( $output->getRedirect() ) { |
351 | $url = $output->getRedirect(); |
352 | if ( $lbFactory->hasStreamingReplicaServers() ) { |
353 | $url = strpos( $url, '?' ) === false |
354 | ? "$url?cpPosIndex=$cpIndex" : "$url&cpPosIndex=$cpIndex"; |
355 | } |
356 | $output->redirect( $url ); |
357 | } else { |
358 | MWExceptionHandler::logException( |
359 | new LogicException( "No redirect; cannot append cpPosIndex parameter." ), |
360 | MWExceptionHandler::CAUGHT_BY_ENTRYPOINT |
361 | ); |
362 | } |
363 | } |
364 | } |
365 | |
366 | if ( $allowHeaders ) { |
367 | // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that |
368 | // handles this POST request (e.g. the "primary" data center). Also have the user |
369 | // briefly bypass CDN so ChronologyProtector works for cacheable URLs. |
370 | if ( $request->wasPosted() && $lbFactory->hasOrMadeRecentPrimaryChanges() ) { |
371 | $expires = $now + max( |
372 | ChronologyProtector::POSITION_COOKIE_TTL, |
373 | $config->get( MainConfigNames::DataCenterUpdateStickTTL ) |
374 | ); |
375 | $options = [ 'prefix' => '' ]; |
376 | $request->response()->setCookie( 'UseDC', 'master', $expires, $options ); |
377 | } |
378 | |
379 | // Avoid letting a few seconds of replica DB lag cause a month of stale data. |
380 | // This logic is also intimately related to the value of $wgCdnReboundPurgeDelay. |
381 | if ( $lbFactory->laggedReplicaUsed() ) { |
382 | $maxAge = $config->get( MainConfigNames::CdnMaxageLagged ); |
383 | $output->lowerCdnMaxage( $maxAge ); |
384 | $request->response()->header( "X-Database-Lagged: true" ); |
385 | wfDebugLog( 'replication', |
386 | "Lagged DB used; CDN cache TTL limited to $maxAge seconds" ); |
387 | } |
388 | |
389 | // Avoid long-term cache pollution due to message cache rebuild timeouts (T133069) |
390 | if ( $this->getMessageCache()->isDisabled() ) { |
391 | $maxAge = $config->get( MainConfigNames::CdnMaxageSubstitute ); |
392 | $output->lowerCdnMaxage( $maxAge ); |
393 | $request->response()->header( "X-Response-Substitute: true" ); |
394 | } |
395 | |
396 | if ( !$output->couldBePublicCached() || $output->haveCacheVaryCookies() ) { |
397 | // Autoblocks: If this user is autoblocked (and the cookie block feature is enabled |
398 | // for autoblocks), then set a cookie to track this block. |
399 | // This has to be done on all logged-in page loads (not just upon saving edits), |
400 | // because an autoblocked editor might not edit again from the same IP address. |
401 | // |
402 | // IP blocks: For anons, if their IP is blocked (and cookie block feature is enabled |
403 | // for IP blocks), we also want to set the cookie whenever it is safe to do. |
404 | // Basically from any url that are definitely not publicly cacheable (like viewing |
405 | // EditPage), or when the HTTP response is personalised for other reasons (e.g. viewing |
406 | // articles within the same browsing session after making an edit). |
407 | $user = $context->getUser(); |
408 | $this->getBlockManager()->trackBlockWithCookie( $user, $request->response() ); |
409 | } |
410 | } |
411 | } |
412 | |
413 | /** |
414 | * @param string $url |
415 | * @return string Either "local", "remote" if in the farm, "external" otherwise |
416 | */ |
417 | private static function getUrlDomainDistance( $url ) { |
418 | $clusterWiki = WikiMap::getWikiFromUrl( $url ); |
419 | if ( WikiMap::isCurrentWikiId( $clusterWiki ) ) { |
420 | return 'local'; // the current wiki |
421 | } |
422 | if ( $clusterWiki !== false ) { |
423 | return 'remote'; // another wiki in this cluster/farm |
424 | } |
425 | |
426 | return 'external'; |
427 | } |
428 | |
429 | /** |
430 | * If the request URL matches a given base path, extract the path part of |
431 | * the request URL after that base, and decode escape sequences in it. |
432 | * |
433 | * If the request URL does not match, false is returned. |
434 | * |
435 | * @internal Should be protected, made public for backwards |
436 | * compatibility code in WebRequest. |
437 | * @param string $basePath |
438 | * |
439 | * @return false|string |
440 | */ |
441 | public function getRequestPathSuffix( $basePath ) { |
442 | return WebRequest::getRequestPathSuffix( |
443 | $basePath, |
444 | $this->getRequestURL() |
445 | ); |
446 | } |
447 | |
448 | /** |
449 | * Forces the response to be sent to the client and then |
450 | * does work that can be done *after* the |
451 | * user gets the HTTP response, so they don't block on it. |
452 | */ |
453 | final protected function postOutputShutdown() { |
454 | $this->doPostOutputShutdown(); |
455 | |
456 | // Just in case doPostOutputShutdown() was overwritten... |
457 | if ( !$this->inPostSendMode() ) { |
458 | $this->flushOutputBuffer(); |
459 | $this->enterPostSendMode(); |
460 | } |
461 | } |
462 | |
463 | /** |
464 | * Forces the response to be sent to the client and then |
465 | * does work that can be done *after* the |
466 | * user gets the HTTP response, so they don't block on it. |
467 | * |
468 | * @since 1.26 (formerly on the MediaWiki class) |
469 | * |
470 | * @note arc-lamp profiling relies on the name of this method, |
471 | * it's hard coded in the arclamp-generate-svgs script! |
472 | */ |
473 | protected function doPostOutputShutdown() { |
474 | // Record backend request timing |
475 | $timing = $this->context->getTiming(); |
476 | $timing->mark( 'requestShutdown' ); |
477 | |
478 | // Defer everything else if possible... |
479 | if ( $this->postSendStrategy === self::DEFER_FASTCGI_FINISH_REQUEST ) { |
480 | // Flush the output to the client, continue processing, and avoid further output |
481 | $this->fastCgiFinishRequest(); |
482 | } elseif ( $this->postSendStrategy === self::DEFER_SET_LENGTH_AND_FLUSH ) { |
483 | // Flush the output to the client, continue processing, and avoid further output |
484 | $this->flushOutputBuffer(); |
485 | } |
486 | |
487 | // Since the headers and output where already flushed, disable WebResponse setters |
488 | // during post-send processing to warnings and unexpected behavior (T191537) |
489 | $this->enterPostSendMode(); |
490 | |
491 | // Run post-send updates while preventing further output... |
492 | $this->startOutputBuffer( static function () { |
493 | return ''; // do not output uncaught exceptions |
494 | } ); |
495 | try { |
496 | $this->restInPeace(); |
497 | } catch ( Throwable $e ) { |
498 | MWExceptionHandler::rollbackPrimaryChangesAndLog( |
499 | $e, |
500 | MWExceptionHandler::CAUGHT_BY_ENTRYPOINT |
501 | ); |
502 | } |
503 | $length = $this->getOutputBufferLength(); |
504 | if ( $length > 0 ) { |
505 | $this->triggerError( |
506 | __METHOD__ . ": suppressed $length byte(s)", |
507 | E_USER_NOTICE |
508 | ); |
509 | } |
510 | $this->discardOutputBuffer(); |
511 | } |
512 | |
513 | /** |
514 | * Check if an HTTP->HTTPS redirect should be done. It may still be aborted |
515 | * by a hook, so this is not the final word. |
516 | * |
517 | * @return bool |
518 | */ |
519 | protected function shouldDoHttpRedirect() { |
520 | $request = $this->context->getRequest(); |
521 | |
522 | // Don't redirect if we're already on HTTPS |
523 | if ( $request->getProtocol() !== 'http' ) { |
524 | return false; |
525 | } |
526 | |
527 | $force = $this->config->get( MainConfigNames::ForceHTTPS ); |
528 | |
529 | // Don't redirect if $wgServer is explicitly HTTP. We test for this here |
530 | // by checking whether UrlUtils::expand() is able to force HTTPS. |
531 | if ( |
532 | !preg_match( |
533 | '#^https://#', |
534 | (string)$this->getUrlUtils()->expand( |
535 | $request->getRequestURL(), |
536 | PROTO_HTTPS |
537 | ) |
538 | ) |
539 | ) { |
540 | if ( $force ) { |
541 | throw new RuntimeException( '$wgForceHTTPS is true but the server is not HTTPS' ); |
542 | } |
543 | return false; |
544 | } |
545 | |
546 | // Configured $wgForceHTTPS overrides the remaining conditions |
547 | if ( $force ) { |
548 | return true; |
549 | } |
550 | |
551 | // Check if HTTPS is required by the session or user preferences |
552 | return $request->getSession()->shouldForceHTTPS() || |
553 | // Check the cookie manually, for paranoia |
554 | $request->getCookie( 'forceHTTPS', '' ) || |
555 | $this->context->getUser()->requiresHTTPS(); |
556 | } |
557 | |
558 | /** |
559 | * Print a response body to the current buffer (if there is one) or the server (otherwise) |
560 | * |
561 | * This method should be called after commitMainTransaction() and before doPostOutputShutdown() |
562 | * |
563 | * Any accompanying Content-Type header is assumed to have already been set |
564 | * |
565 | * @param string $content Response content, usually from OutputPage::output() |
566 | */ |
567 | protected function outputResponsePayload( $content ) { |
568 | // Append any visible profiling data in a manner appropriate for the Content-Type |
569 | $this->startOutputBuffer(); |
570 | try { |
571 | Profiler::instance()->logDataPageOutputOnly(); |
572 | } finally { |
573 | $content .= $this->drainOutputBuffer(); |
574 | } |
575 | |
576 | // By default, usually one output buffer is active now, either the internal PHP buffer |
577 | // started by "output_buffering" in php.ini or the buffer started by MW_SETUP_CALLBACK. |
578 | // The MW_SETUP_CALLBACK buffer has an unlimited chunk size, while the internal PHP |
579 | // buffer only has an unlimited chunk size if output_buffering="On". If the buffer was |
580 | // filled up to the chunk size with printed data, then HTTP headers will have already |
581 | // been sent. Also, if the entry point had to stream content to the client, then HTTP |
582 | // headers will have already been sent as well, regardless of chunk size. |
583 | |
584 | // Disable mod_deflate compression since it interferes with the output buffer set |
585 | // by MW_SETUP_CALLBACK and can also cause the client to wait on deferred updates |
586 | $this->disableModDeflate(); |
587 | |
588 | if ( $this->inPostSendMode() ) { |
589 | // Output already sent. This may happen for actions or special pages |
590 | // that generate raw output and disable OutputPage. In that case, |
591 | // we should just exit, but we should log an error if $content |
592 | // was not empty. |
593 | if ( $content !== '' ) { |
594 | $length = strlen( $content ); |
595 | $this->triggerError( |
596 | __METHOD__ . ": discarded $length byte(s) of output", |
597 | E_USER_NOTICE |
598 | ); |
599 | } |
600 | return; |
601 | } |
602 | |
603 | if ( |
604 | // "Content-Length" is used to prevent clients from waiting on deferred updates |
605 | $this->postSendStrategy === self::DEFER_SET_LENGTH_AND_FLUSH && |
606 | !$this->getResponse()->headersSent() && |
607 | // The HTTP response code clearly allows for a meaningful body |
608 | in_array( $this->getStatusCode(), [ 200, 404 ], true ) && |
609 | // The queue of (post-send) deferred updates is non-empty |
610 | DeferredUpdates::pendingUpdatesCount() && |
611 | // Any buffered output is not spread out across multiple output buffers |
612 | $this->getOutputBufferLevel() <= 1 |
613 | ) { |
614 | $response = $this->context->getRequest()->response(); |
615 | |
616 | $obStatus = $this->getOutputBufferStatus(); |
617 | if ( !isset( $obStatus['name'] ) ) { |
618 | // No output buffer is active |
619 | $response->header( 'Content-Length: ' . strlen( $content ) ); |
620 | } elseif ( $obStatus['name'] === 'default output handler' ) { |
621 | // Internal PHP "output_buffering" output buffer (note that the internal PHP |
622 | // "zlib.output_compression" output buffer is named "zlib output compression") |
623 | $response->header( 'Content-Length: ' . |
624 | ( $this->getOutputBufferLength() + strlen( $content ) ) ); |
625 | } |
626 | |
627 | // The MW_SETUP_CALLBACK output buffer ("MediaWiki\OutputHandler::handle") sets |
628 | // "Content-Length" where applicable. Other output buffer types might not set this |
629 | // header, and since they might mangle or compress the payload, it is not possible |
630 | // to determine the final payload size here. |
631 | |
632 | // Tell the client to immediately end the connection as soon as the response payload |
633 | // has been read (informed by any "Content-Length" header). This prevents the client |
634 | // from waiting on deferred updates. |
635 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection |
636 | if ( $this->getServerInfo( 'SERVER_PROTOCOL' ) === 'HTTP/1.1' ) { |
637 | $response->header( 'Connection: close' ); |
638 | } |
639 | } |
640 | |
641 | // Print the content *after* adjusting HTTP headers and disabling mod_deflate since |
642 | // calling "print" will send the output to the client if there is no output buffer or |
643 | // if the output buffer chunk size is reached |
644 | $this->print( $content ); |
645 | } |
646 | |
647 | /** |
648 | * Ends this task peacefully. |
649 | * Called after the response has been sent to the client. |
650 | * Subclasses in core may override this to add end-of-request code, |
651 | * but should always call the parent method. |
652 | * This method is not safe to override by extensions. |
653 | */ |
654 | protected function restInPeace() { |
655 | // Either all DB and deferred updates should happen or none. |
656 | // The latter should not be cancelled due to client disconnect. |
657 | ignore_user_abort( true ); |
658 | |
659 | // Assure deferred updates are not in the main transaction |
660 | $lbFactory = $this->getDBLoadBalancerFactory(); |
661 | $lbFactory->commitPrimaryChanges( __METHOD__ ); |
662 | |
663 | // Loosen DB query expectations since the HTTP client is unblocked |
664 | $profiler = Profiler::instance(); |
665 | $trxProfiler = $profiler->getTransactionProfiler(); |
666 | $trxProfiler->redefineExpectations( |
667 | $this->context->getRequest()->hasSafeMethod() |
668 | ? $this->config->get( MainConfigNames::TrxProfilerLimits )['PostSend-GET'] |
669 | : $this->config->get( MainConfigNames::TrxProfilerLimits )['PostSend-POST'], |
670 | __METHOD__ |
671 | ); |
672 | |
673 | // Do any deferred jobs; preferring to run them now if a client will not wait on them |
674 | DeferredUpdates::doUpdates(); |
675 | |
676 | // Handle external profiler outputs. |
677 | // Any embedded profiler outputs were already processed in outputResponsePayload(). |
678 | $profiler->logData(); |
679 | |
680 | // Send metrics gathered by StatsFactory |
681 | $this->getStatsFactory()->flush(); |
682 | |
683 | self::emitBufferedStatsdData( |
684 | $this->getStatsdDataFactory(), |
685 | $this->config |
686 | ); |
687 | |
688 | // Commit and close up! |
689 | $lbFactory->commitPrimaryChanges( __METHOD__ ); |
690 | $lbFactory->shutdown( $lbFactory::SHUTDOWN_NO_CHRONPROT ); |
691 | |
692 | wfDebug( "Request ended normally" ); |
693 | } |
694 | |
695 | /** |
696 | * Send out any buffered statsd data according to sampling rules |
697 | * |
698 | * For web requests, this is called once by MediaWiki::restInPeace(), |
699 | * which is post-send (after the response is sent to the client). |
700 | * |
701 | * For maintenance scripts, especially long-running CLI scripts, it is called |
702 | * more often, to avoid OOM, since we buffer stats (T181385), based on the |
703 | * following heuristics: |
704 | * |
705 | * - Long-running scripts that involve database writes often use transactions |
706 | * to commit chunks of work. We flush from IDatabase::setTransactionListener, |
707 | * as wired up by MWLBFactory::applyGlobalState. |
708 | * |
709 | * - Long-running scripts that involve database writes but don't need any |
710 | * transactions will still periodically wait for replication to be |
711 | * graceful to the databases. We flush from ILBFactory::setWaitForReplicationListener |
712 | * as wired up by MWLBFactory::applyGlobalState. |
713 | * |
714 | * - Any other long-running scripts will probably report progress to stdout |
715 | * in some way. We also flush from Maintenance::output(). |
716 | * |
717 | * @param IBufferingStatsdDataFactory $stats |
718 | * @param Config $config |
719 | * @throws ConfigException |
720 | * @since 1.31 (formerly one the MediaWiki class) |
721 | */ |
722 | public static function emitBufferedStatsdData( |
723 | IBufferingStatsdDataFactory $stats, Config $config |
724 | ) { |
725 | if ( $config->get( MainConfigNames::StatsdServer ) && $stats->hasData() ) { |
726 | try { |
727 | $stats->updateCount( 'stats.statsdclient.buffered', $stats->getDataCount() ); |
728 | $statsdServer = explode( ':', $config->get( MainConfigNames::StatsdServer ), 2 ); |
729 | $statsdHost = $statsdServer[0]; |
730 | $statsdPort = $statsdServer[1] ?? 8125; |
731 | $statsdSender = new SocketSender( $statsdHost, $statsdPort ); |
732 | $statsdClient = new StatsdClient( $statsdSender, true, false ); |
733 | $statsdClient->send( $stats->getData() ); |
734 | } catch ( Exception $e ) { |
735 | MWExceptionHandler::logException( $e, MWExceptionHandler::CAUGHT_BY_ENTRYPOINT ); |
736 | } |
737 | } |
738 | // empty buffer for the next round |
739 | $stats->clearData(); |
740 | } |
741 | |
742 | /** |
743 | * @param int $n Number of jobs to try to run |
744 | */ |
745 | protected function triggerSyncJobs( $n ) { |
746 | $scope = Profiler::instance()->getTransactionProfiler()->silenceForScope(); |
747 | $this->getJobRunner()->run( [ 'maxJobs' => $n ] ); |
748 | ScopedCallback::consume( $scope ); |
749 | } |
750 | |
751 | /** |
752 | * @param int $n Number of jobs to try to run |
753 | * @param LoggerInterface $runJobsLogger |
754 | * @return bool Success |
755 | */ |
756 | protected function triggerAsyncJobs( $n, LoggerInterface $runJobsLogger ) { |
757 | // Do not send request if there are probably no jobs |
758 | $group = $this->getJobQueueGroupFactory()->makeJobQueueGroup(); |
759 | if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) { |
760 | return true; |
761 | } |
762 | |
763 | $query = [ 'title' => 'Special:RunJobs', |
764 | 'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ]; |
765 | $query['signature'] = SpecialRunJobs::getQuerySignature( |
766 | $query, $this->config->get( MainConfigNames::SecretKey ) ); |
767 | |
768 | $errno = $errstr = null; |
769 | $info = $this->getUrlUtils()->parse( $this->config->get( MainConfigNames::CanonicalServer ) ) ?? []; |
770 | $https = ( $info['scheme'] ?? null ) === 'https'; |
771 | $host = $info['host'] ?? null; |
772 | $port = $info['port'] ?? ( $https ? 443 : 80 ); |
773 | |
774 | AtEase::suppressWarnings(); |
775 | $sock = $host ? fsockopen( |
776 | $https ? 'tls://' . $host : $host, |
777 | $port, |
778 | $errno, |
779 | $errstr, |
780 | // If it takes more than 100ms to connect to ourselves there is a problem... |
781 | 0.100 |
782 | ) : false; |
783 | AtEase::restoreWarnings(); |
784 | |
785 | $invokedWithSuccess = true; |
786 | if ( $sock ) { |
787 | $special = $this->getSpecialPageFactory()->getPage( 'RunJobs' ); |
788 | $url = $special->getPageTitle()->getCanonicalURL( $query ); |
789 | $req = ( |
790 | "POST $url HTTP/1.1\r\n" . |
791 | "Host: $host\r\n" . |
792 | "Connection: Close\r\n" . |
793 | "Content-Length: 0\r\n\r\n" |
794 | ); |
795 | |
796 | $runJobsLogger->info( "Running $n job(s) via '$url'" ); |
797 | // Send a cron API request to be performed in the background. |
798 | // Give up if this takes too long to send (which should be rare). |
799 | stream_set_timeout( $sock, 2 ); |
800 | $bytes = fwrite( $sock, $req ); |
801 | if ( $bytes !== strlen( $req ) ) { |
802 | $invokedWithSuccess = false; |
803 | $runJobsLogger->error( "Failed to start cron API (socket write error)" ); |
804 | } else { |
805 | // Do not wait for the response (the script should handle client aborts). |
806 | // Make sure that we don't close before that script reaches ignore_user_abort(). |
807 | $start = microtime( true ); |
808 | $status = fgets( $sock ); |
809 | $sec = microtime( true ) - $start; |
810 | if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) { |
811 | $invokedWithSuccess = false; |
812 | $runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" ); |
813 | } |
814 | } |
815 | fclose( $sock ); |
816 | } else { |
817 | $invokedWithSuccess = false; |
818 | $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" ); |
819 | } |
820 | |
821 | return $invokedWithSuccess; |
822 | } |
823 | |
824 | /** |
825 | * Returns the main service container. |
826 | * |
827 | * This is intended as a stepping stone for migration. |
828 | * Ideally, individual service objects should be injected |
829 | * via the constructor. |
830 | * |
831 | * @return MediaWikiServices |
832 | */ |
833 | protected function getServiceContainer(): MediaWikiServices { |
834 | return $this->mediaWikiServices; |
835 | } |
836 | |
837 | protected function getUrlUtils(): UrlUtils { |
838 | return $this->mediaWikiServices->getUrlUtils(); |
839 | } |
840 | |
841 | protected function getReadOnlyMode(): ReadOnlyMode { |
842 | return $this->mediaWikiServices->getReadOnlyMode(); |
843 | } |
844 | |
845 | protected function getJobRunner(): JobRunner { |
846 | return $this->mediaWikiServices->getJobRunner(); |
847 | } |
848 | |
849 | protected function getDBLoadBalancerFactory(): LBFactory { |
850 | return $this->mediaWikiServices->getDBLoadBalancerFactory(); |
851 | } |
852 | |
853 | protected function getMessageCache(): MessageCache { |
854 | return $this->mediaWikiServices->getMessageCache(); |
855 | } |
856 | |
857 | protected function getBlockManager(): BlockManager { |
858 | return $this->mediaWikiServices->getBlockManager(); |
859 | } |
860 | |
861 | protected function getStatsFactory(): StatsFactory { |
862 | return $this->mediaWikiServices->getStatsFactory(); |
863 | } |
864 | |
865 | protected function getStatsdDataFactory(): IBufferingStatsdDataFactory { |
866 | return $this->mediaWikiServices->getStatsdDataFactory(); |
867 | } |
868 | |
869 | protected function getJobQueueGroupFactory(): JobQueueGroupFactory { |
870 | return $this->mediaWikiServices->getJobQueueGroupFactory(); |
871 | } |
872 | |
873 | protected function getSpecialPageFactory(): SpecialPageFactory { |
874 | return $this->mediaWikiServices->getSpecialPageFactory(); |
875 | } |
876 | |
877 | protected function getContext(): IContextSource { |
878 | return $this->context; |
879 | } |
880 | |
881 | protected function getRequest(): WebRequest { |
882 | return $this->context->getRequest(); |
883 | } |
884 | |
885 | protected function getResponse(): WebResponse { |
886 | return $this->getRequest()->response(); |
887 | } |
888 | |
889 | protected function getConfig( string $key ) { |
890 | return $this->config->get( $key ); |
891 | } |
892 | |
893 | protected function isCli(): bool { |
894 | return $this->environment->isCli(); |
895 | } |
896 | |
897 | protected function hasFastCgi(): bool { |
898 | return $this->environment->hasFastCgi(); |
899 | } |
900 | |
901 | protected function getServerInfo( string $key, $default = null ) { |
902 | return $this->environment->getServerInfo( $key, $default ); |
903 | } |
904 | |
905 | protected function print( $data ) { |
906 | if ( $this->inPostSendMode() ) { |
907 | throw new RuntimeException( 'Output already sent!' ); |
908 | } |
909 | |
910 | print $data; |
911 | } |
912 | |
913 | /** |
914 | * @param int $code |
915 | * |
916 | * @return never |
917 | */ |
918 | protected function exit( int $code = 0 ) { |
919 | $this->environment->exit( $code ); |
920 | } |
921 | |
922 | /** |
923 | * Adds a new output buffer level. |
924 | * |
925 | * @param ?callable $callback |
926 | * |
927 | * @see ob_start |
928 | */ |
929 | protected function startOutputBuffer( ?callable $callback = null ): void { |
930 | ob_start( $callback ); |
931 | } |
932 | |
933 | /** |
934 | * Returns the content of the current output buffer and clears it. |
935 | * |
936 | * @see ob_get_clean |
937 | * @return false|string |
938 | */ |
939 | protected function drainOutputBuffer() { |
940 | // NOTE: The ob_get_clean() would *disable* the current buffer, |
941 | // we don't want that! |
942 | |
943 | $contents = ob_get_contents(); |
944 | ob_clean(); |
945 | return $contents; |
946 | } |
947 | |
948 | /** |
949 | * Enable capturing of the current output buffer. |
950 | * |
951 | * There may be mutiple levels of output buffering. The level |
952 | * we are currently at, at the time of calling this method, |
953 | * is the level that will be captured to later retrieve via |
954 | * getCapturedOutput(). |
955 | * |
956 | * When capturing is active, flushOutputBuffer() will not actually |
957 | * write to the real STDOUT, but instead write only to the capture. |
958 | * |
959 | * This exists to ease testing. |
960 | * |
961 | * @internal For use in PHPUnit tests |
962 | * @see ob_start() |
963 | * @see getCapturedOutput(); |
964 | */ |
965 | public function enableOutputCapture(): void { |
966 | $level = ob_get_level(); |
967 | |
968 | if ( $level <= 0 ) { |
969 | throw new RuntimeException( |
970 | 'No capture buffer available, call ob_start first.' |
971 | ); |
972 | } |
973 | |
974 | $this->outputCaptureLevel = $level; |
975 | } |
976 | |
977 | /** |
978 | * Returns the output buffer level. |
979 | * |
980 | * If enableOutputCapture() has been called, the capture buffer |
981 | * level is taking into account by subtracting it from the actual buffer |
982 | * level. |
983 | * |
984 | * @see ob_get_level |
985 | */ |
986 | protected function getOutputBufferLevel(): int { |
987 | return max( 0, ob_get_level() - ( $this->outputCaptureLevel ?? 0 ) ); |
988 | } |
989 | |
990 | /** |
991 | * Ends the current output buffer, appending its content to the parent |
992 | * buffer. |
993 | * @see ob_end_flush |
994 | */ |
995 | protected function commitOutputBuffer(): bool { |
996 | if ( $this->inPostSendMode() ) { |
997 | throw new RuntimeException( 'Output already sent!' ); |
998 | } |
999 | |
1000 | $level = $this->getOutputBufferLevel(); |
1001 | if ( $level === 0 ) { |
1002 | return false; |
1003 | } else { |
1004 | //phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1005 | return @ob_end_flush(); |
1006 | } |
1007 | } |
1008 | |
1009 | /** |
1010 | * Stop capturing and return all output |
1011 | * |
1012 | * It flushes and drains all output buffers, but lets it go |
1013 | * to a return value instead of the real STDOUT. |
1014 | * |
1015 | * You must call enableOutputCapture() and run() before getCapturedOutput(). |
1016 | * |
1017 | * @internal For use in PHPUnit tests |
1018 | * @see enableOutputCapture(); |
1019 | * @see ob_end_clean |
1020 | * @return string HTTP response body |
1021 | */ |
1022 | public function getCapturedOutput(): string { |
1023 | if ( $this->outputCaptureLevel === null ) { |
1024 | throw new LogicException( |
1025 | 'getCapturedOutput() requires enableOutputCapture() to be called first' |
1026 | ); |
1027 | } |
1028 | |
1029 | $this->flushOutputBuffer(); |
1030 | return $this->drainOutputBuffer(); |
1031 | } |
1032 | |
1033 | /** |
1034 | * Flush buffered output to the client. |
1035 | * |
1036 | * If enableOutputCapture() was called, buffered output is committed to |
1037 | * the capture buffer instead. |
1038 | * |
1039 | * If enterPostSendMode() was called before this method, a warning is |
1040 | * triggered and any buffered output is discarded. |
1041 | * |
1042 | * @see ob_end_flush |
1043 | * @see flush |
1044 | */ |
1045 | protected function flushOutputBuffer(): void { |
1046 | // NOTE: Use a for-loop, so we don't loop indefinitely in case |
1047 | // we fail to delete a buffer. This will routinely happen for |
1048 | // PHP's zlib.compression buffer. |
1049 | // See https://www.php.net/manual/en/function.ob-end-flush.php#103387 |
1050 | $levels = $this->getOutputBufferLevel(); |
1051 | |
1052 | // If we are in post-send mode, throw away any buffered output. |
1053 | // Only complain if there actually is buffered output. |
1054 | if ( $this->inPostSendMode() ) { |
1055 | for ( $i = 0; $i < $levels; $i++ ) { |
1056 | $length = $this->getOutputBufferLength(); |
1057 | |
1058 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1059 | @ob_end_clean(); |
1060 | |
1061 | if ( $length > 0 ) { |
1062 | $this->triggerError( |
1063 | __METHOD__ . ": suppressed $length byte(s)", |
1064 | E_USER_NOTICE |
1065 | ); |
1066 | } |
1067 | } |
1068 | return; |
1069 | } |
1070 | |
1071 | for ( $i = 0; $i < $levels; $i++ ) { |
1072 | // Note that ob_end_flush() will fail for buffers created without |
1073 | // the PHP_OUTPUT_HANDLER_FLUSHABLE flag. So we use a for-loop |
1074 | // to avoid looping forever when ob_get_level() won't go down. |
1075 | |
1076 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1077 | @ob_end_flush(); |
1078 | } |
1079 | |
1080 | // Flush the system buffer so the response is actually sent to the client, |
1081 | // unless we intend to capture the output, for testing or otherwise. |
1082 | // Capturing would be enabled by $this->outputCaptureLevel being set. |
1083 | // Note that, when not capturing the output, we want to flush response |
1084 | // to the client even if the loop above did not result in ob_get_level() |
1085 | // to return 0. This would be the case e.g. when zlib.compression |
1086 | // is enabled. |
1087 | // See https://www.php.net/manual/en/function.ob-end-flush.php#103387 |
1088 | if ( $this->outputCaptureLevel === null || ob_get_level() === 0 ) { |
1089 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1090 | @flush(); |
1091 | } |
1092 | wfDebug( "Output buffer flushed" ); |
1093 | } |
1094 | |
1095 | /** |
1096 | * Discards all buffered output, down to the capture buffer level. |
1097 | */ |
1098 | protected function discardAllOutput() { |
1099 | // NOTE: use a for-loop, in case one of the buffers is non-removable. |
1100 | // In that case, getOutputBufferLevel() will never return 0. |
1101 | $levels = $this->getOutputBufferLevel(); |
1102 | for ( $i = 0; $i < $levels; $i++ ) { |
1103 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1104 | @ob_end_clean(); |
1105 | } |
1106 | } |
1107 | |
1108 | /** |
1109 | * @see ob_get_length |
1110 | * @return false|int |
1111 | */ |
1112 | protected function getOutputBufferLength() { |
1113 | return ob_get_length(); |
1114 | } |
1115 | |
1116 | /** |
1117 | * @see ob_get_status |
1118 | */ |
1119 | protected function getOutputBufferStatus(): array { |
1120 | return ob_get_status(); |
1121 | } |
1122 | |
1123 | /** |
1124 | * @see ob_end_clean |
1125 | */ |
1126 | protected function discardOutputBuffer(): bool { |
1127 | return ob_end_clean(); |
1128 | } |
1129 | |
1130 | protected function disableModDeflate(): void { |
1131 | $this->environment->disableModDeflate(); |
1132 | } |
1133 | |
1134 | /** |
1135 | * @see http_response_code |
1136 | * @return int|bool |
1137 | */ |
1138 | protected function getStatusCode() { |
1139 | return $this->getResponse()->getStatusCode(); |
1140 | } |
1141 | |
1142 | /** |
1143 | * Whether enterPostSendMode() has been called. |
1144 | * Indicates whether more data can be sent to the client. |
1145 | * To determine whether more headers can be sent, use |
1146 | * $this->getResponse()->headersSent(). |
1147 | */ |
1148 | protected function inPostSendMode(): bool { |
1149 | return $this->postSendMode; |
1150 | } |
1151 | |
1152 | /** |
1153 | * Triggers a PHP runtime error |
1154 | * |
1155 | * @see trigger_error |
1156 | */ |
1157 | protected function triggerError( string $message, int $level = E_USER_NOTICE ): bool { |
1158 | return $this->environment->triggerError( $message, $level ); |
1159 | } |
1160 | |
1161 | /** |
1162 | * Returns the value of an environment variable. |
1163 | * |
1164 | * @see getenv |
1165 | * |
1166 | * @param string $name |
1167 | * |
1168 | * @return array|false|string |
1169 | */ |
1170 | protected function getEnv( string $name ) { |
1171 | return $this->environment->getEnv( $name ); |
1172 | } |
1173 | |
1174 | /** |
1175 | * Returns the value of an ini option. |
1176 | * |
1177 | * @see ini_get |
1178 | * |
1179 | * @param string $name |
1180 | * |
1181 | * @return false|string |
1182 | */ |
1183 | protected function getIni( string $name ) { |
1184 | return $this->environment->getIni( $name ); |
1185 | } |
1186 | |
1187 | /** |
1188 | * @param string $name |
1189 | * @param mixed $value |
1190 | * |
1191 | * @return false|string |
1192 | */ |
1193 | protected function setIniOption( string $name, $value ) { |
1194 | return $this->environment->setIniOption( $name, $value ); |
1195 | } |
1196 | |
1197 | /** |
1198 | * @see header() function |
1199 | */ |
1200 | protected function header( string $header, bool $replace = true, int $status = 0 ): void { |
1201 | $this->getResponse()->header( $header, $replace, $status ); |
1202 | } |
1203 | |
1204 | /** |
1205 | * @see HttpStatus |
1206 | */ |
1207 | protected function status( int $code ): void { |
1208 | $this->header( HttpStatus::getHeader( $code ), true, $code ); |
1209 | } |
1210 | |
1211 | /** |
1212 | * Calls fastcgi_finish_request if possible. Reasons for not calling |
1213 | * fastcgi_finish_request include the fastcgi extension not being loaded |
1214 | * and the capture buffer level being different from 0. |
1215 | * |
1216 | * @see fastcgi_finish_request |
1217 | * @return bool true if fastcgi_finish_request was called and successful. |
1218 | */ |
1219 | protected function fastCgiFinishRequest(): bool { |
1220 | if ( !$this->inPostSendMode() ) { |
1221 | $this->flushOutputBuffer(); |
1222 | } |
1223 | |
1224 | // Don't mess with fastcgi on CLI mode. |
1225 | if ( $this->isCli() ) { |
1226 | return false; |
1227 | } |
1228 | |
1229 | // Only mess with fastcgi if we really have no buffers left. |
1230 | if ( ob_get_level() > 0 ) { |
1231 | return false; |
1232 | } |
1233 | |
1234 | $success = $this->environment->fastCgiFinishRequest(); |
1235 | wfDebug( $success ? 'FastCGI request finished' : 'FastCGI request finish failed' ); |
1236 | return $success; |
1237 | } |
1238 | |
1239 | /** |
1240 | * Returns the current request's path and query string (not a full URL), |
1241 | * like PHP's built-in $_SERVER['REQUEST_URI']. |
1242 | * |
1243 | * @see WebRequest::getRequestURL() |
1244 | * @see WebRequest::getGlobalRequestURL() |
1245 | */ |
1246 | protected function getRequestURL(): string { |
1247 | // Despite the name, this just returns the path and query string |
1248 | return $this->getRequest()->getRequestURL(); |
1249 | } |
1250 | |
1251 | /** |
1252 | * Disables all output to the client. |
1253 | * After this, calling any output methods on this object will fail. |
1254 | */ |
1255 | protected function enterPostSendMode() { |
1256 | $this->postSendMode = true; |
1257 | |
1258 | $this->getResponse()->disableForPostSend(); |
1259 | } |
1260 | |
1261 | } |