diff --git a/app/Actions/Album/SetProtectionPolicy.php b/app/Actions/Album/SetProtectionPolicy.php index 5c604a63866..91a483f79dc 100644 --- a/app/Actions/Album/SetProtectionPolicy.php +++ b/app/Actions/Album/SetProtectionPolicy.php @@ -30,7 +30,7 @@ class SetProtectionPolicy extends Action public function do(BaseAlbum $album, AlbumProtectionPolicy $protectionPolicy, bool $shallSetPassword, ?string $password): void { $album->is_nsfw = $protectionPolicy->is_nsfw; - $active_permissions = $album->public_permissions; + $active_permissions = $album->public_permissions(); if (!$protectionPolicy->is_public) { $active_permissions?->delete(); @@ -58,9 +58,8 @@ public function do(BaseAlbum $album, AlbumProtectionPolicy $protectionPolicy, bo $active_permissions->password = null; } } - $album->public_permissions()->save($active_permissions); + $active_permissions->base_album_id = $album->id; $active_permissions->save(); - // $album->save(); // Reset permissions for photos $album->photos()->update(['photos.is_public' => false]); diff --git a/app/Actions/Album/Unlock.php b/app/Actions/Album/Unlock.php index 5f7571e55ea..863d719f889 100644 --- a/app/Actions/Album/Unlock.php +++ b/app/Actions/Album/Unlock.php @@ -32,8 +32,8 @@ public function __construct() */ public function do(BaseAlbum $album, string $password): void { - if ($album->public_permissions !== null) { - $album_password = $album->public_permissions->password; + if ($album->public_permissions() !== null) { + $album_password = $album->public_permissions()->password; if ( $album_password === null || $album_password === '' || diff --git a/app/Actions/Albums/Top.php b/app/Actions/Albums/Top.php index 7489879343a..3ce860c01a0 100644 --- a/app/Actions/Albums/Top.php +++ b/app/Actions/Albums/Top.php @@ -74,7 +74,7 @@ public function get(): TopAlbumsResource } $tagAlbumQuery = $this->albumQueryPolicy - ->applyVisibilityFilter(TagAlbum::query()->with(['owner'])); + ->applyVisibilityFilter(TagAlbum::query()->with(['access_permissions', 'owner'])); /** @var BaseCollection $tagAlbums */ $tagAlbums = (new SortingDecorator($tagAlbumQuery)) ->orderBy($this->sorting->column, $this->sorting->order) @@ -82,7 +82,7 @@ public function get(): TopAlbumsResource /** @var NsQueryBuilder $query */ $query = $this->albumQueryPolicy - ->applyVisibilityFilter(Album::query()->with(['owner'])->whereIsRoot()); + ->applyVisibilityFilter(Album::query()->with(['access_permissions', 'owner'])->whereIsRoot()); $userID = Auth::id(); if ($userID !== null) { diff --git a/app/Assets/ArrayToTextTable.php b/app/Assets/ArrayToTextTable.php new file mode 100644 index 00000000000..f2fce113814 --- /dev/null +++ b/app/Assets/ArrayToTextTable.php @@ -0,0 +1,393 @@ + + * @copyright Copyright (c) 2015 Mathieu Viossat + * @license http://opensource.org/licenses/MIT + * + * @see https://github.com/MathieuViossat/arraytotexttable + */ + +namespace App\Assets; + +use Laminas\Text\Table\Decorator\DecoratorInterface; +use Laminas\Text\Table\Decorator\Unicode; +use Safe\Exceptions\MbstringException; +use Safe\Exceptions\PcreException; +use function Safe\mb_internal_encoding; +use function Safe\preg_match_all; + +class ArrayToTextTable +{ + public const ALIGNLEFT = STR_PAD_RIGHT; + public const ALIGNCENTER = STR_PAD_BOTH; + public const ALIGNRIGHT = STR_PAD_LEFT; + + /** @var array */ + protected array $data; + /** @var array */ + protected array $keys; + /** @var array */ + protected array $widths; + protected DecoratorInterface $decorator; + protected string $indentation; + protected bool|string $displayKeys; + /** @var array */ + protected array $ignoredKeys; + protected bool $upperKeys; + protected int $keysAlignment; + protected int $valuesAlignment; + protected ?\Closure $formatter; + + /** + * Create a table. + * + * @param array $rawData + * + * @return void + */ + public function __construct(array $rawData = []) + { + $this->setData($rawData) + ->setDecorator(new Unicode()) + ->setIgnoredKeys([]) + ->setIndentation('') + ->setDisplayKeys('auto') + ->setUpperKeys(true) + ->setKeysAlignment(self::ALIGNCENTER) + ->setValuesAlignment(self::ALIGNLEFT) + ->setFormatter(null); + } + + public function __toString(): string + { + return $this->getTable(); + } + + /** + * return the table. + * + * @param array|null $rawData + * + * @return string + * + * @throws PcreException + * @throws MbstringException + */ + public function getTable(?array $rawData = null): string + { + if (!is_null($rawData)) { + $this->setData($rawData); + } + + $data = $this->prepare(); + $i = $this->indentation; + $d = $this->decorator; + + $displayKeys = $this->displayKeys; + if ($displayKeys === 'auto') { + $displayKeys = false; + foreach ($this->keys as $key) { + if (!is_int($key)) { + $displayKeys = true; + break; + } + } + } + + $table = $i . $this->line($d->getTopLeft(), $d->getHorizontal(), $d->getHorizontalDown(), $d->getTopRight()) . PHP_EOL; + + if ($displayKeys === true || $displayKeys === 'auto') { + $keysRow = array_combine($this->keys, $this->keys); + if ($this->upperKeys) { + $keysRow = array_map('mb_strtoupper', $keysRow); + } + $table .= $i . implode(PHP_EOL, $this->row($keysRow, $this->keysAlignment)) . PHP_EOL; + + $table .= $i . $this->line($d->getVerticalRight(), $d->getHorizontal(), $d->getCross(), $d->getVerticalLeft()) . PHP_EOL; + } + + foreach ($data as $row) { + $table .= $i . implode(PHP_EOL, $this->row($row, $this->valuesAlignment)) . PHP_EOL; + } + + $table .= $i . $this->line($d->getBottomLeft(), $d->getHorizontal(), $d->getHorizontalUp(), $d->getBottomRight()) . PHP_EOL; + + return $table; + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + public function getDecorator(): DecoratorInterface + { + return $this->decorator; + } + + public function getIndentation(): string + { + return $this->indentation; + } + + public function getDisplayKeys(): bool|string + { + return $this->displayKeys; + } + + public function getUpperKeys(): bool + { + return $this->upperKeys; + } + + public function getKeysAlignment(): int + { + return $this->keysAlignment; + } + + public function getValuesAlignment(): int + { + return $this->valuesAlignment; + } + + public function getFormatter(): ?\Closure + { + return $this->formatter; + } + + public function getIgnoredKeys(): array + { + return $this->ignoredKeys; + } + + /** + * @param array|null $data + * + * @return self + */ + public function setData(array|null $data): self + { + if (!is_array($data)) { + $data = []; + } + + $arrayData = []; + foreach ($data as $row) { + if (is_array($row)) { + $arrayData[] = $row; + } elseif (is_object($row)) { + $arrayData[] = get_object_vars($row); + } + } + + $this->data = $arrayData; + + return $this; + } + + public function setDecorator(DecoratorInterface $decorator): self + { + $this->decorator = $decorator; + + return $this; + } + + public function setIndentation(string $indentation): self + { + $this->indentation = $indentation; + + return $this; + } + + public function setDisplayKeys(string|bool $displayKeys): self + { + $this->displayKeys = $displayKeys; + + return $this; + } + + public function setUpperKeys(bool $upperKeys): self + { + $this->upperKeys = $upperKeys; + + return $this; + } + + public function setKeysAlignment(int $keysAlignment): self + { + $this->keysAlignment = $keysAlignment; + + return $this; + } + + public function setValuesAlignment(int $valuesAlignment): self + { + $this->valuesAlignment = $valuesAlignment; + + return $this; + } + + public function setFormatter(?\Closure $formatter): self + { + $this->formatter = $formatter; + + return $this; + } + + /** + * @param array $ignoredKeys + * + * @return ArrayToTextTable + */ + public function setIgnoredKeys(array $ignoredKeys): self + { + $this->ignoredKeys = $ignoredKeys; + + return $this; + } + + protected function line(string $left, string $horizontal, string $link, string $right): string + { + $line = $left; + foreach ($this->keys as $key) { + if (!in_array($key, $this->ignoredKeys, true)) { + $line .= str_repeat($horizontal, $this->widths[$key] + 2) . $link; + } + } + + if (mb_strlen($line) > mb_strlen($left)) { + $line = mb_substr($line, 0, -mb_strlen($horizontal)); + } + + return $line . $right; + } + + protected function row(array $row, int $alignment): array + { + $data = []; + $height = 1; + foreach ($this->keys as $key) { + $data[$key] = isset($row[$key]) ? static::valueToLines($row[$key]) : ['']; + $height = max($height, count($data[$key])); + } + + $rowLines = []; + for ($i = 0; $i < $height; $i++) { + $rowLine = []; + foreach ($data as $key => $value) { + $rowLine[$key] = isset($value[$i]) ? $value[$i] : ''; + } + $rowLines[] = $this->rowLine($rowLine, $alignment); + } + + return $rowLines; + } + + protected function rowLine(array $row, int $alignment): string + { + $line = $this->decorator->getVertical(); + + foreach ($row as $key => $value) { + if (!in_array($key, $this->ignoredKeys, true)) { + $line .= ' ' . static::mb_str_pad($value, $this->widths[$key], ' ', $alignment) . ' ' . $this->decorator->getVertical(); + } + } + + if (count($row) === 0) { + $line .= $this->decorator->getVertical(); + } + + return $line; + } + + /** + * @return array + * + * @throws PcreException + */ + protected function prepare(): array + { + $this->keys = []; + $this->widths = []; + + $data = $this->data; + + if ($this->formatter instanceof \Closure) { + foreach ($data as &$row) { + array_walk($row, $this->formatter, $this); + } + unset($row); + } + + foreach ($data as $row) { + $this->keys = array_merge($this->keys, array_keys($row)); + } + $this->keys = array_unique($this->keys); + + foreach ($this->keys as $key) { + $this->setWidth($key, $key); + } + + foreach ($data as $row) { + foreach ($row as $columnKey => $columnValue) { + $this->setWidth($columnKey, $columnValue); + } + } + + return $data; + } + + protected static function countCJK(string $string): int + { + return preg_match_all('/[\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}]/u', $string); + } + + protected function setWidth(string $key, ?string $value): void + { + if (!isset($this->widths[$key])) { + $this->widths[$key] = 0; + } + + foreach (static::valueToLines($value) as $line) { + $width = mb_strlen($line) + self::countCJK($line); + if ($width > $this->widths[$key]) { + $this->widths[$key] = $width; + } + } + } + + protected static function valueToLines(?string $value): array + { + return explode("\n", $value); + } + + protected static function mb_str_pad( + string $input, + int $pad_length, + string $pad_string = ' ', + int $pad_type = STR_PAD_RIGHT, + string|null $encoding = null + ): string { + /** @var string $encoding */ + $encoding = $encoding === null ? mb_internal_encoding() : $encoding; + $pad_before = $pad_type === STR_PAD_BOTH || $pad_type === STR_PAD_LEFT; + $pad_after = $pad_type === STR_PAD_BOTH || $pad_type === STR_PAD_RIGHT; + $pad_length -= mb_strlen($input, $encoding) + self::countCJK($input); + $target_length = $pad_before && $pad_after ? $pad_length / 2 : $pad_length; + + $repeat_times = (int) ceil($target_length / mb_strlen($pad_string, $encoding)); + $repeated_string = str_repeat($pad_string, max(0, $repeat_times)); + $before = $pad_before ? mb_substr($repeated_string, 0, (int) floor($target_length), $encoding) : ''; + $after = $pad_after ? mb_substr($repeated_string, 0, (int) ceil($target_length), $encoding) : ''; + + return $before . $input . $after; + } +} diff --git a/app/Contracts/Models/AbstractAlbum.php b/app/Contracts/Models/AbstractAlbum.php index 403f7f9cd1b..f9c87c90161 100644 --- a/app/Contracts/Models/AbstractAlbum.php +++ b/app/Contracts/Models/AbstractAlbum.php @@ -28,8 +28,6 @@ * @property string $title * @property Collection $photos * @property Thumb|null $thumb - * @property AccessPermission|null $current_permissions - * @property AccessPermission|null $public_permissions * @property Collection $access_permissions */ interface AbstractAlbum extends \JsonSerializable, Arrayable, Jsonable @@ -38,4 +36,11 @@ interface AbstractAlbum extends \JsonSerializable, Arrayable, Jsonable * @return Relation|Builder */ public function photos(): Relation|Builder; + + /** + * Returns the permissions for the public user. + * + * @return ?AccessPermission + */ + public function public_permissions(): AccessPermission|null; } diff --git a/app/DTO/AlbumProtectionPolicy.php b/app/DTO/AlbumProtectionPolicy.php index 631a2ef8836..2c8ff95616c 100644 --- a/app/DTO/AlbumProtectionPolicy.php +++ b/app/DTO/AlbumProtectionPolicy.php @@ -43,12 +43,12 @@ public function __construct( public static function ofBaseAlbumImplementation(BaseAlbumImpl $baseAlbum): self { return new self( - is_public: $baseAlbum->public_permissions !== null, - is_link_required: $baseAlbum->public_permissions?->is_link_required === true, + is_public: $baseAlbum->public_permissions() !== null, + is_link_required: $baseAlbum->public_permissions()?->is_link_required === true, is_nsfw: $baseAlbum->is_nsfw, - grants_full_photo_access: $baseAlbum->public_permissions?->grants_full_photo_access === true, - grants_download: $baseAlbum->public_permissions?->grants_download === true, - is_password_required: $baseAlbum->public_permissions?->password !== null, + grants_full_photo_access: $baseAlbum->public_permissions()?->grants_full_photo_access === true, + grants_download: $baseAlbum->public_permissions()?->grants_download === true, + is_password_required: $baseAlbum->public_permissions()?->password !== null, ); } @@ -62,12 +62,12 @@ public static function ofBaseAlbumImplementation(BaseAlbumImpl $baseAlbum): self public static function ofBaseAlbum(BaseAlbum $baseAlbum): self { return new self( - is_public: $baseAlbum->public_permissions !== null, - is_link_required: $baseAlbum->public_permissions?->is_link_required === true, + is_public: $baseAlbum->public_permissions() !== null, + is_link_required: $baseAlbum->public_permissions()?->is_link_required === true, is_nsfw: $baseAlbum->is_nsfw, - grants_full_photo_access: $baseAlbum->public_permissions?->grants_full_photo_access === true, - grants_download: $baseAlbum->public_permissions?->grants_download === true, - is_password_required: $baseAlbum->public_permissions?->password !== null, + grants_full_photo_access: $baseAlbum->public_permissions()?->grants_full_photo_access === true, + grants_download: $baseAlbum->public_permissions()?->grants_download === true, + is_password_required: $baseAlbum->public_permissions()?->password !== null, ); } @@ -81,11 +81,11 @@ public static function ofBaseAlbum(BaseAlbum $baseAlbum): self public static function ofSmartAlbum(BaseSmartAlbum $baseSmartAlbum): self { return new self( - is_public: $baseSmartAlbum->public_permissions !== null, + is_public: $baseSmartAlbum->public_permissions() !== null, is_link_required: false, is_nsfw: false, - grants_full_photo_access: $baseSmartAlbum->public_permissions?->grants_full_photo_access === true, - grants_download: $baseSmartAlbum->public_permissions?->grants_download === true, + grants_full_photo_access: $baseSmartAlbum->public_permissions()?->grants_full_photo_access === true, + grants_download: $baseSmartAlbum->public_permissions()?->grants_download === true, is_password_required: false, ); } diff --git a/app/Factories/AlbumFactory.php b/app/Factories/AlbumFactory.php index dfdb6f98415..f53d00917a8 100644 --- a/app/Factories/AlbumFactory.php +++ b/app/Factories/AlbumFactory.php @@ -84,7 +84,7 @@ public function findBaseAlbumOrFail(string $albumId, bool $withRelations = true) $tagAlbumQuery = TagAlbum::query(); if ($withRelations) { - $albumQuery->with(['photos', 'children', 'photos.size_variants']); + $albumQuery->with(['access_permissions', 'photos', 'children', 'photos.size_variants']); $tagAlbumQuery->with(['photos']); } diff --git a/app/Http/Controllers/Administration/SettingsController.php b/app/Http/Controllers/Administration/SettingsController.php index 09ba0116189..537911545ea 100644 --- a/app/Http/Controllers/Administration/SettingsController.php +++ b/app/Http/Controllers/Administration/SettingsController.php @@ -130,14 +130,14 @@ public function setSmartAlbumVisibility(SetSmartAlbumVisibilityRequest $request) { /** @var BaseSmartAlbum $album */ $album = $request->album(); - if ($request->is_public() && $album->public_permissions === null) { + if ($request->is_public() && $album->public_permissions() === null) { $access_permissions = AccessPermission::ofPublic(); $access_permissions->base_album_id = $album->id; $access_permissions->save(); } - if (!$request->is_public() && $album->public_permissions !== null) { - $perm = $album->public_permissions; + if (!$request->is_public() && $album->public_permissions() !== null) { + $perm = $album->public_permissions(); $perm->delete(); } } diff --git a/app/Http/Requests/Album/GetAlbumRequest.php b/app/Http/Requests/Album/GetAlbumRequest.php index 690ae905b55..7021591250c 100644 --- a/app/Http/Requests/Album/GetAlbumRequest.php +++ b/app/Http/Requests/Album/GetAlbumRequest.php @@ -31,7 +31,7 @@ public function authorize(): bool if ( !$result && $this->album instanceof BaseAlbum && - $this->album->public_permissions?->password !== null + $this->album->public_permissions()?->password !== null ) { throw new PasswordRequiredException(); } diff --git a/app/Http/Resources/ConfigurationResource.php b/app/Http/Resources/ConfigurationResource.php index b9d62a02353..2138c5f7687 100644 --- a/app/Http/Resources/ConfigurationResource.php +++ b/app/Http/Resources/ConfigurationResource.php @@ -111,9 +111,9 @@ public function toArray($request): array 'unlock_password_photos_with_url_param' => Configs::getValueAsBool('unlock_password_photos_with_url_param'), 'use_last_modified_date_when_no_exif_date' => Configs::getValueAsBool('use_last_modified_date_when_no_exif_date'), 'smart_album_visibilty' => [ - 'recent' => RecentAlbum::getInstance()->public_permissions !== null, - 'starred' => StarredAlbum::getInstance()->public_permissions !== null, - 'on_this_day' => OnThisDayAlbum::getInstance()->public_permissions !== null, + 'recent' => RecentAlbum::getInstance()->public_permissions() !== null, + 'starred' => StarredAlbum::getInstance()->public_permissions() !== null, + 'on_this_day' => OnThisDayAlbum::getInstance()->public_permissions() !== null, ], ]), diff --git a/app/Models/BaseAlbumImpl.php b/app/Models/BaseAlbumImpl.php index 9a9bac97305..34bbfbcb3b5 100644 --- a/app/Models/BaseAlbumImpl.php +++ b/app/Models/BaseAlbumImpl.php @@ -21,7 +21,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Support\Facades\Auth; /** @@ -89,25 +88,22 @@ * but this class is not a proper parent class (it just provides an * implementation of it) and we need this class to be instantiable. * - * @property string $id - * @property int $legacy_id - * @property Carbon $created_at - * @property Carbon $updated_at - * @property string $title - * @property string|null $description - * @property int $owner_id - * @property User $owner - * @property bool $is_nsfw - * @property Collection $shared_with - * @property int|null $shared_with_count - * @property PhotoSortingCriterion|null $sorting - * @property string|null $sorting_col - * @property string|null $sorting_order - * @property hasMany $access_permissions - * @property AccessPermission|null $current_permissions - * @property AccessPermission|null $public_permissions - * @property int|null $access_permissions_count - * @property AccessPermission|null $current_user_permissions + * @property string $id + * @property int $legacy_id + * @property Carbon $created_at + * @property Carbon $updated_at + * @property string $title + * @property string|null $description + * @property int $owner_id + * @property User $owner + * @property bool $is_nsfw + * @property Collection $shared_with + * @property int|null $shared_with_count + * @property PhotoSortingCriterion|null $sorting + * @property string|null $sorting_col + * @property string|null $sorting_order + * @property Collection $access_permissions + * @property int|null $access_permissions_count * * @method static BaseAlbumImplBuilder|BaseAlbumImpl addSelect($column) * @method static BaseAlbumImplBuilder|BaseAlbumImpl join(string $table, string $first, string $operator = null, string $second = null, string $type = 'inner', string $where = false) @@ -196,7 +192,7 @@ class BaseAlbumImpl extends Model implements HasRandomID /** * The relationships that should always be eagerly loaded by default. */ - protected $with = ['owner', 'access_permissions', 'current_user_permissions', 'public_permissions']; + protected $with = ['owner', 'access_permissions']; /** * @param $query @@ -247,26 +243,21 @@ public function access_permissions(): hasMany /** * Returns the relationship between an album and its associated current user permissions. * - * @return HasOne + * @return ?AccessPermission */ - public function current_user_permissions(): HasOne + public function current_user_permissions(): AccessPermission|null { - return $this->access_permissions() - ->one() - ->whereNotNull(APC::USER_ID) - ->where(APC::USER_ID, '=', Auth::id()); + return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id !== null && $p->user_id === Auth::id()); } /** * Returns the relationship between an album and its associated public permissions. * - * @return HasOne + * @return ?AccessPermission */ - public function public_permissions(): HasOne + public function public_permissions(): AccessPermission|null { - return $this->access_permissions() - ->one() - ->whereNull(APC::USER_ID); + return $this->access_permissions->first(fn (AccessPermission $p) => $p->user_id === null); } protected function getSortingAttribute(): ?PhotoSortingCriterion diff --git a/app/Models/Extensions/BaseAlbum.php b/app/Models/Extensions/BaseAlbum.php index c161d481dbb..819bd831ce8 100644 --- a/app/Models/Extensions/BaseAlbum.php +++ b/app/Models/Extensions/BaseAlbum.php @@ -14,7 +14,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Carbon; @@ -33,8 +32,6 @@ * @property int $owner_id * @property User $owner * @property Collection $access_permissions - * @property AccessPermission|null $current_user_permissions - * @property AccessPermission|null $public_permissions * @property Carbon|null $min_taken_at * @property Carbon|null $max_taken_at * @property PhotoSortingCriterion|null $sorting @@ -101,9 +98,9 @@ public function access_permissions(): HasMany /** * Returns the relationship between an album and its associated current user permissions. * - * @return HasOne + * @return AccessPermission|null */ - public function current_user_permissions(): HasOne + public function current_user_permissions(): AccessPermission|null { return $this->base_class->current_user_permissions(); } @@ -111,9 +108,9 @@ public function current_user_permissions(): HasOne /** * Returns the relationship between an album and its associated public permissions. * - * @return HasOne + * @return AccessPermission|null */ - public function public_permissions(): HasOne + public function public_permissions(): AccessPermission|null { return $this->base_class->public_permissions(); } diff --git a/app/Policies/AlbumPolicy.php b/app/Policies/AlbumPolicy.php index 64e86431d30..99a561f569f 100644 --- a/app/Policies/AlbumPolicy.php +++ b/app/Policies/AlbumPolicy.php @@ -56,7 +56,7 @@ private function isOwner(?User $user, BaseAlbum $album): bool public function canSee(?User $user, BaseSmartAlbum $smartAlbum): bool { return ($user?->may_upload === true) || - $smartAlbum->public_permissions !== null; + $smartAlbum->public_permissions() !== null; } /** @@ -103,12 +103,12 @@ public function canAccess(?User $user, ?AbstractAlbum $album): bool return true; } - if ($album->current_user_permissions !== null) { + if ($album->current_user_permissions() !== null) { return true; } - if ($album->public_permissions !== null && - ($album->public_permissions->password === null || + if ($album->public_permissions() !== null && + ($album->public_permissions()->password === null || $this->isUnlocked($album))) { return true; } @@ -146,13 +146,13 @@ public function canDownload(?User $user, ?AbstractAlbum $abstractAlbum): bool // User is logged in // Or User can download. if ($abstractAlbum instanceof BaseSmartAlbum) { - return $user !== null || $abstractAlbum->public_permissions?->grants_download === true; + return $user !== null || $abstractAlbum->public_permissions()?->grants_download === true; } if ($abstractAlbum instanceof BaseAlbum) { return $this->isOwner($user, $abstractAlbum) || - $abstractAlbum->current_user_permissions?->grants_download === true || - $abstractAlbum->public_permissions?->grants_download === true; + $abstractAlbum->current_user_permissions()?->grants_download === true || + $abstractAlbum->public_permissions()?->grants_download === true; } return false; @@ -185,8 +185,8 @@ public function canUpload(User $user, ?AbstractAlbum $abstractAlbum = null): boo if ($abstractAlbum instanceof BaseAlbum) { return $this->isOwner($user, $abstractAlbum) || - $abstractAlbum->current_user_permissions?->grants_upload === true || - $abstractAlbum->public_permissions?->grants_upload === true; + $abstractAlbum->current_user_permissions()?->grants_upload === true || + $abstractAlbum->public_permissions()?->grants_upload === true; } return false; @@ -233,8 +233,8 @@ public function canEdit(User $user, AbstractAlbum|null $album): bool if ($album instanceof BaseAlbum) { return $this->isOwner($user, $album) || - $album->current_user_permissions?->grants_edit === true || - $album->public_permissions?->grants_edit === true; + $album->current_user_permissions()?->grants_edit === true || + $album->public_permissions()?->grants_edit === true; } return false; diff --git a/app/Policies/AlbumQueryPolicy.php b/app/Policies/AlbumQueryPolicy.php index 098069a53fa..a8f74a81d5d 100644 --- a/app/Policies/AlbumQueryPolicy.php +++ b/app/Policies/AlbumQueryPolicy.php @@ -63,10 +63,12 @@ public function applyVisibilityFilter(AlbumBuilder|FixedQueryBuilder $query): Al $query2 // We laverage that IS_LINK_REQUIRED is NULL if the album is NOT shared publically (left join). ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED, '=', false) - ->when($userID !== null, + ->when( + $userID !== null, // Current user is the owner of the album fn ($q) => $q - ->orWhere('base_albums.owner_id', '=', $userID)); + ->orWhere('base_albums.owner_id', '=', $userID) + ); }; return $query->where($visibilitySubQuery); @@ -115,7 +117,8 @@ public function appendAccessibilityConditions(BaseBuilder $query): BaseBuilder ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_PASSWORD_REQUIRED, '=', true) ->whereIn(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, $unlockedAlbumIDs) ) - ->when($userID !== null, + ->when( + $userID !== null, // Current user is the owner of the album fn (BaseBuilder $q) => $q ->orWhere('base_albums.owner_id', '=', $userID) @@ -183,7 +186,8 @@ public function applyReachabilityFilter(AlbumBuilder $query): AlbumBuilder ->where(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_PASSWORD_REQUIRED, '=', true) ->whereIn(APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, $unlockedAlbumIDs) ) - ->when($userID !== null, + ->when( + $userID !== null, // User is owner of the album fn (Builder $q) => $q ->orWhere('base_albums.owner_id', '=', $userID) @@ -251,7 +255,9 @@ public function applyBrowsabilityFilter(AlbumBuilder $query): AlbumBuilder // such that there are no blocked albums on the path to the album. return $query->whereNotExists(function (BaseBuilder $q) { $this->appendUnreachableAlbumsCondition( - $q, null, null, + $q, + null, + null, ); }); } @@ -354,7 +360,8 @@ public function appendUnreachableAlbumsCondition(BaseBuilder $builder, int|strin ->orWhereNull('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::IS_LINK_REQUIRED) ->orWhereNotIn('inner_' . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, $unlockedAlbumIDs) ) - ->when($userID !== null, + ->when( + $userID !== null, fn (BaseBuilder $q) => $q ->where('inner_base_albums.owner_id', '<>', $userID) ); @@ -421,36 +428,15 @@ private function prepareModelQueryOrFail(AlbumBuilder|FixedQueryBuilder $query): */ public function getComputedAccessPermissionSubQuery(): BaseBuilder { - // MySQL defaults - $min = 'MIN'; - $max = 'MAX'; - $passwordLengthIsBetween0and1 = '1 - MAX(ISNULL(' . APC::PASSWORD . '))'; - $driver = DB::getDriverName(); + $passwordLengthIsBetween0and1 = match ($driver) { + 'pgsql' => $this->getPasswordIsRequiredPgSQL(), + 'sqlite' => $this->getPasswordIsRequiredSqlite(), + default => $this->getPasswordIsRequiredMySQL() + }; - // pgsql has a proper boolean support and does not support ISNULL(x) -> bool - if ($driver === 'pgsql') { - $min = 'bool_and'; - $max = 'bool_or'; - - // If password is null, length returns null, we replace the value by 0 in such case - $passwordLength = 'COALESCE(LENGTH(password),0)'; - // We take the minimum between length and 1 with LEAST - // and then agggregate on the column with MIN - // before casting it to bool - $passwordLengthIsBetween0and1 = 'MIN(LEAST(' . $passwordLength . ',1))::bool'; - } - - // sqlite does not support ISNULL(x) -> bool - if ($driver === 'sqlite') { - // We convert password to empty string if it is null - $passwordIsDefined = 'IFNULL(password,"")'; - // Take the lengh - $passwordLength = 'LENGTH(' . $passwordIsDefined . ')'; - // First min with 1 to upper bound it - // then MIN aggregation - $passwordLengthIsBetween0and1 = 'MIN(MIN(' . $passwordLength . ',1))'; - } + $min = $driver === 'pgsql' ? 'bool_and' : 'MIN'; + $max = $driver === 'pgsql' ? 'bool_or' : 'MAX'; $select = [ APC::BASE_ALBUM_ID, @@ -469,6 +455,34 @@ public function getComputedAccessPermissionSubQuery(): BaseBuilder ->groupBy('base_album_id'); } + private function getPasswordIsRequiredMySQL(): string + { + return '1 - MAX(ISNULL(' . APC::PASSWORD . '))'; + } + + private function getPasswordIsRequiredSqlite(): string + { + // sqlite does not support ISNULL(x) -> bool + // We convert password to empty string if it is null + $passwordIsDefined = 'IFNULL(' . APC::PASSWORD . ',"")'; + // Take the lengh + $passwordLength = 'LENGTH(' . $passwordIsDefined . ')'; + // First min with 1 to upper bound it + // then MIN aggregation + return 'MIN(MIN(' . $passwordLength . ',1))'; + } + + private function getPasswordIsRequiredPgSQL(): string + { + // pgsql has a proper boolean support and does not support ISNULL(x) -> bool + // If password is null, length returns null, we replace the value by 0 in such case + $passwordLength = 'COALESCE(LENGTH(' . APC::PASSWORD . '),0)'; + // We take the minimum between length and 1 with LEAST + // and then agggregate on the column with MIN + // before casting it to bool + return 'MIN(LEAST(' . $passwordLength . ',1))::bool'; + } + /** * Helper to join the the computed property for the possibly logged-in user. * @@ -503,6 +517,7 @@ public function joinSubComputedAccessPermissions( first: $prefix . APC::COMPUTED_ACCESS_PERMISSIONS . '.' . APC::BASE_ALBUM_ID, operator: '=', second: $second, - type: $type); + type: $type + ); } } diff --git a/app/Policies/PhotoPolicy.php b/app/Policies/PhotoPolicy.php index d8c2dc1423e..ff1f6833986 100644 --- a/app/Policies/PhotoPolicy.php +++ b/app/Policies/PhotoPolicy.php @@ -180,7 +180,7 @@ public function canAccessFullPhoto(?User $user, Photo $photo): bool return Configs::getValueAsBool('grants_full_photo_access'); } - return $photo->album->public_permissions?->grants_full_photo_access === true || - $photo->album->current_user_permissions?->grants_full_photo_access === true; + return $photo->album->public_permissions()?->grants_full_photo_access === true || + $photo->album->current_user_permissions()?->grants_full_photo_access === true; } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index fe8b1b6ce74..ac68f769dec 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,7 @@ namespace App\Providers; use App\Actions\InstallUpdate\CheckUpdate; +use App\Assets\ArrayToTextTable; use App\Assets\Helpers; use App\Assets\SizeVariantGroupedWithRandomSuffixNamingStrategy; use App\Contracts\Models\AbstractSizeVariantNamingStrategy; @@ -23,20 +24,22 @@ use App\Policies\PhotoQueryPolicy; use App\Policies\SettingsPolicy; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Events\QueryExecuted; use Illuminate\Http\Resources\Json\JsonResource; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Str; use Opcodes\LogViewer\Facades\LogViewer; use Safe\Exceptions\StreamException; use function Safe\stream_filter_register; class AppServiceProvider extends ServiceProvider { - public array $singletons - = [ + public array $singletons = + [ SymLinkFunctions::class => SymLinkFunctions::class, Helpers::class => Helpers::class, CheckUpdate::class => CheckUpdate::class, @@ -72,10 +75,7 @@ public function boot() JsonResource::withoutWrapping(); if (config('database.db_log_sql', false) === true) { - DB::listen(function ($query) { - $msg = $query->sql . ' [' . implode(', ', $query->bindings) . ']'; - Log::debug($msg); - }); + DB::listen(fn ($q) => $this->logSQL($q)); } try { @@ -154,4 +154,42 @@ public function register() SizeVariantDefaultFactory::class ); } + + private function logSQL(QueryExecuted $query): void + { + // Quick exit + if ( + Str::contains(request()->getRequestUri(), 'logs', true) || + Str::contains($query->sql, ['information_schema', 'EXPLAIN', 'configs']) + ) { + return; + } + + // Get message with binding outside. + $msg = '(' . $query->time . 'ms) ' . $query->sql . ' [' . implode(', ', $query->bindings) . ']'; + + // For pgsql and sqlite we log the query and exit early + if (config('database.default', 'mysql') !== 'mysql') { + Log::debug($msg); + + return; + } + + // For mysql we perform an explain as this is usually the one being slower... + $bindings = collect($query->bindings)->map(function ($q) { + return match (gettype($q)) { + 'NULL' => "''", + 'string' => "'{$q}'", + 'boolean' => $q ? '1' : '0', + default => $q + }; + })->all(); + + $sql_with_bindings = Str::replaceArray('?', $bindings, $query->sql); + + $explain = DB::select('EXPLAIN ' . $sql_with_bindings); + $renderer = new ArrayToTextTable(); + $renderer->setIgnoredKeys(['possible_keys', 'key_len', 'ref']); + Log::debug($msg . PHP_EOL . $renderer->getTable($explain)); + } } diff --git a/app/SmartAlbums/BaseSmartAlbum.php b/app/SmartAlbums/BaseSmartAlbum.php index fd507052884..1ddfc91a51e 100644 --- a/app/SmartAlbums/BaseSmartAlbum.php +++ b/app/SmartAlbums/BaseSmartAlbum.php @@ -132,6 +132,11 @@ protected function getThumbAttribute(): ?Thumb return $this->thumb; } + public function public_permissions(): ?AccessPermission + { + return $this->publicPermissions; + } + public function setPublic(): void { if ($this->publicPermissions !== null) { diff --git a/composer.json b/composer.json index 4db395193f8..7a963aa3d57 100644 --- a/composer.json +++ b/composer.json @@ -49,11 +49,13 @@ "doctrine/dbal": "^3.1", "geocoder-php/cache-provider": "^4.3", "geocoder-php/nominatim-provider": "^5.5", + "laminas/laminas-text": "^2.9", "laragear/webauthn": "^1.2.0", "laravel/framework": "^10.0", "lychee-org/nestedset": "^8.0", "lychee-org/php-exif": "^1.0.0", "maennchen/zipstream-php": "^2.1", + "opcodesio/log-viewer": "dev-lycheeOrg", "php-ffmpeg/php-ffmpeg": "^1.0", "php-http/guzzle7-adapter": "^1.0", "php-http/message": "^1.12", @@ -61,8 +63,7 @@ "spatie/laravel-feed": "^4.0", "spatie/laravel-image-optimizer": "^1.6.2", "symfony/cache": "^v6.0.0", - "thecodingmachine/safe": "^2.4", - "opcodesio/log-viewer": "dev-lycheeOrg" + "thecodingmachine/safe": "^2.4" }, "require-dev": { "ext-imagick": "*", diff --git a/composer.lock b/composer.lock index 838986a14f9..59244e932ff 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e85ddff6e5356047b29da2cfb8461c6d", + "content-hash": "59942082cfd20b5aa671346f6d85f574", "packages": [ { "name": "bepsvpt/secure-headers", @@ -1743,6 +1743,213 @@ ], "time": "2021-10-07T12:57:01+00:00" }, + { + "name": "laminas/laminas-servicemanager", + "version": "3.15.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-servicemanager.git", + "reference": "65910ef6a8066b0369fab77fbec9e030be59c866" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/65910ef6a8066b0369fab77fbec9e030be59c866", + "reference": "65910ef6a8066b0369fab77fbec9e030be59c866", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0", + "laminas/laminas-stdlib": "^3.2.1", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0", + "psr/container": "^1.1 || ^2.0.2" + }, + "conflict": { + "ext-psr": "*", + "laminas/laminas-code": "<3.3.1", + "zendframework/zend-code": "<3.3.1", + "zendframework/zend-servicemanager": "*" + }, + "provide": { + "psr/container-implementation": "^1.1 || ^2.0" + }, + "replace": { + "container-interop/container-interop": "^1.2.0" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~2.3.0", + "laminas/laminas-container-config-test": "^0.6", + "mikey179/vfsstream": "^1.6.10@alpha", + "ocramius/proxy-manager": "^2.11", + "phpbench/phpbench": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5.5", + "psalm/plugin-phpunit": "^0.17.0", + "vimeo/psalm": "^4.8" + }, + "suggest": { + "ocramius/proxy-manager": "ProxyManager ^2.1.1 to handle lazy initialization of services" + }, + "bin": [ + "bin/generate-deps-for-config-factory", + "bin/generate-factory-for-class" + ], + "type": "library", + "autoload": { + "files": [ + "src/autoload.php" + ], + "psr-4": { + "Laminas\\ServiceManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Factory-Driven Dependency Injection Container", + "homepage": "https://laminas.dev", + "keywords": [ + "PSR-11", + "dependency-injection", + "di", + "dic", + "laminas", + "service-manager", + "servicemanager" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-servicemanager/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-servicemanager/issues", + "rss": "https://github.com/laminas/laminas-servicemanager/releases.atom", + "source": "https://github.com/laminas/laminas-servicemanager" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2022-07-18T21:18:56+00:00" + }, + { + "name": "laminas/laminas-stdlib", + "version": "3.17.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-stdlib.git", + "reference": "dd35c868075bad80b6718959740913e178eb4274" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/dd35c868075bad80b6718959740913e178eb4274", + "reference": "dd35c868075bad80b6718959740913e178eb4274", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0" + }, + "conflict": { + "zendframework/zend-stdlib": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "^2.5", + "phpbench/phpbench": "^1.2.9", + "phpunit/phpunit": "^10.0.16", + "psalm/plugin-phpunit": "^0.18.4", + "vimeo/psalm": "^5.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "stdlib" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-stdlib/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-stdlib/issues", + "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", + "source": "https://github.com/laminas/laminas-stdlib" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2023-03-20T13:51:37+00:00" + }, + { + "name": "laminas/laminas-text", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/laminas/laminas-text.git", + "reference": "8879e75d03e09b0d6787e6680cfa255afd4645a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laminas/laminas-text/zipball/8879e75d03e09b0d6787e6680cfa255afd4645a7", + "reference": "8879e75d03e09b0d6787e6680cfa255afd4645a7", + "shasum": "" + }, + "require": { + "laminas/laminas-servicemanager": "^3.4", + "laminas/laminas-stdlib": "^3.6", + "php": "^7.3 || ~8.0.0 || ~8.1.0" + }, + "conflict": { + "zendframework/zend-text": "*" + }, + "require-dev": { + "laminas/laminas-coding-standard": "~1.0.0", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Laminas\\Text\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Create FIGlets and text-based tables", + "homepage": "https://laminas.dev", + "keywords": [ + "laminas", + "text" + ], + "support": { + "chat": "https://laminas.dev/chat", + "docs": "https://docs.laminas.dev/laminas-text/", + "forum": "https://discourse.laminas.dev", + "issues": "https://github.com/laminas/laminas-text/issues", + "rss": "https://github.com/laminas/laminas-text/releases.atom", + "source": "https://github.com/laminas/laminas-text" + }, + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2021-09-02T16:50:53+00:00" + }, { "name": "laragear/webauthn", "version": "v1.2.1", diff --git a/tests/Feature/Base/BaseSharingTest.php b/tests/Feature/Base/BaseSharingTest.php index 59b86ce1a3d..d48e58a1b0e 100644 --- a/tests/Feature/Base/BaseSharingTest.php +++ b/tests/Feature/Base/BaseSharingTest.php @@ -82,11 +82,11 @@ public function setUp(): void $this->photosSortingOrder = Configs::getValueAsString(TestConstants::CONFIG_PHOTOS_SORTING_ORDER); Configs::set(TestConstants::CONFIG_PHOTOS_SORTING_ORDER, 'ASC'); - $this->isRecentAlbumPublic = RecentAlbum::getInstance()->public_permissions !== null; + $this->isRecentAlbumPublic = RecentAlbum::getInstance()->public_permissions() !== null; RecentAlbum::getInstance()->setPublic(); - $this->isStarredAlbumPublic = StarredAlbum::getInstance()->public_permissions !== null; + $this->isStarredAlbumPublic = StarredAlbum::getInstance()->public_permissions() !== null; StarredAlbum::getInstance()->setPublic(); - $this->isOnThisDayAlbumPublic = OnThisDayAlbum::getInstance()->public_permissions !== null; + $this->isOnThisDayAlbumPublic = OnThisDayAlbum::getInstance()->public_permissions() !== null; OnThisDayAlbum::getInstance()->setPublic(); $this->clearCachedSmartAlbums(); } diff --git a/tests/Feature/GeoDataTest.php b/tests/Feature/GeoDataTest.php index 9a7753e0228..0e09e0ca85b 100644 --- a/tests/Feature/GeoDataTest.php +++ b/tests/Feature/GeoDataTest.php @@ -190,7 +190,7 @@ public function testGeo(): void */ public function testThumbnailsInsideHiddenAlbum(): void { - $isRecentPublic = RecentAlbum::getInstance()->public_permissions !== null; + $isRecentPublic = RecentAlbum::getInstance()->public_permissions() !== null; $arePublicPhotosHidden = Configs::getValueAsBool(TestConstants::CONFIG_PUBLIC_HIDDEN); $isPublicSearchEnabled = Configs::getValueAsBool(TestConstants::CONFIG_PUBLIC_SEARCH); $displayMap = Configs::getValueAsBool(TestConstants::CONFIG_MAP_DISPLAY); diff --git a/tests/Feature/PhotosOperationsTest.php b/tests/Feature/PhotosOperationsTest.php index 8f75906e5cd..e62863e6397 100644 --- a/tests/Feature/PhotosOperationsTest.php +++ b/tests/Feature/PhotosOperationsTest.php @@ -319,7 +319,7 @@ public function testTrueNegative(): void */ public function testThumbnailsInsideHiddenAlbum(): void { - $isRecentPublic = RecentAlbum::getInstance()->public_permissions !== null; + $isRecentPublic = RecentAlbum::getInstance()->public_permissions() !== null; $arePublicPhotosHidden = Configs::getValueAsBool(TestConstants::CONFIG_PUBLIC_HIDDEN); $isPublicSearchEnabled = Configs::getValueAsBool(TestConstants::CONFIG_PUBLIC_SEARCH); $albumSortingColumn = Configs::getValueAsString(TestConstants::CONFIG_ALBUMS_SORTING_COL); diff --git a/tests/Feature/SmartAlbumVisibilityTest.php b/tests/Feature/SmartAlbumVisibilityTest.php index 867976edf82..7f638b92fb4 100644 --- a/tests/Feature/SmartAlbumVisibilityTest.php +++ b/tests/Feature/SmartAlbumVisibilityTest.php @@ -62,11 +62,11 @@ public function setUp(): void $this->sharing_tests = new SharingUnitTest($this); $this->photos_tests = new PhotosUnitTest($this); - $this->isRecentAlbumPublic = RecentAlbum::getInstance()->public_permissions !== null; + $this->isRecentAlbumPublic = RecentAlbum::getInstance()->public_permissions() !== null; RecentAlbum::getInstance()->setPrivate(); - $this->isStarredAlbumPublic = StarredAlbum::getInstance()->public_permissions !== null; + $this->isStarredAlbumPublic = StarredAlbum::getInstance()->public_permissions() !== null; StarredAlbum::getInstance()->setPrivate(); - $this->isOnThisDayAlbumPublic = OnThisDayAlbum::getInstance()->public_permissions !== null; + $this->isOnThisDayAlbumPublic = OnThisDayAlbum::getInstance()->public_permissions() !== null; OnThisDayAlbum::getInstance()->setPrivate(); $this->clearCachedSmartAlbums(); }