# Fixes some weird terminal issues such as broken clear / CTRL+L
ENV TERM=linux
-WORKDIR "/application"
-
# Ensure apt doesn't ask questions when installing stuff
ENV DEBIAN_FRONTEND=noninteractive
DEBUGBAR_ENABLED=false
APP_URL=https://dev.toolbox.fluidbook.com
-HEADER_COLOR="#8C0025"
+HEADER_COLOR="#df4759"
LOG_CHANNEL=stack
APP_LOG=daily
use App\Models\FluidbookPublication;
use Carbon\Carbon;
use Carbon\CarbonInterface;
+use Cubist\Matomo\MatomoUtils;
use Cubist\Matomo\Reporting;
+use NumberFormatter;
class Stats extends Reporting
{
// ID >= 21210 (even numbers) = stats4.fluidbook.com
// ID >= 21211 (odd numbers) = stats5.fluidbook.com
$fluidbook_id = intval($id);
-
if ($fluidbook_id < 21210) {
return 'stats3.fluidbook.com';
} elseif ($fluidbook_id % 2 === 0) {
],
];
- $dates = $date ? $this->parseDate($date) : false;
+ $dates = $date ? MatomoUtils::parseDate($date) : false;
// Make sure that we receive a valid period from the URL
$period_override = in_array($period_override, array_keys($period_map)) ? $period_override : null;
}
- private function parseDate($date)
- {
- // Match possible date strings:
- // - YYYY
- // - YYYY-MM
- // - YYYY-MM-DD
- // - YYYY-MM-DD,YYYY-MM-DD
- // https://regex101.com/r/BLrqm0/1
- $regex = '/^(?<start_date>(?<start_year>2\d{3})-?(?<start_month>0[1-9]|1[012])?-?(?<start_day>0[1-9]|[12][0-9]|3[01])?),?(?<end_date>2\d{3}-(?>0[1-9]|1[012])-(?>0[1-9]|[12][0-9]|3[01]))?/';
-
- preg_match($regex, $date, $date_matches);
-
- extract($date_matches); // Just for easier access to match variables
- // Bail out on nonsensical dates
- if (isset($start_date) && isset($end_date) && ($start_date > $end_date)) {
- return false;
- }
-
- return $date_matches;
- }
// Since the report date can be in different formats (ie. YYYY, YYYY-MM or a full range)
// and this format determines the report mode (range, month or year), we have to figure out
// the mode and full starting and ending dates, as necessary
public function getReportSettings($date, $fluidbook)
{
- $date_matches = $this->parseDate($date);
+ $date_matches = MatomoUtils::parseDate($date);
// ...
$dates['end_date'] = now()->isoFormat('YYYY-MM-DD');
}
- // Extract array keys as variables for easier access:
- // $start_date, $start_year, $start_month, $start_day, $end_date
- extract($dates);
-
- // At this point, if the end_date still isn't set, it must either be a YYYY or YYYY-MM that was passed in.
- // Therefore, we can also assume that $start_year will be set. All that's left to do is set appropriate
- // start and end dates so that we can determine the best period based on the overall length of the range.
- if (!isset($end_date)) {
- if (isset($start_month) && !empty($start_month)) { // Month mode
- $start_date = "{$start_year}-{$start_month}-01";
- $end_date = Carbon::parse($start_date)->endOfMonth()->isoFormat('YYYY-MM-DD');
- } else { // Year mode
- $start_date = "{$start_year}-01-01";
- $end_date = Carbon::parse($start_date)->endOfYear()->isoFormat('YYYY-MM-DD');
- }
- }
-
- // Count <= 60 days: period = day
- // Count 61–180 days: period = week
- // Count 180+ days: period = month
- // Count 2+ years: period = year
- $day_count = Carbon::parse($start_date)->diffInDays($end_date);
-
- if ($day_count <= 60) {
- return 'day';
- } elseif ($day_count <= 180) {
- return 'week';
- } elseif ($day_count <= 730) {
- return 'month';
- }
- return 'year';
+ return MatomoUtils::getBestPeriod($dates);
}
}
->withoutMiddleware([CheckIfAdmin::class])
->name('stats'); // Named route is used to generate URLs more consistently using route helper
+
// Since the stats can sometimes be slow to generate, the initial URL displays
// a loader while the actual report is fetched via AJAX and injected into the page.
Route::get($segment . '/stats-data/{fluidbook_id}_{hash}/{date?}/{period_override?}', $controller . '@statsSummary')
->name('stats-report');
// API testing tool (intended for superadmins only)
- Route::get($segment . '/stats/API', $controller . '@statsAPI');
-
- // Shortcuts for easier access to hashed URLs - only users with sufficient permissions will be redirected
- Route::get($segment . '/stats/{fluidbook_id}', $controller . '@statsRedirect');
- Route::get($segment . '/{fluidbook_id}/stats', $controller . '@statsRedirect');
+ Route::get($segment . '/stats/API/{id?}', $controller . '@statsAPI');
}
protected function setupStatsDefaults()
$this->crud->addButtonFromView('line', 'stats', 'fluidbook_publication.stats', 'end');
}
- protected function redirectMatomo($fluidbook_id)
- {
- $url = 'https://' . Stats::getMatomoServer($fluidbook_id) . '/index.php?module=CoreHome&action=index&idSite=' . $fluidbook_id . '&period=day&date=yesterday';
- return redirect($url);
- }
-
- protected function getMatomoTokens()
- {
- // Each stats server has a different instance of Matamo, so we need to provide different API tokens for each
- // Normally this information would be stored in the .env but there's no good way to do that with an array, so
- // it is simpler to keep it here. These are also stored in the shared Bitwarden entry for Matomo.
- return [
- 'stats3.fluidbook.com' => '9df722a0bd30878ddc4d737352427502',
- 'stats4.fluidbook.com' => '3ffdbe052ae625f065573df9fa9515df',
- 'stats5.fluidbook.com' => '85e9cc307b6e5083249949e9472a80b8',
- 'stats6.fluidbook.com' => '16f4c1d77cdc4792b807718388db96a0',
- ];
- }
-
- protected function getMatomoServer($fluidbook_id)
- {
- // Get the appropriate server based on the Fluidbook ID
- // Stats are split across different servers depending on the ID:
- // ID < 21210 = stats3.fluidbook.com
- // ID >= 21210 (even numbers) = stats4.fluidbook.com
- // ID >= 21211 (odd numbers) = stats5.fluidbook.com
-
- $fluidbook_id = intval($fluidbook_id);
-
- if ($fluidbook_id < 21210) {
- return 'stats3.fluidbook.com';
- } elseif ($fluidbook_id % 2 === 0) {
- return 'stats4.fluidbook.com';
- }
- return 'stats5.fluidbook.com';
- }
-
- protected function getMatomoToken($server): bool|string
- {
- $tokens = $this->getMatomoTokens();
- return $tokens[$server] ?? false;
- }
-
- private function getReporting($fluidbook_id): Reporting
- {
- $server = $this->getMatomoServer($fluidbook_id);
- $token = $this->getMatomoToken($server);
- return new Reporting("https://{$server}/", $token, $fluidbook_id);
- }
-
- private function parseDate($date)
- {
- // Match possible date strings:
- // - YYYY
- // - YYYY-MM
- // - YYYY-MM-DD
- // - YYYY-MM-DD,YYYY-MM-DD
- // https://regex101.com/r/BLrqm0/1
- $regex = '/^(?<start_date>(?<start_year>2\d{3})-?(?<start_month>0[1-9]|1[012])?-?(?<start_day>0[1-9]|[12][0-9]|3[01])?),?(?<end_date>2\d{3}-(?>0[1-9]|1[012])-(?>0[1-9]|[12][0-9]|3[01]))?/';
-
- preg_match($regex, $date, $date_matches);
-
- extract($date_matches); // Just for easier access to match variables
-
- // Bail out on nonsensical dates
- if (isset($start_date) && isset($end_date) && ($start_date > $end_date)) {
- return false;
- }
-
- return $date_matches;
- }
-
- // Since the report date can be in different formats (ie. YYYY, YYYY-MM or a full range)
- // and this format determines the report mode (range, month or year), we have to figure out
- // the mode and full starting and ending dates, as necessary
- public function getReportSettings($date, $fluidbook)
- {
- $date_matches = $this->parseDate($date);
-
- // ...
-
- // TODO: centralise all mode + start / end date logic here... then in the main report function, we can use the returned mode in a switch() statement to clean things up a lot.
-
-
- }
-
- // Figure out the best period to use for the stats according to the date range
- public function determinePeriod($dates, $fluidbook)
- {
-
- // TODO: refactor this into getReportSettings() so that this function can simply take the determined start and end dates, calculate the number of days and give an answer based on that. No extra logic to try to figure out which mode it is...
-
- // If no valid date range is given, use the Fluidbook's lifetime
- if (empty($dates) || !is_array($dates)) {
- $dates['start_date'] = $fluidbook->created_at->isoFormat('YYYY-MM-DD');
- $dates['end_date'] = now()->isoFormat('YYYY-MM-DD');
- }
-
- // Extract array keys as variables for easier access:
- // $start_date, $start_year, $start_month, $start_day, $end_date
- extract($dates);
-
- // At this point, if the end_date still isn't set, it must either be a YYYY or YYYY-MM that was passed in.
- // Therefore, we can also assume that $start_year will be set. All that's left to do is set appropriate
- // start and end dates so that we can determine the best period based on the overall length of the range.
- if (!isset($end_date)) {
- if (isset($start_month) && !empty($start_month)) { // Month mode
- $start_date = "{$start_year}-{$start_month}-01";
- $end_date = Carbon::parse($start_date)->endOfMonth()->isoFormat('YYYY-MM-DD');
- } else { // Year mode
- $start_date = "{$start_year}-01-01";
- $end_date = Carbon::parse($start_date)->endOfYear()->isoFormat('YYYY-MM-DD');
- }
- }
-
- // Count <= 60 days: period = day
- // Count 61–180 days: period = week
- // Count 180+ days: period = month
- // Count 2+ years: period = year
- $day_count = Carbon::parse($start_date)->diffInDays($end_date);
-
- if ($day_count <= 60) {
- return 'day';
- } elseif ($day_count <= 180) {
- return 'week';
- } elseif ($day_count <= 730) {
- return 'month';
- }
-
- return 'year';
- }
protected function statsLoader($fluidbook_id, $hash, $date = null, $period_override = null)
{
protected function statsSummary($fluidbook_id, $hash, $date = null, $period_override = null)
{
+ $fluidbook = FluidbookPublication::withoutGlobalScopes()->where('id', $fluidbook_id)->where('hash', $hash)->firstOrFail();
$locale = app()->getLocale();
$base_URL = route('stats', compact('fluidbook_id', 'hash')); // Used by date range picker to update report URL
- // If the Fluidbook ID + hash combination isn't found, a 404 response will be returned
- $fluidbook = FluidbookPublication::where('id', $fluidbook_id)->where('hash', $hash)->firstOrFail();
$fluidbook_settings = json_decode($fluidbook->settings);
// The page count setting is sometimes missing for older Fluidbooks
$formatted_date_range = $this->formatDateRange($start_date, $end_date);
//=== Set up Matomo Reporting API
- $report = $this->getReporting($fluidbook_id);
- $report->setDate($date_range);
- $report->setPeriod($period);
- $report->setLanguage($locale); // Matomo will return translated data when relevant (eg. country names)
+ $report=new Stats($fluidbook->id);
+
// * Note about visits and page views:
// Although Matomo records visits and page views (API: VisitsSummary.get & Actions.get), we don't use these
$expanded_stats = $fluidbook_id >= $new_stats_cutoff ? 1 : 0; // Only fetch extra data when it will be used
$pagesByPeriod = collect($report->getPageUrls(['expanded' => $expanded_stats]))
->map(function ($item, $date) use ($period, $fluidbook_id, $hash, $eventsByPeriod, $new_stats_cutoff) {
-
if (empty($item)) {
return $item; // Some periods might have no data
}
}
- // Redirect users with sufficient permissions to the full hashed URL
- // This was added as a convenience when quickly switching between stats views for Fluidbooks
- // If the current user has sufficient permissions, they will be redirected to the hashed URL.
- // If not, the FluidbookPublication lookup will fail and further execution will halt.
- protected function statsRedirect($fluidbook_id)
+ protected function redirectMatomo($fluidbook_id)
{
- $fluidbook = FluidbookPublication::where('id', $fluidbook_id)->firstOrFail();
-
- $settings = json_decode($fluidbook->settings);
- $hash = $fluidbook->hash;
-
- if (!$fluidbook->stats) {
- return "Statistics are disabled for this Fluidbook #{$fluidbook_id} ({$settings->title})";
- }
-
- return redirect()->route('stats', compact('fluidbook_id', 'hash'));
+ $url = 'https://' . Stats::getMatomoServer($fluidbook_id) . '/index.php?module=CoreHome&action=index&idSite=' . $fluidbook_id . '&period=day&date=yesterday';
+ return redirect($url);
}
// https://toolbox.fluidbook.com/fluidbook-publication/stats/API
- protected function statsAPI()
+ protected function statsAPI($id = null)
{
if (!can('superadmin')) {
}
$matomo_tokens = json_encode($this->getMatomoTokens());
- return view('fluidbook_stats.API', compact('matomo_tokens'));
+ return view('fluidbook_stats.API', compact('matomo_tokens', 'id'));
}
// Format a date range for display in the interface
"source": {
"type": "git",
"url": "git://git.cubedesigners.com/cubedesigners_userdatabase.git",
- "reference": "1310f7a85c4741dfc22a6212bfb788ee0fa99910"
+ "reference": "5217abc590c7a7d5a47dacc0af15a0eeae5790ee"
},
"dist": {
"type": "tar",
- "url": "https://composer.cubedesigners.com/dist/cubedesigners/userdatabase/cubedesigners-userdatabase-dev-backpack5-f9c8c7.tar",
- "reference": "1310f7a85c4741dfc22a6212bfb788ee0fa99910",
- "shasum": "3e0550a4c23c7e2d4257a13d4fd8b2edafc22f9a"
+ "url": "https://composer.cubedesigners.com/dist/cubedesigners/userdatabase/cubedesigners-userdatabase-dev-backpack5-5641b0.tar",
+ "reference": "5217abc590c7a7d5a47dacc0af15a0eeae5790ee",
+ "shasum": "7f4e97289a746086a2831a5d663395396746155c"
},
"require": {
"cubist/cms-back": "dev-backpack5",
}
],
"description": "Cubedesigners common users database",
- "time": "2023-04-30T12:44:03+00:00"
+ "time": "2023-05-24T08:34:53+00:00"
},
{
"name": "cubist/azuretts",
"source": {
"type": "git",
"url": "git://git.cubedesigners.com/cubist_cms-back.git",
- "reference": "1adf6232baf5b6567b72a2a537777bc41dc7674a"
+ "reference": "662c4a98089d04b66a520165f631cf345008a706"
},
"dist": {
"type": "tar",
- "url": "https://composer.cubedesigners.com/dist/cubist/cms-back/cubist-cms-back-dev-backpack5-f19d9a.tar",
- "reference": "1adf6232baf5b6567b72a2a537777bc41dc7674a",
- "shasum": "c1cdaf19cd977610cc9ed971b0f852c90bb1b0fb"
+ "url": "https://composer.cubedesigners.com/dist/cubist/cms-back/cubist-cms-back-dev-backpack5-8e16bf.tar",
+ "reference": "662c4a98089d04b66a520165f631cf345008a706",
+ "shasum": "3f6a221290cb172e1568aac8b6213ede11dae019"
},
"require": {
"backpack/backupmanager": "^v3.0.9",
}
],
"description": "Cubist Backpack extension",
- "time": "2023-05-10T17:43:22+00:00"
+ "time": "2023-05-24T16:11:13+00:00"
},
{
"name": "cubist/cms-front",
"source": {
"type": "git",
"url": "git://git.cubedesigners.com/cubist_matomo.git",
- "reference": "16b89dc9bd52116a343a0ef120311e06c0f3112f"
+ "reference": "f9543e92e7fe8a59a21211ef807936b48222bbc1"
},
"dist": {
"type": "tar",
- "url": "https://composer.cubedesigners.com/dist/cubist/matomo/cubist-matomo-dev-master-484885.tar",
- "reference": "16b89dc9bd52116a343a0ef120311e06c0f3112f",
- "shasum": "c8855c22c5d8a21bbdcdbcfd0c12bcbfea86544b"
+ "url": "https://composer.cubedesigners.com/dist/cubist/matomo/cubist-matomo-dev-master-868e71.tar",
+ "reference": "f9543e92e7fe8a59a21211ef807936b48222bbc1",
+ "shasum": "ac520f2a8d3ab9dca227d3d2a540cc4edde30adc"
},
"require": {
"ext-json": "*",
- "guzzlehttp/guzzle": "^7.4.1",
+ "guzzlehttp/guzzle": "^7.7",
+ "illuminate/cache": ">=5",
"php": ">=7.2"
},
"default-branch": true,
}
],
"description": "Matomo API",
- "time": "2022-09-21T16:20:01+00:00"
+ "time": "2023-05-24T17:00:51+00:00"
},
{
"name": "cubist/net",
"source": {
"type": "git",
"url": "git://git.cubedesigners.com/fluidbook_tools.git",
- "reference": "67ba5e732fc3caa72bce6f0315595c4f05a6790c"
+ "reference": "c858c61c414027bf3d62ffc642f620623f492417"
},
"dist": {
"type": "tar",
- "url": "https://composer.cubedesigners.com/dist/fluidbook/tools/fluidbook-tools-dev-master-94943c.tar",
- "reference": "67ba5e732fc3caa72bce6f0315595c4f05a6790c",
- "shasum": "6a4aceb11c237d34cbc990b489c20bfb8835b403"
+ "url": "https://composer.cubedesigners.com/dist/fluidbook/tools/fluidbook-tools-dev-master-f43473.tar",
+ "reference": "c858c61c414027bf3d62ffc642f620623f492417",
+ "shasum": "ad755be6d01cc2e45a6dfaca90a772f614b66611"
},
"require": {
"barryvdh/laravel-debugbar": "*",
}
],
"description": "Fluidbook Tools",
- "time": "2023-05-22T13:30:55+00:00"
+ "time": "2023-05-23T17:58:08+00:00"
},
{
"name": "fruitcake/php-cors",
},
{
"name": "laravel/framework",
- "version": "v10.11.0",
+ "version": "v10.12.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "21a5b6d9b669f32c10cc8ba776511b5f62599fea"
+ "reference": "9e6dcff23ab1d4b522bef56074c31625cf077576"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/21a5b6d9b669f32c10cc8ba776511b5f62599fea",
- "reference": "21a5b6d9b669f32c10cc8ba776511b5f62599fea",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/9e6dcff23ab1d4b522bef56074c31625cf077576",
+ "reference": "9e6dcff23ab1d4b522bef56074c31625cf077576",
"shasum": ""
},
"require": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2023-05-16T13:59:23+00:00"
+ "time": "2023-05-23T18:04:16+00:00"
},
{
"name": "laravel/serializable-closure",
},
{
"name": "psy/psysh",
- "version": "v0.11.17",
+ "version": "v0.11.18",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "3dc5d4018dabd80bceb8fe1e3191ba8460569f0a"
+ "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3dc5d4018dabd80bceb8fe1e3191ba8460569f0a",
- "reference": "3dc5d4018dabd80bceb8fe1e3191ba8460569f0a",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4f00ee9e236fa6a48f4560d1300b9c961a70a7ec",
+ "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec",
"shasum": ""
},
"require": {
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
- "source": "https://github.com/bobthecow/psysh/tree/v0.11.17"
+ "source": "https://github.com/bobthecow/psysh/tree/v0.11.18"
},
- "time": "2023-05-05T20:02:42+00:00"
+ "time": "2023-05-23T02:31:11+00:00"
},
{
"name": "sebastian/cli-parser",
</template>
</select>
</label>
- <input type="submit" value="GO" class="bg-green-500 text-white py-1 px-4 rounded" :class="method !== '' || 'cursor-not-allowed opacity-60'">
- <input type="button" value="Clear" class="bg-red-500 text-white py-1 px-4 rounded" @click="$refs.result.innerHTML = ''">
+ <input type="submit" value="GO" class="bg-green-500 text-white py-1 px-4 rounded"
+ :class="method !== '' || 'cursor-not-allowed opacity-60'">
+ <input type="button" value="Clear" class="bg-red-500 text-white py-1 px-4 rounded"
+ @click="$refs.result.innerHTML = ''">
</div>
</form>
</div>
<script>
- function matomo() {
- return {
- servers: {!! $matomo_tokens; !!},
- periods: ['day', 'week', 'month', 'year', 'range'],
- siteID: 16976,
- method: '',
- period: 'month',
- date: '{{ date('Y') }}-01-01,today',
+ function matomo() {
+ return {
+ servers: {!! $matomo_tokens; !!},
+ periods: ['day', 'week', 'month', 'year', 'range'],
+ siteID: {{$id?:15407}},
+ method: '',
+ period: 'month',
+ date: '{{ date('Y') }}-01-01,today',
get server() {
if (!this.siteID) return false;
table.stats-details {
width: 100%;
+ table-layout: fixed;
}
table.stats-details th, table.stats-details td {
padding: 0.5em 0.75em;
arrow.className = 'arrow-right';
rangeInputs.insertBefore(arrow, rangeInputs.children[1]); // Insert in 2nd position
+@can('fluidbook-publication:admin')
+ <div style="text-align: right;margin-top: 20px;">
+ <a target="_blank"
+ href="{{route('statsmatomo',['fluidbook_id'=>$fluidbook->id,'hash'=>$fluidbook->hash])}}">{{__('Voir les données brutes sur Matomo')}}</a>
+ @can('superadmin')
+ | <a target="_blank" href="/fluidbook-publication/stats/API/{{$fluidbook->id}}">{{__('API Matomo')}}</a>
+ @endcan
+ </div>
+@endcan
+
</script>