diff --git a/app/Auth.php b/app/Auth.php index b63f0a4794..96ddefc11e 100644 --- a/app/Auth.php +++ b/app/Auth.php @@ -19,6 +19,7 @@ namespace Fisharebest\Webtrees; +use Fisharebest\Webtrees\Cli\Console; use Fisharebest\Webtrees\Contracts\UserInterface; use Fisharebest\Webtrees\Http\Exceptions\HttpAccessDeniedException; use Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException; @@ -140,14 +141,15 @@ public static function accessLevel(Tree $tree, UserInterface|null $user = null): { $user ??= self::user(); + if (self::isAdmin($user)) { + return self::PRIV_HIDE; + } if (self::isManager($tree, $user)) { return self::PRIV_NONE; } - if (self::isMember($tree, $user)) { return self::PRIV_USER; } - return self::PRIV_PRIVATE; } @@ -157,7 +159,6 @@ public static function accessLevel(Tree $tree, UserInterface|null $user = null): public static function id(): int|null { $wt_user = Session::get('wt_user'); - return is_int($wt_user) ? $wt_user : null; } @@ -169,10 +170,20 @@ public static function id(): int|null public static function user(): UserInterface { $user_service = Registry::container()->get(UserService::class); + return $user_service->find(self::id()) ?? self::newVolatileUser(); + } - return $user_service->find(self::id()) ?? new GuestUser(); + private static function newVolatileUser(): UserInterface + { + return self::isCliActive() ? new CliUser() : new GuestUser(); } + private static function isCliActive(): bool + { + return php_sapi_name() === 'cli' && Session::get(Console::CLI_SESSION) === '1'; + } + + /** * Login directly as an explicit user - for masquerading. * diff --git a/app/Cli/Console.php b/app/Cli/Console.php index fedda1f002..728670205d 100644 --- a/app/Cli/Console.php +++ b/app/Cli/Console.php @@ -22,6 +22,7 @@ use Fisharebest\Webtrees\DB; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Registry; +use Fisharebest\Webtrees\Session; use Fisharebest\Webtrees\Webtrees; use Symfony\Component\Console\Application; use Throwable; @@ -30,6 +31,8 @@ final class Console extends Application { + public const string CLI_SESSION = 'cli_session'; + private const array COMMANDS = [ Commands\CompilePoFiles::class, Commands\ConfigIni::class, @@ -52,7 +55,16 @@ public function __construct() parent::__construct(name: Webtrees::NAME, version: Webtrees::VERSION); } - public function loadCommands(): self + public function bootstrap(): self + { + return $this + ->loadCommands() + ->initI18N() + ->connectDatabase() + ->initSession(); + } + + private function loadCommands(): self { foreach (self::COMMANDS as $command) { $this->addCommand(command: Registry::container()->get($command)); @@ -61,10 +73,14 @@ public function loadCommands(): self return $this; } - public function bootstrap(): self + private function initI18N(): self { I18N::init(code: 'en-US', setup: true); + return $this; + } + private function connectDatabase(): self + { try { $config = parse_ini_file(filename: Webtrees::CONFIG_FILE) ?: []; @@ -84,7 +100,13 @@ public function bootstrap(): self } catch (Throwable) { // Ignore errors } + return $this; + } + private function initSession(): self + { + Session::put(self::CLI_SESSION, '1'); return $this; } + } diff --git a/app/CliUser.php b/app/CliUser.php new file mode 100644 index 0000000000..ff48a6aab9 --- /dev/null +++ b/app/CliUser.php @@ -0,0 +1,42 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees; + +use Fisharebest\Webtrees\Contracts\UserInterface; + +/** + * User for command-line invoked functions. + */ +class CliUser extends VolatileUser +{ + public function __construct(string $real_name = 'CLI_USER') + { + parent::__construct('_CLI_', $real_name); + } + + public function getPreference(string $setting_name, string $default = ''): string + { + if ($setting_name === UserInterface::PREF_IS_ADMINISTRATOR) { + return '1'; + } + return parent::getPreference($setting_name, $default); + } + +} \ No newline at end of file diff --git a/app/GuestUser.php b/app/GuestUser.php index ea841b3d60..1e90dce7f8 100644 --- a/app/GuestUser.php +++ b/app/GuestUser.php @@ -19,90 +19,15 @@ namespace Fisharebest\Webtrees; -use Fisharebest\Webtrees\Contracts\UserInterface; - -use function is_string; - /** * A site visitor. */ -class GuestUser implements UserInterface +class GuestUser extends VolatileUser { - private string $email; - - private string $real_name; - - /** - * @param string $email - * @param string $real_name - */ - public function __construct(string $email = 'GUEST_USER', string $real_name = 'GUEST_USER') - { - $this->email = $email; - $this->real_name = $real_name; - } - /** - * The user‘s internal identifier. - * - * @return int - */ - public function id(): int + public function __construct(string $real_name = 'GUEST_USER') { - return 0; + parent::__construct('_GUEST_', $real_name); } - /** - * The users email address. - * - * @return string - */ - public function email(): string - { - return $this->email; - } - - /** - * The user‘s real name. - * - * @return string - */ - public function realName(): string - { - return $this->real_name; - } - - /** - * The user‘s login name. - * - * @return string - */ - public function userName(): string - { - return ''; - } - - /** - * @param string $setting_name - * @param string $default - * - * @return string - */ - public function getPreference(string $setting_name, string $default = ''): string - { - $preference = Session::get('_GUEST_' . $setting_name); - - return is_string($preference) ? $preference : $default; - } - - /** - * @param string $setting_name - * @param string $setting_value - * - * @return void - */ - public function setPreference(string $setting_name, string $setting_value): void - { - Session::put('_GUEST_' . $setting_name, $setting_value); - } } diff --git a/app/VolatileUser.php b/app/VolatileUser.php new file mode 100644 index 0000000000..90787f9b1b --- /dev/null +++ b/app/VolatileUser.php @@ -0,0 +1,73 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees; + +use Fisharebest\Webtrees\Contracts\UserInterface; + +use function is_string; + +/** + * A volatile user is a user which is not persisted, eg a guest visitor. + * Their user preferences are stored in the session and have no email address. + */ +abstract class VolatileUser implements UserInterface +{ + private string $user_name; + private string $real_name; + + public function __construct(string $user_name, string $real_name = 'N/A') + { + $this->user_name = $user_name; + $this->real_name = $real_name; + } + + public function id(): int + { + return 0; + } + + public function userName(): string + { + return $this->user_name; + } + + public function realName(): string + { + return $this->real_name; + } + + public function email(): string + { + return 'N/A'; + } + + public function getPreference(string $setting_name, string $default = ''): string + { + $preference = Session::get($this->userName() . $setting_name); + + return is_string($preference) ? $preference : $default; + } + + public function setPreference(string $setting_name, string $setting_value): void + { + Session::put($this->userName() . $setting_name, $setting_value); + } + +} \ No newline at end of file diff --git a/app/Webtrees.php b/app/Webtrees.php index cd04d1c7b1..1a266a23d3 100644 --- a/app/Webtrees.php +++ b/app/Webtrees.php @@ -254,7 +254,7 @@ public function cliRequest(): int { $console = new Console(); - return $console->loadCommands()->bootstrap()->run(); + return $console->bootstrap()->run(); } /** diff --git a/tests/app/AuthTest.php b/tests/app/AuthTest.php index fecabd841e..50c08e3ed1 100644 --- a/tests/app/AuthTest.php +++ b/tests/app/AuthTest.php @@ -19,13 +19,50 @@ namespace Fisharebest\Webtrees; +use Fisharebest\Webtrees\Cli\Console; +use Fisharebest\Webtrees\Services\UserService; use PHPUnit\Framework\Attributes\CoversClass; #[CoversClass(Auth::class)] class AuthTest extends TestCase { + private UserService $user_service; + + protected static bool $uses_database = true; + + protected function setup(): void + { + parent::setup(); + + $this->user_service = self::createStub(UserService::class); + Registry::container()->set(UserService::class, $this->user_service); + } + public function testClass(): void { self::assertTrue(class_exists(Auth::class)); } + + public function testLoginLogout(): void + { + $user = new DefaultUser(); + $this->user_service->method('find')->willReturn(null, $user, null); + + self::assertInstanceOf('Fisharebest\Webtrees\GuestUser', Auth::user()); + + Auth::login($user); + self::assertInstanceOf('Fisharebest\Webtrees\DefaultUser', Auth::user()); + + Auth::logout(); + $this->user_service->method('find'); + self::assertInstanceOf('Fisharebest\Webtrees\GuestUser', Auth::user()); + } + + public function testCli(): void + { + self::assertInstanceOf('Fisharebest\Webtrees\GuestUser', Auth::user()); + Session::put(Console::CLI_SESSION, '1'); + self::assertInstanceOf('Fisharebest\Webtrees\CLiUser', Auth::user()); + } + } diff --git a/tests/app/CliUserTest.php b/tests/app/CliUserTest.php new file mode 100644 index 0000000000..37c6e0722b --- /dev/null +++ b/tests/app/CliUserTest.php @@ -0,0 +1,52 @@ +. + */ + +declare(strict_types=1); + +namespace Fisharebest\Webtrees; + +use Fisharebest\Webtrees\Contracts\UserInterface; +use PHPUnit\Framework\Attributes\CoversClass; + +#[CoversClass(CliUser::class)] +class CliUserTest extends TestCase +{ + public function testCliUser(): void + { + $user = new CliUser(); + + self::assertSame(0, $user->id()); + self::assertSame('_CLI_', $user->userName()); + self::assertSame('CLI_USER', $user->realName()); + self::assertSame('N/A', $user->email()); + self::assertSame('1', $user->getPreference(UserInterface::PREF_IS_ADMINISTRATOR)); + } + + public function testPreferences(): void + { + $user = new GuestUser(); + + self::assertSame('', $user->getPreference('foo')); + self::assertSame('', $user->getPreference('foo')); + self::assertSame('bar', $user->getPreference('foo', 'bar')); + + // Guests users store preferences in the session + $user->setPreference('foo', 'bar'); + + self::assertSame('bar', $user->getPreference('foo')); + } +} diff --git a/tests/app/GuestUserTest.php b/tests/app/GuestUserTest.php index fcccc9e0e6..088f5b20ea 100644 --- a/tests/app/GuestUserTest.php +++ b/tests/app/GuestUserTest.php @@ -29,19 +29,19 @@ public function testAnonymous(): void $user = new GuestUser(); self::assertSame(0, $user->id()); - self::assertSame('GUEST_USER', $user->email()); + self::assertSame('_GUEST_', $user->userName()); self::assertSame('GUEST_USER', $user->realName()); - self::assertSame('', $user->userName()); + self::assertSame('N/A', $user->email()); } public function testVisitor(): void { - $user = new GuestUser('guest@example.com', 'guest user'); + $user = new GuestUser('guest user'); self::assertSame(0, $user->id()); - self::assertSame('guest@example.com', $user->email()); + self::assertSame('_GUEST_', $user->userName()); self::assertSame('guest user', $user->realName()); - self::assertSame('', $user->userName()); + self::assertSame('N/A', $user->email()); } public function testPreferences(): void