idsite < 1) { debug('Malformed body. Last JSON error:' . json_last_error() . ' ' . json_last_error_msg()); http_response_code(400); exit(1); } // Build output for Matomo Tracking API and send in chunks. $output = []; $reporting_start = time(); $reporting_end = 0; $count = 0; $result = false; if (property_exists($data, 'visits') && gettype($data->visits) === 'array') { foreach ($data->visits as $visit) { $count += renderVisit($visit); } } if (property_exists($data, 'events') && gettype($data->events) === 'array') { foreach ($data->events as $event) { $count += renderEvent($event); } } // Send the rest. send(false); debug('result = ' . ($result ? 'success': 'failure') . ", count = $count"); if ($result === false) { debug('There were no events and visits or the Matomo Tracking API refused all requests.'); http_response_code(400); exit(1); } // Instruct Matomo to re-analyse the complete period of this request // on the next auto-archiving run. $start = new DateTime(); $start->setTimestamp($reporting_start); $end = new DateTime(); $end->setTimestamp($reporting_end); $query = http_build_query([ 'module' => 'API', 'method' => 'CoreAdminHome.invalidateArchivedReports', 'idSites' => $data->idsite, 'period' => 'range', // Need to use `dates[]` instead of `dates`. Otherwise, some automagic happens in Matomo and breaks the call. 'dates[]' => $start->format('Y-m-d') . ',' . $end->format('Y-m-d'), 'token_auth' => $TOKEN_AUTH, ]); $result = callMatomo(null, $query); if ($result === false) { debug('Matomo could not be triggered to reprocess data!'); } else { debug('Matomo was triggered to reprocess data!'); } http_response_code(204); exit(); // Helper functions. function renderVisit(stdClass $visit): int { $action_name = $visit->action_name; return render($visit->period_start, $visit->period_end, $visit->times, function() use ($action_name) { return ['action_name' => $action_name]; }); } function renderEvent(stdClass $event): int { return render($event->period_start, $event->period_end, $event->times, function() use ($event) { $e = ['e_c' => $event->category, 'e_a' => $event->action]; if (property_exists($event, 'name') && $event->name) { $e['e_n'] = $event->name; } if (property_exists($event, 'value') && $event->value) { $e['e_v'] = $event->value; } return $e; }); } function render(int $period_start, int $period_end, int $times, callable $callback): int { global $reporting_start, $reporting_end, $output; $reporting_start = min($reporting_start, $period_start); $reporting_end = max($reporting_end, $period_end); $interval = ($period_end - $period_start) / $times; // Offset the first record by half an interval, so records are neatly "centered" in the period. $period_start += $interval / 2; for ($i = 0; $i < $times; $i++) { $request = $callback(); $request['cdt'] = $period_start + $interval * $i; $output[] = buildRequest($request); send(); } return $times; } function buildRequest(array &$request): string { global $data; $request['idsite'] = $data->idsite; $request['rec'] = 1; if (property_exists($data, 'lang') && $data->lang) $request['lang'] = $data->lang; if (property_exists($data, 'ua') && $data->ua) $request['ua'] = $data->ua; return '?' . http_build_query($request); } function callMatomo(string $path = null, string $query = null, string $body = null): string { global $MATOMO_BASE_URL, $MAX_ATTEMPTS, $DELAY_AFTER_FAILURE; $options = [ 'method' => 'POST', 'header' => 'Content-Type: application/json; charset=UTF-8']; if (isset($body)) { $options['content'] = $body; } $context = stream_context_create(['http' => $options]); $url = "$MATOMO_BASE_URL/"; if (isset($path)) { $url .= $path; } if (isset($query)) { $url .= "?$query"; } $counter = 1; debug("Call Matomo at $url"); do { $result = file_get_contents($url, false, $context); $counter++; if ($result !== false || $counter > $MAX_ATTEMPTS) { break; } debug("Request error. Sleep $DELAY_AFTER_FAILURE seconds and retry!"); sleep($DELAY_AFTER_FAILURE); } while (true); return $result; } function sendToMatomo(array &$output): bool { global $TOKEN_AUTH; $body = json_encode([ 'token_auth' => $TOKEN_AUTH, 'requests' => $output]); if ($body === false) { return false; } $result = callMatomo("matomo.php", null, $body); debug("Called tracking API " . (!$result ? 'un' : '') . "successfully with " . count($output) . " lines of data: " . substr($body, 0, 256)); return !!$result; } function send(bool $onlyIfChunkSizeReached = true) { global $output, $CHUNK_SIZE, $result; if (!$onlyIfChunkSizeReached || count($output) >= $CHUNK_SIZE) { if (sendToMatomo($output)) { // If at least one request succeeded, we consider this good. // Otherwise, the client would re-send already processed data. $result = true; } // Remove already sent requests from memory again. $output = []; } } function debug($message) { global $DEBUG; if (!$DEBUG) return; error_log(print_r($message, true)); }