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