185
common/Core/AppUrl.php
Executable file
185
common/Core/AppUrl.php
Executable file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
|
||||
|
||||
class AppUrl
|
||||
{
|
||||
/**
|
||||
* If host in .env file and current request did not match, but
|
||||
* we were able to find a matching custom domain in database.
|
||||
*/
|
||||
public ?object $matchedCustomDomain = null;
|
||||
|
||||
/**
|
||||
* Url "app.url" config item was changed to dynamically.
|
||||
*/
|
||||
public ?string $newAppUrl = null;
|
||||
|
||||
/**
|
||||
* Whether hosts from APP_URL in .env file and current request match.
|
||||
* This will strip "www" and schemes from both and only compare hosts.
|
||||
*/
|
||||
public bool $envAndCurrentHostsAreEqual;
|
||||
|
||||
public string $htmlBaseUri;
|
||||
|
||||
public string $originalAppUrl;
|
||||
|
||||
public function init(): static
|
||||
{
|
||||
$this->originalAppUrl = config('app.url');
|
||||
if (
|
||||
config('common.site.dynamic_app_url') ||
|
||||
!config('common.site.installed')
|
||||
) {
|
||||
$this->maybeDynamicallyUpdate();
|
||||
} else {
|
||||
$this->envAndCurrentHostsAreEqual = true;
|
||||
}
|
||||
$this->registerHtmlBaseUri();
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function maybeDynamicallyUpdate(): void
|
||||
{
|
||||
$request = app('request');
|
||||
$requestHost = $request->getHost();
|
||||
|
||||
$envParts = parse_url(config('app.url'));
|
||||
|
||||
$requestScheme = in_array($request->header('x-forwarded-proto'), [
|
||||
'https',
|
||||
'on',
|
||||
'ssl',
|
||||
'1',
|
||||
])
|
||||
? 'https'
|
||||
: $request->getScheme();
|
||||
|
||||
$schemeIsDifferent = $requestScheme !== Arr::get($envParts, 'scheme');
|
||||
$this->envAndCurrentHostsAreEqual =
|
||||
$this->getHostFrom($requestHost) ===
|
||||
$this->getHostFrom(Arr::get($envParts, 'host'));
|
||||
$hostsWithWwwAreEqual = $requestHost === Arr::get($envParts, 'host');
|
||||
$customDomainsEnabled = config('common.site.enable_custom_domains');
|
||||
$endsWithSlash = Str::endsWith(Arr::get($envParts, 'path'), '/');
|
||||
|
||||
// update app.url if not installed yet, or if only scheme, slash or www is different
|
||||
if (
|
||||
($this->envAndCurrentHostsAreEqual ||
|
||||
!config('common.site.installed')) &&
|
||||
($schemeIsDifferent || $endsWithSlash || !$hostsWithWwwAreEqual)
|
||||
) {
|
||||
if (!config('common.site.installed')) {
|
||||
$this->handleInstallationAppUrl();
|
||||
return;
|
||||
}
|
||||
|
||||
$this->newAppUrl =
|
||||
$request->getSchemeAndHttpHost() .
|
||||
rtrim(Arr::get($envParts, 'path'), '/');
|
||||
config(['app.url' => $this->newAppUrl]);
|
||||
// update social auth urls as well
|
||||
foreach (config('services') as $serviceName => $serviceConfig) {
|
||||
if (isset($serviceConfig['redirect'])) {
|
||||
config(
|
||||
"services.$serviceName.redirect",
|
||||
"$this->newAppUrl/secure/auth/social/$serviceName/callback",
|
||||
);
|
||||
}
|
||||
}
|
||||
} elseif (!$this->envAndCurrentHostsAreEqual && $customDomainsEnabled) {
|
||||
$this->matchedCustomDomain = DB::table('custom_domains')
|
||||
->where('host', $requestHost)
|
||||
->orWhere('host', $request->getSchemeAndHttpHost())
|
||||
// if there are multiple domains with same host, get the one that has resource attached to it first
|
||||
->orderBy('resource_id', 'desc')
|
||||
->first();
|
||||
if ($this->matchedCustomDomain) {
|
||||
$this->newAppUrl = $request->getSchemeAndHttpHost();
|
||||
config(['app.url' => $this->newAppUrl]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleInstallationAppUrl(): void
|
||||
{
|
||||
// create new request so main laravel request is not instantiated yet,
|
||||
// and "normalizeRequestUri" on CommonProvider works properly
|
||||
$request = SymfonyRequest::createFromGlobals();
|
||||
|
||||
$pathParts = [
|
||||
...explode('/', $request->getBaseUrl()),
|
||||
...explode('/', $request->getPathInfo()),
|
||||
];
|
||||
|
||||
$pathParts = array_values(
|
||||
array_filter($pathParts, fn($part) => $part !== ''),
|
||||
);
|
||||
|
||||
// get path parts up to "install" segment (if it exists), in case site is not installed at root domain
|
||||
$domainParts = [];
|
||||
foreach ($pathParts as $key => $part) {
|
||||
if ($part !== 'install') {
|
||||
$domainParts[] = $part;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$this->newAppUrl = request()->getSchemeAndHttpHost();
|
||||
|
||||
if (!empty($pathParts)) {
|
||||
$this->newAppUrl .= '/' . implode('/', $domainParts);
|
||||
}
|
||||
|
||||
config(['app.url' => $this->newAppUrl]);
|
||||
}
|
||||
|
||||
protected function registerHtmlBaseUri(): void
|
||||
{
|
||||
$htmlBaseUri = '/';
|
||||
|
||||
//get uri for html "base" tag
|
||||
if (substr_count(config('app.url'), '/') > 2) {
|
||||
$htmlBaseUri = parse_url(config('app.url'))['path'] . '/';
|
||||
}
|
||||
|
||||
$this->htmlBaseUri = $htmlBaseUri;
|
||||
}
|
||||
|
||||
public function getRequestHost(): string
|
||||
{
|
||||
return $this->getHostFrom(app('request')->getHost());
|
||||
}
|
||||
|
||||
public function requestHostMatches(
|
||||
$hostOrUrl,
|
||||
$subdomainMatch = false,
|
||||
): bool {
|
||||
$hostOrUrl = $this->getHostFrom($hostOrUrl);
|
||||
$requestHost = $this->getRequestHost();
|
||||
return $hostOrUrl === $requestHost ||
|
||||
($subdomainMatch && Str::endsWith($requestHost, $hostOrUrl));
|
||||
}
|
||||
|
||||
/*
|
||||
* Extract host from full or partial url.
|
||||
* This will remove scheme, port, "www", path and query params.
|
||||
*/
|
||||
public function getHostFrom($hostOrUrl)
|
||||
{
|
||||
// if there's no scheme, add // so it's parsed properly
|
||||
if (!preg_match('/^([a-z][a-z0-9\-\.\+]*:)|(\/)/', $hostOrUrl)) {
|
||||
$hostOrUrl = '//' . $hostOrUrl;
|
||||
}
|
||||
|
||||
$parts = parse_url($hostOrUrl);
|
||||
return preg_replace('/^www\./i', '', $parts['host']);
|
||||
}
|
||||
}
|
||||
144
common/Core/BaseController.php
Executable file
144
common/Core/BaseController.php
Executable file
@@ -0,0 +1,144 @@
|
||||
<?php namespace Common\Core;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Core\Prerender\HandlesSeo;
|
||||
use Common\Core\Rendering\DetectsCrawlers;
|
||||
use Common\Core\Rendering\RendersClientSideApp;
|
||||
use Illuminate\Auth\Access\Response as AuthResponse;
|
||||
use Illuminate\Contracts\Auth\Access\Gate;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class BaseController extends Controller
|
||||
{
|
||||
use AuthorizesRequests,
|
||||
DispatchesJobs,
|
||||
ValidatesRequests,
|
||||
HandlesSeo,
|
||||
RendersClientSideApp,
|
||||
DetectsCrawlers;
|
||||
|
||||
// todo: refactor bedrive and belink policies to use basePolicy permission check and remove guest fetching here
|
||||
|
||||
/**
|
||||
* Authorize a given action for the current user
|
||||
* or guest if user is not logged in.
|
||||
*/
|
||||
public function authorize(
|
||||
string $ability,
|
||||
mixed $arguments = [],
|
||||
): AuthResponse {
|
||||
if (Auth::check()) {
|
||||
[$ability, $arguments] = $this->parseAbilityAndArguments(
|
||||
$ability,
|
||||
$arguments,
|
||||
);
|
||||
return app(Gate::class)->authorize($ability, $arguments);
|
||||
} else {
|
||||
$guest = new User();
|
||||
// make sure ID is not NULL to avoid false positives in authorization
|
||||
$guest->forceFill(['id' => -1]);
|
||||
$guest->setRelation('roles', collect([app('guestRole')]));
|
||||
return $this->authorizeForUser($guest, $ability, $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
public function renderClientOrApi(array $options)
|
||||
{
|
||||
$ssrEnabled =
|
||||
config('common.site.ssr_enabled') && !Arr::get($options, 'noSSR');
|
||||
$data = Arr::get($options, 'data', []);
|
||||
$pageName = Arr::get($options, 'pageName');
|
||||
$isCrawler = $this->isCrawler();
|
||||
if ($pageName) {
|
||||
$customPath = storage_path(
|
||||
"app/editable-views/seo-tags/$pageName.blade.php",
|
||||
);
|
||||
$seoTagsView = file_exists($customPath)
|
||||
? "editable-views::seo-tags.$pageName"
|
||||
: "seo.$pageName.seo-tags";
|
||||
}
|
||||
|
||||
// if it's an API request, simply return data as JSON
|
||||
if (isApiRequest()) {
|
||||
// only include SEO tags for internal API requests
|
||||
if (requestIsFromFrontend() && isset($seoTagsView)) {
|
||||
$data['seo'] = view($seoTagsView, $options['data'])->render();
|
||||
}
|
||||
return response()->json($data);
|
||||
}
|
||||
|
||||
// if it's a web request and SSR is disabled, prerender a simple blade page for crawlers
|
||||
if (
|
||||
!Arr::get($options, 'noPrerender') &&
|
||||
!$ssrEnabled &&
|
||||
$isCrawler &&
|
||||
$pageName &&
|
||||
file_exists(
|
||||
resource_path("views/seo/$pageName/prerender.blade.php"),
|
||||
)
|
||||
) {
|
||||
return view("seo.$pageName.prerender", $data)->with([
|
||||
'htmlBaseUri' => app(AppUrl::class)->htmlBaseUri,
|
||||
'seoTagsView' => $seoTagsView ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
// finally render the full react app with optional SSR
|
||||
return $this->renderClientSideApp([
|
||||
'pageName' => $pageName,
|
||||
'pageData' => $data,
|
||||
'seoTagsView' => $seoTagsView ?? null,
|
||||
'noSSR' => !$ssrEnabled,
|
||||
]);
|
||||
}
|
||||
|
||||
public function success(
|
||||
array|Collection $data = [],
|
||||
int $status = 200,
|
||||
array $options = [],
|
||||
) {
|
||||
$data = $data ?: [];
|
||||
if (!Arr::get($data, 'status')) {
|
||||
$data['status'] = 'success';
|
||||
}
|
||||
|
||||
// only generate seo tags if request is coming from frontend and not from API
|
||||
if (
|
||||
(requestIsFromFrontend() || defined('SHOULD_PRERENDER')) &&
|
||||
($response = $this->handleSeo($data, $options))
|
||||
) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if ($value instanceof Arrayable) {
|
||||
$data[$key] = $value->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json($data, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return error response with specified messages.
|
||||
*/
|
||||
public function error(
|
||||
?string $message = '',
|
||||
array $errors = [],
|
||||
int $status = 422,
|
||||
$data = [],
|
||||
) {
|
||||
$data = array_merge($data, [
|
||||
'message' => $message,
|
||||
'errors' => $errors ?: [],
|
||||
]);
|
||||
return response()->json($data, $status);
|
||||
}
|
||||
}
|
||||
11
common/Core/BaseFormRequest.php
Executable file
11
common/Core/BaseFormRequest.php
Executable file
@@ -0,0 +1,11 @@
|
||||
<?php namespace Common\Core;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class BaseFormRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
114
common/Core/BaseModel.php
Executable file
114
common/Core/BaseModel.php
Executable file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
abstract class BaseModel extends Model
|
||||
{
|
||||
const MODEL_TYPE = '';
|
||||
|
||||
abstract public static function filterableFields(): array;
|
||||
abstract public function toNormalizedArray(): array;
|
||||
abstract public function toSearchableArray(): array;
|
||||
|
||||
abstract public static function getModelTypeAttribute(): string;
|
||||
|
||||
public function getMorphClass()
|
||||
{
|
||||
return static::MODEL_TYPE;
|
||||
}
|
||||
|
||||
public function scopeMysqlSearch(Builder $builder, string $query): Builder
|
||||
{
|
||||
$searchableFields = [];
|
||||
$searchableRelations = [];
|
||||
foreach ((new static())->toSearchableArray() as $field => $value) {
|
||||
if (!in_array($field, static::filterableFields())) {
|
||||
if (
|
||||
method_exists(static::class, $field) &&
|
||||
!$this->$field() instanceof Attribute
|
||||
) {
|
||||
$searchableRelations[] = $field;
|
||||
} else {
|
||||
$searchableFields[] = $field;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$builder->matches($searchableFields, $query);
|
||||
|
||||
foreach ($searchableRelations as $relation) {
|
||||
$builder->orWhereHas(
|
||||
$relation,
|
||||
fn(Builder $q) => $q->mysqlSearch($query),
|
||||
);
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
public function scopeMatches(
|
||||
Builder $builder,
|
||||
array $columns,
|
||||
string $value,
|
||||
): Builder {
|
||||
$mode = config('common.site.scout_mysql_mode');
|
||||
$columns = array_map(fn($col) => $this->qualifyColumn($col), $columns);
|
||||
if ($mode === 'fulltext' && strlen($value) >= 3) {
|
||||
if (is_null($builder->getQuery()->columns)) {
|
||||
$builder->select($this->qualifyColumn('*'));
|
||||
}
|
||||
$colString = implode(',', $columns);
|
||||
$builder->selectRaw(
|
||||
"MATCH($colString) AGAINST(? IN NATURAL LANGUAGE MODE) AS relevance",
|
||||
[$value],
|
||||
);
|
||||
$builder->whereRaw("MATCH($colString) AGAINST(?)", [$value]);
|
||||
} else {
|
||||
$builder->where(function (Builder $nestedBuilder) use (
|
||||
$columns,
|
||||
$mode,
|
||||
$value,
|
||||
) {
|
||||
foreach ($columns as $column) {
|
||||
$nestedBuilder->orWhere(
|
||||
$column,
|
||||
'like',
|
||||
$mode === 'basic' ? "$value%" : "%$value%",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
public function getSearchableValues(): array
|
||||
{
|
||||
$searchableValues = [];
|
||||
foreach ($this->toSearchableArray() as $key => $value) {
|
||||
if (!in_array($key, self::filterableFields())) {
|
||||
$searchableValues[] = $value;
|
||||
}
|
||||
}
|
||||
return $searchableValues;
|
||||
}
|
||||
|
||||
public static function getSearchableKeys($skipRelations = false): array
|
||||
{
|
||||
$searchableKeys = [];
|
||||
foreach ((new static())->toSearchableArray() as $key => $value) {
|
||||
if (
|
||||
!in_array($key, static::filterableFields()) &&
|
||||
(!$skipRelations || !method_exists(static::class, $key))
|
||||
) {
|
||||
$searchableKeys[] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $searchableKeys;
|
||||
}
|
||||
}
|
||||
26
common/Core/BaseTrustHosts.php
Executable file
26
common/Core/BaseTrustHosts.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core;
|
||||
|
||||
use Common\Domains\CustomDomainController;
|
||||
use Illuminate\Http\Middleware\TrustHosts as Middleware;
|
||||
|
||||
class BaseTrustHosts extends Middleware
|
||||
{
|
||||
public function hosts(): array
|
||||
{
|
||||
return [
|
||||
$this->allSubdomainsOfApplicationUrl(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function shouldSpecifyTrustedHosts(): bool
|
||||
{
|
||||
// allow custom domain validation
|
||||
if (request()->path() === CustomDomainController::VALIDATE_CUSTOM_DOMAIN_PATH) {
|
||||
return false;
|
||||
} else {
|
||||
return parent::shouldSpecifyTrustedHosts();
|
||||
}
|
||||
}
|
||||
}
|
||||
45
common/Core/BaseVerifyCsrfToken.php
Executable file
45
common/Core/BaseVerifyCsrfToken.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BaseVerifyCsrfToken extends VerifyCsrfToken
|
||||
{
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
|
||||
/**
|
||||
* Determine if the request has a URI/Domain that should pass through CSRF verification.
|
||||
*
|
||||
* @param Request $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function inExceptArray($request)
|
||||
{
|
||||
return true;
|
||||
if (config('common.site.demo') || config('common.site.disable_csrf')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::inExceptArray($request);
|
||||
}
|
||||
|
||||
protected function addCookieToResponse($request, $response)
|
||||
{
|
||||
// don't add cookie if session is set to null
|
||||
// (belink needs to disable laravel headers for 301 redirect)
|
||||
if (config('session.driver') === null) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return parent::addCookieToResponse($request, $response);
|
||||
}
|
||||
}
|
||||
213
common/Core/Bootstrap/BaseBootstrapData.php
Executable file
213
common/Core/Bootstrap/BaseBootstrapData.php
Executable file
@@ -0,0 +1,213 @@
|
||||
<?php namespace Common\Core\Bootstrap;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Admin\Appearance\Themes\CssTheme;
|
||||
use Common\Auth\Jobs\LogActiveSessionJob;
|
||||
use Common\Auth\Roles\Role;
|
||||
use Common\Billing\Gateways\Stripe\FormatsMoney;
|
||||
use Common\Core\AppUrl;
|
||||
use Common\Localizations\LocalizationsRepository;
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Jenssegers\Agent\Agent;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class BaseBootstrapData implements BootstrapData
|
||||
{
|
||||
use FormatsMoney;
|
||||
|
||||
protected array $data = [];
|
||||
|
||||
public function __construct(
|
||||
protected Settings $settings,
|
||||
protected Request $request,
|
||||
protected Role $role,
|
||||
protected LocalizationsRepository $localizationsRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getEncoded(): string
|
||||
{
|
||||
if ($this->data['user']) {
|
||||
$this->data['user'] = $this->data['user']->toArray();
|
||||
}
|
||||
|
||||
return json_encode($this->data);
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value): static
|
||||
{
|
||||
Arr::set($this->data, $key, $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function get($key = null)
|
||||
{
|
||||
return $key ? Arr::get($this->data, $key) : $this->data;
|
||||
}
|
||||
|
||||
public function getSelectedTheme($key = null)
|
||||
{
|
||||
$themeId = $this->get('themes.selectedThemeId');
|
||||
$theme = Arr::first(
|
||||
$this->data['themes']['all'],
|
||||
fn($theme) => $theme['id'] === (int) $themeId,
|
||||
);
|
||||
if (!$theme) {
|
||||
$theme = $this->data['themes']['all'][0];
|
||||
}
|
||||
|
||||
$value = $key ? Arr::get($theme, $key) : $theme;
|
||||
return $key === 'name' ? strtolower($value) : $value;
|
||||
}
|
||||
|
||||
public function init(): self
|
||||
{
|
||||
$this->data['settings'] = settings()->getUnflattened();
|
||||
$this->data['csrf_token'] = csrf_token();
|
||||
$this->data['is_mobile_device'] = app(Agent::class)->isMobile();
|
||||
$this->data['settings']['base_url'] = config('app.url');
|
||||
$this->data['settings']['asset_url'] = config('app.asset_url');
|
||||
$this->data['settings']['html_base_uri'] = app(
|
||||
AppUrl::class,
|
||||
)->htmlBaseUri;
|
||||
$this->data['settings']['version'] = config('common.site.version');
|
||||
$this->data['sentry_release'] = config('sentry.release');
|
||||
$this->data['default_meta_tags'] = $this->getDefaultMetaTags();
|
||||
$this->data['user'] = $this->getCurrentUser();
|
||||
$this->data['guest_role'] = app('guestRole')?->load('permissions');
|
||||
$this->data['i18n'] =
|
||||
$this->localizationsRepository->getByNameOrCode(
|
||||
app()->getLocale(),
|
||||
settings('i18n.enable', true),
|
||||
) ?:
|
||||
null;
|
||||
$this->data['themes'] = $this->getThemes();
|
||||
$this->data['language'] = $this->data['i18n']
|
||||
? $this->data['i18n']['language']
|
||||
: 'en';
|
||||
|
||||
if (
|
||||
config('common.site.notifications_integrated') &&
|
||||
$this->data['user']
|
||||
) {
|
||||
$this->data['user']->loadCount('unreadNotifications');
|
||||
}
|
||||
|
||||
$alreadyAccepted =
|
||||
!settings('cookie_notice.enable') ||
|
||||
(bool) Arr::get($_COOKIE, 'cookie_notice', false);
|
||||
$this->data['show_cookie_notice'] =
|
||||
!$alreadyAccepted && $this->isCookieLawCountry();
|
||||
|
||||
$this->logActiveSession();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getThemes(): array
|
||||
{
|
||||
$themes = CssTheme::where('default_dark', true)
|
||||
->orWhere('default_light', true)
|
||||
->get();
|
||||
|
||||
if ($themes->isEmpty()) {
|
||||
$themes = CssTheme::limit(2)->get();
|
||||
}
|
||||
|
||||
$selectedTheme = null;
|
||||
|
||||
// first, get theme from cookie or url param, if theme change by user is enabled
|
||||
if (settings('themes.user_change')) {
|
||||
if ($themeFromUrl = $this->request->get('beThemeId')) {
|
||||
$selectedTheme = $themes->find($themeFromUrl);
|
||||
} else {
|
||||
$selectedTheme = $themes->find(
|
||||
Arr::get($_COOKIE, 'be-active-theme'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// if no theme was selected, get default theme specified by admin
|
||||
if (!$selectedTheme && ($defaultId = settings('themes.default_id'))) {
|
||||
$selectedTheme = $themes->find($defaultId);
|
||||
}
|
||||
|
||||
// finally, fallback to default light theme
|
||||
if (!$selectedTheme) {
|
||||
$selectedTheme = $themes->where('default_light', true)->first();
|
||||
}
|
||||
|
||||
return [
|
||||
'all' => $themes,
|
||||
'selectedThemeId' => $selectedTheme?->id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current user and his roles.
|
||||
*/
|
||||
public function getCurrentUser(): ?User
|
||||
{
|
||||
$user = $this->request->user();
|
||||
if ($user) {
|
||||
// load user subscriptions, if billing is enabled
|
||||
if (
|
||||
settings('billing.enable') &&
|
||||
!$user->relationLoaded('subscriptions')
|
||||
) {
|
||||
$user->load('subscriptions.price');
|
||||
}
|
||||
|
||||
// load user roles, if not already loaded
|
||||
if (!$user->relationLoaded('roles')) {
|
||||
$user->load('roles');
|
||||
}
|
||||
|
||||
if (!$user->relationLoaded('permissions')) {
|
||||
$user->loadPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function getDefaultMetaTags(): string
|
||||
{
|
||||
$pageName = 'landing-page';
|
||||
$customPath = storage_path(
|
||||
"app/editable-views/seo-tags/$pageName.blade.php",
|
||||
);
|
||||
if (file_exists($customPath)) {
|
||||
return view("editable-views::seo-tags.$pageName")->render();
|
||||
} else {
|
||||
return view("seo.$pageName.seo-tags")->render();
|
||||
}
|
||||
}
|
||||
|
||||
protected function isCookieLawCountry(): bool
|
||||
{
|
||||
$isoCode = geoip(getIp())['iso_code'];
|
||||
// prettier-ignore
|
||||
return in_array($isoCode, ['AT', 'BE', 'BG', 'BR', 'CY', 'CZ', 'DE', 'DK', 'EE', 'EL', 'ES', 'FI', 'FR', 'GB', 'HR', 'HU', 'IE', 'IT','LT', 'LU', 'LV', 'MT', 'NL', 'NO', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function logActiveSession(): void
|
||||
{
|
||||
if ($this->data['user']) {
|
||||
$token = $this->data['user']->currentAccessToken();
|
||||
LogActiveSessionJob::dispatch([
|
||||
'user_id' => $this->data['user']->id,
|
||||
'ip_address' => getIp(),
|
||||
'user_agent' => $this->request->userAgent(),
|
||||
'session_id' => session()->getId(),
|
||||
'token' =>
|
||||
$token instanceof PersonalAccessToken
|
||||
? $token->token
|
||||
: null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
common/Core/Bootstrap/BootstrapData.php
Executable file
12
common/Core/Bootstrap/BootstrapData.php
Executable file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Bootstrap;
|
||||
|
||||
interface BootstrapData
|
||||
{
|
||||
public function getEncoded(): string;
|
||||
|
||||
public function init(): self;
|
||||
|
||||
public function getThemes(): array;
|
||||
}
|
||||
139
common/Core/Bootstrap/MobileBootstrapData.php
Executable file
139
common/Core/Bootstrap/MobileBootstrapData.php
Executable file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Bootstrap;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Localizations\Localization;
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\Color\Hex;
|
||||
use Spatie\Color\Rgb;
|
||||
|
||||
class MobileBootstrapData extends BaseBootstrapData
|
||||
{
|
||||
public function init(): self
|
||||
{
|
||||
$cssThemes = $this->getThemes()['all'];
|
||||
$themes = [
|
||||
'light' => $cssThemes
|
||||
->where('default_light', true)
|
||||
->first()
|
||||
->toArray(),
|
||||
'dark' => $cssThemes
|
||||
->where('default_dark', true)
|
||||
->first()
|
||||
->toArray(),
|
||||
];
|
||||
|
||||
$themes['light']['values'] = $this->transformValuesForFlutter(
|
||||
$themes['light']['values'],
|
||||
);
|
||||
$themes['dark']['values'] = $this->transformValuesForFlutter(
|
||||
$themes['dark']['values'],
|
||||
);
|
||||
|
||||
$this->data = [
|
||||
'themes' => $themes,
|
||||
'user' => $this->getCurrentUser(),
|
||||
'menus' => $this->getMobileMenus(),
|
||||
'settings' => [
|
||||
'social.google.enable' => (bool) settings(
|
||||
'social.google.enable',
|
||||
),
|
||||
'require_email_confirmation' => (bool) settings(
|
||||
'require_email_confirmation',
|
||||
),
|
||||
'registration.disable' => (bool) settings(
|
||||
'registration.disable',
|
||||
),
|
||||
],
|
||||
'locales' => Localization::get(),
|
||||
];
|
||||
|
||||
if (settings('i18n.enable')) {
|
||||
$langCode = request('activeLocale') ?: app()->getLocale();
|
||||
foreach ($this->data['locales'] as $locale) {
|
||||
if ($locale->language === $langCode) {
|
||||
$locale->loadLines();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logActiveSession();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function refreshToken(string $deviceName): self
|
||||
{
|
||||
$user = $this->data['user'];
|
||||
if ($user) {
|
||||
$user['access_token'] = $user->refreshApiToken($deviceName);
|
||||
$this->loadFcmToken($user);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCurrentUser(): ?User
|
||||
{
|
||||
if ($user = $this->request->user()) {
|
||||
return $this->loadFcmToken($user);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getMobileMenus(): array
|
||||
{
|
||||
return array_values(
|
||||
array_filter(
|
||||
settings('menus'),
|
||||
fn($menu) => collect($menu['positions'])->some(
|
||||
fn($position) => Str::startsWith($position, 'mobile-app'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private function transformValuesForFlutter(array $colors): array
|
||||
{
|
||||
if (!class_exists(Hex::class)) {
|
||||
return $colors;
|
||||
}
|
||||
|
||||
$radiusValues = [
|
||||
'--be-button-radius',
|
||||
'--be-input-radius',
|
||||
'--be-panel-radius',
|
||||
];
|
||||
|
||||
$valuesToSkip = ['--be-navbar-color'];
|
||||
|
||||
return collect($colors)
|
||||
->map(function ($value, $name) use ($valuesToSkip, $radiusValues) {
|
||||
if (in_array($name, $radiusValues)) {
|
||||
if (str_ends_with($value, 'rem')) {
|
||||
return (float) str_replace('rem', '', $value) * 16;
|
||||
} else {
|
||||
return (float) str_replace('px', '', $value);
|
||||
}
|
||||
} elseif (in_array($name, $valuesToSkip)) {
|
||||
return $value;
|
||||
} elseif (str_ends_with($value, '%')) {
|
||||
return (int) str_replace('%', '', $value);
|
||||
} else {
|
||||
$value = str_replace(' ', ',', $value);
|
||||
$rgb = Rgb::fromString("rgb($value)");
|
||||
return [$rgb->red(), $rgb->green(), $rgb->blue(), 1.0];
|
||||
}
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
private function loadFcmToken(User $user): User
|
||||
{
|
||||
if (method_exists($user, 'loadFcmToken')) {
|
||||
$user->loadFcmToken();
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
38
common/Core/Commands/GenerateChecksums.php
Executable file
38
common/Core/Commands/GenerateChecksums.php
Executable file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Commands;
|
||||
|
||||
use File;
|
||||
use Illuminate\Console\Command;
|
||||
use Str;
|
||||
|
||||
class GenerateChecksums extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'checksums:generate';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$rootPath = base_path();
|
||||
$allFiles = File::allFiles($rootPath);
|
||||
$bar = $this->output->createProgressBar(count($allFiles));
|
||||
$bar->start();
|
||||
|
||||
$checksums = [];
|
||||
foreach ($allFiles as $file) {
|
||||
if (Str::startsWith($file->getFilename(), '.')) {
|
||||
continue;
|
||||
}
|
||||
$relativePath = str_replace($rootPath, '', $file->getPathname());
|
||||
$checksums[$relativePath] = md5_file($file);
|
||||
$bar->advance();
|
||||
}
|
||||
file_put_contents("$rootPath/checksums.json", json_encode($checksums));
|
||||
|
||||
$bar->finish();
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
20
common/Core/Commands/GenerateSitemap.php
Executable file
20
common/Core/Commands/GenerateSitemap.php
Executable file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Commands;
|
||||
|
||||
use Common\Admin\Sitemap\BaseSitemapGenerator;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class GenerateSitemap extends Command
|
||||
{
|
||||
protected $signature = 'sitemap:generate';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$sitemap = class_exists('App\Services\SitemapGenerator')
|
||||
? app('App\Services\SitemapGenerator')
|
||||
: app(BaseSitemapGenerator::class);
|
||||
$sitemap->generate();
|
||||
$this->info('Sitemap generated successfully');
|
||||
}
|
||||
}
|
||||
37
common/Core/Commands/SeedCommand.php
Executable file
37
common/Core/Commands/SeedCommand.php
Executable file
@@ -0,0 +1,37 @@
|
||||
<?php namespace Common\Core\Commands;
|
||||
|
||||
use File;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Str;
|
||||
|
||||
class SeedCommand extends Command
|
||||
{
|
||||
protected $signature = 'common:seed';
|
||||
|
||||
protected $description = 'Execute all common package seeders.';
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$paths = collect(File::files(__DIR__ . '/../../Database/Seeds'));
|
||||
|
||||
$paths->filter(function($path) {
|
||||
return Str::endsWith($path, '.php');
|
||||
})->each(function($path) {
|
||||
Model::unguarded(function () use ($path) {
|
||||
$namespace = 'Common\Database\Seeds\\'.basename($path, '.php');
|
||||
$this->getSeeder($namespace)->__invoke();
|
||||
});
|
||||
});
|
||||
|
||||
$this->info('Seeded database successfully.');
|
||||
}
|
||||
|
||||
protected function getSeeder(string $namespace): Seeder
|
||||
{
|
||||
$class = $this->laravel->make($namespace);
|
||||
|
||||
return $class->setContainer($this->laravel)->setCommand($this);
|
||||
}
|
||||
}
|
||||
45
common/Core/Commands/UpdateSimplePaginateTables.php
Executable file
45
common/Core/Commands/UpdateSimplePaginateTables.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UpdateSimplePaginateTables extends Command
|
||||
{
|
||||
protected $signature = 'pagination:optimize';
|
||||
protected $description = 'Optimize pagination for large tables.';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$max = 150000;
|
||||
|
||||
$tables = [];
|
||||
|
||||
collect(DB::select('SHOW TABLES'))
|
||||
->map(function ($val) {
|
||||
foreach ($val as $key => $tbl) {
|
||||
return $tbl;
|
||||
}
|
||||
})
|
||||
->each(function ($table) use ($max, &$tables) {
|
||||
if (DB::table($table)->count() > $max) {
|
||||
$tables[] = $table;
|
||||
}
|
||||
});
|
||||
|
||||
settings()->save([
|
||||
'simple_pagination_tables' => implode(',', $tables),
|
||||
]);
|
||||
|
||||
$this->info(
|
||||
sprintf(
|
||||
'Tables with more than %d rows: %s',
|
||||
$max,
|
||||
implode(', ', $tables),
|
||||
),
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
8
common/Core/Contracts/AppUrlGenerator.php
Executable file
8
common/Core/Contracts/AppUrlGenerator.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Contracts;
|
||||
|
||||
interface AppUrlGenerator
|
||||
{
|
||||
//
|
||||
}
|
||||
25
common/Core/Controllers/BootstrapController.php
Executable file
25
common/Core/Controllers/BootstrapController.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php namespace Common\Core\Controllers;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Common\Core\Bootstrap\BootstrapData;
|
||||
use Common\Core\Bootstrap\MobileBootstrapData;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class BootstrapController extends BaseController
|
||||
{
|
||||
/**
|
||||
* Get data needed to bootstrap the application.
|
||||
*
|
||||
* @param BootstrapData $bootstrapData
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function getBootstrapData(BootstrapData $bootstrapData)
|
||||
{
|
||||
return response()->json(['data' => $bootstrapData->init()->getEncoded()]);
|
||||
}
|
||||
|
||||
public function getMobileBootstrapData(MobileBootstrapData $bootstrapData)
|
||||
{
|
||||
return response()->json($bootstrapData->init()->get());
|
||||
}
|
||||
}
|
||||
57
common/Core/Controllers/HomeController.php
Executable file
57
common/Core/Controllers/HomeController.php
Executable file
@@ -0,0 +1,57 @@
|
||||
<?php namespace Common\Core\Controllers;
|
||||
|
||||
use Common\Core\AppUrl;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Core\Bootstrap\BootstrapData;
|
||||
use Common\Settings\Settings;
|
||||
|
||||
class HomeController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected BootstrapData $bootstrapData,
|
||||
protected Settings $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
// only get meta tags if we're actually
|
||||
// rendering homepage and not a fallback route
|
||||
$data = [];
|
||||
if (
|
||||
request()->path() === '/' &&
|
||||
($response = $this->handleSeo($data))
|
||||
) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->bootstrapData->init();
|
||||
|
||||
$view = view('app')
|
||||
->with('bootstrapData', $this->bootstrapData)
|
||||
->with('htmlBaseUri', app(AppUrl::class)->htmlBaseUri)
|
||||
->with('settings', $this->settings)
|
||||
->with(
|
||||
'customHtmlPath',
|
||||
public_path('storage/custom-code/custom-html.html'),
|
||||
)
|
||||
->with(
|
||||
'customCssPath',
|
||||
public_path('storage/custom-code/custom-styles.css'),
|
||||
);
|
||||
|
||||
if (isset($data['seo'])) {
|
||||
$view->with('meta', $data['seo']);
|
||||
}
|
||||
|
||||
return response($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render basic client side page with optional SSR when page has no data or seo tags.
|
||||
* (contact page, login, register, etc.)
|
||||
*/
|
||||
public function render() {
|
||||
return $this->renderClientOrApi([]);
|
||||
}
|
||||
}
|
||||
8
common/Core/Exceptions/AccessResponseWithAction.php
Executable file
8
common/Core/Exceptions/AccessResponseWithAction.php
Executable file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Exceptions;
|
||||
|
||||
class AccessResponseWithAction extends AccessResponseWithPermission
|
||||
{
|
||||
public array|null $action;
|
||||
}
|
||||
17
common/Core/Exceptions/AccessResponseWithPermission.php
Executable file
17
common/Core/Exceptions/AccessResponseWithPermission.php
Executable file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Exceptions;
|
||||
|
||||
use Illuminate\Auth\Access\Response as LaravelAccessResponse;
|
||||
|
||||
class AccessResponseWithPermission extends LaravelAccessResponse
|
||||
{
|
||||
public function __construct(
|
||||
protected $allowed,
|
||||
public string|null $permission,
|
||||
protected $message = '',
|
||||
protected $code = null,
|
||||
) {
|
||||
parent::__construct($allowed, $message, $code);
|
||||
}
|
||||
}
|
||||
204
common/Core/Exceptions/BaseExceptionHandler.php
Executable file
204
common/Core/Exceptions/BaseExceptionHandler.php
Executable file
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Exceptions;
|
||||
|
||||
use Common\Billing\Models\Product;
|
||||
use ErrorException;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Exceptions\Handler;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Sentry\Laravel\Integration;
|
||||
use Sentry\State\Scope;
|
||||
use Spatie\Ignition\Ignition;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Throwable;
|
||||
use function Sentry\configureScope;
|
||||
|
||||
class BaseExceptionHandler extends Handler
|
||||
{
|
||||
public function render($request, Throwable $e)
|
||||
{
|
||||
$isAuthException =
|
||||
$e instanceof AuthorizationException ||
|
||||
($e instanceof HttpException && $e->getStatusCode() === 403);
|
||||
|
||||
if (
|
||||
$isAuthException &&
|
||||
(requestIsFromFrontend() &&
|
||||
!$request->expectsJson() &&
|
||||
!Auth::check())
|
||||
) {
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if (
|
||||
$e instanceof AuthorizationException &&
|
||||
$e->response() instanceof AccessResponseWithPermission &&
|
||||
$e->response()->permission &&
|
||||
Auth::check() &&
|
||||
settings('billing.enable')
|
||||
) {
|
||||
$permissionExistsInSubscriptionPlan = Product::with(['permissions'])
|
||||
->get()
|
||||
->some(function ($product) use ($e) {
|
||||
// check if there's a plan that has this permission and if user is not already on this plan
|
||||
return $product->permissions->contains(
|
||||
'name',
|
||||
$e->response()->permission,
|
||||
) &&
|
||||
Auth::user()->subscriptions->first()?->product_id !==
|
||||
$product->id;
|
||||
});
|
||||
if ($permissionExistsInSubscriptionPlan) {
|
||||
return Auth::user()->subscribed()
|
||||
? redirect('/billing')
|
||||
: redirect('/pricing');
|
||||
}
|
||||
}
|
||||
|
||||
return parent::render($request, $e);
|
||||
}
|
||||
|
||||
public function register()
|
||||
{
|
||||
if (config('app.env') !== 'production') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->renderable(function (ErrorException $e) {
|
||||
if (
|
||||
Str::contains($e->getMessage(), [
|
||||
'failed to open stream: Permission denied',
|
||||
'mkdir(): Permission denied',
|
||||
])
|
||||
) {
|
||||
return $this->filePermissionResponse($e);
|
||||
}
|
||||
});
|
||||
|
||||
configureScope(function (Scope $scope): void {
|
||||
$scope->setContext('app_name', ['value' => config('app.name')]);
|
||||
});
|
||||
|
||||
$this->reportable(function (Throwable $e) {
|
||||
Integration::captureUnhandledException($e);
|
||||
});
|
||||
}
|
||||
|
||||
protected function convertExceptionToArray(Throwable $e): array
|
||||
{
|
||||
$previous = $e->getPrevious();
|
||||
$isValidationException =
|
||||
$e instanceof HttpException && $e->getStatusCode() === 422;
|
||||
$isExceptionWithAction =
|
||||
$previous &&
|
||||
method_exists($previous, 'response') &&
|
||||
$previous->response() &&
|
||||
property_exists($previous->response(), 'action');
|
||||
|
||||
if (
|
||||
config('app.debug') &&
|
||||
!config('common.site.demo') &&
|
||||
!$isValidationException
|
||||
) {
|
||||
$array = $this->ignitionReportFromThrowable($e);
|
||||
} else {
|
||||
$array = parent::convertExceptionToArray($e);
|
||||
}
|
||||
|
||||
if ($isExceptionWithAction) {
|
||||
$array['action'] = $e->getPrevious()->response()->action;
|
||||
}
|
||||
|
||||
if ($array['message'] === 'Server Error') {
|
||||
$array['message'] = __(
|
||||
'There was an issue. Please try again later.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($array['message'] === 'This action is unauthorized.') {
|
||||
$array['message'] = __(
|
||||
"You don't have required permissions for this action.",
|
||||
);
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
protected function filePermissionResponse(ErrorException $e)
|
||||
{
|
||||
if (request()->expectsJson()) {
|
||||
return response()->json(['message' => 'test']);
|
||||
} else {
|
||||
preg_match('/\((.+?)\):/', $e->getMessage(), $matches);
|
||||
$path = $matches[1] ?? null;
|
||||
// should not return a view here, in case laravel views folder is not readable as well
|
||||
return response(
|
||||
"<div style='text-align:center'><h1>Could not access a file or folder</h1> <br> Location: <b>$path</b><br>" .
|
||||
'<p>See the article here for possible solutions: <a target="_blank" href="https://support.vebto.com/hc/articles/21/25/207/changing-file-permissions">https://support.vebto.com/hc/articles/207/changing-file-permissions</a></p></div>',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function ignitionReportFromThrowable(Throwable $e): array
|
||||
{
|
||||
$report = app(Ignition::class)
|
||||
->shouldDisplayException(false)
|
||||
->handleException($e)
|
||||
->toArray();
|
||||
|
||||
$trace = array_map(function ($item) {
|
||||
$path = Str::of($item['class'] ?? $item['file'])
|
||||
->replace([base_path(), 'vendor/laravel/framework/src/'], '')
|
||||
->replace('\\', '/')
|
||||
->trim('/')
|
||||
->explode('/');
|
||||
return [
|
||||
'applicationFrame' => $item['application_frame'],
|
||||
'codeSnippet' => $item['code_snippet'],
|
||||
'path' => $path,
|
||||
'lineNumber' => $item['line_number'],
|
||||
'method' => $item['method'],
|
||||
];
|
||||
}, $report['stacktrace']);
|
||||
|
||||
$flatIndex = 0;
|
||||
$totalVendorGroups = 0;
|
||||
$groupedTrace = array_reduce(
|
||||
$trace,
|
||||
function ($carry, $item) use (&$flatIndex, &$totalVendorGroups) {
|
||||
$item['flatIndex'] = $flatIndex;
|
||||
if ($item['applicationFrame']) {
|
||||
$carry[] = $item;
|
||||
} else {
|
||||
if (Arr::get(Arr::last($carry), 'vendorGroup')) {
|
||||
$carry[count($carry) - 1]['items'][] = $item;
|
||||
} else {
|
||||
$totalVendorGroups++;
|
||||
$carry[] = [
|
||||
'vendorGroup' => true,
|
||||
'items' => [$item],
|
||||
];
|
||||
}
|
||||
}
|
||||
$flatIndex++;
|
||||
return $carry;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return [
|
||||
'ignitionTrace' => true,
|
||||
'message' => $report['message'],
|
||||
'exception' => $report['exception_class'],
|
||||
'file' => $report['stacktrace'][0]['file'],
|
||||
'line' => $report['stacktrace'][0]['line_number'],
|
||||
'trace' => $groupedTrace,
|
||||
'totalVendorGroups' => $totalVendorGroups,
|
||||
'phpVersion' => $report['language_version'],
|
||||
'appVersion' => config('common.site.version'),
|
||||
];
|
||||
}
|
||||
}
|
||||
75
common/Core/HttpClient.php
Executable file
75
common/Core/HttpClient.php
Executable file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core;
|
||||
|
||||
use Common\Settings\Settings;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
class HttpClient
|
||||
{
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
*/
|
||||
public function __construct($params = [])
|
||||
{
|
||||
if ( ! isset($params['exceptions'])) $params['exceptions'] = false;
|
||||
if ( ! isset($params['timeout'])) $params['timeout'] = 2;
|
||||
$defaultVerify = (bool) app(Settings::class)->get('https.enable_cert_verification', true);
|
||||
if ( ! isset($params['verify'])) $params['verify'] = $defaultVerify;
|
||||
$this->client = new Client($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|string
|
||||
*/
|
||||
public function get(string $url, array $params = [])
|
||||
{
|
||||
try {
|
||||
$r = $this->client->get($url, $params);
|
||||
} catch (ClientException $e) {
|
||||
$r = $e->getResponse();
|
||||
if ($r->getStatusCode() === 429 && $r->hasHeader('Retry-After')) {
|
||||
$seconds = $r->getHeader('Retry-After') ?: 5;
|
||||
sleep((int) $seconds);
|
||||
$r = $this->get($url);
|
||||
}
|
||||
}
|
||||
|
||||
if ($r->getStatusCode() === 429 && $r->hasHeader('Retry-After')) {
|
||||
$seconds = $r->getHeader('Retry-After') ? $r->getHeader('Retry-After') : 5;
|
||||
sleep((int) $seconds);
|
||||
$r = $this->get($url);
|
||||
}
|
||||
|
||||
$contents = is_string($r) ? $r : $r->getBody()->getContents();
|
||||
$json = json_decode($contents, true);
|
||||
return $json ? $json : $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
public function post($url, $params = [])
|
||||
{
|
||||
$r = $this->client->post($url, $params);
|
||||
|
||||
if ($r->getStatusCode() === 429 && $r->hasHeader('Retry-After')) {
|
||||
$seconds = $r->getHeader('Retry-After') ? $r->getHeader('Retry-After') : 5;
|
||||
sleep($seconds);
|
||||
$r = $this->get($url);
|
||||
}
|
||||
|
||||
$contents = $r->getBody()->getContents();
|
||||
$json = json_decode($contents, true);
|
||||
return $json ? $json : $contents;
|
||||
}
|
||||
}
|
||||
|
||||
146
common/Core/Install/CheckSiteHealth.php
Executable file
146
common/Core/Install/CheckSiteHealth.php
Executable file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Install;
|
||||
|
||||
class CheckSiteHealth
|
||||
{
|
||||
public function execute(): array
|
||||
{
|
||||
return $this->performChecks();
|
||||
}
|
||||
|
||||
protected function performChecks(): array
|
||||
{
|
||||
$minPhpVersion = $this->getMinimumPhpversion();
|
||||
$results = collect([
|
||||
'server' => [
|
||||
'items' => [
|
||||
'PHP Version' => [
|
||||
'passes' => version_compare(
|
||||
PHP_VERSION,
|
||||
$minPhpVersion,
|
||||
'>',
|
||||
),
|
||||
'errorMessage' => "You need at least $minPhpVersion PHP version.",
|
||||
],
|
||||
],
|
||||
],
|
||||
'extensions' => [
|
||||
'items' => [
|
||||
'PDO' => [
|
||||
'passes' => defined('PDO::ATTR_DRIVER_NAME'),
|
||||
'errorMessage' =>
|
||||
'PHP PDO extension needs to be enabled.',
|
||||
],
|
||||
'XML' => [
|
||||
'passes' => extension_loaded('xml'),
|
||||
'errorMessage' =>
|
||||
'PHP XML extension needs to be enabled.',
|
||||
],
|
||||
'Mbstring' => [
|
||||
'passes' => extension_loaded('mbstring'),
|
||||
'errorMessage' =>
|
||||
'PHP mbstring extension needs to be enabled.',
|
||||
],
|
||||
'Fileinfo' => [
|
||||
'passes' => extension_loaded('fileinfo'),
|
||||
'errorMessage' =>
|
||||
'PHP fileinfo extension needs to be enabled.',
|
||||
],
|
||||
'OpenSSL' => [
|
||||
'passes' => extension_loaded('openssl'),
|
||||
'errorMessage' =>
|
||||
'PHP openssl extension needs to be enabled.',
|
||||
],
|
||||
'GD' => [
|
||||
'passes' => extension_loaded('gd'),
|
||||
'errorMessage' =>
|
||||
'PHP GD extension needs to be enabled.',
|
||||
],
|
||||
'Curl' => [
|
||||
'passes' => extension_loaded('curl'),
|
||||
'errorMessage' =>
|
||||
'PHP curl extension needs to be enabled.',
|
||||
],
|
||||
'Zip' => [
|
||||
'passes' => class_exists('ZipArchive'),
|
||||
'errorMessage' =>
|
||||
'PHP ZipArchive extension needs to be installed.',
|
||||
],
|
||||
'fpassthru' => [
|
||||
'passes' => function_exists('fpassthru'),
|
||||
'errorMessage' =>
|
||||
'"fpassthru" PHP function needs to be enabled.',
|
||||
],
|
||||
],
|
||||
],
|
||||
'filesystem' => $this->checkFilesystemPermissions(),
|
||||
])->toArray();
|
||||
|
||||
$someFailed = false;
|
||||
foreach ($results as $groupName => $group) {
|
||||
$results[$groupName]['allPassed'] = collect($group['items'])->every(
|
||||
'passes',
|
||||
);
|
||||
if (!$results[$groupName]['allPassed']) {
|
||||
$someFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'results' => $results,
|
||||
'allPassed' => !$someFailed,
|
||||
];
|
||||
}
|
||||
|
||||
protected function checkFilesystemPermissions(): array
|
||||
{
|
||||
$basePath = base_path();
|
||||
return [
|
||||
'items' => collect([
|
||||
'.htaccess',
|
||||
'public/.htaccess',
|
||||
config('common.site.installed') ? '.env' : 'env.example',
|
||||
'storage',
|
||||
'storage/app',
|
||||
'storage/logs',
|
||||
'storage/framework',
|
||||
'public/storage',
|
||||
])
|
||||
->map(function ($directory) use ($basePath) {
|
||||
$path = rtrim("$basePath/$directory", '/');
|
||||
if (is_file($path)) {
|
||||
if (!is_writable($path)) {
|
||||
@chmod($path, 0664);
|
||||
}
|
||||
return [
|
||||
'path' => $path,
|
||||
'passes' =>
|
||||
is_writable($path) && filesize($path) > 0,
|
||||
'errorMessage' => "Make sure <strong>$path</strong> has been uploaded properly and is writable (0664 permission).",
|
||||
];
|
||||
} else {
|
||||
if (!is_writable($path)) {
|
||||
@chmod($path, 0775);
|
||||
}
|
||||
return [
|
||||
'path' => $path,
|
||||
'passes' => is_writable($path),
|
||||
'errorMessage' => "Make <strong>$path</strong> writable by giving it 0775 or 0777 permissions via file manager.",
|
||||
];
|
||||
}
|
||||
})
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
protected function getMinimumPhpversion(): string
|
||||
{
|
||||
$composer = json_decode(
|
||||
file_get_contents(base_path('composer.json')),
|
||||
true,
|
||||
);
|
||||
preg_match('/(\d+\.\d+\.\d+)/', $composer['require']['php'], $matches);
|
||||
return $matches[1] ?? '8.1';
|
||||
}
|
||||
}
|
||||
201
common/Core/Install/InstallController.php
Executable file
201
common/Core/Install/InstallController.php
Executable file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Install;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Admin\Appearance\GenerateFavicon;
|
||||
use Common\Auth\Permissions\Permission;
|
||||
use Common\Database\MigrateAndSeed;
|
||||
use Common\Settings\DotEnvEditor;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class InstallController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (config('common.site.installed')) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
public function introductionStep()
|
||||
{
|
||||
$this->createHtaccessFiles();
|
||||
return view('common::install/introduction');
|
||||
}
|
||||
|
||||
public function requirementsStep()
|
||||
{
|
||||
$data = (new CheckSiteHealth())->execute();
|
||||
return view('common::install/requirements')->with($data);
|
||||
}
|
||||
|
||||
public function databaseStep()
|
||||
{
|
||||
$credentials = config('database.connections.mysql');
|
||||
|
||||
return view('common::install/database', [
|
||||
'host' => $credentials['host'],
|
||||
'database' => $credentials['database'],
|
||||
'username' => $credentials['username'],
|
||||
'password' => $credentials['password'],
|
||||
'port' => $credentials['port'],
|
||||
'prefix' => $credentials['prefix'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function insertAndValidateDatabaseCredentials()
|
||||
{
|
||||
$data = request()->validate([
|
||||
'host' => 'required|string',
|
||||
'database' => 'required|string',
|
||||
'username' => 'required|string',
|
||||
'password' => 'nullable|string',
|
||||
'port' => 'nullable|int',
|
||||
'prefix' => 'nullable|string|max:10',
|
||||
]);
|
||||
|
||||
$this->getEnvWriter()->write(
|
||||
collect($data)->mapWithKeys(
|
||||
fn($value, $key) => ['DB_' . strtoupper($key) => $value],
|
||||
),
|
||||
);
|
||||
config()->set(
|
||||
'database.connections.mysql',
|
||||
array_merge(config('database.connections.mysql'), $data),
|
||||
);
|
||||
|
||||
try {
|
||||
$tables = DB::select('SHOW TABLES');
|
||||
if (count($tables) > 0) {
|
||||
return back()->withErrors([
|
||||
'database' =>
|
||||
'Database is not empty. Please provide an empty database or delete all tables from the current one.',
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors([
|
||||
'database' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!file_exists(base_path('.env'))) {
|
||||
rename(base_path('env.example'), base_path('.env'));
|
||||
}
|
||||
|
||||
return redirect('install/admin');
|
||||
}
|
||||
|
||||
public function adminStep()
|
||||
{
|
||||
return view('common::install/admin');
|
||||
}
|
||||
|
||||
public function validateAdminCredentials()
|
||||
{
|
||||
$data = request()->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|string|min:4|confirmed',
|
||||
]);
|
||||
|
||||
return redirect('install/finalize')->with('adminCredentials', $data);
|
||||
}
|
||||
|
||||
public function finalizeStep()
|
||||
{
|
||||
if (!session()->has('adminCredentials')) {
|
||||
return redirect('install/admin');
|
||||
}
|
||||
|
||||
// app key
|
||||
$appKey = 'base64:' . base64_encode(random_bytes(32));
|
||||
$this->getEnvWriter()->write(['APP_KEY' => $appKey]);
|
||||
|
||||
// clear cache
|
||||
Cache::flush();
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
// migrate/seed/admin account
|
||||
(new MigrateAndSeed())->execute(function () {
|
||||
$credentials = session('adminCredentials');
|
||||
$user = app(User::class)->firstOrNew([
|
||||
'email' => $credentials['email'],
|
||||
]);
|
||||
$user->password = $credentials['password'];
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
$adminPermission = app(Permission::class)->firstOrCreate(
|
||||
['name' => 'admin'],
|
||||
[
|
||||
'name' => 'admin',
|
||||
'group' => 'admin',
|
||||
'display_name' => 'Super Admin',
|
||||
'description' => 'Give all permissions to user.',
|
||||
],
|
||||
);
|
||||
$user->permissions()->syncWithoutDetaching($adminPermission->id);
|
||||
Auth::login($user);
|
||||
});
|
||||
|
||||
$appUrl = $this->getFinalSiteUrl();
|
||||
|
||||
// finalize
|
||||
$this->getEnvWriter()->write([
|
||||
'app_url' => $appUrl,
|
||||
'app_env' => 'production',
|
||||
'app_debug' => false,
|
||||
'cache_driver' => 'file',
|
||||
'installed' => true,
|
||||
]);
|
||||
|
||||
// move default favicons
|
||||
File::copyDirectory(
|
||||
base_path('assets/favicons'),
|
||||
public_path(GenerateFavicon::FAVICON_DIR),
|
||||
);
|
||||
|
||||
Cache::flush();
|
||||
|
||||
return view('common::install/finalize')->with([
|
||||
'url' => $appUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getFinalSiteUrl(): string
|
||||
{
|
||||
// config('app.url') will already be updated by "AppUrl" class at this point,
|
||||
// we just need to trim "public" in case .htaccess file was not created for some reason
|
||||
$url = config('app.url');
|
||||
$url = rtrim($url, 'public');
|
||||
return rtrim($url, '/');
|
||||
}
|
||||
|
||||
protected function createHtaccessFiles(): void
|
||||
{
|
||||
$rootHtaccess = base_path('.htaccess');
|
||||
$rootHtaccessStub = base_path('htaccess.example');
|
||||
$publicHtaccess = public_path('.htaccess');
|
||||
$publicHtaccessStub = base_path('public/htaccess.example');
|
||||
|
||||
if (!file_exists($rootHtaccess)) {
|
||||
$contents = file_get_contents($rootHtaccessStub);
|
||||
file_put_contents($rootHtaccess, $contents);
|
||||
}
|
||||
|
||||
if (!file_exists($publicHtaccess)) {
|
||||
$contents = file_get_contents($publicHtaccessStub);
|
||||
file_put_contents($publicHtaccess, $contents);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getEnvWriter(): DotEnvEditor
|
||||
{
|
||||
return new DotEnvEditor(
|
||||
file_exists(base_path('.env')) ? '.env' : 'env.example',
|
||||
);
|
||||
}
|
||||
}
|
||||
22
common/Core/Install/RedirectIfNotInstalledMiddleware.php
Executable file
22
common/Core/Install/RedirectIfNotInstalledMiddleware.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Install;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class RedirectIfNotInstalledMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (
|
||||
!config('common.site.installed') &&
|
||||
!Str::contains($request->path(), 'install')
|
||||
) {
|
||||
return redirect()->route('install');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
61
common/Core/Install/UpdateActions.php
Executable file
61
common/Core/Install/UpdateActions.php
Executable file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Install;
|
||||
|
||||
use Common\Database\MigrateAndSeed;
|
||||
use Common\Settings\DotEnvEditor;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class UpdateActions
|
||||
{
|
||||
public function execute(): void
|
||||
{
|
||||
@ini_set('memory_limit', '-1');
|
||||
@set_time_limit(0);
|
||||
|
||||
//fix "index is too long" issue on MariaDB and older mysql versions
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
app(MigrateAndSeed::class)->execute();
|
||||
|
||||
if (
|
||||
file_exists(base_path('env.example')) &&
|
||||
file_exists(base_path('.env'))
|
||||
) {
|
||||
$envExampleValues = (new DotEnvEditor('env.example'))->load(
|
||||
'env.example',
|
||||
);
|
||||
$currentEnvValues = (new DotEnvEditor())->load();
|
||||
$envValuesToWrite = array_diff_key(
|
||||
$envExampleValues,
|
||||
$currentEnvValues,
|
||||
);
|
||||
$envValuesToWrite['app_version'] = $envExampleValues['app_version'];
|
||||
$envValuesToWrite['installed'] = true;
|
||||
|
||||
// mark mail as setup if app was installed before this setting was added.
|
||||
if (!isset($currentEnvValues['mail_setup'])) {
|
||||
$envValuesToWrite['mail_setup'] = true;
|
||||
}
|
||||
|
||||
if (
|
||||
isset($currentEnvValues['scout_driver']) &&
|
||||
$currentEnvValues['scout_driver'] === 'mysql-like'
|
||||
) {
|
||||
$envValuesToWrite['scout_driver'] = 'mysql';
|
||||
}
|
||||
|
||||
(new DotEnvEditor())->write($envValuesToWrite);
|
||||
}
|
||||
|
||||
Cache::flush();
|
||||
|
||||
// clear cached views
|
||||
$path = config('view.compiled');
|
||||
foreach (File::glob("{$path}/*") as $view) {
|
||||
File::delete($view);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
common/Core/Install/UpdateActionsCommand.php
Executable file
19
common/Core/Install/UpdateActionsCommand.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Install;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class UpdateActionsCommand extends Command
|
||||
{
|
||||
protected $signature = 'update:run';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
(new UpdateActions())->execute();
|
||||
|
||||
$this->info('Update complete');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
43
common/Core/Install/UpdateController.php
Executable file
43
common/Core/Install/UpdateController.php
Executable file
@@ -0,0 +1,43 @@
|
||||
<?php namespace Common\Core\Install;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
use Common\Settings\DotEnvEditor;
|
||||
use Exception;
|
||||
|
||||
class UpdateController extends BaseController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
if (
|
||||
!config('common.site.disable_update_auth') &&
|
||||
version_compare(
|
||||
config('common.site.version'),
|
||||
$this->getAppVersion(),
|
||||
) === 0
|
||||
) {
|
||||
$this->middleware('isAdmin');
|
||||
}
|
||||
}
|
||||
|
||||
public function show()
|
||||
{
|
||||
$data = (new CheckSiteHealth())->execute();
|
||||
return view('common::install/update')->with($data);
|
||||
}
|
||||
|
||||
public function performUpdate()
|
||||
{
|
||||
(new UpdateActions())->execute();
|
||||
|
||||
return view('common::install/update-complete');
|
||||
}
|
||||
|
||||
private function getAppVersion(): string
|
||||
{
|
||||
try {
|
||||
return (new DotEnvEditor('env.example'))->load()['app_version'];
|
||||
} catch (Exception $e) {
|
||||
return config('common.site.version');
|
||||
}
|
||||
}
|
||||
}
|
||||
35
common/Core/Manifest/BuildManifestFile.php
Executable file
35
common/Core/Manifest/BuildManifestFile.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Manifest;
|
||||
|
||||
use Common\Core\AppUrl;
|
||||
use Common\Settings\Settings;
|
||||
|
||||
class BuildManifestFile
|
||||
{
|
||||
public function __construct(protected Settings $settings)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(): void
|
||||
{
|
||||
$primaryColor = config('common.themes.light.--be-primary');
|
||||
$bgColor = config('common.themes.light.--be-background');
|
||||
$replacements = [
|
||||
'DUMMY_NAME' => config('app.name'),
|
||||
'DUMMY_SHORT_NAME' => config('app.name'),
|
||||
'DUMMY_THEME_COLOR' => "rgb($primaryColor)",
|
||||
'DUMMY_BACKGROUND_COLOR' => "rgb($bgColor)",
|
||||
'DUMMY_START_URL' => app(AppUrl::class)->htmlBaseUri,
|
||||
];
|
||||
|
||||
@file_put_contents(
|
||||
public_path('manifest.json'),
|
||||
str_replace(
|
||||
array_keys($replacements),
|
||||
$replacements,
|
||||
file_get_contents(__DIR__ . '/manifest-example.json'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
51
common/Core/Manifest/manifest-example.json
Executable file
51
common/Core/Manifest/manifest-example.json
Executable file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "DUMMY_NAME",
|
||||
"short_name": "DUMMY_SHORT_NAME",
|
||||
"theme_color": "DUMMY_THEME_COLOR",
|
||||
"background_color": "DUMMY_BACKGROUND_COLOR",
|
||||
"display": "standalone",
|
||||
"scope": "DUMMY_START_URL",
|
||||
"start_url": "DUMMY_START_URL",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "favicon/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
common/Core/Middleware/EnableDebugIfLoggedInAsAdmin.php
Executable file
24
common/Core/Middleware/EnableDebugIfLoggedInAsAdmin.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class EnableDebugIfLoggedInAsAdmin
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($this->loggedInAsAdmin()) {
|
||||
config(['app.debug' => true]);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
protected function loggedInAsAdmin(): bool
|
||||
{
|
||||
return Auth::user() && Auth::user()->hasPermission('admin');
|
||||
}
|
||||
}
|
||||
24
common/Core/Middleware/EnsureEmailIsVerified.php
Executable file
24
common/Core/Middleware/EnsureEmailIsVerified.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Auth\Middleware\EnsureEmailIsVerified as LaravelMiddleware;
|
||||
|
||||
class EnsureEmailIsVerified extends LaravelMiddleware
|
||||
{
|
||||
public function handle($request, Closure $next, $redirectToRoute = null)
|
||||
{
|
||||
// bail if user is not logged in, it will be handled by policies
|
||||
// also bail if email verification is disabled from settings page
|
||||
if (
|
||||
!$request->user() ||
|
||||
!app(Settings::class)->get('require_email_confirmation')
|
||||
) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return parent::handle($request, $next, $redirectToRoute);
|
||||
}
|
||||
}
|
||||
45
common/Core/Middleware/EnsureFrontendRequestsAreStateful.php
Executable file
45
common/Core/Middleware/EnsureFrontendRequestsAreStateful.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful as LaravelMiddleware;
|
||||
|
||||
class EnsureFrontendRequestsAreStateful extends LaravelMiddleware
|
||||
{
|
||||
public static function fromFrontend($request): bool
|
||||
{
|
||||
$domain =
|
||||
$request->headers->get('referer') ?:
|
||||
$request->headers->get('origin');
|
||||
|
||||
if (is_null($domain)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// make sure api calls from api docs page are not considered stateful to avoid 419 errors on POST requests
|
||||
if (Str::contains($domain, '/api-docs')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$domain = parse_url($domain, PHP_URL_HOST);
|
||||
$domain = Str::replaceFirst('www.', '', $domain);
|
||||
$domain = Str::endsWith($domain, '/') ? $domain : "{$domain}/";
|
||||
|
||||
$stateful = [
|
||||
...array_filter(config('sanctum.stateful', [])),
|
||||
parse_url(config('app.url'), PHP_URL_HOST),
|
||||
];
|
||||
|
||||
return Str::is(
|
||||
Collection::make($stateful)
|
||||
->map(
|
||||
fn($uri) => Str::replaceFirst('www.', '', trim($uri)) .
|
||||
'/*',
|
||||
)
|
||||
->all(),
|
||||
$domain,
|
||||
);
|
||||
}
|
||||
}
|
||||
25
common/Core/Middleware/IsAdmin.php
Executable file
25
common/Core/Middleware/IsAdmin.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Auth;
|
||||
use Closure;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class IsAdmin
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ( ! Auth::check()) {
|
||||
throw new AuthenticationException();
|
||||
}
|
||||
|
||||
if ( ! Auth::user()->hasPermission('admin')) {
|
||||
throw new AuthorizationException();
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
17
common/Core/Middleware/JsonMiddleware.php
Executable file
17
common/Core/Middleware/JsonMiddleware.php
Executable file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class JsonMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
//$request->headers->set('Content-Type', 'application/json');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
92
common/Core/Middleware/PrerenderIfCrawler.php
Executable file
92
common/Core/Middleware/PrerenderIfCrawler.php
Executable file
@@ -0,0 +1,92 @@
|
||||
<?php namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Common\Core\Controllers\HomeController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PrerenderIfCrawler
|
||||
{
|
||||
protected array $crawlerUserAgents = [
|
||||
'Yahoo! Slurp',
|
||||
'bingbot',
|
||||
'yandex',
|
||||
'baiduspider',
|
||||
'facebookexternalhit',
|
||||
'twitterbot',
|
||||
'rogerbot',
|
||||
'linkedinbot',
|
||||
'embedly',
|
||||
'quora link preview',
|
||||
'showyoubot',
|
||||
'outbrain',
|
||||
'pinterest/0.',
|
||||
'slackbot',
|
||||
'vkShare',
|
||||
'W3C_Validator',
|
||||
'redditbot',
|
||||
'Applebot',
|
||||
'WhatsApp',
|
||||
'flipboard',
|
||||
'tumblr',
|
||||
'bitlybot',
|
||||
'SkypeUriPreview',
|
||||
'nuzzel',
|
||||
'Discordbot',
|
||||
'Qwantify',
|
||||
'pinterestbot',
|
||||
'Bitrix link preview',
|
||||
'XING-contenttabreceiver',
|
||||
'developers.google.com/+/web/snippet',
|
||||
];
|
||||
|
||||
public function handle(
|
||||
Request $request,
|
||||
Closure $next,
|
||||
string $routeName = null
|
||||
) {
|
||||
if ($this->shouldPrerender($request)) {
|
||||
define('SHOULD_PRERENDER', true);
|
||||
|
||||
// Always fallback to client routes if not prerendering
|
||||
// otherwise prerender routes will override client side routing
|
||||
} elseif ($routeName !== 'homepage') {
|
||||
return app(HomeController::class)->show();
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
protected function shouldPrerender(Request $request): bool
|
||||
{
|
||||
$userAgent = strtolower($request->server->get('HTTP_USER_AGENT'));
|
||||
$bufferAgent = $request->server->get('X-BUFFERBOT');
|
||||
|
||||
$shouldPrerender = false;
|
||||
|
||||
if (!$userAgent) {
|
||||
return false;
|
||||
}
|
||||
if (!$request->isMethod('GET')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prerender if _escaped_fragment_ is in the query string
|
||||
if ($request->query->has('_escaped_fragment_')) {
|
||||
$shouldPrerender = true;
|
||||
}
|
||||
|
||||
// prerender if a crawler is detected
|
||||
foreach ($this->crawlerUserAgents as $crawlerUserAgent) {
|
||||
if (Str::contains($userAgent, strtolower($crawlerUserAgent))) {
|
||||
$shouldPrerender = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bufferAgent) {
|
||||
$shouldPrerender = true;
|
||||
}
|
||||
|
||||
return $shouldPrerender;
|
||||
}
|
||||
}
|
||||
177
common/Core/Middleware/RestrictDemoSiteFunctionality.php
Executable file
177
common/Core/Middleware/RestrictDemoSiteFunctionality.php
Executable file
@@ -0,0 +1,177 @@
|
||||
<?php namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class RestrictDemoSiteFunctionality
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (
|
||||
Auth::user() &&
|
||||
Auth::user()->email === 'Ic0OdCIodqz8q1r@demo.com'
|
||||
) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$uri = str_replace(
|
||||
['secure/', 'api/v1/'],
|
||||
'',
|
||||
$request->route()->uri(),
|
||||
);
|
||||
|
||||
if ($this->shouldForbidRequest($request, $uri)) {
|
||||
abort(403, "You can't do that on demo site.");
|
||||
}
|
||||
|
||||
if ($uri === 'settings') {
|
||||
return $this->manglePrivateSettings($next($request));
|
||||
}
|
||||
|
||||
if ($uri === 'users' || $uri === 'billing/subscriptions') {
|
||||
return $this->mangleUserEmails($next($request));
|
||||
}
|
||||
|
||||
if (
|
||||
($uri === 'billing/stripe/cards/add' ||
|
||||
$uri === 'billing/subscriptions/paypal/agreement/create') &&
|
||||
(Auth::user() && Auth::user()->email === 'admin@admin.com')
|
||||
) {
|
||||
abort(403, "Demo admin account can't subscribe to plans.");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if specified request should be forbidden on demo site.
|
||||
*/
|
||||
private function shouldForbidRequest(Request $request, string $uri): bool
|
||||
{
|
||||
$method = $request->method();
|
||||
|
||||
foreach (config('common.demo-blocked-routes') as $route) {
|
||||
if (
|
||||
$method === $route['method'] &&
|
||||
trim($uri) === trim($route['name'])
|
||||
) {
|
||||
$originMatches = true;
|
||||
$paramsMatch = true;
|
||||
|
||||
//block this request only if it originated from specified origin, for example: admin area
|
||||
if (isset($route['origin'])) {
|
||||
$originMatches = Str::contains(
|
||||
$request->server('HTTP_REFERER'),
|
||||
$route['origin'],
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($route['params'])) {
|
||||
$paramsMatch =
|
||||
collect($route['params'])->first(function (
|
||||
$param,
|
||||
$key
|
||||
) use ($request) {
|
||||
$routeParam = $request->route($key);
|
||||
if (is_array($param)) {
|
||||
return in_array($routeParam, $param);
|
||||
} else {
|
||||
return $routeParam == $param;
|
||||
}
|
||||
}) !== null;
|
||||
}
|
||||
|
||||
return $originMatches && $paramsMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mangle settings values, so they are not visible on demo site.
|
||||
*/
|
||||
private function manglePrivateSettings(Response $response): Response
|
||||
{
|
||||
$serverKeys = [
|
||||
'google_id',
|
||||
'google_secret',
|
||||
'twitter_id',
|
||||
'twitter_secret',
|
||||
'facebook_id',
|
||||
'facebook_secret',
|
||||
'spotify_id',
|
||||
'spotify_secret',
|
||||
'lastfm_api_key',
|
||||
'soundcloud_api_key',
|
||||
'sentry_dns',
|
||||
'mailgun_secret',
|
||||
'sentry_dsn',
|
||||
'paypal_client_id',
|
||||
'pusher_key',
|
||||
'pusher_secret',
|
||||
'paypal_secret',
|
||||
'stripe_key',
|
||||
'stripe_secret',
|
||||
'mail_password',
|
||||
'tmdb_api_key',
|
||||
'storage_digitalocean_key',
|
||||
'storage_digitalocean_secret',
|
||||
'stripe_webhook_secret',
|
||||
'openai_api_key',
|
||||
];
|
||||
|
||||
$clientKeys = [
|
||||
'youtube_api_key',
|
||||
'logging.sentry_public',
|
||||
'analytics.google_id',
|
||||
'builder.google_fonts_api_key',
|
||||
'recaptcha.site_key',
|
||||
'recaptcha.secret_key',
|
||||
];
|
||||
|
||||
$settings = json_decode($response->getContent(), true);
|
||||
|
||||
foreach ($serverKeys as $key) {
|
||||
if (isset($settings['server'][$key])) {
|
||||
$settings['server'][$key] = Str::random(30);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($clientKeys as $key) {
|
||||
if (isset($settings['client'][$key])) {
|
||||
$settings['client'][$key] = Str::random(30);
|
||||
}
|
||||
}
|
||||
|
||||
$response->setContent(json_encode($settings));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mangle settings values, so they are not visible on demo site.
|
||||
*/
|
||||
private function mangleUserEmails(Response $response): Response
|
||||
{
|
||||
$pagination = json_decode($response->getContent(), true);
|
||||
|
||||
$pagination['data'] = array_map(function ($item) {
|
||||
if (isset($item['email'])) {
|
||||
$item['email'] = 'hidden@demo.com';
|
||||
} elseif (isset($item['user']['email'])) {
|
||||
$item['user']['email'] = 'hidden@demo.com';
|
||||
}
|
||||
|
||||
return $item;
|
||||
}, Arr::get($pagination, 'data', []));
|
||||
|
||||
$response->setContent(json_encode($pagination));
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
49
common/Core/Middleware/SetAppLocale.php
Executable file
49
common/Core/Middleware/SetAppLocale.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Common\Localizations\Localization;
|
||||
use Common\Localizations\UserLocaleController;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
use Negotiation\LanguageNegotiator;
|
||||
|
||||
class SetAppLocale
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (settings('i18n.enable')) {
|
||||
// 1. Check if current user has manually selected a specific language
|
||||
$langCode =
|
||||
$request->get('lang') ??
|
||||
($request->user()->language ??
|
||||
Cookie::get(UserLocaleController::COOKIE_NAME));
|
||||
|
||||
$defaultLocale = settings('locale.default', 'auto');
|
||||
|
||||
// 2. if admin manually selected a specific default locale, use that
|
||||
if (!$langCode && $defaultLocale && $defaultLocale !== 'auto') {
|
||||
$langCode = $defaultLocale;
|
||||
}
|
||||
|
||||
// 3. Try to use language based on browser settings
|
||||
if (!$langCode && ($header = $request->header('Accept-Language'))) {
|
||||
$languages = Localization::pluck('language');
|
||||
if ($languages->isNotEmpty()) {
|
||||
$bestLanguage = (new LanguageNegotiator())->getBest(
|
||||
$header,
|
||||
$languages->toArray(),
|
||||
);
|
||||
$langCode = $bestLanguage?->getBasePart();
|
||||
}
|
||||
}
|
||||
|
||||
if ($langCode) {
|
||||
app()->setLocale($langCode);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
23
common/Core/Middleware/SetSentryUserMiddleware.php
Executable file
23
common/Core/Middleware/SetSentryUserMiddleware.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Sentry\State\Scope;
|
||||
use function Sentry\configureScope;
|
||||
|
||||
class SetSentryUserMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($user = Auth::user()) {
|
||||
configureScope(function (Scope $scope) use ($user) {
|
||||
$scope->setUser(['email' => $user->email, 'id' => $user->id]);
|
||||
});
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
23
common/Core/Middleware/SimulateSlowConnectionMiddleware.php
Executable file
23
common/Core/Middleware/SimulateSlowConnectionMiddleware.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SimulateSlowConnectionMiddleware
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if ($speed = config('common.site.simulated_connection')) {
|
||||
if ($speed === 'medium') {
|
||||
// 200ms
|
||||
usleep(200000);
|
||||
} elseif ($speed === 'slow') {
|
||||
// 1s
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
26
common/Core/Middleware/TrustProxies.php
Executable file
26
common/Core/Middleware/TrustProxies.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Middleware;
|
||||
|
||||
use Illuminate\Http\Middleware\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
/**
|
||||
* The trusted proxies for this application.
|
||||
*/
|
||||
protected $proxies = '*';
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers =
|
||||
Request::HEADER_X_FORWARDED_FOR |
|
||||
Request::HEADER_X_FORWARDED_HOST |
|
||||
Request::HEADER_X_FORWARDED_PORT |
|
||||
Request::HEADER_X_FORWARDED_PROTO |
|
||||
Request::HEADER_X_FORWARDED_AWS_ELB;
|
||||
}
|
||||
19
common/Core/Policies/AppearancePolicy.php
Executable file
19
common/Core/Policies/AppearancePolicy.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class AppearancePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function index(User $user)
|
||||
{
|
||||
return $user->hasPermission('appearance.index');
|
||||
}
|
||||
|
||||
public function update(User $user)
|
||||
{
|
||||
return $user->hasPermission('appearance.update');
|
||||
}
|
||||
}
|
||||
126
common/Core/Policies/BasePolicy.php
Executable file
126
common/Core/Policies/BasePolicy.php
Executable file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Core\Exceptions\AccessResponseWithAction;
|
||||
use Common\Core\Exceptions\AccessResponseWithPermission;
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
use Illuminate\Http\Request;
|
||||
use Str;
|
||||
|
||||
abstract class BasePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
protected Settings $settings,
|
||||
) {
|
||||
}
|
||||
|
||||
protected function denyWithAction(
|
||||
$message,
|
||||
array|null $action = null,
|
||||
): AccessResponseWithAction {
|
||||
/** @var AccessResponseWithAction $response */
|
||||
$response = AccessResponseWithAction::deny($message);
|
||||
$response->action = $action;
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function storeWithCountRestriction(
|
||||
User $user,
|
||||
string $namespace,
|
||||
): Response {
|
||||
[
|
||||
$relationName,
|
||||
$permission,
|
||||
$singularName,
|
||||
$pluralName,
|
||||
] = $this->parseNamespace($namespace);
|
||||
|
||||
// user can't create resource at all
|
||||
if (!$this->hasPermission($user, $permission)) {
|
||||
return Response::deny();
|
||||
}
|
||||
|
||||
// user is admin, can ignore count restriction
|
||||
if ($user->hasPermission('admin')) {
|
||||
return Response::allow();
|
||||
}
|
||||
|
||||
// user does not have any restriction on maximum resource count
|
||||
$maxCount = $user->getRestrictionValue($permission, 'count');
|
||||
if (!$maxCount) {
|
||||
return Response::allow();
|
||||
}
|
||||
|
||||
// check if user did not go over their max quota
|
||||
if ($user->$relationName->count() >= $maxCount) {
|
||||
$message = __('policies.quota_exceeded', [
|
||||
'resources' => $pluralName,
|
||||
'resource' => $singularName,
|
||||
]);
|
||||
return $this->denyWithAction($message, $this->upgradeAction());
|
||||
}
|
||||
|
||||
return Response::allow();
|
||||
}
|
||||
|
||||
protected function hasPermission(?User $user, string $permission): bool
|
||||
{
|
||||
$model = $user ?: app('guestRole');
|
||||
return $model?->hasPermission($permission) ?? false;
|
||||
}
|
||||
|
||||
protected function authorizePermission(
|
||||
?User $user,
|
||||
string $permission,
|
||||
): AccessResponseWithPermission {
|
||||
return new AccessResponseWithPermission(
|
||||
$this->hasPermission($user, $permission),
|
||||
$permission,
|
||||
'',
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
protected function parseNamespace(
|
||||
string $namespace,
|
||||
string $ability = 'create',
|
||||
): array {
|
||||
// 'App\SomeModel' => 'Some_Model'
|
||||
$resourceName = Str::snake(class_basename($namespace));
|
||||
|
||||
// 'Some_Model' => 'someModels'
|
||||
$relationName = Str::camel(Str::plural($resourceName));
|
||||
|
||||
// 'Some_Model' => 'Some Model'
|
||||
$singularName = str_replace('_', ' ', $resourceName);
|
||||
|
||||
// 'Some Model' => 'Some Models'
|
||||
$pluralName = Str::plural($singularName);
|
||||
|
||||
// parent might need to override permission name. custom_domains instead of links_domains for example.
|
||||
$permissionName = $this->permissionName ?? Str::snake($relationName);
|
||||
|
||||
return [
|
||||
$relationName,
|
||||
"$permissionName.$ability",
|
||||
$singularName,
|
||||
$pluralName,
|
||||
];
|
||||
}
|
||||
|
||||
protected function upgradeAction(): ?array
|
||||
{
|
||||
if ($this->settings->get('billing.enable')) {
|
||||
return ['label' => __('Upgrade'), 'action' => '/pricing'];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
187
common/Core/Policies/FileEntryPolicy.php
Executable file
187
common/Core/Policies/FileEntryPolicy.php
Executable file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Arr;
|
||||
use Common\Files\FileEntry;
|
||||
use Common\Files\FileEntryUser;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class FileEntryPolicy extends BasePolicy
|
||||
{
|
||||
public function index(
|
||||
?User $user,
|
||||
array $entryIds = null,
|
||||
int $userId = null,
|
||||
): bool {
|
||||
if ($entryIds) {
|
||||
return $this->userCan($user, 'files.view', $entryIds);
|
||||
} else {
|
||||
return $user->hasPermission('files.view') || $userId === $user->id;
|
||||
}
|
||||
}
|
||||
|
||||
public function show(?User $user, FileEntry $entry): bool
|
||||
{
|
||||
$token = $this->getAccessTokenFromRequest();
|
||||
|
||||
if ($token) {
|
||||
if ($entry->preview_token === $token) {
|
||||
return true;
|
||||
} elseif (
|
||||
$accessToken = app(PersonalAccessToken::class)->findToken(
|
||||
$token,
|
||||
)
|
||||
) {
|
||||
$user = $accessToken->tokenable;
|
||||
}
|
||||
}
|
||||
|
||||
return $user && $this->userCan($user, 'files.view', $entry);
|
||||
}
|
||||
|
||||
public function download(User $user, $entries): bool
|
||||
{
|
||||
$token = $this->getAccessTokenFromRequest();
|
||||
if ($token) {
|
||||
$previewTokenMatches = collect($entries)->every(function (
|
||||
$entry,
|
||||
) use ($token) {
|
||||
return $entry['preview_token'] === $token;
|
||||
});
|
||||
if ($previewTokenMatches) {
|
||||
return true;
|
||||
} elseif (
|
||||
$accessToken = app(PersonalAccessToken::class)->findToken(
|
||||
$token,
|
||||
)
|
||||
) {
|
||||
$user = $accessToken->tokenable;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->userCan($user, 'files.download', $entries);
|
||||
}
|
||||
|
||||
public function store(User $user, int $parentId = null): bool
|
||||
{
|
||||
//check if user can modify parent entry (if specified)
|
||||
if ($parentId) {
|
||||
return $this->userCan($user, 'files.update', [$parentId]);
|
||||
}
|
||||
|
||||
return $user->hasPermission('files.create');
|
||||
}
|
||||
|
||||
public function update(User $user, Collection|array|FileEntry $entries)
|
||||
{
|
||||
return $this->userCan($user, 'files.update', $entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Collection|array|FileEntry $entries
|
||||
* @return bool
|
||||
*/
|
||||
public function destroy(User $user, $entries)
|
||||
{
|
||||
return $this->userCan($user, 'files.delete', $entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $currentUser
|
||||
* @param string $permission
|
||||
* @param FileEntry|array|Collection $entries
|
||||
* @return bool
|
||||
*/
|
||||
protected function userCan(User $currentUser, string $permission, $entries)
|
||||
{
|
||||
if ($currentUser->hasPermission($permission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$entries = $this->findEntries($entries);
|
||||
|
||||
// extending class might use "findEntries" method so we load users here
|
||||
if (!$entries->every->relationLoaded('users')) {
|
||||
$entries->load([
|
||||
'users' => function (MorphToMany $builder) use ($currentUser) {
|
||||
$builder->where('users.id', $currentUser->id);
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
return $entries->every(function (FileEntry $entry) use (
|
||||
$permission,
|
||||
$currentUser,
|
||||
) {
|
||||
$user = $entry->users->find($currentUser->id);
|
||||
return $this->userOwnsEntryOrWasGrantedPermission(
|
||||
$user,
|
||||
$permission,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param null|array|FileEntryUser $user
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function userOwnsEntryOrWasGrantedPermission(
|
||||
$user,
|
||||
string $permission,
|
||||
) {
|
||||
return $user &&
|
||||
($user['owns_entry'] ||
|
||||
Arr::get(
|
||||
$user['entry_permissions'],
|
||||
$this->sharedFilePermission($permission),
|
||||
));
|
||||
}
|
||||
|
||||
protected function findEntries(
|
||||
FileEntry|array|Collection $entries,
|
||||
): Collection {
|
||||
if ($entries instanceof FileEntry) {
|
||||
return $entries->newCollection([$entries]);
|
||||
} elseif (isset($entries[0]) && is_numeric($entries[0])) {
|
||||
return app(FileEntry::class)
|
||||
->whereIn('id', $entries)
|
||||
->get();
|
||||
} else {
|
||||
return $entries;
|
||||
}
|
||||
}
|
||||
|
||||
protected function sharedFilePermission($fullPermission): string
|
||||
{
|
||||
switch ($fullPermission) {
|
||||
case 'files.view':
|
||||
return 'view';
|
||||
case 'files.create':
|
||||
case 'files.update':
|
||||
return 'edit';
|
||||
case 'files.delete':
|
||||
return 'delete';
|
||||
case 'files.download':
|
||||
return 'download';
|
||||
}
|
||||
}
|
||||
|
||||
protected function getAccessTokenFromRequest(): ?string
|
||||
{
|
||||
if ($token = request()->bearerToken()) {
|
||||
return $token;
|
||||
} elseif ($token = request()->get('preview_token')) {
|
||||
return $token;
|
||||
} elseif ($token = request()->get('accessToken')) {
|
||||
return $token;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
common/Core/Policies/LocalizationPolicy.php
Executable file
34
common/Core/Policies/LocalizationPolicy.php
Executable file
@@ -0,0 +1,34 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class LocalizationPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function index(?User $user)
|
||||
{
|
||||
return $user->hasPermission('localizations.view');
|
||||
}
|
||||
|
||||
public function show(?User $user)
|
||||
{
|
||||
return $user->hasPermission('localizations.view');
|
||||
}
|
||||
|
||||
public function store(User $user)
|
||||
{
|
||||
return $user->hasPermission('localizations.create');
|
||||
}
|
||||
|
||||
public function update(User $user)
|
||||
{
|
||||
return $user->hasPermission('localizations.update');
|
||||
}
|
||||
|
||||
public function destroy(User $user)
|
||||
{
|
||||
return $user->hasPermission('localizations.delete');
|
||||
}
|
||||
}
|
||||
24
common/Core/Policies/MailTemplatePolicy.php
Executable file
24
common/Core/Policies/MailTemplatePolicy.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class MailTemplatePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function index(User $user)
|
||||
{
|
||||
return $user->hasPermission('mail_templates.view');
|
||||
}
|
||||
|
||||
public function show(User $user)
|
||||
{
|
||||
return $user->hasPermission('mail_templates.view');
|
||||
}
|
||||
|
||||
public function update(User $user)
|
||||
{
|
||||
return $user->hasPermission('mail_templates.update');
|
||||
}
|
||||
}
|
||||
40
common/Core/Policies/PagePolicy.php
Executable file
40
common/Core/Policies/PagePolicy.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Pages\CustomPage;
|
||||
|
||||
class PagePolicy extends BasePolicy
|
||||
{
|
||||
public function index(?User $user, int $userId = null)
|
||||
{
|
||||
return $user->hasPermission('custom_pages.view') || $user->id === $userId;
|
||||
}
|
||||
|
||||
public function show(?User $user, CustomPage $customPage)
|
||||
{
|
||||
return $user->hasPermission('custom_pages.view') || $customPage->user_id === $user->id;
|
||||
}
|
||||
|
||||
public function store(User $user)
|
||||
{
|
||||
return $user->hasPermission('custom_pages.create');
|
||||
}
|
||||
|
||||
public function update(User $user)
|
||||
{
|
||||
return $user->hasPermission('custom_pages.update');
|
||||
}
|
||||
|
||||
public function destroy(User $user, $pageIds)
|
||||
{
|
||||
if ($user->hasPermission('custom_pages.delete')) {
|
||||
return true;
|
||||
} else {
|
||||
$dbCount = app(CustomPage::class)
|
||||
->whereIn('id', $pageIds)
|
||||
->where('user_id', $user->id)
|
||||
->count();
|
||||
return $dbCount === count($pageIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
common/Core/Policies/ProductPolicy.php
Executable file
34
common/Core/Policies/ProductPolicy.php
Executable file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
|
||||
class ProductPolicy extends BasePolicy
|
||||
{
|
||||
public function index(?User $user): bool|Response
|
||||
{
|
||||
return settings('billing.enable') || $user->hasPermission('plans.view');
|
||||
}
|
||||
|
||||
public function show(?User $user): bool|Response
|
||||
{
|
||||
return settings('billing.enable') || $user->hasPermission('plans.view');
|
||||
}
|
||||
|
||||
public function store(User $user): bool|Response
|
||||
{
|
||||
return $user->hasPermission('plans.create');
|
||||
}
|
||||
|
||||
public function update(User $user): bool|Response
|
||||
{
|
||||
return $user->hasPermission('plans.update');
|
||||
}
|
||||
|
||||
public function destroy(User $user): bool|Response
|
||||
{
|
||||
return $user->hasPermission('plans.delete');
|
||||
}
|
||||
}
|
||||
14
common/Core/Policies/ReportPolicy.php
Executable file
14
common/Core/Policies/ReportPolicy.php
Executable file
@@ -0,0 +1,14 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ReportPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function index(User $user)
|
||||
{
|
||||
return $user->hasPermission('admin.access');
|
||||
}
|
||||
}
|
||||
35
common/Core/Policies/RolePolicy.php
Executable file
35
common/Core/Policies/RolePolicy.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Auth\Roles\Role;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class RolePolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function index(User $user): bool
|
||||
{
|
||||
return $user->hasPermission('roles.view');
|
||||
}
|
||||
|
||||
public function show(User $user): bool
|
||||
{
|
||||
return $user->hasPermission('roles.show');
|
||||
}
|
||||
|
||||
public function store(User $user): bool
|
||||
{
|
||||
return $user->hasPermission('roles.create');
|
||||
}
|
||||
|
||||
public function update(User $user): bool
|
||||
{
|
||||
return $user->hasPermission('roles.update');
|
||||
}
|
||||
|
||||
public function destroy(User $user, Role $role): bool
|
||||
{
|
||||
return !$role->internal && $user->hasPermission('roles.delete');
|
||||
}
|
||||
}
|
||||
19
common/Core/Policies/SettingPolicy.php
Executable file
19
common/Core/Policies/SettingPolicy.php
Executable file
@@ -0,0 +1,19 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class SettingPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function index(User $user)
|
||||
{
|
||||
return $user->hasPermission('settings.view');
|
||||
}
|
||||
|
||||
public function update(User $user)
|
||||
{
|
||||
return $user->hasPermission('settings.update');
|
||||
}
|
||||
}
|
||||
36
common/Core/Policies/SubscriptionPolicy.php
Executable file
36
common/Core/Policies/SubscriptionPolicy.php
Executable file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class SubscriptionPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function index(User $user)
|
||||
{
|
||||
return $user->hasPermission('subscription.view');
|
||||
}
|
||||
|
||||
public function show(User $user)
|
||||
{
|
||||
return $user->hasPermission('subscription.view');
|
||||
}
|
||||
|
||||
public function store(User $user)
|
||||
{
|
||||
return $user->hasPermission('subscription.create');
|
||||
}
|
||||
|
||||
public function update(User $user)
|
||||
{
|
||||
return $user->hasPermission('subscription.update');
|
||||
}
|
||||
|
||||
public function destroy(User $user)
|
||||
{
|
||||
return $user->hasPermission('subscription.delete');
|
||||
}
|
||||
}
|
||||
33
common/Core/Policies/TagPolicy.php
Executable file
33
common/Core/Policies/TagPolicy.php
Executable file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class TagPolicy extends BasePolicy
|
||||
{
|
||||
public function index(?User $user)
|
||||
{
|
||||
return $this->hasPermission($user, 'tags.view');
|
||||
}
|
||||
|
||||
public function show(?User $user)
|
||||
{
|
||||
return $this->hasPermission($user, 'tags.view');
|
||||
}
|
||||
|
||||
public function store(User $user)
|
||||
{
|
||||
return $this->hasPermission($user, 'tags.create');
|
||||
}
|
||||
|
||||
public function update(User $user)
|
||||
{
|
||||
return $this->hasPermission($user, 'tags.update');
|
||||
}
|
||||
|
||||
public function destroy(User $user)
|
||||
{
|
||||
return $this->hasPermission($user, 'tags.delete');
|
||||
}
|
||||
}
|
||||
56
common/Core/Policies/UserPolicy.php
Executable file
56
common/Core/Policies/UserPolicy.php
Executable file
@@ -0,0 +1,56 @@
|
||||
<?php namespace Common\Core\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserPolicy extends BasePolicy
|
||||
{
|
||||
public function index(?User $user)
|
||||
{
|
||||
return $this->hasPermission($user, 'users.view');
|
||||
}
|
||||
|
||||
public function show(?User $current, User $requested)
|
||||
{
|
||||
return $this->hasPermission($current, 'users.view') ||
|
||||
$current->id === $requested->id;
|
||||
}
|
||||
|
||||
public function store(User $user)
|
||||
{
|
||||
return $this->hasPermission($user, 'users.create');
|
||||
}
|
||||
|
||||
public function update(User $current, User $toUpdate = null)
|
||||
{
|
||||
// user has proper permissions
|
||||
if ($this->hasPermission($current, 'users.update')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// no permissions and not trying to update his own model
|
||||
if (!$toUpdate || $current->id !== $toUpdate->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// user should not be able to change his own permissions or roles
|
||||
if (
|
||||
$this->request->get('permissions') ||
|
||||
$this->request->get('roles')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function destroy(User $user, array $userIds)
|
||||
{
|
||||
$deletingOwnAccount = collect($userIds)->every(function (
|
||||
int $userId
|
||||
) use ($user) {
|
||||
return $userId === $user->id;
|
||||
});
|
||||
|
||||
return $deletingOwnAccount || $this->hasPermission($user, 'users.delete');
|
||||
}
|
||||
}
|
||||
169
common/Core/Prerender/Actions/ReplacePlaceholders.php
Executable file
169
common/Core/Prerender/Actions/ReplacePlaceholders.php
Executable file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Prerender\Actions;
|
||||
|
||||
use Common\Core\Contracts\AppUrlGenerator;
|
||||
use Illuminate\Pagination\AbstractPaginator;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ReplacePlaceholders
|
||||
{
|
||||
private array $allData;
|
||||
|
||||
public function __construct(protected AppUrlGenerator $urls)
|
||||
{
|
||||
}
|
||||
|
||||
public function execute(array|string $config, array $data): array|string
|
||||
{
|
||||
$this->allData = $data;
|
||||
return $this->replace($config);
|
||||
}
|
||||
|
||||
private function replace(array|string $config): array|string
|
||||
{
|
||||
if (is_array($config)) {
|
||||
if (array_key_exists('_ifNotNull', $config)) {
|
||||
if (is_null(Arr::get($this->allData, $config['_ifNotNull']))) {
|
||||
return [];
|
||||
}
|
||||
unset($config['_ifExists']);
|
||||
}
|
||||
|
||||
if (Arr::get($config, '_type') === 'loop') {
|
||||
return $this->replaceLoop($config);
|
||||
} else {
|
||||
return array_map(function ($item) {
|
||||
return $this->replace($item);
|
||||
}, $config);
|
||||
}
|
||||
} else {
|
||||
return $this->replaceString($config, $this->allData);
|
||||
}
|
||||
}
|
||||
|
||||
private function replaceLoop(array $config): array
|
||||
{
|
||||
$dataSelector = strtolower($config['dataSelector']);
|
||||
$loopData = Arr::get($this->allData, $dataSelector);
|
||||
|
||||
// won't be able to access paginator data via dot notation
|
||||
// selector (items.data), need to extract it manually
|
||||
if ($loopData instanceof AbstractPaginator) {
|
||||
$loopData = $loopData->items();
|
||||
}
|
||||
$loopData = collect($loopData);
|
||||
|
||||
// apply filter (if provided), filter will specify which array
|
||||
// prop of loop item should match what value. For example:
|
||||
// ['key' => 'pivot.department', 'value' => 'cast' will get
|
||||
// only cast from movie credits array instead of full credits
|
||||
if ($filter = Arr::get($config, 'filter')) {
|
||||
$loopData = $loopData->filter(function ($loopItem) use ($filter) {
|
||||
return Arr::get($loopItem, $filter['key']) === $filter['value'];
|
||||
});
|
||||
}
|
||||
|
||||
if ($limit = Arr::get($config, 'limit')) {
|
||||
$loopData = $loopData->slice(0, $limit);
|
||||
}
|
||||
|
||||
// if _type is "nested" we only need to return the first item so instead
|
||||
// of nested [['name' => 'foo'], ['name' => 'bar']] only ['name' => 'foo']
|
||||
if ($returnFirstOnly = Arr::get($config, 'returnFirstOnly')) {
|
||||
$loopData->slice(0, 1);
|
||||
}
|
||||
|
||||
$generated = collect($loopData)->map(function ($loopItem) use (
|
||||
$config,
|
||||
) {
|
||||
// make sure template can access data via dot notation (TAG.NAME)
|
||||
// so instead of passing just tag, pass ['tag' => $tag]
|
||||
$model = is_array($loopItem)
|
||||
? modelTypeToNamespace($loopItem['model_type'])
|
||||
: $loopItem;
|
||||
$name = strtolower(class_basename($model));
|
||||
return $this->replaceString($config['template'], [
|
||||
$name => $loopItem,
|
||||
]);
|
||||
});
|
||||
|
||||
return $returnFirstOnly
|
||||
? $generated->first()
|
||||
: $generated->values()->toArray();
|
||||
}
|
||||
|
||||
private function replaceString(mixed $template, array $originalData): mixed
|
||||
{
|
||||
$data = [];
|
||||
foreach ($originalData as $key => $value) {
|
||||
$data[Str::lower($key)] = $value;
|
||||
}
|
||||
|
||||
return preg_replace_callback(
|
||||
'/{{([\w\.\-\?\:]+?)}}/',
|
||||
function ($matches) use ($data) {
|
||||
if (!isset($matches[1])) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
$placeholder = $matches[1];
|
||||
|
||||
// replace site name
|
||||
if ($placeholder === 'site_name') {
|
||||
return config('app.name');
|
||||
}
|
||||
|
||||
// replace base url
|
||||
if ($placeholder === 'url.base') {
|
||||
return url('');
|
||||
}
|
||||
|
||||
// replace by url generator url
|
||||
if (Str::startsWith($placeholder, 'url.')) {
|
||||
// "url.movie" => "movie"
|
||||
$resource = str_replace('url.', '', $placeholder);
|
||||
// "new_releases" => "newReleases"
|
||||
$method = Str::camel($resource);
|
||||
return $this->urls->$method(
|
||||
Arr::get($data, $resource) ?: $data, $data,
|
||||
);
|
||||
}
|
||||
|
||||
// replace placeholder with actual value.
|
||||
// supports dot notation: 'artist.bio.text' as well as ?:
|
||||
$replacement = $this->findUsingDotNotation($data, $placeholder);
|
||||
|
||||
// prefix relative image urls with base site url
|
||||
if ($replacement && Str::startsWith($replacement, 'storage/')) {
|
||||
$replacement = config('app.url') . "/$replacement";
|
||||
url();
|
||||
}
|
||||
|
||||
// return null if we could not replace placeholder
|
||||
if (!$replacement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Str::limit(
|
||||
strip_tags(
|
||||
$this->replaceString($replacement, $data),
|
||||
'<br>',
|
||||
),
|
||||
400,
|
||||
);
|
||||
},
|
||||
$template,
|
||||
);
|
||||
}
|
||||
|
||||
private function findUsingDotNotation(array $data, string $item)
|
||||
{
|
||||
foreach (explode('?:', $item) as $itemVariant) {
|
||||
if ($value = Arr::get($data, $itemVariant)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
common/Core/Prerender/BaseUrlGenerator.php
Executable file
45
common/Core/Prerender/BaseUrlGenerator.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Prerender;
|
||||
|
||||
use Common\Core\Contracts\AppUrlGenerator;
|
||||
use Common\Pages\CustomPage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BaseUrlGenerator implements AppUrlGenerator
|
||||
{
|
||||
const SEPARATOR = '-';
|
||||
|
||||
public function customPage(array|CustomPage $page): string
|
||||
{
|
||||
if (isset($page['page'])) {
|
||||
$originalSlug = $page['page']['slug'];
|
||||
} else {
|
||||
$originalSlug = $page['slug'];
|
||||
}
|
||||
|
||||
$slug = slugify($originalSlug);
|
||||
return url("pages/$slug");
|
||||
}
|
||||
|
||||
public function home(): string
|
||||
{
|
||||
return url('');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Model|array $model
|
||||
*/
|
||||
public function generate($model): string
|
||||
{
|
||||
$method =
|
||||
$model instanceof Model ? $model::MODEL_TYPE : $model['modelType'];
|
||||
return $this->$method($model);
|
||||
}
|
||||
|
||||
public function __call(string $name, array $arguments): string
|
||||
{
|
||||
return url(Str::kebab($name));
|
||||
}
|
||||
}
|
||||
70
common/Core/Prerender/HandlesSeo.php
Executable file
70
common/Core/Prerender/HandlesSeo.php
Executable file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Prerender;
|
||||
|
||||
use Arr;
|
||||
use Common\Core\AppUrl;
|
||||
use Illuminate\Support\Collection;
|
||||
use Request;
|
||||
use Str;
|
||||
|
||||
trait HandlesSeo
|
||||
{
|
||||
protected function handleSeo(
|
||||
array|Collection &$data = [],
|
||||
array $options = []
|
||||
) {
|
||||
if (Request::method() === 'GET') {
|
||||
$data['seo'] = $this->getMetaTags($data, $options) ?: null;
|
||||
}
|
||||
|
||||
if (defined('SHOULD_PRERENDER')) {
|
||||
$viewName =
|
||||
Arr::get($options, 'prerender.view') ?:
|
||||
$this->namespaceFromRouteAction();
|
||||
$viewPath = "prerender.$viewName";
|
||||
$view = null;
|
||||
|
||||
// load view from app views folder or fall back to common views otherwise
|
||||
if (view()->exists($viewPath)) {
|
||||
$view = view($viewPath);
|
||||
} else {
|
||||
$view = view("common::$viewPath");
|
||||
}
|
||||
|
||||
return response(
|
||||
$view->with([
|
||||
'meta' => $data['seo'],
|
||||
'htmlBaseUri' => app(AppUrl::class)->htmlBaseUri,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getMetaTags($data = [], $options = []): ?MetaTags
|
||||
{
|
||||
$namespace = Arr::get(
|
||||
$options,
|
||||
'prerender.config',
|
||||
$this->namespaceFromRouteAction(),
|
||||
);
|
||||
|
||||
if ($seoConfig = config("seo.$namespace")) {
|
||||
$dataForSeo = Arr::get($options, 'prerender.dataForSeo') ?: $data;
|
||||
return new MetaTags($seoConfig, $dataForSeo, $namespace);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function namespaceFromRouteAction(): string
|
||||
{
|
||||
// 'App/Http/Controllers/ArtistController@show'
|
||||
$uses = request()->route()->action['uses'];
|
||||
|
||||
// get resource name and verb from route action
|
||||
preg_match('/\\\(\w+?)Controller@(\w+)$/', $uses, $matches);
|
||||
$resource = Str::kebab($matches[1]);
|
||||
$verb = Str::kebab($matches[2]);
|
||||
return "$resource.$verb";
|
||||
}
|
||||
}
|
||||
274
common/Core/Prerender/MetaTags.php
Executable file
274
common/Core/Prerender/MetaTags.php
Executable file
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Prerender;
|
||||
|
||||
use Arr;
|
||||
use Common\Core\Contracts\AppUrlGenerator;
|
||||
use Common\Core\Prerender\Actions\ReplacePlaceholders;
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Contracts\Support\Arrayable;
|
||||
use Str;
|
||||
|
||||
class MetaTags implements Arrayable
|
||||
{
|
||||
/**
|
||||
* Tag types that can be edited by the user.
|
||||
*/
|
||||
const EDITABLE_TAGS = ['og:title', 'og:description', 'keywords'];
|
||||
|
||||
/**
|
||||
* Data for replacing meta tag config placeholders.
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* Meta tag config before generation.
|
||||
*/
|
||||
protected array $tags = [];
|
||||
|
||||
/**
|
||||
* Final tags for appending to site head.
|
||||
*/
|
||||
protected array $generatedTags = [];
|
||||
|
||||
/**
|
||||
* Namespace for current tag config. "artist.show".
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $namespace;
|
||||
|
||||
public AppUrlGenerator $urls;
|
||||
|
||||
private Settings $settings;
|
||||
|
||||
public function __construct($tags, $data, $namespace)
|
||||
{
|
||||
$this->namespace = $namespace;
|
||||
$this->data = $data;
|
||||
$tags = $this->overrideTagsWithUserValues($tags);
|
||||
$this->tags = array_merge($tags, config('seo.common'));
|
||||
$this->urls = app(AppUrlGenerator::class);
|
||||
$this->settings = app(Settings::class);
|
||||
$this->generatedTags = $this->generateTags();
|
||||
}
|
||||
|
||||
public function toArray()
|
||||
{
|
||||
// remove all tags to which placeholders could not be replaced with actual content
|
||||
$tags = collect($this->getAll())->map(function ($tag) {
|
||||
// ld+json tags will contain child arrays
|
||||
$strings = array_filter($tag, function ($value) {
|
||||
return !is_array($value);
|
||||
});
|
||||
$content = implode($strings);
|
||||
$shouldRemove =
|
||||
Str::contains($content, '{{') && Str::contains($content, '}}');
|
||||
|
||||
// if could not replace title placeholder, return app name as title instead
|
||||
if ($shouldRemove && $tag['nodeName'] === 'title') {
|
||||
$tag['_text'] = config('app.name');
|
||||
$shouldRemove = false;
|
||||
}
|
||||
if ($shouldRemove && Arr::get($tag, 'property') === 'og:title') {
|
||||
$tag['content'] = config('app.name');
|
||||
$shouldRemove = false;
|
||||
}
|
||||
|
||||
return $shouldRemove ? null : $tag;
|
||||
});
|
||||
|
||||
return $tags
|
||||
->filter()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->get('og:title');
|
||||
}
|
||||
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->get('og:description');
|
||||
}
|
||||
|
||||
public function getImage()
|
||||
{
|
||||
return url($this->get('og:image'));
|
||||
}
|
||||
|
||||
public function getMenu(string $position): array
|
||||
{
|
||||
$default = ['items' => []];
|
||||
return Arr::first(
|
||||
settings('menus'),
|
||||
fn($menu) => in_array($position, $menu['positions']),
|
||||
$default,
|
||||
);
|
||||
}
|
||||
|
||||
public function get($value, $prop = 'property')
|
||||
{
|
||||
$tag = Arr::first(
|
||||
$this->generatedTags,
|
||||
function ($tag) use ($prop, $value) {
|
||||
return Arr::get($tag, $prop) === $value;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return Arr::get($tag, 'content');
|
||||
}
|
||||
|
||||
public function getData($selector = null, $default = null)
|
||||
{
|
||||
return Arr::get($this->data, $selector) ?? $default;
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
return $this->generatedTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert specified tag config into a string.
|
||||
*
|
||||
* @param array $tag
|
||||
* @return string
|
||||
*/
|
||||
public function tagToString($tag)
|
||||
{
|
||||
$string = '';
|
||||
|
||||
foreach (Arr::except($tag, 'nodeName') as $key => $value) {
|
||||
$value = is_array($value) ? implode(',', $value) : $value;
|
||||
$string .= "$key=\"$value\" ";
|
||||
}
|
||||
|
||||
return trim($string);
|
||||
}
|
||||
|
||||
private function generateTags(): array
|
||||
{
|
||||
$tags = $this->tags;
|
||||
|
||||
$tags = array_map(function ($tag) {
|
||||
return $this->replacePlaceholders($tag);
|
||||
}, $tags);
|
||||
$tags = $this->removeEmptyValues($tags);
|
||||
$tags = $this->duplicateTags($tags);
|
||||
$tags = $this->escapeValues($tags);
|
||||
|
||||
$tags = array_map(function ($tag) {
|
||||
// set nodeName to <meta> tag, if not already specified
|
||||
if (!array_key_exists('nodeName', $tag)) {
|
||||
$tag['nodeName'] = 'meta';
|
||||
}
|
||||
return $tag;
|
||||
}, $tags);
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
function removeEmptyValues(array &$array): array
|
||||
{
|
||||
foreach ($array as $key => &$value) {
|
||||
if (is_array($value)) {
|
||||
$value = $this->removeEmptyValues($value);
|
||||
}
|
||||
if (empty($value)) {
|
||||
unset($array[$key]);
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
private function replacePlaceholders(array $tag): array
|
||||
{
|
||||
$replacer = app(ReplacePlaceholders::class);
|
||||
|
||||
// if tag does not have "content" or "_text" prop, we can continue
|
||||
if (array_key_exists('content', $tag)) {
|
||||
$tag['content'] = $replacer->execute($tag['content'], $this->data);
|
||||
} elseif (array_key_exists('_text', $tag)) {
|
||||
$tag['_text'] = $replacer->execute($tag['_text'], $this->data);
|
||||
}
|
||||
|
||||
return $tag;
|
||||
}
|
||||
|
||||
private function escapeValues(array $tags): array
|
||||
{
|
||||
// only escape meta tags that have "content" property
|
||||
foreach ($tags as $key => $tag) {
|
||||
if (array_key_exists('content', $tag)) {
|
||||
$tags[$key]['content'] = str_replace(
|
||||
'"',
|
||||
'"',
|
||||
$tag['content'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create duplicate tags from generated tags.
|
||||
* (for example: canonical link from og:url)
|
||||
*
|
||||
* @param array $tags
|
||||
* @return array
|
||||
*/
|
||||
private function duplicateTags($tags)
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!isset($tag['content'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Arr::get($tag, 'property') === 'og:url') {
|
||||
$tags[] = [
|
||||
'nodeName' => 'link',
|
||||
'rel' => 'canonical',
|
||||
'href' => $tag['content'],
|
||||
];
|
||||
}
|
||||
|
||||
if (Arr::get($tag, 'property') === 'og:title') {
|
||||
$tags[] = [
|
||||
'nodeName' => 'title',
|
||||
'_text' => ucfirst($tag['content']),
|
||||
];
|
||||
}
|
||||
|
||||
if (Arr::get($tag, 'property') === 'og:description') {
|
||||
$tags[] = [
|
||||
'name' => 'description',
|
||||
'content' => $tag['content'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
private function overrideTagsWithUserValues(array $metaTags): array
|
||||
{
|
||||
$overrides = app(Settings::class)->all();
|
||||
foreach ($metaTags as $key => $tagConfig) {
|
||||
$property = Arr::get($tagConfig, 'property');
|
||||
$settingKey = "seo.{$this->namespace}.{$property}";
|
||||
if (
|
||||
in_array($property, self::EDITABLE_TAGS) &&
|
||||
array_key_exists($settingKey, $overrides)
|
||||
) {
|
||||
$metaTags[$key]['content'] = $overrides[$settingKey];
|
||||
}
|
||||
}
|
||||
|
||||
return $metaTags;
|
||||
}
|
||||
}
|
||||
74
common/Core/Rendering/DetectsCrawlers.php
Executable file
74
common/Core/Rendering/DetectsCrawlers.php
Executable file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Rendering;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
trait DetectsCrawlers
|
||||
{
|
||||
protected array $crawlerUserAgents = [
|
||||
'Yahoo! Slurp',
|
||||
'bingbot',
|
||||
'yandex',
|
||||
'baiduspider',
|
||||
'facebookexternalhit',
|
||||
'twitterbot',
|
||||
'rogerbot',
|
||||
'linkedinbot',
|
||||
'embedly',
|
||||
'quora link preview',
|
||||
'showyoubot',
|
||||
'outbrain',
|
||||
'pinterest/0.',
|
||||
'slackbot',
|
||||
'vkShare',
|
||||
'W3C_Validator',
|
||||
'redditbot',
|
||||
'Applebot',
|
||||
'WhatsApp',
|
||||
'flipboard',
|
||||
'tumblr',
|
||||
'bitlybot',
|
||||
'SkypeUriPreview',
|
||||
'nuzzel',
|
||||
'Discordbot',
|
||||
'Qwantify',
|
||||
'pinterestbot',
|
||||
'Bitrix link preview',
|
||||
'XING-contenttabreceiver',
|
||||
'developers.google.com/+/web/snippet',
|
||||
];
|
||||
|
||||
protected function isCrawler(): bool
|
||||
{
|
||||
$userAgent = strtolower(request()->server->get('HTTP_USER_AGENT'));
|
||||
$bufferAgent = request()->server->get('X-BUFFERBOT');
|
||||
|
||||
$shouldPrerender = false;
|
||||
|
||||
if (!$userAgent) {
|
||||
return false;
|
||||
}
|
||||
if (!request()->isMethod('GET')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prerender if _escaped_fragment_ is in the query string
|
||||
if (request()->query->has('_escaped_fragment_')) {
|
||||
$shouldPrerender = true;
|
||||
}
|
||||
|
||||
// prerender if a crawler is detected
|
||||
foreach ($this->crawlerUserAgents as $crawlerUserAgent) {
|
||||
if (Str::contains($userAgent, strtolower($crawlerUserAgent))) {
|
||||
$shouldPrerender = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($bufferAgent) {
|
||||
$shouldPrerender = true;
|
||||
}
|
||||
|
||||
return $shouldPrerender;
|
||||
}
|
||||
}
|
||||
77
common/Core/Rendering/RendersClientSideApp.php
Executable file
77
common/Core/Rendering/RendersClientSideApp.php
Executable file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Rendering;
|
||||
|
||||
use Common\Core\AppUrl;
|
||||
use Common\Core\Bootstrap\BootstrapData;
|
||||
use Common\SSR\RenderPageWithNode;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
|
||||
trait RendersClientSideApp
|
||||
{
|
||||
protected function renderClientSideApp(array $options = [])
|
||||
{
|
||||
$bootstrapData = app(BootstrapData::class)->init();
|
||||
$pageData = Arr::get($options, 'pageData', []);
|
||||
// so "PageMetaTags" can know whether default meta tags should be rendered
|
||||
$pageData['set_seo'] = isset($options['seoTagsView']);
|
||||
|
||||
if (isset($pageData['loader'])) {
|
||||
$loader = $pageData['loader'];
|
||||
$bootstrapData->set("loaders.$loader", $pageData);
|
||||
}
|
||||
$ssrContent =
|
||||
isset($options['pageName']) && !Arr::get($options, 'noSSR')
|
||||
? app(RenderPageWithNode::class)->execute($bootstrapData->get())
|
||||
: null;
|
||||
$bootstrapData->set('rendered_ssr', !is_null($ssrContent));
|
||||
|
||||
$view = view('app')
|
||||
->with('pageData', $pageData)
|
||||
->with('devCssPath', $this->getDevCssPath())
|
||||
->with('seoTagsView', $options['seoTagsView'] ?? null)
|
||||
->with('ssrContent', $ssrContent)
|
||||
->with('bootstrapData', $bootstrapData)
|
||||
->with('htmlBaseUri', app(AppUrl::class)->htmlBaseUri)
|
||||
->with(
|
||||
'customHtmlPath',
|
||||
public_path('storage/custom-code/custom-html.html'),
|
||||
)
|
||||
->with(
|
||||
'customCssPath',
|
||||
public_path('storage/custom-code/custom-styles.css'),
|
||||
);
|
||||
|
||||
if (isset($data['seo'])) {
|
||||
$view->with('meta', $data['seo']);
|
||||
}
|
||||
|
||||
return response($view);
|
||||
}
|
||||
|
||||
protected function getDevCssPath(): string|null
|
||||
{
|
||||
if (
|
||||
config('app.env') !== 'local' ||
|
||||
!config('common.site.ssr_enabled') ||
|
||||
!Vite::isRunningHot()
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$manifestPath = public_path('build/manifest.json');
|
||||
if (!file_exists($manifestPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$manifest = json_decode(file_get_contents($manifestPath), true);
|
||||
$cssPath = 'build/' . $manifest['resources/client/main.css']['file'];
|
||||
|
||||
if (file_exists(public_path($cssPath))) {
|
||||
return $cssPath;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
18
common/Core/Resources/BaseJsonResource.php
Executable file
18
common/Core/Resources/BaseJsonResource.php
Executable file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class BaseJsonResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @param mixed $resource
|
||||
* @return AnonymousResourceCollection
|
||||
*/
|
||||
public static function paginated($resource)
|
||||
{
|
||||
return new PaginatedResourceCollection($resource, static::class);
|
||||
}
|
||||
}
|
||||
32
common/Core/Resources/PaginatedResourceCollection.php
Executable file
32
common/Core/Resources/PaginatedResourceCollection.php
Executable file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
|
||||
class PaginatedResourceCollection extends AnonymousResourceCollection
|
||||
{
|
||||
/**
|
||||
* @var LengthAwarePaginator
|
||||
*/
|
||||
public $resource;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'current_page' => $this->resource->currentPage(),
|
||||
'data' => $this->collection,
|
||||
'from' => $this->resource->firstItem(),
|
||||
'last_page' => $this->resource->lastPage(),
|
||||
'per_page' => $this->resource->perPage(),
|
||||
'to' => $this->resource->lastItem(),
|
||||
'total' => $this->total(),
|
||||
];
|
||||
}
|
||||
}
|
||||
124
common/Core/Values/GetStaticPermissions.php
Executable file
124
common/Core/Values/GetStaticPermissions.php
Executable file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Core\Values;
|
||||
|
||||
use Arr;
|
||||
use Common\Settings\Settings;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Str;
|
||||
|
||||
class GetStaticPermissions
|
||||
{
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $fs;
|
||||
|
||||
/**
|
||||
* @param Filesystem $fs
|
||||
*/
|
||||
public function __construct(Filesystem $fs)
|
||||
{
|
||||
$this->fs = $fs;
|
||||
}
|
||||
|
||||
public function execute()
|
||||
{
|
||||
$permissions = array_merge_recursive(
|
||||
$this->fs->getRequire(
|
||||
app('path.common') . '/resources/defaults/permissions.php',
|
||||
),
|
||||
$this->fs->getRequire(resource_path('defaults/permissions.php')),
|
||||
)['all'];
|
||||
|
||||
$compiled = [];
|
||||
foreach ($permissions as $key => $permissionGroup) {
|
||||
// format permissions and add generic description, if needed
|
||||
$compiled[$key] = collect($permissionGroup)->map(function ($item) {
|
||||
if (!is_array($item)) {
|
||||
$item = ['name' => $item];
|
||||
}
|
||||
|
||||
if (!Arr::get($item, 'display_name')) {
|
||||
$item['display_name'] = $this->getDisplayName(
|
||||
$item['name'],
|
||||
);
|
||||
}
|
||||
|
||||
if (!Arr::get($item, 'description')) {
|
||||
$item['description'] = $this->getGenericDescription(
|
||||
$item['name'],
|
||||
);
|
||||
}
|
||||
|
||||
return $item;
|
||||
});
|
||||
}
|
||||
|
||||
// remove billing permissions, if billing functionality is disabled
|
||||
if (
|
||||
isset($compiled['billing_plans']) &&
|
||||
!app(Settings::class)->get('billing.enable')
|
||||
) {
|
||||
unset($compiled['billing_plans']);
|
||||
}
|
||||
|
||||
return $compiled;
|
||||
}
|
||||
|
||||
private function getDisplayName($original)
|
||||
{
|
||||
// files.create => Create Files
|
||||
if (!\Str::contains($original, '.')) {
|
||||
return $original;
|
||||
}
|
||||
[$resource, $action] = explode('.', $original);
|
||||
return ucfirst($action) .
|
||||
' ' .
|
||||
ucwords(str_replace('_', ' ', $resource));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $permission
|
||||
* @return string|null
|
||||
*/
|
||||
private function getGenericDescription($permission)
|
||||
{
|
||||
if (!Str::contains($permission, '.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$resource, $action] = explode('.', $permission);
|
||||
$pluralAction = Str::plural(str_replace('_', ' ', $resource));
|
||||
$verb = $this->getGenericVerb($action, $resource);
|
||||
|
||||
return "Allow $verb $pluralAction, regardless of who is the owner.";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $action
|
||||
* @param string $resource
|
||||
* @return string|null
|
||||
*/
|
||||
private function getGenericVerb($action, $resource)
|
||||
{
|
||||
if ($resource === 'file' && $action === 'create') {
|
||||
return 'uploading new';
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'view':
|
||||
return 'viewing ALL';
|
||||
case 'create':
|
||||
return 'creating new';
|
||||
case 'update':
|
||||
return 'updating ALL';
|
||||
case 'delete':
|
||||
return 'deleting ALL';
|
||||
case 'download':
|
||||
return 'downloading ALL';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
240
common/Core/Values/ValueLists.php
Executable file
240
common/Core/Values/ValueLists.php
Executable file
@@ -0,0 +1,240 @@
|
||||
<?php namespace Common\Core\Values;
|
||||
|
||||
use Common\Admin\Appearance\Themes\CssTheme;
|
||||
use Common\Auth\Permissions\Permission;
|
||||
use Common\Auth\Roles\Role;
|
||||
use Common\Domains\CustomDomain;
|
||||
use Common\Files\FileEntry;
|
||||
use Common\Localizations\Localization;
|
||||
use Common\Pages\CustomPage;
|
||||
use Illuminate\Contracts\Auth\Access\Gate;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Filesystem\Filesystem;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Str;
|
||||
use const App\Providers\WORKSPACED_RESOURCES;
|
||||
|
||||
class ValueLists
|
||||
{
|
||||
public function __construct(
|
||||
protected Filesystem $fs,
|
||||
protected Localization $localization
|
||||
) {
|
||||
}
|
||||
|
||||
public function get(string $names, array $params = []): Collection|array
|
||||
{
|
||||
return collect(explode(',', $names))
|
||||
->mapWithKeys(function ($name) use ($params) {
|
||||
$methodName = Str::studly($name);
|
||||
$value = method_exists($this, $methodName)
|
||||
? $this->$methodName($params)
|
||||
: $this->loadAppValueFile($name, $params);
|
||||
return [$name => $value];
|
||||
})
|
||||
->filter();
|
||||
}
|
||||
|
||||
public function permissions(): Collection
|
||||
{
|
||||
$query = app(Permission::class)->where('type', 'sitewide');
|
||||
|
||||
if (!config('common.site.enable_custom_domains')) {
|
||||
$query->where('group', '!=', 'custom_domains');
|
||||
}
|
||||
|
||||
if (!config('common.site.notifications_integrated')) {
|
||||
$query->where('group', '!=', 'notifications');
|
||||
}
|
||||
|
||||
// TODO: fetch and merge advanced and description from config files here
|
||||
// instead of storing in db. Then can override workspace descriptions and
|
||||
// advanced state easily here from separate config file
|
||||
|
||||
if (!config('common.site.workspaces_integrated')) {
|
||||
$query
|
||||
->where('group', '!=', 'workspaces')
|
||||
->where('group', '!=', 'workspace_members');
|
||||
}
|
||||
|
||||
if (!config('common.site.billing_integrated')) {
|
||||
$query
|
||||
->where('group', '!=', 'plans')
|
||||
->where('group', '!=', 'invoices');
|
||||
}
|
||||
|
||||
if (!Auth::user() || !Auth::user()->hasExactPermission('admin')) {
|
||||
$query->where('name', '!=', 'admin');
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function roles(): Collection
|
||||
{
|
||||
return app(Role::class)
|
||||
->where('type', 'sitewide')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function workspaceRoles(): Collection
|
||||
{
|
||||
return app(Role::class)
|
||||
->where('type', 'workspace')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function workspacePermissions($params = []): Collection
|
||||
{
|
||||
$filters = array_map(function ($resource) {
|
||||
if (is_subclass_of($resource, FileEntry::class)) {
|
||||
return 'files';
|
||||
} elseif (is_subclass_of($resource, CustomDomain::class)) {
|
||||
return 'custom_domains';
|
||||
} else {
|
||||
return Str::snake(Str::pluralStudly(class_basename($resource)));
|
||||
}
|
||||
}, WORKSPACED_RESOURCES);
|
||||
|
||||
return app(Permission::class)
|
||||
->where('type', 'workspace')
|
||||
->orWhere(function (Builder $builder) use ($filters) {
|
||||
$builder->where('type', 'sitewide')->whereIn('group', $filters);
|
||||
})
|
||||
// don't return restrictions for workspace permissions so they
|
||||
// are not shown when creating workspace role from admin area
|
||||
->get([
|
||||
'id',
|
||||
'name',
|
||||
'display_name',
|
||||
'description',
|
||||
'group',
|
||||
'type',
|
||||
])
|
||||
->map(function (Permission $permission) {
|
||||
$permission->description = str_replace(
|
||||
'ALL',
|
||||
'all workspace',
|
||||
$permission->description,
|
||||
);
|
||||
$permission->description = str_replace(
|
||||
'creating new',
|
||||
'creating new workspace',
|
||||
$permission->description,
|
||||
);
|
||||
return $permission;
|
||||
});
|
||||
}
|
||||
|
||||
public function currencies()
|
||||
{
|
||||
return json_decode(
|
||||
$this->fs->get(__DIR__ . '/../../resources/lists/currencies.json'),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
public function timezones()
|
||||
{
|
||||
return json_decode(
|
||||
$this->fs->get(__DIR__ . '/../../resources/lists/timezones.json'),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
public function countries()
|
||||
{
|
||||
return json_decode(
|
||||
$this->fs->get(__DIR__ . '/../../resources/lists/countries.json'),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
public function languages()
|
||||
{
|
||||
return json_decode(
|
||||
$this->fs->get(__DIR__ . '/../../resources/lists/languages.json'),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
public function localizations()
|
||||
{
|
||||
return $this->localization->get(['id', 'name', 'language']);
|
||||
}
|
||||
|
||||
public function googleFonts(): array
|
||||
{
|
||||
$googleFonts = json_decode(
|
||||
$this->fs->get(
|
||||
__DIR__ . '/../../resources/lists/google-fonts.json',
|
||||
),
|
||||
true,
|
||||
);
|
||||
return array_map(function ($font) {
|
||||
return [
|
||||
'family' => $font['family'],
|
||||
'category' => $font['category'],
|
||||
'google' => true,
|
||||
];
|
||||
}, $googleFonts);
|
||||
}
|
||||
|
||||
public function menuItemCategories(): array
|
||||
{
|
||||
return array_map(function ($category) {
|
||||
$category['items'] = app($category['itemsLoader'])->execute();
|
||||
unset($category['itemsLoader']);
|
||||
return $category;
|
||||
}, config('common.menus'));
|
||||
}
|
||||
|
||||
public function pages($params = [])
|
||||
{
|
||||
if (!isset($params['userId'])) {
|
||||
app(Gate::class)->authorize('index', CustomPage::class);
|
||||
}
|
||||
|
||||
$query = app(CustomPage::class)
|
||||
->select(['id', 'title'])
|
||||
->where(
|
||||
'type',
|
||||
Arr::get($params, 'pageType') ?: CustomPage::PAGE_TYPE,
|
||||
);
|
||||
|
||||
if ($userId = Arr::get($params, 'userId')) {
|
||||
$query->where('user_id', $userId);
|
||||
}
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function domains(Collection|array $params): Collection
|
||||
{
|
||||
return app(CustomDomain::class)
|
||||
->select(['host', 'id'])
|
||||
->where('user_id', Arr::get($params, 'userId'))
|
||||
->orWhere('global', true)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function themes(Collection|array $params): Collection
|
||||
{
|
||||
app(Gate::class)->authorize('index', CssTheme::class);
|
||||
return app(CssTheme::class)
|
||||
->select(['name', 'id'])
|
||||
->get();
|
||||
}
|
||||
|
||||
private function loadAppValueFile(string $name, array $params): ?array
|
||||
{
|
||||
$fileName = Str::kebab($name);
|
||||
$path = resource_path("lists/$fileName.json");
|
||||
if (file_exists($path)) {
|
||||
return json_decode(file_get_contents($path), true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
12
common/Core/Values/ValueListsController.php
Executable file
12
common/Core/Values/ValueListsController.php
Executable file
@@ -0,0 +1,12 @@
|
||||
<?php namespace Common\Core\Values;
|
||||
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class ValueListsController extends BaseController
|
||||
{
|
||||
public function index(string $names)
|
||||
{
|
||||
$values = app(ValueLists::class)->get($names, request()->all());
|
||||
return $this->success($values);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user