PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.13.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.13.2
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 3 years ago Access 3 years ago Application 4 years ago Archive 3 years ago ArchiveProcessor 3 years ago Archiver 5 years ago AssetManager 3 years ago Auth 3 years ago Category 5 years ago Changes 4 years ago CliMulti 4 years ago Columns 3 years ago Concurrency 3 years ago Config 4 years ago Container 4 years ago CronArchive 3 years ago DataAccess 3 years ago DataFiles 5 years ago DataTable 3 years ago Db 3 years ago DeviceDetector 3 years ago Email 5 years ago Exception 3 years ago Http 4 years ago Intl 4 years ago Mail 3 years ago Measurable 5 years ago Menu 3 years ago Metrics 4 years ago Notification 4 years ago Period 4 years ago Plugin 3 years ago ProfessionalServices 4 years ago Report 5 years ago ReportRenderer 3 years ago Scheduler 4 years ago Segment 3 years ago Session 4 years ago Settings 3 years ago Tracker 3 years ago Translation 4 years ago UpdateCheck 5 years ago Updater 3 years ago Updates 3 years ago Validators 4 years ago View 4 years ago ViewDataTable 3 years ago Visualization 4 years ago Widget 5 years ago .htaccess 6 years ago Access.php 4 years ago Archive.php 4 years ago ArchiveProcessor.php 4 years ago AssetManager.php 4 years ago Auth.php 5 years ago AuthResult.php 5 years ago BaseFactory.php 5 years ago Cache.php 5 years ago CacheId.php 5 years ago CliMulti.php 4 years ago Common.php 3 years ago Config.php 3 years ago Console.php 3 years ago Context.php 5 years ago Cookie.php 3 years ago CronArchive.php 3 years ago DataArray.php 4 years ago DataTable.php 3 years ago Date.php 4 years ago Db.php 4 years ago DbHelper.php 3 years ago Development.php 5 years ago ErrorHandler.php 5 years ago EventDispatcher.php 3 years ago ExceptionHandler.php 3 years ago FileIntegrity.php 5 years ago Filechecks.php 4 years ago Filesystem.php 3 years ago FrontController.php 3 years ago Http.php 3 years ago IP.php 4 years ago Log.php 4 years ago LogDeleter.php 4 years ago Mail.php 4 years ago Metrics.php 3 years ago NoAccessException.php 5 years ago Nonce.php 3 years ago Notification.php 5 years ago NumberFormatter.php 4 years ago Option.php 4 years ago Period.php 5 years ago Piwik.php 3 years ago Plugin.php 4 years ago Profiler.php 4 years ago ProxyHeaders.php 5 years ago ProxyHttp.php 3 years ago QuickForm2.php 5 years ago RankingQuery.php 3 years ago ReportRenderer.php 4 years ago Segment.php 3 years ago Sequence.php 5 years ago Session.php 3 years ago SettingsPiwik.php 3 years ago SettingsServer.php 5 years ago Singleton.php 5 years ago Site.php 3 years ago SiteContentDetector.php 3 years ago SupportedBrowser.php 3 years ago TCPDF.php 5 years ago Theme.php 5 years ago Timer.php 5 years ago Tracker.php 4 years ago Twig.php 4 years ago Unzip.php 5 years ago UpdateCheck.php 5 years ago Updater.php 4 years ago UpdaterErrorException.php 5 years ago Updates.php 5 years ago Url.php 3 years ago UrlHelper.php 3 years ago Version.php 3 years ago View.php 3 years ago bootstrap.php 3 years ago dispatch.php 5 years ago testMinimumPhpVersion.php 3 years ago
CliMulti.php
506 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 */
8 namespace Piwik;
9
10 use Piwik\Archiver\Request;
11 use Piwik\CliMulti\CliPhp;
12 use Piwik\CliMulti\Output;
13 use Piwik\CliMulti\OutputInterface;
14 use Piwik\CliMulti\Process;
15 use Piwik\CliMulti\StaticOutput;
16 use Piwik\Container\StaticContainer;
17 use Psr\Log\LoggerInterface;
18 use Psr\Log\NullLogger;
19
20 /**
21 * Class CliMulti.
22 */
23 class CliMulti
24 {
25 const BASE_WAIT_TIME = 250000; // 250 * 1000 = 250ms
26
27 /**
28 * If set to true or false it will overwrite whether async is supported or not.
29 *
30 * @var null|bool
31 */
32 public $supportsAsync = null;
33
34 /**
35 * @var Process[]
36 */
37 private $processes = array();
38
39 /**
40 * If set it will issue at most concurrentProcessesLimit requests
41 * @var int
42 */
43 private $concurrentProcessesLimit = null;
44
45 /**
46 * @var OutputInterface[]
47 */
48 private $outputs = array();
49
50 private $acceptInvalidSSLCertificate = false;
51
52 /**
53 * @var bool
54 */
55 private $runAsSuperUser = false;
56
57 /**
58 * Only used when doing synchronous curl requests.
59 *
60 * @var string
61 */
62 private $urlToPiwik = null;
63
64 private $phpCliOptions = '';
65
66 /**
67 * @var callable
68 */
69 private $onProcessFinish = null;
70
71 /**
72 * @var Timer[]
73 */
74 protected $timers = [];
75
76 protected $isTimingRequests = false;
77
78 /**
79 * @var LoggerInterface
80 */
81 private $logger;
82
83 public function __construct(LoggerInterface $logger = null)
84 {
85 $this->supportsAsync = $this->supportsAsync();
86 $this->logger = $logger ?: new NullLogger();
87 }
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 Timer();
107 }
108 }
109
110 $chunks = array($piwikUrls);
111 if ($this->concurrentProcessesLimit) {
112 $chunks = array_chunk($piwikUrls, $this->concurrentProcessesLimit);
113 }
114
115 $results = array();
116 foreach ($chunks as $urlsChunk) {
117 $results = array_merge($results, $this->requestUrls($urlsChunk));
118 }
119
120 return $results;
121 }
122
123 /**
124 * Forwards the given configuration options to the PHP cli command.
125 * @param string $phpCliOptions eg "-d memory_limit=8G -c=path/to/php.ini"
126 */
127 public function setPhpCliConfigurationOptions($phpCliOptions)
128 {
129 $this->phpCliOptions = (string) $phpCliOptions;
130 }
131
132 /**
133 * Ok, this sounds weird. Why should we care about ssl certificates when we are in CLI mode? It is needed for
134 * our simple fallback mode for Windows where we initiate HTTP requests instead of CLI.
135 * @param $acceptInvalidSSLCertificate
136 */
137 public function setAcceptInvalidSSLCertificate($acceptInvalidSSLCertificate)
138 {
139 $this->acceptInvalidSSLCertificate = $acceptInvalidSSLCertificate;
140 }
141
142 /**
143 * @param $limit int Maximum count of requests to issue in parallel
144 */
145 public function setConcurrentProcessesLimit($limit)
146 {
147 $this->concurrentProcessesLimit = $limit;
148 }
149
150 public function runAsSuperUser($runAsSuperUser = true)
151 {
152 $this->runAsSuperUser = $runAsSuperUser;
153 }
154
155 private function start($piwikUrls)
156 {
157 $numUrls = count($piwikUrls);
158 foreach ($piwikUrls as $index => $url) {
159 $shouldStart = null;
160 if ($url instanceof Request) {
161 $shouldStart = $url->start();
162 }
163
164 $cmdId = $this->generateCommandId($url) . $index;
165
166 if ($shouldStart === Request::ABORT) {
167 // output is needed to ensure same order of url to response
168 $output = new StaticOutput($cmdId);
169 $output->write(serialize(array('aborted' => '1')));
170 $this->outputs[] = $output;
171 } else {
172 $this->executeUrlCommand($cmdId, $url, $numUrls);
173 }
174 }
175 }
176
177 private function executeUrlCommand($cmdId, $url, $numUrls)
178 {
179 if ($this->supportsAsync) {
180 if ($numUrls === 1) {
181 $output = new StaticOutput($cmdId);
182 $this->executeSyncCli($url, $output);
183 } else {
184 $output = new Output($cmdId);
185 $this->executeAsyncCli($url, $output, $cmdId);
186 }
187 } else {
188 $output = new StaticOutput($cmdId);
189 $this->executeNotAsyncHttp($url, $output);
190 }
191
192 $this->outputs[] = $output;
193 }
194
195 private function buildCommand($hostname, $query, $outputFileIfAsync, $doEsacpeArg = true)
196 {
197 $bin = $this->findPhpBinary();
198 $superuserCommand = $this->runAsSuperUser ? "--superuser" : "";
199
200 $append = '2>&1';
201 if ($outputFileIfAsync) {
202 $append = sprintf(' > %s 2>&1 &', $outputFileIfAsync);
203 }
204
205 if ($doEsacpeArg) {
206 $hostname = escapeshellarg($hostname);
207 $query = escapeshellarg($query);
208 }
209
210 return sprintf('%s %s %s/console climulti:request -q --matomo-domain=%s %s %s %s',
211 $bin, $this->phpCliOptions, PIWIK_INCLUDE_PATH, $hostname, $superuserCommand, $query,$append);
212 }
213
214 private function getResponse()
215 {
216 $response = array();
217
218 foreach ($this->outputs as $output) {
219 $content = $output->get();
220 // Remove output that can be ignored in climulti . works around some worpdress setups where the hash bang may
221 // be printed
222 $search = '#!/usr/bin/env php';
223 if (!empty($content)
224 && is_string($content)
225 && mb_substr(trim($content), 0, strlen($search)) === $search) {
226 $content = trim(mb_substr(trim($content), strlen($search)));
227 }
228 $response[] = $content;
229 }
230
231 return $response;
232 }
233
234 private function hasFinished()
235 {
236 foreach ($this->processes as $index => $process) {
237 $hasStarted = $process->hasStarted();
238
239 if (!$hasStarted && 8 <= $process->getSecondsSinceCreation()) {
240 // if process was created more than 8 seconds ago but still not started there must be something wrong.
241 // ==> declare the process as finished
242 $process->finishProcess();
243 continue;
244 } elseif (!$hasStarted) {
245 return false;
246 }
247
248 if ($process->isRunning()) {
249 return false;
250 }
251
252 $pid = $process->getPid();
253 foreach ($this->outputs as $output) {
254 if ($output->getOutputId() === $pid && $output->isAbnormal()) {
255 $process->finishProcess();
256 continue;
257 }
258 }
259
260 if ($process->hasFinished()) {
261 // prevent from checking this process over and over again
262 unset($this->processes[$index]);
263
264 if ($this->isTimingRequests) {
265 $this->timers[$index]->finish();
266 }
267
268 if ($this->onProcessFinish) {
269 $onProcessFinish = $this->onProcessFinish;
270 $onProcessFinish($pid);
271 }
272 }
273 }
274
275 return true;
276 }
277
278 private function generateCommandId($command)
279 {
280 return substr(Common::hash($command . microtime(true) . rand(0, 99999)), 0, 100);
281 }
282
283 /**
284 * What is missing under windows? Detection whether a process is still running in Process::isProcessStillRunning
285 * and how to send a process into background in start()
286 */
287 public function supportsAsync()
288 {
289 $supportsAsync = Process::isSupported() && !Common::isPhpCgiType() && $this->findPhpBinary();
290
291 /**
292 * Triggered to allow plugins to force the usage of async cli multi execution or to disable it.
293 *
294 * **Example**
295 *
296 * public function supportsAsync(&$supportsAsync)
297 * {
298 * $supportsAsync = false; // do not allow async climulti execution
299 * }
300 *
301 * @param bool &$supportsAsync Whether async is supported or not.
302 */
303 Piwik::postEvent('CliMulti.supportsAsync', array(&$supportsAsync));
304
305 return $supportsAsync;
306 }
307
308 private function findPhpBinary()
309 {
310 $cliPhp = new CliPhp();
311 return $cliPhp->findPhpBinary();
312 }
313
314 private function cleanup()
315 {
316 foreach ($this->processes as $pid) {
317 $pid->finishProcess();
318 }
319
320 foreach ($this->outputs as $output) {
321 $output->destroy();
322 }
323
324 $this->processes = array();
325 $this->outputs = array();
326 }
327
328 /**
329 * Remove files older than one week. They should be cleaned up automatically after each request but for whatever
330 * reason there can be always some files left.
331 */
332 public static function cleanupNotRemovedFiles()
333 {
334 $timeOneWeekAgo = strtotime('-1 week');
335
336 $files = _glob(self::getTmpPath() . '/*');
337 if (empty($files)) {
338 return;
339 }
340
341 foreach ($files as $file) {
342 if (file_exists($file)) {
343 $timeLastModified = filemtime($file);
344
345 if ($timeLastModified !== false && $timeOneWeekAgo > $timeLastModified) {
346 unlink($file);
347 }
348 }
349 }
350 }
351
352 public static function getTmpPath()
353 {
354 return StaticContainer::get('path.tmp') . '/climulti';
355 }
356
357 private function executeAsyncCli($url, Output $output, $cmdId)
358 {
359 $this->processes[] = new Process($cmdId);
360
361 $url = $this->appendTestmodeParamToUrlIfNeeded($url);
362 $query = UrlHelper::getQueryFromUrl($url, array('pid' => $cmdId, 'runid' => getmypid()));
363 $hostname = Url::getHost($checkIfTrusted = false);
364 $command = $this->buildCommand($hostname, $query, $output->getPathToFile());
365
366 $this->logger->debug("Running command: {command}", ['command' => $command]);
367 shell_exec($command);
368 }
369
370 private function executeSyncCli($url, StaticOutput $output)
371 {
372 $url = $this->appendTestmodeParamToUrlIfNeeded($url);
373 $query = UrlHelper::getQueryFromUrl($url, array());
374 $hostname = Url::getHost($checkIfTrusted = false);
375 $command = $this->buildCommand($hostname, $query, '', true);
376
377 $this->logger->debug("Running command: {command}", ['command' => $command]);
378 $result = shell_exec($command);
379 if ($result) {
380 $result = trim($result);
381 }
382 $output->write($result);
383 }
384
385 private function executeNotAsyncHttp($url, StaticOutput $output)
386 {
387 $piwikUrl = $this->urlToPiwik ?: SettingsPiwik::getPiwikUrl();
388 if (empty($piwikUrl)) {
389 $piwikUrl = 'http://' . Url::getHost() . '/';
390 }
391
392 $url = $piwikUrl . $url;
393 if (Config::getInstance()->General['force_ssl'] == 1) {
394 $url = str_replace("http://", "https://", $url);
395 }
396
397 $requestBody = null;
398 if ($this->runAsSuperUser) {
399 $tokenAuth = self::getSuperUserTokenAuth();
400
401 if (strpos($url, '?') === false) {
402 $url .= '?';
403 } else {
404 $url .= '&';
405 }
406
407 $requestBody = 'token_auth=' . $tokenAuth;
408 }
409
410 try {
411 $this->logger->debug("Execute HTTP API request: " . $url);
412 $response = 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);
413 $output->write($response);
414 } catch (\Exception $e) {
415 $message = "Got invalid response from API request: $url. ";
416
417 if (isset($response) && empty($response)) {
418 $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.";
419 } else {
420 $message .= "Response was '" . $e->getMessage() . "'";
421 }
422
423 $output->write($message);
424
425 $this->logger->debug($message, ['exception' => $e]);
426 }
427 }
428
429 private function appendTestmodeParamToUrlIfNeeded($url)
430 {
431 $isTestMode = defined('PIWIK_TEST_MODE');
432
433 if ($isTestMode && false === strpos($url, '?')) {
434 $url .= "?testmode=1";
435 } elseif ($isTestMode) {
436 $url .= "&testmode=1";
437 }
438
439 return $url;
440 }
441
442 /**
443 * @param array $piwikUrls
444 * @return array
445 */
446 private function requestUrls(array $piwikUrls)
447 {
448 $this->start($piwikUrls);
449
450 $startTime = time();
451 do {
452 $elapsed = time() - $startTime;
453 $timeToWait = $this->getTimeToWaitBeforeNextCheck($elapsed);
454
455 if (count($this->processes)) {
456 usleep($timeToWait);
457 }
458 } while (!$this->hasFinished());
459
460 $results = $this->getResponse();
461 $this->cleanup();
462
463 self::cleanupNotRemovedFiles();
464
465 return $results;
466 }
467
468 private static function getSuperUserTokenAuth()
469 {
470 return Piwik::requestTemporarySystemAuthToken('CliMultiNonAsyncArchive', 36);
471 }
472
473 public function setUrlToPiwik($urlToPiwik)
474 {
475 $this->urlToPiwik = $urlToPiwik;
476 }
477
478 public function onProcessFinish(callable $callback)
479 {
480 $this->onProcessFinish = $callback;
481 }
482
483 // every minute that passes adds an extra 100ms to the wait time. so 5 minutes results in 500ms extra, 20mins results in 2s extra.
484 private function getTimeToWaitBeforeNextCheck($elapsed)
485 {
486 $minutes = floor($elapsed / 60);
487 return self::BASE_WAIT_TIME + $minutes * 100000; // 100 * 1000 = 100ms
488 }
489
490 public static function isCliMultiRequest()
491 {
492 return Common::getRequestVar('pid', false) !== false;
493 }
494
495 public function timeRequests()
496 {
497 $this->timers = [];
498 $this->isTimingRequests = true;
499 }
500
501 public function getTimers()
502 {
503 return $this->timers;
504 }
505 }
506