Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
10.88% |
41 / 377 |
|
16.13% |
10 / 62 |
CRAP | |
0.00% |
0 / 1 |
MediaWikiEntryPoint | |
10.88% |
41 / 377 |
|
16.13% |
10 / 62 |
15029.35 | |
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 / 25 |
|
0.00% |
0 / 1 |
20 | |||
emitBufferedStats | |
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.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 | use Wikimedia\Telemetry\SpanInterface; |
59 | use Wikimedia\Telemetry\TracerState; |
60 | |
61 | /** |
62 | * @defgroup entrypoint Entry points |
63 | * |
64 | * Web entry points reside in top-level MediaWiki directory (i.e. installation path). |
65 | * These entry points handle web requests to interact with the wiki. Other PHP files |
66 | * in the repository are not accessed directly from the web, but instead included by |
67 | * an entry point. |
68 | */ |
69 | |
70 | /** |
71 | * Base class for entry point handlers. |
72 | * |
73 | * @note: This is not stable to extend by extensions, because MediaWiki does not |
74 | * allow extensions to define new entry points. |
75 | * |
76 | * @ingroup entrypoint |
77 | * @since 1.42, factored out of the previously existing MediaWiki class. |
78 | */ |
79 | abstract class MediaWikiEntryPoint { |
80 | use ProtectedHookAccessorTrait; |
81 | |
82 | private IContextSource $context; |
83 | private Config $config; |
84 | private ?int $outputCaptureLevel = null; |
85 | |
86 | private bool $postSendMode = false; |
87 | |
88 | /** @var int Class DEFER_* constant; how non-blocking post-response tasks should run */ |
89 | private int $postSendStrategy; |
90 | |
91 | /** Call fastcgi_finish_request() to make post-send updates async */ |
92 | private const DEFER_FASTCGI_FINISH_REQUEST = 1; |
93 | |
94 | /** Set Content-Length and call ob_end_flush()/flush() to make post-send updates async */ |
95 | private const DEFER_SET_LENGTH_AND_FLUSH = 2; |
96 | |
97 | /** Do not try to make post-send updates async (e.g. for CLI mode) */ |
98 | private const DEFER_CLI_MODE = 3; |
99 | |
100 | private bool $preparedForOutput = false; |
101 | |
102 | protected EntryPointEnvironment $environment; |
103 | |
104 | private MediaWikiServices $mediaWikiServices; |
105 | |
106 | /** |
107 | * @param IContextSource $context |
108 | * @param EntryPointEnvironment $environment |
109 | * @param MediaWikiServices $mediaWikiServices |
110 | */ |
111 | public function __construct( |
112 | IContextSource $context, |
113 | EntryPointEnvironment $environment, |
114 | MediaWikiServices $mediaWikiServices |
115 | ) { |
116 | $this->context = $context; |
117 | $this->config = $this->context->getConfig(); |
118 | $this->environment = $environment; |
119 | $this->mediaWikiServices = $mediaWikiServices; |
120 | |
121 | if ( $environment->isCli() ) { |
122 | $this->postSendStrategy = self::DEFER_CLI_MODE; |
123 | } elseif ( $environment->hasFastCgi() ) { |
124 | $this->postSendStrategy = self::DEFER_FASTCGI_FINISH_REQUEST; |
125 | } else { |
126 | $this->postSendStrategy = self::DEFER_SET_LENGTH_AND_FLUSH; |
127 | } |
128 | } |
129 | |
130 | /** |
131 | * Perform any setup needed before execute() is called. |
132 | * Final wrapper function for setup(). |
133 | * Will be called by doRun(). |
134 | */ |
135 | final protected function setup() { |
136 | // Much of the functionality in WebStart.php and Setup.php should be moved here eventually. |
137 | // As of MW 1.41, a lot of it still wants to run in file scope. |
138 | |
139 | // TODO: move define( 'MW_ENTRY_POINT' here ) |
140 | // TODO: move ProfilingContext::singleton()->init( ... ) here. |
141 | |
142 | $this->doSetup(); |
143 | } |
144 | |
145 | /** |
146 | * Perform any setup needed before execute() is called. |
147 | * Called by doRun() via doSetup(). |
148 | */ |
149 | protected function doSetup() { |
150 | // no-op |
151 | // TODO: move ob_start( [ MediaWiki\Output\OutputHandler::class, 'handle' ] ) here |
152 | // TODO: move MW_NO_OUTPUT_COMPRESSION handling here. |
153 | // TODO: move HeaderCallback::register() here |
154 | // TODO: move SessionManager::getGlobalSession() here (from Setup.php) |
155 | // TODO: move AuthManager::autoCreateUser here (from Setup.php) |
156 | // TODO: move pingback here (from Setup.php) |
157 | } |
158 | |
159 | /** |
160 | * Prepare for sending the output. Should be called by entry points before |
161 | * sending the response. |
162 | * Final wrapper function for doPrepareForOutput(). |
163 | * Will be called automatically at the end of doRun(), but will do nothing if it was |
164 | * already called from execute(). |
165 | */ |
166 | final protected function prepareForOutput() { |
167 | if ( $this->preparedForOutput ) { |
168 | // only do this once. |
169 | return; |
170 | } |
171 | |
172 | $this->preparedForOutput = true; |
173 | |
174 | $this->doPrepareForOutput(); |
175 | } |
176 | |
177 | /** |
178 | * Prepare for sending the output. Should be called by entry points before |
179 | * sending the response. |
180 | * Will be called automatically by run() via prepareForOutput(). |
181 | * Subclasses may override this method, but should not call it directly. |
182 | * |
183 | * @note arc-lamp profiling relies on the name of this method, |
184 | * it's hard coded in the arclamp-generate-svgs script! |
185 | */ |
186 | protected function doPrepareForOutput() { |
187 | // Commit any changes in the current transaction round so that: |
188 | // a) the transaction is not rolled back after success output was already sent |
189 | // b) error output is not jumbled together with success output in the response |
190 | // TODO: split this up and pull out stuff like spreading cookie blocks |
191 | $this->commitMainTransaction(); |
192 | } |
193 | |
194 | /** |
195 | * Main app life cycle: Calls doSetup(), execute(), |
196 | * prepareForOutput(), and postOutputShutdown(). |
197 | */ |
198 | final public function run() { |
199 | $this->setup(); |
200 | |
201 | try { |
202 | $this->execute(); |
203 | |
204 | // Prepare for flushing the output. Will do nothing if it was already called by execute(). |
205 | $this->prepareForOutput(); |
206 | } catch ( Throwable $e ) { |
207 | $this->status( 500 ); |
208 | $this->handleTopLevelError( $e ); |
209 | } |
210 | |
211 | $this->postOutputShutdown(); |
212 | } |
213 | |
214 | /** |
215 | * Report a top level error. |
216 | * Subclasses in core may override this to handle errors according |
217 | * to the expected output format. |
218 | * This method is not safe to override for extensions. |
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 | self::emitBufferedStats( |
681 | $this->getStatsFactory(), |
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 | // End the root span of this request or process and export trace data. |
691 | $isServerError = $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; |
692 | // This is too generic a place to determine if the request was truly successful. |
693 | // Err on the side of unset. |
694 | $spanStatus = $isServerError ? SpanInterface::SPAN_STATUS_ERROR : SpanInterface::SPAN_STATUS_UNSET; |
695 | TracerState::getInstance()->endRootSpan( $spanStatus ); |
696 | $this->mediaWikiServices->getTracer()->shutdown(); |
697 | |
698 | wfDebug( "Request ended normally" ); |
699 | } |
700 | |
701 | /** |
702 | * Send out any buffered stats according to sampling rules |
703 | * |
704 | * For web requests, this is called once by MediaWiki::restInPeace(), |
705 | * which is post-send (after the response is sent to the client). |
706 | * |
707 | * For maintenance scripts, especially long-running CLI scripts, it is called |
708 | * more often, to avoid OOM, since we buffer stats (T181385), based on the |
709 | * following heuristics: |
710 | * |
711 | * - Long-running scripts that involve database writes often use transactions |
712 | * to commit chunks of work. We flush from IDatabase::setTransactionListener, |
713 | * as wired up by MWLBFactory::applyGlobalState. |
714 | * |
715 | * - Long-running scripts that involve database writes but don't need any |
716 | * transactions will still periodically wait for replication to be |
717 | * graceful to the databases. We flush from ILBFactory::setWaitForReplicationListener |
718 | * as wired up by MWLBFactory::applyGlobalState. |
719 | * |
720 | * - Any other long-running scripts will probably report progress to stdout |
721 | * in some way. We also flush from Maintenance::output(). |
722 | * |
723 | * @param StatsFactory $statsFactory |
724 | * @param IBufferingStatsdDataFactory $stats |
725 | * @param Config $config |
726 | * @throws ConfigException |
727 | * @since 1.31 (formerly one the MediaWiki class) |
728 | */ |
729 | public static function emitBufferedStats( |
730 | StatsFactory $statsFactory, |
731 | IBufferingStatsdDataFactory $stats, |
732 | Config $config |
733 | ) { |
734 | // Send metrics gathered by StatsFactory |
735 | $statsFactory->flush(); |
736 | |
737 | if ( $config->get( MainConfigNames::StatsdServer ) && $stats->hasData() ) { |
738 | try { |
739 | $stats->updateCount( 'stats.statsdclient.buffered', $stats->getDataCount() ); |
740 | $statsdServer = explode( ':', $config->get( MainConfigNames::StatsdServer ), 2 ); |
741 | $statsdHost = $statsdServer[0]; |
742 | $statsdPort = $statsdServer[1] ?? 8125; |
743 | $statsdSender = new SocketSender( $statsdHost, $statsdPort ); |
744 | $statsdClient = new StatsdClient( $statsdSender, true, false ); |
745 | $statsdClient->send( $stats->getData() ); |
746 | } catch ( Exception $e ) { |
747 | MWExceptionHandler::logException( $e, MWExceptionHandler::CAUGHT_BY_ENTRYPOINT ); |
748 | } |
749 | } |
750 | // empty buffer for the next round |
751 | $stats->clearData(); |
752 | } |
753 | |
754 | /** |
755 | * @param int $n Number of jobs to try to run |
756 | */ |
757 | protected function triggerSyncJobs( $n ) { |
758 | $scope = Profiler::instance()->getTransactionProfiler()->silenceForScope(); |
759 | $this->getJobRunner()->run( [ 'maxJobs' => $n ] ); |
760 | ScopedCallback::consume( $scope ); |
761 | } |
762 | |
763 | /** |
764 | * @param int $n Number of jobs to try to run |
765 | * @param LoggerInterface $runJobsLogger |
766 | * @return bool Success |
767 | */ |
768 | protected function triggerAsyncJobs( $n, LoggerInterface $runJobsLogger ) { |
769 | // Do not send request if there are probably no jobs |
770 | $group = $this->getJobQueueGroupFactory()->makeJobQueueGroup(); |
771 | if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) { |
772 | return true; |
773 | } |
774 | |
775 | $query = [ 'title' => 'Special:RunJobs', |
776 | 'tasks' => 'jobs', 'maxjobs' => $n, 'sigexpiry' => time() + 5 ]; |
777 | $query['signature'] = SpecialRunJobs::getQuerySignature( |
778 | $query, $this->config->get( MainConfigNames::SecretKey ) ); |
779 | |
780 | $errno = $errstr = null; |
781 | $info = $this->getUrlUtils()->parse( $this->config->get( MainConfigNames::CanonicalServer ) ) ?? []; |
782 | $https = ( $info['scheme'] ?? null ) === 'https'; |
783 | $host = $info['host'] ?? null; |
784 | $port = $info['port'] ?? ( $https ? 443 : 80 ); |
785 | |
786 | AtEase::suppressWarnings(); |
787 | $sock = $host ? fsockopen( |
788 | $https ? 'tls://' . $host : $host, |
789 | $port, |
790 | $errno, |
791 | $errstr, |
792 | // If it takes more than 100ms to connect to ourselves there is a problem... |
793 | 0.100 |
794 | ) : false; |
795 | AtEase::restoreWarnings(); |
796 | |
797 | $invokedWithSuccess = true; |
798 | if ( $sock ) { |
799 | $special = $this->getSpecialPageFactory()->getPage( 'RunJobs' ); |
800 | $url = $special->getPageTitle()->getCanonicalURL( $query ); |
801 | $req = ( |
802 | "POST $url HTTP/1.1\r\n" . |
803 | "Host: $host\r\n" . |
804 | "Connection: Close\r\n" . |
805 | "Content-Length: 0\r\n\r\n" |
806 | ); |
807 | |
808 | $runJobsLogger->info( "Running $n job(s) via '$url'" ); |
809 | // Send a cron API request to be performed in the background. |
810 | // Give up if this takes too long to send (which should be rare). |
811 | stream_set_timeout( $sock, 2 ); |
812 | $bytes = fwrite( $sock, $req ); |
813 | if ( $bytes !== strlen( $req ) ) { |
814 | $invokedWithSuccess = false; |
815 | $runJobsLogger->error( "Failed to start cron API (socket write error)" ); |
816 | } else { |
817 | // Do not wait for the response (the script should handle client aborts). |
818 | // Make sure that we don't close before that script reaches ignore_user_abort(). |
819 | $start = microtime( true ); |
820 | $status = fgets( $sock ); |
821 | $sec = microtime( true ) - $start; |
822 | if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) { |
823 | $invokedWithSuccess = false; |
824 | $runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" ); |
825 | } |
826 | } |
827 | fclose( $sock ); |
828 | } else { |
829 | $invokedWithSuccess = false; |
830 | $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" ); |
831 | } |
832 | |
833 | return $invokedWithSuccess; |
834 | } |
835 | |
836 | /** |
837 | * Returns the main service container. |
838 | * |
839 | * This is intended as a stepping stone for migration. |
840 | * Ideally, individual service objects should be injected |
841 | * via the constructor. |
842 | */ |
843 | protected function getServiceContainer(): MediaWikiServices { |
844 | return $this->mediaWikiServices; |
845 | } |
846 | |
847 | protected function getUrlUtils(): UrlUtils { |
848 | return $this->mediaWikiServices->getUrlUtils(); |
849 | } |
850 | |
851 | protected function getReadOnlyMode(): ReadOnlyMode { |
852 | return $this->mediaWikiServices->getReadOnlyMode(); |
853 | } |
854 | |
855 | protected function getJobRunner(): JobRunner { |
856 | return $this->mediaWikiServices->getJobRunner(); |
857 | } |
858 | |
859 | protected function getDBLoadBalancerFactory(): LBFactory { |
860 | return $this->mediaWikiServices->getDBLoadBalancerFactory(); |
861 | } |
862 | |
863 | protected function getMessageCache(): MessageCache { |
864 | return $this->mediaWikiServices->getMessageCache(); |
865 | } |
866 | |
867 | protected function getBlockManager(): BlockManager { |
868 | return $this->mediaWikiServices->getBlockManager(); |
869 | } |
870 | |
871 | protected function getStatsFactory(): StatsFactory { |
872 | return $this->mediaWikiServices->getStatsFactory(); |
873 | } |
874 | |
875 | protected function getStatsdDataFactory(): IBufferingStatsdDataFactory { |
876 | return $this->mediaWikiServices->getStatsdDataFactory(); |
877 | } |
878 | |
879 | protected function getJobQueueGroupFactory(): JobQueueGroupFactory { |
880 | return $this->mediaWikiServices->getJobQueueGroupFactory(); |
881 | } |
882 | |
883 | protected function getSpecialPageFactory(): SpecialPageFactory { |
884 | return $this->mediaWikiServices->getSpecialPageFactory(); |
885 | } |
886 | |
887 | protected function getContext(): IContextSource { |
888 | return $this->context; |
889 | } |
890 | |
891 | protected function getRequest(): WebRequest { |
892 | return $this->context->getRequest(); |
893 | } |
894 | |
895 | protected function getResponse(): WebResponse { |
896 | return $this->getRequest()->response(); |
897 | } |
898 | |
899 | protected function getConfig( string $key ) { |
900 | return $this->config->get( $key ); |
901 | } |
902 | |
903 | protected function isCli(): bool { |
904 | return $this->environment->isCli(); |
905 | } |
906 | |
907 | protected function hasFastCgi(): bool { |
908 | return $this->environment->hasFastCgi(); |
909 | } |
910 | |
911 | protected function getServerInfo( string $key, $default = null ) { |
912 | return $this->environment->getServerInfo( $key, $default ); |
913 | } |
914 | |
915 | protected function print( $data ) { |
916 | if ( $this->inPostSendMode() ) { |
917 | throw new RuntimeException( 'Output already sent!' ); |
918 | } |
919 | |
920 | print $data; |
921 | } |
922 | |
923 | /** |
924 | * @param int $code |
925 | * |
926 | * @return never |
927 | */ |
928 | protected function exit( int $code = 0 ) { |
929 | $this->environment->exit( $code ); |
930 | } |
931 | |
932 | /** |
933 | * Adds a new output buffer level. |
934 | * |
935 | * @param ?callable $callback |
936 | * |
937 | * @see ob_start |
938 | */ |
939 | protected function startOutputBuffer( ?callable $callback = null ): void { |
940 | ob_start( $callback ); |
941 | } |
942 | |
943 | /** |
944 | * Returns the content of the current output buffer and clears it. |
945 | * |
946 | * @see ob_get_clean |
947 | * @return false|string |
948 | */ |
949 | protected function drainOutputBuffer() { |
950 | // NOTE: The ob_get_clean() would *disable* the current buffer, |
951 | // we don't want that! |
952 | |
953 | $contents = ob_get_contents(); |
954 | ob_clean(); |
955 | return $contents; |
956 | } |
957 | |
958 | /** |
959 | * Enable capturing of the current output buffer. |
960 | * |
961 | * There may be mutiple levels of output buffering. The level |
962 | * we are currently at, at the time of calling this method, |
963 | * is the level that will be captured to later retrieve via |
964 | * getCapturedOutput(). |
965 | * |
966 | * When capturing is active, flushOutputBuffer() will not actually |
967 | * write to the real STDOUT, but instead write only to the capture. |
968 | * |
969 | * This exists to ease testing. |
970 | * |
971 | * @internal For use in PHPUnit tests |
972 | * @see ob_start() |
973 | * @see getCapturedOutput(); |
974 | */ |
975 | public function enableOutputCapture(): void { |
976 | $level = ob_get_level(); |
977 | |
978 | if ( $level <= 0 ) { |
979 | throw new RuntimeException( |
980 | 'No capture buffer available, call ob_start first.' |
981 | ); |
982 | } |
983 | |
984 | $this->outputCaptureLevel = $level; |
985 | } |
986 | |
987 | /** |
988 | * Returns the output buffer level. |
989 | * |
990 | * If enableOutputCapture() has been called, the capture buffer |
991 | * level is taking into account by subtracting it from the actual buffer |
992 | * level. |
993 | * |
994 | * @see ob_get_level |
995 | */ |
996 | protected function getOutputBufferLevel(): int { |
997 | return max( 0, ob_get_level() - ( $this->outputCaptureLevel ?? 0 ) ); |
998 | } |
999 | |
1000 | /** |
1001 | * Ends the current output buffer, appending its content to the parent |
1002 | * buffer. |
1003 | * @see ob_end_flush |
1004 | */ |
1005 | protected function commitOutputBuffer(): bool { |
1006 | if ( $this->inPostSendMode() ) { |
1007 | throw new RuntimeException( 'Output already sent!' ); |
1008 | } |
1009 | |
1010 | $level = $this->getOutputBufferLevel(); |
1011 | if ( $level === 0 ) { |
1012 | return false; |
1013 | } else { |
1014 | //phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1015 | return @ob_end_flush(); |
1016 | } |
1017 | } |
1018 | |
1019 | /** |
1020 | * Stop capturing and return all output |
1021 | * |
1022 | * It flushes and drains all output buffers, but lets it go |
1023 | * to a return value instead of the real STDOUT. |
1024 | * |
1025 | * You must call enableOutputCapture() and run() before getCapturedOutput(). |
1026 | * |
1027 | * @internal For use in PHPUnit tests |
1028 | * @see enableOutputCapture(); |
1029 | * @see ob_end_clean |
1030 | * @return string HTTP response body |
1031 | */ |
1032 | public function getCapturedOutput(): string { |
1033 | if ( $this->outputCaptureLevel === null ) { |
1034 | throw new LogicException( |
1035 | 'getCapturedOutput() requires enableOutputCapture() to be called first' |
1036 | ); |
1037 | } |
1038 | |
1039 | $this->flushOutputBuffer(); |
1040 | return $this->drainOutputBuffer(); |
1041 | } |
1042 | |
1043 | /** |
1044 | * Flush buffered output to the client. |
1045 | * |
1046 | * If enableOutputCapture() was called, buffered output is committed to |
1047 | * the capture buffer instead. |
1048 | * |
1049 | * If enterPostSendMode() was called before this method, a warning is |
1050 | * triggered and any buffered output is discarded. |
1051 | * |
1052 | * @see ob_end_flush |
1053 | * @see flush |
1054 | */ |
1055 | protected function flushOutputBuffer(): void { |
1056 | // NOTE: Use a for-loop, so we don't loop indefinitely in case |
1057 | // we fail to delete a buffer. This will routinely happen for |
1058 | // PHP's zlib.compression buffer. |
1059 | // See https://www.php.net/manual/en/function.ob-end-flush.php#103387 |
1060 | $levels = $this->getOutputBufferLevel(); |
1061 | |
1062 | // If we are in post-send mode, throw away any buffered output. |
1063 | // Only complain if there actually is buffered output. |
1064 | if ( $this->inPostSendMode() ) { |
1065 | for ( $i = 0; $i < $levels; $i++ ) { |
1066 | $length = $this->getOutputBufferLength(); |
1067 | |
1068 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1069 | @ob_end_clean(); |
1070 | |
1071 | if ( $length > 0 ) { |
1072 | $this->triggerError( |
1073 | __METHOD__ . ": suppressed $length byte(s)", |
1074 | E_USER_NOTICE |
1075 | ); |
1076 | } |
1077 | } |
1078 | return; |
1079 | } |
1080 | |
1081 | for ( $i = 0; $i < $levels; $i++ ) { |
1082 | // Note that ob_end_flush() will fail for buffers created without |
1083 | // the PHP_OUTPUT_HANDLER_FLUSHABLE flag. So we use a for-loop |
1084 | // to avoid looping forever when ob_get_level() won't go down. |
1085 | |
1086 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1087 | @ob_end_flush(); |
1088 | } |
1089 | |
1090 | // Flush the system buffer so the response is actually sent to the client, |
1091 | // unless we intend to capture the output, for testing or otherwise. |
1092 | // Capturing would be enabled by $this->outputCaptureLevel being set. |
1093 | // Note that, when not capturing the output, we want to flush response |
1094 | // to the client even if the loop above did not result in ob_get_level() |
1095 | // to return 0. This would be the case e.g. when zlib.compression |
1096 | // is enabled. |
1097 | // See https://www.php.net/manual/en/function.ob-end-flush.php#103387 |
1098 | if ( $this->outputCaptureLevel === null || ob_get_level() === 0 ) { |
1099 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1100 | @flush(); |
1101 | } |
1102 | wfDebug( "Output buffer flushed" ); |
1103 | } |
1104 | |
1105 | /** |
1106 | * Discards all buffered output, down to the capture buffer level. |
1107 | */ |
1108 | protected function discardAllOutput() { |
1109 | // NOTE: use a for-loop, in case one of the buffers is non-removable. |
1110 | // In that case, getOutputBufferLevel() will never return 0. |
1111 | $levels = $this->getOutputBufferLevel(); |
1112 | for ( $i = 0; $i < $levels; $i++ ) { |
1113 | // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged |
1114 | @ob_end_clean(); |
1115 | } |
1116 | } |
1117 | |
1118 | /** |
1119 | * @see ob_get_length |
1120 | * @return false|int |
1121 | */ |
1122 | protected function getOutputBufferLength() { |
1123 | return ob_get_length(); |
1124 | } |
1125 | |
1126 | /** |
1127 | * @see ob_get_status |
1128 | */ |
1129 | protected function getOutputBufferStatus(): array { |
1130 | return ob_get_status(); |
1131 | } |
1132 | |
1133 | /** |
1134 | * @see ob_end_clean |
1135 | */ |
1136 | protected function discardOutputBuffer(): bool { |
1137 | return ob_end_clean(); |
1138 | } |
1139 | |
1140 | protected function disableModDeflate(): void { |
1141 | $this->environment->disableModDeflate(); |
1142 | } |
1143 | |
1144 | /** |
1145 | * @see http_response_code |
1146 | * @return int|bool |
1147 | */ |
1148 | protected function getStatusCode() { |
1149 | return $this->getResponse()->getStatusCode(); |
1150 | } |
1151 | |
1152 | /** |
1153 | * Whether enterPostSendMode() has been called. |
1154 | * Indicates whether more data can be sent to the client. |
1155 | * To determine whether more headers can be sent, use |
1156 | * $this->getResponse()->headersSent(). |
1157 | */ |
1158 | protected function inPostSendMode(): bool { |
1159 | return $this->postSendMode; |
1160 | } |
1161 | |
1162 | /** |
1163 | * Triggers a PHP runtime error |
1164 | * |
1165 | * @see trigger_error |
1166 | */ |
1167 | protected function triggerError( string $message, int $level = E_USER_NOTICE ): bool { |
1168 | return $this->environment->triggerError( $message, $level ); |
1169 | } |
1170 | |
1171 | /** |
1172 | * Returns the value of an environment variable. |
1173 | * |
1174 | * @see getenv |
1175 | * |
1176 | * @param string $name |
1177 | * |
1178 | * @return array|false|string |
1179 | */ |
1180 | protected function getEnv( string $name ) { |
1181 | return $this->environment->getEnv( $name ); |
1182 | } |
1183 | |
1184 | /** |
1185 | * Returns the value of an ini option. |
1186 | * |
1187 | * @see ini_get |
1188 | * |
1189 | * @param string $name |
1190 | * |
1191 | * @return false|string |
1192 | */ |
1193 | protected function getIni( string $name ) { |
1194 | return $this->environment->getIni( $name ); |
1195 | } |
1196 | |
1197 | /** |
1198 | * @param string $name |
1199 | * @param mixed $value |
1200 | * |
1201 | * @return false|string |
1202 | */ |
1203 | protected function setIniOption( string $name, $value ) { |
1204 | return $this->environment->setIniOption( $name, $value ); |
1205 | } |
1206 | |
1207 | /** |
1208 | * @see header() function |
1209 | */ |
1210 | protected function header( string $header, bool $replace = true, int $status = 0 ): void { |
1211 | $this->getResponse()->header( $header, $replace, $status ); |
1212 | } |
1213 | |
1214 | /** |
1215 | * @see HttpStatus |
1216 | */ |
1217 | protected function status( int $code ): void { |
1218 | $this->header( HttpStatus::getHeader( $code ), true, $code ); |
1219 | } |
1220 | |
1221 | /** |
1222 | * Calls fastcgi_finish_request if possible. Reasons for not calling |
1223 | * fastcgi_finish_request include the fastcgi extension not being loaded |
1224 | * and the capture buffer level being different from 0. |
1225 | * |
1226 | * @see fastcgi_finish_request |
1227 | * @return bool true if fastcgi_finish_request was called and successful. |
1228 | */ |
1229 | protected function fastCgiFinishRequest(): bool { |
1230 | if ( !$this->inPostSendMode() ) { |
1231 | $this->flushOutputBuffer(); |
1232 | } |
1233 | |
1234 | // Don't mess with fastcgi on CLI mode. |
1235 | if ( $this->isCli() ) { |
1236 | return false; |
1237 | } |
1238 | |
1239 | // Only mess with fastcgi if we really have no buffers left. |
1240 | if ( ob_get_level() > 0 ) { |
1241 | return false; |
1242 | } |
1243 | |
1244 | $success = $this->environment->fastCgiFinishRequest(); |
1245 | wfDebug( $success ? 'FastCGI request finished' : 'FastCGI request finish failed' ); |
1246 | return $success; |
1247 | } |
1248 | |
1249 | /** |
1250 | * Returns the current request's path and query string (not a full URL), |
1251 | * like PHP's built-in $_SERVER['REQUEST_URI']. |
1252 | * |
1253 | * @see WebRequest::getRequestURL() |
1254 | * @see WebRequest::getGlobalRequestURL() |
1255 | */ |
1256 | protected function getRequestURL(): string { |
1257 | // Despite the name, this just returns the path and query string |
1258 | return $this->getRequest()->getRequestURL(); |
1259 | } |
1260 | |
1261 | /** |
1262 | * Disables all output to the client. |
1263 | * After this, calling any output methods on this object will fail. |
1264 | */ |
1265 | protected function enterPostSendMode() { |
1266 | $this->postSendMode = true; |
1267 | |
1268 | $this->getResponse()->disableForPostSend(); |
1269 | } |
1270 | |
1271 | } |