diff --git a/.gitignore b/.gitignore index a238cbe..38af445 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ assets/js/app.js assets/css/plugin-MultiplLocalAuth.css mix-manifest.json node_modules/ +vendor/ +.phpunit.cache/ +composer.phar +composer-setup.php diff --git a/AccountLifecycleService.php b/AccountLifecycleService.php new file mode 100644 index 0000000..4b0684e --- /dev/null +++ b/AccountLifecycleService.php @@ -0,0 +1,126 @@ + false, + 'accountInTrash' => true, + 'profileName' => $profileName, + 'errors' => $errors, + ]; + } + + /** + * Pré-condições de confirmRestoreAccount(). + * Retorna 'expired' | 'not_trash' | null (ok). + * + * @param object|null $user objeto com propriedade status (User ou stub) + */ + public static function confirmRestoreError(?int $sessionUserId, $user, int $trashStatus = self::STATUS_TRASH): ?string + { + if (!$sessionUserId) { + return 'expired'; + } + + if (!$user || (int) $user->status !== $trashStatus) { + return 'not_trash'; + } + + return null; + } + + public static function shouldRestoreOnEmailConfirm(?string $pendingTrashRestoreConfirmMetadata): bool + { + return $pendingTrashRestoreConfirmMetadata === '1'; + } + + public static function relatedEntityTypesToRestore(): array + { + return self::RELATED_ENTITY_TYPES; + } + + public static function entityShouldBeUndeleted(int $entityStatus, int $trashStatus = self::STATUS_TRASH): bool + { + return $entityStatus === $trashStatus; + } + + public static function storePendingTrashRestore(int $userId): void + { + $_SESSION[self::PENDING_TRASH_RESTORE_SESSION_KEY] = $userId; + } + + public static function getPendingTrashRestoreUserId(): ?int + { + $userId = $_SESSION[self::PENDING_TRASH_RESTORE_SESSION_KEY] ?? null; + if ($userId === null || $userId === '') { + return null; + } + + return (int) $userId; + } + + public static function clearPendingTrashRestore(): void + { + unset($_SESSION[self::PENDING_TRASH_RESTORE_SESSION_KEY]); + } +} diff --git a/Decidim/DecidimStrategy.php b/Decidim/DecidimStrategy.php new file mode 100644 index 0000000..f298641 --- /dev/null +++ b/Decidim/DecidimStrategy.php @@ -0,0 +1,245 @@ + 'email'); + */ + public $defaults = ['redirect_uri' => '{complete_url_to_strategy}oauth2callback']; + + /** + * Auth request + */ + public function request(){ + $url = $this->strategy['auth_endpoint']; + $params = array( + 'client_id' => $this->strategy['client_id'], + 'client_secret' => $this->strategy['client_secret'], + 'redirect_uri' => $this->strategy['redirect_uri'], + 'response_type' => 'code', + 'scope' => $this->strategy['scope'] + ); + foreach ($this->optionals as $key){ + if (!empty($this->strategy[$key])) $params[$key] = $this->strategy[$key]; + } + + $this->clientGet($url, $params); + } + + /** + * Internal callback, after OAuth + */ + public function oauth2callback(){ + if (array_key_exists('code', $_GET) && !empty($_GET['code'])){ + $code = $_GET['code']; + $url = $this->strategy['token_endpoint']; + $params = array( + 'code' => $code, + 'client_id' => $this->strategy['client_id'], + 'client_secret' => $this->strategy['client_secret'], + 'redirect_uri' => $this->strategy['redirect_uri'], + 'grant_type' => 'authorization_code' + ); + $response = $this->serverPost($url, $params, null, $headers); + + $results = json_decode($response); + + if (!empty($results) && !empty($results->access_token)){ + + $userinfo = $this->userinfo($results->access_token); + + + $this->auth = array( + 'uid' => $userinfo['id'], + 'info' => array(), + 'credentials' => array( + 'token' => $results->access_token, + 'expires' => date('c', time() + $results->expires_in) + ), + 'raw' => $userinfo + ); + + + if (!empty($results->refresh_token)) + { + $this->auth['credentials']['refresh_token'] = $results->refresh_token; + } + + $this->mapProfile($userinfo, 'name', 'info.name'); + $this->mapProfile($userinfo, 'email', 'info.email'); + $this->mapProfile($userinfo, 'given_name', 'info.first_name'); + $this->mapProfile($userinfo, 'family_name', 'info.last_name'); + $this->mapProfile($userinfo, 'picture', 'info.image'); + + $this->callback(); + } + else{ + $error = array( + 'code' => 'access_token_error', + 'message' => 'Failed when attempting to obtain access token', + 'raw' => array( + 'response' => $response, + 'headers' => $headers + ) + ); + $this->errorCallback($error); + } + } + else{ + $error = array( + 'code' => 'oauth2callback_error', + 'raw' => $_GET + ); + + $this->errorCallback($error); + } + } + + /** + * Queries Google API for user info + * + * @param string $access_token + * @return array Parsed JSON results + */ + private function userinfo($access_token){ + $options = [ + 'http' => [ + 'header' => "Authorization: Bearer {$access_token}\r\nAccept: application/json", + 'ignore_errors' => true, + 'method' => 'GET' + ] + ]; + + // Alterado para passar os headers corretamente e manter o uso do serverGet + $userinfo = $this->serverGet($this->strategy['userinfo_endpoint'], [], $options, $responseHeaders); + // $userinfo = $this->serverGet($this->strategy['userinfo_endpoint'], array('access_token' => $access_token), null, $headers); + + if (!empty($userinfo)){ + return $this->recursiveGetObjectVars(json_decode($userinfo)); + } + else{ + $error = array( + 'code' => 'userinfo_error', + 'message' => 'Failed when attempting to query for user information', + 'raw' => array( + 'response' => $userinfo, + 'headers' => $headers + ) + ); + $this->errorCallback($error); + } + } + + /** + * Atualiza dados do usuário autenticado a partir da resposta da estratégia Decidim. + * + * @param \MapasCulturais\Entities\User $user Usuário autenticado que terá os dados atualizados. + * @param array $response Resposta completa retornada pela estratégia Decidim. + * @return void + */ + public static function verifyUpdateData($user, $response) + { + $app = App::i(); + + $userinfo = (object) $response['auth']['raw']; + + self::getFile($user->profile, $userinfo->image); + } + + /** + * Faz o download de uma imagem remota e salva como avatar para o agente informado. + * + * @param \MapasCulturais\Entities\Agent $owner Agente proprietário do avatar. + * @param string|null $url URL da imagem a ser baixada. + * @return void + */ + public static function getFile($owner, $url){ + + $curl = new Curl; + $curl->get($url); + $curl->close(); + $response = $curl->response; + + if(mb_strpos($response, 'não encontrada')){ + return; + } + + $tmp = tempnam("/tmp", ""); + $handle = fopen($tmp, "wb"); + fwrite($handle,$response); + fclose($handle); + + // Confere MIME e extensões aceitas + if (!self::checkFileType($tmp)) { + unlink($tmp); + return; + } + + $mime = mime_content_type($tmp) ?: 'application/octet-stream'; + + $extension = match ($mime) { + 'image/jpeg', 'image/jpg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + default => null, + }; + + if(!$extension) { + unlink($tmp); + return; + } + + $basename = sprintf('%s.%s', md5(uniqid('', true)), $extension); + + $class_name = $owner->fileClassName; + + $file = new $class_name([ + "name" => $basename, + "type" => $mime, + "tmp_name" => $tmp, + "error" => 0, + "size" => filesize($tmp) + ]); + + $file->group = "avatar"; + $file->owner = $owner; + $file->save(true); + + if(is_file($tmp)) { + unlink($tmp); + } + } + + /** + * Verifica se um arquivo temporário corresponde a um formato de imagem suportado. + * + * @param string $filename Caminho absoluto do arquivo temporário a ser verificado. + * @return bool Retorna true se o arquivo for uma imagem suportada; caso contrário, false. + */ + public static function checkFileType($filename) + { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mimetype = finfo_file($finfo, $filename); + if ($mimetype == 'image/jpg' || $mimetype == 'image/jpeg' || $mimetype == 'image/gif' || $mimetype == 'image/png') { + $is_image = true; + } else { + $is_image = false; + } + + return $is_image; + } + +} diff --git a/GovBr/GovBrAccountService.php b/GovBr/GovBrAccountService.php new file mode 100644 index 0000000..0484a63 --- /dev/null +++ b/GovBr/GovBrAccountService.php @@ -0,0 +1,182 @@ +em->createQuery( + 'SELECT u.id FROM MapasCulturais\Entities\User u WHERE LOWER(u.email) = :email' + ); + $query->setParameter('email', $email); + $query->setMaxResults(1); + + return (bool) $query->getOneOrNullResult(); + } + + /** + * Conta Gov.br nova com e-mail do GOV já usado por outro usr. + * + * @param callable|null $emailExistsChecker fn(string $email): bool + */ + public static function hasEmailConflictOnCreate(array $response, ?callable $emailExistsChecker = null): bool + { + if (!self::isGovBrProvider($response['auth']['provider'] ?? null)) { + return false; + } + + $email = self::extractEmailFromResponse($response); + if ($email === null) { + // Sem e-mail verificado no GOV: também exige coleta na UI. + return true; + } + + return self::emailExists($email, $emailExistsChecker); + } + + public static function applyEmailToResponse(array $response, string $email): array + { + $email = self::normalizeEmail($email); + if ($email === null) { + throw new \InvalidArgumentException('E-mail inválido.'); + } + + $response['auth']['info']['email'] = $email; + return $response; + } + + public static function storePendingRegistration(array $response): void + { + $_SESSION[self::SESSION_PENDING_KEY] = $response; + } + + public static function getPendingRegistration(): ?array + { + $pending = $_SESSION[self::SESSION_PENDING_KEY] ?? null; + return is_array($pending) ? $pending : null; + } + + public static function clearPendingRegistration(): void + { + unset($_SESSION[self::SESSION_PENDING_KEY]); + } + + /** + * Valida e-mail alternativo informado na UI. + * Retorna lista de erros (vazia = ok). + * + * @param callable|null $emailExistsChecker fn(string $email): bool + * @return string[] + */ + public static function validateAlternateEmail(?string $email, ?callable $emailExistsChecker = null): array + { + $errors = []; + $email = self::normalizeEmail($email); + + if ($email === null || !self::isValidEmail($email)) { + $errors[] = 'Informe um e-mail válido.'; + return $errors; + } + + if (self::emailExists($email, $emailExistsChecker)) { + $errors[] = 'Este e-mail já está em uso. Informe outro e-mail.'; + } + + return $errors; + } + + public static function profileCpfMatchesGovBr(User $user, array $response, string $metadataFieldCpf): bool + { + $govCpf = self::extractCpfFromResponse($response); + if ($govCpf === null || !$user->profile) { + return false; + } + + $profileCpf = self::maskCpf((string) ($user->profile->$metadataFieldCpf ?? '')); + if ($profileCpf === null) { + $profileCpf = self::maskCpf((string) ($user->profile->cpf ?? '')); + } + + return $profileCpf !== null && $profileCpf === $govCpf; + } +} diff --git a/GovBr/GovBrStrategy.php b/GovBr/GovBrStrategy.php index 65d80d3..8c622cd 100644 --- a/GovBr/GovBrStrategy.php +++ b/GovBr/GovBrStrategy.php @@ -94,8 +94,10 @@ public function oauth2callback() 'dic_agent_fields_update' => $this->strategy['dic_agent_fields_update'] ]; + // CPF (sub) é a identidade estável no Gov.br. + // jti muda a cada token e não pode ser usado como authUid. $this->auth = array( - 'uid' => $userinfo->jti, + 'uid' => $userinfo->sub, 'credentials' => array( 'token' => $results->id_token, 'expires' => $userinfo->exp @@ -233,12 +235,18 @@ public static function applySeal($user, $response){ $app = App::i(); $agent = $user->profile; - $sealId = $response['auth']['applySeal']; + $sealId = $response['auth']['applySeal'] ?? null; if($sealId){ $app->disableAccessControl(); $seal = $app->repo('Seal')->find($sealId); + if (!$seal) { + $app->log->error("Gov.br applySeal: selo {$sealId} não encontrado"); + $app->enableAccessControl(); + return; + } + $relations = $agent->getSealRelations(); $has_new_seal = false; @@ -264,6 +272,17 @@ public static function verifyUpdateData($user, $response) $auth_data = $response['auth']['info']; $userinfo = (object) $response['auth']['raw']; + $metadataFieldCpf = $app->config['auth.config']['metadataFieldCPF'] ?? 'documento'; + + // Nunca sobrescrever perfil de outra pessoa (CPF diferente do token Gov.br). + $govCpf = \MultipleLocalAuth\GovBrAccountService::extractCpfFromResponse($response); + $profileCpf = \MultipleLocalAuth\GovBrAccountService::maskCpf((string) ($user->profile->$metadataFieldCpf ?? '')); + if ($profileCpf === null) { + $profileCpf = \MultipleLocalAuth\GovBrAccountService::maskCpf((string) ($user->profile->cpf ?? '')); + } + if ($govCpf && $profileCpf && $govCpf !== $profileCpf) { + return; + } $app->hook("entity(Agent).get(lockedFields)", function(&$lockedFields) use ($app){ $config = $app->config['auth.config']['strategies']['govbr']; diff --git a/Plugin.php b/Plugin.php index 9ad2020..1cb1a54 100644 --- a/Plugin.php +++ b/Plugin.php @@ -9,6 +9,10 @@ include('LinkedIn/LinkedInStrategy.php'); include('LoginCidadao/LoginCidadaoStrategy.php'); include('GovBr/GovBrStrategy.php'); +if (!class_exists(__NAMESPACE__ . '\\GovBrAccountService', false)) { + include __DIR__ . '/GovBr/GovBrAccountService.php'; +} +include('Decidim/DecidimStrategy.php'); class Plugin extends \MapasCulturais\Plugin { @@ -56,6 +60,8 @@ public function register() { $this->registerUserMetadata(Provider::$accountIsActiveMetadata, ['label' => i::__('Conta ativa?')]); $this->registerUserMetadata(Provider::$tokenVerifyAccountMetadata, ['label' => i::__('Token de verificação')]); $this->registerUserMetadata(Provider::$loginAttempMetadata, ['label' => i::__('Número de tentativas de login')]); - $this->registerUserMetadata(Provider::$timeBlockedloginAttempMetadata, ['label' => i::__('Tempo de bloqueio por excesso de tentativas')]); + $this->registerUserMetadata(Provider::$timeBlockedloginAttempMetadata, ['label' => i::__('Tempo de bloqueio por excesso de tentativas')]); + $this->registerUserMetadata(Provider::$pendingTrashRestoreConfirmMetadata, ['label' => i::__('Aguardando confirmação de recuperação de conta')]); + $this->registerUserMetadata(Provider::$forcePasswordChangeMetadata, ['label' => i::__('Forçar troca de senha no próximo login')]); } } diff --git a/Provider.php b/Provider.php index a252964..57130a4 100644 --- a/Provider.php +++ b/Provider.php @@ -1,11 +1,17 @@ env('AUTH_LOGIN_ON_REGISTER', false), 'enableLoginByCPF' => env('AUTH_LOGIN_BY_CPF', true), + 'requireCpf' => env('AUTH_REQUIRED_CPF', true), + 'passwordMustHaveCapitalLetters' => env('AUTH_PASS_CAPITAL_LETTERS', true), 'passwordMustHaveLowercaseLetters' => env('AUTH_PASS_LOWERCASE_LETTERS', true), 'passwordMustHaveSpecialCharacters' => env('AUTH_PASS_SPECIAL_CHARS', true), @@ -63,7 +76,7 @@ function __construct ($config) { 'urlImageToUseInEmails' => env('AUTH_EMAIL_IMAGE'), 'urlTermsOfUse' => env('LINK_TERMOS', $app->createUrl('auth', 'termos-e-condicoes')), - 'statusCreateAgent' => env('STATUS_CREATE_AGENT', Agent::STATUS_DRAFT), + 'statusCreateAgent' => env('STATUS_CREATE_AGENT', Agent::STATUS_ENABLED), 'strategies' => [ 'Facebook' => [ 'visible' => env('AUTH_FACEBOOK_CLIENT_ID', false), @@ -109,6 +122,17 @@ function __construct ($config) { 'applySealId' => env('AUTH_GOV_BR_APPLY_SEAL_ID', null), 'menssagem_authenticated' => env('AUTH_GOV_BR_MENSSAGEM_AUTHENTICATED','Usuário já se autenticou pelo GovBr'), 'dic_agent_fields_update' => env('AUTH_GOV_BR_DICT_AGENT_FIELDS_UPDATE','[]') + ], + 'decidim' => [ + 'visible' => env('AUTH_DECIDIM_CLIENT_ID', false), + 'client_id' => env('AUTH_DECIDIM_CLIENT_ID', null), + 'client_secret' => env('AUTH_DECIDIM_CLIENT_SECRET', null), + 'redirect_uri' => env('AUTH_DECIDIM_REDIRECT_URI', null), + 'scope' => env('AUTH_DECIDIM_SCOPE', null), + 'auth_endpoint' => env('AUTH_DECIDIM_AUTH_ENDPOINT', null), + 'token_endpoint' => env('AUTH_DECIDIM_TOKEN_ENDPOINT', null), + 'userinfo_endpoint' => env('AUTH_DECIDIM_USERINFO_ENDPOINT', null), + 'button_text' => env('AUTH_DECIDIM_BUTTON_TEXT', 'Entrar com Decidim'), ] ] ]; @@ -166,6 +190,14 @@ protected function _init() { $user->setMetadata(Provider::$accountIsActiveMetadata, '1'); $app->disableAccessControl(); + + // só agora, com o link do email confirmado, a conta (e tudo que foi pra lixeira + // junto com ela) realmente sai da lixeira. + if (AccountLifecycleService::shouldRestoreOnEmailConfirm($user->getMetadata(Provider::$pendingTrashRestoreConfirmMetadata))) { + $user->setMetadata(Provider::$pendingTrashRestoreConfirmMetadata, '0'); + $app->auth->restoreUserFromTrash($user, true); + } + $user->saveMetadata(true); $app->enableAccessControl(); $app->em->flush(); @@ -257,8 +289,11 @@ protected function _init() { } // add actions to auth controller - $app->hook('GET(auth.index)', function () use($config){ - $this->render('multiple-local', [ 'config' => $config ]); + $app->hook('GET(auth.index)', function () use($app, $config){ + $this->render('multiple-local', [ + 'config' => $config, + 'forcePasswordChange' => $app->auth->userMustChangePassword(), + ]); }); $app->hook('GET(auth.register)', function () use($config){ @@ -295,6 +330,75 @@ protected function _init() { } }); + // Coleta de e-mail alternativo quando o e-mail do Gov.br já existe no Mapa + $app->hook('GET(auth.govbr-email)', function () use ($app, $config) { + $pending = GovBrAccountService::getPendingRegistration(); + if (!$pending) { + $app->redirect($this->createUrl('')); + return; + } + + $conflict_email = GovBrAccountService::extractEmailFromResponse($pending); + $this->render('govbr-email', [ + 'config' => $config, + 'conflictEmail' => $conflict_email, + 'formAction' => $app->createUrl('auth', 'govbr-email'), + ]); + }); + + $app->hook('POST(auth.govbr-email)', function () use ($app, $config) { + /** @var \MultipleLocalAuth\Provider $auth */ + $auth = $app->auth; + $pending = GovBrAccountService::getPendingRegistration(); + if (!$pending) { + $app->redirect($this->createUrl('')); + return; + } + + $new_email = $app->request->post('email'); + $errors = GovBrAccountService::validateAlternateEmail($new_email); + if ($errors) { + $this->render('govbr-email', [ + 'config' => $config, + 'conflictEmail' => GovBrAccountService::extractEmailFromResponse($pending), + 'formAction' => $app->createUrl('auth', 'govbr-email'), + 'errors' => $errors, + 'triedEmail' => $new_email, + ]); + return; + } + + $response = GovBrAccountService::applyEmailToResponse($pending, $new_email); + GovBrAccountService::clearPendingRegistration(); + + $user = $auth->createUserFromGovBrPending($response); + if (!$user) { + GovBrAccountService::storePendingRegistration($pending); + $this->render('govbr-email', [ + 'config' => $config, + 'conflictEmail' => GovBrAccountService::extractEmailFromResponse($pending), + 'formAction' => $app->createUrl('auth', 'govbr-email'), + 'errors' => [i::__('Não foi possível criar o usuário. Tente novamente.', 'multipleLocal')], + 'triedEmail' => $new_email, + ]); + return; + } + + $auth->authenticateUser($user); + + if (method_exists('GovBrStrategy', 'verifyUpdateData')) { + \GovBrStrategy::verifyUpdateData($user, $response); + } + if (method_exists('GovBrStrategy', 'applySeal')) { + \GovBrStrategy::applySeal($user, $response); + } + + $app->applyHook('auth.successful'); + $redirect_url = $auth->getRedirectPath(); + unset($_SESSION['mapasculturais.auth.redirect_path']); + $app->redirect($redirect_url); + }); + /******* INIT LOCAL AUTH **********/ @@ -335,15 +439,58 @@ protected function _init() { $login = $app->auth->doLogin(); if ($login['success']) { + // Mantém auth.successful (AccountStatus, lastLogin, etc.), mas: + // 1) grava a sessão ANTES do trabalho pesado do hook + // 2) responde o JSON na hora + // 3) roda o hook no shutdown (após a resposta ao browser) + $redirectTo = $app->auth->getRedirectPath(); + + if (session_status() === PHP_SESSION_ACTIVE) { + session_write_close(); + } + + register_shutdown_function(function () use ($app) { + try { + ignore_user_abort(true); + set_time_limit(120); + if (function_exists('fastcgi_finish_request')) { + @fastcgi_finish_request(); + } + $app->applyHook('auth.successful'); + } catch (\Throwable $e) { + $app->log->error('auth.successful after login failed: ' . $e->getMessage()); + } + }); + $this->json([ - 'error' => false, - 'redirectTo' => $app->auth->getRedirectPath() + 'error' => false, + 'redirectTo' => $redirectTo + ]); + } else if (!empty($login['accountInTrash'])) { + $this->json([ + 'error' => true, + 'accountInTrash' => true, + 'profileName' => $login['profileName'], ]); } else { $this->errorJson($login['errors'], 200); } }); + $app->hook('POST(auth.confirmrestore)', function () use($app){ + /** + * @var \MapasCulturais\Controller $this + */ + + $restore = $app->auth->confirmRestoreAccount(); + + if ($restore['success']) { + $this->json(['error' => false]); + } else { + $this->errorJson($restore['errors'], 200); + } + }); + $app->hook('POST(auth.recover)', function () use($app){ /** * @var \MapasCulturais\Controller $this @@ -386,6 +533,22 @@ protected function _init() { } }); + $app->hook('POST(auth.doforcedpasswordchange)', function () use($app){ + /** + * @var \MapasCulturais\Controller $this + */ + + $this->requireAuthentication(); + + $doForcedPasswordChange = $app->auth->doForcedPasswordChange(); + + if ($doForcedPasswordChange['success']) { + $this->json(['error' => false]); + } else { + $this->errorJson($doForcedPasswordChange['errors'], 200); + } + }); + $app->hook('POST(auth.newpassword)', function () use($app){ /** * @var \MapasCulturais\Controller $this @@ -413,8 +576,46 @@ protected function _init() { $this->errorJson($adminchangeuserpassword['errors'], 200); } }); - - + + $app->hook('POST(auth.forcepasswordchange)', function () use ($app) { + /** + * @var \MapasCulturais\Controller $this + */ + + $forcePasswordChange = $app->auth->forcePasswordChange(); + + if ($forcePasswordChange['success']) { + $this->json(['error' => false]); + } else { + $this->errorJson($forcePasswordChange['errors'], 200); + } + }); + + $app->hook('auth.redirectUrl', function (&$redirect) use ($app) { + if ($app->auth->userMustChangePassword()) { + $redirect = $app->createUrl('auth', 'index'); + } + }); + + // Enquanto a troca de senha estiver pendente, o usuário fica preso em /autenticacao/ + // (form de troca, logout e endpoints auxiliares do auth). Qualquer outra rota + // (incluindo API) redireciona de volta — o controller auth fica liberado. + // O padrão segue o do módulo LGPD: callAction dispara ALL(controller.action):before, + // então ALL(<<*>>):before casa com qualquer controller/ação. + $app->hook('ALL(<<*>>):before,API(<<*>>):before,-ALL(auth.<<*>>):before,-API(auth.<<*>>):before', function () use ($app) { + /** @var \MapasCulturais\Controller $this */ + if (!$app->auth->userMustChangePassword()) { + return; + } + + if ($app->request->isAjax()) { + $app->halt(403, i::__('É necessário trocar a senha antes de continuar.', 'multipleLocal')); + } + + $app->redirect($app->createUrl('auth', 'index')); + }); + + $app->hook('panel.menu:after', function () use($app){ $active = $this->template == 'panel/my-account' ? 'class="active"' : ''; @@ -548,29 +749,32 @@ function validateRegisterFields() { if($config['enableLoginByCPF']) { // validate cpf - if(empty($cpf) || !$this->validateCPF($cpf)) { + if($config['requireCpf'] && !$this->validateCPF($cpf)) { array_push($errors['user']['cpf'], i::__('Por favor, informe um cpf válido.', 'multipleLocal')); $hasErrors = true; } - $foundAgent = []; - $metadataFieldCpf = $this->getMetadataFieldCpfFromConfig(); - $_cpf = implode("','", [$cpf, preg_replace('/[^0-9]/i', '', $cpf)]); - $foundAgent = $conn->fetchAll("SELECT * FROM agent_meta WHERE key IN ('{$metadataFieldCpf}', 'cpf') AND value IN ('{$_cpf}')"); - - // creates an array with agents with status == 1, because the user can have, for example, 3 agents, but 2 have status == 0 - $existAgent = []; - if($foundAgent){ - foreach ($foundAgent as $agentMeta) { - if($agentMeta->owner->status >= 0) { - $existAgent[] = $agentMeta; + if($this->validateCPF($cpf)) { + $foundAgent = []; + $metadataFieldCpf = $this->getMetadataFieldCpfFromConfig(); + + $_cpf = implode("','", [$cpf, preg_replace('/[^0-9]/i', '', $cpf)]); + $foundAgent = $conn->fetchAll("SELECT * FROM agent_meta WHERE key IN ('{$metadataFieldCpf}', 'cpf') AND value IN ('{$_cpf}')"); + + // creates an array with agents with status == 1, because the user can have, for example, 3 agents, but 2 have status == 0 + $existAgent = []; + if($foundAgent){ + foreach ($foundAgent as $agentMeta) { + if($agentMeta->owner->status >= 0) { + $existAgent[] = $agentMeta; + } } } - } - if(count($existAgent) > 0) { - array_push($errors['user']['cpf'], i::__('Este CPF já esta em uso. Tente recuperar a sua senha.', 'multipleLocal')); - $hasErrors = true; + if(count($existAgent) > 0) { + array_push($errors['user']['cpf'], i::__('Este CPF já esta em uso. Tente recuperar a sua senha.', 'multipleLocal')); + $hasErrors = true; + } } } @@ -811,16 +1015,123 @@ function adminchangeuserpassword() { } if (!$hasErrors) { + $user->setMetadata(self::$forcePasswordChangeMetadata, '0'); + $app->disableAccessControl(); $user->saveMetadata(true); $app->enableAccessControl(); $user->save(true); $app->em->flush(); - return [ + return [ 'success' => true ]; } else { - return [ + return [ + 'success' => false, + 'errors' => $errors + ]; + } + } + + /** + * Ação de admin: marca o usuário para ser obrigado a trocar a senha no próximo login + * bem-sucedido (o redirecionamento pós-login é tratado no hook 'auth.redirectUrl'). + */ + function forcePasswordChange() { + $app = App::i(); + + $errors = [ + 'forcePasswordChange' => [] + ]; + + $email = $app->request->post('email'); + $user = $this->getUserFromDB($email); + + $preconditionError = AccountLifecycleService::forcePasswordChangeError( + $app->user->is('admin'), + (bool) $user + ); + + if ($preconditionError === 'permission') { + array_push($errors['forcePasswordChange'], i::__('Você não tem permissão para executar esta ação.', 'multipleLocal')); + return [ + 'success' => false, + 'errors' => $errors + ]; + } + + if ($preconditionError === 'not_found') { + array_push($errors['forcePasswordChange'], i::__('Usuário não encontrado.', 'multipleLocal')); + return [ + 'success' => false, + 'errors' => $errors + ]; + } + + $user->setMetadata(self::$forcePasswordChangeMetadata, '1'); + + $app->disableAccessControl(); + $user->saveMetadata(true); + $app->enableAccessControl(); + $app->em->flush(); + + return [ + 'success' => true + ]; + } + + /** + * True se o usuário logado precisa trocar a senha antes de continuar usando o sistema + * (flag marcada por um admin via forcePasswordChange()). + */ + function userMustChangePassword() { + $app = App::i(); + $user = $app->user; + + return $user instanceof Entities\User + && AccountLifecycleService::mustChangePassword($user->getMetadata(self::$forcePasswordChangeMetadata)); + } + + /** + * Troca a senha do usuário já autenticado que está com a troca de senha obrigatória + * pendente. Diferente de changePassword(), não pede a senha atual: o próprio login + * bem-sucedido (com a senha antiga) já provou que o usuário é quem diz ser. + */ + function doForcedPasswordChange() { + $app = App::i(); + $user = $app->user; + + $hasErrors = false; + $errors = [ + 'password' => [], + ]; + + if (!AccountLifecycleService::canDoForcedPasswordChange($this->userMustChangePassword())) { + array_push($errors['password'], i::__('Não há troca de senha pendente para este usuário.', 'multipleLocal')); + return [ + 'success' => false, + 'errors' => $errors + ]; + } + + $newPassword = $app->request->post('new_password'); + $confirmNewPassword = $app->request->post('confirm_new_password'); + + $errors['password'] = $this->verifyPassowrds($newPassword, $confirmNewPassword); + if (!empty($errors['password'])) { + $hasErrors = true; + } else { + $user->setMetadata(self::$passMetaName, $this->hashPassword($newPassword)); + $user->setMetadata(self::$forcePasswordChangeMetadata, '0'); + } + + if (!$hasErrors) { + $user->save(true); + return [ + 'success' => true + ]; + } else { + return [ 'success' => false, 'errors' => $errors ]; @@ -850,17 +1161,18 @@ function changePassword() { $hasErrors = true; } else { $user->setMetadata($meta, $app->auth->hashPassword($newPassword)); - } + $user->setMetadata(self::$forcePasswordChangeMetadata, '0'); + } } else { array_push($errors['password'], i::__('Senha atual inválida.', 'multipleLocal')); $hasErrors = true; - } + } } else { array_push($errors['password'], i::__('Insira sua nova senha.', 'multipleLocal')); $hasErrors = true; } - + if (!$hasErrors) { $user->save(true); return [ @@ -1058,12 +1370,10 @@ function doLogin() { } } else { // LOGIN COM EMAIL - $query = new \MapasCulturais\ApiQuery ('MapasCulturais\Entities\User', ['@select' => 'id', 'email' => 'ILIKE(' . $emailToCheck . ')']); - if($user = $query->findOne()){ - unset($user['@entityType']); - array_filter($user); - $user = $app->repo("User")->findOneBy($user); - } + // getUserFromDB() usa uma query direta (sem o filtro de status do ApiQuery), então + // também encontra contas na lixeira: precisamos disso para mostrar o aviso de + // recuperação de conta em vez de simplesmente negar o login. + $user = $this->getUserFromDB($emailToCheck); } @@ -1076,14 +1386,14 @@ function doLogin() { array_push($errors['login'], i::__('Usuário ou senha inválidos.', 'multipleLocal')); $hasErrors = true; } else { - $accountIsActive = $user->getMetadata(self::$accountIsActiveMetadata); - if($config['userMustConfirmEmailToUseTheSystem']) { + $accountIsActive = $user->getMetadata(self::$accountIsActiveMetadata); + if($config['userMustConfirmEmailToUseTheSystem']) { if(isset($user) && $accountIsActive === '0' ) { array_push($errors['confirmEmail'], i::__('Verifique seu email para validar a sua conta.', 'multipleLocal')); $hasErrors = true; - } + } } - + $config = $this->_config; $timeBlockedloginAttemp = $config['timeBlockedloginAttemp']; //verifica se o metadata 'timeBlockedloginAttempMetadata' existe e é maior que o tempo de agora, se for, então o usuario ta bloqueado te tentar fazer login @@ -1101,8 +1411,18 @@ function doLogin() { $meta = self::$passMetaName; $savedPass = $user->getMetadata($meta); - + if (password_verify($pass, $savedPass)) { + if (AccountLifecycleService::shouldOfferTrashRestore($hasErrors, (int) $userToLogin->status, \MapasCulturais\Entity::STATUS_TRASH)) { + $this->middlewareLoginAttempts(true); + AccountLifecycleService::storePendingTrashRestore((int) $userToLogin->id); + + return AccountLifecycleService::buildAccountInTrashLoginResult( + $userToLogin->profile ? $userToLogin->profile->name : '', + $errors + ); + } + $this->middlewareLoginAttempts(true); $this->authenticateUser($userToLogin); } else { @@ -1110,14 +1430,130 @@ function doLogin() { array_push($errors['login'], i::__('Usuário ou senha inválidos.', 'multipleLocal')); $hasErrors = true; } - } + } return [ 'success' => !$hasErrors, 'errors' => $errors ];; } - + + /** + * Inverso do User::delete() do core: tira o usuário e as entidades que foram pra + * lixeira junto com ele. Fica no plugin (não no core) porque só o fluxo de + * recuperação de conta do MultipleLocalAuth usa isso. + */ + function restoreUserFromTrash(Entities\User $user, $flush = false) { + $app = App::i(); + $user->checkPermission('undelete'); + + $app->disableAccessControl(); + + $user->undelete($flush); + + foreach (AccountLifecycleService::relatedEntityTypesToRestore() as $entity_type) { + foreach ($user->$entity_type as $entity) { + if (AccountLifecycleService::entityShouldBeUndeleted((int) $entity->status, Entities\User::STATUS_TRASH)) { + $entity->undelete($flush); + } + } + } + + $app->enableAccessControl(); + + if ($flush) { + $app->em->flush(); + } + } + + /** + * Envia o email de confirmação para restaurar uma conta (e tudo que foi pra lixeira junto + * com ela) de um usuário cujo login foi barrado por estar na lixeira (ver o bloco + * 'accountInTrash' em doLogin()). A conta só sai da lixeira de fato quando o link do + * email é clicado (ver GET(auth.confirma-email)) — aqui só preparamos o token e avisamos. + */ + function confirmRestoreAccount() { + $app = App::i(); + + $errors = [ + 'restore' => [] + ]; + + $userId = AccountLifecycleService::getPendingTrashRestoreUserId(); + $user = $userId ? $app->repo("User")->find($userId) : null; + + $preconditionError = AccountLifecycleService::confirmRestoreError( + $userId, + $user, + \MapasCulturais\Entity::STATUS_TRASH + ); + + if ($preconditionError === 'expired') { + array_push($errors['restore'], i::__('Sessão expirada, faça login novamente.', 'multipleLocal')); + return [ + 'success' => false, + 'errors' => $errors + ]; + } + + if ($preconditionError === 'not_trash') { + AccountLifecycleService::clearPendingTrashRestore(); + array_push($errors['restore'], i::__('Esta conta não está mais com exclusão parcial.', 'multipleLocal')); + return [ + 'success' => false, + 'errors' => $errors + ]; + } + + // generate the token hash + $source = rand(3333, 8888); + $cut = rand(10, 30); + $string = $this->hashPassword($source); + $token = substr($string, $cut, 20); + + $app->disableAccessControl(); + $user->setMetadata(self::$tokenVerifyAccountMetadata, $token); + $user->setMetadata(self::$pendingTrashRestoreConfirmMetadata, '1'); + $user->saveMetadata(true); + $app->enableAccessControl(); + + AccountLifecycleService::clearPendingTrashRestore(); + + $baseUrl = $app->getBaseUrl(); + $site_name = $app->siteName; + + $mustache = new \Mustache_Engine(); + $content = $mustache->render( + file_get_contents( + __DIR__. + DIRECTORY_SEPARATOR.'views'. + DIRECTORY_SEPARATOR.'auth'. + DIRECTORY_SEPARATOR.'email-account-restored.html' + ), array( + "siteName" => $site_name, + "user" => $user->profile->name, + "urlToValidateAccount" => $baseUrl.'auth/confirma-email?token='.$token, + "baseUrl" => $baseUrl, + "urlSupportChat" => $this->_config['urlSupportChat'], + "urlSupportEmail" => $this->_config['urlSupportEmail'], + "urlSupportSite" => $this->_config['urlSupportSite'], + "textSupportSite" => $this->_config['textSupportSite'], + "urlImageToUseInEmails" => $this->getImageImageURl(), + ) + ); + + $app->createAndSendMailMessage([ + 'from' => $app->config['mailer.from'], + 'to' => $user->email, + 'subject' => sprintf(i::__('Confirme a recuperação da sua conta no %s', 'multipleLocal'), $site_name), + 'body' => $content + ]); + + return [ + 'success' => true + ]; + } + function doRegister() { $app = App::i(); $config = $app->_config; @@ -1161,6 +1597,15 @@ function doRegister() { $baseUrl = $app->getBaseUrl(); + if(!$user) { + $error['user']['createUser'] = i::__('Não foi possível criar o usuário. Entre em contato com suporte', 'multipleLocal'); + + return [ + 'success' => false, + 'errors' => $error + ]; + } + //ATENÇÃO !! Se for necessario "padronizar" os emails com header/footers, é necessario adapatar o 'mustache', e criar uma mini estrutura de pasta de emails em 'MultipleLocalAuth\views' $mustache = new \Mustache_Engine(); $site_name = $app->siteName; @@ -1195,7 +1640,8 @@ function doRegister() { $user->{self::$tokenVerifyAccountMetadata} = $token; $user->{self::$accountIsActiveMetadata} = '0'; $app->modules['LGPD']->acceptTerms($app->request->post('slugs'), $user); - $user->save(); + $user->save(true); + $app->enableAccessControl(); @@ -1296,7 +1742,7 @@ protected function _validateResponse(){ // verifica se a resposta é um erro if (array_key_exists('error', $response)) { - $app->flash('auth error', 'Opauth returns error auth response'); + // $app->flash('auth error', 'Opauth returns error auth response'); } else { /** * Auth response validation @@ -1327,14 +1773,41 @@ public function _getAuthenticatedUser() { if (is_object($this->_authenticatedUser)) { return $this->_authenticatedUser; } - - if (isset($_SESSION['multipleLocalUserId'])) { + + $govBrResponse = null; + if ($this->_validateResponse()) { + $response = $this->_getResponse(); + if (GovBrAccountService::isGovBrProvider($response['auth']['provider'] ?? null)) { + $govBrResponse = $response; + } + } + + // Em callback Gov.br, não reutilizar sessão de outro CPF. + if (!$govBrResponse && isset($_SESSION['multipleLocalUserId'])) { $user_id = $_SESSION['multipleLocalUserId']; $user = App::i()->repo("User")->find($user_id); return $user; } $user = null; + if ($govBrResponse) { + $app = App::i(); + $cpf = GovBrAccountService::extractCpfFromResponse($govBrResponse); + if (!empty($cpf)) { + $metadataFieldCpf = $this->getMetadataFieldCpfFromConfig(); + $agent_meta = $app->repo('AgentMeta')->findOneBy(["key" => $metadataFieldCpf, "value" => $cpf]); + if (empty($agent_meta)) { + $digits = preg_replace('/\D+/', '', $cpf); + $agent_meta = $app->repo('AgentMeta')->findOneBy(["key" => $metadataFieldCpf, "value" => $digits]); + } + if (!empty($agent_meta)) { + $user = $agent_meta->owner->user; + } + } + // Gov.br: sem fallback por e-mail (evita hijack de conta). + return $user; + } + if($this->_validateResponse()){ $app = App::i(); $response = $this->_getResponse(); @@ -1372,7 +1845,16 @@ public function processResponse(){ // e ainda não existe um usuário no sistema $user = $this->_getAuthenticatedUser(); $response = $this->_getResponse(); + if(!$user){ + if (GovBrAccountService::isGovBrProvider($response['auth']['provider'] ?? null) + && GovBrAccountService::hasEmailConflictOnCreate($response) + ) { + GovBrAccountService::storePendingRegistration($response); + App::i()->redirect(App::i()->createUrl('auth', 'govbr-email')); + return false; + } + $user = $this->createUser($response); $profile = $user->profile; @@ -1398,6 +1880,19 @@ public function processResponse(){ return false; } } + + /** + * Cria usuário a partir do fluxo pendente Gov.br (após e-mail alternativo). + * Expõe o createUser final do core sem alterar o core. + */ + public function createUserFromGovBrPending(array $response) + { + try { + return $this->createUser($response); + } catch (\Throwable $e) { + return null; + } + } @@ -1424,24 +1919,33 @@ function authenticateUser(Entities\User $user) { $this->_setAuthenticatedUser($user); $_SESSION['multipleLocalUserId'] = $user->id; } - - protected function _createUser($response) { + + protected function _createUser($response) + { $app = App::i(); + /** @var \MapasCulturais\Connection $conn */ + $conn = $app->em->getConnection(); + $app->disableAccessControl(); $config = $this->_config; $user = null; - if($provider_class = $response['auth']['provider']."Strategy"){ - if(method_exists($provider_class, "newAccountCheck")){ - if($user = $provider_class::newAccountCheck($response)){ + if ($provider_class = $response['auth']['provider'] . "Strategy") { + if (method_exists($provider_class, "newAccountCheck")) { + if ($user = $provider_class::newAccountCheck($response)) { $agent = $user->profile; } } } - if(!$user){ + if ($user) { + return $user; + } + + try { + $app->em->beginTransaction(); // cria o usuário $user = new Entities\User; $user->authProvider = $response['auth']['provider']; @@ -1449,73 +1953,79 @@ protected function _createUser($response) { $user->email = $response['auth']['info']['email']; $app->em->persist($user); - + // cria um agente do tipo user profile para o usuário criado acima $agent = new Entities\Agent($user); - if(isset($response['auth']['info']['name'])){ + if (isset($response['auth']['info']['name'])) { $agent->name = $response['auth']['info']['name']; - } - elseif(isset($response['auth']['info']['first_name']) && isset($response['auth']['info']['last_name'])){ + } elseif (isset($response['auth']['info']['first_name']) && isset($response['auth']['info']['last_name'])) { $agent->name = $response['auth']['info']['first_name'] . ' ' . $response['auth']['info']['last_name']; - } - elseif(isset($response['auth']['agentData']['name'])){ + } elseif (isset($response['auth']['agentData']['name'])) { $agent->name = $response['auth']['agentData']['name']; - } - else{ + } else { $agent->name = ''; } - - if(isset($response['auth']['info']['phone_number'])){ - $metadataFieldPhone = $this->getMetadataFieldPhone(); - $metadataFieldPhone = $this->getMetadataFieldPhone(); - $metadataFieldPhone = $this->getMetadataFieldPhone(); + + if (isset($response['auth']['info']['phone_number'])) { + $metadataFieldPhone = $this->getMetadataFieldPhone(); + $metadataFieldPhone = $this->getMetadataFieldPhone(); + $metadataFieldPhone = $this->getMetadataFieldPhone(); $agent->$metadataFieldPhone = $response['auth']['info']['phone_number']; } - if(isset($response['auth']['agentData']['shortDescription'])){ + if (isset($response['auth']['agentData']['shortDescription'])) { $agent->shortDescription = $response['auth']['agentData']['shortDescription']; } - if(isset($response['auth']['agentData']['terms:area'])){ + if (isset($response['auth']['agentData']['terms:area'])) { $agent->terms['area'] = $response['auth']['agentData']['terms:area']; } - if(isset($response['auth']['info']['phone_number'])){ - $metadataFieldPhone = $this->getMetadataFieldPhone(); + if (isset($response['auth']['info']['phone_number'])) { + $metadataFieldPhone = $this->getMetadataFieldPhone(); $agent->setMetadata($metadataFieldPhone, $response['auth']['info']['phone_number']); } //cpf - $cpf = (isset($response['auth']['info']['cpf']) && $response['auth']['info']['cpf'] != "") ? $this->mask($response['auth']['info']['cpf'],'###.###.###-##') : null; - if(!empty($cpf)){ - $metadataFieldCpf = $this->getMetadataFieldCpfFromConfig(); + $cpf = (isset($response['auth']['info']['cpf']) && $response['auth']['info']['cpf'] != "") ? $this->mask($response['auth']['info']['cpf'], '###.###.###-##') : null; + if (!empty($cpf)) { + $metadataFieldCpf = $this->getMetadataFieldCpfFromConfig(); $agent->$metadataFieldCpf = $cpf; } $agent->status = (int) $config['statusCreateAgent'] ?? '0'; $agent->emailPrivado = $user->email; - - $agent->save(); - $app->em->flush(); + $agent->save(true); + $user->profile = $agent; $user->save(true); + if(!$conn->fetchScalar("SELECT profile_id FROM usr where id = {$user->id}")) { + throw new Exception("Error create agent"); + } + $user->createPermissionsCacheForUsers([$user]); $agent->createPermissionsCacheForUsers([$user]); - } - - $app->enableAccessControl(); - $redirectUrl = $agent->status == Agent::STATUS_DRAFT ? $agent->editUrl : $this->getRedirectPath(); - $app->applyHookBoundTo($this, 'auth.createUser:redirectUrl', [&$redirectUrl]); - if ($redirectUrl) { - $this->_setRedirectPath($redirectUrl); + $app->em->commit(); + + $app->enableAccessControl(); + $redirectUrl = $agent->status == Agent::STATUS_DRAFT ? $agent->editUrl : $this->getRedirectPath(); + $app->applyHookBoundTo($this, 'auth.createUser:redirectUrl', [&$redirectUrl]); + + if ($redirectUrl) { + $this->_setRedirectPath($redirectUrl); + } + + return $user; + + } catch (\Throwable $th) { + $app->em->rollback(); + return null; } - - return $user; } function mask($val, $mask) { diff --git a/README.md b/README.md index e910937..e6a0c0c 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,169 @@ # MultipleLocalAuth -Plugin que implementa um método de autenticação local para o Mapas Culturais, em conjunto com login via redes sociais. - -## Instalação e Configuração - -Faça download ou clone do plugin e coloque a pasta MultipleLocalAuth no pasta dos plugins do Mapas Culturais. - -No arquivo de configuração do Mapas Culturais, config.php, você deve: - -1. Ativar o plugin -2. Configurar MultipleLocalAuth como seu Provider de autenticação -3. Configurar as chaves das redes sociais - -Para ativar o plugin, adicione na sua array de Plugins: -``` +Plugin de autenticação para o Mapas Culturais que combina login local (e-mail e CPF) com múltiplas estratégias sociais (Google, Facebook, LinkedIn, Twitter, Login Cidadão, Gov.br, Decidim etc.), regras de senha configuráveis e proteção contra abuso. + +## Recursos principais +- Cadastro local com fluxo multi-etapas, validação de CPF e aceite de termos LGPD. +- Login por e-mail ou CPF, com limite de tentativas e bloqueio temporário automático. +- Confirmação de conta por e-mail, recuperação de senha com token e troca de senha pelo painel. +- Troca de senha forçada por admin: no próximo login o usuário fica preso em `/autenticacao/` até definir uma nova senha. +- Recuperação de conta na lixeira: login com senha correta em conta soft-deleted oferece confirmação por e-mail antes de restaurar usuário e entidades relacionadas. +- Integração com Google reCAPTCHA v2 (visível) para login, cadastro e recuperação. +- Autenticação social via Opauth (Google, Facebook, LinkedIn, Twitter, Login Cidadão, Gov.br, Decidim) com mapeamento automático de dados. +- Atualização opcional de avatar e metadados ao autenticar via Gov.br ou Decidim. +- Componentes Vue (`login`, `create-account`, `change-password`, `password-strongness`) prontos para a Base V2. + +## Instalação +1. Faça download/clonagem deste repositório e coloque a pasta `MultipleLocalAuth` em `protected/application/plugins/` do Mapas Culturais. +2. Garanta que o módulo `LGPD` esteja habilitado, pois o cadastro consome `LGPD::acceptTerms`. +3. Instale dependências do Mapas Culturais (este plugin não adiciona dependências externas além das que já vêm com o core/opauth). + +## Configuração +Edite `config.php` do Mapas Culturais: + +```php 'plugins' => [ // ... outros plugins 'MultipleLocalAuth' => [ 'namespace' => 'MultipleLocalAuth', ], ], -``` - -Para definir este plugin como seu método de autenticação, defina a configuraço *auth.provider*: -``` -'auth.provider' => '\MultipleLocalAuth\Provider', -``` -Finalmente, defina a configuração *auth.config* para definir as estratégias utilizadas e as chaves dos serviços: +'auth.provider' => \MultipleLocalAuth\Provider::class, -``` 'auth.config' => [ - - //SALT da senha do usuario - 'salt' => 'LT_SECURITY_SALT_SECURITY_SALT_SECURITY_SALT_SECURITY_SALT_SECU', - - 'timeout' => '24 hours', - - //url de suporte por chat para ser enviado nos emails - 'urlSupportChat' => 'https://www.google.com', - - //url de suporte por email para ser enviado nos emails - 'urlSupportEmail' => 'https://www.google.com', - - //url do site de suporte para ser enviado nos emails - 'urlSupportSite' => 'https://www.google.com', - - //url dos termos de uso para utilizar a plataforma - 'urlTermsOfUse' => 'https://www.google.com', - - //url de uma imagem para ser enviado como plano de fundo nos emails - 'urlImageToUseInEmails' => 'https://mapacultural.juazeiro.ce.gov.br/files/project/1561/file/963893/blob-3d922310b0a1eb1c16791a06023f56df.png', - - //Habilita registro e login através do CPF - 'enableLoginByCPF' => true, - - //apelido do metadata que será salvo o campo CPF - 'metadataFieldCPF' => 'documento', - - //Regra para saber se o usuario deve ou não confiar o email para poder utilizar o sistema - 'userMustConfirmEmailToUseTheSystem' => false, - - //Regra de força de senha - Ter no mínimo 1 letra maiúscula - 'passwordMustHaveCapitalLetters' => true, - - //Regra de força de senha - Ter no mínimo 1 letra minúscula - 'passwordMustHaveLowercaseLetters' => true, - - //Regra de força de senha - Ter no mínimo 1 caractere especial - 'passwordMustHaveSpecialCharacters' => true, - - //Regra de força de senha - Ter no mínimo 1 caractere numérico - 'passwordMustHaveNumbers' => true, - - //Regra de força de senha - Ter no mínimo n caracteres - 'minimumPasswordLength' => 6, - - //Configuração de GOOGLE Recaptcha - 'google-recaptcha-secret' => '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe', - 'google-recaptcha-sitekey' => '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI', - - //Tempo da sessao do usuario em segundos - 'sessionTime' => 7200, - - //Limite de tentativas não sucedidas de login antes de bloquear o usuario por X minutos - 'numberloginAttemp' => '5', - - //Tempo de bloqueio do usuario em segundos, após romper limites de tentativas não sucedidas - 'timeBlockedloginAttemp' => '900', - - //Estratégias de autenticação - 'strategies' => [ - 'Facebook' => array( - 'app_id' => 'SUA_APP_ID', - 'app_secret' => 'SUA_APP_SECRET', - 'scope' => 'email' - ), - 'LinkedIn' => array( - 'api_key' => 'SUA_API_KEY', - 'secret_key' => 'SUA_SECRET_KEY', - 'redirect_uri' => URL_DO_SEU_SITE . '/autenticacao/linkedin/oauth2callback', - 'scope' => 'r_emailaddress' - ), - 'Google' => array( - 'client_id' => 'SEU_CLIENT_ID', - 'client_secret' => 'SEU_CLIENT_SECRET', - 'redirect_uri' => URL_DO_SEU_SITE . '/autenticacao/google/oauth2callback', - 'scope' => 'email' - ), - 'Twitter' => array( - 'app_id' => 'SUA_APP_ID', - 'app_secret' => 'SUA_APP_SECRET', - ), - ] + // ajuste conforme as tabelas abaixo ], ``` + +### Opções gerais +| Chave | Descrição | Padrão | Variável `.env` | +| --- | --- | --- | --- | +| `salt` | Salt usado pelo Opauth | `env('AUTH_SALT')` | `AUTH_SALT` | +| `timeout` | Tempo máximo da sessão OAuth | `24 hours` | `AUTH_TIMEOUT` | +| `loginOnRegister` | Autenticar automaticamente após cadastro | `false` | `AUTH_LOGIN_ON_REGISTER` | +| `enableLoginByCPF` | Permite login/cadastro via CPF | `true` | `AUTH_LOGIN_BY_CPF` | +| `requireCpf` | Exige CPF no cadastro | `true` | `AUTH_REQUIRED_CPF` | +| `metadataFieldCPF` | Campo de metadata que armazena CPF | `documento` | `AUTH_METADATA_FIELD_DOCUMENT` | +| `metadataFieldPhone` | Campo de metadata que armazena telefone | `telefone1` | `AUTH_METADATA_FIELD_PHONE` | +| `userMustConfirmEmailToUseTheSystem` | Exige validação por e-mail antes do uso | `false` | `AUTH_EMAIL_CONFIRMATION` | +| `sessionTime` | Duração da sessão (segundos) | `7200` | `AUTH_SESSION_TIME` | +| `statusCreateAgent` | Status default do agente criado | `Agent::STATUS_ENABLED` | `STATUS_CREATE_AGENT` | + +### Comunicação e suporte +| Chave | Uso | Padrão | Variável `.env` | +| --- | --- | --- | --- | +| `urlSupportChat` | Link incluído nos e-mails | `''` | `AUTH_SUPPORT_CHAT` | +| `urlSupportEmail` | Link de contato por e-mail | `''` | `AUTH_SUPPORT_EMAIL` | +| `urlSupportSite` | URL geral de suporte | `''` | `AUTH_SUPPORT_SITE` | +| `textSupportSite` | Texto exibido com o link | `''` | `AUTH_SUPPORT_TEXT` | +| `urlImageToUseInEmails` | Imagem de fundo para e-mails | `null` | `AUTH_EMAIL_IMAGE` | +| `urlTermsOfUse` | URL dos termos de uso | `auth/termos-e-condicoes` | `LINK_TERMOS` | + +### Regras de senha +| Chave | Descrição | Padrão | Variável `.env` | +| --- | --- | --- | --- | +| `passwordMustHaveCapitalLetters` | Exigir letra maiúscula | `true` | `AUTH_PASS_CAPITAL_LETTERS` | +| `passwordMustHaveLowercaseLetters` | Exigir letra minúscula | `true` | `AUTH_PASS_LOWERCASE_LETTERS` | +| `passwordMustHaveSpecialCharacters` | Exigir caractere especial | `true` | `AUTH_PASS_SPECIAL_CHARS` | +| `passwordMustHaveNumbers` | Exigir número | `true` | `AUTH_PASS_NUMBERS` | +| `minimumPasswordLength` | Tamanho mínimo da senha | `6` | `AUTH_PASS_LENGTH` | + +### Proteção contra abuso +| Chave | Descrição | Padrão | Variável `.env` | +| --- | --- | --- | --- | +| `numberloginAttemp` | Tentativas antes do bloqueio | `5` | `AUTH_NUMBER_ATTEMPTS` | +| `timeBlockedloginAttemp` | Tempo de bloqueio (segundos) | `900` | `AUTH_BLOCK_TIME` | + +### Google reCAPTCHA v2 +| Chave | Descrição | Padrão | +| --- | --- | --- | +| `google-recaptcha-secret` | Secret da integração | `env('GOOGLE_RECAPTCHA_SECRET')` | +| `google-recaptcha-sitekey` | Site key usada no front | `env('GOOGLE_RECAPTCHA_SITEKEY')` | + +Se ambas as chaves estiverem ausentes, o captcha é desativado. + +### Estratégias de autenticação +Cada estratégia pode receber `visible => bool` para controlar se o botão aparece na interface. + +#### Google +- `client_id`, `client_secret`, `redirect_uri`, `scope` (`email profile` por padrão). + +#### Facebook +- `app_id`, `app_secret`, `scope` (default `email`). + +#### LinkedIn +- `api_key`, `secret_key`, `redirect_uri`, `scope` (default `r_emailaddress`). + +#### Twitter +- `app_id`, `app_secret`. (Fluxo direto do Opauth). + +#### Login Cidadão +- `client_id`, `client_secret`, `auth_endpoint`, `token_endpoint`, `userinfo_endpoint`, `redirect_uri`, `scope`. + +#### Gov.br +- `client_id`, `client_secret`, `scope`, `auth_endpoint`, `token_endpoint`, `userinfo_endpoint`, `redirect_uri`. +- `state_salt`, `code_verifier`, `code_challenge`, `code_challenge_method` para PKCE. +- `applySealId` (opcional): selo aplicado ao agente autenticado. +- `dic_agent_fields_update`: mapa de campos que podem ser atualizados automaticamente (JSON, ex: `{"name": "full_name"}`). +- `menssagem_authenticated`: mensagem exibida quando o usuário já autenticou via Gov.br. +- Identidade estável: matching **somente por CPF** (`sub`); `authUid` usa `sub` (não `jti`). Sem fallback por e-mail (evita hijack). +- Se o e-mail do Gov.br já existir em `usr.email` (único/obrigatório no core), a criação é interrompida e o usuário informa outro e-mail em `auth/govbr-email`. +- `verifyUpdateData` não sobrescreve perfil cujo CPF diverge do token Gov.br. + +#### Decidim +- `client_id`, `client_secret`, `auth_endpoint`, `token_endpoint`, `userinfo_endpoint`, `redirect_uri`, `scope`. +- Atualiza automaticamente avatar do agente com a imagem fornecida. + +Você pode adicionar ou remover estratégias conforme necessário; qualquer estratégia Opauth disponível no diretório do plugin pode ser configurada. + +## Fluxos e endpoints +- `GET auth.index`: renderiza o componente de login (ou o formulário de troca forçada, se `forcePasswordChange` estiver pendente). +- `GET auth.register`: fluxo multi-etapas de cadastro. +- `GET auth.recover`: formulário para solicitar redefinição de senha. +- `GET auth.confirma-email`: valida o token enviado por e-mail e ativa a conta; se houver `pendingTrashRestoreConfirm`, também restaura a conta da lixeira. +- `POST auth.validate`: validação assíncrona do primeiro passo do cadastro. +- `POST auth.register`: criação de conta (gera agente, token de verificação e envia e-mail). +- `POST auth.login`: autenticação local (com bloqueio por tentativas via metadata). Conta na lixeira + senha correta → resposta `accountInTrash` (sem autenticar). +- `POST auth.confirmrestore`: após o aviso de conta na lixeira, envia e-mail de confirmação de recuperação (token + flag `pendingTrashRestoreConfirm`). +- `POST auth.recover` / `POST auth.dorecover`: solicitação e conclusão da recuperação de senha. +- `POST auth.changepassword` / `POST auth.newpassword`: alteração de senha logado ou via token (limpa `forcePasswordChange`). +- `POST auth.forcepasswordchange`: admin marca o usuário para trocar a senha no próximo login. +- `POST auth.doforcedpasswordchange`: usuário autenticado com troca pendente define a nova senha (sem pedir a senha atual). +- `POST auth.adminchangeuseremail` / `POST auth.adminchangeuserpassword`: rotinas administrativas (acessos protegidos; alteração de senha por admin também limpa `forcePasswordChange`). +- `GET auth.passwordvalidationinfos`: retorna as regras de senha atuais para o front-end. +- `GET|POST auth.govbr-email`: coleta e-mail alternativo quando o e-mail do Gov.br já está em uso (criação de conta). + +Metadados de usuário usados nesses fluxos: `forcePasswordChange`, `pendingTrashRestoreConfirm` (mais `tokenVerifyAccount` no e-mail de restore). + +## Testes +Regras isoladas em services (`GovBrAccountService`, `AccountLifecycleService`) têm testes unitários em `tests/` — cobrem CPF/e-mail único do Gov.br, troca de senha forçada e recuperação de conta na lixeira: + +```bash +cd plugins/MultipleLocalAuth +php composer.phar install # ou: composer install +./vendor/bin/phpunit +``` + +## Componentes que acompanham o plugin +- `components/login`: formulário de login com reCAPTCHA, recuperação de senha, aviso de conta na lixeira e botões sociais. +- `components/create-account`: esteira de cadastro com validações de senha, CPF e aceite de termos LGPD. +- `components/change-password`: formulário para troca de senha no painel e fluxo de troca forçada pós-login. +- `components/password-strongness`: barra de força da senha, reutilizada em cadastro e redefinição. + +Todos os componentes carregam textos a partir de `components/*/texts.php`, permitindo tradução personalizada. + +## Personalização +- E-mails: templates Mustache em `views/auth/email-to-validate-account.html`, `views/auth/email-resert-password.html` e `views/auth/email-account-restored.html` (confirmação de recuperação de conta na lixeira). Você pode copiar/adaptar mantendo as variáveis esperadas. +- Telas: `views/auth/*.php` rendem os componentes Vue; é possível sobrescrever esses arquivos em um tema customizado. +- Estilos: CSS compilado em `assets/css/plugin-MultiplLocalAuth.css`. O SCSS-fonte está em `assets-src/sass/`. +- Traduções: arquivos `.po` em `translations/` (domínio `multipleLocal`). + +## Boas práticas +- Configure o cron/serviço de fila de e-mail do Mapas Culturais antes de habilitar a confirmação por e-mail. +- Ajuste `metadataFieldCPF`/`metadataFieldPhone` para corresponder ao schema de metadados do seu deployment. +- Revise as mensagens carregadas via `textSupportSite`, `urlSupport*` para garantir contato adequado ao usuário. +- Gere URLs de callback das estratégias sociais com HTTPS e defina-as nos painéis dos provedores. + +--- +Mantemos este documento atualizado a partir do código-fonte do plugin. Contribuições são bem-vindas! \ No newline at end of file diff --git a/assets-src/sass/4-components/_c-change-password.scss b/assets-src/sass/4-components/_c-change-password.scss index db30d14..04384ff 100644 --- a/assets-src/sass/4-components/_c-change-password.scss +++ b/assets-src/sass/4-components/_c-change-password.scss @@ -1,7 +1,7 @@ .change-password { display: flex; flex-direction: column; - gap: 5px; + gap: 8px; padding: 16px 0 13px; &__title { @@ -10,9 +10,26 @@ font-weight: 600; } + &__pending-warning { + align-items: center; + background: var(--mc-alert-100, #fff3cd); + border-radius: 4px; + color: var(--mc-alert-700, #7a5b00); + display: flex; + font-size: 13px; + gap: 8px; + padding: 8px 12px; + + .iconify { + flex-shrink: 0; + font-size: 16px; + } + } + &__password { align-items: center; display: flex; + flex-wrap: wrap; gap: 18px; &--fakePassword { @@ -48,6 +65,29 @@ } } + &__force { + margin-top: 4px; + + &--action { + align-items: center; + color: var(--mc-helper-500); + cursor: pointer; + display: inline-flex; + gap: 6px; + + .iconify { + font-size: 15px; + } + + .label { + cursor: pointer; + font-size: 14px; + font-weight: 700; + line-height: 19px; + } + } + } + &__modal { .modal-content { max-width: 570px; diff --git a/components/change-password/script.js b/components/change-password/script.js index bd57308..8e3123b 100644 --- a/components/change-password/script.js +++ b/components/change-password/script.js @@ -65,6 +65,7 @@ app.component('change-password', { if (dataReturn.error) { this.throwErrors(dataReturn.data); } else { + this.entity.forcePasswordChange = '0'; this.messages.success('Senha alterada com sucesso!'); this.cancel(modal); } @@ -72,6 +73,22 @@ app.component('change-password', { } }, + async forcePasswordChange(modal) { + let api = new API(); + let data = { + 'email': this.entity.email, + } + await api.POST($MAPAS.baseURL+"autenticacao/forcepasswordchange", data).then(response => response.json().then(dataReturn => { + if (dataReturn.error) { + this.throwErrors(dataReturn.data); + } else { + this.entity.forcePasswordChange = '1'; + this.messages.success('Na próxima vez que este usuário entrar com a senha atual, ele será levado a trocá-la.'); + modal.close(); + } + })); + }, + cancel(modal) { this.newPassword = ''; this.confirmNewPassword = ''; diff --git a/components/change-password/template.php b/components/change-password/template.php index 8ec74b8..db12a05 100644 --- a/components/change-password/template.php +++ b/components/change-password/template.php @@ -14,6 +14,11 @@
+
+ + +
+
@@ -61,4 +66,26 @@
-
\ No newline at end of file + +
+ + + + + + + +
+
diff --git a/components/create-account/script.js b/components/create-account/script.js index bc83102..a7e4d8c 100644 --- a/components/create-account/script.js +++ b/components/create-account/script.js @@ -300,6 +300,17 @@ app.component('create-account', { if (this.agent.terms.area.length == 0) { errors.agent.push(__('Área de atuação obrigatória', 'create-account')); } + + // Validação de campos obrigatórios das taxonomias + Object.keys($TAXONOMIES).forEach(taxonomy => { + const t = $TAXONOMIES[taxonomy]; + if (t.required && t.entities.includes('MapasCulturais\\Entities\\Agent')) { + if(this.agent.terms[taxonomy].length == 0) { + errors.agent.push(`${t.description} ${__('required', 'create-account')}`); + } + } + }); + if (errors.agent.length > 0) { this.throwErrors(errors); return false; diff --git a/components/create-account/template.php b/components/create-account/template.php index a776a64..112acfd 100644 --- a/components/create-account/template.php +++ b/components/create-account/template.php @@ -15,6 +15,8 @@ mc-stepper password-strongness '); + +$taxonomies = $app->getRegisteredTaxonomies("MapasCulturais\Entities\Agent"); ?>
@@ -45,7 +47,7 @@
@@ -105,7 +107,12 @@ prop="name" fieldDescription=""> "> - "> + + + required): ?> + + + diff --git a/components/create-account/texts.php b/components/create-account/texts.php index 775370e..db2b025 100644 --- a/components/create-account/texts.php +++ b/components/create-account/texts.php @@ -5,5 +5,6 @@ 'Nome obrigatório' => i::__('O nome é obrigatório!'), 'Descrição obrigatória' => i::__('A descrição é obrigatória!'), 'Área de atuação obrigatória' => i::__('A área de atuação é obrigatória!'), + 'required' => i::__('é um campo obrigatório!'), ]; \ No newline at end of file diff --git a/components/login/script.js b/components/login/script.js index d616b91..0f04c29 100644 --- a/components/login/script.js +++ b/components/login/script.js @@ -21,9 +21,17 @@ app.component('login', { recoveryRequest: false, recoveryEmailSent: false, - + + accountInTrash: false, + trashProfileName: '', + restoreEmailSent: false, + + recoveryMode: $MAPAS.recoveryMode?.status ?? '', recoveryToken: $MAPAS.recoveryMode?.token ?? '', + + forcePasswordChangeMode: $MAPAS.forcePasswordChangeMode ?? false, + forcePasswordChangeEmail: $MAPAS.forcePasswordChangeEmail ?? '', } }, @@ -65,7 +73,12 @@ app.component('login', { await api.POST($MAPAS.baseURL+"autenticacao/login", dataPost).then(response => response.json().then(dataReturn => { if (dataReturn.error) { - this.throwErrors(dataReturn.data); + if (dataReturn.accountInTrash) { + this.accountInTrash = true; + this.trashProfileName = dataReturn.profileName; + } else { + this.throwErrors(dataReturn.data); + } } else { if(dataReturn.redirectTo) { window.location.href = dataReturn.redirectTo; @@ -76,6 +89,41 @@ app.component('login', { })); }, + /* Mandatory password change, forced by an admin, after a successful login */ + async doForcedPasswordChange() { + let api = new API(); + + let dataPost = { + 'new_password': this.password, + 'confirm_new_password': this.confirmPassword + } + + await api.POST($MAPAS.baseURL+"autenticacao/doforcedpasswordchange", dataPost).then(response => response.json().then(dataReturn => { + if (dataReturn.error) { + this.throwErrors(dataReturn.data); + } else { + const messages = useMessages(); + messages.success('Senha alterada com sucesso!'); + setTimeout(() => { + window.location.href = Utils.createUrl('panel', 'index'); + }, "1000") + } + })); + }, + + /* Confirm the restore of an account found in the trash bin during login */ + async confirmRestore() { + let api = new API(); + + await api.POST($MAPAS.baseURL+"autenticacao/confirmrestore", {}).then(response => response.json().then(dataReturn => { + if (dataReturn.error) { + this.throwErrors(dataReturn.data); + } else { + this.restoreEmailSent = true; + } + })); + }, + /* Request password recover */ async requestRecover() { let api = new API(); diff --git a/components/login/template.php b/components/login/template.php index 1ddbe24..5fe45de 100644 --- a/components/login/template.php +++ b/components/login/template.php @@ -17,7 +17,7 @@ -
+
@@ -112,6 +116,74 @@
+ +
+
+ + + +
+ +
+ +
+
+ + +
+
+ + + +
+
+
diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b891f19 --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "mapasculturais/plugin-multiple-local-auth", + "description": "Multiple local and social authentication for Mapas Culturais", + "type": "library", + "require": { + "php": ">=8.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "autoload": { + "psr-4": { + "MultipleLocalAuth\\": "" + }, + "classmap": [ + "GovBr/GovBrAccountService.php" + ] + }, + "autoload-dev": { + "psr-4": { + "MultipleLocalAuth\\Tests\\": "tests/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..6ad7def --- /dev/null +++ b/composer.lock @@ -0,0 +1,1673 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "9d6368cce9d419478bb8d8727831f859", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.64", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "0e8c1d19cea35ad97d4887f363d07c78e30fbf06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0e8c1d19cea35ad97d4887f363d07c78e30fbf06", + "reference": "0e8c1d19cea35ad97d4887f363d07c78e30fbf06", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.64" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-07-06T14:50:35+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.0" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..eab7133 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,12 @@ + + + + + tests + + + diff --git a/tests/AccountLifecycleServiceTest.php b/tests/AccountLifecycleServiceTest.php new file mode 100644 index 0000000..795a100 --- /dev/null +++ b/tests/AccountLifecycleServiceTest.php @@ -0,0 +1,185 @@ +assertTrue(AccountLifecycleService::mustChangePassword('1')); + $this->assertFalse(AccountLifecycleService::mustChangePassword('0')); + $this->assertFalse(AccountLifecycleService::mustChangePassword(null)); + $this->assertFalse(AccountLifecycleService::mustChangePassword('')); + $this->assertFalse(AccountLifecycleService::mustChangePassword('true')); + } + + public function testForcePasswordChangeRequiresAdmin(): void + { + $this->assertSame('permission', AccountLifecycleService::forcePasswordChangeError(false, true)); + $this->assertSame('permission', AccountLifecycleService::forcePasswordChangeError(false, false)); + } + + public function testForcePasswordChangeRequiresExistingUser(): void + { + $this->assertSame('not_found', AccountLifecycleService::forcePasswordChangeError(true, false)); + } + + public function testForcePasswordChangeOkForAdminWithUser(): void + { + $this->assertNull(AccountLifecycleService::forcePasswordChangeError(true, true)); + } + + public function testCanDoForcedPasswordChangeRequiresPendingFlag(): void + { + $this->assertTrue(AccountLifecycleService::canDoForcedPasswordChange(true)); + $this->assertFalse(AccountLifecycleService::canDoForcedPasswordChange(false)); + } + + public function testMetadataConstantsMatchRegisteredKeys(): void + { + $this->assertSame('forcePasswordChange', AccountLifecycleService::FORCE_PASSWORD_CHANGE_METADATA); + $this->assertSame('pendingTrashRestoreConfirm', AccountLifecycleService::PENDING_TRASH_RESTORE_CONFIRM_METADATA); + $this->assertSame('pendingTrashRestoreUserId', AccountLifecycleService::PENDING_TRASH_RESTORE_SESSION_KEY); + } + + // --- Trash restore on login --- + + public function testShouldOfferTrashRestoreWhenPasswordOkAndStatusTrash(): void + { + $this->assertTrue(AccountLifecycleService::shouldOfferTrashRestore( + false, + AccountLifecycleService::STATUS_TRASH + )); + } + + public function testShouldNotOfferTrashRestoreWhenOtherErrorsExist(): void + { + $this->assertFalse(AccountLifecycleService::shouldOfferTrashRestore( + true, + AccountLifecycleService::STATUS_TRASH + )); + } + + public function testShouldNotOfferTrashRestoreWhenAccountIsActive(): void + { + $this->assertFalse(AccountLifecycleService::shouldOfferTrashRestore(false, 1)); + $this->assertFalse(AccountLifecycleService::shouldOfferTrashRestore(false, 0)); + } + + public function testBuildAccountInTrashLoginResultShape(): void + { + $errors = ['login' => []]; + $result = AccountLifecycleService::buildAccountInTrashLoginResult('Maria Silva', $errors); + + $this->assertFalse($result['success']); + $this->assertTrue($result['accountInTrash']); + $this->assertSame('Maria Silva', $result['profileName']); + $this->assertSame($errors, $result['errors']); + } + + public function testBuildAccountInTrashLoginResultWithEmptyProfileName(): void + { + $result = AccountLifecycleService::buildAccountInTrashLoginResult(''); + $this->assertSame('', $result['profileName']); + $this->assertSame([], $result['errors']); + } + + // --- confirmRestoreAccount preconditions --- + + public function testConfirmRestoreExpiredWithoutSession(): void + { + $user = (object) ['status' => AccountLifecycleService::STATUS_TRASH]; + $this->assertSame('expired', AccountLifecycleService::confirmRestoreError(null, $user)); + $this->assertSame('expired', AccountLifecycleService::confirmRestoreError(0, $user)); + } + + public function testConfirmRestoreNotTrashWhenUserMissingOrActive(): void + { + $this->assertSame( + 'not_trash', + AccountLifecycleService::confirmRestoreError(42, null) + ); + + $active = (object) ['status' => 1]; + $this->assertSame( + 'not_trash', + AccountLifecycleService::confirmRestoreError(42, $active) + ); + } + + public function testConfirmRestoreOkWhenUserInTrash(): void + { + $trashed = (object) ['status' => AccountLifecycleService::STATUS_TRASH]; + $this->assertNull(AccountLifecycleService::confirmRestoreError(42, $trashed)); + } + + // --- Email confirm restores trash --- + + public function testShouldRestoreOnEmailConfirmOnlyWhenPendingFlagIsOne(): void + { + $this->assertTrue(AccountLifecycleService::shouldRestoreOnEmailConfirm('1')); + $this->assertFalse(AccountLifecycleService::shouldRestoreOnEmailConfirm('0')); + $this->assertFalse(AccountLifecycleService::shouldRestoreOnEmailConfirm(null)); + $this->assertFalse(AccountLifecycleService::shouldRestoreOnEmailConfirm('')); + } + + // --- restoreUserFromTrash helpers --- + + public function testRelatedEntityTypesToRestore(): void + { + $types = AccountLifecycleService::relatedEntityTypesToRestore(); + $this->assertSame( + ['agents', 'spaces', 'projects', 'opportunities', 'events'], + $types + ); + } + + public function testEntityShouldBeUndeletedOnlyWhenInTrash(): void + { + $this->assertTrue(AccountLifecycleService::entityShouldBeUndeleted(AccountLifecycleService::STATUS_TRASH)); + $this->assertFalse(AccountLifecycleService::entityShouldBeUndeleted(1)); + $this->assertFalse(AccountLifecycleService::entityShouldBeUndeleted(0)); + } + + // --- Session pending restore --- + + public function testPendingTrashRestoreSessionRoundtrip(): void + { + $this->assertNull(AccountLifecycleService::getPendingTrashRestoreUserId()); + + AccountLifecycleService::storePendingTrashRestore(99); + $this->assertSame(99, AccountLifecycleService::getPendingTrashRestoreUserId()); + $this->assertSame( + 99, + $_SESSION[AccountLifecycleService::PENDING_TRASH_RESTORE_SESSION_KEY] + ); + + AccountLifecycleService::clearPendingTrashRestore(); + $this->assertNull(AccountLifecycleService::getPendingTrashRestoreUserId()); + $this->assertArrayNotHasKey( + AccountLifecycleService::PENDING_TRASH_RESTORE_SESSION_KEY, + $_SESSION + ); + } +} diff --git a/tests/GovBrAccountServiceTest.php b/tests/GovBrAccountServiceTest.php new file mode 100644 index 0000000..ede5dc5 --- /dev/null +++ b/tests/GovBrAccountServiceTest.php @@ -0,0 +1,128 @@ + [ + 'provider' => $provider, + 'uid' => $cpf, + 'raw' => [ + 'sub' => preg_replace('/\D+/', '', $cpf), + 'cpf' => preg_replace('/\D+/', '', $cpf), + 'email' => $email, + ], + 'info' => [ + 'cpf' => preg_replace('/\D+/', '', $cpf), + 'email' => $email, + 'name' => 'Marcia', + 'full_name' => 'Marcia Barbosa', + ], + ], + ]; + } + + public function testMaskCpf(): void + { + $this->assertSame('867.427.604-00', GovBrAccountService::maskCpf('86742760400')); + $this->assertSame('867.427.604-00', GovBrAccountService::maskCpf('867.427.604-00')); + $this->assertNull(GovBrAccountService::maskCpf('123')); + } + + public function testNormalizeAndValidateEmail(): void + { + $this->assertSame('a@b.com', GovBrAccountService::normalizeEmail(' A@B.com ')); + $this->assertTrue(GovBrAccountService::isValidEmail('a@b.com')); + $this->assertFalse(GovBrAccountService::isValidEmail('nao-email')); + $this->assertFalse(GovBrAccountService::isValidEmail('')); + } + + public function testExtractCpfAndEmailFromResponse(): void + { + $response = $this->sampleResponse('867.427.604-00', 'idealfotostudio@hotmail.com'); + $this->assertSame('867.427.604-00', GovBrAccountService::extractCpfFromResponse($response)); + $this->assertSame('idealfotostudio@hotmail.com', GovBrAccountService::extractEmailFromResponse($response)); + } + + public function testHasEmailConflictWhenEmailAlreadyExists(): void + { + $existing = ['idealfotostudio@hotmail.com']; + $checker = function (string $email) use ($existing) { + return in_array($email, $existing, true); + }; + + $response = $this->sampleResponse('867.427.604-00', 'idealfotostudio@hotmail.com'); + $this->assertTrue(GovBrAccountService::hasEmailConflictOnCreate($response, $checker)); + + $free = $this->sampleResponse('867.427.604-00', 'marcia.unica@example.com'); + $this->assertFalse(GovBrAccountService::hasEmailConflictOnCreate($free, $checker)); + } + + public function testHasEmailConflictWhenGovBrEmailMissing(): void + { + $response = $this->sampleResponse('867.427.604-00', null); + $this->assertTrue(GovBrAccountService::hasEmailConflictOnCreate($response, fn () => false)); + } + + public function testNoEmailConflictForNonGovBrProvider(): void + { + $response = $this->sampleResponse('867.427.604-00', 'idealfotostudio@hotmail.com', 'Google'); + $this->assertFalse(GovBrAccountService::hasEmailConflictOnCreate($response, fn () => true)); + } + + public function testValidateAlternateEmailRejectsExisting(): void + { + $checker = fn (string $email) => $email === 'ja.existe@example.com'; + + $errors = GovBrAccountService::validateAlternateEmail('ja.existe@example.com', $checker); + $this->assertNotEmpty($errors); + $this->assertStringContainsString('já está em uso', $errors[0]); + + $ok = GovBrAccountService::validateAlternateEmail('novo@example.com', $checker); + $this->assertSame([], $ok); + } + + public function testValidateAlternateEmailRejectsInvalid(): void + { + $errors = GovBrAccountService::validateAlternateEmail('xyz', fn () => false); + $this->assertNotEmpty($errors); + } + + public function testApplyEmailToResponse(): void + { + $response = $this->sampleResponse('867.427.604-00', 'idealfotostudio@hotmail.com'); + $updated = GovBrAccountService::applyEmailToResponse($response, ' Novo@Example.com '); + $this->assertSame('novo@example.com', $updated['auth']['info']['email']); + } + + public function testPendingRegistrationSessionRoundtrip(): void + { + $_SESSION = []; + $response = $this->sampleResponse('867.427.604-00', 'idealfotostudio@hotmail.com'); + + GovBrAccountService::storePendingRegistration($response); + $this->assertSame($response, GovBrAccountService::getPendingRegistration()); + + GovBrAccountService::clearPendingRegistration(); + $this->assertNull(GovBrAccountService::getPendingRegistration()); + } + + public function testIsGovBrProvider(): void + { + $this->assertTrue(GovBrAccountService::isGovBrProvider('GovBr')); + $this->assertTrue(GovBrAccountService::isGovBrProvider('govbr')); + $this->assertFalse(GovBrAccountService::isGovBrProvider('Google')); + } +} diff --git a/translations/es_AR.mo b/translations/es_AR.mo new file mode 100644 index 0000000..aeb86e6 Binary files /dev/null and b/translations/es_AR.mo differ diff --git a/translations/es_AR.po b/translations/es_AR.po new file mode 100644 index 0000000..e332910 --- /dev/null +++ b/translations/es_AR.po @@ -0,0 +1,598 @@ +msgid "" +msgstr "" +"Project-Id-Version: Multiple Local Auth\n" +"POT-Creation-Date: 2024-02-28 11:07-0300\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4.2\n" +"X-Poedit-Basepath: ..\n" +"X-Poedit-KeywordsList: _e;__;esc_attr_e;i::__\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-SearchPath-0: .\n" + +#: Plugin.php:42 +msgid "modificar senha" +msgstr "modificar contraseña" + +#: Plugin.php:53 components/change-password/template.php:33 +#: components/create-account/template.php:56 components/login/template.php:36 +#: components/login/template.php:125 +msgid "Senha" +msgstr "Contraseña" + +#: Plugin.php:54 +msgid "Token para recuperação de senha" +msgstr "Token de recuperación de contraseña" + +#: Plugin.php:55 +msgid "Timestamp do token para recuperação de senha" +msgstr "Marca de tiempo del token para la recuperación de la contraseña" + +#: Plugin.php:56 +msgid "Conta ativa?" +msgstr "¿Cuenta activa?" + +#: Plugin.php:57 +msgid "Token de verificação" +msgstr "Token de verificación" + +#: Plugin.php:58 +msgid "Número de tentativas de login" +msgstr "Número de intentos de inicio de sesión" + +#: Plugin.php:59 +msgid "Tempo de bloqueio por excesso de tentativas" +msgstr "Tiempo de bloqueo debido a intentos excesivos" + +#: Provider.php:161 +msgid "Token inválidos" +msgstr "Token incorrectos" + +#: Provider.php:422 +msgid "Minha conta" +msgstr "Mi cuenta" + +#: Provider.php:524 +msgid "Sua senha deve conter pelo menos " +msgstr "La contraseña debe contener al menos " + +#: Provider.php:527 +msgid " Sua senha deve conter pelo menos 1 número!" +msgstr " ¡Su contraseña debe contener al menos 1 número!" + +#: Provider.php:530 +msgid " Sua senha deve conter pelo menos 1 letra maiúscula!" +msgstr " ¡Su contraseña debe contener al menos 1 letra mayúscula !" + +#: Provider.php:533 +msgid " Sua senha deve conter pelo menos 1 letra minúscula!" +msgstr " ¡Su contraseña debe contener al menos 1 letra minúscula!" + +#: Provider.php:536 +msgid " Sua senha deve conter pelo menos 1 caractere especial!" +msgstr " ¡Su contraseña debe contener al menos 1 carácter especial!" + +#: Provider.php:539 +msgid "Por favor, insira sua senha." +msgstr "Por favor, introduzca su contraseña." + +#: Provider.php:543 +msgid "As senhas não conferem." +msgstr "Las contraseñas no coinciden." + +#: Provider.php:575 Provider.php:742 Provider.php:1031 +msgid "Captcha incorreto, tente novamente!" +msgstr "¡Captcha incorrecto, inténtalo de nuevo!" + +#: Provider.php:587 +msgid "Por favor, informe um cpf válido." +msgstr "Por favor, introduzca una ci válida." + +#: Provider.php:611 +msgid "Este CPF já esta em uso. Tente recuperar a sua senha." +msgstr "Esta CI ya está en uso. Intente recuperar su contraseña." + +#: Provider.php:622 +msgid "Este endereço de email já está em uso. Tente recuperar a sua senha." +msgstr "" +"Esta dirección de correo electrónico ya está en uso. Intente recuperar su " +"contraseña." + +#: Provider.php:628 +msgid "Por favor, informe um email válido." +msgstr "Por favor, ingrese un correo electrónico válido." + +#: Provider.php:675 +msgid "Token não encontrado." +msgstr "Token no encontrado." + +#: Provider.php:694 +msgid "Este token expirou." +msgstr "Este token expiró." + +#: Provider.php:750 +msgid "Email não encontrado" +msgstr "Email no encontrado" + +#: Provider.php:776 +#, php-format +msgid "Pedido de recuperação de senha para %s" +msgstr "Pedido de recuperación de contraseña para %s" + +#: Provider.php:811 +msgid "" +"Erro ao enviar email de recuperação. Entre em contato com os administradors " +"do site." +msgstr "" +"Error al enviar el correo electrónico de recuperación. Entre en contacto con " +"los administradores del sitio." + +#: Provider.php:848 Provider.php:899 +msgid "Insira sua nova senha." +msgstr "Agregar su nueva contraseña." + +#: Provider.php:894 +msgid "Senha atual inválida." +msgstr "Contraseña actual incorrecta." + +#: Provider.php:921 +msgid "A senha deve conter uma letra maiúscula" +msgstr "La contraseña debe contener una letra mayúscula" + +#: Provider.php:922 +msgid "A senha deve conter uma letra minúscula" +msgstr "La contraseña debe contener una letra minúscula" + +#: Provider.php:923 +msgid "A senha deve conter um caractere especial" +msgstr "La contraseña debe contener un carácter especial" + +#: Provider.php:924 +msgid "A senha deve conter um número " +msgstr "La contraseña debe contener un número " + +#: Provider.php:925 +msgid "O tamanho mínimo da senha é de: " +msgstr "La longitud mínima de la contraseña es: " + +#: Provider.php:1062 +msgid "CPF ou senha incorreta, tente novamente!" +msgstr "¡CI o contraseña incorrecta, inténtalo de nuevo!" + +#: Provider.php:1084 +msgid "" +"Você possui 2 ou mais agente com o mesmo CPF! Por favor entre em contato com " +"o suporte." +msgstr "" +"¡Tienes 2 o más agentes con la misma CI! Por favor contacta al servicio de " +"soporte." + +#: Provider.php:1089 +msgid "" +"Você possui 2 ou mais agentes inativos com o mesmo CPF! Por favor entre em " +"contato com o suporte." +msgstr "" +"¡Tienes 2 o más agentes inactivos con la misma CI! Por favor contacta con el " +"servicio de soporte." + +#: Provider.php:1095 +msgid "CPF ou senha incorreta. Utilize o CPF do seu agente principal." +msgstr "CI o contraseña incorrectas. Utilice la CI de su agente principal." + +#: Provider.php:1115 Provider.php:1149 +msgid "Usuário ou senha inválidos." +msgstr "Usuario o contraseña incorrectos." + +#: Provider.php:1121 +msgid "Verifique seu email para validar a sua conta." +msgstr "Revise su correo electrónico para validar su cuenta." + +#: Provider.php:1130 +msgid "Login bloqueado, tente novamente em " +msgstr "Inicio de sesión bloqueado, inténtalo de nuevo en " + +#: Provider.php:1455 +msgid "É preciso estar autenticado para realizar esta ação" +msgstr "Es preciso está autenticado para realizar esta acción" + +#: components/change-password/template.php:15 +msgid "Senha:" +msgstr "Contraseña:" + +#: components/change-password/template.php:27 +#: views/panel/multiple-local-auth--my-account.php:33 +msgid "Senha atual" +msgstr "Contraseña actual" + +#: components/change-password/template.php:37 +msgid "A senha deve ter:" +msgstr "La contraseña debe tener:" + +#: components/change-password/template.php:38 +msgid "" +" caracteres, um número, um caractere especial (! @ # $ & *), pelo menos uma " +"letra maiúscula e uma minúscula." +msgstr "" +" caracteres, un número, un carácter especial (! @ # $ & *), al menos una " +"letra mayúscula y una letra minúscula." + +#: components/change-password/template.php:43 +msgid "Confirme a senha" +msgstr "Confirmar contraseña" + +#: components/change-password/template.php:54 +#: components/change-password/template.php:59 components/login/template.php:93 +msgid "Alterar senha" +msgstr "Cambiar contraseña" + +#: components/change-password/template.php:60 +msgid "Cancelar" +msgstr "Cancelar" + +#: components/create-account/script.js:302 +msgid "Nome obrigatório" +msgstr "Nombre obligatorio" + +#: components/create-account/script.js:305 +msgid "Descrição obrigatória" +msgstr "Descripción obligatoria" + +#: components/create-account/script.js:308 +msgid "Área de atuação obrigatória" +msgstr "Área de actuación obligatoria" + +#: components/create-account/template.php:23 +msgid "Novo cadastro" +msgstr "Nuevo registro" + +#: components/create-account/template.php:24 +#, php-format +msgid "Siga os passos para criar o seu cadastro no %s." +msgstr "Siga los pasos para crear su registro en %s." + +#: components/create-account/template.php:38 components/login/template.php:89 +msgid "E-mail" +msgstr "E-mail" + +#: components/create-account/template.php:43 +msgid "CPF" +msgstr "CI" + +#: components/create-account/template.php:46 +msgid "Por que pedimos este dado" +msgstr "Por qué pedimos este dato" + +#: components/create-account/template.php:48 +msgid "Texto sobre o motivo da coleta do CPF" +msgstr "Texto sobre el motivo de la recogida del dato de la CI" + +#: components/create-account/template.php:62 +msgid "Confirme sua senha" +msgstr "Confirmar contraseña" + +#: components/create-account/template.php:71 +msgid "Continuar" +msgstr "Continuar" + +#: components/create-account/template.php:79 components/login/template.php:55 +msgid "Entrar com Gov.br" +msgstr "Entrar con Gob.uy" + +#: components/create-account/template.php:83 components/login/template.php:60 +msgid "Entrar com Google" +msgstr "Entrar con Google" + +#: components/create-account/template.php:94 +msgid "Voltar e excluir minhas informações" +msgstr "Volver atrás y borrar mi información" + +#: components/create-account/template.php:101 +msgid "Falta pouco para finalizar o seu cadastro!" +msgstr "Queda poco para que finalice su registro!" + +#: components/create-account/template.php:102 +msgid "Dê um nome e faça uma breve descrição sua." +msgstr "Ponte un nombre y una breve descripción." + +#: components/create-account/template.php:106 +msgid "Nome" +msgstr "Nombre" + +#: components/create-account/template.php:106 +msgid "As pessoas irão encontrar você por esse nome." +msgstr "Las personas te encontrarán por ese nombre." + +#: components/create-account/template.php:107 +msgid "Mini Bio" +msgstr "Mini Bio" + +#: components/create-account/template.php:108 +msgid "Área de atuação" +msgstr "Área de actuación" + +#: components/create-account/template.php:112 +msgid "Criar cadastro" +msgstr "Crear registro" + +#: components/create-account/template.php:123 +msgid "E-mail de confirmação enviado!" +msgstr "Confirmación enviada por correo electrónico!" + +#: components/create-account/template.php:124 +msgid "Seu cadastro foi criado com sucesso!" +msgstr "Su registro fue realizado con éxito!" + +#: components/create-account/template.php:127 +#, php-format +msgid "Acesse seu e-mail para confirmar a criação de seu cadastro no %s." +msgstr "" +"Acceda a su correo electrónico para confirmar la creación de su registro en " +"%s." + +#: components/create-account/template.php:129 +msgid "Acessar meu cadastro" +msgstr "Acceder a mi inscripción" + +#: components/create-account/texts.php:5 +msgid "O nome é obrigatório!" +msgstr "El nombre es obligatorio!" + +#: components/create-account/texts.php:6 +msgid "A descrição é obrigatória!" +msgstr "La descripción es obligatoria!" + +#: components/create-account/texts.php:7 +msgid "A área de atuação é obrigatória!" +msgstr "El área de actuación es obligatoria!" + +#: components/login/template.php:23 +msgid "Boas vindas!" +msgstr "Bienvenid@!" + +#: components/login/template.php:24 +#, php-format +msgid "Entre na sua conta do %s" +msgstr "Entre en su cuenta de %s" + +#: components/login/template.php:31 +msgid "E-mail ou CPF" +msgstr "E-mail o CI" + +#: components/login/template.php:38 +msgid "Esqueci minha senha" +msgstr "Olvidé mi contraseña" + +#: components/login/template.php:46 +msgid "Entrar" +msgstr "Ingresar" + +#: components/login/template.php:49 +msgid "Ou entre com" +msgstr "O entre con" + +#: components/login/template.php:67 +#, php-format +msgid "Ainda não tem cadastro no %s? Realize seu cadastro agora!" +msgstr "Todavía no está registrado en %s? Realice su registro ahora!" + +#: components/login/template.php:70 +msgid "Fazer cadastro" +msgstr "Hacer registro" + +#: components/login/template.php:82 components/login/template.php:103 +#: views/auth/confirm-email.php:20 +msgid "Alteração de senha" +msgstr "Cambio de contraseña" + +#: components/login/template.php:83 +msgid "Se você esqueceu a senha, não se preocupe, todo mundo passa por isso." +msgstr "" +"Si has olvidado tu contraseña, no te preocupes, todo el mundo pasa por eso." + +#: components/login/template.php:83 +msgid "Digite seu e-mail para criar uma nova." +msgstr "Escriba su correo electrónico para crear su cuenta." + +#: components/login/template.php:94 components/login/template.php:109 +#: views/auth/register.php:16 +msgid "Voltar" +msgstr "Volver" + +#: components/login/template.php:105 views/auth/confirm-email.php:22 +msgid "Enviamos as instruções de alteração de senha para seu e-mail." +msgstr "" +"Enviamos las instrucciones para el cambio de contraseña a su correo " +"electrónico." + +#: components/login/template.php:108 +msgid "Não recebi o e-mail" +msgstr "No recibí el correo electrónico" + +#: components/login/template.php:119 +msgid "Redefinir senha de acesso" +msgstr "Redefinir contraseña de acceso" + +#: components/login/template.php:131 +msgid "Confirme sua nova senha" +msgstr "Confirmar nueva contraseña" + +#: components/login/template.php:139 +msgid "Redefinir senha" +msgstr "Redefinir contraseña" + +#: components/password-strongness/template.php:12 +msgid "Força da senha" +msgstr "Fuerza de la contraseña" + +#: components/password-strongness/template.php:18 +msgid "A senha deve conter:" +msgstr "La contraseña debe contener:" + +#: components/password-strongness/texts.php:6 +msgid "{num} caracteres" +msgstr "{num} caracteres" + +#: components/password-strongness/texts.php:7 +msgid "pelo menos uma letra maiúscula" +msgstr "por lo menos una letra mayúscula" + +#: components/password-strongness/texts.php:8 +msgid "pelo menos uma letra minúscula" +msgstr "por lo menos una letra mayúscula" + +#: components/password-strongness/texts.php:9 +msgid "um caracter especial (! @ # $ % & * < > ?)" +msgstr "un carácter especial (! @ # $ % & * < > ?)" + +#: components/password-strongness/texts.php:10 +msgid "um número" +msgstr "un número" + +#: views/auth/confirm-email.php:25 +msgid "Entrar na minha conta" +msgstr "Entrar a mi cuenta" + +#: views/panel/multiple-local-auth--my-account.php:19 +#: views/panel/multiple-local-auth--my-account.php:53 +msgid "Guardar alteraçoes" +msgstr "Guardar cambios" + +#: views/panel/multiple-local-auth--my-account.php:20 +msgid "Trocar e-mail" +msgstr "Cambiar contraseña" + +#: views/panel/multiple-local-auth--my-account.php:24 +msgid "Email" +msgstr "E-mail" + +#: views/panel/multiple-local-auth--my-account.php:30 +msgid "Trocar Senha" +msgstr "Cambiar contraseña" + +#: views/panel/multiple-local-auth--my-account.php:40 +msgid "Nova senha" +msgstr "Nueva contraseña" + +#: views/panel/multiple-local-auth--my-account.php:48 +msgid "Confirmar nova senha" +msgstr "Confirmar nueva contraseña" + +#: views/panel/multiple-local-auth--my-account.php:65 +msgid "Vincular conta com" +msgstr "Vincular cuenta con" + +#~ msgid "Email validado com sucesso" +#~ msgstr "Email validado con éxito" + +#~ msgid "Senha alterada com sucesso. Agora você pode fazer login" +#~ msgstr "Contraseña cambiada con éxito! Usted puede ingresar ahora" + +#~ msgid "Por favor, informe seu nome" +#~ msgstr "Por favor, ingrese su nombre" + +#~ msgid "Este endereço de email já está em uso" +#~ msgstr "Esta dirección de email ya está en uso" + +#~ msgid "Email alterado com sucesso" +#~ msgstr "Email cambiado con éxito" + +#~ msgid "Informe um email válido" +#~ msgstr "Ingrese un email válido" + +#~ msgid "Email e senha alterados com sucecsso" +#~ msgstr "Email y contraseña cambiados con éxito" + +#~ msgid "Senha alterada com sucesso" +#~ msgstr "Contraseña cambiada con éxito" + +#~ msgid "Email ou token inválidos" +#~ msgstr "Email o token incorrectos" + +#~ msgid "Senha alterada com sucesso! Você pode fazer login agora" +#~ msgstr "Contraseña cambiada con éxito! Ahora puede ingresar" + +#~ msgid "" +#~ "Sucesso: Um e-mail foi enviado com instruções para recuperação da senha." +#~ msgstr "" +#~ "Un mensaje de correo electrónico fue enviado con instrucciones para " +#~ "recuperar la contraseña." + +#~ msgid "CPF ou senha incorreta" +#~ msgstr "CI o contraseña incorrectas" + +#~ msgid "Sucesso: Um e-mail lhe foi enviado com detalhes sobre a plataforma " +#~ msgstr "" +#~ "Éxito: se le envió un correo electrónico con detalles sobre la plataforma " + +#~ msgid "Informação" +#~ msgstr "Información" + +#~ msgid "Clique aqui para entrar no sistema" +#~ msgstr "Haga clic aquí para ingresar al sistema" + +#~ msgid "Se você já possui uma conta no " +#~ msgstr "Si ya tiene una cuenta en " + +#~ msgid "esqueci a senha" +#~ msgstr "olvidé mi contraseña" + +#~ msgid "Ou conecte usando sua conta em" +#~ msgstr "O conéctese usando su cuenta en" + +#~ msgid "Ainda não possui uma conta?" +#~ msgstr "¿Aún no tienes una cuenta?" + +#~ msgid "Crie uma conta agora" +#~ msgstr "Cree una cuenta ahora" + +#~ msgid "Para recuperar sua senha, informe o e-mail utilizado no cadastro." +#~ msgstr "" +#~ "Para recuperar su contraseña, informe el e-mail utilizado en su registro." + +#~ msgid "Se ainda não possui conta no " +#~ msgstr "Si no tiene una cuenta en " + +#~ msgid "Criar Conta" +#~ msgstr "Crear cuenta" + +#~ msgid "Recuperar" +#~ msgstr "Recuperar" + +#~ msgid "" +#~ "Alguém solicitou a recuperação da senha utilizada em %s por este email.\n" +#~ "\n" +#~ "Para recuperá-la, acesse o link: %s. /n/n Se você não pediu a recuperação " +#~ "desta senha, apenas ignore esta mensagem." +#~ msgstr "" +#~ "Alguien solicitó la recuperación de contraseña utilizada en %s por este " +#~ "email.\n" +#~ "\n" +#~ "Para recuperarla, haga clic en el link: %s./n/n Si usted no pidió la " +#~ "recuperación de esta contraseña, ignore este mensaje." + +#~ msgid "Registrar-se" +#~ msgstr "Registrarse" + +#~ msgid "Redes Sociais" +#~ msgstr "Redes Sociales" + +#~ msgid "Utilize sua conta em outros serviços para autenticar-se" +#~ msgstr "Utilice su cuenta en otros servicios para autenticarse" + +#~ msgid "Clique para marcar/desmarcar este" +#~ msgstr "Haga clic para marcar/desmarcar este" + +#~ msgid "Este" +#~ msgstr "Este" + +#~ msgid "Para relacionar o selo ao" +#~ msgstr "Para relacionar el sello al" + +#~ msgid "teste pt" +#~ msgstr "teste es" diff --git a/translations/es_ES.mo b/translations/es_ES.mo old mode 100644 new mode 100755 index 4172e6c..4e28068 Binary files a/translations/es_ES.mo and b/translations/es_ES.mo differ diff --git a/translations/es_ES.po b/translations/es_ES.po old mode 100644 new mode 100755 index 8cf5ccc..f4a9f10 --- a/translations/es_ES.po +++ b/translations/es_ES.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Multiple Local Auth\n" -"POT-Creation-Date: 2017-01-12 02:36-0300\n" +"POT-Creation-Date: 2025-04-07 19:07-0300\n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: \n" @@ -9,193 +9,521 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 1.5.4\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.6\n" "X-Poedit-Basepath: ..\n" "X-Poedit-KeywordsList: _e;__;esc_attr_e;i::__\n" "X-Poedit-SourceCharset: UTF-8\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Poedit-SearchPath-0: .\n" +"X-Poedit-SearchPathExcluded-0: assets\n" + +#: Plugin.php:42 +msgid "modificar senha" +msgstr "modificar contraseña" + +#: Plugin.php:53 components/change-password/template.php:33 +#: components/create-account/template.php:56 components/login/template.php:36 +#: components/login/template.php:125 +msgid "Senha" +msgstr "Contraseña" + +#: Plugin.php:54 +msgid "Token para recuperação de senha" +msgstr "Token de recuperación de contraseña" + +#: Plugin.php:55 +msgid "Timestamp do token para recuperação de senha" +msgstr "Marca de tiempo del token para la recuperación de la contraseña" -#: Provider.php:119 +#: Plugin.php:56 +msgid "Conta ativa?" +msgstr "¿Cuenta activa?" + +#: Plugin.php:57 +msgid "Token de verificação" +msgstr "Token de verificación" + +#: Plugin.php:58 +msgid "Número de tentativas de login" +msgstr "Número de intentos de inicio de sesión" + +#: Plugin.php:59 +msgid "Tempo de bloqueio por excesso de tentativas" +msgstr "Tiempo de bloqueo debido a intentos excesivos" + +#: Provider.php:163 +msgid "Token inválidos" +msgstr "Token incorrectos" + +#: Provider.php:424 msgid "Minha conta" msgstr "Mi cuenta" -#: Provider.php:152 -msgid "A senha deve conter no mínimo 6 caracteres" -msgstr "La contraseña debe contener al menos 6 caracteres" +#: Provider.php:488 +msgid "Sua senha deve conter pelo menos " +msgstr "La contraseña debe contener al menos " + +#: Provider.php:491 +msgid " Sua senha deve conter pelo menos 1 número!" +msgstr " ¡Su contraseña debe contener al menos 1 número!" -#: Provider.php:155 -msgid "As senhas não conferem" -msgstr "Las contraseñas no coinciden" +#: Provider.php:494 +msgid " Sua senha deve conter pelo menos 1 letra maiúscula!" +msgstr " ¡Su contraseña debe contener al menos 1 letra mayúscula !" -#: Provider.php:173 -msgid "Por favor, informe seu nome" -msgstr "Por favor, ingrese su nombre" +#: Provider.php:497 +msgid " Sua senha deve conter pelo menos 1 letra minúscula!" +msgstr " ¡Su contraseña debe contener al menos 1 letra minúscula!" -#: Provider.php:178 -msgid "Este endereço de email já está em uso" -msgstr "Esta dirección de email ya está en uso" +#: Provider.php:500 +msgid " Sua senha deve conter pelo menos 1 caractere especial!" +msgstr " ¡Su contraseña debe contener al menos 1 carácter especial!" -#: Provider.php:182 -msgid "Por favor, informe um email válido" -msgstr "Por favor, ingrese un email válido" +#: Provider.php:503 +msgid "Por favor, insira sua senha." +msgstr "Por favor, introduzca su contraseña." -#: Provider.php:207 -msgid "Email alterado com sucesso" -msgstr "Email cambiado con éxito" +#: Provider.php:507 +msgid "As senhas não conferem." +msgstr "Las contraseñas no coinciden." -#: Provider.php:210 -msgid "Informe um email válido" -msgstr "Ingrese un email válido" +#: Provider.php:542 Provider.php:708 Provider.php:997 +msgid "Captcha incorreto, tente novamente!" +msgstr "¡Captcha incorrecto, inténtalo de nuevo!" -#: Provider.php:227 -msgid "Email e senha alterados com sucecsso" -msgstr "Email y contraseña cambiados con éxito" +#: Provider.php:554 +msgid "Por favor, informe um cpf válido." +msgstr "Por favor, introduzca una ci válida." -#: Provider.php:227 -msgid "Senha alterada com sucesso" -msgstr "Contraseña cambiada con éxito" +#: Provider.php:576 +msgid "Este CPF já esta em uso. Tente recuperar a sua senha." +msgstr "Esta CI ya está en uso. Intente recuperar su contraseña." -#: Provider.php:234 -msgid "Senha inválida" -msgstr "Contraseña incorrecta" +#: Provider.php:588 +msgid "Este endereço de email já está em uso. Tente recuperar a sua senha." +msgstr "" +"Esta dirección de correo electrónico ya está en uso. Intente recuperar su " +"contraseña." -#: Provider.php:269 Provider.php:279 -msgid "Email ou token inválidos" -msgstr "Email o token incorrectos" +#: Provider.php:594 +msgid "Por favor, informe um email válido." +msgstr "Por favor, ingrese un correo electrónico válido." -#: Provider.php:290 -msgid "Este token expirou" -msgstr "Este token expiró" +#: Provider.php:641 +msgid "Token não encontrado." +msgstr "Token no encontrado." -#: Provider.php:305 -msgid "Senha alterada com sucesso! Você pode fazer login agora" -msgstr "Contraseña cambiada con éxito! Ahora puede ingresar" +#: Provider.php:660 +msgid "Este token expirou." +msgstr "Este token expiró." -#: Provider.php:318 +#: Provider.php:716 msgid "Email não encontrado" msgstr "Email no encontrado" -#: Provider.php:337 +#: Provider.php:742 #, php-format msgid "Pedido de recuperação de senha para %s" msgstr "Pedido de recuperación de contraseña para %s" -#: Provider.php:338 -#, php-format +#: Provider.php:777 msgid "" -"Alguém solicitou a recuperação da senha utilizada em %s por este email.\n" -"\n" -"Para recuperá-la, acesse o link: %s. /n/n Se você não pediu a recuperação " -"desta senha, apenas ignore esta mensagem." +"Erro ao enviar email de recuperação. Entre em contato com os administradors " +"do site." msgstr "" -"Alguien solicitó la recuperación de contraseña utilizada en %s por este " -"email.\n" -"\n" -"Para recuperarla, haga clic en el link: %s./n/n Si usted no pidió la " -"recuperación de esta contraseña, ignore este mensaje." +"Error al enviar el correo electrónico de recuperación. Entre en contacto con " +"los administradores del sitio." + +#: Provider.php:814 Provider.php:865 +msgid "Insira sua nova senha." +msgstr "Ingrese su nueva contraseña." + +#: Provider.php:860 +msgid "Senha atual inválida." +msgstr "Contraseña actual no válida." + +#: Provider.php:887 +msgid "A senha deve conter uma letra maiúscula" +msgstr "La contraseña debe contener una letra mayúscula" + +#: Provider.php:888 +msgid "A senha deve conter uma letra minúscula" +msgstr "La contraseña debe contener una letra minúscula" -#: Provider.php:355 +#: Provider.php:889 +msgid "A senha deve conter um caractere especial" +msgstr "La contraseña debe contener un carácter especial" + +#: Provider.php:890 +msgid "A senha deve conter um número " +msgstr "La contraseña debe contener un número " + +#: Provider.php:891 +msgid "O tamanho mínimo da senha é de: " +msgstr "La longitud mínima de la contraseña es: " + +#: Provider.php:1028 +msgid "CPF ou senha incorreta, tente novamente!" +msgstr "¡CI o contraseña incorrecta, inténtalo de nuevo!" + +#: Provider.php:1050 msgid "" -"Sucesso: Um e-mail foi enviado com instruções para recuperação da senha." +"Você possui 2 ou mais agente com o mesmo CPF! Por favor entre em contato com " +"o suporte." msgstr "" -"Un mensaje de correo electrónico fue enviado con instrucciones para " -"recuperar la contraseña." +"¡Tienes 2 o más agentes con la misma CI! Por favor contacta al servicio de " +"soporte." -#: Provider.php:358 +#: Provider.php:1055 msgid "" -"Erro ao enviar email de recuperação. Entre em contato com os administradors " -"do site." +"Você possui 2 ou mais agentes inativos com o mesmo CPF! Por favor entre em " +"contato com o suporte." msgstr "" -"Error al enviar el correo electrónico de recuperación. Entre en contacto con " -"los administradores del sitio." +"¡Tienes 2 o más agentes inactivos con la misma CI! Por favor contacta con el " +"servicio de soporte." -#: Provider.php:384 Provider.php:397 -msgid "Usuário ou senha inválidos" -msgstr "Usuario o contraseña incorrectos" +#: Provider.php:1061 +msgid "CPF ou senha incorreta. Utilize o CPF do seu agente principal." +msgstr "CI o contraseña incorrectas. Utilice la CI de su agente principal." -#: Provider.php:598 +#: Provider.php:1081 Provider.php:1115 +msgid "Usuário ou senha inválidos." +msgstr "Usuario o contraseña incorrectos." + +#: Provider.php:1087 +msgid "Verifique seu email para validar a sua conta." +msgstr "Revise su correo electrónico para validar su cuenta." + +#: Provider.php:1096 +msgid "Login bloqueado, tente novamente em " +msgstr "Inicio de sesión bloqueado, inténtalo de nuevo en " + +#: Provider.php:1421 msgid "É preciso estar autenticado para realizar esta ação" msgstr "Es preciso está autenticado para realizar esta acción" -#: views/auth/multiple-local.php:22 views/auth/multiple-local.php:33 -msgid "Entrar" -msgstr "Ingresar" +#: components/change-password/template.php:15 +msgid "Senha:" +msgstr "Contraseña:" + +#: components/change-password/template.php:27 +msgid "Senha atual" +msgstr "Contraseña actual" + +#: components/change-password/template.php:37 +msgid "A senha deve ter:" +msgstr "La contraseña debe tener:" + +#: components/change-password/template.php:38 +msgid "" +" caracteres, um número, um caractere especial (! @ # $ & *), pelo menos uma " +"letra maiúscula e uma minúscula." +msgstr "" +" caracteres, un número, un carácter especial (!@#$&*), al menos una letra " +"mayúscula y una minúscula." + +#: components/change-password/template.php:43 +msgid "Confirme a senha" +msgstr "Confirmar contraseña" + +#: components/change-password/template.php:54 +#: components/change-password/template.php:59 components/login/template.php:93 +msgid "Alterar senha" +msgstr "Cambiar contraseña" -#: views/auth/multiple-local.php:27 views/auth/multiple-local.php:48 -#: views/auth/multiple-local.php:72 views/auth/pass-recover.php:16 +#: components/change-password/template.php:60 +msgid "Cancelar" +msgstr "Cancelar" + +#: components/create-account/script.js:295 +msgid "Nome obrigatório" +msgstr "Nombre obligatorio" + +#: components/create-account/script.js:298 +msgid "Descrição obrigatória" +msgstr "Descripción obligatoria" + +#: components/create-account/script.js:301 +msgid "Área de atuação obrigatória" +msgstr "Área de actuación obligatoria" + +#: components/create-account/template.php:23 +msgid "Novo cadastro" +msgstr "Nuevo registro" + +#: components/create-account/template.php:24 +#, php-format +msgid "Siga os passos para criar o seu cadastro no %s." +msgstr "Siga los pasos para crear su registro en %s." + +#: components/create-account/template.php:38 components/login/template.php:89 msgid "E-mail" msgstr "E-mail" -#: views/auth/multiple-local.php:30 views/auth/multiple-local.php:75 -#: views/auth/pass-recover.php:19 -msgid "Senha" -msgstr "Contraseña" +#: components/create-account/template.php:43 +msgid "CPF" +msgstr "CI" -#: views/auth/multiple-local.php:35 views/auth/multiple-local.php:43 -msgid "Esqueci minha senha" -msgstr "Olvidé mi contraseña" +#: components/create-account/template.php:46 +msgid "Por que pedimos este dado" +msgstr "Por qué pedimos este dato" + +#: components/create-account/template.php:48 +msgid "Texto sobre o motivo da coleta do CPF" +msgstr "Texto sobre el motivo de la recogida del dato de la CI" + +#: components/create-account/template.php:62 +msgid "Confirme sua senha" +msgstr "Confirmar contraseña" -#: views/auth/multiple-local.php:47 -msgid "Para recuperar sua senha, informe o e-mail utilizado no cadastro." +#: components/create-account/template.php:71 +msgid "Continuar" +msgstr "Continuar" + +#: components/create-account/template.php:79 components/login/template.php:55 +msgid "Entrar com Gov.br" msgstr "" -"Para recuperar su contraseña, informe el e-mail utilizado en su registro." -#: views/auth/multiple-local.php:52 views/auth/pass-recover.php:3 -msgid "Recuperar senha" -msgstr "Recuperar contraseña" +#: components/create-account/template.php:83 components/login/template.php:60 +msgid "Entrar com Google" +msgstr "Entrar con Google" -#: views/auth/multiple-local.php:54 -msgid "Cancelar" -msgstr "Cancelar" +#: components/create-account/template.php:94 +msgid "Voltar e excluir minhas informações" +msgstr "Regresar y borrar mi información" + +#: components/create-account/template.php:101 +msgid "Falta pouco para finalizar o seu cadastro!" +msgstr "Queda poco para que finalice su registro!" -#: views/auth/multiple-local.php:63 views/auth/multiple-local.php:81 -msgid "Registrar-se" -msgstr "Registrarse" +#: components/create-account/template.php:102 +msgid "Dê um nome e faça uma breve descrição sua." +msgstr "Ponte un nombre y haz una breve descripción de ti mismo." -#: views/auth/multiple-local.php:69 +#: components/create-account/template.php:106 msgid "Nome" msgstr "Nombre" -#: views/auth/multiple-local.php:78 views/auth/pass-recover.php:22 -msgid "Confirmar senha" -msgstr "Confirmar contraseña" +#: components/create-account/template.php:106 +msgid "As pessoas irão encontrar você por esse nome." +msgstr "Las personas te encontrarán por ese nombre." -#: views/auth/multiple-local.php:88 -msgid "Redes Sociais" -msgstr "Redes Sociales" +#: components/create-account/template.php:107 +msgid "Mini Bio" +msgstr "Mini Bio" -#: views/auth/multiple-local.php:91 -msgid "Utilize sua conta em outros serviços para autenticar-se" -msgstr "Utilice su cuenta en otros servicios para autenticarse" +#: components/create-account/template.php:108 +msgid "Área de atuação" +msgstr "Área de actuación" -#: views/auth/pass-recover.php:25 -msgid "Recuperar" -msgstr "Recuperar" +#: components/create-account/template.php:112 +msgid "Criar cadastro" +msgstr "Crear registro" -#: views/panel/my-account.php:15 views/panel/my-account.php:18 -msgid "Email" -msgstr "E-mail" +#: components/create-account/template.php:123 +msgid "E-mail de confirmação enviado!" +msgstr "¡Correo electrónico de confirmación enviado!" -#: views/panel/my-account.php:22 views/panel/my-account.php:35 -msgid "Guardar alteraçoes" -msgstr "Guardar cambios" +#: components/create-account/template.php:124 +msgid "Seu cadastro foi criado com sucesso!" +msgstr "Su registro fue creado con éxito!" -#: views/panel/my-account.php:24 -msgid "Trocar Senha" -msgstr "Cambiar contraseña" +#: components/create-account/template.php:127 +#, php-format +msgid "Acesse seu e-mail para confirmar a criação de seu cadastro no %s." +msgstr "" +"Acceda a su correo electrónico para confirmar la creación de su registro en " +"%s." -#: views/panel/my-account.php:26 -msgid "Senha atual" -msgstr "Contraseña actual" +#: components/create-account/template.php:129 +msgid "Acessar meu cadastro" +msgstr "Acceder a mi registro" + +#: components/create-account/texts.php:5 +msgid "O nome é obrigatório!" +msgstr "El nombre es obligatorio!" + +#: components/create-account/texts.php:6 +msgid "A descrição é obrigatória!" +msgstr "La descripción es obligatoria!" + +#: components/create-account/texts.php:7 +msgid "A área de atuação é obrigatória!" +msgstr "El área de actuación es obligatoria!" + +#: components/login/template.php:23 +msgid "Boas vindas!" +msgstr "Bienvenid@!" + +#: components/login/template.php:24 +#, php-format +msgid "Entre na sua conta do %s" +msgstr "Entre en su cuenta de %s" + +#: components/login/template.php:31 +msgid "E-mail ou CPF" +msgstr "E-mail o CI" + +#: components/login/template.php:38 +msgid "Esqueci minha senha" +msgstr "Olvidé mi contraseña" + +#: components/login/template.php:46 +msgid "Entrar" +msgstr "Ingresar" + +#: components/login/template.php:49 +msgid "Ou entre com" +msgstr "O entre con" + +#: components/login/template.php:67 +#, php-format +msgid "Ainda não tem cadastro no %s? Realize seu cadastro agora!" +msgstr "Todavía no está registrado en %s? Realiza el registro ahora!" + +#: components/login/template.php:70 +msgid "Fazer cadastro" +msgstr "Hacer registro" + +#: components/login/template.php:82 components/login/template.php:103 +#: views/auth/confirm-email.php:20 +msgid "Alteração de senha" +msgstr "Cambio de contraseña" + +#: components/login/template.php:83 +msgid "Se você esqueceu a senha, não se preocupe, todo mundo passa por isso." +msgstr "" +"Si has olvidado tu contraseña, no te preocupes, todo el mundo pasa por eso." + +#: components/login/template.php:83 +msgid "Digite seu e-mail para criar uma nova." +msgstr "Introduzca su dirección de correo electrónico para crear una nueva." + +#: components/login/template.php:94 components/login/template.php:109 +#: views/auth/register.php:16 +msgid "Voltar" +msgstr "Volver" + +#: components/login/template.php:105 views/auth/confirm-email.php:22 +msgid "Enviamos as instruções de alteração de senha para seu e-mail." +msgstr "" +"Enviamos las instrucciones para el cambio de contraseña a su correo " +"electrónico." + +#: components/login/template.php:108 +msgid "Não recebi o e-mail" +msgstr "No recibí el correo electrónico" + +#: components/login/template.php:119 +msgid "Redefinir senha de acesso" +msgstr "Redefinir contraseña de acceso" + +#: components/login/template.php:131 +msgid "Confirme sua nova senha" +msgstr "Confirme su nueva contraseña" + +#: components/login/template.php:139 +msgid "Redefinir senha" +msgstr "Redefinir contraseña" + +#: components/password-strongness/template.php:12 +msgid "Força da senha" +msgstr "Fuerza de la contraseña" + +#: components/password-strongness/template.php:18 +msgid "A senha deve conter:" +msgstr "La contraseña debe contener:" + +#: components/password-strongness/texts.php:6 +msgid "{num} caracteres" +msgstr "{num} caracteres" + +#: components/password-strongness/texts.php:7 +msgid "pelo menos uma letra maiúscula" +msgstr "por lo menos una letra mayúscula" + +#: components/password-strongness/texts.php:8 +msgid "pelo menos uma letra minúscula" +msgstr "por lo menos una letra minúscula" + +#: components/password-strongness/texts.php:9 +msgid "um caracter especial (! @ # $ % & * < > ?)" +msgstr "un carácter especial (! @ # $ % & * < > ?)" + +#: components/password-strongness/texts.php:10 +msgid "um número" +msgstr "un número" + +#: views/auth/confirm-email.php:25 +msgid "Entrar na minha conta" +msgstr "Entrar a mi cuenta" + +#~ msgid "Guardar alteraçoes" +#~ msgstr "Guardar cambios" + +#~ msgid "Trocar e-mail" +#~ msgstr "Cambiar e-mail" + +#~ msgid "Email" +#~ msgstr "E-mail" + +#~ msgid "Trocar Senha" +#~ msgstr "Cambiar contraseña" + +#~ msgid "Nova senha" +#~ msgstr "Nueva contraseña" + +#~ msgid "Confirmar nova senha" +#~ msgstr "Confirme su nueva contraseña" + +#~ msgid "Vincular conta com" +#~ msgstr "Vincular cuenta con" + +#~ msgid "Email alterado com sucesso" +#~ msgstr "Email cambiado con éxito" + +#~ msgid "Informe um email válido" +#~ msgstr "Ingrese un email válido" + +#~ msgid "Email e senha alterados com sucecsso" +#~ msgstr "Email y contraseña cambiados con éxito" + +#~ msgid "Senha alterada com sucesso! Você pode fazer login agora" +#~ msgstr "Contraseña cambiada con éxito! Ahora puede ingresar" + +#~ msgid "" +#~ "Alguém solicitou a recuperação da senha utilizada em %s por este email.\n" +#~ "\n" +#~ "Para recuperá-la, acesse o link: %s. /n/n Se você não pediu a recuperação " +#~ "desta senha, apenas ignore esta mensagem." +#~ msgstr "" +#~ "Alguien solicitó la recuperación de contraseña utilizada en %s por este " +#~ "email.\n" +#~ "\n" +#~ "Para recuperarla, haga clic en el link: %s./n/n Si usted no pidió la " +#~ "recuperación de esta contraseña, ignore este mensaje." + +#~ msgid "" +#~ "Sucesso: Um e-mail foi enviado com instruções para recuperação da senha." +#~ msgstr "" +#~ "Un mensaje de correo electrónico fue enviado con instrucciones para " +#~ "recuperar la contraseña." + +#~ msgid "Para recuperar sua senha, informe o e-mail utilizado no cadastro." +#~ msgstr "" +#~ "Para recuperar su contraseña, informe el e-mail utilizado en su registro." + +#~ msgid "Registrar-se" +#~ msgstr "Registrarse" + +#~ msgid "Redes Sociais" +#~ msgstr "Redes Sociales" -#: views/panel/my-account.php:29 -msgid "Nova senha" -msgstr "Nueva contraseña" +#~ msgid "Utilize sua conta em outros serviços para autenticar-se" +#~ msgstr "Utilice su cuenta en otros servicios para autenticarse" -#: views/panel/my-account.php:32 -msgid "Confirmar nova senha" -msgstr "Confirmar nueva contraseña" +#~ msgid "Recuperar" +#~ msgstr "Recuperar" #~ msgid "Clique para marcar/desmarcar este" #~ msgstr "Haga clic para marcar/desmarcar este" diff --git a/views/auth/email-account-restored.html b/views/auth/email-account-restored.html new file mode 100644 index 0000000..396216a --- /dev/null +++ b/views/auth/email-account-restored.html @@ -0,0 +1,431 @@ + + + + + + + + + + + + + + + + + + diff --git a/views/auth/govbr-email.php b/views/auth/govbr-email.php new file mode 100644 index 0000000..c63bc8c --- /dev/null +++ b/views/auth/govbr-email.php @@ -0,0 +1,56 @@ +createUrl('auth', 'govbr-email'); +?> +
+

+ +

+ + + () + +

+

+ +

+ + +
    + +
  • + +
+ + +
+ +
+ + +
+ +

+ + + +

+
diff --git a/views/auth/multiple-local.php b/views/auth/multiple-local.php index 8d1115c..61bd680 100644 --- a/views/auth/multiple-local.php +++ b/views/auth/multiple-local.php @@ -9,7 +9,12 @@ if (trim($_GET['t'] ?? '')) { $this->jsObject['recoveryMode']['status'] = true; - $this->jsObject['recoveryMode']['token'] = $_GET['t']; + $this->jsObject['recoveryMode']['token'] = $_GET['t']; +} + +if (!empty($forcePasswordChange)) { + $this->jsObject['forcePasswordChangeMode'] = true; + $this->jsObject['forcePasswordChangeEmail'] = $app->user->email; } $this->import('