32
common/Admin/AdminSetupAlertsController.php
Executable file
32
common/Admin/AdminSetupAlertsController.php
Executable file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Common\Logging\Schedule\ScheduleLogItem;
|
||||
|
||||
class AdminSetupAlertsController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('isAdmin');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$alerts = [];
|
||||
|
||||
if (!ScheduleLogItem::scheduleRanInLast30Minutes()) {
|
||||
$alerts[] = [
|
||||
'id' => 'cronNotSetup',
|
||||
'title' => 'There is an issue with CRON schedule',
|
||||
'description' =>
|
||||
'The CRON schedule has not run in the last 30 minutes. If you did not set it up yet, see the documentation <a class="underline font-semibold" target="_blank" href="https://support.vebto.com/hc/articles/21/23/169/automated-tasks-cron-jobs">here</a>.',
|
||||
];
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'alerts' => $alerts,
|
||||
]);
|
||||
}
|
||||
}
|
||||
12
common/Admin/Analytics/Actions/BuildAnalyticsReport.php
Executable file
12
common/Admin/Analytics/Actions/BuildAnalyticsReport.php
Executable file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Analytics\Actions;
|
||||
|
||||
interface BuildAnalyticsReport
|
||||
{
|
||||
/**
|
||||
* Get data for admin area analytics page from active provider.
|
||||
* (Demo or Google Analytics currently)
|
||||
*/
|
||||
public function execute(array $params): array;
|
||||
}
|
||||
136
common/Admin/Analytics/Actions/BuildDemoAnalyticsReport.php
Executable file
136
common/Admin/Analytics/Actions/BuildDemoAnalyticsReport.php
Executable file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Analytics\Actions;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Database\Metrics\MetricDateRange;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class BuildDemoAnalyticsReport implements BuildAnalyticsReport
|
||||
{
|
||||
protected MetricDateRange $dateRange;
|
||||
|
||||
public function execute(array $params): array
|
||||
{
|
||||
$this->dateRange = $params['dateRange'];
|
||||
|
||||
return [
|
||||
'pageViews' => $this->buildPageviewsMetric(),
|
||||
'browsers' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildBrowsersMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'locations' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildLocationsMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'devices' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildDevicesMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'platforms' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildPlatformsMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildPageviewsMetric(): array
|
||||
{
|
||||
$current = (new DemoTrend(
|
||||
User::query(),
|
||||
dateRange: $this->dateRange,
|
||||
))->count();
|
||||
|
||||
$previous = (new DemoTrend(
|
||||
User::query(),
|
||||
dateRange: $this->dateRange,
|
||||
))->count();
|
||||
|
||||
return [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'total' => array_sum(Arr::pluck($current, 'value')),
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Current period'),
|
||||
'data' => $current,
|
||||
],
|
||||
[
|
||||
'label' => __('Previous period'),
|
||||
'data' => $previous,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildBrowsersMetric(): array
|
||||
{
|
||||
return [
|
||||
['label' => 'Chrome', 'value' => random_int(300, 500)],
|
||||
['label' => 'Firefox', 'value' => random_int(200, 400)],
|
||||
['label' => 'IE', 'value' => random_int(100, 150)],
|
||||
['label' => 'Edge', 'value' => random_int(100, 200)],
|
||||
['label' => 'Safari', 'value' => random_int(200, 300)],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildDevicesMetric(): array
|
||||
{
|
||||
return [
|
||||
['label' => 'Mobile', 'value' => random_int(300, 500)],
|
||||
['label' => 'Tablet', 'value' => random_int(200, 400)],
|
||||
['label' => 'Desktop', 'value' => random_int(100, 150)],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildPlatformsMetric(): array
|
||||
{
|
||||
return [
|
||||
['label' => 'Windows', 'value' => random_int(300, 500)],
|
||||
['label' => 'Linux', 'value' => random_int(200, 400)],
|
||||
['label' => 'iOS', 'value' => random_int(100, 150)],
|
||||
['label' => 'Android', 'value' => random_int(100, 150)],
|
||||
];
|
||||
}
|
||||
|
||||
public function buildLocationsMetric(): array
|
||||
{
|
||||
$data = [
|
||||
['label' => 'United States', 'code' => 'US', 'value' => random_int(300, 500)],
|
||||
['label' => 'India', 'code' => 'IN', 'value' => random_int(100, 300)],
|
||||
['label' => 'Russia', 'code' => 'RU', 'value' => random_int(250, 400)],
|
||||
['label' => 'Germany', 'code' => 'DE', 'value' => random_int(200, 500)],
|
||||
['label' => 'France', 'code' => 'FR', 'value' => random_int(150, 300)],
|
||||
['label' => 'Japan', 'code' => 'JP', 'value' => random_int(150, 300)],
|
||||
['label' => 'United Kingdom', 'code' => 'GB', 'value' => random_int(300, 400)],
|
||||
['label' => 'Canada', 'code' => 'CA', 'value' => random_int(100, 150)],
|
||||
];
|
||||
|
||||
$total = array_sum(Arr::pluck($data, 'value'));
|
||||
|
||||
return array_map(function($item) use($total) {
|
||||
$item['percentage'] = round($item['value'] / $total * 100, 2);
|
||||
return $item;
|
||||
}, $data);
|
||||
}
|
||||
}
|
||||
331
common/Admin/Analytics/Actions/BuildGoogleAnalyticsReport.php
Executable file
331
common/Admin/Analytics/Actions/BuildGoogleAnalyticsReport.php
Executable file
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Analytics\Actions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Common\Database\Metrics\MetricDateRange;
|
||||
use Common\Database\Metrics\Traits\GeneratesTrendResults;
|
||||
use Google\Analytics\Data\V1beta\BetaAnalyticsDataClient;
|
||||
use Google\Analytics\Data\V1beta\DateRange;
|
||||
use Google\Analytics\Data\V1beta\Dimension;
|
||||
use Google\Analytics\Data\V1beta\Metric;
|
||||
use Google\Analytics\Data\V1beta\OrderBy;
|
||||
use Google\Analytics\Data\V1beta\OrderBy\DimensionOrderBy;
|
||||
use Google\Analytics\Data\V1beta\OrderBy\MetricOrderBy;
|
||||
use Google\Analytics\Data\V1beta\Row;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class BuildGoogleAnalyticsReport implements BuildAnalyticsReport
|
||||
{
|
||||
use GeneratesTrendResults;
|
||||
|
||||
protected BetaAnalyticsDataClient $client;
|
||||
protected MetricDateRange $dateRange;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new BetaAnalyticsDataClient([
|
||||
'credentials' => storage_path('laravel-analytics/certificate.json'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function execute(array $params = []): array
|
||||
{
|
||||
$this->dateRange = $params['dateRange'] ?? new MetricDateRange();
|
||||
|
||||
return $this->buildReport();
|
||||
}
|
||||
|
||||
protected function buildReport(): array
|
||||
{
|
||||
return [
|
||||
'pageViews' => [
|
||||
'datasets' => $this->buildPageViewsMetric(),
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
],
|
||||
'browsers' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildTopBrowsersMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'locations' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildTopLocationsMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'devices' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildTopDevicesMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
'platforms' => [
|
||||
'granularity' => $this->dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => $this->buildTopPlatformsMetric(),
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function buildTopBrowsersMetric(int $maxResults = 10): Collection
|
||||
{
|
||||
$rows = $this->performQuery($this->dateRange, [
|
||||
'dimensions' => ['browser'],
|
||||
'metrics' => ['sessions'],
|
||||
'sort' => ['direction' => 'desc', 'metric' => 'sessions'],
|
||||
]);
|
||||
|
||||
$topBrowsers = $rows->map(function (Row $row) {
|
||||
return [
|
||||
'label' => $row->getDimensionValues()[0]->getValue(),
|
||||
'value' => $row->getMetricValues()[0]->getValue(),
|
||||
];
|
||||
});
|
||||
|
||||
if ($topBrowsers->count() <= $maxResults) {
|
||||
return $topBrowsers;
|
||||
}
|
||||
|
||||
return $topBrowsers->take($maxResults - 1)->push([
|
||||
'label' => __('Others'),
|
||||
'value' => $topBrowsers->splice($maxResults - 1)->sum('value'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildTopLocationsMetric(): Collection
|
||||
{
|
||||
$maxResults = 6;
|
||||
$rows = $this->performQuery($this->dateRange, [
|
||||
'metrics' => ['sessions'],
|
||||
'dimensions' => ['country'],
|
||||
'sort' => [
|
||||
'direction' => 'desc',
|
||||
'metric' => 'sessions',
|
||||
],
|
||||
]);
|
||||
|
||||
$locations = $rows->map(function (Row $row) {
|
||||
return [
|
||||
'label' => $row->getDimensionValues()[0]->getValue(),
|
||||
'value' => $row->getMetricValues()[0]->getValue(),
|
||||
];
|
||||
});
|
||||
$total = $locations->sum('value');
|
||||
|
||||
$locations = $locations->map(function ($location) use ($total) {
|
||||
$location['percentage'] = round(
|
||||
(100 * $location['value']) / $total,
|
||||
1,
|
||||
);
|
||||
return $location;
|
||||
});
|
||||
|
||||
if ($locations->count() <= $maxResults) {
|
||||
return $locations;
|
||||
}
|
||||
|
||||
return $locations->take($maxResults - 1)->push([
|
||||
'label' => __('Other'),
|
||||
'value' => $locations->splice($maxResults - 1)->sum('value'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function buildTopDevicesMetric(int $maxResults = 10): Collection
|
||||
{
|
||||
$rows = $this->performQuery($this->dateRange, [
|
||||
'dimensions' => ['deviceCategory'],
|
||||
'metrics' => ['sessions'],
|
||||
'sort' => ['direction' => 'desc', 'metric' => 'sessions'],
|
||||
]);
|
||||
|
||||
$devices = $rows->map(function (Row $row) {
|
||||
return [
|
||||
'label' => __(
|
||||
ucfirst($row->getDimensionValues()[0]->getValue()),
|
||||
),
|
||||
'value' => $row->getMetricValues()[0]->getValue(),
|
||||
];
|
||||
});
|
||||
|
||||
if ($devices->count() <= $maxResults) {
|
||||
return $devices;
|
||||
}
|
||||
|
||||
return $devices->take($maxResults - 1)->push([
|
||||
'label' => __('Others'),
|
||||
'value' => $devices->splice($maxResults - 1)->sum('value'),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function buildTopPlatformsMetric(int $maxResults = 10): Collection
|
||||
{
|
||||
$rows = $this->performQuery($this->dateRange, [
|
||||
'dimensions' => ['operatingSystem'],
|
||||
'metrics' => ['sessions'],
|
||||
'sort' => ['direction' => 'desc', 'metric' => 'sessions'],
|
||||
]);
|
||||
|
||||
$platforms = $rows->map(
|
||||
fn(Row $row) => [
|
||||
'label' => $row->getDimensionValues()[0]->getValue(),
|
||||
'value' => $row->getMetricValues()[0]->getValue(),
|
||||
],
|
||||
);
|
||||
|
||||
if ($platforms->count() <= $maxResults) {
|
||||
return $platforms;
|
||||
}
|
||||
|
||||
return $platforms->take($maxResults - 1)->push([
|
||||
'label' => __('Others'),
|
||||
'value' => $platforms->splice($maxResults - 1)->sum('value'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildPageViewsMetric(): array
|
||||
{
|
||||
$this->getPageViews($this->dateRange->getPreviousPeriod());
|
||||
return [
|
||||
[
|
||||
'label' => __('Current period'),
|
||||
'data' => $this->getPageViews($this->dateRange),
|
||||
],
|
||||
[
|
||||
'label' => __('Previous period'),
|
||||
'data' => $this->getPageViews(
|
||||
$this->dateRange->getPreviousPeriod(),
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function getPageViews(MetricDateRange $dateRange): Collection
|
||||
{
|
||||
$format = $this->dateRange->getGroupingFormat();
|
||||
|
||||
$results = $this->fetchVisitorsAndPageViews($dateRange)
|
||||
// Google Analytics will return views at one minute granularity always.
|
||||
// Group these by date format based on selected granularity instead.
|
||||
// e.g. "day" granularity will equal "YYYY-MM-DD" => ["value" => 1]
|
||||
->groupBy(fn($item) => $item['date']->format($format))
|
||||
// reduce each granularity group to a single value
|
||||
->map(function (Collection $dateGroup) {
|
||||
return $dateGroup->reduce(
|
||||
function ($result, $item) {
|
||||
$result['value'] += $item['value'];
|
||||
return $result;
|
||||
},
|
||||
[
|
||||
'date' => $dateGroup[0]['date'],
|
||||
'value' => 0,
|
||||
],
|
||||
);
|
||||
})
|
||||
->mapWithKeys(
|
||||
fn($item) => [
|
||||
$item['date']->format($format) => $this->formatTrendResult(
|
||||
$dateRange->granularity,
|
||||
$item['date'],
|
||||
$item['value'],
|
||||
),
|
||||
],
|
||||
)
|
||||
->sortKeys()
|
||||
->all();
|
||||
|
||||
$mergedResults = array_replace(
|
||||
$this->getAllPossibleDateResults($dateRange),
|
||||
$results,
|
||||
);
|
||||
|
||||
return collect($mergedResults)->values();
|
||||
}
|
||||
|
||||
protected function fetchVisitorsAndPageViews(
|
||||
MetricDateRange $dateRange,
|
||||
): Collection {
|
||||
$rows = $this->performQuery($dateRange, [
|
||||
'dimensions' => ['dateHourMinute', 'pageTitle'], // YYYYMMDDHHMM
|
||||
'metrics' => ['totalUsers', 'screenPageViews'],
|
||||
]);
|
||||
|
||||
return $rows->map(function (Row $dateRow) {
|
||||
return [
|
||||
'date' => Carbon::createFromFormat(
|
||||
'YmdHi',
|
||||
$dateRow->getDimensionValues()[0]->getValue(),
|
||||
),
|
||||
'pageTitle' => $dateRow->getDimensionValues()[1]->getValue(),
|
||||
'visitors' => (int) $dateRow->getMetricValues()[0]->getValue(),
|
||||
'value' => (int) $dateRow->getMetricValues()[1]->getValue(),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
protected function performQuery(
|
||||
MetricDateRange $dateRange,
|
||||
array $options,
|
||||
): Collection {
|
||||
$orderBy = null;
|
||||
if (isset($options['sort'])) {
|
||||
$sortOptions = [];
|
||||
if (Arr::get($options, 'sort.direction') == 'desc') {
|
||||
$sortOptions['desc'] = true;
|
||||
}
|
||||
|
||||
if ($metricName = Arr::get($options, 'sort.metric')) {
|
||||
$sortOptions['metric'] = (new MetricOrderBy())->setMetricName(
|
||||
$metricName,
|
||||
);
|
||||
}
|
||||
|
||||
if ($dimensionName = Arr::get($options, 'sort.dimensions')) {
|
||||
$sortOptions[
|
||||
'dimension'
|
||||
] = (new DimensionOrderBy())->setDimensionName($dimensionName);
|
||||
}
|
||||
|
||||
$orderBy = new OrderBy($sortOptions);
|
||||
}
|
||||
|
||||
$propertyId = config('services.google.analytics_property_id');
|
||||
$response = $this->client->runReport([
|
||||
'property' => "properties/$propertyId",
|
||||
'dateRanges' => [$this->getGoogleDateRange($dateRange)],
|
||||
'dimensions' => array_map(
|
||||
fn($name) => new Dimension(['name' => $name]),
|
||||
$options['dimensions'],
|
||||
),
|
||||
'metrics' => array_map(
|
||||
fn($name) => new Metric(['name' => $name]),
|
||||
$options['metrics'],
|
||||
),
|
||||
'orderBys' => $orderBy ? [$orderBy] : null,
|
||||
]);
|
||||
|
||||
return collect($response->getRows() ?: []);
|
||||
}
|
||||
|
||||
protected function getGoogleDateRange(MetricDateRange $range): DateRange
|
||||
{
|
||||
return (new DateRange())
|
||||
->setStartDate($range->start->toDateString())
|
||||
->setEndDate($range->end->toDateString());
|
||||
}
|
||||
}
|
||||
63
common/Admin/Analytics/Actions/BuildNullAnalyticsReport.php
Executable file
63
common/Admin/Analytics/Actions/BuildNullAnalyticsReport.php
Executable file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Analytics\Actions;
|
||||
|
||||
class BuildNullAnalyticsReport implements BuildAnalyticsReport
|
||||
{
|
||||
public function execute(array $params): array
|
||||
{
|
||||
$dateRange = $params['dateRange'];
|
||||
|
||||
return [
|
||||
'pageViews' => [
|
||||
'granularity' => $dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Current period'),
|
||||
'data' => [],
|
||||
],
|
||||
[
|
||||
'label' => __('Previous period'),
|
||||
'data' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'browsers' => [
|
||||
'granularity' => $dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'locations' => [
|
||||
'granularity' => $dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'devices' => [
|
||||
'granularity' => $dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
'platforms' => [
|
||||
'granularity' => $dateRange->granularity,
|
||||
'datasets' => [
|
||||
[
|
||||
'label' => __('Sessions'),
|
||||
'data' => [],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
18
common/Admin/Analytics/Actions/DemoTrend.php
Executable file
18
common/Admin/Analytics/Actions/DemoTrend.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Analytics\Actions;
|
||||
|
||||
use Common\Database\Metrics\Trend;
|
||||
|
||||
class DemoTrend extends Trend
|
||||
{
|
||||
protected function aggregate(string $function): array
|
||||
{
|
||||
$data = array_map(function($item) {
|
||||
$item['value'] = random_int(100, 500);
|
||||
return $item;
|
||||
}, $this->getAllPossibleDateResults($this->dateRange));
|
||||
|
||||
return array_values($data);
|
||||
}
|
||||
}
|
||||
11
common/Admin/Analytics/Actions/GetAnalyticsHeaderDataAction.php
Executable file
11
common/Admin/Analytics/Actions/GetAnalyticsHeaderDataAction.php
Executable file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Analytics\Actions;
|
||||
|
||||
interface GetAnalyticsHeaderDataAction
|
||||
{
|
||||
/**
|
||||
* Get analytics header data.
|
||||
*/
|
||||
public function execute(array $params): array;
|
||||
}
|
||||
72
common/Admin/Analytics/AnalyticsController.php
Executable file
72
common/Admin/Analytics/AnalyticsController.php
Executable file
@@ -0,0 +1,72 @@
|
||||
<?php namespace Common\Admin\Analytics;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Common\Admin\Analytics\Actions\BuildAnalyticsReport;
|
||||
use Common\Admin\Analytics\Actions\BuildNullAnalyticsReport;
|
||||
use Common\Admin\Analytics\Actions\GetAnalyticsHeaderDataAction;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Metrics\MetricDateRange;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class AnalyticsController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
protected BuildAnalyticsReport $getDataAction,
|
||||
protected GetAnalyticsHeaderDataAction $getHeaderDataAction,
|
||||
) {
|
||||
}
|
||||
|
||||
public function report()
|
||||
{
|
||||
$this->authorize('index', 'ReportPolicy');
|
||||
|
||||
$types = explode(',', $this->request->get('types', 'visitors,header'));
|
||||
$dateRange = $this->getDateRange();
|
||||
$cacheKey = sprintf(
|
||||
'%s-%s',
|
||||
$dateRange->getCacheKey(),
|
||||
implode(',', $types),
|
||||
);
|
||||
|
||||
$response = [];
|
||||
$reportParams = ['dateRange' => $dateRange];
|
||||
if (in_array('visitors', $types)) {
|
||||
try {
|
||||
$response['visitorsReport'] = Cache::remember(
|
||||
"adminReport.main.$cacheKey",
|
||||
CarbonImmutable::now()->addDay(),
|
||||
fn() => $this->getDataAction->execute($reportParams),
|
||||
);
|
||||
} catch (Exception $e) {
|
||||
$response['visitorsReport'] = app(
|
||||
BuildNullAnalyticsReport::class,
|
||||
)->execute($reportParams);
|
||||
}
|
||||
}
|
||||
if (in_array('header', $types)) {
|
||||
$response['headerReport'] = Cache::remember(
|
||||
"adminReport.header.$cacheKey",
|
||||
CarbonImmutable::now()->addDay(),
|
||||
fn() => $this->getHeaderDataAction->execute($reportParams),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->success($response);
|
||||
}
|
||||
|
||||
protected function getDateRange(): MetricDateRange
|
||||
{
|
||||
$startDate = $this->request->get('startDate');
|
||||
$endDate = $this->request->get('endDate');
|
||||
$timezone = $this->request->get('timezone', config('app.timezone'));
|
||||
|
||||
return new MetricDateRange(
|
||||
start: $startDate,
|
||||
end: $endDate,
|
||||
timezone: $timezone,
|
||||
);
|
||||
}
|
||||
}
|
||||
37
common/Admin/Analytics/AnalyticsServiceProvider.php
Executable file
37
common/Admin/Analytics/AnalyticsServiceProvider.php
Executable file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Analytics;
|
||||
|
||||
use Common\Admin\Analytics\Actions\BuildAnalyticsReport;
|
||||
use Common\Admin\Analytics\Actions\BuildDemoAnalyticsReport;
|
||||
use Common\Admin\Analytics\Actions\BuildGoogleAnalyticsReport;
|
||||
use Common\Admin\Analytics\Actions\BuildNullAnalyticsReport;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AnalyticsServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register bindings in the container.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(BuildAnalyticsReport::class, function () {
|
||||
if (config('common.site.demo')) {
|
||||
return new BuildDemoAnalyticsReport();
|
||||
} else {
|
||||
return $this->getGoogleAnalyticsData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function getGoogleAnalyticsData()
|
||||
{
|
||||
try {
|
||||
return new BuildGoogleAnalyticsReport();
|
||||
} catch (\Exception $e) {
|
||||
return new BuildNullAnalyticsReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
97
common/Admin/Appearance/AppearanceSaver.php
Executable file
97
common/Admin/Appearance/AppearanceSaver.php
Executable file
@@ -0,0 +1,97 @@
|
||||
<?php namespace Common\Admin\Appearance;
|
||||
|
||||
use Common\Admin\Appearance\Themes\CssTheme;
|
||||
use Common\Settings\DotEnvEditor;
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class AppearanceSaver
|
||||
{
|
||||
const CUSTOM_CSS_PATH = 'custom-code/custom-styles.css';
|
||||
const CUSTOM_HTML_PATH = 'custom-code/custom-html.html';
|
||||
|
||||
public function __construct(
|
||||
protected Settings $settings,
|
||||
protected DotEnvEditor $envEditor,
|
||||
) {
|
||||
}
|
||||
|
||||
public function save(array $values): void
|
||||
{
|
||||
foreach ($values['appearance'] as $groupName => $groupValues) {
|
||||
if ($groupName === 'env') {
|
||||
$this->saveEnvSettings($groupValues);
|
||||
} elseif ($groupName === 'custom_code') {
|
||||
$this->saveCustomCode($groupValues);
|
||||
} elseif ($groupName === 'themes') {
|
||||
$this->syncThemes($groupValues['all']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($values['settings'])) {
|
||||
//generate and store favicon
|
||||
if (isset($values['settings']['branding']['favicon'])) {
|
||||
$path = $values['settings']['branding']['favicon'];
|
||||
unset($values['settings']['branding']['favicon']);
|
||||
app(GenerateFavicon::class)->execute($path);
|
||||
}
|
||||
|
||||
$this->settings->save($values['settings']);
|
||||
}
|
||||
}
|
||||
|
||||
private function saveEnvSettings(array $settings): void
|
||||
{
|
||||
foreach ($settings as $key => $value) {
|
||||
$this->envEditor->write([
|
||||
$key => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function saveCustomCode(array $settings): void
|
||||
{
|
||||
foreach ($settings as $key => $value) {
|
||||
$path =
|
||||
$key === 'css' ? self::CUSTOM_CSS_PATH : self::CUSTOM_HTML_PATH;
|
||||
|
||||
if (!File::exists(public_path('storage/custom-code'))) {
|
||||
File::makeDirectory(public_path('storage/custom-code'));
|
||||
}
|
||||
|
||||
File::put(public_path("storage/$path"), trim($value));
|
||||
}
|
||||
}
|
||||
|
||||
private function syncThemes(array $themes): void
|
||||
{
|
||||
$dbThemes = CssTheme::get();
|
||||
|
||||
// delete themes that were removed in appearance editor
|
||||
$dbThemes->each(function (CssTheme $theme) use ($themes) {
|
||||
if (
|
||||
!Arr::first(
|
||||
$themes,
|
||||
fn($current) => $current['id'] === $theme['id'],
|
||||
)
|
||||
) {
|
||||
$theme->delete();
|
||||
}
|
||||
});
|
||||
|
||||
// update changed themes and create new ones
|
||||
foreach ($themes as $theme) {
|
||||
$existing = $dbThemes->find($theme['id']);
|
||||
$newValue = Arr::except($theme, ['id', 'updated_at']);
|
||||
if (!$existing) {
|
||||
CssTheme::create(
|
||||
array_merge($newValue, ['user_id' => Auth::id()]),
|
||||
);
|
||||
} else {
|
||||
$existing->fill($newValue)->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
common/Admin/Appearance/AppearanceValues.php
Executable file
127
common/Admin/Appearance/AppearanceValues.php
Executable file
@@ -0,0 +1,127 @@
|
||||
<?php namespace Common\Admin\Appearance;
|
||||
|
||||
use Common\Admin\Appearance\Themes\CssTheme;
|
||||
use Common\Core\Prerender\MetaTags;
|
||||
use Common\Settings\Settings;
|
||||
use Exception;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class AppearanceValues
|
||||
{
|
||||
/**
|
||||
* ENV values to include.
|
||||
*/
|
||||
const ENV_KEYS = ['app_name'];
|
||||
|
||||
public function get(): array
|
||||
{
|
||||
// split values into db settings and appearance specific settings, to avoid naming collisions
|
||||
$values = [
|
||||
'settings' => app(Settings::class)->getUnflattened(),
|
||||
'appearance' => [],
|
||||
];
|
||||
|
||||
// add env settings
|
||||
$values['appearance']['env'] = [];
|
||||
foreach (self::ENV_KEYS as $key) {
|
||||
$values['appearance']['env'][$key] = config(
|
||||
str_replace('_', '.', $key),
|
||||
);
|
||||
}
|
||||
|
||||
$values['appearance']['themes'] = [
|
||||
'all' => CssTheme::get(),
|
||||
'selectedThemeId' => null,
|
||||
];
|
||||
|
||||
// add custom code
|
||||
$values['appearance']['custom_code'] = [
|
||||
'css' => $this->getCustomCodeValue(
|
||||
AppearanceSaver::CUSTOM_CSS_PATH,
|
||||
),
|
||||
'html' => $this->getCustomCodeValue(
|
||||
AppearanceSaver::CUSTOM_HTML_PATH,
|
||||
),
|
||||
];
|
||||
|
||||
$values['appearance']['seo'] = $this->prepareSeoValues();
|
||||
|
||||
$defaults = [];
|
||||
$defaultSettings = collect(config('common.default-settings'))
|
||||
->mapWithKeys(fn($item) => [$item['name'] => $item['value']])
|
||||
->toArray();
|
||||
$defaults['settings'] = settings()->getUnflattened(
|
||||
false,
|
||||
$defaultSettings,
|
||||
);
|
||||
$defaults['appearance']['themes'] = config('common.themes');
|
||||
|
||||
return [
|
||||
'values' => $values,
|
||||
'defaults' => $defaults,
|
||||
];
|
||||
}
|
||||
|
||||
private function prepareSeoValues(): array
|
||||
{
|
||||
$flat = [];
|
||||
$seoConfig = config('seo');
|
||||
|
||||
if (!$seoConfig) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$seo = Arr::except($seoConfig, 'common');
|
||||
$seo = array_filter($seo, function ($config) {
|
||||
return is_array($config);
|
||||
});
|
||||
|
||||
// resource groups meta tags for artist, movie, track etc.
|
||||
foreach ($seo as $resourceName => $resource) {
|
||||
// resource has config for each verb (show, index etc.)
|
||||
foreach ($resource as $verbName => $verb) {
|
||||
// verb has a list of meta tags (og:title, description etc.)
|
||||
if (is_array($verb)) {
|
||||
foreach ($verb as $metaTag) {
|
||||
$property = Arr::get($metaTag, 'property');
|
||||
if (!in_array($property, MetaTags::EDITABLE_TAGS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = str_replace(
|
||||
'og:',
|
||||
'',
|
||||
"$resourceName / $verbName / $property",
|
||||
);
|
||||
$name = str_replace('-', ' ', $name);
|
||||
|
||||
$key = "seo.$resourceName.$verbName.$property";
|
||||
$defaultValue = $metaTag['content'];
|
||||
|
||||
$flat[] = [
|
||||
'name' => $name,
|
||||
'key' => $key,
|
||||
'value' => app(Settings::class)->get(
|
||||
$key,
|
||||
$defaultValue,
|
||||
),
|
||||
'defaultValue' => $defaultValue,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $flat;
|
||||
}
|
||||
|
||||
private function getCustomCodeValue($path): string
|
||||
{
|
||||
try {
|
||||
return File::get(public_path("storage/$path"));
|
||||
} catch (Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
32
common/Admin/Appearance/Controllers/AppearanceController.php
Executable file
32
common/Admin/Appearance/Controllers/AppearanceController.php
Executable file
@@ -0,0 +1,32 @@
|
||||
<?php namespace Common\Admin\Appearance\Controllers;
|
||||
|
||||
use Common\Admin\Appearance\AppearanceSaver;
|
||||
use Common\Admin\Appearance\AppearanceValues;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AppearanceController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
protected AppearanceValues $values,
|
||||
protected AppearanceSaver $saver,
|
||||
) {
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->authorize('update', 'AppearancePolicy');
|
||||
|
||||
$this->saver->save($this->request->get('changes'));
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function getValues(): JsonResponse
|
||||
{
|
||||
$this->authorize('update', 'AppearancePolicy');
|
||||
|
||||
return $this->success($this->values->get());
|
||||
}
|
||||
}
|
||||
55
common/Admin/Appearance/Controllers/SeoTagsController.php
Executable file
55
common/Admin/Appearance/Controllers/SeoTagsController.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance\Controllers;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class SeoTagsController extends BaseController
|
||||
{
|
||||
public function show(string $names)
|
||||
{
|
||||
$this->authorize('update', 'AppearancePolicy');
|
||||
|
||||
$names = explode(',', $names);
|
||||
|
||||
$response = [];
|
||||
|
||||
foreach ($names as $name) {
|
||||
try {
|
||||
$customView = storage_path(
|
||||
"app/editable-views/seo-tags/$name.blade.php",
|
||||
);
|
||||
$response[$name] = [
|
||||
'custom' => file_exists($customView)
|
||||
? file_get_contents($customView)
|
||||
: null,
|
||||
'original' => file_get_contents(
|
||||
resource_path("views/seo/$name/seo-tags.blade.php"),
|
||||
),
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success($response);
|
||||
}
|
||||
|
||||
public function update(string $name)
|
||||
{
|
||||
$this->authorize('update', 'AppearancePolicy');
|
||||
|
||||
$data = $this->validate(request(), [
|
||||
'tags' => 'required|string',
|
||||
]);
|
||||
|
||||
$directory = storage_path('app/editable-views/seo-tags');
|
||||
File::ensureDirectoryExists($directory);
|
||||
|
||||
file_put_contents("$directory/$name.blade.php", $data['tags']);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
39
common/Admin/Appearance/Events/AppearanceSettingSaved.php
Executable file
39
common/Admin/Appearance/Events/AppearanceSettingSaved.php
Executable file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance\Events;
|
||||
|
||||
class AppearanceSettingSaved
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $key;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $previousValue;
|
||||
|
||||
public function __construct(
|
||||
string $type,
|
||||
string $key,
|
||||
string $value,
|
||||
string $previousValue = null
|
||||
) {
|
||||
//
|
||||
$this->type = $type;
|
||||
$this->key = $key;
|
||||
$this->value = $value;
|
||||
$this->previousValue = $previousValue;
|
||||
}
|
||||
}
|
||||
65
common/Admin/Appearance/GenerateFavicon.php
Executable file
65
common/Admin/Appearance/GenerateFavicon.php
Executable file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance;
|
||||
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class GenerateFavicon
|
||||
{
|
||||
const FAVICON_DIR = 'favicon';
|
||||
protected string $absoluteFaviconDir;
|
||||
|
||||
protected array $sizes = [
|
||||
[72, 72],
|
||||
[96, 96],
|
||||
[128, 128],
|
||||
[144, 144],
|
||||
[152, 152],
|
||||
[192, 192],
|
||||
[384, 384],
|
||||
[512, 512],
|
||||
];
|
||||
protected string $initialFilePath;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->absoluteFaviconDir = public_path(self::FAVICON_DIR);
|
||||
}
|
||||
|
||||
public function execute(string $filePath): void
|
||||
{
|
||||
if (str_starts_with($filePath, 'http') || !file_exists($filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
File::ensureDirectoryExists($this->absoluteFaviconDir);
|
||||
$this->initialFilePath = $filePath;
|
||||
|
||||
foreach ($this->sizes as $size) {
|
||||
$this->generateFaviconForSize($size);
|
||||
}
|
||||
$this->generateFaviconForSize([16, 16], public_path(), 'favicon.ico');
|
||||
|
||||
$uri = self::FAVICON_DIR . '/icon-144x144.png?v=' . time();
|
||||
app(Settings::class)->save(['branding.favicon' => $uri]);
|
||||
}
|
||||
|
||||
private function generateFaviconForSize(
|
||||
array $size,
|
||||
string $dir = null,
|
||||
string $name = null,
|
||||
): void {
|
||||
$manager = new ImageManager(new Driver());
|
||||
|
||||
$img = $manager->read($this->initialFilePath);
|
||||
$img->coverDown($size[0], $size[1]);
|
||||
|
||||
$dir = $dir ?? $this->absoluteFaviconDir;
|
||||
$name = $name ?? "icon-$size[0]x$size[1].png";
|
||||
|
||||
$img->toPng()->save("$dir/$name");
|
||||
}
|
||||
}
|
||||
33
common/Admin/Appearance/Themes/CrupdateCssTheme.php
Executable file
33
common/Admin/Appearance/Themes/CrupdateCssTheme.php
Executable file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance\Themes;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class CrupdateCssTheme
|
||||
{
|
||||
public function execute(array $data, CssTheme $cssTheme = null): ?CssTheme
|
||||
{
|
||||
if (!$cssTheme) {
|
||||
$cssTheme = CssTheme::newInstance([
|
||||
'user_id' => Auth::id(),
|
||||
'values' => $data['is_dark']
|
||||
? config('common.themes.dark')
|
||||
: config('common.themes.light'),
|
||||
]);
|
||||
}
|
||||
|
||||
$attributes = Arr::only($data, [
|
||||
'name',
|
||||
'is_dark',
|
||||
'default_dark',
|
||||
'default_light',
|
||||
'values',
|
||||
]);
|
||||
|
||||
$cssTheme->fill($attributes)->save();
|
||||
|
||||
return $cssTheme;
|
||||
}
|
||||
}
|
||||
28
common/Admin/Appearance/Themes/CrupdateCssThemeRequest.php
Executable file
28
common/Admin/Appearance/Themes/CrupdateCssThemeRequest.php
Executable file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance\Themes;
|
||||
|
||||
use Auth;
|
||||
use Common\Core\BaseFormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class CrupdateCssThemeRequest extends BaseFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
$required = $this->getMethod() === 'POST' ? 'required' : '';
|
||||
$ignore = $this->getMethod() === 'PUT' ? $this->route('css_theme')->id : '';
|
||||
$userId = $this->route('css_theme') ? $this->route('css_theme')->user_id : Auth::id();
|
||||
|
||||
return [
|
||||
'name' => [
|
||||
$required, 'string', 'min:3',
|
||||
Rule::unique('css_themes')->where('user_id', $userId)->ignore($ignore)
|
||||
],
|
||||
'is_dark' => 'boolean',
|
||||
'default_dark' => 'boolean',
|
||||
'default_light' => 'boolean',
|
||||
'colors' => 'array',
|
||||
];
|
||||
}
|
||||
}
|
||||
75
common/Admin/Appearance/Themes/CssTheme.php
Executable file
75
common/Admin/Appearance/Themes/CssTheme.php
Executable file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance\Themes;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CssTheme extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'is_dark' => 'boolean',
|
||||
'default_dark' => 'boolean',
|
||||
'default_light' => 'boolean',
|
||||
'font' => 'json',
|
||||
];
|
||||
|
||||
const MODEL_TYPE = 'css_theme';
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
|
||||
public function setValuesAttribute($value)
|
||||
{
|
||||
if ($value && is_array($value)) {
|
||||
$this->attributes['values'] = json_encode($value);
|
||||
}
|
||||
}
|
||||
|
||||
public function getValuesAttribute($value): array
|
||||
{
|
||||
if ($value && is_string($value)) {
|
||||
return json_decode($value, true);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function getCssVariables(): string
|
||||
{
|
||||
// don't decode from json
|
||||
$values = $this->attributes['values'] ?? '';
|
||||
$values = preg_replace('/"/', '', $values);
|
||||
$values = preg_replace('/\\\/', '', $values);
|
||||
$values = preg_replace('/[{}]/', '', $values);
|
||||
$values = preg_replace('/, ?--/', ';--', $values);
|
||||
if ($family = $this->getFontFamily()) {
|
||||
$values .= ";--be-font-family: $family";
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function getFontFamily(): string|null
|
||||
{
|
||||
return $this->font['family'] ?? null;
|
||||
}
|
||||
|
||||
public function isGoogleFont(): bool
|
||||
{
|
||||
return $this->font['google'] ?? false;
|
||||
}
|
||||
|
||||
public function getHtmlThemeColor()
|
||||
{
|
||||
if ($this->is_dark) {
|
||||
return $this->values['--be-background-alt'];
|
||||
} else {
|
||||
return $this->values['--be-primary'];
|
||||
}
|
||||
}
|
||||
}
|
||||
76
common/Admin/Appearance/Themes/CssThemeController.php
Executable file
76
common/Admin/Appearance/Themes/CssThemeController.php
Executable file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance\Themes;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CssThemeController extends BaseController
|
||||
{
|
||||
/**
|
||||
* @var CssTheme
|
||||
*/
|
||||
private $cssTheme;
|
||||
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
private $request;
|
||||
|
||||
public function __construct(CssTheme $cssTheme, Request $request)
|
||||
{
|
||||
$this->cssTheme = $cssTheme;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$userId = $this->request->get('userId');
|
||||
$this->authorize('index', [CssTheme::class, $userId]);
|
||||
|
||||
$builder = $this->cssTheme->newQuery();
|
||||
if ($userId) {
|
||||
$builder->where('user_id', $userId);
|
||||
}
|
||||
|
||||
$dataSource = new Datasource($this->cssTheme, $this->request->all());
|
||||
$pagination = $dataSource->paginate();
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function show(CssTheme $cssTheme)
|
||||
{
|
||||
$this->authorize('show', $cssTheme);
|
||||
|
||||
return $this->success(['theme' => $cssTheme]);
|
||||
}
|
||||
|
||||
public function store(CrupdateCssThemeRequest $request)
|
||||
{
|
||||
$this->authorize('store', CssTheme::class);
|
||||
|
||||
$cssTheme = app(CrupdateCssTheme::class)->execute($request->all());
|
||||
|
||||
return $this->success(['theme' => $cssTheme]);
|
||||
}
|
||||
|
||||
public function update(CssTheme $cssTheme, CrupdateCssThemeRequest $request)
|
||||
{
|
||||
$this->authorize('store', $cssTheme);
|
||||
|
||||
$cssTheme = app(CrupdateCssTheme::class)->execute($request->all(), $cssTheme);
|
||||
|
||||
return $this->success(['theme' => $cssTheme]);
|
||||
}
|
||||
|
||||
public function destroy(CssTheme $cssTheme)
|
||||
{
|
||||
$this->authorize('destroy', $cssTheme);
|
||||
|
||||
$cssTheme->delete();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
46
common/Admin/Appearance/Themes/CssThemePolicy.php
Executable file
46
common/Admin/Appearance/Themes/CssThemePolicy.php
Executable file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Appearance\Themes;
|
||||
|
||||
use Common\Auth\BaseUser;
|
||||
use Common\Core\Policies\BasePolicy;
|
||||
|
||||
class CssThemePolicy extends BasePolicy
|
||||
{
|
||||
public function index(BaseUser $user, $userId = null)
|
||||
{
|
||||
return $user->hasPermission('cssTheme.view') || $user->id === (int) $userId;
|
||||
}
|
||||
|
||||
public function show(BaseUser $user, CssTheme $cssTheme)
|
||||
{
|
||||
return $user->hasPermission('cssTheme.view') || $cssTheme->user_id === $user->id;
|
||||
}
|
||||
|
||||
public function store(BaseUser $user)
|
||||
{
|
||||
return $user->hasPermission('cssTheme.create');
|
||||
}
|
||||
|
||||
public function update(BaseUser $user, CssTheme $cssTheme)
|
||||
{
|
||||
return $user->hasPermission('cssTheme.update') || $cssTheme->user_id === $user->id;
|
||||
}
|
||||
|
||||
public function destroy(BaseUser $user, CssTheme $theme)
|
||||
{
|
||||
if ($theme->default_dark && app(CssTheme::class)->where('default_dark', true)->count() < 2) {
|
||||
return $this->deny("Default dark theme can't be deleted");
|
||||
}
|
||||
|
||||
if ($theme->default_light && app(CssTheme::class)->where('default_light', true)->count() < 2) {
|
||||
return $this->deny("Default light theme can't be deleted");
|
||||
}
|
||||
|
||||
if (app(CssTheme::class)->count() <= 1) {
|
||||
return $this->deny("All themes can't be deleted");
|
||||
}
|
||||
|
||||
return $user->hasPermission('cssTheme.delete') || $theme->user_id === $user->id;
|
||||
}
|
||||
}
|
||||
33
common/Admin/CacheController.php
Executable file
33
common/Admin/CacheController.php
Executable file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin;
|
||||
|
||||
use Artisan;
|
||||
use Cache;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Settings\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CacheController extends BaseController
|
||||
{
|
||||
/**
|
||||
* @var Request
|
||||
*/
|
||||
private $request;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
*/
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->middleware('isAdmin');
|
||||
}
|
||||
|
||||
public function flush()
|
||||
{
|
||||
Cache::flush();
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
64
common/Admin/ImpersonateUserController.php
Executable file
64
common/Admin/ImpersonateUserController.php
Executable file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
||||
use Laravel\Fortify\LoginRateLimiter;
|
||||
|
||||
class ImpersonateUserController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected StatefulGuard $guard,
|
||||
protected LoginRateLimiter $limiter,
|
||||
) {
|
||||
$this->middleware('isAdmin');
|
||||
}
|
||||
|
||||
public function impersonate(int $userId)
|
||||
{
|
||||
$impersonated = User::findOrFail($userId);
|
||||
|
||||
if ($impersonated->id === $this->guard->id()) {
|
||||
return $this->error(__('You are already logged in as this user.'));
|
||||
}
|
||||
|
||||
$impersonatorId = $this->guard->id();
|
||||
|
||||
$this->logout();
|
||||
|
||||
$this->guard->login($impersonated, true);
|
||||
request()
|
||||
->session()
|
||||
->regenerate();
|
||||
$this->limiter->clear(request());
|
||||
|
||||
session()->put('impersonator_id', $impersonatorId);
|
||||
|
||||
return $this->success([
|
||||
'user' => $impersonated,
|
||||
]);
|
||||
}
|
||||
|
||||
public function stopImpersonating()
|
||||
{
|
||||
$this->logout();
|
||||
session()->forget('impersonator_id');
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
protected function logout(): void
|
||||
{
|
||||
$this->guard->logout();
|
||||
if (request()->hasSession()) {
|
||||
request()
|
||||
->session()
|
||||
->invalidate();
|
||||
request()
|
||||
->session()
|
||||
->regenerateToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
234
common/Admin/Sitemap/BaseSitemapGenerator.php
Executable file
234
common/Admin/Sitemap/BaseSitemapGenerator.php
Executable file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Sitemap;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Common\Core\Contracts\AppUrlGenerator;
|
||||
use Common\Pages\CustomPage;
|
||||
use File;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BaseSitemapGenerator
|
||||
{
|
||||
|
||||
protected int $queryChunkSize = 1000;
|
||||
protected string $currentDateTimeString;
|
||||
protected int $currentResourceSitemapCount = 0;
|
||||
protected string | null $currentXml = null;
|
||||
protected int $currentLineCount = 0;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@ini_set('memory_limit', '160M');
|
||||
@ini_set('max_execution_time', 7200);
|
||||
$this->currentDateTimeString = Carbon::now()->toDateTimeString();
|
||||
}
|
||||
|
||||
protected function getAppQueries(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
protected function getAppStaticUrls(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function generate(): void
|
||||
{
|
||||
$index = [];
|
||||
|
||||
$queries = array_merge(
|
||||
[
|
||||
app(CustomPage::class)->select(['id', 'title', 'slug']),
|
||||
],
|
||||
$this->getAppQueries(),
|
||||
);
|
||||
|
||||
foreach ($queries as $query) {
|
||||
$resourceName = str_replace(
|
||||
'_',
|
||||
'-',
|
||||
$query->getModel()->getTable(),
|
||||
);
|
||||
$index[$resourceName] = $this->createSitemapForResource(
|
||||
$query,
|
||||
$resourceName,
|
||||
);
|
||||
}
|
||||
|
||||
$this->makeStaticMap();
|
||||
$this->makeIndex($index);
|
||||
}
|
||||
|
||||
protected function createSitemapForResource(
|
||||
Builder $model,
|
||||
string $name
|
||||
): int {
|
||||
$model
|
||||
->orderBy('id')
|
||||
->chunk($this->queryChunkSize, function ($records) use ($name) {
|
||||
$modelName = ucfirst(Str::snake($records->first()::MODEL_TYPE));
|
||||
$methodName = "add{$modelName}Line";
|
||||
foreach ($records as $record) {
|
||||
// allow extending class to override line adding for specific model
|
||||
if (method_exists($this, $methodName)) {
|
||||
$this->$methodName(
|
||||
$this->getModelUrl($record),
|
||||
$this->getModelUpdatedAt($record),
|
||||
$name,
|
||||
);
|
||||
} else {
|
||||
$this->addNewLine(
|
||||
$this->getModelUrl($record),
|
||||
$this->getModelUpdatedAt($record),
|
||||
$name,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($this->currentLineCount) {
|
||||
$this->save("$name-sitemap-{$this->currentResourceSitemapCount}");
|
||||
}
|
||||
|
||||
$numberOfSitemapsGenerated = $this->currentResourceSitemapCount;
|
||||
|
||||
$this->currentResourceSitemapCount = 0;
|
||||
$this->currentLineCount = 0;
|
||||
|
||||
return $numberOfSitemapsGenerated;
|
||||
}
|
||||
|
||||
protected function addNewLine(string $url, string $updatedAt, string $name)
|
||||
{
|
||||
if (!$this->currentXml) {
|
||||
$this->startNewXmlFile();
|
||||
}
|
||||
|
||||
if ($this->currentLineCount === 50000) {
|
||||
$this->save("$name-sitemap-{$this->currentResourceSitemapCount}");
|
||||
$this->startNewXmlFile();
|
||||
}
|
||||
|
||||
$updatedAt = $this->formatDate($updatedAt);
|
||||
|
||||
$line =
|
||||
"\t" .
|
||||
"<url>\n\t\t<loc>" .
|
||||
htmlspecialchars($url) .
|
||||
"</loc>\n\t\t<lastmod>" .
|
||||
$updatedAt .
|
||||
"</lastmod>\n\t\t<changefreq>weekly</changefreq>\n\t\t<priority>1.00</priority>\n\t</url>\n";
|
||||
|
||||
$this->currentXml .= $line;
|
||||
|
||||
$this->currentLineCount++;
|
||||
}
|
||||
|
||||
protected function save(string $fileName)
|
||||
{
|
||||
$this->currentXml .= "\n</urlset>";
|
||||
|
||||
File::ensureDirectoryExists(public_path('storage/sitemaps'));
|
||||
File::put(
|
||||
public_path("storage/sitemaps/$fileName.xml"),
|
||||
$this->currentXml,
|
||||
);
|
||||
|
||||
$this->currentXml = null;
|
||||
$this->currentLineCount = 0;
|
||||
$this->currentResourceSitemapCount++;
|
||||
}
|
||||
|
||||
protected function startNewXmlFile()
|
||||
{
|
||||
$this->currentXml =
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' .
|
||||
"\n" .
|
||||
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">' .
|
||||
"\n";
|
||||
}
|
||||
|
||||
protected function makeStaticMap(): void
|
||||
{
|
||||
$urls = array_merge(
|
||||
['', 'login', 'register', 'contact'],
|
||||
$this->getAppStaticUrls(),
|
||||
);
|
||||
|
||||
$urls = array_map(function ($data) {
|
||||
if (is_string($data)) {
|
||||
return [
|
||||
'path' => $data,
|
||||
'updated_at' => $this->currentDateTimeString,
|
||||
];
|
||||
} else {
|
||||
return $data;
|
||||
}
|
||||
}, $urls);
|
||||
|
||||
foreach ($urls as $url) {
|
||||
$this->addNewLine(
|
||||
url($url['path']),
|
||||
$url['updated_at'],
|
||||
'static-urls',
|
||||
);
|
||||
}
|
||||
|
||||
$this->save('static-urls-sitemap');
|
||||
}
|
||||
|
||||
protected function makeIndex(array $index): void
|
||||
{
|
||||
$baseUrl = url('storage/sitemaps');
|
||||
|
||||
$string =
|
||||
'<?xml version="1.0" encoding="UTF-8"?>' .
|
||||
"\n" .
|
||||
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' .
|
||||
"\n";
|
||||
|
||||
foreach ($index as $resourceName => $resourceSitemapCount) {
|
||||
// sitemap count starts from 1
|
||||
for ($i = 0; $i <= $resourceSitemapCount - 1; $i++) {
|
||||
$url = "{$baseUrl}/{$resourceName}-sitemap-$i.xml";
|
||||
$string .=
|
||||
"\t<sitemap>\n" .
|
||||
"\t\t<loc>$url</loc>\n" .
|
||||
"\t\t<lastmod>{$this->formatDate()}</lastmod>\n" .
|
||||
"\t</sitemap>\n";
|
||||
}
|
||||
}
|
||||
|
||||
$string .= "\t<sitemap>\n\t\t<loc>{$baseUrl}/static-urls-sitemap.xml</loc>\n\t\t<lastmod>{$this->formatDate()}</lastmod>\n\t</sitemap>\n";
|
||||
|
||||
$string .= '</sitemapindex>';
|
||||
|
||||
File::put(public_path('storage/sitemaps/sitemap-index.xml'), $string);
|
||||
}
|
||||
|
||||
protected function getModelUrl(Model $model): string
|
||||
{
|
||||
$resourceName = Str::camel(class_basename($model));
|
||||
return app(AppUrlGenerator::class)->$resourceName($model);
|
||||
}
|
||||
|
||||
protected function formatDate(string $date = null): string
|
||||
{
|
||||
if (!$date) {
|
||||
$date = $this->currentDateTimeString;
|
||||
}
|
||||
return date('Y-m-d\TH:i:sP', strtotime($date));
|
||||
}
|
||||
|
||||
protected function getModelUpdatedAt(Model $model): string
|
||||
{
|
||||
return !$model->updated_at ||
|
||||
$model->updated_at === '0000-00-00 00:00:00'
|
||||
? $this->currentDateTimeString
|
||||
: $model->updated_at;
|
||||
}
|
||||
}
|
||||
23
common/Admin/Sitemap/SitemapController.php
Executable file
23
common/Admin/Sitemap/SitemapController.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Admin\Sitemap;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class SitemapController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('isAdmin');
|
||||
}
|
||||
|
||||
public function generate(): JsonResponse
|
||||
{
|
||||
$sitemap = class_exists('App\Services\SitemapGenerator')
|
||||
? app('App\Services\SitemapGenerator')
|
||||
: app(BaseSitemapGenerator::class);
|
||||
$sitemap->generate();
|
||||
return $this->success([]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user