<?php
require_once('wp-webauthn-vendor/autoload.php');
use Webauthn\Server;
use Webauthn\PublicKeyCredentialRpEntity;
use Webauthn\PublicKeyCredentialUserEntity;
use Webauthn\PublicKeyCredentialCreationOptions;
use Webauthn\PublicKeyCredentialSourceRepository as PublicKeyCredentialSourceRepositoryInterface;
use Webauthn\PublicKeyCredentialSource;
use Webauthn\AuthenticatorSelectionCriteria;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;

/**
 * Store all publickeys and pubilckey metas
 */
class PublicKeyCredentialSourceRepository implements PublicKeyCredentialSourceRepositoryInterface {
    // Get one credential by credential ID
    public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource {
        $data = $this->read();
        if(isset($data[base64_encode($publicKeyCredentialId)])){
            return PublicKeyCredentialSource::createFromArray($data[base64_encode($publicKeyCredentialId)]);
        }
        return null;
    }

    // Get one credential's meta by credential ID
    public function findOneMetaByCredentialId(string $publicKeyCredentialId): ?array {
        $meta = json_decode(wwa_get_option("user_credentials_meta"), true);
        if(isset($meta[base64_encode($publicKeyCredentialId)])){
            return $meta[base64_encode($publicKeyCredentialId)];
        }
        return null;
    }

    // Get all credentials of one user
    public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
        $sources = [];
        foreach($this->read() as $data){
            $source = PublicKeyCredentialSource::createFromArray($data);
            if($source->getUserHandle() === $publicKeyCredentialUserEntity->getId()){
                $sources[] = $source;
            }
        }
        return $sources;
    }

    public function findCredentialsForUserEntityByType(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity, string $credentialType): array {
        $credentialsForUserEntity = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
        $credentialsByType = [];
        foreach($credentialsForUserEntity as $credential){
            if($this->findOneMetaByCredentialId($credential->getPublicKeyCredentialId())["authenticator_type"] === $credentialType){
                $credentialsByType[] = $credential;
            }
        }
        return $credentialsByType;
    }

    // Save credential into database
    public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, bool $usernameless = false): void {
        $data = $this->read();
        $data_key = base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId());
        $data[$data_key] = $publicKeyCredentialSource;
        $this->write($data, $data_key, $usernameless);
    }

    // Update credential's last used
    public function updateCredentialLastUsed(string $publicKeyCredentialId): void {
        $credential = $this->findOneMetaByCredentialId($publicKeyCredentialId);
        if($credential !== null){
            $credential["last_used"] = date('Y-m-d H:i:s', current_time('timestamp')); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
            $meta = json_decode(wwa_get_option("user_credentials_meta"), true);
            $meta[base64_encode($publicKeyCredentialId)] = $credential;
            wwa_update_option("user_credentials_meta", wp_json_encode($meta));
        }
    }

    // List all authenticators for the front-end
    public function getShowList(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array {
        $data = json_decode(wwa_get_option("user_credentials_meta"), true);
        $arr = array();
        $user_id = $publicKeyCredentialUserEntity->getId();
        foreach($data as $key => $value){
            if($user_id === $value["user"]){
                array_push($arr, array(
                    "key" => rtrim(strtr(base64_encode($key), '+/', '-_'), '='),
                    "name" => base64_decode($value["human_name"]),
                    "type" => $value["authenticator_type"],
                    "added" => $value["added"],
                    "usernameless" => isset($value["usernameless"]) ? $value["usernameless"] : false,
                    "last_used" => isset($value["last_used"]) ? $value["last_used"] : "-"
                ));
            }
        }
        return array_map(function($item){return array("key" => $item["key"], "name" => esc_html($item["name"]), "type" => $item["type"], "added" => $item["added"], "usernameless" => $item["usernameless"], "last_used" => $item["last_used"]);}, $arr);
    }

    // Modify an authenticator
    public function modifyAuthenticator(string $id, string $name, PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity, string $action, string $res_id): string {
        $keys = $this->findAllForUserEntity($publicKeyCredentialUserEntity);
        $user_id = $publicKeyCredentialUserEntity->getId();

        // Check if the user has the authenticator
        foreach($keys as $item){
            if($item->getUserHandle() === $user_id){
                if(base64_encode($item->getPublicKeyCredentialId()) === base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT))){
                    if($action === "rename"){
                        $this->renameCredential(base64_encode($item->getPublicKeyCredentialId()), $name, $res_id);
                    }elseif($action === "remove"){
                        $this->removeCredential(base64_encode($item->getPublicKeyCredentialId()), $res_id);
                    }
                    wwa_add_log($res_id, "ajax_modify_authenticator: Done");
                    return "true";
                }
            }
        }
        wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Authenticator not found, exit");
        return "Not Found.";
    }

    // Rename a credential from database by credential ID
    private function renameCredential(string $id, string $name, string $res_id): void {
        $meta = json_decode(wwa_get_option("user_credentials_meta"), true);
        wwa_add_log($res_id, "ajax_modify_authenticator: Rename \"".base64_decode($meta[$id]["human_name"])."\" -> \"".$name."\"");
        $meta[$id]["human_name"] = base64_encode($name);
        wwa_update_option("user_credentials_meta", wp_json_encode($meta));
    }

    // Remove a credential from database by credential ID
    private function removeCredential(string $id, string $res_id): void {
        $data = $this->read();
        unset($data[$id]);
        $this->write($data, '');
        $meta = json_decode(wwa_get_option("user_credentials_meta"), true);
        wwa_add_log($res_id, "ajax_modify_authenticator: Remove \"".base64_decode($meta[$id]["human_name"])."\"");
        unset($meta[$id]);
        wwa_update_option("user_credentials_meta", wp_json_encode($meta));
    }

    // Read credential database
    private function read(): array {
        if(wwa_get_option("user_credentials") !== NULL){
            try{
                return json_decode(wwa_get_option("user_credentials"), true);
            }catch(\Throwable $exception) {
                return [];
            }
        }
        return [];
    }

    // Save credentials data
    private function write(array $data, string $key, bool $usernameless = false): void {
        if(isset($_POST["name"]) && isset($_POST["type"]) && ($_POST["type"] === "platform" || $_POST["type"] == "cross-platform" || $_POST["type"] === "none") && $key !== ''){
            // Save credentials's meta separately
            $source = $data[$key]->getUserHandle();
            $meta = json_decode(wwa_get_option("user_credentials_meta"), true);
            $meta[$key] = array(
                "human_name" => base64_encode(sanitize_text_field(wp_unslash($_POST["name"]))),
                "added" => date('Y-m-d H:i:s', current_time('timestamp')), // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
                "authenticator_type" => sanitize_text_field(wp_unslash($_POST["type"])),
                "user" => $source,
                "usernameless" => $usernameless,
                "last_used" => "-"
            );
            wwa_update_option("user_credentials_meta", wp_json_encode($meta));
        }
        wwa_update_option("user_credentials", wp_json_encode($data));
    }
}

