JFIFxxC      C  " }!1AQa"q2#BR$3br %&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz w!1AQaq"2B #3Rbr{ gilour
<?php namespace App\Actions\Link; use App\Exceptions\LinkRedirectFailed; use App\Models\Biolink; use App\Models\Link; use App\Models\LinkDomain; use App\Models\LinkeableRule; use App\Models\LinkGroup; use App\Notifications\ClickQuotaExhausted; use Common\Core\AppUrl; use Common\Domains\CustomDomain; use Illuminate\Notifications\DatabaseNotification; use Str; use Symfony\Component\Mailer\Exception\TransportException; class LinkeablePublicPolicy { private string $modelName; public function __construct(protected AppUrl $appUrl) { } public function isAccessible(Link|LinkGroup|Biolink $model): bool { $this->modelName = (string) Str::of(get_class($model)) ->classBasename() ->snake() ->replace('_', ' ') ->title(); return $this->isValidDomain($model) && !$this->pastExpirationDateOrClicks($model) && !$this->overClickQuota($model) && !$this->isDisabled($model) && !$model->user?->isBanned(); } public static function linkeableExpired(Link|LinkGroup|Biolink $model): bool { return $model->expires_at && $model->expires_at->lessThan(now()); } public static function linkeableWillActivateLater( Link|LinkGroup|Biolink $model, ): bool { return $model->activates_at && $model->activates_at->isAfter(now()); } private function pastExpirationDateOrClicks( Link|LinkGroup|Biolink $model, ): bool { if (static::linkeableExpired($model)) { throw (new LinkRedirectFailed( "$this->modelName is past its expiration date ($model->expires_at)", ))->setModel($model); } if (static::linkeableWillActivateLater($model)) { throw (new LinkRedirectFailed( "$this->modelName is set to activate on ($model->activates_at)", ))->setModel($model); } $expClicksRule = $model->rules->first(function (LinkeableRule $rule) { return $rule->type === 'exp_clicks'; }); if ( $expClicksRule && (int) $expClicksRule->key <= $model->clicks_count ) { $msg = "$this->modelName is past it's specified expiration clicks ($expClicksRule->key)."; if ($expClicksRule->value) { $msg .= " Visits to this $this->modelName will redirect to '$expClicksRule->value'."; } throw (new LinkRedirectFailed($msg)) ->setModel($model) ->setRedirectUrl($expClicksRule->value); } return false; } private function isValidDomain(Link|LinkGroup|Biolink $model): bool { $defaultHost = settings('custom_domains.default_host') ?: $this->appUrl->originalAppUrl; $defaultHost = $this->appUrl->getHostFrom($defaultHost); $requestHost = $this->appUrl->getRequestHost(); // link should only be accessible via single domain if ($model->domain_id > 0) { $domain = LinkDomain::forUser($model->user_id)->find( $model->domain_id, ); if (!$domain || !$this->appUrl->requestHostMatches($domain->host)) { throw (new LinkRedirectFailed( "$this->modelName is set to only be accessible via '$domain?->host', but request domain is '$requestHost'", ))->setModel($model); } } // link should be accessible via default domain only elseif ($model->domain_id === 0) { if (!$this->appUrl->requestHostMatches($defaultHost)) { throw (new LinkRedirectFailed( "$this->modelName is set to only be accessible via '$defaultHost' (default domain), but request domain is '$requestHost'", ))->setModel($model); } } // link should be accessible via default and all user connected domains else { if ($this->appUrl->requestHostMatches($defaultHost, true)) { return true; } $domains = LinkDomain::forUser($model->user_id)->get(); $customDomainMatches = $domains->contains(function ( CustomDomain $domain, ) { return $this->appUrl->requestHostMatches($domain->host); }); if (!$customDomainMatches) { throw (new LinkRedirectFailed( "Current domain '$requestHost' does not match default domain or any custom domains connected by user.", ))->setModel($model); } } return true; } private function overClickQuota(Link|LinkGroup|Biolink $model): bool { // link might not be attached to user if (!$model->user) { return false; } $quota = $model->user->getRestrictionValue( 'links.create', 'click_count', ); if (is_null($quota)) { return false; } $totalClicks = app(GetMonthlyClicks::class)->execute($model->user); if ($quota < $totalClicks) { $alreadyNotifiedThisMonth = app(DatabaseNotification::class) ->where('type', ClickQuotaExhausted::class) ->whereBetween('created_at', [ now()->startOfMonth(), now()->endOfMonth(), ]) ->exists(); if (!$alreadyNotifiedThisMonth) { try { $model->user->notify(new ClickQuotaExhausted()); } catch (TransportException $e) { // } } throw (new LinkRedirectFailed( "User this $this->modelName belongs to is over their click quota for the month.", ))->setModel($model); } return false; } private function isDisabled(Link|LinkGroup|Biolink $model): bool { if (!$model->active) { throw (new LinkRedirectFailed( "This $this->modelName is disabled and will not redirect user to destination url.", ))->setModel($model); } return false; } }