PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.2.0
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.2.0
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / CliMulti.php
matomo / app / core Last commit date
API 1 year ago Access 1 year ago Application 1 year ago Archive 1 year ago ArchiveProcessor 1 year ago Archiver 2 years ago AssetManager 1 year ago Auth 1 year ago Category 2 years ago Changes 1 year ago CliMulti 1 year ago Columns 1 year ago Concurrency 1 year ago Config 1 year ago Container 1 year ago CronArchive 1 year ago DataAccess 1 year ago DataFiles 2 years ago DataTable 1 year ago Db 1 year ago DeviceDetector 1 year ago Email 2 years ago Exception 1 year ago Http 1 year ago Intl 1 year ago Log 2 years ago Mail 1 year ago Measurable 1 year ago Menu 1 year ago Metrics 1 year ago Notification 1 year ago Period 1 year ago Plugin 1 year ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 1 year ago Scheduler 1 year ago Segment 1 year ago Session 1 year ago Settings 1 year ago Tracker 1 year ago Translation 1 year ago Twig 1 year ago UpdateCheck 1 year ago Updater 1 year ago Updates 1 year ago Validators 1 year ago View 1 year ago ViewDataTable 1 year ago Visualization 1 year ago Widget 1 year ago .htaccess 2 years ago Access.php 1 year ago Archive.php 1 year ago ArchiveProcessor.php 1 year ago AssetManager.php 1 year ago Auth.php 2 years ago AuthResult.php 2 years ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 1 year ago CliMulti.php 1 year ago Common.php 1 year ago Config.php 1 year ago Console.php 1 year ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 1 year ago DI.php 1 year ago DataArray.php 1 year ago DataTable.php 1 year ago Date.php 1 year ago Db.php 1 year ago DbHelper.php 1 year ago Development.php 1 year ago ErrorHandler.php 1 year ago EventDispatcher.php 1 year ago ExceptionHandler.php 1 year ago FileIntegrity.php 1 year ago Filechecks.php 1 year ago Filesystem.php 1 year ago FrontController.php 1 year ago Http.php 1 year ago IP.php 1 year ago Log.php 2 years ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 1 year ago NoAccessException.php 2 years ago Nonce.php 1 year ago Notification.php 1 year ago NumberFormatter.php 1 year ago Option.php 1 year ago Period.php 1 year ago Piwik.php 1 year ago Plugin.php 1 year ago Process.php 1 year ago Profiler.php 1 year ago ProxyHeaders.php 2 years ago ProxyHttp.php 1 year ago QuickForm2.php 1 year ago RankingQuery.php 1 year ago ReportRenderer.php 1 year ago Request.php 1 year ago Segment.php 1 year ago Sequence.php 2 years ago Session.php 1 year ago SettingsPiwik.php 1 year ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 1 year ago SiteContentDetector.php 1 year ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 2 years ago Tracker.php 1 year ago Twig.php 1 year ago Unzip.php 1 year ago UpdateCheck.php 1 year ago Updater.php 1 year ago UpdaterErrorException.php 2 years ago Updates.php 1 year ago Url.php 1 year ago UrlHelper.php 1 year ago Version.php 1 year ago View.php 1 year ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 2 years ago
CliMulti.php
517 lines
1 <?php
2
3 /**
4 * Matomo - free/libre analytics platform
5 *
6 * @link https://matomo.org
7 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8 */
9 namespace Piwik;
10
11 use Piwik\Archiver\Request;
12 use Piwik\CliMulti\CliPhp;
13 use Piwik\CliMulti\Output;
14 use Piwik\CliMulti\OutputInterface;
15 use Piwik\CliMulti\Process;
16 use Piwik\CliMulti\ProcessSymfony;
17 use Piwik\CliMulti\StaticOutput;
18 use Piwik\Container\StaticContainer;
19 use Piwik\Log\LoggerInterface;
20 use Piwik\Log\NullLogger;
21 use Piwik\Plugins\CoreConsole\FeatureFlags\CliMultiProcessSymfony;
22 use Piwik\Plugins\FeatureFlags\FeatureFlagManager;
23 use Throwable;
24 /**
25 * Class CliMulti.
26 */
27 class CliMulti
28 {
29 public const BASE_WAIT_TIME = 250000;
30 // 250 * 1000 = 250ms
31 /**
32 * If set to true or false it will overwrite whether async is supported or not.
33 *
34 * @var null|bool
35 */
36 public $supportsAsync = null;
37 /**
38 * If set to true or false it will overwrite whether async using Symfony\Process is supported or not.
39 *
40 * Will only be checked if the default $supportsAsync is true.
41 *
42 * @var null|bool
43 */
44 public $supportsAsyncSymfony = null;
45 /**
46 * @var Process[]|ProcessSymfony[]
47 */
48 private $processes = [];
49 /**
50 * If set it will issue at most concurrentProcessesLimit requests
51 * @var int
52 */
53 private $concurrentProcessesLimit = null;
54 /**
55 * @var OutputInterface[]
56 */
57 private $outputs = [];
58 private $acceptInvalidSSLCertificate = \false;
59 /**
60 * @var bool
61 */
62 private $runAsSuperUser = \false;
63 /**
64 * Only used when doing synchronous curl requests.
65 *
66 * @var string
67 */
68 private $urlToPiwik = null;
69 private $phpCliOptions = '';
70 /**
71 * @var callable
72 */
73 private $onProcessFinish = null;
74 /**
75 * @var Timer[]
76 */
77 protected $timers = [];
78 protected $isTimingRequests = \false;
79 /**
80 * @var LoggerInterface
81 */
82 private $logger;
83 public function __construct(?LoggerInterface $logger = null)
84 {
85 $this->supportsAsync = $this->supportsAsync();
86 $this->supportsAsyncSymfony = $this->supportsAsyncSymfony();
87 $this->logger = $logger ?: new NullLogger();
88 }
89 /**
90 * It will request all given URLs in parallel (async) using the CLI and wait until all requests are finished.
91 * If multi cli is not supported (eg windows) it will initiate an HTTP request instead (not async).
92 *
93 * @param string[] $piwikUrls An array of urls, for instance:
94 *
95 * `array('http://www.example.com/piwik?module=API...')`
96 *
97 * **Make sure query parameter values are properly encoded in the URLs.**
98 *
99 * @return array The response of each URL in the same order as the URLs. The array can contain null values in case
100 * there was a problem with a request, for instance if the process died unexpected.
101 */
102 public function request(array $piwikUrls)
103 {
104 if ($this->isTimingRequests) {
105 foreach ($piwikUrls as $url) {
106 $this->timers[] = new \Piwik\Timer();
107 }
108 }
109 $chunks = array($piwikUrls);
110 if ($this->concurrentProcessesLimit) {
111 $chunks = array_chunk($piwikUrls, $this->concurrentProcessesLimit);
112 }
113 $results = array();
114 foreach ($chunks as $urlsChunk) {
115 $results = array_merge($results, $this->requestUrls($urlsChunk));
116 }
117 return $results;
118 }
119 /**
120 * Forwards the given configuration options to the PHP cli command.
121 * @param string $phpCliOptions eg "-d memory_limit=8G -c=path/to/php.ini"
122 */
123 public function setPhpCliConfigurationOptions($phpCliOptions)
124 {
125 $this->phpCliOptions = (string) $phpCliOptions;
126 }
127 /**
128 * Ok, this sounds weird. Why should we care about ssl certificates when we are in CLI mode? It is needed for
129 * our simple fallback mode for Windows where we initiate HTTP requests instead of CLI.
130 * @param $acceptInvalidSSLCertificate
131 */
132 public function setAcceptInvalidSSLCertificate($acceptInvalidSSLCertificate)
133 {
134 $this->acceptInvalidSSLCertificate = $acceptInvalidSSLCertificate;
135 }
136 /**
137 * @param $limit int Maximum count of requests to issue in parallel
138 */
139 public function setConcurrentProcessesLimit($limit)
140 {
141 $this->concurrentProcessesLimit = $limit;
142 }
143 public function runAsSuperUser($runAsSuperUser = \true)
144 {
145 $this->runAsSuperUser = $runAsSuperUser;
146 }
147 private function start($piwikUrls)
148 {
149 $numUrls = count($piwikUrls);
150 foreach ($piwikUrls as $index => $url) {
151 $shouldStart = null;
152 if ($url instanceof Request) {
153 $shouldStart = $url->start();
154 }
155 $cmdId = $this->generateCommandId($url) . $index;
156 if ($shouldStart === Request::ABORT) {
157 // output is needed to ensure same order of url to response
158 $output = new StaticOutput($cmdId);
159 $output->write(serialize(array('aborted' => '1')));
160 $this->outputs[] = $output;
161 } else {
162 $this->executeUrlCommand($cmdId, $url, $numUrls);
163 }
164 }
165 }
166 private function executeUrlCommand($cmdId, $url, $numUrls)
167 {
168 if ($this->supportsAsync) {
169 if ($this->supportsAsyncSymfony) {
170 $output = new StaticOutput($cmdId);
171 $this->executeAsyncCliSymfony($url, $cmdId);
172 } elseif ($numUrls === 1) {
173 $output = new StaticOutput($cmdId);
174 $this->executeSyncCli($url, $output);
175 } else {
176 $output = new Output($cmdId);
177 $this->executeAsyncCli($url, $output, $cmdId);
178 }
179 } else {
180 $output = new StaticOutput($cmdId);
181 $this->executeNotAsyncHttp($url, $output);
182 }
183 $this->outputs[] = $output;
184 }
185 private function buildCommand($hostname, $query, $outputFileIfAsync, $doEsacpeArg = \true)
186 {
187 $bin = $this->findPhpBinary();
188 $superuserCommand = $this->runAsSuperUser ? "--superuser" : "";
189 $append = '2>&1';
190 if ($outputFileIfAsync) {
191 $append = sprintf(' > %s 2>&1 &', $outputFileIfAsync);
192 }
193 if ($doEsacpeArg) {
194 $hostname = escapeshellarg($hostname);
195 $query = escapeshellarg($query);
196 }
197 return sprintf('%s %s %s/console climulti:request -q --matomo-domain=%s %s %s %s', $bin, $this->phpCliOptions, PIWIK_INCLUDE_PATH, $hostname, $superuserCommand, $query, $append);
198 }
199 private function getResponse()
200 {
201 $response = array();
202 foreach ($this->outputs as $output) {
203 $content = $output->get();
204 // Remove output that can be ignored in climulti . works around some worpdress setups where the hash bang may
205 // be printed
206 $search = '#!/usr/bin/env php';
207 if (!empty($content) && is_string($content) && mb_substr(trim($content), 0, strlen($search)) === $search) {
208 $content = trim(mb_substr(trim($content), strlen($search)));
209 }
210 $response[] = $content;
211 }
212 return $response;
213 }
214 private function hasFinished() : bool
215 {
216 $hasFinished = \true;
217 foreach ($this->processes as $index => $process) {
218 if ($process instanceof ProcessSymfony) {
219 $processFinished = $this->hasFinishedProcessSymfony($process);
220 } else {
221 $processFinished = $this->hasFinishedProcess($process);
222 }
223 if (\true === $processFinished) {
224 // prevent from checking this process over and over again
225 unset($this->processes[$index]);
226 if ($this->isTimingRequests) {
227 $this->timers[$index]->finish();
228 }
229 if ($this->onProcessFinish) {
230 $pid = $process->getPid();
231 $onProcessFinish = $this->onProcessFinish;
232 $onProcessFinish($pid);
233 }
234 }
235 $hasFinished = $hasFinished && $processFinished;
236 }
237 return $hasFinished;
238 }
239 private function hasFinishedProcess(Process $process) : bool
240 {
241 $hasStarted = $process->hasStarted();
242 if (!$hasStarted && 8 <= $process->getSecondsSinceCreation()) {
243 // if process was created more than 8 seconds ago but still not started there must be something wrong.
244 // ==> declare the process as finished
245 $process->finishProcess();
246 return \true;
247 } elseif (!$hasStarted) {
248 return \false;
249 }
250 if ($process->isRunning()) {
251 return \false;
252 }
253 $pid = $process->getPid();
254 foreach ($this->outputs as $output) {
255 if ($output->getOutputId() === $pid && $output->isAbnormal()) {
256 $process->finishProcess();
257 return \true;
258 }
259 }
260 return $process->hasFinished();
261 }
262 private function hasFinishedProcessSymfony(ProcessSymfony $process) : bool
263 {
264 $finished = $process->isStarted() && !$process->isRunning();
265 if ($finished) {
266 foreach ($this->outputs as $output) {
267 if ($output->getOutputId() !== $process->getCommandId()) {
268 continue;
269 }
270 $output->write($process->getOutput());
271 break;
272 }
273 }
274 return $finished;
275 }
276 private function generateCommandId($command)
277 {
278 return substr(\Piwik\Common::hash($command . microtime(\true) . rand(0, 99999)), 0, 100);
279 }
280 /**
281 * What is missing under windows? Detection whether a process is still running in Process::isProcessStillRunning
282 * and how to send a process into background in start()
283 */
284 public function supportsAsync()
285 {
286 $supportsAsync = Process::isSupported() && !\Piwik\Common::isPhpCgiType() && $this->findPhpBinary();
287 /**
288 * Triggered to allow plugins to force the usage of async cli multi execution or to disable it.
289 *
290 * **Example**
291 *
292 * public function supportsAsync(&$supportsAsync)
293 * {
294 * $supportsAsync = false; // do not allow async climulti execution
295 * }
296 *
297 * @param bool &$supportsAsync Whether async is supported or not.
298 */
299 \Piwik\Piwik::postEvent('CliMulti.supportsAsync', array(&$supportsAsync));
300 return $supportsAsync;
301 }
302 /**
303 * Returns whether Symfony\Process instead of the default Piwik\Process for
304 * async cli multi execution.
305 *
306 * Requirements:
307 * - supportsAsync has to be true
308 * - feature flag "CliMultiProcessSymfony" has to be active
309 * - proc_open has to be available
310 *
311 * Several of the regular supportsAsync requirements may be obsolete after
312 * a complete switch has been done.
313 *
314 * @return bool
315 */
316 public function supportsAsyncSymfony() : bool
317 {
318 // When updating from an older version the required symfony component
319 // may not be available. We have to verify that outside of the
320 // CliMulti\ProcessSymfony wrapper because it extends the component.
321 if (!$this->supportsAsync || Process::isMethodDisabled('proc_open') || !class_exists(\Symfony\Component\Process\Process::class)) {
322 return \false;
323 }
324 // The required DI configuration may not be loaded during the update process.
325 // This can happen for an upgrade from a version that did not yet contain
326 // the feature flag plugin.
327 try {
328 $featureFlagManager = StaticContainer::get(FeatureFlagManager::class);
329 return $featureFlagManager->isFeatureActive(CliMultiProcessSymfony::class);
330 } catch (Throwable $e) {
331 return \false;
332 }
333 }
334 private function findPhpBinary()
335 {
336 $cliPhp = new CliPhp();
337 return $cliPhp->findPhpBinary();
338 }
339 private function cleanup()
340 {
341 foreach ($this->processes as $process) {
342 if ($process instanceof ProcessSymfony) {
343 $process->stop(0);
344 } else {
345 $process->finishProcess();
346 }
347 }
348 foreach ($this->outputs as $output) {
349 $output->destroy();
350 }
351 $this->processes = array();
352 $this->outputs = array();
353 }
354 /**
355 * Remove files older than one week. They should be cleaned up automatically after each request but for whatever
356 * reason there can be always some files left.
357 */
358 public static function cleanupNotRemovedFiles()
359 {
360 $timeOneWeekAgo = strtotime('-1 week');
361 $files = _glob(self::getTmpPath() . '/*');
362 if (empty($files)) {
363 return;
364 }
365 foreach ($files as $file) {
366 if (file_exists($file)) {
367 $timeLastModified = filemtime($file);
368 if ($timeLastModified !== \false && $timeOneWeekAgo > $timeLastModified) {
369 unlink($file);
370 }
371 }
372 }
373 }
374 public static function getTmpPath()
375 {
376 return StaticContainer::get('path.tmp') . '/climulti';
377 }
378 private function executeAsyncCli($url, Output $output, $cmdId)
379 {
380 $this->processes[] = new Process($cmdId);
381 $url = $this->appendTestmodeParamToUrlIfNeeded($url);
382 $query = \Piwik\UrlHelper::getQueryFromUrl($url, ['pid' => $cmdId, 'runid' => \Piwik\Common::getProcessId()]);
383 $hostname = \Piwik\Url::getHost($checkIfTrusted = \false);
384 $command = $this->buildCommand($hostname, $query, $output->getPathToFile());
385 $this->logger->debug('Running command: {command} [method = {method}]', ['command' => $command, 'method' => 'asyncCli']);
386 shell_exec($command);
387 }
388 private function executeAsyncCliSymfony(string $url, string $cmdId) : void
389 {
390 $url = $this->appendTestmodeParamToUrlIfNeeded($url);
391 $query = \Piwik\UrlHelper::getQueryFromUrl($url, array());
392 $hostname = \Piwik\Url::getHost($checkIfTrusted = \false);
393 $command = $this->buildCommand($hostname, $query, '', \true);
394 $this->logger->debug('Running command: {command} [method = {method}]', ['command' => $command, 'method' => 'asyncCliSymfony']);
395 // Prepending "exec" is required to send signals to the process
396 // Not using array notation because $command can contain complex parameters
397 if ('\\' !== \DIRECTORY_SEPARATOR) {
398 $command = 'exec ' . $command;
399 }
400 $process = ProcessSymfony::fromShellCommandline($command);
401 $process->setTimeout(null);
402 $process->start();
403 $process->setCommandId($cmdId);
404 $this->processes[] = $process;
405 }
406 private function executeSyncCli($url, StaticOutput $output)
407 {
408 $url = $this->appendTestmodeParamToUrlIfNeeded($url);
409 $query = \Piwik\UrlHelper::getQueryFromUrl($url, array());
410 $hostname = \Piwik\Url::getHost($checkIfTrusted = \false);
411 $command = $this->buildCommand($hostname, $query, '', \true);
412 $this->logger->debug('Running command: {command} [method = {method}]', ['command' => $command, 'method' => 'syncCli']);
413 $result = shell_exec($command);
414 if ($result) {
415 $result = trim($result);
416 }
417 $output->write($result);
418 }
419 private function executeNotAsyncHttp($url, StaticOutput $output)
420 {
421 $piwikUrl = $this->urlToPiwik ?: \Piwik\SettingsPiwik::getPiwikUrl();
422 if (empty($piwikUrl)) {
423 $piwikUrl = 'http://' . \Piwik\Url::getHost() . '/';
424 }
425 $url = $piwikUrl . $url;
426 if (\Piwik\Config::getInstance()->General['force_ssl'] == 1) {
427 $url = str_replace("http://", "https://", $url);
428 }
429 $requestBody = null;
430 if ($this->runAsSuperUser) {
431 $tokenAuth = self::getSuperUserTokenAuth();
432 if (strpos($url, '?') === \false) {
433 $url .= '?';
434 } else {
435 $url .= '&';
436 }
437 $requestBody = 'token_auth=' . $tokenAuth;
438 }
439 try {
440 $this->logger->debug("Execute HTTP API request: " . $url);
441 $response = \Piwik\Http::sendHttpRequestBy('curl', $url, $timeout = 0, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0, $acceptLanguage = \false, $this->acceptInvalidSSLCertificate, \false, \false, 'POST', null, null, $requestBody, [], $forcePost = \true);
442 $output->write($response);
443 } catch (\Exception $e) {
444 $message = "Got invalid response from API request: {$url}. ";
445 if (isset($response) && empty($response)) {
446 $message .= "The response was empty. This usually means a server error. This solution to this error is generally to increase the value of 'memory_limit' in your php.ini file. Please check your Web server Error Log file for more details.";
447 } else {
448 $message .= "Response was '" . $e->getMessage() . "'";
449 }
450 $output->write($message);
451 $this->logger->debug($message, ['exception' => $e]);
452 }
453 }
454 private function appendTestmodeParamToUrlIfNeeded($url)
455 {
456 $isTestMode = defined('PIWIK_TEST_MODE');
457 if ($isTestMode && \false === strpos($url, '?')) {
458 $url .= "?testmode=1";
459 } elseif ($isTestMode) {
460 $url .= "&testmode=1";
461 }
462 return $url;
463 }
464 /**
465 * @param array $piwikUrls
466 * @return array
467 */
468 private function requestUrls(array $piwikUrls)
469 {
470 $this->start($piwikUrls);
471 $startTime = time();
472 do {
473 $elapsed = time() - $startTime;
474 $timeToWait = $this->getTimeToWaitBeforeNextCheck($elapsed);
475 if (count($this->processes)) {
476 usleep($timeToWait);
477 }
478 } while (!$this->hasFinished());
479 $results = $this->getResponse();
480 $this->cleanup();
481 self::cleanupNotRemovedFiles();
482 return $results;
483 }
484 private static function getSuperUserTokenAuth()
485 {
486 return \Piwik\Piwik::requestTemporarySystemAuthToken('CliMultiNonAsyncArchive', 36);
487 }
488 public function setUrlToPiwik($urlToPiwik)
489 {
490 $this->urlToPiwik = $urlToPiwik;
491 }
492 public function onProcessFinish(callable $callback)
493 {
494 $this->onProcessFinish = $callback;
495 }
496 // every minute that passes adds an extra 100ms to the wait time. so 5 minutes results in 500ms extra, 20mins results in 2s extra.
497 private function getTimeToWaitBeforeNextCheck($elapsed)
498 {
499 $minutes = floor($elapsed / 60);
500 return self::BASE_WAIT_TIME + $minutes * 100000;
501 // 100 * 1000 = 100ms
502 }
503 public static function isCliMultiRequest()
504 {
505 return \Piwik\Common::getRequestVar('pid', \false) !== \false;
506 }
507 public function timeRequests()
508 {
509 $this->timers = [];
510 $this->isTimingRequests = \true;
511 }
512 public function getTimers()
513 {
514 return $this->timers;
515 }
516 }
517