diff --git a/metager/app/Models/Matomo.php b/metager/app/Http/Controllers/StatisticsController.php similarity index 58% rename from metager/app/Models/Matomo.php rename to metager/app/Http/Controllers/StatisticsController.php index d5761be38a67ea17aa22a5211f37a3beaae5a774..e66c903cf3c17d5e83c47d65e3cc1b7ee6087d09 100644 --- a/metager/app/Models/Matomo.php +++ b/metager/app/Http/Controllers/StatisticsController.php @@ -1,15 +1,16 @@ <?php -namespace App\Models; +namespace App\Http\Controllers; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Redis; -use Request; -class Matomo + +class StatisticsController extends Controller { - static function PAGE_VISIT() + public function pageLoad(Request $request) { - if (!config("metager.matomo.enabled") || config("metager.matomo.url") === null || request()->is("health-check/*")) + if (!config("metager.matomo.enabled") || config("metager.matomo.url") === null) return; $params = [ @@ -18,18 +19,16 @@ class Matomo "rand" => md5(microtime(true)), "rec" => "1", "send_image" => "0", - "cip" => request()->ip(), + "cip" => $request->ip(), + "_id" => substr(md5($request->ip() . now()->format("Y-m-d")), 0, 16) ]; - // Page URL - $url = request()->getPathInfo(); - if (stripos($url, "/img") === 0 || stripos($url, "/meta/meta.ger3") === 0 || stripos($url, "/meta/loadMore") === 0 || preg_match("/\.(gif|png|jpg|jpeg|css)$/", $url) || preg_match("/csp-report$/", $url)) - return; - $url = request()->schemeAndHttpHost() . preg_replace("/^\/[a-z]{2}-[A-Z]{2}/", "", $url); - $params["url"] = $url; - // Referer - $params["urlref"] = request()->headers->get("referer"); + $http_params = $request->all(); + // Useragent - $params["ua"] = request()->userAgent(); + $params["ua"] = $request->userAgent(); + // Accept-Language + $params["lang"] = $request->header("Accept-Language"); + $params = array_merge($http_params, $params); // Merge arrays keeping our serverside defined options if key is set multiple times $url = config("metager.matomo.url") . "/matomo.php?" . http_build_query($params); @@ -48,4 +47,4 @@ class Matomo Redis::rpush(\App\MetaGer::FETCHQUEUE_KEY, $mission); } -} \ No newline at end of file +} diff --git a/metager/app/Http/Middleware/Statistics.php b/metager/app/Http/Middleware/Statistics.php deleted file mode 100644 index 5671ad62bd227bf1ea004a5328587fc90e715a79..0000000000000000000000000000000000000000 --- a/metager/app/Http/Middleware/Statistics.php +++ /dev/null @@ -1,22 +0,0 @@ -<?php - -namespace App\Http\Middleware; - -use App\Models\Matomo; -use Closure; -use Illuminate\Http\Request; -use Symfony\Component\HttpFoundation\Response; - -class Statistics -{ - /** - * Handle an incoming request. - * - * @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next - */ - public function handle(Request $request, Closure $next): Response - { - Matomo::PAGE_VISIT(); - return $next($request); - } -} diff --git a/metager/bootstrap/app.php b/metager/bootstrap/app.php index f748aebd509355521ab200e055bd92568bc7a0c5..8086b79d895e3ef55a64d665922a0ab90c74cbda 100644 --- a/metager/bootstrap/app.php +++ b/metager/bootstrap/app.php @@ -24,7 +24,6 @@ return Application::configure(basePath: dirname(__DIR__)) \App\Http\Middleware\TrimStrings::class, \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, TrustProxies::class, - Statistics::class, ]); $middleware->trustProxies(at: [ '10.0.0.0/8', diff --git a/metager/resources/js/statistics.js b/metager/resources/js/statistics.js new file mode 100644 index 0000000000000000000000000000000000000000..1c6ae1eb1a70029ce2a897eeade882fe38441c9a --- /dev/null +++ b/metager/resources/js/statistics.js @@ -0,0 +1,90 @@ +/** + * Class to gather and report anonymous statistics + * to our self hosted matomo instance. + */ +class Statistics { + #load_complete = false; + + constructor() { + let performance = window.performance.getEntriesByType('navigation')[0]; + if (performance.loadEventEnd != 0) { + this.#init(); + } else { + window.addEventListener("load", e => { + let readyStateCheckInterval = setInterval(() => { + performance = window.performance.getEntriesByType('navigation')[0]; + if (performance.loadEventEnd == 0) return; + console.log("load end"); + clearInterval(readyStateCheckInterval); + this.#init(); + }, 100); + }); + } + } + + #init() { + this.pageLoad(); + document.querySelectorAll("a").forEach(anchor => { + anchor.addEventListener("click", e => this.pageLeave(e.target.href).bind(this)); + }); + } + + pageLeave(target) { + let params = {}; + params.url = target; + params.link = target; + try { + let url = new URL(target); + if (url.host == document.location.host) return; + this.pageLoad(params); + navigator.sendBeacon("/stats/pl", new URLSearchParams(params)); + } catch (error) { } + } + + pageLoad(overwrite_params = {}) { + if (this.#load_complete) return; + this.#load_complete = true; + + let params = {}; + + // Page performance + try { + let performance = window.performance.getEntriesByType('navigation')[0]; + params.pf_net = performance.connectEnd - performance.connectStart; + params.pf_srv = performance.responseStart - performance.requestStart; + params.pf_tfr = performance.responseEnd - performance.responseStart; + params.pf_dm1 = performance.domInteractive - performance.responseEnd; + params.pf_dm2 = performance.domContentLoadedEventEnd - performance.domContentLoadedEventStart; + params.pf_onl = performance.loadEventEnd - performance.loadEventStart; + } catch (error) { } + + try { + params.res = window.screen.width + "x" + window.screen.height; + } catch (error) { } + + // Page URL + try { + params.url = document.location.href; + } catch (error) { } + + // Page Title + try { + params.action_name = document.title + } catch (error) { } + + // Referrer + try { + params.urlref = document.referrer; + } catch (error) { } + + // Cookies available + try { + params.cookie = navigator.cookieEnabled ? "1" : "0"; + } catch (error) { } + + params = { ...params, ...overwrite_params }; + + navigator.sendBeacon("/stats/pl", new URLSearchParams(params)); + } +} +export const statistics = new Statistics(); \ No newline at end of file diff --git a/metager/resources/js/utility.js b/metager/resources/js/utility.js index 3b8d7e7dc2e7ccc6b0e61954e8b296acddd1467d..0f5001337745b8b5e435965539b47bd085a7c905 100644 --- a/metager/resources/js/utility.js +++ b/metager/resources/js/utility.js @@ -1,3 +1,5 @@ +import { statistics } from "./statistics"; + document.addEventListener("DOMContentLoaded", (event) => { document .querySelectorAll(".js-only") diff --git a/metager/routes/web.php b/metager/routes/web.php index 3a55bc299aaf97cfe2d361f6797a8843a098f38a..6c6995dce0692e225a921a98cc6f97c9c9e9a256 100644 --- a/metager/routes/web.php +++ b/metager/routes/web.php @@ -13,6 +13,7 @@ use App\Http\Controllers\Prometheus; use App\Http\Controllers\SearchEngineList; use App\Http\Controllers\SitesearchController; use App\Http\Controllers\StartpageController; +use App\Http\Controllers\StatisticsController; use App\Http\Controllers\SuggestionController; use App\Http\Controllers\TTSController; use App\Http\Controllers\ZitatController; @@ -344,7 +345,7 @@ Route::withoutMiddleware([\Illuminate\Foundation\Http\Middleware\ValidateCsrfTok return redirect(LaravelLocalization::getLocalizedURL(LaravelLocalization::getCurrentLocale(), '/')); }); - Route::match (['get', 'post'], 'meta/meta.ger3', [MetaGerSearch::class, 'search'])->middleware('httpcache', 'externalimagesearch', 'spam', 'browserverification', 'humanverification', 'useragentmaster')->name("resultpage"); + Route::match(['get', 'post'], 'meta/meta.ger3', [MetaGerSearch::class, 'search'])->middleware('httpcache', 'externalimagesearch', 'spam', 'browserverification', 'humanverification', 'useragentmaster')->name("resultpage"); Route::get('meta/loadMore', [MetaGerSearch::class, 'loadMore']); @@ -449,4 +450,8 @@ Route::withoutMiddleware([\Illuminate\Foundation\Http\Middleware\ValidateCsrfTok Route::get('liveness-scheduler', [HealthcheckController::class, 'livenessScheduler']); Route::get('liveness-worker', [HealthcheckController::class, 'livenessWorker']); }); + + Route::group(['prefix' => 'stats'], function () { + Route::post('pl', [StatisticsController::class, 'pageLoad']); + }); }); \ No newline at end of file