Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / TwoFactor.php
blob2b77f92392485405d8194b4fd15e49dd229fcdfd
1 <?php
2 /**
3 * Two authentication factor handling
4 */
6 declare(strict_types=1);
8 namespace PhpMyAdmin;
10 use BaconQrCode\Renderer\ImageRenderer;
11 use CodeLts\U2F\U2FServer\U2FServer;
12 use PhpMyAdmin\ConfigStorage\Relation;
13 use PhpMyAdmin\Http\ServerRequest;
14 use PhpMyAdmin\Plugins\TwoFactor\Application;
15 use PhpMyAdmin\Plugins\TwoFactor\Invalid;
16 use PhpMyAdmin\Plugins\TwoFactor\Key;
17 use PhpMyAdmin\Plugins\TwoFactorPlugin;
18 use PragmaRX\Google2FAQRCode\Google2FA;
19 use XMLWriter;
21 use function array_merge;
22 use function class_exists;
23 use function extension_loaded;
24 use function in_array;
25 use function is_array;
26 use function is_bool;
27 use function is_string;
28 use function ucfirst;
30 /**
31 * Two factor authentication wrapper class
33 class TwoFactor
35 /**
36 * @var mixed[]
37 * @psalm-var array{backend: string, settings: mixed[], type?: 'session'|'db'}
39 public array $config;
41 protected bool $writable;
43 protected TwoFactorPlugin $backend;
45 /** @var string[] */
46 protected array $available;
48 private UserPreferences $userPreferences;
50 /**
51 * Creates new TwoFactor object
53 * @param string $user User name
55 public function __construct(public string $user)
57 $dbi = DatabaseInterface::getInstance();
59 $this->userPreferences = new UserPreferences($dbi, new Relation($dbi), new Template());
60 $this->available = $this->getAvailableBackends();
61 $this->config = $this->readConfig();
62 $this->writable = $this->config['type'] === 'db';
63 $this->backend = $this->getBackendForCurrentUser();
66 /**
67 * Reads the configuration
69 * @psalm-return array{backend: string, settings: mixed[], type: 'session'|'db'}
71 public function readConfig(): array
73 $result = [];
74 $config = $this->userPreferences->load();
75 if (isset($config['config_data']['2fa']) && is_array($config['config_data']['2fa'])) {
76 $result = $config['config_data']['2fa'];
79 $backend = '';
80 if (isset($result['backend']) && is_string($result['backend'])) {
81 $backend = $result['backend'];
84 $settings = [];
85 if (isset($result['settings']) && is_array($result['settings'])) {
86 $settings = $result['settings'];
89 return ['backend' => $backend, 'settings' => $settings, 'type' => $config['type']];
92 public function isWritable(): bool
94 return $this->writable;
97 public function getBackend(): TwoFactorPlugin
99 return $this->backend;
102 /** @return string[] */
103 public function getAvailable(): array
105 return $this->available;
108 public function showSubmit(): bool
110 return $this->backend::$showSubmit;
114 * Returns list of available backends
116 * @return string[]
118 public function getAvailableBackends(): array
120 $result = [];
121 $config = Config::getInstance();
122 if ($config->config->debug->simple2fa) {
123 $result[] = 'simple';
126 if (
127 class_exists(Google2FA::class)
128 && class_exists(ImageRenderer::class)
129 && (class_exists(XMLWriter::class) || extension_loaded('imagick'))
131 $result[] = 'application';
134 $result[] = 'WebAuthn';
136 if (class_exists(U2FServer::class)) {
137 $result[] = 'key';
140 return $result;
144 * Returns list of missing dependencies
146 * @return array<int, array{class: string, dep: string}>
148 public function getMissingDeps(): array
150 $result = [];
151 if (! class_exists(Google2FA::class)) {
152 $result[] = ['class' => Application::getName(), 'dep' => 'pragmarx/google2fa-qrcode'];
155 if (! class_exists(ImageRenderer::class)) {
156 $result[] = ['class' => Application::getName(), 'dep' => 'bacon/bacon-qr-code'];
159 if (! class_exists(U2FServer::class)) {
160 $result[] = ['class' => Key::getName(), 'dep' => 'code-lts/u2f-php-server'];
163 return $result;
167 * Returns class name for given name
169 * @param string $name Backend name
171 * @psalm-return class-string<TwoFactorPlugin>
173 public function getBackendClass(string $name): string
175 $result = TwoFactorPlugin::class;
176 if (in_array($name, $this->available, true)) {
177 /** @psalm-var class-string<TwoFactorPlugin> $result */
178 $result = 'PhpMyAdmin\\Plugins\\TwoFactor\\' . ucfirst($name);
179 } elseif ($name !== '') {
180 $result = Invalid::class;
183 return $result;
187 * Returns backend for current user
189 public function getBackendForCurrentUser(): TwoFactorPlugin
191 $name = $this->getBackendClass($this->config['backend']);
193 return new $name($this);
197 * Checks authentication, returns true on success
199 * @param bool $skipSession Skip session cache
201 public function check(ServerRequest $request, bool $skipSession = false): bool
203 if ($skipSession) {
204 return $this->backend->check($request);
207 if (! isset($_SESSION['two_factor_check']) || ! is_bool($_SESSION['two_factor_check'])) {
208 $_SESSION['two_factor_check'] = $this->backend->check($request);
211 return $_SESSION['two_factor_check'];
215 * Renders user interface to enter two-factor authentication
217 * @return string HTML code
219 public function render(ServerRequest $request): string
221 return $this->backend->getError() . $this->backend->render($request);
225 * Renders user interface to configure two-factor authentication
227 * @return string HTML code
229 public function setup(ServerRequest $request): string
231 return $this->backend->getError() . $this->backend->setup($request);
235 * Saves current configuration.
237 * @return true|Message
239 public function save(): bool|Message
241 return $this->userPreferences->persistOption('2fa', $this->config, null);
245 * Changes two-factor authentication settings
247 * The object might stay in partially changed setup
248 * if configuration fails.
250 * @param string $name Backend name
252 public function configure(ServerRequest $request, string $name): bool
254 $this->config = ['backend' => $name, 'settings' => []];
255 if ($name === '') {
256 $cls = $this->getBackendClass($name);
257 $this->backend = new $cls($this);
258 } else {
259 if (! in_array($name, $this->available, true)) {
260 return false;
263 $cls = $this->getBackendClass($name);
264 $this->backend = new $cls($this);
265 if (! $this->backend->configure($request)) {
266 return false;
270 $result = $this->save();
271 if ($result !== true) {
272 echo $result->getDisplay();
275 return true;
279 * Returns array with all available backends
281 * @return array<int, array{id: mixed, name: mixed, description: mixed}>
283 public function getAllBackends(): array
285 $all = array_merge([''], $this->available);
286 $backends = [];
287 foreach ($all as $name) {
288 $cls = $this->getBackendClass($name);
289 $backends[] = ['id' => $cls::$id, 'name' => $cls::getName(), 'description' => $cls::getDescription()];
292 return $backends;