// Bind an authenticator
function wwa_ajax_create(){
    try{
        $res_id = wwa_generate_random_string(5);
        $client_id = strval(time()).wwa_generate_random_string(24);

        wwa_init_new_options();

        wwa_add_log($res_id, "ajax_create: Start");

        if(!current_user_can("read")){
            wwa_add_log($res_id, "ajax_create: (ERROR)Permission denied, exit");
            wwa_wp_die("Something went wrong.", $client_id);
        }

        if(wwa_get_option("website_name") === "" || wwa_get_option('website_domain') ===""){
            wwa_add_log($res_id, "ajax_create: (ERROR)Plugin not configured, exit");
            wwa_wp_die("Not configured.", $client_id);
        }

        // Check queries
        if(!isset($_GET["name"]) || !isset($_GET["type"]) || !isset($_GET["usernameless"])){
            wwa_add_log($res_id, "ajax_create: (ERROR)Missing parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }else{
            // Sanitize the input
            $wwa_get = array();
            $wwa_get["name"] = sanitize_text_field(wp_unslash($_GET["name"]));
            $wwa_get["type"] = sanitize_text_field(wp_unslash($_GET["type"]));
            $wwa_get["usernameless"] = sanitize_text_field(wp_unslash($_GET["usernameless"]));
            wwa_add_log($res_id, "ajax_create: name => \"".$wwa_get["name"]."\", type => \"".$wwa_get["type"]."\", usernameless => \"".$wwa_get["usernameless"]."\"");
        }

        $user_info = wp_get_current_user();

        if(isset($_GET["user_id"])){
            $user_id = intval(sanitize_text_field(wp_unslash($_GET["user_id"])));
            if($user_id <= 0){
                wwa_add_log($res_id, "ajax_create: (ERROR)Wrong parameters, exit");
                wwa_wp_die("Bad Request.");
            }

            if($user_info->ID !== $user_id){
                if(!current_user_can("edit_user", $user_id)){
                    wwa_add_log($res_id, "ajax_create: (ERROR)No permission, exit");
                    wwa_wp_die("Something went wrong.");
                }
                $user_info = get_user_by('id', $user_id);

                if($user_info === false){
                    wwa_add_log($res_id, "ajax_create: (ERROR)Wrong user ID, exit");
                    wwa_wp_die("Something went wrong.");
                }
            }
        }

        // Empty authenticator name
        if($wwa_get["name"] === ""){
            wwa_add_log($res_id, "ajax_create: (ERROR)Empty name, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }

        // Usernameless authentication not allowed
        if($wwa_get["usernameless"] === "true" && wwa_get_option("usernameless_login") !== "true"){
            wwa_add_log($res_id, "ajax_create: (ERROR)Usernameless authentication not allowed, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }

        // Check authenticator type
        $allow_authenticator_type = wwa_get_option("allow_authenticator_type");
        if($allow_authenticator_type !== false && $allow_authenticator_type !== "none"){
            if($allow_authenticator_type != $wwa_get["type"]){
                wwa_add_log($res_id, "ajax_create: (ERROR)Credential type error, type => \"".$wwa_get["type"]."\", allow_authenticator_type => \"".$allow_authenticator_type."\", exit");
                wwa_wp_die("Bad Request.", $client_id);
            }
        }

        $rpEntity = new PublicKeyCredentialRpEntity(
            wwa_get_option("website_name"),
            wwa_get_option("website_domain")
        );
        $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();

        $server = new Server(
            $rpEntity,
            $publicKeyCredentialSourceRepository,
            null
        );

        wwa_add_log($res_id, "ajax_create: user => \"".$user_info->user_login."\"");

        // Get user ID or create one
        $user_key = "";
        if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
            wwa_add_log($res_id, "ajax_create: User not initialized, initialize");
            $user_array = wwa_get_option("user_id");
            $user_key = hash("sha256", $user_info->user_login."-".$user_info->display_name."-".wwa_generate_random_string(10));
            $user_array[$user_info->user_login] = $user_key;
            wwa_update_option("user_id", $user_array);
        }else{
            $user_key = wwa_get_option("user_id")[$user_info->user_login];
        }

        $user = array(
            "login" => $user_info->user_login,
            "id" => $user_key,
            "display" => $user_info->display_name,
            "icon" => get_avatar_url($user_info->user_email, array("scheme" => "https"))
        );

        $userEntity = new PublicKeyCredentialUserEntity(
            $user["login"],
            $user["id"],
            $user["display"],
            $user["icon"]
        );

        $credentialSourceRepository = new PublicKeyCredentialSourceRepository();

        $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);

        // Convert the Credential Sources into Public Key Credential Descriptors for excluding
        $excludeCredentials = array_map(function (PublicKeyCredentialSource $credential) {
            return $credential->getPublicKeyCredentialDescriptor();
        }, $credentialSources);

        wwa_add_log($res_id, "ajax_create: excludeCredentials => ".wp_json_encode($excludeCredentials));

        // Set authenticator type
        if($wwa_get["type"] === "platform"){
            $authenticator_type = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_PLATFORM;
        }elseif($wwa_get["type"] === "cross-platform"){
            $authenticator_type = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_CROSS_PLATFORM;
        }else{
            $authenticator_type = AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE;
        }

        // Set user verification
        if(wwa_get_option("user_verification") === "true"){
            wwa_add_log($res_id, "ajax_create: user_verification => \"true\"");
            $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED;
        }else{
            wwa_add_log($res_id, "ajax_create: user_verification => \"false\"");
            $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
        }

        $resident_key = false;
        // Set usernameless authentication
        if($wwa_get["usernameless"] === "true"){
            wwa_add_log($res_id, "ajax_create: Usernameless set, user_verification => \"true\"");
            $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED;
            $resident_key = true;
        }

        // Create authenticator selection
        $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
            $authenticator_type,
            $resident_key,
            $user_verification
        );

        // Create a creation challenge
        $publicKeyCredentialCreationOptions = $server->generatePublicKeyCredentialCreationOptions(
            $userEntity,
            PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE,
            $excludeCredentials,
            $authenticatorSelectionCriteria
        );

        // Save for future use
        wwa_set_temp_val("pkcco", base64_encode(serialize($publicKeyCredentialCreationOptions)), $client_id);
        wwa_set_temp_val("bind_config", array("name" => $wwa_get["name"], "type" => $wwa_get["type"], "usernameless" => $resident_key), $client_id);

        header("Content-Type: application/json");
        $publicKeyCredentialCreationOptions = json_decode(wp_json_encode($publicKeyCredentialCreationOptions), true);
        $publicKeyCredentialCreationOptions["clientID"] = $client_id;
        echo wp_json_encode($publicKeyCredentialCreationOptions);
        wwa_add_log($res_id, "ajax_create: Challenge sent");
        exit;
    }catch(\Exception $exception){
        wwa_add_log($res_id, "ajax_create: (ERROR)".$exception->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($exception));
        wwa_add_log($res_id, "ajax_create: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }catch(\Error $error){
        wwa_add_log($res_id, "ajax_create: (ERROR)".$error->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($error));
        wwa_add_log($res_id, "ajax_create: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }
}
add_action("wp_ajax_wwa_create" , "wwa_ajax_create");

// Verify the attestation
function wwa_ajax_create_response(){
    $client_id = false;
    try{
        $res_id = wwa_generate_random_string(5);

        wwa_init_new_options();

        wwa_add_log($res_id, "ajax_create_response: Client response received");

        if(!isset($_POST["clientid"])){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Missing parameters, exit");
            wp_die("Bad Request.");
        }else{
            // Sanitize the input
            $post_client_id = sanitize_text_field(wp_unslash($_POST["clientid"]));
            if(strlen($post_client_id) < 34 || strlen($post_client_id) > 35){
                wwa_add_log($res_id, "ajax_create_response: (ERROR)Wrong client ID, exit");
                wwa_wp_die("Bad Request.", $client_id);
            }
            $client_id = $post_client_id;
        }

        if(!current_user_can("read")){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Permission denied, exit");
            wwa_wp_die("Something went wrong.", $client_id);
        }

        // Check POST
        if(!isset($_POST["data"]) || !isset($_POST["name"]) || !isset($_POST["type"]) || !isset($_POST["usernameless"])){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Missing parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }else{
            // Sanitize the input
            $wwa_post = array();
            $wwa_post["name"] = sanitize_text_field(wp_unslash($_POST["name"]));
            $wwa_post["type"] = sanitize_text_field(wp_unslash($_POST["type"]));
            $wwa_post["usernameless"] = sanitize_text_field(wp_unslash($_POST["usernameless"]));
            wwa_add_log($res_id, "ajax_create_response: name => \"".$wwa_post["name"]."\", type => \"".$wwa_post["type"]."\", usernameless => \"".$wwa_post["usernameless"]."\"");
            wwa_add_log($res_id, "ajax_create_response: data => ".base64_decode(sanitize_text_field(wp_unslash($_POST["data"]))));
        }

        if(isset($_POST["user_id"])){
            $user_id = intval(sanitize_text_field(wp_unslash($_POST["user_id"])));
            if($user_id <= 0){
                wwa_add_log($res_id, "ajax_create_response: (ERROR)Wrong parameters, exit");
                wwa_wp_die("Bad Request.");
            }

            if(wp_get_current_user()->ID !== $user_id){
                if(!current_user_can("edit_user", $user_id)){
                    wwa_add_log($res_id, "ajax_create_response: (ERROR)No permission, exit");
                    wwa_wp_die("Something went wrong.");
                }
            }
        }

        $temp_val = array(
            "pkcco" => wwa_get_temp_val("pkcco", $client_id),
            "bind_config" => wwa_get_temp_val("bind_config", $client_id)
        );

        // May not get the challenge yet
        if($temp_val["pkcco"] === false || $temp_val["bind_config"] === false){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Challenge not found in transient, exit");
            wwa_wp_die("Bad request.", $client_id);
        }

        // Check parameters
        if($temp_val["bind_config"]["type"] !== "platform" && $temp_val["bind_config"]["type"] !== "cross-platform" && $temp_val["bind_config"]["type"] !== "none"){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Wrong type, exit");
            wwa_wp_die("Bad request.", $client_id);
        }

        if($temp_val["bind_config"]["type"] !== $wwa_post["type"] || $temp_val["bind_config"]["name"] !== $wwa_post["name"]){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Wrong parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }

        if(!isset($_POST["data"]) || $_POST["data"] === ""){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Empty data, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }

        // Check global unique credential ID
        $credential_id = base64_decode(json_decode(base64_decode(sanitize_text_field(wp_unslash($_POST["data"]))), true)["rawId"]);
        $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
        if($publicKeyCredentialSourceRepository->findOneMetaByCredentialId($credential_id) !== null){
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Credential ID not unique, ID => \"".base64_encode($credential_id)."\" , exit");
            wwa_wp_die("Something went wrong.", $client_id);
        }else{
            wwa_add_log($res_id, "ajax_create_response: Credential ID unique check passed");
        }

        $psr17Factory = new Psr17Factory();
        $creator = new ServerRequestCreator(
            $psr17Factory,
            $psr17Factory,
            $psr17Factory,
            $psr17Factory
        );

        $serverRequest = $creator->fromGlobals();

        $rpEntity = new PublicKeyCredentialRpEntity(
            wwa_get_option("website_name"),
            wwa_get_option("website_domain")
        );

        $server = new Server(
            $rpEntity,
            $publicKeyCredentialSourceRepository,
            null
        );

        // Allow to bypass scheme verification when under localhost
        $current_domain = wwa_get_option('website_domain');
        if($current_domain === "localhost" || $current_domain === "127.0.0.1"){
            $server->setSecuredRelyingPartyId([$current_domain]);
            wwa_add_log($res_id, "ajax_create_response: Localhost, bypass HTTPS check");
        }

        // Verify
        try {
            $publicKeyCredentialSource = $server->loadAndCheckAttestationResponse(
                base64_decode(sanitize_text_field(wp_unslash($_POST["data"]))),
                unserialize(base64_decode($temp_val["pkcco"])),
                $serverRequest
            );

            wwa_add_log($res_id, "ajax_create_response: Challenge verified");

            $publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource, $temp_val["bind_config"]["usernameless"]);

            if($temp_val["bind_config"]["usernameless"]){
                wwa_add_log($res_id, "ajax_create_response: Authenticator added with usernameless authentication feature");
            }else{
                wwa_add_log($res_id, "ajax_create_response: Authenticator added");
            }

            // Success
            echo "true";
        }catch(\Throwable $exception){
            // Failed to verify
            wwa_add_log($res_id, "ajax_create_response: (ERROR)".$exception->getMessage());
            wwa_add_log($res_id, wwa_generate_call_trace($exception));
            wwa_add_log($res_id, "ajax_create_response: (ERROR)Challenge not verified, exit");
            wwa_wp_die("Something went wrong.", $client_id);
        }

        // Destroy transients
        wwa_destroy_temp_val($client_id);
        exit;
    }catch(\Exception $exception){
        wwa_add_log($res_id, "ajax_create_response: (ERROR)".$exception->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($exception));
        wwa_add_log($res_id, "ajax_create_response: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }catch(\Error $error){
        wwa_add_log($res_id, "ajax_create_response: (ERROR)".$error->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($error));
        wwa_add_log($res_id, "ajax_create_response: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }
}
add_action("wp_ajax_wwa_create_response" , "wwa_ajax_create_response");

// Auth challenge
function wwa_ajax_auth_start(){
    try{
        $res_id = wwa_generate_random_string(5);
        $client_id = strval(time()).wwa_generate_random_string(24);

        wwa_init_new_options();

        wwa_add_log($res_id, "ajax_auth: Start");

        // Check queries
        if(!isset($_GET["type"])){
            wwa_add_log($res_id, "ajax_auth: (ERROR)Missing parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }else{
            // Sanitize the input
            $wwa_get = array();
            $wwa_get["type"] = sanitize_text_field(wp_unslash($_GET["type"]));
            if(isset($_GET["user"])){
                $wwa_get["user"] = sanitize_text_field(wp_unslash($_GET["user"]));
            }
            if(isset($_GET["usernameless"])){
                $wwa_get["usernameless"] = sanitize_text_field(wp_unslash($_GET["usernameless"]));
                // Usernameless authentication not allowed
                if($wwa_get["usernameless"] === "true" && wwa_get_option("usernameless_login") !== "true"){
                    wwa_add_log($res_id, "ajax_auth: (ERROR)Usernameless authentication not allowed, exit");
                    wwa_wp_die("Bad Request.", $client_id);
                }
            }
        }

        if($wwa_get["type"] === "test" && !current_user_can('read')){
            // Test but not logged in
            wwa_add_log($res_id, "ajax_auth: (ERROR)Permission denied, exit");
            wwa_wp_die("Bad request.", $client_id);
        }

        $user_key = "";
        $usernameless_flag = false;
        $user_icon = null;
        $user_exist = true;
        if($wwa_get["type"] === "test"){
            if(isset($wwa_get["usernameless"])){
                if($wwa_get["usernameless"] !== "true"){
                    // Logged in and testing, if the user haven't bound any authenticator yet, exit
                    $user_info = wp_get_current_user();

                    if(isset($_GET["user_id"])){
                        $user_id = intval(sanitize_text_field(wp_unslash($_GET["user_id"])));
                        if($user_id <= 0){
                            wwa_add_log($res_id, "ajax_auth: (ERROR)Wrong parameters, exit");
                            wwa_wp_die("Bad Request.");
                        }

                        if($user_info->ID !== $user_id){
                            if(!current_user_can("edit_user", $user_id)){
                                wwa_add_log($res_id, "ajax_auth: (ERROR)No permission, exit");
                                wwa_wp_die("Something went wrong.");
                            }
                            $user_info = get_user_by('id', $user_id);

                            if($user_info === false){
                                wwa_add_log($res_id, "ajax_auth: (ERROR)Wrong user ID, exit");
                                wwa_wp_die("Something went wrong.");
                            }
                        }
                    }

                    wwa_add_log($res_id, "ajax_auth: type => \"test\", user => \"".$user_info->user_login."\", usernameless => \"false\"");

                    if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
                        wwa_add_log($res_id, "ajax_auth: (ERROR)User not initialized, exit");
                        wwa_wp_die("User not inited.", $client_id);
                    }else{
                        $user_key = wwa_get_option("user_id")[$user_info->user_login];
                        $user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
                    }
                }else{
                    if(wwa_get_option("usernameless_login") === "true"){
                        wwa_add_log($res_id, "ajax_auth: type => \"test\", usernameless => \"true\"");
                        $usernameless_flag = true;
                    }else{
                        wwa_add_log($res_id, "ajax_auth: (ERROR)Wrong parameters, exit");
                        wwa_wp_die("Bad Request.", $client_id);
                    }
                }
            }else{
                wwa_add_log($res_id, "ajax_auth: (ERROR)Missing parameters, exit");
                wwa_wp_die("Bad Request.", $client_id);
            }
        }else{
            // Not testing, create a fake user ID if the user does not exist or haven't bound any authenticator yet
            if(isset($wwa_get["user"]) && $wwa_get["user"] !== ""){
                $wp_user = wwa_get_user($wwa_get["user"]);
                if(wwa_get_option("email_login") === "true" && is_email($wwa_get["user"])){
                    wwa_add_log($res_id, "ajax_auth: email_login => \"true\", trying to find user by email address \"".$wwa_get["user"]."\"");
                }
                if($wp_user !== false){
                    $user_info = $wp_user;
                    $user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
                    wwa_add_log($res_id, "ajax_auth: type => \"auth\", user => \"".$user_info->user_login."\"");
                    if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
                        wwa_add_log($res_id, "ajax_auth: User found but not initialized, create a fake id");
                        $user_key = hash("sha256", $wwa_get["user"]."-".$wwa_get["user"]."-".wwa_generate_random_string(10));
                        $user_exist = false;
                    }else{
                        $user_key = wwa_get_option("user_id")[$user_info->user_login];
                    }
                }else{
                    $user_info = new stdClass();
                    $user_info->user_login = $wwa_get["user"];
                    $user_info->display_name = $wwa_get["user"];
                    $user_key = hash("sha256", $wwa_get["user"]."-".$wwa_get["user"]."-".wwa_generate_random_string(10));
                    wwa_add_log($res_id, "ajax_auth: User not exists, create a fake id");
                    wwa_add_log($res_id, "ajax_auth: type => \"auth\", user => \"".$wwa_get["user"]."\"");
                    $user_exist = false;
                }
            }else{
                if(wwa_get_option("usernameless_login") === "true"){
                    $usernameless_flag = true;
                    wwa_add_log($res_id, "ajax_auth: Empty username, try usernameless authentication");
                }else{
                    wwa_add_log($res_id, "ajax_auth: (ERROR)Missing parameters, exit");
                    wwa_wp_die("Bad Request.", $client_id);
                }
            }
        }

        if(!$usernameless_flag){
            $userEntity = new PublicKeyCredentialUserEntity(
                $user_info->user_login,
                $user_key,
                $user_info->display_name,
                $user_icon
            );
        }

        $credentialSourceRepository = new PublicKeyCredentialSourceRepository();
        $rpEntity = new PublicKeyCredentialRpEntity(
            wwa_get_option("website_name"),
            wwa_get_option("website_domain")
        );

        $server = new Server(
            $rpEntity,
            $credentialSourceRepository,
            null
        );

        if($usernameless_flag){
            // Usernameless authentication, return empty allowed credentials list
            wwa_add_log($res_id, "ajax_auth: Usernameless authentication, allowedCredentials => []");
            $allowedCredentials = array();
        }else{
            // Get the list of authenticators associated to the user
            // $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
            $allow_authenticator_type = wwa_get_option("allow_authenticator_type");
            if($allow_authenticator_type === false || $allow_authenticator_type === "none"){
                $credentialSources = $credentialSourceRepository->findAllForUserEntity($userEntity);
            }elseif($allow_authenticator_type !== false && $allow_authenticator_type !== "none"){
                wwa_add_log($res_id, "ajax_auth: allow_authenticator_type => \"".$allow_authenticator_type."\", filter authenticators");
                $credentialSources = $credentialSourceRepository->findCredentialsForUserEntityByType($userEntity, $allow_authenticator_type);
            }

            // Logged in and testing, if the user haven't bind a authenticator yet, exit
            if(count($credentialSources) === 0 && $wwa_get["type"] === "test" && current_user_can("read")){
                wwa_add_log($res_id, "ajax_auth: (ERROR)No authenticator, exit");
                wwa_wp_die("User not inited.", $client_id);
            }

            // Convert the Credential Sources into Public Key Credential Descriptors for excluding
            $allowedCredentials = array_map(function(PublicKeyCredentialSource $credential){
                return $credential->getPublicKeyCredentialDescriptor();
            }, $credentialSources);

            wwa_add_log($res_id, "ajax_auth: allowedCredentials => ".wp_json_encode($allowedCredentials));
        }

        // Set user verification
        if(wwa_get_option("user_verification") === "true"){
            wwa_add_log($res_id, "ajax_auth: user_verification => \"true\"");
            $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED;
        }else{
            wwa_add_log($res_id, "ajax_auth: user_verification => \"false\"");
            $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED;
        }

        if($usernameless_flag){
            wwa_add_log($res_id, "ajax_auth: Usernameless authentication, user_verification => \"true\"");
            $user_verification = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED;
        }

        // Create a auth challenge
        $publicKeyCredentialRequestOptions = $server->generatePublicKeyCredentialRequestOptions(
            $user_verification,
            $allowedCredentials
        );

        // Save for future use
        wwa_set_temp_val("pkcco_auth", base64_encode(serialize($publicKeyCredentialRequestOptions)), $client_id);
        wwa_set_temp_val("auth_type", $wwa_get["type"], $client_id);
        wwa_set_temp_val("user_exist", $user_exist, $client_id);
        if(!$usernameless_flag){
            wwa_set_temp_val("user_name_auth", $user_info->user_login, $client_id);
        }
        wwa_set_temp_val("usernameless_auth", serialize($usernameless_flag), $client_id);

        // Save the user entity if is not logged in and not usernameless
        if(!($wwa_get["type"] === "test" && current_user_can("read")) && !$usernameless_flag){
            wwa_set_temp_val("user_auth", serialize($userEntity), $client_id);
        }

        header("Content-Type: application/json");
        $publicKeyCredentialRequestOptions = json_decode(wp_json_encode($publicKeyCredentialRequestOptions), true);
        $publicKeyCredentialRequestOptions["clientID"] = $client_id;
        echo wp_json_encode($publicKeyCredentialRequestOptions);
        wwa_add_log($res_id, "ajax_auth: Challenge sent");
        exit;
    }catch(\Exception $exception){
        wwa_add_log($res_id, "ajax_auth: (ERROR)".$exception->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($exception));
        wwa_add_log($res_id, "ajax_auth: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }catch(\Error $error){
        wwa_add_log($res_id, "ajax_auth: (ERROR)".$error->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($error));
        wwa_add_log($res_id, "ajax_auth: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }
}
add_action("wp_ajax_wwa_auth_start" , "wwa_ajax_auth_start");
add_action("wp_ajax_nopriv_wwa_auth_start" , "wwa_ajax_auth_start");

function wwa_ajax_auth(){
    $client_id = false;
    try{
        $res_id = wwa_generate_random_string(5);

        wwa_init_new_options();

        wwa_add_log($res_id, "ajax_auth_response: Client response received");

        if(!isset($_POST["clientid"])){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Missing parameters, exit");
            wp_die("Bad Request.");
        }else{
            // Sanitize the input
            $post_client_id = sanitize_text_field(wp_unslash($_POST["clientid"]));
            if(strlen($post_client_id) < 34 || strlen($post_client_id) > 35){
                wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong client ID, exit");
                wwa_wp_die("Bad Request.", $client_id);
            }
            $client_id = $post_client_id;
        }

        // Check POST
        if(!isset($_POST["type"]) || !isset($_POST["data"]) || !isset($_POST["remember"])){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Missing parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }else{
            // Sanitize the input
            $wwa_post = array();
            $wwa_post["type"] = sanitize_text_field(wp_unslash($_POST["type"]));
            $wwa_post["remember"] = sanitize_text_field(wp_unslash($_POST["remember"]));
        }

        $temp_val = array(
            "pkcco_auth" => wwa_get_temp_val("pkcco_auth", $client_id),
            "auth_type" => wwa_get_temp_val("auth_type", $client_id),
            "usernameless_auth" => wwa_get_temp_val("usernameless_auth", $client_id),
            "user_auth" => wwa_get_temp_val("user_auth", $client_id),
            "user_name_auth" => wwa_get_temp_val("user_name_auth", $client_id),
            "user_exist" => wwa_get_temp_val("user_exist", $client_id),
        );

        if($temp_val["auth_type"] === false || $wwa_post["type"] !== $temp_val["auth_type"]){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }

        // Check remember me
        if($wwa_post["remember"] !== "true" && $wwa_post["remember"] !== "false"){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }elseif(wwa_get_option("remember_me") !== "true" && $wwa_post["remember"] === "true"){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong parameters, exit");
            wwa_wp_die("Bad Request.", $client_id);
        }

        // May not get the challenge yet
        if($temp_val["pkcco_auth"] === false || $temp_val["usernameless_auth"] === false || ($wwa_post["type"] !== "test" && $wwa_post["type"] !== "auth")){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Challenge not found in transient, exit");
            wwa_wp_die("Bad request.", $client_id);
        }

        $temp_val["usernameless_auth"] = unserialize($temp_val["usernameless_auth"]);

        if($temp_val["usernameless_auth"] === false && $temp_val["user_name_auth"] === false){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Username not found in transient, exit");
            wwa_wp_die("Bad request.", $client_id);
        }
        if($wwa_post["type"] === "test" && !current_user_can("read")){
            // Test but not logged in
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Permission denied, exit");
            wwa_wp_die("Bad request.", $client_id);
        }
        if(!($wwa_post["type"] === "test" && current_user_can("read")) && ($temp_val["usernameless_auth"] === false && $temp_val["user_auth"] === false)){
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Permission denied, exit");
            wwa_wp_die("Bad request.", $client_id);
        }

        $usernameless_flag = $temp_val["usernameless_auth"];

        $psr17Factory = new Psr17Factory();
        $creator = new ServerRequestCreator(
            $psr17Factory,
            $psr17Factory,
            $psr17Factory,
            $psr17Factory
        );

        $serverRequest = $creator->fromGlobals();
        $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();

        // If user entity is not saved, read from WordPress
        $user_key = "";
        if($wwa_post["type"] === "test" && current_user_can('read') && !$usernameless_flag){
            $user_info = wp_get_current_user();

            if(isset($_POST["user_id"])){
                $user_id = intval(sanitize_text_field(wp_unslash($_POST["user_id"])));
                if($user_id <= 0){
                    wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong parameters, exit");
                    wwa_wp_die("Bad Request.");
                }

                if($user_info->ID !== $user_id){
                    if(!current_user_can("edit_user", $user_id)){
                        wwa_add_log($res_id, "ajax_auth_response: (ERROR)No permission, exit");
                        wwa_wp_die("Something went wrong.");
                    }
                    $user_info = get_user_by('id', $user_id);

                    if($user_info === false){
                        wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong user ID, exit");
                        wwa_wp_die("Something went wrong.");
                    }
                }
            }

            if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
                wwa_add_log($res_id, "ajax_auth_response: (ERROR)User not initialized, exit");
                wwa_wp_die("User not inited.", $client_id);
            }else{
                $user_key = wwa_get_option("user_id")[$user_info->user_login];
                $user_icon = get_avatar_url($user_info->user_email, array("scheme" => "https"));
            }

            $userEntity = new PublicKeyCredentialUserEntity(
                $user_info->user_login,
                $user_key,
                $user_info->display_name,
                $user_icon
            );

            wwa_add_log($res_id, "ajax_auth_response: type => \"test\", user => \"".$user_info->user_login."\"");
        }else{
            if($usernameless_flag){
                $data_array = json_decode(base64_decode(sanitize_text_field(wp_unslash($_POST["data"]))), true);
                if(!isset($data_array["response"]["userHandle"]) || !isset($data_array["rawId"])){
                    wwa_add_log($res_id, "ajax_auth_response: (ERROR)Client data not correct, exit");
                    wwa_wp_die("Bad request.", $client_id);
                }

                wwa_add_log($res_id, "ajax_auth_response: type => \"".$wwa_post["type"]."\"");
                wwa_add_log($res_id, "ajax_auth_response: Usernameless authentication, try to find user by credential_id => \"".$data_array["rawId"]."\", userHandle => \"".$data_array["response"]["userHandle"]."\"");

                $credential_meta = $publicKeyCredentialSourceRepository->findOneMetaByCredentialId(base64_decode($data_array["rawId"]));

                if($credential_meta !== null){
                    $allow_authenticator_type = wwa_get_option("allow_authenticator_type");
                    if($allow_authenticator_type !== false && $allow_authenticator_type !== 'none'){
                        if($credential_meta["authenticator_type"] !== $allow_authenticator_type){
                            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential type error, authenticator_type => \"".$credential_meta["authenticator_type"]."\", allow_authenticator_type => \"".$allow_authenticator_type."\", exit");
                            wwa_wp_die("Bad request.", $client_id);
                        }
                    }
                    if($credential_meta["usernameless"] === true){
                        wwa_add_log($res_id, "ajax_auth_response: Credential found, usernameless => \"true\", user_key => \"".$credential_meta["user"]."\"");

                        // Try to find user
                        $all_user = wwa_get_option("user_id");
                        $user_login_name = false;
                        foreach($all_user as $user => $user_id){
                            if($user_id === $credential_meta["user"]){
                                $user_login_name = $user;
                                break;
                            }
                        }

                        // Match userHandle
                        if($credential_meta["user"] === base64_decode($data_array["response"]["userHandle"])){
                            // Found user
                            if($user_login_name !== false){
                                wwa_add_log($res_id, "ajax_auth_response: Found user => \"".$user_login_name."\", user_key => \"".$credential_meta["user"]."\"");

                                // Testing, verify user
                                if($wwa_post["type"] === "test" && current_user_can('read')){
                                    $user_wp = wp_get_current_user();
                                    if($user_login_name !== $user_wp->user_login){
                                        wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but user not match, exit");
                                        wwa_wp_die("Bad request.", $client_id);
                                    }
                                }

                                $user_info = get_user_by('login', $user_login_name);

                                if($user_info === false){
                                    wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong user ID, exit");
                                    wwa_wp_die("Something went wrong.");
                                }

                                $userEntity = new PublicKeyCredentialUserEntity(
                                    $user_info->user_login,
                                    $credential_meta["user"],
                                    $user_info->display_name,
                                    get_avatar_url($user_info->user_email, array("scheme" => "https"))
                                );
                            }else{
                                wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but user not found, exit");
                                wwa_wp_die("Bad request.", $client_id);
                            }
                        }else{
                            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but userHandle not matched, exit");
                            wwa_wp_die("Bad request.", $client_id);
                        }
                    }else{
                        wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential found, but usernameless => \"false\", exit");
                        wwa_wp_die("Bad request.", $client_id);
                    }
                }else{
                    wwa_add_log($res_id, "ajax_auth_response: (ERROR)Credential not found, exit");
                    wwa_wp_die("Bad request.", $client_id);
                }
            }else{
                wwa_add_log($res_id, "ajax_auth_response: type => \"auth\", user => \"".$temp_val["user_name_auth"]."\"");
                $userEntity = unserialize($temp_val["user_auth"]);
            }
        }

        $decoded_data = base64_decode(sanitize_text_field(wp_unslash($_POST["data"])));
        wwa_add_log($res_id, "ajax_auth_response: data => ".$decoded_data);

        if($temp_val["user_exist"]){
            $rpEntity = new PublicKeyCredentialRpEntity(
                wwa_get_option("website_name"),
                wwa_get_option("website_domain")
            );

            $server = new Server(
                $rpEntity,
                $publicKeyCredentialSourceRepository,
                null
            );

            // Allow to bypass scheme verification when under localhost
            $current_domain = wwa_get_option("website_domain");
            if($current_domain === "localhost" || $current_domain === "127.0.0.1"){
                $server->setSecuredRelyingPartyId([$current_domain]);
                wwa_add_log($res_id, "ajax_auth_response: Localhost, bypass HTTPS check");
            }

            // Verify
            try {
                $server->loadAndCheckAssertionResponse(
                    $decoded_data,
                    unserialize(base64_decode($temp_val["pkcco_auth"])),
                    $userEntity,
                    $serverRequest
                );

                wwa_add_log($res_id, "ajax_auth_response: Challenge verified");

                // Success
                $publicKeyCredentialSourceRepository->updateCredentialLastUsed(base64_decode(json_decode($decoded_data, true)["rawId"]));
                if(!($wwa_post["type"] === "test" && current_user_can("read"))){
                    // Log user in
                    if (!is_user_logged_in()) {
                        include("wwa-compatibility.php");

                        if(!$usernameless_flag){
                            $user_login = $temp_val["user_name_auth"];
                        }else{
                            $user_login = $user_login_name;
                        }

                        $user = get_user_by("login", $user_login);

                        if($user === false){
                            wwa_add_log($res_id, "ajax_auth_response: (ERROR)Wrong user ID, exit");
                            wwa_wp_die("Something went wrong.");
                        }

                        $user_id = $user->ID;

                        wwa_add_log($res_id, "ajax_auth_response: Log in user => \"".$user_login."\"");

                        $remember_flag = false;

                        if ($wwa_post["remember"] === "true" && (wwa_get_option("remember_me") === false ? "false" : wwa_get_option("remember_me")) !== "false") {
                            $remember_flag = true;
                            wwa_add_log($res_id, "ajax_auth_response: Remember login for 14 days");
                        }

                        wp_set_current_user($user_id, $user_login);
                        if(isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] === "on"){
                            wp_set_auth_cookie($user_id, $remember_flag, true);
                        }else{
                            wp_set_auth_cookie($user_id, $remember_flag);
                        }
                        do_action("wp_login", $user_login, $user);
                    }
                }
                echo "true";
            }catch(\Throwable $exception){
                // Failed to verify
                wwa_add_log($res_id, "ajax_auth_response: (ERROR)".$exception->getMessage());
                wwa_add_log($res_id, wwa_generate_call_trace($exception));
                wwa_add_log($res_id, "ajax_auth_response: (ERROR)Challenge not verified, exit");
                wwa_wp_die("Something went wrong.", $client_id);
            }
        }else{
            wwa_add_log($res_id, "ajax_auth_response: (ERROR)User not exists or has no authenticator, exit without verification");
            wwa_wp_die("Something went wrong.", $client_id);
        }

        // Destroy session
        wwa_destroy_temp_val($client_id);
        exit;
    }catch(\Exception $exception){
        wwa_add_log($res_id, "ajax_auth_response: (ERROR)".$exception->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($exception));
        wwa_add_log($res_id, "ajax_auth_response: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }catch(\Error $error){
        wwa_add_log($res_id, "ajax_auth_response: (ERROR)".$error->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($error));
        wwa_add_log($res_id, "ajax_auth_response: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.", $client_id);
    }
}
add_action("wp_ajax_wwa_auth" , "wwa_ajax_auth");
add_action("wp_ajax_nopriv_wwa_auth" , "wwa_ajax_auth");

// Get authenticator list
function wwa_ajax_authenticator_list(){
    $res_id = wwa_generate_random_string(5);

    wwa_init_new_options();

    if(!current_user_can("read")){
        wwa_add_log($res_id, "ajax_authenticator_list: (ERROR)Missing parameters, exit");
        wwa_wp_die("Something went wrong.");
    }

    $user_info = wp_get_current_user();

    if(isset($_GET["user_id"])){
        $user_id = intval(sanitize_text_field(wp_unslash($_GET["user_id"])));
        if($user_id <= 0){
            wwa_add_log($res_id, "ajax_authenticator_list: (ERROR)Wrong parameters, exit");
            wwa_wp_die("Bad Request.");
        }

        if($user_info->ID !== $user_id){
            if(!current_user_can("edit_user", $user_id)){
                wwa_add_log($res_id, "ajax_authenticator_list: (ERROR)No permission, exit");
                wwa_wp_die("Something went wrong.");
            }
            $user_info = get_user_by('id', $user_id);

            if($user_info === false){
                wwa_add_log($res_id, "ajax_authenticator_list: (ERROR)Wrong user ID, exit");
                wwa_wp_die("Something went wrong.");
            }
        }
    }

    header('Content-Type: application/json');

    $user_key = "";
    if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
        // The user haven't bound any authenticator, return empty list
        echo "[]";
        exit;
    }else{
        $user_key = wwa_get_option("user_id")[$user_info->user_login];
    }

    $userEntity = new PublicKeyCredentialUserEntity(
        $user_info->user_login,
        $user_key,
        $user_info->display_name,
        get_avatar_url($user_info->user_email, array("scheme" => "https"))
    );

    $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();
    echo wp_json_encode($publicKeyCredentialSourceRepository->getShowList($userEntity));
    exit;
}
add_action("wp_ajax_wwa_authenticator_list" , "wwa_ajax_authenticator_list");

// Modify an authenticator
function wwa_ajax_modify_authenticator(){
    try{
        $res_id = wwa_generate_random_string(5);

        wwa_init_new_options();

        wwa_add_log($res_id, "ajax_modify_authenticator: Start");

        if(!current_user_can("read")){
            wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Permission denied, exit");
            wwa_wp_die("Bad Request.");
        }

        if(!isset($_GET["id"]) || !isset($_GET["target"])){
            wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Missing parameters, exit");
            wwa_wp_die("Bad Request.");
        }

        $user_info = wp_get_current_user();

        if(isset($_GET["user_id"])){
            $user_id = intval(sanitize_text_field(wp_unslash($_GET["user_id"])));
            if($user_id <= 0){
                wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Wrong parameters, exit");
                wwa_wp_die("Bad Request.");
            }

            if($user_info->ID !== $user_id){
                if(!current_user_can("edit_user", $user_id)){
                    wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)No permission, exit");
                    wwa_wp_die("Something went wrong.");
                }
                $user_info = get_user_by('id', $user_id);

                if($user_info === false){
                    wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Wrong user ID, exit");
                    wwa_wp_die("Something went wrong.");
                }
            }
        }

        if($_GET["target"] !== "rename" && $_GET["target"] !== "remove"){
            wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Wrong target, exit");
            wwa_wp_die("Bad Request.");
        }

        if($_GET["target"] === "rename" && !isset($_GET["name"])){
            wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Missing parameters, exit");
            wwa_wp_die("Bad Request.");
        }

        $user_key = "";
        if(!isset(wwa_get_option("user_id")[$user_info->user_login])){
            // The user haven't bound any authenticator, exit
            wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)User not initialized, exit");
            wwa_wp_die("User not inited.");
        }else{
            $user_key = wwa_get_option("user_id")[$user_info->user_login];
        }

        $userEntity = new PublicKeyCredentialUserEntity(
            $user_info->user_login,
            $user_key,
            $user_info->display_name,
            get_avatar_url($user_info->user_email, array("scheme" => "https"))
        );

        wwa_add_log($res_id, "ajax_modify_authenticator: user => \"".$user_info->user_login."\"");

        $publicKeyCredentialSourceRepository = new PublicKeyCredentialSourceRepository();

        if($_GET["target"] === "rename"){
            echo $publicKeyCredentialSourceRepository->modifyAuthenticator(sanitize_text_field(wp_unslash($_GET["id"])), sanitize_text_field(wp_unslash($_GET["name"])), $userEntity, "rename", $res_id);
        }elseif($_GET["target"] === "remove"){
            echo $publicKeyCredentialSourceRepository->modifyAuthenticator(sanitize_text_field(wp_unslash($_GET["id"])), "", $userEntity, "remove", $res_id);
        }
        exit;
    }catch(\Exception $exception){
        wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)".$exception->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($exception));
        wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.");
    }catch(\Error $error){
        wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)".$error->getMessage());
        wwa_add_log($res_id, wwa_generate_call_trace($error));
        wwa_add_log($res_id, "ajax_modify_authenticator: (ERROR)Unknown error, exit");
        wwa_wp_die("Something went wrong.");
    }
}
add_action("wp_ajax_wwa_modify_authenticator" , "wwa_ajax_modify_authenticator");

// Print log
function wwa_ajax_get_log(){
    if(!wwa_validate_privileges()){
        wwa_wp_die("Bad Request.");
    }

    header('Content-Type: application/json');

    $log = get_option("wwa_log");

    if($log === false){
        echo "[]";
    }else{
        echo wp_json_encode($log);
    }

    exit;
}
add_action("wp_ajax_wwa_get_log" , "wwa_ajax_get_log");

// Clear log
function wwa_ajax_clear_log(){
    if(!wwa_validate_privileges()){
        wwa_wp_die("Bad Request.");
    }

    $log = get_option("wwa_log");

    if($log !== false){
        update_option("wwa_log", array());
    }

    echo "true";
    exit;
}
add_action("wp_ajax_wwa_clear_log" , "wwa_ajax_clear_log");
